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