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.*;
017import java.awt.event.ActionEvent;
018import java.awt.event.ActionListener;
019import java.awt.event.WindowAdapter;
020import java.awt.event.WindowEvent;
021import java.io.FilenameFilter;
022import java.io.IOException;
023import java.net.URI;
024import java.util.*;
025import java.util.List;
026import static java.util.Objects.*;
027import javax.swing.*;
028
029
030/**
031 * This class defines a MacOS friendly application instance. It is a replacement for
032 * Steve Roy's excellent MRJAdapter package of old that adds Window Menu support and other
033 * features used by Multi-Document-Interface (MDI) applications. Under MacOS, an
034 * application instance will automatically call "handleNew()" method if the user brings
035 * the application forward and there are no open window's registered. On non-Mac
036 * platforms, the application will quit once the last document window has been closed.
037 *
038 * <p> Modified by: Joseph A. Huwaldt </p>
039 *
040 * @author Joseph A. Huwaldt, Date: April 25, 2004
041 * @version February 22, 2025
042 */
043public class MDIApplication {
044
045    /**
046     * The application instance.
047     */
048    private static MDIApplication instance;
049    
050        /**
051         * The name of the application.
052         */
053        private String name;
054    
055    /**
056     * A reference to the AWT Desktop.
057     */
058    private static Desktop desktop;
059    
060    static {
061        if (Desktop.isDesktopSupported())
062            desktop = Desktop.getDesktop();
063    }
064    
065        /**
066         * The frameless menu bar that we have set.
067         */
068        private static JMenuBar framelessMenuBar;
069    
070    /**
071     * A list of all open document windows.
072     */
073    private static final Stack<Window> openWindows = new Stack();
074
075    /**
076     * A list of references to JMenu objects used to hold "Windows" menus.
077     */
078    private static final ArrayList<JMenu> windowsMenus = new ArrayList();
079
080    /**
081     * A list of objects that want to be notified if the application is going to quit.
082     */
083    private static final List<QuitListener> quitList = new ArrayList();
084
085    /**
086     * The resource bundle for this application.
087     */
088    private static ResourceBundle resBundle = null;
089
090    /**
091     * The user preferences for this application.
092     */
093    private static Preferences prefs = null;
094
095    /**
096     * The filename filter for this application.
097     */
098    private static FilenameFilter fnFilter = null;
099
100    /**
101     * Flag indicating if the application should quit when the last window is closed.
102     */
103    private static boolean quitOnClose = !AppUtilities.isMacOS();
104    
105    private QuitJMenuItem quitJMenuItem = null;
106    private AboutJMenuItem aboutJMenuItem = null;
107    private PreferencesJMenuItem prefsJMenuItem = null;
108    
109    private static AboutHandler aboutHdlr = null;
110    private static PreferencesHandler prefsHdlr = null;
111    
112
113    //-------------------------------------------------------------------------
114    /**
115     * Constructor a new MDIApplication instance with no name. Note that only one can ever
116     * be created. Attempting to instantiate more will result in an
117     * <code>IllegalStateException</code> being thrown.
118     */
119    protected MDIApplication(String name) {
120        if (nonNull(instance))
121            throw new IllegalStateException();
122        instance = this;
123        
124        // Set the application name.
125        if (nonNull(name))
126                setName(name);
127        
128        //  Handle "Application ReOpen Event" by bringing a window forward or by creating a
129        //  new blank document window if there are no currently open windows.
130        desktop.addAppEventListener(new AppReopenedListener() {
131            @Override
132            public void appReopened(AppReopenedEvent e) {
133                // If a window exists, bring it to the front, otherwise create a new document window.
134                if (!openWindows.empty()) {
135                    Window window = openWindows.peek();
136                    window.setVisible(true);
137
138                } else {
139                    //  Create a new blank document window for this application.
140                    handleNew(null);
141                }
142            }
143        });
144
145    }
146
147        /**
148         * Get the unique application instance. If it doesn't exist,
149         * this method creates it.
150         * @return the unique application instance
151         */
152        public static synchronized MDIApplication getInstance() {
153                if (isNull(instance))
154                        new MDIApplication(null);
155                return instance;
156        }
157        
158     //-------------------------------------------------------------------------
159        /**
160         * Set the name of the application.
161         * @param appName the name of the application
162         */
163        public final void setName(String appName) {
164                name = appName;
165        }
166        
167        /**
168         * Get the name of the application.
169         * @return the name of the application
170         */
171        public String getName() {
172                if (isNull(name))
173                        name = "";
174                return name;
175        }
176        
177        /**
178         * Set the Swing frameless menu bar. This menu bar is shown when
179         * no frame is visible. This state is normal for a Mac application
180         * Note that this method won't have any visual effect if the application
181     * is running on platforms other than MacOS. This method sets
182         * the menu bar internally so that it will be properly returned by
183         * a subsequent call to <code>getFramelessMenuBar()</code>.
184         *
185         * @param menuBar The Swing menu bar to use as frameless menu bar.
186         */
187        public static void setFramelessJMenuBar(JMenuBar menuBar) {
188                if (Desktop.isDesktopSupported() && desktop.isSupported(Desktop.Action.APP_MENU_BAR)) {
189            desktop.setDefaultMenuBar(menuBar);
190        }
191        framelessMenuBar = menuBar;
192        }
193
194        /**
195         * Get the Swing frameless menu bar. This method is functional on
196         * all platforms.
197         *
198         * @return the Swing frameless menu bar or null if it hasn't been set.
199         */
200        public static JMenuBar getFramelessJMenuBar() {
201                return framelessMenuBar;
202        }
203
204    /**
205     * Used to set the resource bundle for this application. This is provided as a
206     * convenience for storing the resource bundle so that it is accessible throughout an
207     * application.
208     *
209     * @param bundle The resource bundle to be stored with this application.
210     */
211    public static void setResourceBundle(ResourceBundle bundle) {
212        resBundle = bundle;
213    }
214
215    /**
216     * Returns the resource bundle stored with this application. If no resource bundle has
217     * been stored, then null is returned.
218     *
219     * @return The resource bundle stored with this application.
220     */
221    public ResourceBundle getResourceBundle() {
222        return resBundle;
223    }
224
225    /**
226     * Used to set the user preferences for this application. This is provided as a
227     * convenience for storing the preferences so that it is accessible throughout an
228     * application.
229     *
230     * @param appPrefs The user preferences for this application.
231     */
232    public static final void setPreferences(Preferences appPrefs) {
233        prefs = appPrefs;
234    }
235
236    /**
237     * @return a reference to the user preferences for this application.
238     */
239    public final Preferences getPreferences() {
240        return prefs;
241    }
242
243    /**
244     * Installs a handler to show a custom Preference window for your application.
245     *
246     * @param preferencesHandler The handler to respond to the
247     *                     PreferencesHandler.handlePreferences(PreferencesEvent) message.
248     * @throws SecurityException if a security manager exists and it denies the
249     * RuntimePermission("canProcessApplicationEvents") permission
250     * @throws UnsupportedOperationException if the current platform does not support the
251     * Desktop.Action.APP_ABOUT action.
252     */
253    public static void setPreferencesHandler(PreferencesHandler preferencesHandler) {
254        prefsHdlr = preferencesHandler;
255        if (Desktop.isDesktopSupported() && desktop.isSupported(Desktop.Action.APP_ABOUT))
256            desktop.setPreferencesHandler(prefsHdlr);
257    }
258    
259        /**
260         * Get the About item as a Swing menu item. This item will automatically have the correct
261     * text and will have an ActionListener registered that will call the registered AboutHandler
262     * if the ActionEvent is triggered.
263     * 
264         * @return the About Swing menu item
265         */
266        public PreferencesJMenuItem getPreferencesJMenuItem() {
267                return getPreferencesJMenuItem("Preferences");
268        }
269        
270        /**
271         * Get the About item as a Swing menu item. This item will automatically have the correct
272     * text and will have an ActionListener registered that will call the registered AboutHandler
273     * if the ActionEvent is triggered.
274     * 
275     * @param prefsLabel the label for the Preferences menu item.
276         * @return the About Swing menu item
277         */
278        public PreferencesJMenuItem getPreferencesJMenuItem(String prefsLabel) {
279                if (isNull(prefsJMenuItem)) {
280                        prefsJMenuItem = new PreferencesJMenuItem(prefsLabel);
281            prefsJMenuItem.addActionListener(new ActionListener() {
282                @Override
283                public void actionPerformed(ActionEvent e) {
284                    if (nonNull(prefsHdlr)) {
285                        PreferencesEvent pe = new PreferencesEvent();
286                        prefsHdlr.handlePreferences(pe);
287                    }
288                }
289            });
290        }
291                return prefsJMenuItem;
292        }
293        
294    /**
295     * Used to set the filename filter for this application. This is provided as a
296     * convenience for storing the filename filter so that it is accessible throughout an
297     * application.
298     *
299     * @param filter The filename filter to be stored with this application.
300     */
301    public static void setFilenameFilter(FilenameFilter filter) {
302        fnFilter = filter;
303    }
304
305    /**
306     * Return a reference to this application's default file name filter or null if a
307     * filename filter has not been stored.
308     *
309     * @return The default filename filter for this application.
310     */
311    public static FilenameFilter getFilenameFilter() {
312        return fnFilter;
313    }
314
315    /**
316     * Sets a flag indicating if the application should quit when the last window is
317     * closed (true) or stay open (false; allowing the user to select "New" from the file
318     * menu for instance). The default value is true except on MacOS X where the default
319     * value if false.
320     *
321     * @param flag Set to true to have the application automatically quit when all the
322     *             windows close.
323     */
324    public static void setQuitOnClose(boolean flag) {
325        quitOnClose = flag;
326    }
327
328    /**
329     * Returns a flag indicating if the application should quit when the last window is
330     * closed (true) or stay open (false; allowing the user to select "New" from the file
331     * menu for instance). The default value is true except on MacOS X where the default
332     * value if false.
333     *
334     * @return A flag indicating if the application should quit when the last window is
335     *         closed.
336     */
337    public static boolean getQuitOnClose() {
338        return quitOnClose;
339    }
340
341        /**
342         * Get the Quit item as a Swing menu item. This item will automatically have the correct
343     * text and shortcut key for the platform and will have an ActionListener registered
344     * that will call System.exit(0) if the ActionEvent is triggered.
345     * 
346         * @return the Quit Swing menu item
347         */
348        public QuitJMenuItem getQuitJMenuItem() {
349                return getQuitJMenuItem("Exit");
350        }
351        
352        /**
353         * Get the Quit item as a Swing menu item. This item will automatically have the correct
354     * text and shortcut key for the platform and will have an ActionListener registered
355     * that will call System.exit(0) if the ActionEvent is triggered.
356     * 
357     * @param exitLabel The label for the quit menu on non-MacOS platforms.
358         * @return the Quit Swing menu item
359         */
360        public QuitJMenuItem getQuitJMenuItem(String exitLabel) {
361                if (isNull(quitJMenuItem)) {
362                        quitJMenuItem = new QuitJMenuItem(this, exitLabel);
363            quitJMenuItem.addActionListener(new ActionListener() {
364                @Override
365                public void actionPerformed(ActionEvent e) {
366                    //  Ask Java to quit if the user selects this menu item.
367                    //  The quit handler (if one is registered) will be called automatically.
368                    System.exit(0);
369                }
370            });
371        }
372                return quitJMenuItem;
373        }
374        
375    /**
376     * Register the supplied window with the list of windows managed by this
377     * MDIApplication. When the window is made visible, it will be added to the "Windows"
378     * menu. The window will be removed from the "Windows" menu when it is closed.
379     *
380     * @param window The window to be added to the list of open windows.
381     */
382    public static void addWindow(Window window) {
383        if (openWindows.contains(window))
384            return;   //  Don't add a window twice.
385        
386        //  Add a window event listener in order to keep the open windows list and menus up to date.
387        window.addWindowListener(new WindowAdapter() {
388            @Override
389            public void windowClosed(WindowEvent e) {
390                Window aWindow = e.getWindow();
391                int idx = openWindows.indexOf(aWindow);
392                if (idx >= 0) {
393                    removeMenuItem(openWindows.size() - idx - 1);
394                    openWindows.remove(idx);
395                }
396
397                //  If requested, quit if there are no open windows.
398                if (quitOnClose && openWindows.isEmpty())
399                    handleQuit(null,null);
400            }
401
402            @Override
403            public void windowActivated(WindowEvent e) {
404                Window aWindow = e.getWindow();
405                if (aWindow.isVisible()) {
406                    int idx = openWindows.indexOf(aWindow);
407                    if (idx >= 0) {
408                        //  Change the position of the window in the Windows menu.
409                        removeMenuItem(openWindows.size() - idx - 1);
410                        openWindows.remove(idx);
411                        openWindows.push(aWindow);
412                        addNewMenuItem(aWindow);
413                        
414                    } else {
415                        //  Add a new item to the Windows menu.
416                        openWindows.push(aWindow);
417                        addNewMenuItem(aWindow);
418                    }
419                }
420            }
421        });
422
423    }
424
425    /**
426     * Register an object to receive notification that the application is going to quit
427     * soon.
428     *
429     * @param listener The listener to register.
430     */
431    public static void addQuitListener(QuitListener listener) {
432        quitList.add(listener);
433    }
434
435    /**
436     * Method to remove a quit listener from the list of quit listeners for this
437     * application.
438     *
439     * @param listener The listener to remove.
440     */
441    public static void removeQuitListener(QuitListener listener) {
442        quitList.remove(listener);
443    }
444
445    /**
446     * Installs the handler which determines if the application should quit. The handler
447     * is passed a one-shot QuitResponse which can cancel or proceed with the quit.
448     * Setting the handler to null causes all quit requests to directly perform the
449     * default QuitStrategy.
450     *
451     * @param quitHandler The handler which determines if the application should quit.
452     */
453    public static void setQuitHandler(QuitHandler quitHandler) {
454        if (Desktop.isDesktopSupported() && desktop.isSupported(Desktop.Action.APP_QUIT_HANDLER))
455            desktop.setQuitHandler(quitHandler);
456    }
457
458    /**
459     * Launches the default browser to display a URI. If the default browser is not able
460     * to handle the specified URI, the application registered for handling URIs of the
461     * specified type is invoked. The application is determined from the protocol and path
462     * of the URI, as defined by the URI class.
463     *
464     * @param uri The URI to be displayed in the user default browser.
465     * @throws IOException If the supplied URI can not be displayed.
466     */
467    public static void browse(URI uri) throws IOException {
468        if (Desktop.isDesktopSupported() && desktop.isSupported(Desktop.Action.BROWSE))
469            desktop.browse(uri);
470    }
471    
472    /**
473     * Installs a handler to show a custom About window for your application.
474     *
475     * @param aboutHandler the handler to respond to the
476     *                     AboutHandler.handleAbout(AboutEvent) message
477     * @throws SecurityException if a security manager exists and it denies the
478     * RuntimePermission("canProcessApplicationEvents") permission
479     * @throws UnsupportedOperationException if the current platform does not support the
480     * Desktop.Action.APP_ABOUT action.
481     */
482    public static void setAboutHandler(AboutHandler aboutHandler) {
483        aboutHdlr = aboutHandler;
484        if (Desktop.isDesktopSupported() && desktop.isSupported(Desktop.Action.APP_ABOUT))
485            desktop.setAboutHandler(aboutHandler);
486    }
487    
488        /**
489         * Get the About item as a Swing menu item. This item will automatically have the correct
490     * text and will have an ActionListener registered that will call the registered AboutHandler
491     * if the ActionEvent is triggered.
492     * 
493         * @return the About Swing menu item
494         */
495        public AboutJMenuItem getAboutJMenuItem() {
496                if (isNull(aboutJMenuItem)) {
497                        aboutJMenuItem = new AboutJMenuItem(this);
498            aboutJMenuItem.addActionListener(new ActionListener() {
499                @Override
500                public void actionPerformed(ActionEvent e) {
501                    if (nonNull(aboutHdlr)) {
502                        AboutEvent ae = new AboutEvent();
503                        aboutHdlr.handleAbout(ae);
504                    }
505                }
506            });
507        }
508                return aboutJMenuItem;
509        }
510        
511    /**
512     * Installs the handler which is notified when the application is asked to open a list
513     * of files.
514     *
515     * @param openFilesHandler handler
516     * @throws SecurityException if a security manager exists and its
517     * SecurityManager.checkRead(java.lang.String) method denies read access to the files,
518     * or it denies the RuntimePermission("canProcessApplicationEvents") permission, or
519     * the calling thread is not allowed to create a subprocess.
520     * @throws UnsupportedOperationException if the current platform does not support the
521     * Desktop.Action.APP_OPEN_FILE action
522     */
523    public static void setOpenFileHandler(OpenFilesHandler openFilesHandler) throws SecurityException, UnsupportedOperationException {
524        if (Desktop.isDesktopSupported() && desktop.isSupported(Desktop.Action.APP_OPEN_FILE))
525            desktop.setOpenFileHandler(openFilesHandler);
526    }
527    
528    /**
529     * Returns a reference to the top-most window in the list of all open windows
530     * registered with this application. If there are no open windows, null is returned.
531     *
532     * @return The top-most window in the list of all open windows.
533     */
534    public static Window getTopWindow() {
535        Window window = null;
536        if (!openWindows.empty())
537            window = openWindows.peek();
538        return window;
539    }
540
541    /**
542     * Get an unmodifiable list of all the currently open windows in the application.
543     *
544     * @return An unmodifiable list of all the currently open windows.
545     */
546    public static List<Window> allOpenWindows() {
547        return Collections.unmodifiableList(openWindows);
548    }
549
550    /**
551     * Get a new JMenu instance that can be used as a Windows menu using the specified
552     * title (typically "Windows"). The menu that is returned is maintained by this class
553     * and should not be modified by the user.
554     *
555     * @param title The title for the Windows menu.
556     * @return A JMenu that represents the application's Windows menu.
557     */
558    public static JMenu newWindowsMenu(String title) {
559        JMenu menu = new JMenu(title);
560
561        //  Add the new menu to our list of Windows menus.
562        windowsMenus.add(menu);
563
564        createNewMenuItems(menu);
565
566        return menu;
567    }
568
569    /**
570     * Call this method when a window being shown in a Windows menu has changed it's
571     * title. This method will update the titles shown in the Windows menu.
572     *
573     * @param window The window that has changed titles.
574     */
575    public static void windowTitleChanged(Window window) {
576        //  Find the window in the list of open windows.
577        int idx = openWindows.indexOf(window);
578        if (idx < 0)
579            return;
580
581        //  Adjust the index to be top down instead of bottom up.
582        idx = openWindows.size() - idx - 1;
583
584        if (idx >= 0 && !windowsMenus.isEmpty()) {
585            //  Get the new title.
586            String title = getWindowTitle(window);
587
588            //  Loop over all the Windows Menus in existence.
589            for (JMenu menu : windowsMenus) {
590                if (menu != null) {
591                    JMenuItem item = menu.getItem(idx);
592                    item.setText(title);
593                }
594            }
595        }
596    }
597
598    /**
599     * Return the title of the given window (assuming it is either a Frame or a Dialog).
600     * If it is actually a Window instance, this returns the title of the parent Frame.
601     *
602     * @param window The window to return the title for.
603     */
604    private static String getWindowTitle(Window window) {
605        String title;
606        if (window instanceof Frame)
607            title = ((Frame)window).getTitle();
608        else if (window instanceof Dialog)
609            title = ((Dialog)window).getTitle();
610        else {
611            Frame frame = AppUtilities.getFrameForComponent(window);
612            title = frame.getTitle();
613        }
614        return title;
615    }
616
617    /**
618     * Method that adds a menu item to the specified menu for each open window.
619     */
620    private static void createNewMenuItems(JMenu menu) {
621        for (int i = openWindows.size() - 1; i >= 0; --i) {
622            Window window = openWindows.get(i);
623            JMenuItem item = new JMenuItem(getWindowTitle(window));
624            item.addActionListener(new MenuListener(window));
625            menu.add(item);
626        }
627    }
628
629    /**
630     * Method that adds a single new menu item to the top of ALL the Windows menus.
631     */
632    private static void addNewMenuItem(Window window) {
633        if (!windowsMenus.isEmpty()) {
634
635            String title = getWindowTitle(window);
636            for (Iterator<JMenu> i = windowsMenus.iterator(); i.hasNext();) {
637                JMenu menu = i.next();
638
639                if (menu != null) {
640                    JMenuItem item = new JMenuItem(title);
641                    item.addActionListener(new MenuListener(window));
642                    menu.add(item, 0);
643                } else {
644                    i.remove();
645                }
646            }
647
648        }
649    }
650
651    /**
652     * Method that removes a single menu item from ALL of the Windows menus.
653     */
654    private static void removeMenuItem(int idx) {
655        if (!windowsMenus.isEmpty()) {
656
657            for (Iterator<JMenu> i = windowsMenus.iterator(); i.hasNext();) {
658                JMenu menu = i.next();
659                if (menu != null) {
660                    menu.remove(idx);
661                } else {
662                    i.remove();
663                }
664            }
665
666        }
667    }
668
669    /**
670     * Class that provides an action listener for Window menu items. When this listener is
671     * called, it brings it's window to the front.
672     */
673    private static class MenuListener implements ActionListener {
674
675        private final Window theWindow;
676
677        MenuListener(Window window) {
678            theWindow = window;
679        }
680
681        @Override
682        public void actionPerformed(ActionEvent event) {
683            theWindow.setVisible(true);
684        }
685    }
686
687    /**
688     * Handle the user requesting a new document window. If it is inappropriate for the
689     * application to create a new document window, then this method should do nothing.
690     * The default implementation does nothing and returns null.
691     *
692     * @param event The event that caused this method to be called. May be "null" if this
693     *              method is called by MDIApplication or one of it's subclasses.
694     * @return A reference to the newly created window frame or null if a window was not
695     *         created.
696     */
697    public Frame handleNew(ActionEvent event) {
698        return null;
699    }
700
701    /**
702     * Handle the user choosing "Close" from the File menu. This implementation dispatches
703     * a "Window Closing" event on the top most window.
704     *
705     * @param event The event that caused this method to be called.
706     */
707    public static void handleClose(ActionEvent event) {
708        if (!openWindows.isEmpty()) {
709            Window window = openWindows.peek();
710            window.dispatchEvent(new WindowEvent(window, WindowEvent.WINDOW_CLOSING));
711        }
712    }
713
714    /**
715     * Handle the user choosing "Quit" from the File menu. This implementation dispatches a
716     * separate "window closing" event to each window. This gives each window an
717     * opportunity to cancel the quit process.
718     *
719     * @param event The event that caused this method to be called or null if none.
720     * @param response the one-shot response object used to cancel or proceed with the quit action or null if none.
721     * @return True if the application can proceed with the quit or false if it shoudl be canceled.
722     */
723    public static synchronized boolean handleQuit(QuitEvent event, QuitResponse response) {
724
725        boolean quitFlag = true;
726        if (!quitList.isEmpty()) {
727    
728            // Loop over all the quit listeners, notifying them one at a time.
729            // When the last notification is made.  The application will quit.
730            QuitListener[] array = new QuitListener[quitList.size()];
731            quitList.toArray(array);
732            for (QuitListener next : array) {
733
734                //  Tell the listener that we are going to quit.
735                boolean cancel = next.quit();
736                if (cancel) {
737                    quitFlag = false;
738                    break;
739                }
740            }
741        }
742        
743        if (nonNull(response)) {
744            if (quitFlag)
745                response.performQuit();
746            else
747                response.cancelQuit();
748        }
749        
750        return quitFlag;
751    }
752
753}