001/**
002 * Please feel free to use any fragment of the code in this file that you need in your own
003 * work. As far as I am concerned, it's in the public domain. No permission is necessary
004 * or required. Credit is always appreciated if you use a large chunk or base a
005 * significant product on one of my examples, but that's not required either.
006 *
007 * This code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
008 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
009 * PURPOSE.
010 *
011 * --- Joseph A. Huwaldt
012 */
013package jahuwaldt.swing;
014
015import java.awt.*;
016import java.awt.event.ActionEvent;
017import java.awt.event.ActionListener;
018import java.awt.image.ImageObserver;
019import java.io.File;
020import java.io.FilenameFilter;
021import java.io.PrintWriter;
022import java.io.StringWriter;
023import java.lang.reflect.*;
024import java.net.MalformedURLException;
025import java.net.URISyntaxException;
026import java.net.URL;
027import java.text.MessageFormat;
028import java.util.ArrayList;
029import java.util.Calendar;
030import java.util.List;
031import javax.swing.*;
032
033/**
034 * A set of generic utilities that I have found useful and that are used by most of my
035 * Java applications.
036 *
037 * <p> Modified by: Joseph A. Huwaldt </p>
038 *
039 * @author Joseph A. Huwaldt, Date: February 16, 2000
040 * @version December 12, 2023
041 */
042public class AppUtilities {
043
044    // Used to neatly locate new windows on the display.
045    private static int nthWindow = 0;
046
047    //  OS flags.
048    private static final boolean IS_WINDOWS = System.getProperty("os.name").startsWith("Windows");
049
050    /**
051     * Prevent anyone from instantiating this utility class.
052     */
053    private AppUtilities() { }
054
055    /**
056     * Returns true if this program is running in any MacOS environment, false is
057     * returned otherwise.
058     *
059     * @return true if this program is running in any MacOS environment, false is
060     *         returned otherwise.
061     */
062    public static boolean isMacOS() {
063        return MacOSUtilities.isMacOS();
064    }
065
066    /**
067     * Returns true if this program is running in a MS Windows environment, false is
068     * returned otherwise.
069     *
070     * @return true if this program is running in a MS Windows environment.
071     */
072    public static boolean isWindows() {
073        return IS_WINDOWS;
074    }
075
076    /**
077     * Method that displays a dialog with a scrollable text field that contains the text
078     * of a Java exception message. This allows caught exceptions to be displayed in a GUI
079     * rather than being hidden at a console window or not displayed at all.
080     *
081     * @param parent  The component that the exception dialog should be associated with
082     *                (<code>null</code> is fine).
083     * @param title   The title of the dialog window.
084     * @param message An optional message to display above the text pane
085     *                (<code>null</code> is fine).
086     * @param th      The exception to be displayed in the text pane of the dialog.
087     */
088    public static void showException(Component parent, String title, String message, Throwable th) {
089        StringWriter tracer = new StringWriter();
090        th.printStackTrace(new PrintWriter(tracer, true));
091        String trace = tracer.toString();
092        JPanel view = new JPanel(new BorderLayout());
093        if (message != null)
094            view.add(new JLabel(message), BorderLayout.NORTH);
095        view.add(new JScrollPane(new JTextArea(trace, 10, 40)), BorderLayout.CENTER);
096        JOptionPane.showMessageDialog(parent, view, title, 0);
097    }
098
099    /**
100     * Center the "inside" component inside of the bounding rectangle of the "outside"
101     * component.
102     *
103     * @param outside The component that the "inside" component should be center on.
104     * @param inside  The component that is to be center on the "outside" component.
105     * @return A point representing the upper left corner location required to center the
106     *         inside component in the outside one.
107     */
108    public static Point centerIt(Component outside, Component inside) {
109        if (outside == null || !outside.isVisible() || !outside.isDisplayable())
110            return centerIt(inside);
111        Dimension outerSize = outside.getSize();
112        Dimension innerSize = inside.getSize();
113        Point outerLoc = outside.getLocationOnScreen();
114        Point innerLoc = new Point();
115        innerLoc.x = (outerSize.width - innerSize.width) / 2 + outerLoc.x;
116        innerLoc.y = (outerSize.height - innerSize.height) / 2 + outerLoc.y;
117        return innerLoc;
118    }
119
120    /**
121     * Center the specified component on the screen.
122     *
123     * @param comp The component to be centered on the display screen.
124     * @return A point representing the upper left corner location required to center the
125     *         specified component on the screen.
126     */
127    public static Point centerIt(Component comp) {
128        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
129        Dimension componentSize = comp.getSize();
130        Point componentLoc = new Point(0, 0);
131        componentLoc.x = (screenSize.width - componentSize.width) / 2;
132        componentLoc.y = screenSize.height / 2 - componentSize.height / 2;
133        return componentLoc;
134    }
135
136    /**
137     * Returns a point that can be used to locate a component in the "dialog" position
138     * (1/3 of the way from the top to the bottom of the screen).
139     *
140     * @param comp The component to be located in a "dialog" position.
141     * @return A point representing the upper left corner location required to locate a
142     *         component in the "dialog" position on the screen.
143     */
144    public static Point dialogPosition(Component comp) {
145        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
146        Dimension componentSize = comp.getSize();
147        Point componentLoc = new Point(0, 0);
148        componentLoc.x = (screenSize.width - componentSize.width) / 2;
149        componentLoc.y = screenSize.height / 3 - componentSize.height / 2;
150
151        // Top of screen check
152        componentLoc.y = (componentLoc.y > 0) ? componentLoc.y : 20;
153        return componentLoc;
154    }
155
156    /**
157     * Positions the specified window neatly on the screen such that document windows are
158     * staggered one after the other. The window is set to the input width and height.
159     * Care is also taken to make sure the window will fit on the screen.
160     *
161     * @param inWindow The window to be positioned neatly on the screen.
162     * @param width    the width to set the window to.
163     * @param height   the height to set the window to.
164     */
165    public static void positionWindow(Window inWindow, int width, int height) {
166        final int margin = 30;
167
168        // Window location: staggered
169        int xOffset = (nthWindow * 20) + margin;
170        int yOffset = (nthWindow * 20) + margin;
171        Point windowLoc = new Point(xOffset, yOffset);
172
173        // Window size: minimum of either the input dimensions or the display size.
174        Dimension windowSize = inWindow.getToolkit().getScreenSize();
175        windowSize.width -= xOffset + margin;
176        windowSize.height -= yOffset + margin;
177        if ((width > 0) && (width < windowSize.width))
178            windowSize.width = width;
179
180        if ((height > 0) && (height < windowSize.height))
181            windowSize.height = height;
182
183        // Set final size and location
184        inWindow.setLocation(windowLoc);
185        inWindow.setSize(windowSize);
186
187        // Next window position
188        nthWindow = (nthWindow < 5) ? nthWindow + 1 : 0;
189    }
190
191    /**
192     * Positions the specified window neatly on the screen such that document windows are
193     * staggered one after the other. The size of the window is also set to the input
194     * values. This version assumes that windows are a fixed size and can not be shrunk.
195     *
196     * @param inWindow The window to be placed neatly on the screen.
197     * @param width    the width to set the window to.
198     * @param height   the height to set the window to.
199     */
200    public static void positionWindowFixedSize(Window inWindow, int width, int height) {
201        final int margin = 30;
202
203        // Window location
204        int xOffset = (nthWindow * 20) + margin;
205        int yOffset = (nthWindow * 20) + margin;
206        Point windowLoc = new Point(xOffset, yOffset);
207
208        // Window size
209        Dimension screenSize = inWindow.getToolkit().getScreenSize();
210
211        // Make sure fixed size window fits.
212        int sum = xOffset + margin + width;
213        if (sum > screenSize.width)
214            windowLoc.x -= sum - screenSize.width;
215
216        sum = yOffset + margin + height;
217        if (sum > screenSize.height)
218            windowLoc.y -= sum - screenSize.height;
219
220        // Set final size and location
221        inWindow.setLocation(windowLoc);
222        inWindow.setSize(width, height);
223
224        // Next window position
225        nthWindow = (nthWindow < 5) ? nthWindow + 1 : 0;
226    }
227
228    /**
229     * Fills in the GridBagConstraints record for a given component with input items.
230     *
231     * @param gbc The GridBagConstraints record to be filled in.
232     * @param gx  Grid (cell) index for this component in the x direction (1 == 2nd
233     *            column).
234     * @param gy  Grid (cell) index for this component in the y direction (3 == 4th row).
235     * @param gw  The number of cells this component spans in width.
236     * @param gh  The number of cells this component spans in height.
237     * @param wx  Proportional width of this grid cell compared to others.
238     * @param wy  Proportional height of this grid cell compared to others.
239     */
240    public static void buildConstraints(GridBagConstraints gbc, int gx, int gy,
241            int gw, int gh, int wx, int wy) {
242        gbc.gridx = gx;
243        gbc.gridy = gy;
244
245        gbc.gridwidth = gw;
246        gbc.gridheight = gh;
247
248        gbc.weightx = wx;
249        gbc.weighty = wy;
250
251    }
252
253    /**
254     * Try and determine the directory to where the program is installed and return that
255     * as a URL. Has essentially the same function as Applet.getDocumentBase(), but works
256     * on local file systems in applications.
257     *
258     * @return A URL pointing to the directory where the program is installed.
259     */
260    public static URL getDocumentBase() {
261
262        //  Object is to try and figure out where the program is installed.
263        //  First see if the program was installed using ZeroG's Install Anywhere installer.
264        String dir = System.getProperty("lax.root.install.dir");
265
266        if (dir == null || dir.equals("")) {
267            try {
268                //  Try this class's code source location as a file.
269                URL location = AppUtilities.class.getProtectionDomain().getCodeSource().getLocation();
270                File file = new File(location.toURI());
271                if (file.exists()) {
272                    file = file.getParentFile();
273                    return file.toURI().toURL();
274                }
275            } catch (MalformedURLException | URISyntaxException e) {
276                /* Just move on. */
277            }
278            /* Just move on. */
279
280            //  Fall back on "user.dir" if all else fails.
281            //  However, on some systems this will return the directory where the program is executed,
282            //  not where it is installed.
283            dir = System.getProperty("user.dir");
284        }
285
286        //  Deal with different file separator characters.
287        String urlDir = dir.replace(File.separatorChar, '/');
288        if (!urlDir.endsWith("/"))
289            urlDir = urlDir + "/";
290
291        URL output = null;
292        try {
293            output = new URL("file", null, urlDir);
294
295        } catch (MalformedURLException e) {
296        }
297
298        return output;
299    }
300
301    /**
302     * Return the root component of the given component.
303     *
304     * @param source The Component or MenuComponent to find the root Component for.
305     * @return the root component of the given component.
306     */
307    public static Component getRootComponent(Object source) {
308        Component root = null;
309
310        if (source instanceof Component)
311            root = SwingUtilities.getRoot((Component)source);
312
313        else if (source instanceof MenuComponent) {
314            MenuContainer mParent = ((MenuComponent)source).getParent();
315            return getRootComponent(mParent);
316        }
317
318        return root;
319    }
320
321    /**
322     * Returns the specified component's top-level <code>Frame</code>.
323     *
324     * @param parentComponent the <code>Component</code> to check for a <code>Frame</code>
325     * @return the <code>Frame</code> that contains the component, or <code>null</code> if
326     *         the component is <code>null</code>, or does not have a valid
327     *         <code>Frame</code> parent
328     */
329    public static Frame getFrameForComponent(Component parentComponent) {
330        if (parentComponent == null)
331            return null;
332        if (parentComponent instanceof Frame)
333            return (Frame)parentComponent;
334        return getFrameForComponent(parentComponent.getParent());
335    }
336
337    /**
338     * Method that loads an image from a URL and creates a custom mouse cursor from it.
339     *
340     * @param url      A URL to the image to be loaded as a cursor.
341     * @param hsx      The x-coordinate of the point on the image that represents the
342     *                 cursor hot spot.
343     * @param hsy      The y coordinate of the point on the image that represents the
344     *                 cursor hot spot.
345     * @param name     The name to assign to this cursor for Java Accessibility.
346     * @param observer The component to use to observe the loading of the image. Pass
347     *                 <code>null</code> if none.
348     * @return The custom cursor generated from the specified image. If any error occurs
349     *         <code>null</code> will be returned.
350     */
351    public static Cursor getImageCursor(URL url, int hsx, int hsy, String name, ImageObserver observer) {
352        Toolkit tk = Toolkit.getDefaultToolkit();
353        Image img = tk.getImage(url);
354        return makeImageCursor(img, hsx, hsy, name, observer);
355    }
356
357    /**
358     * Method that loads an image from a file and creates a custom mouse cursor from it.
359     *
360     * @param path     The path to the image to be loaded as a cursor.
361     * @param hsx      The x-coordinate of the point on the image that represents the
362     *                 cursor hot spot.
363     * @param hsy      The y coordinate of the point on the image that represents the
364     *                 cursor hot spot.
365     * @param name     The name to assign to this cursor for Java Accessibility.
366     * @param observer The component to use to observe the loading of the image. Pass
367     *                 <code>null</code> if none.
368     * @return The custom cursor generated from the specified image. If any error occurs
369     *         <code>null</code> will be returned.
370     */
371    public static Cursor getImageCursor(String path, int hsx, int hsy, String name, ImageObserver observer) {
372        Toolkit tk = Toolkit.getDefaultToolkit();
373        Image img = tk.getImage(path);
374        return makeImageCursor(img, hsx, hsy, name, observer);
375    }
376
377    /**
378     * Method that creates a custom mouse cursor from the specified image.
379     *
380     * @param img      The image to be loaded as a cursor.
381     * @param hsx      The x-coordinate of the point on the image that represents the
382     *                 cursor hot spot.
383     * @param hsy      The y coordinate of the point on the image that represents the
384     *                 cursor hot spot.
385     * @param name     The name to assign to this cursor for Java Accessibility.
386     * @param observer The component to use to observe the loading of the image. Pass
387     *                 <code>null</code> if none.
388     * @return The custom cursor generated from the specified image. If any error occurs
389     *         <code>null</code> will be returned.
390     */
391    public static Cursor makeImageCursor(Image img, int hsx, int hsy, String name, ImageObserver observer) {
392        Cursor cursor = null;
393
394        if (img == null)
395            return null;
396
397        try {
398            Toolkit tk = Toolkit.getDefaultToolkit();
399
400            //  Wait for the image to load.
401            int width = 0, height = 0;
402            int count = 0;
403            while ((width < 1 || height < 1) && count < 10000) {
404                width = img.getWidth(observer);
405                height = img.getHeight(observer);
406                ++count;
407            }
408
409            if (width > 0 && height > 0) {
410                //  Scale hot spot to the best cursor size.
411                Dimension bestSize = tk.getBestCursorSize(width, height);
412
413                if (bestSize.width > 0 && bestSize.height > 0) {
414                    Point hotSpot = new Point(hsx * bestSize.width / width, hsy * bestSize.height / height);
415
416                    //  Create the cursor.
417                    cursor = tk.createCustomCursor(img, hotSpot, name);
418                }
419            }
420
421        } catch (IndexOutOfBoundsException e) {
422            //  Just return null.
423        }
424
425        return cursor;
426    }
427
428    /**
429     * Sets the Swing look and feel to hide that hideous default Java LAF.
430     *
431     * @throws javax.swing.UnsupportedLookAndFeelException
432     * @throws java.lang.IllegalAccessException
433     * @throws java.lang.ClassNotFoundException
434     * @throws java.lang.InstantiationException
435     */
436    public static void setSystemLAF() throws UnsupportedLookAndFeelException,
437            IllegalAccessException, ClassNotFoundException, InstantiationException {
438
439        // Set the system look and feel to hide that hideous Java LAF.
440        String laf = UIManager.getSystemLookAndFeelClassName();
441        UIManager.setLookAndFeel(laf);
442
443    }
444
445    /**
446     * Returns true if the current look and feel is the same as the system look and feel.
447     * Otherwise, returns false.
448     *
449     * @return true if the current look and feel is the system look and feel.
450     */
451    public static boolean isSystemLAF() {
452        return UIManager.getSystemLookAndFeelClassName().equals(UIManager.getLookAndFeel().getClass().getName());
453    }
454
455    /**
456     * Build up a JMenu from a description stored in a list of String arrays. The
457     * description contains the text of each menu item, accelerator key, and the name of a
458     * public method in the specified "eventTarget" object that handles the menu item
459     * action event.
460     *
461     * @param eventTarget This is the object that must contain the methods used to handle
462     *                    action events for this menu's items.
463     * @param name        Name of the menu to create.
464     * @param menuDesc    List of String arrays that describes each of the menu items that
465     *                    will be built into this menu. For each String array, item 0 is
466     *                    the menu item text string. Item 1 is the accelerator key for the
467     *                    menu item. Item 2 is the name of the public method in
468     *                    "eventTarget" that will handle the user choosing the menu item.
469     *                    This method must take an ActionEvent as it's only parameter. If
470     *                    this is <code>null</code>, the menu item will be shown as
471     *                    disabled. Items 3 is optional and is the tool-tip text to show
472     *                    for the menu item if not <code>null</code>.
473     * @return A menu built up from the menu description supplied.
474     * @throws NoSuchMethodException If one of the action event methods in parent could
475     * not be found.
476     */
477    public static JMenu buildMenu(Object eventTarget, String name, List<String[]> menuDesc)
478            throws NoSuchMethodException {
479        return buildMenu(eventTarget, name, menuDesc.toArray(new String[0][]));
480    }
481
482    /**
483     * Build up a JMenu from a description stored in a String array. The description
484     * contains the text of each menu item, accelerator key, and the name of a public
485     * method in the specified "eventTarget" object that handles the menu item action
486     * event.
487     *
488     * @param eventTarget This is the object that must contain the public methods used to
489     *                    handle action events for this menu's items.
490     * @param name        Name of the menu to create.
491     * @param menuDesc    String array that describes each of the menu items that will be
492     *                    built into this menu. For each String array, item 0 is the menu
493     *                    item text string. Item 1 is the accelerator key for the menu
494     *                    item. Item 2 is the name of the public method in "eventTarget"
495     *                    that will handle the user choosing the menu item. This method
496     *                    must take an ActionEvent as it's only parameter. If this is
497     *                    <code>null</code>, the menu item will be shown as disabled.
498     *                    Items 3 is optional and is the tool-tip text to show for the
499     *                    menu item if not <code>null</code>.
500     * @return A menu built up from the menu description supplied.
501     * @throws NoSuchMethodException If one of the action event methods in parent could
502     * not be found.
503     */
504    public static JMenu buildMenu(Object eventTarget, String name, String[][] menuDesc)
505            throws NoSuchMethodException {
506        if (menuDesc == null || eventTarget == null) {
507            throw new NullPointerException("menuDesc or eventTarget are null!");
508        }
509
510        // Create a menu to add items to.
511        JMenu aMenu = new JMenu(name);
512
513        // Add a list of items to a specified menu.
514        int numItems = menuDesc.length;
515        for (int i = 0; i < numItems; ++i) {
516
517            // Is this a menu separator?
518            if (menuDesc[i][0] == null) {
519                JSeparator separator = new JSeparator();
520                aMenu.add(separator);
521
522            } else {
523
524                // Create the menu item.
525                JMenuItem menuItem = buildMenuItem(eventTarget, menuDesc[i]);
526
527                // Add this new menu item to the specified menu.
528                aMenu.add(menuItem);
529            }
530        }
531
532        return aMenu;
533    }
534
535    /**
536     * Build up a JMenuItem from a description stored in a String array. The description
537     * contains the text of each menu item, accelerator key, and the name of a public
538     * method in the specified "eventTarget" object that handles the menu item action
539     * event.
540     *
541     * @param eventTarget This is the object that must contain the public methods used to
542     *                    handle action events for this item.
543     * @param menuDesc    A 3 or 4-element String array that describes the menu item that
544     *                    will be built. Item 0 is the menu item text string. Item 1 is
545     *                    the accelerator key for the menu item. Item 2 is the name of the
546     *                    public method in "eventTarget" that will handle the user
547     *                    choosing this menu item. This method must take an ActionEvent as
548     *                    it's only parameter. If this is <code>null</code>, the menu item
549     *                    will be shown as disabled. Items 3 is optional and is the
550     *                    tool-tip text to show for the menu item if not
551     *                    <code>null</code>.
552     * @return A new JMenuItem built from the description in menuDesc with action
553     *         listeners that call methods in parent.
554     * @throws NoSuchMethodException If one of the action event methods in parent could
555     * not be found.
556     */
557    public static JMenuItem buildMenuItem(Object eventTarget, String[] menuDesc) throws NoSuchMethodException {
558        if (eventTarget == null)
559            throw new NullPointerException("eventTarget is null");
560
561        // Create the menu item.
562        String menuString = menuDesc[0];
563        JMenuItem menuItem = new JMenuItem(menuString);
564
565        // Does this menu item have an accelerator key?
566        String accelerator = menuDesc[1];
567        if (accelerator != null && !accelerator.equals("")) {
568            // Add the accelerator key.
569            Toolkit tk = Toolkit.getDefaultToolkit();
570            int accCharMask = tk.getMenuShortcutKeyMaskEx();
571            menuItem.setAccelerator(KeyStroke.getKeyStroke(accelerator.charAt(0), accCharMask, false));
572        }
573
574        // Does this menu item have an action method?
575        String methodStr = menuDesc[2];
576        if (methodStr != null && !methodStr.equals("")) {
577            // Create an action listener by reflection that refers back to
578            // the specified method in the specified frame.
579            ActionListener listener = getActionListenerForMethod(eventTarget, methodStr);
580            menuItem.addActionListener(listener);
581            menuItem.setActionCommand(menuString);
582            menuItem.setEnabled(true);
583
584        } else {
585            // If no method, disable this menu item.
586            menuItem.setEnabled(false);
587        }
588
589        if (menuDesc.length > 3) {
590            String tooltip = menuDesc[3];
591            if (tooltip != null)
592                menuItem.setToolTipText(tooltip);
593        }
594
595        return menuItem;
596    }
597
598    /**
599     * Build up a ButtonGroup containing toggle buttons from a description stored in a
600     * String array. The description contains, for each button, the text to appear in the
601     * button (<code>null</code> if no text), the name of the method in the parent object
602     * that will be called when a button is clicked on (if <code>null</code> is passed,
603     * the button is disabled), and the tool tip text to display for the button;
604     * (<code>null</code> for no tool tip).
605     *
606     * @param parent  The parent object that will contain handle the user clicking on one
607     *                of the buttons in this group. This object is also used to observe
608     *                the reading in of the button icon images.
609     * @param defs    String array that describes each of the text string that will be
610     *                displayed in the button. Item 0 is the text to be displayed in each
611     *                button. If this is <code>null</code> no text is displayed. Item 1 is
612     *                the name of the method in "parent" that will handle the user
613     *                clicking on this button item. If this is <code>null</code>, the menu
614     *                button will be shown as disabled. Item 2 is the tool tip to be shown
615     *                above this button item. If <code>null</code> is passed no tool tip
616     *                is shown.
617     * @param imgURLs An array of URLs for the images to be displayed in the buttons. If
618     *                <code>null</code> is passed in, then there will be no button images.
619     *                If any element is <code>null</code>, that button will have no image.
620     * @return A button group built up from the button description supplied.
621     * @throws java.lang.NoSuchMethodException if a specified method in the parent object
622     * doesn't exist.
623     */
624    public static ButtonGroup buildButtonGroup(ImageObserver parent, String[][] defs, URL[] imgURLs) throws NoSuchMethodException {
625
626        ImageIcon[] icons = null;
627
628        if (imgURLs != null) {
629            int length = imgURLs.length;
630            if (defs.length != length)
631                throw new IllegalArgumentException("The defs & imgURLs arrays have different lengths.");
632            icons = new ImageIcon[length];
633            for (int i = 0; i < length; ++i) {
634                if (imgURLs[i] != null)
635                    icons[i] = new ImageIcon(imgURLs[i]);
636            }
637        }
638
639        return buildButtonGroup(parent, defs, icons);
640    }
641
642    /**
643     * Build up a ButtonGroup containing toggle buttons from a description stored in a
644     * String array. The description contains, for each button, the text to appear in the
645     * button (<code>null</code> if no text), the name of the method in the parent object
646     * that will be called when a button is clicked on (if <code>null</code> is passed,
647     * the button is disabled), and the tool tip text to display for the button
648     * (<code>null</code> for no tool tip).
649     *
650     * @param parent   The parent object that will contain handle the user clicking on one
651     *                 of the buttons in this group. This object is also used to observe
652     *                 the reading in of the button icon images.
653     * @param defs     String array that describes each of the text string that will be
654     *                 displayed in the button. Item 0 is the text to be displayed in each
655     *                 button. If this is <code>null</code> no text is displayed. Item 1
656     *                 is the name of the method in "parent" that will handle the user
657     *                 clicking on this button item. If this is <code>null</code>, the
658     *                 menu button will be shown as disabled. Item 2 is the tool tip to be
659     *                 shown above this button item. If <code>null</code> is passed no
660     *                 tool tip is shown.
661     * @param imgPaths An array of image file paths for the images to be displayed in the
662     *                 buttons. If <code>null</code> is passed in, then there will be no
663     *                 button images. If any element is <code>null</code>, that button
664     *                 will have no image.
665     * @return A button group built up from the button description supplied.
666     * @throws java.lang.NoSuchMethodException if a specified method in the parent object
667     * doesn't exist.
668     */
669    public static ButtonGroup buildButtonGroup(ImageObserver parent, String[][] defs, String[] imgPaths) throws NoSuchMethodException {
670
671        ImageIcon[] icons = null;
672
673        if (imgPaths != null) {
674            int length = imgPaths.length;
675            if (defs.length != length)
676                throw new IllegalArgumentException("The defs & imgPaths arrays have different lengths.");
677            icons = new ImageIcon[length];
678            for (int i = 0; i < length; ++i) {
679                if (imgPaths[i] != null)
680                    icons[i] = new ImageIcon(imgPaths[i]);
681            }
682        }
683
684        return buildButtonGroup(parent, defs, icons);
685    }
686
687    /**
688     * Build up a ButtonGroup containing toggle buttons from a description stored in a
689     * String array. The description contains, for each button, the text to appear in the
690     * button (<code>null</code> if no text), the path to an image file to display in the
691     * button (<code>null</code> for no image), the name of the method in the parent
692     * object that will be called when a button is clicked on (if <code>null</code> is
693     * passed, the button is disabled), and the tool tip text to display for the button
694     * (<code>null</code> for no tool tip).
695     *
696     * @param parent The parent object that will contain handle the user clicking on one
697     *               of the buttons in this group. This object is also used to observe the
698     *               reading in of the button icon images.
699     * @param defs   String array that describes each of the text string that will be
700     *               displayed in the button. Item 0 is the text to be displayed in each
701     *               button. If this is <code>null</code> no text is displayed. Item 1 is
702     *               the name of the method in "parent" that will handle the user clicking
703     *               on this button item. If this is <code>null</code>, the menu button
704     *               will be shown as disabled. Item 2 is the tool tip to be shown above
705     *               this button item. If <code>null</code> is passed no tool tip is
706     *               shown.
707     * @param icons  An array of icon images to be displayed in the buttons. If this array
708     *               is <code>null</code>, there will be no images displayed in the
709     *               buttons. If one of the elements of the array is <code>null</code>,
710     *               that button will have no image.
711     * @return A button group built up from the button description supplied.
712     * @throws java.lang.NoSuchMethodException if a specified method in the parent object
713     * doesn't exist.
714     */
715    public static ButtonGroup buildButtonGroup(ImageObserver parent, String[][] defs, ImageIcon[] icons) throws NoSuchMethodException {
716
717        if (icons != null && defs.length != icons.length)
718            throw new IllegalArgumentException("The defs & icons arrays have different lengths.");
719
720        // Create a group for these buttons so that they toggle.
721        ButtonGroup bGroup = new ButtonGroup();
722
723        // Create buttons.
724        int numBtns = defs.length;
725        for (int i = 0; i < numBtns; ++i) {
726
727            // Extract information about this button.
728            String title = defs[i][0];
729            String methodStr = defs[i][1];
730            String toolTip = defs[i][2];
731            ImageIcon icon = null;
732            if (icons != null)
733                icon = icons[i];
734
735            JToggleButton button = null;
736            if (title != null && !title.equals("")) {
737                if (icon != null)
738                    button = new JToggleButton(title, icon);
739                else
740                    button = new JToggleButton(title);
741
742            } else {
743                if (icon != null) {
744                    button = new JToggleButton(icon);
745                    Image image = icon.getImage();
746                    Dimension size = new Dimension(image.getWidth(parent),
747                            image.getHeight(parent));
748                    button.setMaximumSize(size);
749                }
750            }
751
752            if (button != null) {
753                bGroup.add(button);
754                button.setAlignmentX(JToggleButton.CENTER_ALIGNMENT);
755                button.setAlignmentY(JToggleButton.CENTER_ALIGNMENT);
756                button.setMargin(new Insets(0, 0, 0, 0));
757                if (toolTip != null)
758                    button.setToolTipText(toolTip);
759
760                if (i == 0)
761                    button.setSelected(true);
762
763                if (methodStr != null && !methodStr.equals("")) {
764
765                        // Create an action listener by reflection that refers back to
766                    // the specified method in the specified frame.
767                    ActionListener listener = getActionListenerForMethod(parent, methodStr);
768                    button.addActionListener(listener);
769                    button.setActionCommand(title);
770                    button.setEnabled(true);
771
772                } else {
773                    // If no method, disable this menu item.
774                    button.setEnabled(false);
775                }
776
777            }
778        }
779
780        return bGroup;
781    }
782
783    /**
784     * Build up a List of buttons from a description stored in a String array. The
785     * description contains, for each button, the text to appear in the button
786     * (<code>null</code> if no text), the path to an image file to display in the button
787     * (<code>null</code> for no image), the name of the method in the parent object that
788     * will be called when a button is clicked on (if <code>null</code> is passed, the
789     * button is disabled), and the tool tip text to display for the button
790     * (<code>null</code> for no tool tip).
791     *
792     * @param parent   The parent object that will handle the user clicking on one of the
793     *                 buttons in this group.
794     * @param observer The object used to observe the reading in of the button icon
795     *                 images, if any (if no images being read in, this may be null).
796     * @param defs     String array that describes each button. Item 0 is the text string
797     *                 that will be displayed in the button. If this is <code>null</code>
798     *                 no text is displayed. Item 1 is the file path to the image to
799     *                 display in this button. If <code>null</code> is passed, no image
800     *                 icon is displayed. Item 2 is the name of the method in "parent"
801     *                 that will handle the user clicking on this button item. If this is
802     *                 <code>null</code>, the menu button will be shown as disabled. Item
803     *                 3 is the tool tip to be shown above this button item. If
804     *                 <code>null</code> is passed no tool tip is shown.
805     * @return A List containing all the buttons built up from the description supplied.
806     * @throws java.lang.NoSuchMethodException if a specified method in the parent object
807     * doesn't exist.
808     */
809    public static List<JButton> buildButtonList(Object parent, ImageObserver observer, String[][] defs) throws NoSuchMethodException {
810        // Create a group for these buttons so that they toggle.
811        ArrayList<JButton> bList = new ArrayList();
812
813        // Create buttons.
814        int numBtns = defs.length;
815        for (int i = 0; i < numBtns; ++i) {
816
817            // Extract information about this button.
818            String title = defs[i][0];
819            String imagePath = defs[i][1];
820            String methodStr = defs[i][2];
821            String toolTip = defs[i][3];
822
823            JButton button = null;
824            if (title != null && !title.equals("")) {
825                if (imagePath != null && !imagePath.equals(""))
826                    button = new JButton(title, new ImageIcon(imagePath));
827                else
828                    button = new JButton(title);
829
830            } else {
831                if (imagePath != null && !imagePath.equals("")) {
832                    ImageIcon icon = new ImageIcon(imagePath);
833                    button = new JButton(icon);
834                    Image image = icon.getImage();
835                    Dimension size = new Dimension(image.getWidth(observer),
836                            image.getHeight(observer));
837                    button.setMaximumSize(size);
838                }
839            }
840
841            if (button != null) {
842                bList.add(button);
843                button.setAlignmentX(JButton.CENTER_ALIGNMENT);
844                button.setAlignmentY(JButton.CENTER_ALIGNMENT);
845                button.setMargin(new Insets(0, 0, 0, 0));
846                if (toolTip != null && !toolTip.equals(""))
847                    button.setToolTipText(toolTip);
848
849                if (methodStr != null && !methodStr.equals("")) {
850
851                        // Create an action listener by reflection that refers back to
852                    // the specified method in the specified frame.
853                    ActionListener listener = getActionListenerForMethod(parent, methodStr);
854                    button.addActionListener(listener);
855                    button.setActionCommand(title);
856                    button.setEnabled(true);
857
858                } else {
859                    // If no method, disable this menu item.
860                    button.setEnabled(false);
861                }
862
863            }
864        }
865
866        return bList;
867    }
868
869    /**
870     * Method that returns an action listener that simply calls the specified method in
871     * the specified class. This is little trick is done using the magic of reflection.
872     *
873     * @param target    The object which contains the specified method.
874     * @param methodStr The name of the method to be called in the target object. This
875     *                  method must be contained in target, must be publicly accessible
876     *                  and must accept an ActionEvent object as the only parameter.
877     * @return An ActionListener that will call the specified method in the specified
878     *         target object and pass to it an ActionEvent object.
879     * @throws NoSuchMethodException if the target object does not contain the specified
880     * method.
881     */
882    public static ActionListener getActionListenerForMethod(Object target, String methodStr)
883            throws NoSuchMethodException {
884        Method m = target.getClass().getMethod(methodStr, new Class[]{ActionEvent.class});
885        ActionListener listener = new GenericActionListener(target, m);
886        return listener;
887    }
888
889    /**
890     * Method that exits the program (with a warning message) if the current data is after
891     * the specified date.
892     *
893     * @param date    The data and time when the program should expire.
894     * @param message The warning message to show if the program has expired.
895     */
896    public static void checkDateAndDie(Calendar date, String message) {
897        if (Calendar.getInstance().after(date)) {
898            JOptionPane.showMessageDialog(null, message, "Expired", JOptionPane.ERROR_MESSAGE);
899            System.exit(0);
900        }
901    }
902
903    /**
904     * Method that returns the index to an option that a user has selected from an array
905     * of options using a standard input dialog.
906     *
907     * @param parent                The parent Component for the dialog
908     * @param msg                   The message to display to the user.
909     * @param title                 The title of the dialog window.
910     * @param messageType           One of the JOptionPane message type constants:
911     *                              ERROR_MESSAGE, INFORMATION_MESSAGE, WARNING_MESSAGE,
912     *                              QUESTION_MESSAGE, or PLAIN_MESSAGE.
913     * @param selectionValues       An array of Objects that gives the possible
914     *                              selections.
915     * @param initialSelectionValue The value used to initialize the input field.
916     * @return The index into the input array of the value the user has selected or -1 if
917     *         the user canceled.
918     */
919    public static int userSelectionFromArray(Component parent, Object msg, String title, int messageType,
920            Object[] selectionValues, Object initialSelectionValue) {
921
922        Object selection = JOptionPane.showInputDialog(parent, msg, title, messageType, null,
923                selectionValues, initialSelectionValue);
924        if (selection == null)
925            return -1;
926
927        int length = selectionValues.length;
928        int i = 0;
929        for (; i < length; ++i)
930            if (selection.equals(selectionValues[i]))
931                break;
932
933        return i;
934    }
935
936    /**
937     * Method that returns the bounded integer value that a user has entered in a standard
938     * input dialog.
939     *
940     * @param parent       The parent Component for the dialog
941     * @param msg          The message to display to the user.
942     * @param title        The title of the dialog window.
943     * @param messageType  One of the JOptionPane message type constants: ERROR_MESSAGE,
944     *                     INFORMATION_MESSAGE, WARNING_MESSAGE, QUESTION_MESSAGE, or
945     *                     PLAIN_MESSAGE.
946     * @param initialValue The initial value for the dialog's text field.
947     * @param lowerBound   The lowest (most negative) value to allow the user to enter
948     *                     (use Integer.MIN_VALUE to allow any negative integer).
949     * @param upperBound   The largest (most positive) value to allow the user to enter
950     *                     (use Integer.MAX_VALUE to allow any positive integer).
951     * @return The integer value the user has entered or <code>null</code> if the user has
952     *         canceled.
953     */
954    public static Integer userEnteredInteger(Component parent, String msg, String title, int messageType,
955            int initialValue, int lowerBound, int upperBound) {
956        Integer value = null;
957        String output;
958        do {
959            output = (String)JOptionPane.showInputDialog(parent, msg, title, messageType, null,
960                    null, initialValue);
961            if (output != null) {
962                try {
963                    value = Integer.valueOf(output);
964                    if (value < lowerBound || value > upperBound)
965                        throw new NumberFormatException();
966                    break;
967                } catch (NumberFormatException e) {
968                    Toolkit.getDefaultToolkit().beep();
969                }
970            }
971        } while (output != null);
972
973        return value;
974    }
975
976    /**
977     * Method that brings up a file chooser dialog and allows the user to select a file.
978     *
979     * @param parent    The owner of the dialog (<code>null</code> is fine).
980     * @param mode      Either FileDialog.LOAD or FileDialog.SAVE.
981     * @param message   The message for the dialog. Something like "Choose a name for this
982     *                  file:".
983     * @param directory The directory to prompt the user with by default
984     *                  (<code>null</code> is fine).
985     * @param name      The name of the file to prompt the user with by default
986     *                  (<code>null</code> is fine).
987     * @param filter    The filename filter to use (<code>null</code> means no filter).
988     * @return The file selected by the user, or <code>null</code> if no file was
989     *         selected.
990     */
991    public static File selectFile(Component parent, int mode, String message,
992            String directory, String name, FilenameFilter filter) {
993
994        Frame frame;
995        if (parent == null)
996            frame = new Frame();
997        else
998            frame = getFrameForComponent(parent);
999
1000        if (message == null)
1001            message = "";
1002
1003        // Bring up a file chooser.
1004        FileDialog fd = new FileDialog(frame, message, mode);
1005        fd.addNotify();
1006
1007        if (directory != null && (isWindows() || isMacOS()) && !directory.equals("")) {
1008            // Prompt the user with the existing directory path.
1009            File dirFile = new File(directory);
1010            if (!dirFile.isDirectory())
1011                directory = dirFile.getParent();
1012            if (new File(directory).exists())
1013                fd.setDirectory(directory);
1014        }
1015
1016        if (name != null)
1017            //  Prompt the user with the existing file name.
1018            fd.setFile(name);
1019
1020        if (filter != null)
1021            fd.setFilenameFilter(filter);
1022
1023        fd.setVisible(true);
1024        String fileName = fd.getFile();
1025
1026        if (fileName != null) {
1027
1028            //  Create the file reference to the chosen file.
1029            File chosenFile = new File(fd.getDirectory(), fileName);
1030
1031            return chosenFile;
1032        }
1033
1034        return null;
1035    }
1036
1037    /**
1038     * Method that brings up a file chooser dialog and allows the user to select a
1039     * directory.
1040     *
1041     * @param parent    The owner of the dialog (<code>null</code> is fine).
1042     * @param mode      Either FileDialog.LOAD or FileDialog.SAVE.
1043     * @param message   The message for the dialog. Something like "Choose a name for this
1044     *                  directory:".
1045     * @param directory The directory to prompt the user with by default
1046     *                  (<code>null</code> is fine).
1047     * @param filter    The filename filter to use (<code>null</code> means no filter).
1048     * @return The file selected by the user, or <code>null</code> if no file was
1049     *         selected.
1050     */
1051    public static File selectDirectory(Component parent, int mode, String message,
1052            String directory, FilenameFilter filter) {
1053
1054        Frame frame;
1055        if (parent == null)
1056            frame = new Frame();
1057        else
1058            frame = getFrameForComponent(parent);
1059
1060        if (message == null)
1061            message = "";
1062
1063        File theFile = null;
1064        if (isMacOS()) {
1065            //  Use the native file chooser (the Java Swing one is inadequate for MacOS users).
1066
1067            // Bring up a directory chooser.
1068            System.setProperty("apple.awt.fileDialogForDirectories", "true");
1069            FileDialog fd = new FileDialog(frame, message, mode);
1070            fd.addNotify();
1071
1072            if (directory != null && !directory.equals(""))
1073                // Prompt the user with the existing directory path.
1074                fd.setDirectory(directory);
1075
1076            if (filter != null)
1077                fd.setFilenameFilter(filter);
1078
1079            fd.setVisible(true);
1080            String fileName = fd.getFile();
1081
1082            if (fileName != null)
1083                //  The user has chosen a directory.
1084                theFile = new File(fd.getDirectory(), fileName);
1085
1086            System.setProperty("apple.awt.fileDialogForDirectories", "false");
1087
1088        } else {
1089            //  Use the Java Swing file chooser.
1090            JFileChooser fc = new JFileChooser(directory);
1091            fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
1092            int returnVal;
1093            if (mode == FileDialog.LOAD)
1094                returnVal = fc.showOpenDialog(parent);
1095            else
1096                returnVal = fc.showSaveDialog(parent);
1097
1098            if (returnVal == JFileChooser.APPROVE_OPTION)
1099                //  The user has chosen a directory.
1100                theFile = fc.getSelectedFile();
1101        }
1102
1103        return theFile;
1104    }
1105
1106    /**
1107     * Method that displays a "Save As..." dialog asking the user to select or input a
1108     * file name to save a file to and returns a reference to the chosen file. This
1109     * version of selectFile automatically adds a user supplied extension to the file
1110     * returned if it doesn't already have it. N.B.: No test is made to see if the user
1111     * can actually write to that file! See: File.canWrite().
1112     *
1113     * @param parent          The owner of the dialog (<code>null</code> is fine).
1114     * @param message         The message for the dialog. Something like "Choose a name
1115     *                        for this file:".
1116     * @param directory       The directory to prompt the user with by default
1117     *                        (<code>null</code> is fine).
1118     * @param name            The name of the file to prompt the user with by default
1119     *                        (<code>null</code> is fine).
1120     * @param filter          The filename filter to use (<code>null</code> means no
1121     *                        filter).
1122     * @param extension       The filename extension. This is appended to the filename
1123     *                        provided by the user if the filename input doesn't already
1124     *                        have it. Passing <code>null</code> means that no extension
1125     *                        will be forced onto the filename.
1126     * @param existsErrFmtMsg A MessageFormat compatible "file exists" error message that
1127     *                        will have the file name substituted into it. Pass something
1128     *                        like, "A file with the named \"{0}\" already exists. Do you
1129     *                        want to replace it?"
1130     * @param dlgTitle        The title of the warning dialog that is shown if the
1131     *                        selected file already exists.
1132     * @return The file chosen by the user for saving out a file. Returns
1133     *         <code>null</code> if the a valid file was not chosen.
1134     */
1135    public static File selectFile4Save(Component parent, String message, String directory, String name,
1136            FilenameFilter filter, String extension, String existsErrFmtMsg, String dlgTitle) {
1137
1138        //  Have the user choose a file.
1139        File chosenFile = selectFile(parent, FileDialog.SAVE, message, directory, name, filter);
1140
1141        chosenFile = addExtensionToFile(parent, chosenFile, extension, existsErrFmtMsg, dlgTitle);
1142
1143        return chosenFile;
1144    }
1145
1146    /**
1147     * Return a version of the provided file reference that has the specified extension on
1148     * it. This is intended to be used as part of processing a user input file name.
1149     * <ul>
1150     * <li>If the input file already has the extension, it is simply returned.</li>
1151     * <li>If the input file doesn't have the extension, it is added to the end.</li>
1152     * <li>If the modified file reference is an existing file, the user is asked if they
1153     * want to overwrite it.</li>
1154     * <li>If the user chooses to overwrite the existing file, then the file reference is
1155     * returned.</li>
1156     * <li>If the user chooses not to overwrite it, then <code>null</code> is
1157     * returned.</li>
1158     * </ul>
1159     *
1160     * @param parent          The owner of the dialog (<code>null</code> is fine).
1161     * @param theFile         The file to enforce an extension for. If <code>null</code>,
1162     *                        then this dialog does nothing.
1163     * @param extension       The extension to ensure the file has. If <code>null</code>,
1164     *                        then this method does nothing.
1165     * @param existsErrFmtMsg A MessageFormat compatible "file exists" error message that
1166     *                        will have the file name substituted into it. Pass something
1167     *                        like, "A file with the named \"{0}\" already exists. Do you
1168     *                        want to replace it?"
1169     * @param dlgTitle        The title of the warning dialog that is shown if the
1170     *                        selected file already exists.
1171     * @return a version of the provided file reference that has the specified extension
1172     *         on it.
1173     */
1174    public static File addExtensionToFile(Component parent, File theFile, String extension,
1175            String existsErrFmtMsg, String dlgTitle) {
1176
1177        if (theFile != null && extension != null) {
1178            if (!extension.startsWith("."))
1179                extension = "." + extension;
1180
1181            String fileName = theFile.getName();
1182            if (!fileName.toLowerCase().endsWith(extension)) {
1183                //  Create a new file reference including the extension.
1184                theFile = new File(theFile.getParent(), fileName + extension);
1185
1186                //  Since we have changed the name from what the user input, make sure the newly named file
1187                //  doesn't already exist.
1188                if (theFile.exists()) {
1189                    // Build up a message to show the user.
1190                    String msg = MessageFormat.format(existsErrFmtMsg, fileName);
1191
1192                    //  Ask the user if they want to replace the existing file.
1193                    int result = JOptionPane.showConfirmDialog(parent, msg, dlgTitle,
1194                            JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
1195                    if (result != JOptionPane.YES_OPTION)
1196                        // The user does not want to overwrite the existing file.
1197                        theFile = null;
1198                }
1199            }
1200        }
1201
1202        return theFile;
1203    }
1204}