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