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