001package hirondelle.starfield.util;
002
003import java.util.*;
004import java.net.URL;
005import javax.swing.*;
006import javax.swing.border.Border;
007import java.awt.*;
008
009/** 
010 Static convenience methods for GUIs which eliminate code duplication.
011 
012 <P>Your application will likely need to add to such a class. For example, 
013 using <tt>GrdiBagLayout</tt> usually benefits from utility methods to 
014 reduce code repetition.
015*/
016public final class UiUtil {
017
018  /**
019   <tt>pack</tt>, center, and <tt>show</tt> a window on the screen.
020  
021   <P>If the size of <tt>aWindow</tt> exceeds that of the screen, 
022   then the size of <tt>aWindow</tt> is reset to the size of the screen.
023  */
024  public static void centerAndShow(Window aWindow){
025    //note that the order here is important
026    
027    aWindow.pack();
028    /*
029     * If called from outside the event dispatch thread (as is 
030     * the case upon startup, in the launch thread), then 
031     * in principle this code is not thread-safe: once pack has 
032     * been called, the component is realized, and (most) further
033     * work on the component should take place in the event-dispatch 
034     * thread. 
035     *
036     * In practice, it is exceedingly unlikely that this will lead 
037     * to an error, since invisible components cannot receive events.
038     */
039    Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
040    Dimension window = aWindow.getSize();
041    //ensure that no parts of aWindow will be off-screen
042    if (window.height > screen.height) {
043      window.height = screen.height;
044    }
045    if (window.width > screen.width) {
046      window.width = screen.width;
047    }
048    int xCoord = (screen.width/2 - window.width/2);
049    int yCoord = (screen.height/2 - window.height/2);
050    aWindow.setLocation( xCoord, yCoord );
051   
052    aWindow.setVisible(true);
053  }
054  
055  /**
056   A window is packed, centered with respect to a parent, and then shown.
057  
058   <P>This method is intended for dialogs only.
059  
060   <P>If centering with respect to a parent causes any part of the dialog 
061   to be off screen, then the centering is overidden, such that all of the 
062   dialog will always appear fully on screen, but it will still appear 
063   near the parent.
064  
065   @param aWindow must have non-null result for <tt>aWindow.getParent</tt>.
066  */
067  public static void centerOnParentAndShow(Window aWindow){
068    aWindow.pack();
069    
070    Dimension parent = aWindow.getParent().getSize();
071    Dimension window = aWindow.getSize();
072    int xCoord = 
073      aWindow.getParent().getLocationOnScreen().x + 
074     (parent.width/2 - window.width/2)
075    ;
076    int yCoord = 
077      aWindow.getParent().getLocationOnScreen().y + 
078      (parent.height/2 - window.height/2)
079    ;
080    
081    //Ensure that no part of aWindow will be off-screen
082    Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
083    int xOffScreenExcess = xCoord + window.width - screen.width;
084    if ( xOffScreenExcess > 0 ) {
085      xCoord = xCoord - xOffScreenExcess;
086    }
087    if (xCoord < 0 ) {
088      xCoord = 0;
089    }
090    int yOffScreenExcess = yCoord + window.height - screen.height;
091    if ( yOffScreenExcess > 0 ) {
092      yCoord = yCoord - yOffScreenExcess;
093    }
094    if (yCoord < 0) {
095      yCoord = 0;
096    }
097    
098    aWindow.setLocation( xCoord, yCoord );
099    aWindow.setVisible(true);
100  }
101
102  /**
103   Return a border of dimensions recommended by the Java Look and Feel 
104   Design Guidelines, suitable for many common cases.
105  
106  <P>Each side of the border has size {@link UiConsts#STANDARD_BORDER}.
107  */
108  public static Border getStandardBorder(){
109    return BorderFactory.createEmptyBorder(
110      UiConsts.STANDARD_BORDER, 
111      UiConsts.STANDARD_BORDER, 
112      UiConsts.STANDARD_BORDER, 
113      UiConsts.STANDARD_BORDER
114    );
115  }
116
117  /**
118   Return text which conforms to the Look and Feel Design Guidelines 
119   for the title of a dialog : the application name, a colon, then 
120   the name of the specific dialog.
121  
122  <P>Example return value: <tt>My Movies: Login</tt>
123  
124   @param aSpecificDialogName must have visible content
125  */
126  public static String getDialogTitle(String aSpecificDialogName){
127    Args.checkForContent(aSpecificDialogName);
128    StringBuilder result = new StringBuilder(Consts.APP_NAME);
129    result.append(": ");
130    result.append(aSpecificDialogName);
131    return result.toString(); 
132  }
133  
134  /**
135   Make a horizontal row of buttons of equal size, whch are equally spaced, 
136   and aligned on the right.
137  
138   <P>The returned component has border spacing only on the top (of the size 
139   recommended by the Look and Feel Design Guidelines).
140   All other spacing must be applied elsewhere ; usually, this will only mean 
141   that the dialog's top-level panel should use {@link #getStandardBorder}.
142   
143   @param aButtons contains the buttons to be placed in a row.
144  */
145  public static JComponent getCommandRow(java.util.List<JComponent> aButtons){
146    equalizeSizes( aButtons );
147    JPanel panel = new JPanel();
148    LayoutManager layout = new BoxLayout(panel, BoxLayout.X_AXIS);
149    panel.setLayout(layout);
150    panel.setBorder(BorderFactory.createEmptyBorder(UiConsts.THREE_SPACES, 0, 0, 0));
151    panel.add(Box.createHorizontalGlue());
152    Iterator<JComponent> buttonsIter = aButtons.iterator();
153    while (buttonsIter.hasNext()) {
154      panel.add( buttonsIter.next() );
155      if (buttonsIter.hasNext()) {
156        panel.add(Box.createHorizontalStrut(UiConsts.ONE_SPACE));
157      }
158    }
159    return panel;
160  }
161  
162  /**
163   Make a vertical row of buttons of equal size, whch are equally spaced, 
164   and aligned on the right.
165  
166   <P>The returned component has border spacing only on the left (of the size 
167   recommended by the Look and Feel Design Guidelines).
168   All other spacing must be applied elsewhere ; usually, this will only mean 
169   that the dialog's top-level panel should use {@link #getStandardBorder}.
170   
171   @param aButtons contains the buttons to be placed in a column
172  */
173  public static JComponent getCommandColumn( java.util.List<JComponent> aButtons ){
174    equalizeSizes( aButtons );
175    JPanel panel = new JPanel();
176    LayoutManager layout = new BoxLayout(panel, BoxLayout.Y_AXIS);
177    panel.setLayout( layout );
178    panel.setBorder(
179      BorderFactory.createEmptyBorder(0,UiConsts.THREE_SPACES, 0,0)
180    );
181    //(no for-each is used here, because of the 'not-yet-last' check)
182    Iterator<JComponent> buttonsIter = aButtons.iterator();
183    while ( buttonsIter.hasNext() ) {
184      panel.add(buttonsIter.next());
185      if ( buttonsIter.hasNext() ) {
186        panel.add( Box.createVerticalStrut(UiConsts.ONE_SPACE) );
187      }
188    }
189    panel.add( Box.createVerticalGlue() );
190    return panel;
191  }
192
193  /** Return the currently active frame. */
194  public static Frame getActiveFrame() {
195    Frame result = null;
196    Frame[] frames = Frame.getFrames();
197    for (int i = 0; i < frames.length; i++) {
198      Frame frame = frames[i];
199      if (frame.isVisible()) { //Component method
200        result = frame;
201        break;
202      }
203    }
204    return result;
205  }
206  
207  /**
208   Return a <tt>Dimension</tt> whose size is defined not in terms of pixels, 
209   but in terms of a given percent of the screen's width and height. 
210  
211  <P> Use to set the preferred size of a component to a certain 
212   percentage of the screen.  
213  
214   @param aPercentWidth percentage width of the screen, in range <tt>1..100</tt>.
215   @param aPercentHeight percentage height of the screen, in range <tt>1..100</tt>.
216  */
217  public static final Dimension getDimensionFromPercent(int aPercentWidth, int aPercentHeight){
218    int low = 1;
219    int high = 100;
220    Args.checkForRange(aPercentWidth, low, high);
221    Args.checkForRange(aPercentHeight, low, high);
222    Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
223    return calcDimensionFromPercent(screenSize, aPercentWidth, aPercentHeight);
224  }
225
226  /**
227    Sets the items in <tt>aComponents</tt> to the same size.
228   
229    <P>Sets each component's preferred and maximum sizes. 
230    The actual size is determined by the layout manager, whcih adjusts 
231    for locale-specific strings and customized fonts. (See this 
232    <a href="http://java.sun.com/products/jlf/ed2/samcode/prefere.html">Sun doc</a> 
233    for more information.)
234   
235    @param aComponents items whose sizes are to be equalized
236   */
237  public static void equalizeSizes(java.util.List<JComponent> aComponents) {
238    Dimension targetSize = new Dimension(0,0);
239    for(JComponent comp: aComponents ) {
240      Dimension compSize = comp.getPreferredSize();
241      double width = Math.max(targetSize.getWidth(), compSize.getWidth());
242      double height = Math.max(targetSize.getHeight(), compSize.getHeight());
243      targetSize.setSize(width, height);
244    }
245    setSizes(aComponents, targetSize);
246  }
247
248  /**
249   Make the system emit a beep.
250  
251   <P>May not beep unless the speakers are turned on, so this cannot 
252   be guaranteed to work.
253  */
254  public static void beep(){
255    Toolkit.getDefaultToolkit().beep();
256  }
257  
258  /**
259   Imposes a uniform horizontal alignment on all items in a container.
260  
261  <P> Intended especially for <tt>BoxLayout</tt>, where all components need 
262   to share the same alignment in order for display to be reasonable. 
263   (Indeed, this method may only work for <tt>BoxLayout</tt>, since apparently 
264   it is the only layout to use <tt>setAlignmentX, setAlignmentY</tt>.)
265  
266   @param aContainer contains only <tt>JComponent</tt> objects.
267  */
268  public static void alignAllX(Container aContainer, UiUtil.AlignX aAlignment){
269    java.util.List<Component> components = Arrays.asList( aContainer.getComponents() );
270    for(Component comp: components){
271      JComponent jcomp = (JComponent)comp;
272      jcomp.setAlignmentX( aAlignment.getValue() );
273    }
274  }
275  
276  /** Enumeration for horizontal alignment. */
277  public enum AlignX {
278    LEFT(Component.LEFT_ALIGNMENT),
279    CENTER(Component.CENTER_ALIGNMENT),
280    RIGHT(Component.RIGHT_ALIGNMENT);
281    public float getValue(){
282      return fValue;
283    }
284    private final float fValue;
285    private AlignX(float aValue){
286      fValue = aValue;
287    }
288  }
289  
290  /**
291   Imposes a uniform vertical alignment on all items in a container.
292  
293  <P> Intended especially for <tt>BoxLayout</tt>, where all components need 
294   to share the same alignment in order for display to be reasonable.
295   (Indeed, this method may only work for <tt>BoxLayout</tt>, since apparently 
296   it is the only layout to use <tt>setAlignmentX, setAlignmentY</tt>.)
297  
298   @param aContainer contains only <tt>JComponent</tt> objects.
299  */
300  public static void alignAllY(Container aContainer, UiUtil.AlignY aAlignment){
301    java.util.List components = Arrays.asList( aContainer.getComponents() );
302    Iterator compsIter = components.iterator();
303    while ( compsIter.hasNext() ) {
304      JComponent comp = (JComponent)compsIter.next();
305      comp.setAlignmentY( aAlignment.getValue() );
306    }
307  }
308
309  /** Type-safe enumeration vertical alignment. */
310  public enum AlignY {
311    TOP(Component.TOP_ALIGNMENT),
312    CENTER(Component.CENTER_ALIGNMENT),
313    BOTTOM(Component.BOTTOM_ALIGNMENT);
314    float getValue(){
315      return fValue;
316    }
317    private final float fValue;
318    private AlignY( float aValue){
319      fValue = aValue;
320    }
321  }
322  
323  /**
324   Ensure that <tt>aRootPane</tt> has no default button associated with it.
325  
326   <P>Intended mainly for dialogs where the user is confirming a delete action.
327   In this case, an explicit Yes or No is preferred, with no default action being 
328   taken when the user hits the Enter key. 
329  */
330  public static void noDefaultButton(JRootPane aRootPane){
331    aRootPane.setDefaultButton(null);
332  }
333  
334  /**
335  Create an icon for use by a given class.
336  
337  Returns <tt>null</tt> if the icon cannot be found.
338  
339  @param aPath path to the file, relative to the calling class, as in '../images/blah.png'
340  @param aDescription description of the image
341  @param aClass class that needs to use the image
342  */
343  public static ImageIcon createImageIcon(String aPath, String aDescription, Class aClass) {
344    ImageIcon result = null;
345    URL imageURL = aClass.getResource(aPath); //resolves to an absolute path
346    if (imageURL != null) {
347      result = new ImageIcon(imageURL, aDescription);
348    } 
349    return result;
350  }
351
352  // PRIVATE
353  
354  private static void setSizes(java.util.List aComponents, Dimension aDimension){
355    Iterator compsIter = aComponents.iterator();      
356    while ( compsIter.hasNext() ) {
357      JComponent comp = (JComponent) compsIter.next();
358      comp.setPreferredSize( (Dimension)aDimension.clone() );
359      comp.setMaximumSize( (Dimension)aDimension.clone() );
360    }
361  }
362
363  private static Dimension calcDimensionFromPercent(Dimension aSourceDimension, int aPercentWidth, int aPercentHeight){
364    int width = aSourceDimension.width * aPercentWidth/100;
365    int height = aSourceDimension.height * aPercentHeight/100;
366    return new Dimension(width, height);
367  }
368}