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}