001    package hirondelle.movies.util.ui;
002    
003    import hirondelle.movies.LaunchApplication;
004    import hirondelle.movies.util.Args;
005    import java.util.*;
006    import java.net.URL;
007    import javax.swing.*;
008    import javax.swing.border.Border;
009    import 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    */
018    public 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    }