001package hirondelle.stocks.util.ui;
002
003import hirondelle.stocks.util.Args;
004import hirondelle.stocks.util.Consts;
005import hirondelle.stocks.util.Util;
006
007import java.util.*;
008import java.text.*;
009import java.net.URL;
010import javax.swing.*;
011import javax.swing.border.Border;
012import java.awt.*;
013import javax.swing.plaf.metal.MetalLookAndFeel;
014import hirondelle.stocks.preferences.GeneralLookPreferencesEditor;
015
016/** Static convenience methods for GUIs which eliminate code duplication.*/
017public final class UiUtil {
018
019  /**
020  * <tt>pack</tt>, center, and <tt>show</tt> a window on the screen.
021  *
022  * <P>If the size of <tt>aWindow</tt> exceeds that of the screen, 
023  * then the size of <tt>aWindow</tt> is reset to the size of the screen.
024  */
025  public static void centerAndShow(Window aWindow){
026    //note that the order here is important
027    
028    aWindow.pack();
029    /*
030     * If called from outside the event dispatch thread (as is 
031     * the case upon startup, in the launch thread), then 
032     * in principle this code is not thread-safe: once pack has 
033     * been called, the component is realized, and (most) further
034     * work on the component should take place in the event-dispatch 
035     * thread. 
036     *
037     * In practice, it is exceedingly unlikely that this will lead 
038     * to an error, since invisible components cannot receive events.
039     */
040    Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
041    Dimension window = aWindow.getSize();
042    //ensure that no parts of aWindow will be off-screen
043    if (window.height > screen.height) {
044      window.height = screen.height;
045    }
046    if (window.width > screen.width) {
047      window.width = screen.width;
048    }
049    int xCoord = (screen.width/2 - window.width/2);
050    int yCoord = (screen.height/2 - window.height/2);
051    aWindow.setLocation( xCoord, yCoord );
052   
053    aWindow.setVisible(true);
054  }
055  
056  /**
057  * A window is packed, centered with respect to a parent, and then shown.
058  *
059  * <P>This method is intended for dialogs only.
060  *
061  * <P>If centering with respect to a parent causes any part of the dialog 
062  * to be off screen, then the centering is overidden, such that all of the 
063  * dialog will always appear fully on screen, but it will still appear 
064  * near the parent.
065  *
066  * @param aWindow must have non-null result for <tt>aWindow.getParent</tt>.
067  */
068  public static void centerOnParentAndShow(Window aWindow){
069    aWindow.pack();
070    
071    Dimension parent = aWindow.getParent().getSize();
072    Dimension window = aWindow.getSize();
073    int xCoord = 
074      aWindow.getParent().getLocationOnScreen().x + 
075     (parent.width/2 - window.width/2)
076    ;
077    int yCoord = 
078      aWindow.getParent().getLocationOnScreen().y + 
079      (parent.height/2 - window.height/2)
080    ;
081    
082    //Ensure that no part of aWindow will be off-screen
083    Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
084    int xOffScreenExcess = xCoord + window.width - screen.width;
085    if ( xOffScreenExcess > 0 ) {
086      xCoord = xCoord - xOffScreenExcess;
087    }
088    if (xCoord < 0 ) {
089      xCoord = 0;
090    }
091    int yOffScreenExcess = yCoord + window.height - screen.height;
092    if ( yOffScreenExcess > 0 ) {
093      yCoord = yCoord - yOffScreenExcess;
094    }
095    if (yCoord < 0) {
096      yCoord = 0;
097    }
098    
099    aWindow.setLocation( xCoord, yCoord );
100    aWindow.setVisible(true);
101  }
102
103  /**
104  * Return a border of dimensions recommended by the Java Look and Feel 
105  * Design Guidelines, suitable for many common cases.
106  *
107  *<P>Each side of the border has size {@link UiConsts#STANDARD_BORDER}.
108  */
109  public static Border getStandardBorder(){
110    return BorderFactory.createEmptyBorder(
111      UiConsts.STANDARD_BORDER, 
112      UiConsts.STANDARD_BORDER, 
113      UiConsts.STANDARD_BORDER, 
114      UiConsts.STANDARD_BORDER
115    );
116  }
117
118  /**
119  * Return text which conforms to the Look and Feel Design Guidelines 
120  * for the title of a dialog : the application name, a colon, then 
121  * the name of the specific dialog.
122  *
123  *<P>Example return value: <tt>StocksMonitor: Preferences</tt>
124  *
125  * @param aSpecificDialogName must have visible content
126  */
127  public static String getDialogTitle(String aSpecificDialogName){
128    Args.checkForContent(aSpecificDialogName);
129    StringBuilder result = new StringBuilder(Consts.APP_NAME);
130    result.append(": ");
131    result.append(aSpecificDialogName);
132    return result.toString(); 
133  }
134  
135  /**
136  * Make a horizontal row of buttons of equal size, whch are equally spaced, 
137  * and aligned on the right.
138  *
139  * <P>The returned component has border spacing only on the top (of the size 
140  * recommended by the Look and Feel Design Guidelines).
141  * All other spacing must be applied elsewhere ; usually, this will only mean 
142  * that the dialog's top-level panel should use {@link #getStandardBorder}.
143  * 
144  * @param aButtons contains the buttons to be placed in a row.
145  */
146  public static JComponent getCommandRow(java.util.List<JComponent> aButtons){
147    equalizeSizes( aButtons );
148    JPanel panel = new JPanel();
149    LayoutManager layout = new BoxLayout(panel, BoxLayout.X_AXIS);
150    panel.setLayout(layout);
151    panel.setBorder(BorderFactory.createEmptyBorder(UiConsts.THREE_SPACES, 0, 0, 0));
152    panel.add(Box.createHorizontalGlue());
153    Iterator<JComponent> buttonsIter = aButtons.iterator();
154    while (buttonsIter.hasNext()) {
155      panel.add(buttonsIter.next());
156      if (buttonsIter.hasNext()) {
157        panel.add(Box.createHorizontalStrut(UiConsts.ONE_SPACE));
158      }
159    }
160    return panel;
161  }
162  
163  /**
164  * Make a vertical row of buttons of equal size, whch are equally spaced, 
165  * and aligned on the right.
166  *
167  * <P>The returned component has border spacing only on the left (of the size 
168  * recommended by the Look and Feel Design Guidelines).
169  * All other spacing must be applied elsewhere ; usually, this will only mean 
170  * that the dialog's top-level panel should use {@link #getStandardBorder}.
171  * 
172  * @param aButtons contains the buttons to be placed in a column
173  */
174  public static JComponent getCommandColumn(java.util.List<JComponent> aButtons){
175    equalizeSizes(aButtons);
176    JPanel panel = new JPanel();
177    LayoutManager layout = new BoxLayout(panel, BoxLayout.Y_AXIS);
178    panel.setLayout(layout);
179    panel.setBorder(
180      BorderFactory.createEmptyBorder(0,UiConsts.THREE_SPACES, 0,0)
181    );
182    //(no for-each is used here, because of the 'not-yet-last' check)
183    Iterator<JComponent> buttonsIter = aButtons.iterator();
184    while (buttonsIter.hasNext()) {
185      panel.add(buttonsIter.next());
186      if (buttonsIter.hasNext()) {
187        panel.add( Box.createVerticalStrut(UiConsts.ONE_SPACE) );
188      }
189    }
190    panel.add(Box.createVerticalGlue());
191    return panel;
192  }
193
194  /**
195  * Return an <tt>ImageIcon</tt> using its <tt>String</tt> identifier.
196  *
197  * @param aImageId starts with '/', and refers to an image resource 
198  * which is accessible through {@link Class#getResource}.
199  */
200  public static ImageIcon getImageIcon(String aImageId){
201    if( ! aImageId.startsWith(BACK_SLASH) ){
202      throw new IllegalArgumentException(
203        "Image identifier does not start with backslash: " + aImageId
204      );
205    }
206    return fetchImageIcon(aImageId, UiUtil.class);
207  }
208
209  /**
210  * Return an <tt>ImageIcon</tt> using its <tt>String</tt> identifier, relative to 
211  * a given class.
212  *
213  * @param aImageId does NOT start with '/', and must refer to an image resource which is 
214  * accessible through {@link Class#getResource}.
215  * @param aClass the class relative to which the image is located.
216  */
217  public static ImageIcon getImageIcon(String aImageId, Class<?> aClass){
218    if( aImageId.startsWith(BACK_SLASH) ){
219      throw new IllegalArgumentException(
220        "Image identifier starts with a backslash: " + aImageId
221      );
222    }
223    return fetchImageIcon(aImageId, aClass);
224  }
225  
226  /**
227  * Return a square icon which paints nothing, and whose dimensions correspond 
228  * to the user preference for icon size.
229  *
230  * <P>A common problem occurs with text alignment in menus, where there is 
231  * a mixture of menu items with and without an icon. Adding an empty icon 
232  * to menu items which do not have one will adjust its alignment to match 
233  * that of the others which do have an icon.
234  */
235  public static Icon getEmptyIcon(){
236    GeneralLookPreferencesEditor prefs = new GeneralLookPreferencesEditor();
237    return prefs.hasLargeIcons() ? EmptyIcon.SIZE_24 : EmptyIcon.SIZE_16;
238  }
239  
240  /**
241  * Return a <tt>Dimension</tt> whose size is defined not in terms of pixels, 
242  * but in terms of a given percent of the screen's width and height. 
243  *
244  *<P> Use to set the preferred size of a component to a certain 
245  * percentage of the screen.  
246  *
247  * @param aPercentWidth percentage width of the screen, in range <tt>1..100</tt>.
248  * @param aPercentHeight percentage height of the screen, in range <tt>1..100</tt>.
249  */
250  public static final Dimension getDimensionFromPercent(
251    int aPercentWidth, int aPercentHeight
252   ){
253    int low = 1;
254    int high = 100;
255    Args.checkForRange(aPercentWidth, low, high);
256    Args.checkForRange(aPercentHeight, low, high);
257    Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
258    return calcDimensionFromPercent(screenSize, aPercentWidth, aPercentHeight);
259  }
260
261  /**
262   * Sets the items in <tt>aComponents</tt> to the same size.
263   *
264   * <P>Sets each component's preferred and maximum sizes. 
265   * The actual size is determined by the layout manager, whcih adjusts 
266   * for locale-specific strings and customized fonts. (See this 
267   * <a href="http://java.sun.com/products/jlf/ed2/samcode/prefere.html">Sun doc</a> 
268   * for more information.)
269   *
270   * @param aComponents items whose sizes are to be equalized
271   */
272  public static void equalizeSizes(java.util.List<JComponent> aComponents) {
273    Dimension targetSize = new Dimension(0,0);
274    for(JComponent comp: aComponents ) {
275      Dimension compSize = comp.getPreferredSize();
276      double width = Math.max(targetSize.getWidth(), compSize.getWidth());
277      double height = Math.max(targetSize.getHeight(), compSize.getHeight());
278      targetSize.setSize(width, height);
279    }
280    setSizes(aComponents, targetSize);
281  }
282  
283  /**
284  * Create a pair of components, a <tt>JLabel</tt> and an associated 
285  * <tt>JTextField</tt>, as is typically used for user input.
286  *
287  *<P>The <tt>JLabel</tt> appears on the left, and the <tt>JTextField</tt>
288  * appears on the same  row, just to the right of the <tt>JLabel</tt>. 
289  * The <tt>JLabel</tt> has a mnemonic which forwards focus to the 
290  * <tt>JTextField</tt> when activated.
291  * 
292  * @param aContainer holds the pair of components.
293  * @param aName text of the <tt>JLabel</tt> component.
294  * @param aInitialValue possibly-null initial value to appear 
295  * in the <tt>JTextField</tt>; if <tt>null</tt>, then 
296  * <tt>JTextField</tt> will be blank.
297  * @param aMnemonic <tt>KeyEvent</tt> field, used as the mnemonic for 
298  * the <tt>JLabel</tt>.
299  * @param aConstraints applied to the <tt>JLabel</tt>; the corresponding 
300  * constraints for the <tt>JTextField</tt> are the same as 
301  * <tt>aConstraints</tt>, except for <tt>gridx</tt> being incremented by one; 
302  * in addition, if <tt>aConstraints</tt> has <tt>weightx=0</tt> (the default),
303  * then the entry field will receive <tt>weightx=1.0</tt> (entry field gets more 
304  * horizontal space upon resize).
305  * @param aTooltip possibly-null text displayed as tool tip for the 
306  * <tt>JTextField</tt> ; if <tt>null</tt>, the tool tip is turned off.
307  * @return the user input <tt>JTextField</tt>.
308  */
309  public static JTextField addSimpleEntryField(
310    Container aContainer, String aName, String aInitialValue, 
311    int aMnemonic, GridBagConstraints aConstraints, String aTooltip
312  ){
313    Args.checkForNull(aName);
314    
315    JLabel label = new JLabel(aName);
316    label.setDisplayedMnemonic(aMnemonic);
317    aContainer.add( label, aConstraints );
318
319    JTextField result = new JTextField(UiConsts.SIMPLE_FIELD_WIDTH);
320    label.setLabelFor(result);
321    result.setToolTipText(aTooltip);
322    if (aInitialValue != null) {
323      result.setText(aInitialValue);
324    }
325    aConstraints.gridx = ++aConstraints.gridx;
326    if (aConstraints.weightx == 0.0){
327      aConstraints.weightx = 1.0;
328    }
329    aContainer.add(result, aConstraints);
330    return result;
331  }
332
333  /**
334  * Return a set of constraints with convenient default values.
335  *
336  *<P>Return constraints with these values :
337  *<ul>
338  * <li> <tt>gridx, gridy</tt> - set to <tt>aX, aY</tt>
339  * <li> <tt>anchor - GridBagConstraints.WEST</tt>
340  * <li> <tt>insets - Insets(0,0,0, UiConsts.ONE_SPACE)</tt>
341  *</ul>
342  * 
343  *<P> All other items simply take their default values :
344  *<ul>
345  * <li> <tt>fill - GridBagConstraints.NONE</tt>
346  * <li> <tt>gridwidth, gridheight - 0, 0</tt> 
347  * <li> <tt>weightx , weighty - 0, 0</tt>
348  * <li> <tt>ipadx, ipady - 0, 0</tt>
349  *</ul>
350  *
351  * <P>The caller is free to change the returned constraints, to customize for 
352  * their particular needs.
353  *
354  * @param aY in range <tt>0..10</tt>.
355  * @param aX in range <tt>0..10</tt>.
356  */
357  public static GridBagConstraints getConstraints(int aY, int aX){
358    int low = 0;
359    int high = 10;
360    Args.checkForRange(aY, low, high);
361    Args.checkForRange(aX, low, high);
362    GridBagConstraints result = new GridBagConstraints();
363    result.gridy = aY;
364    result.gridx = aX;
365    result.anchor = GridBagConstraints.WEST;
366    result.insets = new Insets(0,0,0,UiConsts.ONE_SPACE);
367    return result;
368  }
369
370  /**
371  * Return {@link #getConstraints(int, int)}, with the addition of setting 
372  * <tt>gridwidth</tt> to <tt>aWidth</tt>, and setting 
373  * <tt>gridheight</tt> to <tt>aHeight</tt>.
374  *
375  * <P>The caller is free to change the returned constraints, to customize for
376  * their particular needs.
377  *
378  * @param aY in range <tt>0..10</tt>.
379  * @param aX in range <tt>0..10</tt>.
380  * @param aWidth in range <tt>1..10</tt>.
381  * @param aHeight in range <tt>1..10</tt>.
382  */
383  public static GridBagConstraints getConstraints(int aY, int aX, int aWidth, int aHeight){
384    int low = 0;
385    int high = 10;
386    Args.checkForRange(aHeight, low, high);
387    Args.checkForRange(aWidth, low, high);
388    GridBagConstraints result = getConstraints(aY, aX);
389    result.gridheight = aHeight;
390    result.gridwidth = aWidth;
391    return result;
392  }
393  
394  /**
395  * Create a pair of <tt>JLabel</tt> components, as is typically needed 
396  * for display of a name-value pair.
397  *
398  * <P>The name appears on the left, and the value appears on the right, 
399  * all on the same row. A colon and an empty space are appended to the name. 
400  * 
401  * <P> If the the length of "value" label is greater than 
402  * {@link UiConsts#MAX_LABEL_LENGTH}, then the text is truncated, an ellipsis
403  * is placed at its end, and the full text is placed in a tooltip.
404  * 
405  * @param aContainer holds the pair of components.
406  * @param aName text of the name <tt>JLabel</tt>.
407  * @param aValue possibly-null ; if null, then an empty <tt>String</tt>
408  * is used for the value; otherwise <tt>Object.toString</tt> is used.
409  * @param aConstraints for the name <tt>JLabel</tt>; the corresponding 
410  * constraints for the value <tt>JLabel</tt> are mostly taken from 
411  * <tt>aConstraints</tt>, except for <tt>gridx</tt> being incremented by one
412  * (<tt>weightx</tt> may differ as well - see <tt>aWeightOnDisplay</tt>.)
413  * @param aWeightOnDisplay if true, then set <tt>weightx</tt> for the value 
414  * field to 1.0 (to give it more horizontal space upon resize).
415  * @return the <tt>JLabel</tt> for the value (which is usually variable).
416  */
417  public static JLabel addSimpleDisplayField(
418    Container aContainer, String aName,  Object aValue, 
419    GridBagConstraints aConstraints, boolean aWeightOnDisplay
420  ){
421    StringBuilder formattedName = new StringBuilder(aName);
422    formattedName.append(": ");
423    JLabel name = new JLabel( formattedName.toString() );
424    aContainer.add( name, aConstraints );
425    
426    String valueText = (aValue != null? aValue.toString() : Consts.EMPTY_STRING);
427    JLabel value = new JLabel(valueText);
428    truncateLabelIfLong(value);
429    aConstraints.gridx = ++aConstraints.gridx;
430    if (aWeightOnDisplay){
431      aConstraints.weightx = 1.0;
432    }
433    aContainer.add( value, aConstraints );
434    
435    return value;
436  }
437  
438  /**
439  * Present a number of read-only items to the user as a vertical listing 
440  * of <tt>JLabel</tt> name-value pairs.
441  *
442  * <P>Each pair is added in the style of 
443  * {@link #addSimpleDisplayField} (its <tt>aConstraints</tt> param are those 
444  * returned by {@link #getConstraints(int, int)}, and its <tt>aWeightOnDisplay</tt> 
445  * param is set to <tt>true</tt>).
446  *
447  * <P>The order of presentation is determined by the iteration order of 
448  * <tt>aNameValuePairs</tt>.
449  *
450  *<P>The number of items which should be presented using this method is limited, since
451  * no scrolling mechanism is given to the user.
452  *
453  * @param aContainer holds the display fields.
454  * @param aNameValuePairs has <tt>String</tt> keys for the names, 
455  * and values are possibly null <tt>Object</tt>s; 
456  * if null, then an empty <tt>String</tt> is displayed, otherwise
457  * <tt>Object.toString</tt> is called on the value and displayed.
458  */
459  public static void addSimpleDisplayFields(
460    Container aContainer, Map<String, String> aNameValuePairs
461  ) {
462    Set<String> keys = aNameValuePairs.keySet();
463    int rowIdx = 0;
464    for(String name: keys) {
465      String value = aNameValuePairs.get(name);
466      if(value == null){
467        value = Consts.EMPTY_STRING;
468      }
469      UiUtil.addSimpleDisplayField(
470        aContainer, 
471        name, 
472        value, 
473        UiUtil.getConstraints(rowIdx,0),
474        true
475      );
476      ++rowIdx;
477    }
478  }
479 
480  /**
481  * Adds "glue" (an empty component with desired resizing behavior) to the bottom 
482  * row of a <tt>GridBagLayout</tt> of components. When resized, this glue will 
483  * take up extra vertical space.
484  *
485  * <P>This method is especially useful for text data presented in a listing or 
486  * tabular form. Such components naturally resize horizontally, while their vertical 
487  * resizing should often be absent. If such a listing is resized vertically, then this 
488  * glue can take up the remaining vertical space, keeping the text at the top.
489  *
490  * @param aPanel uses <tt>GridBagLayout</tt>, and contains components whose 
491  * <tt>weighty</tt> values are all 0.0 (the default).
492  * @param aLastRowIdx index of the last row of components, in which the glue will be
493  * placed.
494  */
495  public static void addVerticalGridGlue(JPanel aPanel, int aLastRowIdx) {   
496    GridBagConstraints glueConstraints = UiUtil.getConstraints(aLastRowIdx,0);
497    glueConstraints.weighty = 1.0;
498    glueConstraints.fill = GridBagConstraints.VERTICAL;
499    aPanel.add(new JLabel(), glueConstraints);
500  }
501  
502  /**
503  * Return a <tt>String</tt>, suitable for presentation to the end user, 
504  * representing a percentage having two decimal places, using the default locale.
505  *
506  * <P>An example return value is "5.15%". The intent of this method is to 
507  * provide a standard representation and number of decimals for the entire 
508  * application. If a different number of decimal places is required, then 
509  * the caller should use <tt>NumberFormat</tt> instead.
510  */
511  public static String getLocalizedPercent( Number aNumber ){
512    NumberFormat localFormatter = NumberFormat.getPercentInstance();
513    localFormatter.setMinimumFractionDigits(2);
514    return localFormatter.format(aNumber.doubleValue());
515  }
516  
517  /**
518  * Return a <tt>String</tt>, suitable for presentation to the end user, 
519  * representing an integral number with no decimal places, using the default 
520  * locale.
521  *
522  * <P>An example return value is "8,000". The intent of this method is to 
523  * provide a standard representation of integers for the entire 
524  * application.
525  */
526  public static String getLocalizedInteger( Number aNumber ) {
527    NumberFormat localFormatter = NumberFormat.getNumberInstance();
528    return localFormatter.format(aNumber.intValue());
529  }
530
531  /**
532  * Return a <tt>String</tt>, suitable for presentation to the end user, 
533  * representing a date in <tt>DateFormat.SHORT</tt> and the default locale.
534  */
535  public static String getLocalizedTime(Date aDate){
536    DateFormat dateFormat = DateFormat.getTimeInstance(DateFormat.SHORT);
537    return dateFormat.format(aDate);
538  }
539
540  /**
541  * Make the sytem emit a beep.
542  *
543  * <P>May not beep unless the speakers are turned on, so this cannot 
544  * be guaranteed to work.
545  */
546  public static void beep(){
547    Toolkit.getDefaultToolkit().beep();
548  }
549
550  /**
551  * An alternative to multi-line labels, for the presentation of 
552  * several lines of text, and for which the line breaks are determined 
553  * solely by the widget.
554  *
555  * @param aText must have visible content, doesn't contain newline characters or html.
556  * @return <tt>JTextArea</tt> which is not editable, has improved spacing over the 
557  * supplied default (placing {@link UiConsts#ONE_SPACE} on the left and right), 
558  * which wraps lines on word boundaries, and whose background color is the 
559  * same as {@link javax.swing.plaf.metal.MetalLookAndFeel#getMenuBackground}.
560  */
561  public static JTextArea getStandardTextArea(String aText){
562    Args.checkForContent(aText);
563    if ( aText.indexOf(Consts.NEW_LINE) != -1 ){
564      throw new IllegalArgumentException("Must not contain new line characters: " + aText);
565    }
566    JTextArea result = new JTextArea(aText);
567    result.setEditable(false);
568    result.setWrapStyleWord(true);
569    result.setLineWrap(true);
570    result.setMargin( new Insets(0,UiConsts.ONE_SPACE,0,UiConsts.ONE_SPACE) );
571    //this is a bit hacky: the desired color is "secondary3", but cannot see how 
572    //to reference it directly; hence, an element which uses secondary3 is used instead.
573    result.setBackground( MetalLookAndFeel.getMenuBackground() ); 
574    
575    return result;
576  }
577  
578  /**
579  * An alternative to multi-line labels, for the presentation of 
580  * several lines of text, and for which line breaks are determined 
581  * solely by <tt>aText</tt>, and not by the widget.
582  *
583  * @param aText has visible content
584  * @return <tt>JTextArea</tt> which is not editable, has improved spacing over the 
585  * supplied default (placing {@link UiConsts#ONE_SPACE} on the left and right), 
586  * and whose background color is the same as 
587  * {@link javax.swing.plaf.metal.MetalLookAndFeel#getMenuBackground}.
588  */
589  public static JTextArea getStandardTextAreaHardNewLines(String aText){
590    Args.checkForContent(aText);
591    JTextArea result = new JTextArea(aText);
592    result.setEditable(false);
593    result.setMargin(new Insets(0,UiConsts.ONE_SPACE,0,UiConsts.ONE_SPACE));
594    result.setBackground( MetalLookAndFeel.getMenuBackground() ); 
595    return result;
596  }
597  
598  /**
599  * Imposes a uniform horizontal alignment on all items in a container.
600  *
601  *<P> Intended especially for <tt>BoxLayout</tt>, where all components need 
602  * to share the same alignment in order for display to be reasonable. 
603  * (Indeed, this method may only work for <tt>BoxLayout</tt>, since apparently 
604  * it is the only layout to use <tt>setAlignmentX, setAlignmentY</tt>.)
605  *
606  * @param aContainer contains only <tt>JComponent</tt> objects.
607  */
608  public static void alignAllX(Container aContainer, UiUtil.AlignX aAlignment){
609    java.util.List<Component> components = Arrays.asList( aContainer.getComponents() );
610    for(Component comp: components){
611      JComponent jcomp = (JComponent)comp;
612      jcomp.setAlignmentX(aAlignment.getValue());
613    }
614  }
615  
616  /** Enumeration for horizontal alignment. */
617  public enum AlignX {
618    LEFT(Component.LEFT_ALIGNMENT),
619    CENTER(Component.CENTER_ALIGNMENT),
620    RIGHT(Component.RIGHT_ALIGNMENT);
621    public float getValue(){
622      return fValue;
623    }
624    private final float fValue;
625    private AlignX(float aValue){
626      fValue = aValue;
627    }
628  }
629  
630  /**
631  * Imposes a uniform vertical alignment on all items in a container.
632  *
633  *<P> Intended especially for <tt>BoxLayout</tt>, where all components need 
634  * to share the same alignment in order for display to be reasonable.
635  * (Indeed, this method may only work for <tt>BoxLayout</tt>, since apparently 
636  * it is the only layout to use <tt>setAlignmentX, setAlignmentY</tt>.)
637  *
638  * @param aContainer contains only <tt>JComponent</tt> objects.
639  */
640  public static void alignAllY(Container aContainer, UiUtil.AlignY aAlignment){
641    java.util.List components = Arrays.asList( aContainer.getComponents() );
642    Iterator compsIter = components.iterator();
643    while ( compsIter.hasNext() ) {
644      JComponent comp = (JComponent)compsIter.next();
645      comp.setAlignmentY( aAlignment.getValue() );
646    }
647  }
648
649  /** Type-safe enumeration vertical alignment. */
650  public enum AlignY {
651    TOP(Component.TOP_ALIGNMENT),
652    CENTER(Component.CENTER_ALIGNMENT),
653    BOTTOM(Component.BOTTOM_ALIGNMENT);
654    float getValue(){
655      return fValue;
656    }
657    private final float fValue;
658    private AlignY( float aValue){
659      fValue = aValue;
660    }
661  }
662  
663  /**
664  * Ensure that <tt>aRootPane</tt> has no default button associated with it.
665  *
666  * <P>Intended mainly for dialogs where the user is confirming a delete action.
667  * In this case, an explicit Yes or No is preferred, with no default action being 
668  * taken when the user hits the Enter key. 
669  */
670  public static void noDefaultButton(JRootPane aRootPane){
671    aRootPane.setDefaultButton(null);
672  }
673
674  // PRIVATE
675  
676  private static final String BACK_SLASH = "/";
677
678  /**
679  * If <tt>aIconName</tt> indicates that the icon is part of the standard graphic
680  * repository (by starting with "/toolbar"), then append either "16.gif" or 
681  * "24.gif" to the name, according to the user's current preference for icon size.
682  */
683  private static String addSizeToStandardIcon(String aIconName){
684    assert( Util.textHasContent(aIconName) );
685    StringBuilder result = new StringBuilder(aIconName);
686    if ( aIconName.startsWith("/toolbar") ) {
687      GeneralLookPreferencesEditor prefs = new GeneralLookPreferencesEditor();
688      if ( prefs.hasLargeIcons() ) {
689        result.append("24.gif");
690      }
691      else {
692        result.append("16.gif");
693      }
694    }
695    return result.toString();
696  }
697
698  private static void setSizes(java.util.List aComponents, Dimension aDimension){
699    Iterator compsIter = aComponents.iterator();      
700    while ( compsIter.hasNext() ) {
701      JComponent comp = (JComponent) compsIter.next();
702      comp.setPreferredSize((Dimension)aDimension.clone());
703      comp.setMaximumSize((Dimension)aDimension.clone());
704    }
705  }
706
707  private static Dimension calcDimensionFromPercent(
708    Dimension aSourceDimension, int aPercentWidth, int aPercentHeight
709  ){
710    int width = aSourceDimension.width * aPercentWidth/100;
711    int height = aSourceDimension.height * aPercentHeight/100;
712    return new Dimension(width, height);
713  }
714
715  /**
716  * If aLabel has text which is longer than MAX_LABEL_LENGTH, then truncate 
717  * the label text and place an ellipsis at the end; the original text is placed 
718  * in a tooltip.
719  *
720  * This is particularly useful for displaying file names, whose length 
721  * can vary widely between deployments.
722  */
723  private static void truncateLabelIfLong(JLabel aLabel){
724    String originalText = aLabel.getText();
725    if (originalText.length() > UiConsts.MAX_LABEL_LENGTH){
726      aLabel.setToolTipText( originalText );
727      String truncatedText = 
728        originalText.substring(0, UiConsts.MAX_LABEL_LENGTH) + Consts.ELLIPSIS
729      ;
730      aLabel.setText(truncatedText);
731    }
732  }
733  
734  private static ImageIcon fetchImageIcon(String aImageId, Class<?> aClass){
735    String imgLocation = addSizeToStandardIcon(aImageId);
736    URL imageURL = aClass.getResource(imgLocation);
737    if ( imageURL != null ) {
738      return new ImageIcon(imageURL);
739    }
740    else {
741      throw new IllegalArgumentException("Cannot retrieve image using id: " + aImageId);
742    }
743  }
744}