001package jmri.util; 002 003import java.awt.Dimension; 004import java.awt.Frame; 005import java.awt.GraphicsConfiguration; 006import java.awt.GraphicsDevice; 007import java.awt.GraphicsEnvironment; 008import java.awt.Insets; 009import java.awt.Point; 010import java.awt.Rectangle; 011import java.awt.Toolkit; 012import java.awt.event.ActionEvent; 013import java.awt.event.ComponentListener; 014import java.awt.event.KeyEvent; 015import java.awt.event.WindowListener; 016import java.util.ArrayList; 017import java.util.HashMap; 018import java.util.HashSet; 019import java.util.List; 020import java.util.Set; 021 022import javax.annotation.Nonnull; 023import javax.annotation.OverridingMethodsMustInvokeSuper; 024import javax.swing.AbstractAction; 025import javax.swing.InputMap; 026import javax.swing.JComponent; 027import javax.swing.JFrame; 028import javax.swing.JMenuBar; 029import javax.swing.JRootPane; 030import javax.swing.KeyStroke; 031 032import jmri.InstanceManager; 033import jmri.ShutDownManager; 034import jmri.UserPreferencesManager; 035import jmri.beans.BeanInterface; 036import jmri.beans.BeanUtil; 037import jmri.implementation.AbstractShutDownTask; 038import jmri.util.swing.JmriAbstractAction; 039import jmri.util.swing.JmriJOptionPane; 040import jmri.util.swing.JmriPanel; 041import jmri.util.swing.WindowInterface; 042import jmri.util.swing.sdi.JmriJFrameInterface; 043 044/** 045 * JFrame extended for common JMRI use. 046 * <p> 047 * We needed a place to refactor common JFrame additions in JMRI code, so this 048 * class was created. 049 * <p> 050 * Features: 051 * <ul> 052 * <li>Size limited to the maximum available on the screen, after removing any 053 * menu bars (macOS) and taskbars (Windows) 054 * <li>Cleanup upon closing the frame: When the frame is closed (WindowClosing 055 * event), the {@link #dispose()} method is invoked to do cleanup. This is inherited from 056 * JFrame itself, so super.dispose() needs to be invoked in the over-loading 057 * methods. 058 * <li>Maintains a list of existing JmriJFrames 059 * </ul> 060 * <h2>Window Closing</h2> 061 * Normally, a JMRI window wants to be disposed when it closes. This is what's 062 * needed when each invocation of the corresponding action can create a new copy 063 * of the window. To do this, you don't have to do anything in your subclass. 064 * <p> 065 * If you want this behavior, but need to do something when the window is 066 * closing, override the {@link #windowClosing(java.awt.event.WindowEvent)} 067 * method to do what you want. Also, if you override {@link #dispose()}, make 068 * sure to call super.dispose(). 069 * <p> 070 * If you want the window to just do nothing or just hide, rather than be 071 * disposed, when closed, set the DefaultCloseOperation to DO_NOTHING_ON_CLOSE 072 * or HIDE_ON_CLOSE depending on what you're looking for. 073 * 074 * @author Bob Jacobsen Copyright 2003, 2008, 2023 075 */ 076public class JmriJFrame extends JFrame implements WindowListener, jmri.ModifiedFlag, 077 ComponentListener, WindowInterface, BeanInterface { 078 079 protected boolean allowInFrameServlet = true; 080 081 /** 082 * Creates a JFrame with standard settings, optional save/restore of size 083 * and position. 084 * 085 * @param saveSize Set true to save the last known size 086 * @param savePosition Set true to save the last known location 087 */ 088 public JmriJFrame(boolean saveSize, boolean savePosition) { 089 super(); 090 reuseFrameSavedPosition = savePosition; 091 reuseFrameSavedSized = saveSize; 092 initFrame(); 093 } 094 095 final void initFrame() { 096 addWindowListener(this); 097 addComponentListener(this); 098 windowInterface = new JmriJFrameInterface(); 099 100 /* 101 * This ensures that different jframes do not get placed directly on top of each other, 102 * but are offset. However a saved preferences can override this. 103 */ 104 JmriJFrameManager m = getJmriJFrameManager(); 105 int X_MARGIN = 3; // observed uncertainty in window position, maybe due to roundoff 106 int Y_MARGIN = 3; 107 synchronized (m) { 108 for (JmriJFrame j : m) { 109 if ((j.getExtendedState() != ICONIFIED) && (j.isVisible())) { 110 if ( Math.abs(j.getX() - this.getX()) < X_MARGIN+j.getInsets().left 111 && Math.abs(j.getY() - this.getY()) < Y_MARGIN+j.getInsets().top) { 112 offSetFrameOnScreen(j); 113 } 114 } 115 } 116 117 m.add(this); 118 } 119 // Set the image for use when minimized 120 setIconImage(getToolkit().getImage("resources/jmri32x32.gif")); 121 // set the close short cut 122 setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE); 123 addWindowCloseShortCut(); 124 125 windowFrameRef = this.getClass().getName(); 126 if (!this.getClass().getName().equals(JmriJFrame.class.getName())) { 127 generateWindowRef(); 128 setFrameLocation(); 129 } 130 } 131 132 /** 133 * Creates a JFrame with standard settings, including saving/restoring of 134 * size and position. 135 */ 136 public JmriJFrame() { 137 this(true, true); 138 } 139 140 /** 141 * Creates a JFrame with with given name plus standard settings, including 142 * saving/restoring of size and position. 143 * 144 * @param name Title of the JFrame 145 */ 146 public JmriJFrame(String name) { 147 this(name, true, true); 148 } 149 150 /** 151 * Creates a JFrame with with given name plus standard settings, including 152 * optional save/restore of size and position. 153 * 154 * @param name Title of the JFrame 155 * @param saveSize Set true to save the last knowm size 156 * @param savePosition Set true to save the last known location 157 */ 158 public JmriJFrame(String name, boolean saveSize, boolean savePosition) { 159 this(saveSize, savePosition); 160 setFrameTitle(name); 161 } 162 163 final void setFrameTitle(String name) { 164 setTitle(name); 165 generateWindowRef(); 166 if (this.getClass().getName().equals(JmriJFrame.class.getName())) { 167 if ((this.getTitle() == null) || (this.getTitle().isEmpty())) { 168 return; 169 } 170 } 171 setFrameLocation(); 172 } 173 174 /** 175 * Remove this window from the Windows Menu by removing it from the list of 176 * active JmriJFrames. 177 */ 178 public void makePrivateWindow() { 179 JmriJFrameManager m = getJmriJFrameManager(); 180 synchronized (m) { 181 m.remove(this); 182 } 183 } 184 185 /** 186 * Add this window to the Windows Menu by adding it to the list of 187 * active JmriJFrames. 188 */ 189 public void makePublicWindow() { 190 JmriJFrameManager m = getJmriJFrameManager(); 191 synchronized (m) { 192 if (! m.contains(this)) { 193 m.add(this); 194 } 195 } 196 } 197 198 /** 199 * Reset frame location and size to stored preference value 200 */ 201 public void setFrameLocation() { 202 InstanceManager.getOptionalDefault(UserPreferencesManager.class).ifPresent(prefsMgr -> { 203 if (prefsMgr.hasProperties(windowFrameRef)) { 204 // Track the computed size and position of this window 205 Rectangle window = new Rectangle(this.getX(),this.getY(),this.getWidth(), this.getHeight()); 206 boolean isVisible = false; 207 log.debug("Initial window location & size: {}", window); 208 209 log.debug("Detected {} screens.",GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices().length); 210 log.debug("windowFrameRef: {}", windowFrameRef); 211 if (reuseFrameSavedPosition) { 212 log.debug("setFrameLocation 1st clause sets \"{}\" location to {}", getTitle(), prefsMgr.getWindowLocation(windowFrameRef)); 213 window.setLocation(prefsMgr.getWindowLocation(windowFrameRef)); 214 } 215 // 216 // Simple case that if either height or width are zero, then we should not set them 217 // 218 if ((reuseFrameSavedSized) 219 && (!((prefsMgr.getWindowSize(windowFrameRef).getWidth() == 0.0) || (prefsMgr.getWindowSize( 220 windowFrameRef).getHeight() == 0.0)))) { 221 log.debug("setFrameLocation 2nd clause sets \"{}\" preferredSize to {}", getTitle(), prefsMgr.getWindowSize(windowFrameRef)); 222 this.setPreferredSize(prefsMgr.getWindowSize(windowFrameRef)); 223 log.debug("setFrameLocation 2nd clause sets \"{}\" size to {}", getTitle(), prefsMgr.getWindowSize(windowFrameRef)); 224 window.setSize(prefsMgr.getWindowSize(windowFrameRef)); 225 log.debug("window now set to location: {}", window); 226 } 227 228 // 229 // We just check to make sure that having set the location that we do not have another frame with the same 230 // class name and title in the same location, if it is we offset 231 // 232 for (JmriJFrame j : getJmriJFrameManager()) { 233 if (j.getClass().getName().equals(this.getClass().getName()) && (j.getExtendedState() != ICONIFIED) 234 && (j.isVisible()) && j.getTitle().equals(getTitle())) { 235 if ((j.getX() == this.getX()) && (j.getY() == this.getY())) { 236 log.debug("setFrameLocation 3rd clause calls offSetFrameOnScreen({})", j); 237 offSetFrameOnScreen(j); 238 } 239 } 240 } 241 242 // 243 // Now we loop through all possible displays to determine if this window rectangle would intersect 244 // with any of these screens - in other words, ensure that this frame would be (partially) visible 245 // on at least one of the connected screens 246 // 247 for (ScreenDimensions sd: getScreenDimensions()) { 248 boolean canShow = window.intersects(sd.getBounds()); 249 if (canShow) isVisible = true; 250 log.debug("Screen {} bounds {}, {}", sd.getGraphicsDevice().getIDstring(), sd.getBounds(), sd.getInsets()); 251 log.debug("Does \"{}\" window {} fit on screen {}? {}", getTitle(), window, sd.getGraphicsDevice().getIDstring(), canShow); 252 } 253 254 log.debug("Can \"{}\" window {} display on a screen? {}", getTitle(), window, isVisible); 255 256 // 257 // We've determined that at least one of the connected screens can display this window 258 // so set its location and size based upon previously stored values 259 // 260 if (isVisible) { 261 this.setLocation(window.getLocation()); 262 this.setSize(window.getSize()); 263 log.debug("Set \"{}\" location to {} and size to {}", getTitle(), window.getLocation(), window.getSize()); 264 } 265 } 266 }); 267 } 268 269 private final static ArrayList<ScreenDimensions> screenDim = getInitialScreenDimensionsOnce(); 270 271 /** 272 * returns the previously initialized array of screens. See getScreenDimensionsOnce() 273 * @return ArrayList of screen bounds and insets 274 */ 275 public static ArrayList<ScreenDimensions> getScreenDimensions() { 276 return screenDim; 277 } 278 279 /** 280 * Iterates through the attached displays and retrieves bounds, insets 281 * and id for each screen. 282 * Size of returned ArrayList equals the number of detected displays. 283 * Used to initialize a static final array. 284 * @return ArrayList of screen bounds and insets 285 */ 286 private static ArrayList<ScreenDimensions> getInitialScreenDimensionsOnce() { 287 ArrayList<ScreenDimensions> screenDimensions = new ArrayList<>(); 288 if (GraphicsEnvironment.isHeadless()) { 289 // there are no screens 290 return screenDimensions; 291 } 292 for (GraphicsDevice gd: GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices()) { 293 Rectangle bounds = new Rectangle(); 294 Insets insets = new Insets(0, 0, 0, 0); 295 for (GraphicsConfiguration gc: gd.getConfigurations()) { 296 if (bounds.isEmpty()) { 297 bounds = gc.getBounds(); 298 } else { 299 bounds = bounds.union(gc.getBounds()); 300 } 301 insets = Toolkit.getDefaultToolkit().getScreenInsets(gc); 302 } 303 screenDimensions.add(new ScreenDimensions(bounds, insets, gd)); 304 } 305 return screenDimensions; 306 } 307 308 /** 309 * Represents the dimensions of an attached screen/display 310 */ 311 public static class ScreenDimensions { 312 final Rectangle bounds; 313 final Insets insets; 314 final GraphicsDevice gd; 315 316 public ScreenDimensions(Rectangle bounds, Insets insets, GraphicsDevice gd) { 317 this.bounds = bounds; 318 this.insets = insets; 319 this.gd = gd; 320 } 321 322 public Rectangle getBounds() { 323 return bounds; 324 } 325 326 public Insets getInsets() { 327 return insets; 328 } 329 330 public GraphicsDevice getGraphicsDevice() { 331 return gd; 332 } 333 } 334 335 /** 336 * Regenerates the window frame ref that is used for saving and setting 337 * frame size and position against. 338 */ 339 public void generateWindowRef() { 340 String initref = this.getClass().getName(); 341 if ((this.getTitle() != null) && (!this.getTitle().equals(""))) { 342 if (initref.equals(JmriJFrame.class.getName())) { 343 initref = this.getTitle(); 344 } else { 345 initref = initref + ":" + this.getTitle(); 346 } 347 } 348 349 int refNo = 1; 350 String ref = initref; 351 JmriJFrameManager m = getJmriJFrameManager(); 352 synchronized (m) { 353 for (JmriJFrame j : m) { 354 if (j != this && j.getWindowFrameRef() != null && j.getWindowFrameRef().equals(ref)) { 355 ref = initref + ":" + refNo; 356 refNo++; 357 } 358 } 359 } 360 log.debug("Created windowFrameRef: {}", ref); 361 windowFrameRef = ref; 362 } 363 364 /** {@inheritDoc} */ 365 @Override 366 public void pack() { 367 // work around for Linux, sometimes the stored window size is too small 368 if (this.getPreferredSize().width < 100 || this.getPreferredSize().height < 100) { 369 this.setPreferredSize(null); // try without the preferred size 370 } 371 super.pack(); 372 reSizeToFitOnScreen(); 373 } 374 375 /** 376 * Remove any decoration, such as the title bar or close window control, 377 * from the JFrame. 378 * <p> 379 * JmriJFrames are often built internally and presented to the user before 380 * any scripting action can interact with them. At that point it's too late 381 * to directly invoke setUndecorated(true) because the JFrame is already 382 * displayable. This method uses dispose() to drop the windowing resources, 383 * sets undecorated, and then redisplays the window. 384 */ 385 public void undecorate() { 386 boolean visible = isVisible(); 387 388 setVisible(false); 389 log.debug("super.dispose() called in undecorate()"); 390 super.dispose(); 391 392 setUndecorated(true); 393 getRootPane().setWindowDecorationStyle(javax.swing.JRootPane.NONE); 394 395 pack(); 396 setVisible(visible); 397 } 398 399 /** 400 * Initialize only once the MaximumSize for the screen 401 */ 402 private final Dimension maxSizeDimension = getMaximumSize(); 403 404 /** 405 * Tries to get window to fix entirely on screen. First choice is to move 406 * the origin up and left as needed, then to make the window smaller 407 */ 408 void reSizeToFitOnScreen() { 409 int width = this.getPreferredSize().width; 410 int height = this.getPreferredSize().height; 411 log.trace("reSizeToFitOnScreen of \"{}\" starts with maximum size {}", getTitle(), maxSizeDimension); 412 log.trace("reSizeToFitOnScreen starts with preferred height {} width {}", height, width); 413 log.trace("reSizeToFitOnScreen starts with location {},{}", getX(), getY()); 414 log.trace("reSizeToFitOnScreen starts with insets {},{}", getInsets().left, getInsets().top); 415 // Normalise the location 416 ScreenDimensions sd = getContainingDisplay(this.getLocation()); 417 Point locationOnDisplay = new Point(getLocation().x - sd.getBounds().x, getLocation().y - sd.getBounds().y); 418 log.trace("reSizeToFitOnScreen normalises origin to {}, {}", locationOnDisplay.x, locationOnDisplay.y); 419 420 if ((width + locationOnDisplay.x) >= maxSizeDimension.getWidth()) { 421 // not fit in width, try to move position left 422 int offsetX = (width + locationOnDisplay.x) - (int) maxSizeDimension.getWidth(); // pixels too large 423 log.trace("reSizeToFitOnScreen moves \"{}\" left {} pixels", getTitle(), offsetX); 424 int positionX = locationOnDisplay.x - offsetX; 425 if (positionX < this.getInsets().left) { 426 positionX = this.getInsets().left; 427 log.trace("reSizeToFitOnScreen sets \"{}\" X to minimum {}", getTitle(), positionX); 428 } 429 this.setLocation(positionX + sd.getBounds().x, this.getY()); 430 log.trace("reSizeToFitOnScreen during X calculation sets location {}, {}", positionX + sd.getBounds().x, this.getY()); 431 // try again to see if it doesn't fit 432 if ((width + locationOnDisplay.x) >= maxSizeDimension.getWidth()) { 433 width = width - (int) ((width + locationOnDisplay.x) - maxSizeDimension.getWidth()); 434 log.trace("reSizeToFitOnScreen sets \"{}\" width to {}", getTitle(), width); 435 } 436 } 437 if ((height + locationOnDisplay.y) >= maxSizeDimension.getHeight()) { 438 // not fit in height, try to move position up 439 int offsetY = (height + locationOnDisplay.y) - (int) maxSizeDimension.getHeight(); // pixels too large 440 log.trace("reSizeToFitOnScreen moves \"{}\" up {} pixels", getTitle(), offsetY); 441 int positionY = locationOnDisplay.y - offsetY; 442 if (positionY < this.getInsets().top) { 443 positionY = this.getInsets().top; 444 log.trace("reSizeToFitScreen sets \"{}\" Y to minimum {}", getTitle(), positionY); 445 } 446 this.setLocation(this.getX(), positionY + sd.getBounds().y); 447 log.trace("reSizeToFitOnScreen during Y calculation sets location {}, {}", getX(), positionY + sd.getBounds().y); 448 // try again to see if it doesn't fit 449 if ((height + this.getY()) >= maxSizeDimension.getHeight()) { 450 height = height - (int) ((height + locationOnDisplay.y) - maxSizeDimension.getHeight()); 451 log.trace("reSizeToFitOnScreen sets \"{}\" height to {}", getTitle(), height); 452 } 453 } 454 this.setSize(width, height); 455 log.debug("reSizeToFitOnScreen sets height {} width {} position {},{}", height, width, getX(), getY()); 456 457 } 458 459 /** 460 * Move a frame down and to the left by it's top offset or a fixed amount, whichever is larger 461 * @param f JmirJFrame to move 462 */ 463 void offSetFrameOnScreen(JmriJFrame f) { 464 /* 465 * We use the frame that we are moving away from for insets, as at this point our own insets have not been correctly 466 * built and always return a size of zero 467 */ 468 int REQUIRED_OFFSET = 25; // units are pixels 469 int REQUIRED_OFFSET_X = Math.max(REQUIRED_OFFSET, f.getInsets().left); 470 int REQUIRED_OFFSET_Y = Math.max(REQUIRED_OFFSET, f.getInsets().top); 471 472 int frameOffSetx = this.getX() + REQUIRED_OFFSET_X; 473 int frameOffSety = this.getY() + REQUIRED_OFFSET_Y; 474 475 Dimension dim = getMaximumSize(); 476 477 if (frameOffSetx >= (dim.getWidth() * 0.75)) { 478 frameOffSety = 0; 479 frameOffSetx = (f.getInsets().top) * 2; 480 } 481 if (frameOffSety >= (dim.getHeight() * 0.75)) { 482 frameOffSety = 0; 483 frameOffSetx = (f.getInsets().top) * 2; 484 } 485 /* 486 * If we end up with our off Set of X being greater than the width of the screen we start back at the beginning 487 * but with a half offset 488 */ 489 if (frameOffSetx >= dim.getWidth()) { 490 frameOffSetx = f.getInsets().top / 2; 491 } 492 this.setLocation(frameOffSetx, frameOffSety); 493 } 494 495 String windowFrameRef; 496 497 public String getWindowFrameRef() { 498 return windowFrameRef; 499 } 500 501 /** 502 * By default, Swing components should be created an installed in this 503 * method, rather than in the ctor itself. 504 */ 505 public void initComponents() { 506 } 507 508 /** 509 * Add a standard help menu, including window specific help item. 510 * 511 * Final because it defines the content of a standard help menu, not to be messed with individually 512 * 513 * @param ref JHelp reference for the desired window-specific help page; null means no page 514 * @param direct true if the help main-menu item goes directly to the help system, 515 * such as when there are no items in the help menu 516 */ 517 final public void addHelpMenu(String ref, boolean direct) { 518 // only works if no menu present? 519 JMenuBar bar = getJMenuBar(); 520 if (bar == null) { 521 bar = new JMenuBar(); 522 } 523 // add Window menu 524 bar.add(new WindowMenu(this)); 525 // add Help menu 526 jmri.util.HelpUtil.helpMenu(bar, ref, direct); 527 setJMenuBar(bar); 528 } 529 530 /** 531 * Adds a "Close Window" key shortcut to close window on op-W. 532 */ 533 @SuppressWarnings("deprecation") // getMenuShortcutKeyMask() 534 void addWindowCloseShortCut() { 535 // modelled after code in JavaDev mailing list item by Bill Tschumy <bill@otherwise.com> 08 Dec 2004 536 AbstractAction act = new AbstractAction() { 537 538 /** {@inheritDoc} */ 539 @Override 540 public void actionPerformed(ActionEvent e) { 541 // log.debug("keystroke requested close window ", JmriJFrame.this.getTitle()); 542 JmriJFrame.this.processWindowEvent(new java.awt.event.WindowEvent(JmriJFrame.this, 543 java.awt.event.WindowEvent.WINDOW_CLOSING)); 544 } 545 }; 546 getRootPane().getActionMap().put("close", act); 547 548 int stdMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx(); 549 InputMap im = getRootPane().getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); 550 551 // We extract the modifiers as a string, then add the I18N string, and 552 // build a key code 553 String modifier = KeyStroke.getKeyStroke(KeyEvent.VK_W, stdMask).toString(); 554 String keyCode = modifier.substring(0, modifier.length() - 1) 555 + Bundle.getMessage("VkKeyWindowClose").substring(0, 1); 556 557 im.put(KeyStroke.getKeyStroke(keyCode), "close"); // NOI18N 558 // im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "close"); 559 } 560 561 private static String escapeKeyAction = "escapeKeyAction"; 562 private boolean escapeKeyActionClosesWindow = false; 563 564 /** 565 * Bind an action to the Escape key. 566 * <p> 567 * Binds an AbstractAction to the Escape key. If an action is already bound 568 * to the Escape key, that action will be replaced. Passing 569 * <code>null</code> unbinds any existing actions from the Escape key. 570 * <p> 571 * Note that binding the Escape key to any action may break expected or 572 * standardized behaviors. See <a 573 * href="http://java.sun.com/products/jlf/ed2/book/Appendix.A.html">Keyboard 574 * Shortcuts, Mnemonics, and Other Keyboard Operations</a> in the Java Look 575 * and Feel Design Guidelines for standardized behaviors. 576 * 577 * @param action The AbstractAction to bind to. 578 * @see #getEscapeKeyAction() 579 * @see #setEscapeKeyClosesWindow(boolean) 580 */ 581 public void setEscapeKeyAction(AbstractAction action) { 582 JRootPane root = this.getRootPane(); 583 KeyStroke escape = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0); 584 escapeKeyActionClosesWindow = false; // setEscapeKeyClosesWindow will set to true as needed 585 if (action != null) { 586 root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(escape, escapeKeyAction); 587 root.getActionMap().put(escapeKeyAction, action); 588 } else { 589 root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).remove(escape); 590 root.getActionMap().remove(escapeKeyAction); 591 } 592 } 593 594 /** 595 * The action associated with the Escape key. 596 * 597 * @return An AbstractAction or null if no action is bound to the Escape 598 * key. 599 * @see #setEscapeKeyAction(javax.swing.AbstractAction) 600 * @see javax.swing.AbstractAction 601 */ 602 public AbstractAction getEscapeKeyAction() { 603 return (AbstractAction) this.getRootPane().getActionMap().get(escapeKeyAction); 604 } 605 606 /** 607 * Bind the Escape key to an action that closes the window. 608 * <p> 609 * If closesWindow is true, this method creates an action that triggers the 610 * "window is closing" event; otherwise this method removes any actions from 611 * the Escape key. 612 * 613 * @param closesWindow Create or destroy an action to close the window. 614 * @see java.awt.event.WindowEvent#WINDOW_CLOSING 615 * @see #setEscapeKeyAction(javax.swing.AbstractAction) 616 */ 617 public void setEscapeKeyClosesWindow(boolean closesWindow) { 618 if (closesWindow) { 619 setEscapeKeyAction(new AbstractAction() { 620 621 /** {@inheritDoc} */ 622 @Override 623 public void actionPerformed(ActionEvent ae) { 624 JmriJFrame.this.processWindowEvent(new java.awt.event.WindowEvent(JmriJFrame.this, 625 java.awt.event.WindowEvent.WINDOW_CLOSING)); 626 } 627 }); 628 } else { 629 setEscapeKeyAction(null); 630 } 631 escapeKeyActionClosesWindow = closesWindow; 632 } 633 634 /** 635 * Does the Escape key close the window? 636 * 637 * @return <code>true</code> if Escape key is bound to action created by 638 * setEscapeKeyClosesWindow, <code>false</code> in all other cases. 639 * @see #setEscapeKeyClosesWindow 640 * @see #setEscapeKeyAction 641 */ 642 public boolean getEscapeKeyClosesWindow() { 643 return (escapeKeyActionClosesWindow && getEscapeKeyAction() != null); 644 } 645 646 private ScreenDimensions getContainingDisplay(Point location) { 647 // Loop through attached screen to determine which 648 // contains the top-left origin point of this window 649 for (ScreenDimensions sd: getScreenDimensions()) { 650 boolean isOnThisScreen = sd.getBounds().contains(location); 651 log.debug("Is \"{}\" window origin {} located on screen {}? {}", getTitle(), this.getLocation(), sd.getGraphicsDevice().getIDstring(), isOnThisScreen); 652 if (isOnThisScreen) { 653 // We've found the screen that contains this origin 654 return sd; 655 } 656 } 657 // As a fall-back, return the first display which is the primary 658 log.debug("Falling back to using the primary display"); 659 return getScreenDimensions().get(0); 660 } 661 662 /** 663 * {@inheritDoc} 664 * Provide a maximum frame size that is limited to what can fit on the 665 * screen after toolbars, etc are deducted. 666 * <p> 667 * Some of the methods used here return null pointers on some Java 668 * implementations, however, so this will return the superclasses's maximum 669 * size if the algorithm used here fails. 670 * 671 * @return the maximum window size 672 */ 673 @Override 674 public Dimension getMaximumSize() { 675 // adjust maximum size to full screen minus any toolbars 676 if (GraphicsEnvironment.isHeadless()) { 677 // there are no screens 678 return new Dimension(0,0); 679 } 680 try { 681 // Try our own algorithm. This throws null-pointer exceptions on 682 // some Java installs, however, for unknown reasons, so be 683 // prepared to fall back. 684 try { 685 ScreenDimensions sd = getContainingDisplay(this.getLocation()); 686 int widthInset = sd.getInsets().right + sd.getInsets().left; 687 int heightInset = sd.getInsets().top + sd.getInsets().bottom; 688 689 // If insets are zero, guess based on system type 690 if (widthInset == 0 && heightInset == 0) { 691 String osName = SystemType.getOSName(); 692 if (SystemType.isLinux()) { 693 // Linux generally has a bar across the top and/or bottom 694 // of the screen, but lets you have the full width. 695 heightInset = 70; 696 } // Windows generally has values, but not always, 697 // so we provide observed values just in case 698 else if (osName.equals("Windows XP") || osName.equals("Windows 98") 699 || osName.equals("Windows 2000")) { 700 heightInset = 28; // bottom 28 701 } 702 } 703 704 // Insets may also be provided as system parameters 705 String sw = System.getProperty("jmri.inset.width"); 706 if (sw != null) { 707 try { 708 widthInset = Integer.parseInt(sw); 709 } catch (NumberFormatException e1) { 710 log.error("Error parsing jmri.inset.width: {}", e1.getMessage()); 711 } 712 } 713 String sh = System.getProperty("jmri.inset.height"); 714 if (sh != null) { 715 try { 716 heightInset = Integer.parseInt(sh); 717 } catch (NumberFormatException e1) { 718 log.error("Error parsing jmri.inset.height: {}", e1.getMessage()); 719 } 720 } 721 722 // calculate size as screen size minus space needed for offsets 723 log.trace("getMaximumSize returns normally {},{}", (sd.getBounds().width - widthInset), (sd.getBounds().height - heightInset)); 724 return new Dimension(sd.getBounds().width - widthInset, sd.getBounds().height - heightInset); 725 726 } catch (NoSuchMethodError e) { 727 Dimension screen = getToolkit().getScreenSize(); 728 log.trace("getMaximumSize returns approx due to failure {},{}", screen.width, screen.height); 729 return new Dimension(screen.width, screen.height - 45); // approximate this... 730 } 731 } catch (RuntimeException e2) { 732 // failed completely, fall back to standard method 733 log.trace("getMaximumSize returns super due to failure {}", super.getMaximumSize()); 734 return super.getMaximumSize(); 735 } 736 } 737 738 /** 739 * {@inheritDoc} 740 * The preferred size must fit on the physical screen, so calculate the 741 * lesser of either the preferred size from the layout or the screen size. 742 * 743 * @return the preferred size or the maximum size, whichever is smaller 744 */ 745 @Override 746 public Dimension getPreferredSize() { 747 // limit preferred size to size of screen (from getMaximumSize()) 748 Dimension screen = getMaximumSize(); 749 int width = Math.min(super.getPreferredSize().width, screen.width); 750 int height = Math.min(super.getPreferredSize().height, screen.height); 751 log.debug("getPreferredSize \"{}\" returns width {} height {}", getTitle(), width, height); 752 return new Dimension(width, height); 753 } 754 755 /** 756 * Get a List of the currently-existing JmriJFrame objects. The returned 757 * list is a copy made at the time of the call, so it can be manipulated as 758 * needed by the caller. 759 * 760 * @return a list of JmriJFrame instances. If there are no instances, an 761 * empty list is returned. 762 */ 763 @Nonnull 764 public static List<JmriJFrame> getFrameList() { 765 JmriJFrameManager m = getJmriJFrameManager(); 766 synchronized (m) { 767 return new ArrayList<>(m); 768 } 769 } 770 771 /** 772 * Get a list of currently-existing JmriJFrame objects that are specific 773 * sub-classes of JmriJFrame. 774 * <p> 775 * The returned list is a copy made at the time of the call, so it can be 776 * manipulated as needed by the caller. 777 * 778 * @param <T> generic JmriJframe. 779 * @param type The Class the list should be limited to. 780 * @return An ArrayList of Frames. 781 */ 782 @SuppressWarnings("unchecked") // cast in add() checked at run time 783 public static <T extends JmriJFrame> List<T> getFrameList(@Nonnull Class<T> type) { 784 List<T> result = new ArrayList<>(); 785 JmriJFrameManager m = getJmriJFrameManager(); 786 synchronized (m) { 787 m.stream().filter((f) -> (type.isInstance(f))).forEachOrdered((f) -> 788 { 789 result.add((T)f); 790 }); 791 } 792 return result; 793 } 794 795 /** 796 * Get a JmriJFrame of a particular name. If more than one exists, there's 797 * no guarantee as to which is returned. 798 * 799 * @param name the name of one or more JmriJFrame objects 800 * @return a JmriJFrame with the matching name or null if no matching frames 801 * exist 802 */ 803 public static JmriJFrame getFrame(String name) { 804 for (JmriJFrame j : getFrameList()) { 805 if (j.getTitle().equals(name)) { 806 return j; 807 } 808 } 809 return null; 810 } 811 812 /** 813 * Set whether the frame Position is saved or not after it has been created. 814 * 815 * @param save true if the frame position should be saved. 816 */ 817 public void setSavePosition(boolean save) { 818 reuseFrameSavedPosition = save; 819 InstanceManager.getOptionalDefault(UserPreferencesManager.class).ifPresent(prefsMgr -> { 820 prefsMgr.setSaveWindowLocation(windowFrameRef, save); 821 }); 822 } 823 824 /** 825 * Set whether the frame Size is saved or not after it has been created. 826 * 827 * @param save true if the frame size should be saved. 828 */ 829 public void setSaveSize(boolean save) { 830 reuseFrameSavedSized = save; 831 InstanceManager.getOptionalDefault(UserPreferencesManager.class).ifPresent(prefsMgr -> { 832 prefsMgr.setSaveWindowSize(windowFrameRef, save); 833 }); 834 } 835 836 /** 837 * Returns if the frame Position is saved or not. 838 * 839 * @return true if the frame position should be saved 840 */ 841 public boolean getSavePosition() { 842 return reuseFrameSavedPosition; 843 } 844 845 /** 846 * Returns if the frame Size is saved or not. 847 * 848 * @return true if the frame size should be saved 849 */ 850 public boolean getSaveSize() { 851 return reuseFrameSavedSized; 852 } 853 854 /** 855 * {@inheritDoc} 856 * A frame is considered "modified" if it has changes that have not been 857 * stored. 858 */ 859 @Override 860 public void setModifiedFlag(boolean flag) { 861 this.modifiedFlag = flag; 862 // mark the window in the GUI 863 markWindowModified(this.modifiedFlag); 864 } 865 866 /** {@inheritDoc} */ 867 @Override 868 public boolean getModifiedFlag() { 869 return modifiedFlag; 870 } 871 872 private boolean modifiedFlag = false; 873 874 /** 875 * Handle closing a window or quiting the program while the modified bit was 876 * set. 877 */ 878 protected void handleModified() { 879 if (getModifiedFlag()) { 880 this.setVisible(true); 881 int result = JmriJOptionPane.showOptionDialog(this, Bundle.getMessage("WarnChangedMsg"), 882 Bundle.getMessage("WarningTitle"), JmriJOptionPane.YES_NO_OPTION, 883 JmriJOptionPane.WARNING_MESSAGE, null, // icon 884 new String[]{Bundle.getMessage("WarnYesSave"), Bundle.getMessage("WarnNoClose")}, Bundle 885 .getMessage("WarnYesSave")); 886 if (result == 0 ) { // array option 0 , WarnYesSave 887 // user wants to save 888 storeValues(); 889 } 890 } 891 } 892 893 protected void storeValues() { 894 log.error("default storeValues does nothing for \"{}\"", getTitle()); 895 } 896 897 // For marking the window as modified on Mac OS X 898 // See: https://web.archive.org/web/20090712161630/http://developer.apple.com/qa/qa2001/qa1146.html 899 final static String WINDOW_MODIFIED = "windowModified"; 900 901 public void markWindowModified(boolean yes) { 902 getRootPane().putClientProperty(WINDOW_MODIFIED, yes ? Boolean.TRUE : Boolean.FALSE); 903 } 904 905 // Window methods 906 /** Does nothing in this class */ 907 @Override 908 public void windowOpened(java.awt.event.WindowEvent e) { 909 } 910 911 /** Does nothing in this class */ 912 @Override 913 public void windowClosed(java.awt.event.WindowEvent e) { 914 } 915 916 /** Does nothing in this class */ 917 @Override 918 public void windowActivated(java.awt.event.WindowEvent e) { 919 } 920 921 /** Does nothing in this class */ 922 @Override 923 public void windowDeactivated(java.awt.event.WindowEvent e) { 924 } 925 926 /** Does nothing in this class */ 927 @Override 928 public void windowIconified(java.awt.event.WindowEvent e) { 929 } 930 931 /** Does nothing in this class */ 932 @Override 933 public void windowDeiconified(java.awt.event.WindowEvent e) { 934 } 935 936 /** 937 * {@inheritDoc} 938 * 939 * The JmriJFrame implementation calls {@link #handleModified()}. 940 */ 941 @Override 942 @OverridingMethodsMustInvokeSuper 943 public void windowClosing(java.awt.event.WindowEvent e) { 944 handleModified(); 945 } 946 947 /** Does nothing in this class */ 948 @Override 949 public void componentHidden(java.awt.event.ComponentEvent e) { 950 } 951 952 /** {@inheritDoc} */ 953 @Override 954 public void componentMoved(java.awt.event.ComponentEvent e) { 955 InstanceManager.getOptionalDefault(UserPreferencesManager.class).ifPresent(p -> { 956 if (reuseFrameSavedPosition && isVisible()) { 957 p.setWindowLocation(windowFrameRef, this.getLocation()); 958 } 959 }); 960 } 961 962 /** {@inheritDoc} */ 963 @Override 964 public void componentResized(java.awt.event.ComponentEvent e) { 965 InstanceManager.getOptionalDefault(UserPreferencesManager.class).ifPresent(p -> { 966 if (reuseFrameSavedSized && isVisible()) { 967 saveWindowSize(p); 968 } 969 }); 970 } 971 972 /** Does nothing in this class */ 973 @Override 974 public void componentShown(java.awt.event.ComponentEvent e) { 975 } 976 977 private transient AbstractShutDownTask task = null; 978 979 protected void setShutDownTask() { 980 task = new AbstractShutDownTask(getTitle()) { 981 @Override 982 public Boolean call() { 983 handleModified(); 984 return Boolean.TRUE; 985 } 986 987 @Override 988 public void run() { 989 } 990 }; 991 InstanceManager.getDefault(ShutDownManager.class).register(task); 992 } 993 994 protected boolean reuseFrameSavedPosition = true; 995 protected boolean reuseFrameSavedSized = true; 996 997 /** 998 * {@inheritDoc} 999 * 1000 * When window is finally destroyed, remove it from the list of windows. 1001 * <p> 1002 * Subclasses that over-ride this method must invoke this implementation 1003 * with super.dispose() right before returning. 1004 */ 1005 @OverridingMethodsMustInvokeSuper 1006 @Override 1007 public void dispose() { 1008 log.debug("JmriJFrame dispose invoked on {}", getTitle()); 1009 InstanceManager.getOptionalDefault(UserPreferencesManager.class).ifPresent(p -> { 1010 if (reuseFrameSavedPosition) { 1011 p.setWindowLocation(windowFrameRef, this.getLocation()); 1012 } 1013 if (reuseFrameSavedSized) { 1014 saveWindowSize(p); 1015 } 1016 }); 1017 log.debug("dispose \"{}\"", getTitle()); 1018 if (windowInterface != null) { 1019 windowInterface.dispose(); 1020 } 1021 if (task != null) { 1022 jmri.InstanceManager.getDefault(jmri.ShutDownManager.class).deregister(task); 1023 task = null; 1024 } 1025 JmriJFrameManager m = getJmriJFrameManager(); 1026 synchronized (m) { 1027 m.remove(this); 1028 } 1029 1030 // workaround for code that directly calls dispose() 1031 // instead of dispatching a WINDOW_CLOSED event. This 1032 // causes the windowClosing method to not be called. This in turn is an 1033 // issue because people have put code in the windowClosed method that 1034 // should really be in windowClosing. 1035 ThreadingUtil.runOnGUIDelayed(() -> { 1036 removeWindowListener(this); 1037 removeComponentListener(this); 1038 }, 500); 1039 1040 super.dispose(); 1041 } 1042 1043 /* 1044 * Save current window size, do not put adjustments here. Search elsewhere for the problem. 1045 */ 1046 private void saveWindowSize(jmri.UserPreferencesManager p) { 1047 p.setWindowSize(windowFrameRef, super.getSize()); 1048 } 1049 1050 /* 1051 * This field contains a list of properties that do not correspond to the JavaBeans properties coding pattern, or 1052 * known properties that do correspond to that pattern. The default JmriJFrame implementation of 1053 * BeanInstance.hasProperty checks this hashmap before using introspection to find properties corresponding to the 1054 * JavaBean properties coding pattern. 1055 */ 1056 protected HashMap<String, Object> properties = new HashMap<>(); 1057 1058 /** {@inheritDoc} */ 1059 @Override 1060 public void setIndexedProperty(String key, int index, Object value) { 1061 if (BeanUtil.hasIntrospectedProperty(this, key)) { 1062 BeanUtil.setIntrospectedIndexedProperty(this, key, index, value); 1063 } else { 1064 if (!properties.containsKey(key)) { 1065 properties.put(key, new Object[0]); 1066 } 1067 ((Object[]) properties.get(key))[index] = value; 1068 } 1069 } 1070 1071 /** {@inheritDoc} */ 1072 @Override 1073 public Object getIndexedProperty(String key, int index) { 1074 if (properties.containsKey(key) && properties.get(key).getClass().isArray()) { 1075 return ((Object[]) properties.get(key))[index]; 1076 } 1077 return BeanUtil.getIntrospectedIndexedProperty(this, key, index); 1078 } 1079 1080 /** {@inheritDoc} 1081 * Subclasses should override this method with something more direct and faster 1082 */ 1083 @Override 1084 public void setProperty(String key, Object value) { 1085 if (BeanUtil.hasIntrospectedProperty(this, key)) { 1086 BeanUtil.setIntrospectedProperty(this, key, value); 1087 } else { 1088 properties.put(key, value); 1089 } 1090 } 1091 1092 /** {@inheritDoc} 1093 * Subclasses should override this method with something more direct and faster 1094 */ 1095 @Override 1096 public Object getProperty(String key) { 1097 if (properties.containsKey(key)) { 1098 return properties.get(key); 1099 } 1100 return BeanUtil.getIntrospectedProperty(this, key); 1101 } 1102 1103 /** {@inheritDoc} */ 1104 @Override 1105 public boolean hasProperty(String key) { 1106 return (properties.containsKey(key) || BeanUtil.hasIntrospectedProperty(this, key)); 1107 } 1108 1109 /** {@inheritDoc} */ 1110 @Override 1111 public boolean hasIndexedProperty(String key) { 1112 return ((this.properties.containsKey(key) && this.properties.get(key).getClass().isArray()) 1113 || BeanUtil.hasIntrospectedIndexedProperty(this, key)); 1114 } 1115 1116 protected transient WindowInterface windowInterface = null; 1117 1118 /** {@inheritDoc} */ 1119 @Override 1120 public void show(JmriPanel child, JmriAbstractAction action) { 1121 if (null != windowInterface) { 1122 windowInterface.show(child, action); 1123 } 1124 } 1125 1126 /** {@inheritDoc} */ 1127 @Override 1128 public void show(JmriPanel child, JmriAbstractAction action, Hint hint) { 1129 if (null != windowInterface) { 1130 windowInterface.show(child, action, hint); 1131 } 1132 } 1133 1134 /** {@inheritDoc} */ 1135 @Override 1136 public boolean multipleInstances() { 1137 if (null != windowInterface) { 1138 return windowInterface.multipleInstances(); 1139 } 1140 return false; 1141 } 1142 1143 public void setWindowInterface(WindowInterface wi) { 1144 windowInterface = wi; 1145 } 1146 1147 public WindowInterface getWindowInterface() { 1148 return windowInterface; 1149 } 1150 1151 /** {@inheritDoc} */ 1152 @Override 1153 public Set<String> getPropertyNames() { 1154 Set<String> names = new HashSet<>(); 1155 names.addAll(properties.keySet()); 1156 names.addAll(BeanUtil.getIntrospectedPropertyNames(this)); 1157 return names; 1158 } 1159 1160 public void setAllowInFrameServlet(boolean allow) { 1161 allowInFrameServlet = allow; 1162 } 1163 1164 public boolean getAllowInFrameServlet() { 1165 return allowInFrameServlet; 1166 } 1167 1168 /** {@inheritDoc} */ 1169 @Override 1170 public Frame getFrame() { 1171 return this; 1172 } 1173 1174 private static JmriJFrameManager getJmriJFrameManager() { 1175 return InstanceManager.getOptionalDefault(JmriJFrameManager.class).orElseGet(() -> { 1176 return InstanceManager.setDefault(JmriJFrameManager.class, new JmriJFrameManager()); 1177 }); 1178 } 1179 1180 /** 1181 * A list container of JmriJFrame objects. Not a straight ArrayList, but a 1182 * specific class so that the {@link jmri.InstanceManager} can be used to 1183 * retain the reference to the list instead of relying on a static variable. 1184 */ 1185 private static class JmriJFrameManager extends ArrayList<JmriJFrame> { 1186 1187 } 1188 1189 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(JmriJFrame.class); 1190 1191}