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 December 24, 2023 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 * @param menuBar the Swing menu bar to use as frameless menu bar 185 */ 186 public static void setFramelessJMenuBar(JMenuBar menuBar) { 187 if (Desktop.isDesktopSupported() && desktop.isSupported(Desktop.Action.APP_MENU_BAR)) { 188 desktop.setDefaultMenuBar(menuBar); 189 } 190 framelessMenuBar = menuBar; 191 } 192 193 /** 194 * Get the Swing frameless menu bar. This method is functional on 195 * all platforms. 196 * @return the Swing frameless menu bar or null if it hasn't been set. 197 */ 198 public static JMenuBar getFramelessJMenuBar() { 199 return framelessMenuBar; 200 } 201 202 /** 203 * Used to set the resource bundle for this application. This is provided as a 204 * convenience for storing the resource bundle so that it is accessible throughout an 205 * application. 206 * 207 * @param bundle The resource bundle to be stored with this application. 208 */ 209 public static void setResourceBundle(ResourceBundle bundle) { 210 resBundle = bundle; 211 } 212 213 /** 214 * Returns the resource bundle stored with this application. If no resource bundle has 215 * been stored, then null is returned. 216 * 217 * @return The resource bundle stored with this application. 218 */ 219 public ResourceBundle getResourceBundle() { 220 return resBundle; 221 } 222 223 /** 224 * Used to set the user preferences for this application. This is provided as a 225 * convenience for storing the preferences so that it is accessible throughout an 226 * application. 227 * 228 * @param appPrefs The user preferences for this application. 229 */ 230 public static final void setPreferences(Preferences appPrefs) { 231 prefs = appPrefs; 232 } 233 234 /** 235 * @return a reference to the user preferences for this application. 236 */ 237 public final Preferences getPreferences() { 238 return prefs; 239 } 240 241 /** 242 * Installs a handler to show a custom Preference window for your application. 243 * 244 * @param prefsHandler the handler to respond to the 245 * PreferencesHandler.handlePreferences(PreferencesEvent) message 246 * @throws SecurityException if a security manager exists and it denies the 247 * RuntimePermission("canProcessApplicationEvents") permission 248 * @throws UnsupportedOperationException if the current platform does not support the 249 * Desktop.Action.APP_ABOUT action. 250 */ 251 public static void setPreferencesHandler(PreferencesHandler preferencesHandler) { 252 prefsHdlr = preferencesHandler; 253 if (Desktop.isDesktopSupported() && desktop.isSupported(Desktop.Action.APP_ABOUT)) 254 desktop.setPreferencesHandler(prefsHdlr); 255 } 256 257 /** 258 * Get the About item as a Swing menu item. This item will automatically have the correct 259 * text and will have an ActionListener registered that will call the registered AboutHandler 260 * if the ActionEvent is triggered. 261 * 262 * @return the About Swing menu item 263 */ 264 public PreferencesJMenuItem getPreferencesJMenuItem() { 265 return getPreferencesJMenuItem("Preferences"); 266 } 267 268 /** 269 * Get the About item as a Swing menu item. This item will automatically have the correct 270 * text and will have an ActionListener registered that will call the registered AboutHandler 271 * if the ActionEvent is triggered. 272 * 273 * @param prefsLabel the label for the Preferences menu item. 274 * @return the About Swing menu item 275 */ 276 public PreferencesJMenuItem getPreferencesJMenuItem(String prefsLabel) { 277 if (isNull(prefsJMenuItem)) { 278 prefsJMenuItem = new PreferencesJMenuItem(prefsLabel); 279 prefsJMenuItem.addActionListener(new ActionListener() { 280 @Override 281 public void actionPerformed(ActionEvent e) { 282 if (nonNull(prefsHdlr)) { 283 PreferencesEvent pe = new PreferencesEvent(); 284 prefsHdlr.handlePreferences(pe); 285 } 286 } 287 }); 288 } 289 return prefsJMenuItem; 290 } 291 292 /** 293 * Used to set the filename filter for this application. This is provided as a 294 * convenience for storing the filename filter so that it is accessible throughout an 295 * application. 296 * 297 * @param filter The filename filter to be stored with this application. 298 */ 299 public static void setFilenameFilter(FilenameFilter filter) { 300 fnFilter = filter; 301 } 302 303 /** 304 * Return a reference to this application's default file name filter or null if a 305 * filename filter has not been stored. 306 * 307 * @return The default filename filter for this application. 308 */ 309 public static FilenameFilter getFilenameFilter() { 310 return fnFilter; 311 } 312 313 /** 314 * Sets a flag indicating if the application should quit when the last window is 315 * closed (true) or stay open (false; allowing the user to select "New" from the file 316 * menu for instance). The default value is true except on MacOS X where the default 317 * value if false. 318 * 319 * @param flag Set to true to have the application automatically quit when all the 320 * windows close. 321 */ 322 public static void setQuitOnClose(boolean flag) { 323 quitOnClose = flag; 324 } 325 326 /** 327 * Returns a flag indicating if the application should quit when the last window is 328 * closed (true) or stay open (false; allowing the user to select "New" from the file 329 * menu for instance). The default value is true except on MacOS X where the default 330 * value if false. 331 * 332 * @return A flag indicating if the application should quit when the last window is 333 * closed. 334 */ 335 public static boolean getQuitOnClose() { 336 return quitOnClose; 337 } 338 339 /** 340 * Get the Quit item as a Swing menu item. This item will automatically have the correct 341 * text and shortcut key for the platform and will have an ActionListener registered 342 * that will call System.exit(0) if the ActionEvent is triggered. 343 * 344 * @return the Quit Swing menu item 345 */ 346 public QuitJMenuItem getQuitJMenuItem() { 347 return getQuitJMenuItem("Exit"); 348 } 349 350 /** 351 * Get the Quit item as a Swing menu item. This item will automatically have the correct 352 * text and shortcut key for the platform and will have an ActionListener registered 353 * that will call System.exit(0) if the ActionEvent is triggered. 354 * 355 * @param exitLabel The label for the quit menu on non-MacOS platforms. 356 * @return the Quit Swing menu item 357 */ 358 public QuitJMenuItem getQuitJMenuItem(String exitLabel) { 359 if (isNull(quitJMenuItem)) { 360 quitJMenuItem = new QuitJMenuItem(this, exitLabel); 361 quitJMenuItem.addActionListener(new ActionListener() { 362 @Override 363 public void actionPerformed(ActionEvent e) { 364 // Ask Java to quit if the user selects this menu item. 365 // The quit handler (if one is registered) will be called automatically. 366 System.exit(0); 367 } 368 }); 369 } 370 return quitJMenuItem; 371 } 372 373 /** 374 * Register the supplied window with the list of windows managed by this 375 * MDIApplication. When the window is made visible, it will be added to the "Windows" 376 * menu. The window will be removed from the "Windows" menu when it is closed. 377 * 378 * @param window The window to be added to the list of open windows. 379 */ 380 public static void addWindow(Window window) { 381 if (openWindows.contains(window)) 382 return; // Don't add a window twice. 383 384 // Add a window event listener in order to keep the open windows list and menus up to date. 385 window.addWindowListener(new WindowAdapter() { 386 @Override 387 public void windowClosed(WindowEvent e) { 388 Window aWindow = e.getWindow(); 389 int idx = openWindows.indexOf(aWindow); 390 if (idx >= 0) { 391 removeMenuItem(openWindows.size() - idx - 1); 392 openWindows.remove(idx); 393 } 394 395 // If requested, quit if there are no open windows. 396 if (quitOnClose && openWindows.isEmpty()) 397 handleQuit(null,null); 398 } 399 400 @Override 401 public void windowActivated(WindowEvent e) { 402 Window aWindow = e.getWindow(); 403 if (aWindow.isVisible()) { 404 int idx = openWindows.indexOf(aWindow); 405 if (idx >= 0) { 406 // Change the position of the window in the Windows menu. 407 removeMenuItem(openWindows.size() - idx - 1); 408 openWindows.remove(idx); 409 openWindows.push(aWindow); 410 addNewMenuItem(aWindow); 411 412 } else { 413 // Add a new item to the Windows menu. 414 openWindows.push(aWindow); 415 addNewMenuItem(aWindow); 416 } 417 } 418 } 419 }); 420 421 } 422 423 /** 424 * Register an object to receive notification that the application is going to quit 425 * soon. 426 * 427 * @param listener The listener to register. 428 */ 429 public static void addQuitListener(QuitListener listener) { 430 quitList.add(listener); 431 } 432 433 /** 434 * Method to remove a quit listener from the list of quit listeners for this 435 * application. 436 * 437 * @param listener The listener to remove. 438 */ 439 public static void removeQuitListener(QuitListener listener) { 440 quitList.remove(listener); 441 } 442 443 /** 444 * Installs the handler which determines if the application should quit. The handler 445 * is passed a one-shot QuitResponse which can cancel or proceed with the quit. 446 * Setting the handler to null causes all quit requests to directly perform the 447 * default QuitStrategy. 448 * 449 * @param listener The listener to register. 450 */ 451 public static void setQuitHandler​(QuitHandler quitHandler) { 452 if (Desktop.isDesktopSupported() && desktop.isSupported(Desktop.Action.APP_QUIT_HANDLER)) 453 desktop.setQuitHandler(quitHandler); 454 } 455 456 /** 457 * Launches the default browser to display a URI. If the default browser is not able 458 * to handle the specified URI, the application registered for handling URIs of the 459 * specified type is invoked. The application is determined from the protocol and path 460 * of the URI, as defined by the URI class. 461 * 462 * @param uri the URI to be displayed in the user default browser. 463 */ 464 public static void browse(URI uri) throws IOException { 465 if (Desktop.isDesktopSupported() && desktop.isSupported(Desktop.Action.BROWSE)) 466 desktop.browse(uri); 467 } 468 469 /** 470 * Installs a handler to show a custom About window for your application. 471 * 472 * @param aboutHandler the handler to respond to the 473 * AboutHandler.handleAbout(AboutEvent) message 474 * @throws SecurityException if a security manager exists and it denies the 475 * RuntimePermission("canProcessApplicationEvents") permission 476 * @throws UnsupportedOperationException if the current platform does not support the 477 * Desktop.Action.APP_ABOUT action. 478 */ 479 public static void setAboutHandler(AboutHandler aboutHandler) { 480 aboutHdlr = aboutHandler; 481 if (Desktop.isDesktopSupported() && desktop.isSupported(Desktop.Action.APP_ABOUT)) 482 desktop.setAboutHandler(aboutHandler); 483 } 484 485 /** 486 * Get the About item as a Swing menu item. This item will automatically have the correct 487 * text and will have an ActionListener registered that will call the registered AboutHandler 488 * if the ActionEvent is triggered. 489 * 490 * @return the About Swing menu item 491 */ 492 public AboutJMenuItem getAboutJMenuItem() { 493 if (isNull(aboutJMenuItem)) { 494 aboutJMenuItem = new AboutJMenuItem(this); 495 aboutJMenuItem.addActionListener(new ActionListener() { 496 @Override 497 public void actionPerformed(ActionEvent e) { 498 if (nonNull(aboutHdlr)) { 499 AboutEvent ae = new AboutEvent(); 500 aboutHdlr.handleAbout(ae); 501 } 502 } 503 }); 504 } 505 return aboutJMenuItem; 506 } 507 508 /** 509 * Installs the handler which is notified when the application is asked to open a list 510 * of files. 511 * 512 * @param openFilesHandler handler 513 * @throws SecurityException if a security manager exists and its 514 * SecurityManager.checkRead(java.lang.String) method denies read access to the files, 515 * or it denies the RuntimePermission("canProcessApplicationEvents") permission, or 516 * the calling thread is not allowed to create a subprocess. 517 * @throws UnsupportedOperationException if the current platform does not support the 518 * Desktop.Action.APP_OPEN_FILE action 519 */ 520 public static void setOpenFileHandler(OpenFilesHandler openFilesHandler) throws SecurityException, UnsupportedOperationException { 521 if (Desktop.isDesktopSupported() && desktop.isSupported(Desktop.Action.APP_OPEN_FILE)) 522 desktop.setOpenFileHandler(openFilesHandler); 523 } 524 525 /** 526 * Returns a reference to the top-most window in the list of all open windows 527 * registered with this application. If there are no open windows, null is returned. 528 * 529 * @return The top-most window in the list of all open windows. 530 */ 531 public static Window getTopWindow() { 532 Window window = null; 533 if (!openWindows.empty()) 534 window = openWindows.peek(); 535 return window; 536 } 537 538 /** 539 * Get an unmodifiable list of all the currently open windows in the application. 540 * 541 * @return An unmodifiable list of all the currently open windows. 542 */ 543 public static List<Window> allOpenWindows() { 544 return Collections.unmodifiableList(openWindows); 545 } 546 547 /** 548 * Get a new JMenu instance that can be used as a Windows menu using the specified 549 * title (typically "Windows"). The menu that is returned is maintained by this class 550 * and should not be modified by the user. 551 * 552 * @param title The title for the Windows menu. 553 * @return A JMenu that represents the application's Windows menu. 554 */ 555 public static JMenu newWindowsMenu(String title) { 556 JMenu menu = new JMenu(title); 557 558 // Add the new menu to our list of Windows menus. 559 windowsMenus.add(menu); 560 561 createNewMenuItems(menu); 562 563 return menu; 564 } 565 566 /** 567 * Call this method when a window being shown in a Windows menu has changed it's 568 * title. This method will update the titles shown in the Windows menu. 569 * 570 * @param window The window that has changed titles. 571 */ 572 public static void windowTitleChanged(Window window) { 573 // Find the window in the list of open windows. 574 int idx = openWindows.indexOf(window); 575 if (idx < 0) 576 return; 577 578 // Adjust the index to be top down instead of bottom up. 579 idx = openWindows.size() - idx - 1; 580 581 if (idx >= 0 && !windowsMenus.isEmpty()) { 582 // Get the new title. 583 String title = getWindowTitle(window); 584 585 // Loop over all the Windows Menus in existence. 586 for (JMenu menu : windowsMenus) { 587 if (menu != null) { 588 JMenuItem item = menu.getItem(idx); 589 item.setText(title); 590 } 591 } 592 } 593 } 594 595 /** 596 * Return the title of the given window (assuming it is either a Frame or a Dialog). 597 * If it is actually a Window instance, this returns the title of the parent Frame. 598 * 599 * @param window The window to return the title for. 600 */ 601 private static String getWindowTitle(Window window) { 602 String title; 603 if (window instanceof Frame) 604 title = ((Frame)window).getTitle(); 605 else if (window instanceof Dialog) 606 title = ((Dialog)window).getTitle(); 607 else { 608 Frame frame = AppUtilities.getFrameForComponent(window); 609 title = frame.getTitle(); 610 } 611 return title; 612 } 613 614 /** 615 * Method that adds a menu item to the specified menu for each open window. 616 */ 617 private static void createNewMenuItems(JMenu menu) { 618 for (int i = openWindows.size() - 1; i >= 0; --i) { 619 Window window = openWindows.get(i); 620 JMenuItem item = new JMenuItem(getWindowTitle(window)); 621 item.addActionListener(new MenuListener(window)); 622 menu.add(item); 623 } 624 } 625 626 /** 627 * Method that adds a single new menu item to the top of ALL the Windows menus. 628 */ 629 private static void addNewMenuItem(Window window) { 630 if (!windowsMenus.isEmpty()) { 631 632 String title = getWindowTitle(window); 633 for (Iterator<JMenu> i = windowsMenus.iterator(); i.hasNext();) { 634 JMenu menu = i.next(); 635 636 if (menu != null) { 637 JMenuItem item = new JMenuItem(title); 638 item.addActionListener(new MenuListener(window)); 639 menu.add(item, 0); 640 } else { 641 i.remove(); 642 } 643 } 644 645 } 646 } 647 648 /** 649 * Method that removes a single menu item from ALL of the Windows menus. 650 */ 651 private static void removeMenuItem(int idx) { 652 if (!windowsMenus.isEmpty()) { 653 654 for (Iterator<JMenu> i = windowsMenus.iterator(); i.hasNext();) { 655 JMenu menu = i.next(); 656 if (menu != null) { 657 menu.remove(idx); 658 } else { 659 i.remove(); 660 } 661 } 662 663 } 664 } 665 666 /** 667 * Class that provides an action listener for Window menu items. When this listener is 668 * called, it brings it's window to the front. 669 */ 670 private static class MenuListener implements ActionListener { 671 672 private final Window theWindow; 673 674 MenuListener(Window window) { 675 theWindow = window; 676 } 677 678 @Override 679 public void actionPerformed(ActionEvent event) { 680 theWindow.setVisible(true); 681 } 682 } 683 684 /** 685 * Handle the user requesting a new document window. If it is inappropriate for the 686 * application to create a new document window, then this method should do nothing. 687 * The default implementation does nothing and returns null. 688 * 689 * @param event The event that caused this method to be called. May be "null" if this 690 * method is called by MDIApplication or one of it's subclasses. 691 * @return A reference to the newly created window frame or null if a window was not 692 * created. 693 */ 694 public Frame handleNew(ActionEvent event) { 695 return null; 696 } 697 698 /** 699 * Handle the user choosing "Close" from the File menu. This implementation dispatches 700 * a "Window Closing" event on the top most window. 701 * 702 * @param event The event that caused this method to be called. 703 */ 704 public static void handleClose(ActionEvent event) { 705 if (!openWindows.isEmpty()) { 706 Window window = openWindows.peek(); 707 window.dispatchEvent(new WindowEvent(window, WindowEvent.WINDOW_CLOSING)); 708 } 709 } 710 711 /** 712 * Handle the user choosing "Quit" from the File menu. This implementation dispatches a 713 * separate "window closing" event to each window. This gives each window an 714 * opportunity to cancel the quit process. 715 * 716 * @param event The event that caused this method to be called or null if none. 717 * @param response the one-shot response object used to cancel or proceed with the quit action or null if none. 718 * @return True if the application can proceed with the quit or false if it shoudl be canceled. 719 */ 720 public static synchronized boolean handleQuit(QuitEvent event, QuitResponse response) { 721 722 boolean quitFlag = true; 723 if (!quitList.isEmpty()) { 724 725 // Loop over all the quit listeners, notifying them one at a time. 726 // When the last notification is made. The application will quit. 727 QuitListener[] array = new QuitListener[quitList.size()]; 728 quitList.toArray(array); 729 for (QuitListener next : array) { 730 731 // Tell the listener that we are going to quit. 732 boolean cancel = next.quit(); 733 if (cancel) { 734 quitFlag = false; 735 break; 736 } 737 } 738 } 739 740 if (nonNull(response)) { 741 if (quitFlag) 742 response.performQuit(); 743 else 744 response.cancelQuit(); 745 } 746 747 return quitFlag; 748 } 749 750}