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 * Tries to get window to fix entirely on screen. First choice is to move 401 * the origin up and left as needed, then to make the window smaller 402 */ 403 void reSizeToFitOnScreen() { 404 int width = this.getPreferredSize().width; 405 int height = this.getPreferredSize().height; 406 Dimension maxSizeDimension = getMaximumSize(); 407 log.trace("reSizeToFitOnScreen of \"{}\" starts with maximum size {}", getTitle(), maxSizeDimension); 408 log.trace("reSizeToFitOnScreen starts with preferred height {} width {}", height, width); 409 log.trace("reSizeToFitOnScreen starts with location {},{}", getX(), getY()); 410 log.trace("reSizeToFitOnScreen starts with insets {},{}", getInsets().left, getInsets().top); 411 // Normalise the location 412 int screenNb = getContainingDisplay(this.getLocation()); 413 ScreenDimensions sd = getScreenDimensions().get(screenNb); 414 Point locationOnDisplay = new Point(getLocation().x - sd.getBounds().x, getLocation().y - sd.getBounds().y); 415 log.trace("reSizeToFitOnScreen normalises origin to {}, {}", locationOnDisplay.x, locationOnDisplay.y); 416 417 if ((width + locationOnDisplay.x) >= maxSizeDimension.getWidth()) { 418 // not fit in width, try to move position left 419 int offsetX = (width + locationOnDisplay.x) - (int) maxSizeDimension.getWidth(); // pixels too large 420 log.trace("reSizeToFitOnScreen moves \"{}\" left {} pixels", getTitle(), offsetX); 421 int positionX = locationOnDisplay.x - offsetX; 422 if (positionX < this.getInsets().left) { 423 positionX = this.getInsets().left; 424 log.trace("reSizeToFitOnScreen sets \"{}\" X to minimum {}", getTitle(), positionX); 425 } 426 this.setLocation(positionX + sd.getBounds().x, this.getY()); 427 log.trace("reSizeToFitOnScreen during X calculation sets location {}, {}", positionX + sd.getBounds().x, this.getY()); 428 // try again to see if it doesn't fit 429 if ((width + locationOnDisplay.x) >= maxSizeDimension.getWidth()) { 430 width = width - (int) ((width + locationOnDisplay.x) - maxSizeDimension.getWidth()); 431 log.trace("reSizeToFitOnScreen sets \"{}\" width to {}", getTitle(), width); 432 } 433 } 434 if ((height + locationOnDisplay.y) >= maxSizeDimension.getHeight()) { 435 // not fit in height, try to move position up 436 int offsetY = (height + locationOnDisplay.y) - (int) maxSizeDimension.getHeight(); // pixels too large 437 log.trace("reSizeToFitOnScreen moves \"{}\" up {} pixels", getTitle(), offsetY); 438 int positionY = locationOnDisplay.y - offsetY; 439 if (positionY < this.getInsets().top) { 440 positionY = this.getInsets().top; 441 log.trace("reSizeToFitScreen sets \"{}\" Y to minimum {}", getTitle(), positionY); 442 } 443 this.setLocation(this.getX(), positionY + sd.getBounds().y); 444 log.trace("reSizeToFitOnScreen during Y calculation sets location {}, {}", getX(), positionY + sd.getBounds().y); 445 // try again to see if it doesn't fit 446 if ((height + this.getY()) >= maxSizeDimension.getHeight()) { 447 height = height - (int) ((height + locationOnDisplay.y) - maxSizeDimension.getHeight()); 448 log.trace("reSizeToFitOnScreen sets \"{}\" height to {}", getTitle(), height); 449 } 450 } 451 this.setSize(width, height); 452 log.debug("reSizeToFitOnScreen sets height {} width {} position {},{}", height, width, getX(), getY()); 453 454 } 455 456 /** 457 * Move a frame down and to the left by it's top offset or a fixed amount, whichever is larger 458 * @param f JmirJFrame to move 459 */ 460 void offSetFrameOnScreen(JmriJFrame f) { 461 /* 462 * We use the frame that we are moving away from for insets, as at this point our own insets have not been correctly 463 * built and always return a size of zero 464 */ 465 int REQUIRED_OFFSET = 25; // units are pixels 466 int REQUIRED_OFFSET_X = Math.max(REQUIRED_OFFSET, f.getInsets().left); 467 int REQUIRED_OFFSET_Y = Math.max(REQUIRED_OFFSET, f.getInsets().top); 468 469 int frameOffSetx = this.getX() + REQUIRED_OFFSET_X; 470 int frameOffSety = this.getY() + REQUIRED_OFFSET_Y; 471 472 Dimension dim = getMaximumSize(); 473 474 if (frameOffSetx >= (dim.getWidth() * 0.75)) { 475 frameOffSety = 0; 476 frameOffSetx = (f.getInsets().top) * 2; 477 } 478 if (frameOffSety >= (dim.getHeight() * 0.75)) { 479 frameOffSety = 0; 480 frameOffSetx = (f.getInsets().top) * 2; 481 } 482 /* 483 * If we end up with our off Set of X being greater than the width of the screen we start back at the beginning 484 * but with a half offset 485 */ 486 if (frameOffSetx >= dim.getWidth()) { 487 frameOffSetx = f.getInsets().top / 2; 488 } 489 this.setLocation(frameOffSetx, frameOffSety); 490 } 491 492 String windowFrameRef; 493 494 public String getWindowFrameRef() { 495 return windowFrameRef; 496 } 497 498 /** 499 * By default, Swing components should be created an installed in this 500 * method, rather than in the ctor itself. 501 */ 502 public void initComponents() { 503 } 504 505 /** 506 * Add a standard help menu, including window specific help item. 507 * 508 * Final because it defines the content of a standard help menu, not to be messed with individually 509 * 510 * @param ref JHelp reference for the desired window-specific help page; null means no page 511 * @param direct true if the help main-menu item goes directly to the help system, 512 * such as when there are no items in the help menu 513 */ 514 final public void addHelpMenu(String ref, boolean direct) { 515 // only works if no menu present? 516 JMenuBar bar = getJMenuBar(); 517 if (bar == null) { 518 bar = new JMenuBar(); 519 } 520 // add Window menu 521 bar.add(new WindowMenu(this)); 522 // add Help menu 523 jmri.util.HelpUtil.helpMenu(bar, ref, direct); 524 setJMenuBar(bar); 525 } 526 527 /** 528 * Adds a "Close Window" key shortcut to close window on op-W. 529 */ 530 @SuppressWarnings("deprecation") // getMenuShortcutKeyMask() 531 void addWindowCloseShortCut() { 532 // modelled after code in JavaDev mailing list item by Bill Tschumy <bill@otherwise.com> 08 Dec 2004 533 AbstractAction act = new AbstractAction() { 534 535 /** {@inheritDoc} */ 536 @Override 537 public void actionPerformed(ActionEvent e) { 538 // log.debug("keystroke requested close window ", JmriJFrame.this.getTitle()); 539 JmriJFrame.this.processWindowEvent(new java.awt.event.WindowEvent(JmriJFrame.this, 540 java.awt.event.WindowEvent.WINDOW_CLOSING)); 541 } 542 }; 543 getRootPane().getActionMap().put("close", act); 544 545 int stdMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx(); 546 InputMap im = getRootPane().getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); 547 548 // We extract the modifiers as a string, then add the I18N string, and 549 // build a key code 550 String modifier = KeyStroke.getKeyStroke(KeyEvent.VK_W, stdMask).toString(); 551 String keyCode = modifier.substring(0, modifier.length() - 1) 552 + Bundle.getMessage("VkKeyWindowClose").substring(0, 1); 553 554 im.put(KeyStroke.getKeyStroke(keyCode), "close"); // NOI18N 555 // im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "close"); 556 } 557 558 private static String escapeKeyAction = "escapeKeyAction"; 559 private boolean escapeKeyActionClosesWindow = false; 560 561 /** 562 * Bind an action to the Escape key. 563 * <p> 564 * Binds an AbstractAction to the Escape key. If an action is already bound 565 * to the Escape key, that action will be replaced. Passing 566 * <code>null</code> unbinds any existing actions from the Escape key. 567 * <p> 568 * Note that binding the Escape key to any action may break expected or 569 * standardized behaviors. See <a 570 * href="http://java.sun.com/products/jlf/ed2/book/Appendix.A.html">Keyboard 571 * Shortcuts, Mnemonics, and Other Keyboard Operations</a> in the Java Look 572 * and Feel Design Guidelines for standardized behaviors. 573 * 574 * @param action The AbstractAction to bind to. 575 * @see #getEscapeKeyAction() 576 * @see #setEscapeKeyClosesWindow(boolean) 577 */ 578 public void setEscapeKeyAction(AbstractAction action) { 579 JRootPane root = this.getRootPane(); 580 KeyStroke escape = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0); 581 escapeKeyActionClosesWindow = false; // setEscapeKeyClosesWindow will set to true as needed 582 if (action != null) { 583 root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(escape, escapeKeyAction); 584 root.getActionMap().put(escapeKeyAction, action); 585 } else { 586 root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).remove(escape); 587 root.getActionMap().remove(escapeKeyAction); 588 } 589 } 590 591 /** 592 * The action associated with the Escape key. 593 * 594 * @return An AbstractAction or null if no action is bound to the Escape 595 * key. 596 * @see #setEscapeKeyAction(javax.swing.AbstractAction) 597 * @see javax.swing.AbstractAction 598 */ 599 public AbstractAction getEscapeKeyAction() { 600 return (AbstractAction) this.getRootPane().getActionMap().get(escapeKeyAction); 601 } 602 603 /** 604 * Bind the Escape key to an action that closes the window. 605 * <p> 606 * If closesWindow is true, this method creates an action that triggers the 607 * "window is closing" event; otherwise this method removes any actions from 608 * the Escape key. 609 * 610 * @param closesWindow Create or destroy an action to close the window. 611 * @see java.awt.event.WindowEvent#WINDOW_CLOSING 612 * @see #setEscapeKeyAction(javax.swing.AbstractAction) 613 */ 614 public void setEscapeKeyClosesWindow(boolean closesWindow) { 615 if (closesWindow) { 616 setEscapeKeyAction(new AbstractAction() { 617 618 /** {@inheritDoc} */ 619 @Override 620 public void actionPerformed(ActionEvent ae) { 621 JmriJFrame.this.processWindowEvent(new java.awt.event.WindowEvent(JmriJFrame.this, 622 java.awt.event.WindowEvent.WINDOW_CLOSING)); 623 } 624 }); 625 } else { 626 setEscapeKeyAction(null); 627 } 628 escapeKeyActionClosesWindow = closesWindow; 629 } 630 631 /** 632 * Does the Escape key close the window? 633 * 634 * @return <code>true</code> if Escape key is bound to action created by 635 * setEscapeKeyClosesWindow, <code>false</code> in all other cases. 636 * @see #setEscapeKeyClosesWindow 637 * @see #setEscapeKeyAction 638 */ 639 public boolean getEscapeKeyClosesWindow() { 640 return (escapeKeyActionClosesWindow && getEscapeKeyAction() != null); 641 } 642 643 private int getContainingDisplay(Point location) { 644 // Loop through attached screen to determine which 645 // contains the top-left origin point of this window 646 int si = 0; 647 for (ScreenDimensions sd: getScreenDimensions()) { 648 boolean isOnThisScreen = sd.getBounds().contains(location); 649 log.debug("Is \"{}\" window origin {} located on screen {}? {}", getTitle(), this.getLocation(), sd.getGraphicsDevice().getIDstring(), isOnThisScreen); 650 if (isOnThisScreen) { 651 // We've found the screen that contains this origin 652 return si; 653 } 654 si++; 655 } 656 // As a fall-back, return the first display which is the primary 657 log.debug("Falling back to using the primary display"); 658 return 0; 659 } 660 661 /** 662 * {@inheritDoc} 663 * Provide a maximum frame size that is limited to what can fit on the 664 * screen after toolbars, etc are deducted. 665 * <p> 666 * Some of the methods used here return null pointers on some Java 667 * implementations, however, so this will return the superclasses's maximum 668 * size if the algorithm used here fails. 669 * 670 * @return the maximum window size 671 */ 672 @Override 673 public Dimension getMaximumSize() { 674 // adjust maximum size to full screen minus any toolbars 675 if (GraphicsEnvironment.isHeadless()) { 676 // there are no screens 677 return new Dimension(0,0); 678 } 679 try { 680 // Try our own algorithm. This throws null-pointer exceptions on 681 // some Java installs, however, for unknown reasons, so be 682 // prepared to fall back. 683 try { 684 int screenNb = getContainingDisplay(this.getLocation()); 685 ScreenDimensions sd = getScreenDimensions().get(screenNb); 686 log.trace("getMaximumSize on screen {} with size {}", screenNb, sd.getBounds()); 687 int widthInset = sd.getInsets().right + sd.getInsets().left; 688 int heightInset = sd.getInsets().top + sd.getInsets().bottom; 689 690 // If insets are zero, guess based on system type 691 if (widthInset == 0 && heightInset == 0) { 692 String osName = SystemType.getOSName(); 693 if (SystemType.isLinux()) { 694 // Linux generally has a bar across the top and/or bottom 695 // of the screen, but lets you have the full width. 696 // Linux generally has a bar across the top and/or bottom 697 // of the main screen, but lets you have the full width. 698 if ( screenNb == 0) { 699 heightInset = 70; 700 } 701 } // Windows generally has values, but not always, 702 // so we provide observed values just in case 703 else if (osName.equals("Windows XP") || osName.equals("Windows 98") 704 || osName.equals("Windows 2000")) { 705 heightInset = 28; // bottom 28 706 } 707 } 708 709 // Insets may also be provided as system parameters 710 String sw = System.getProperty("jmri.inset.width"); 711 if (sw != null) { 712 try { 713 widthInset = Integer.parseInt(sw); 714 } catch (NumberFormatException e1) { 715 log.error("Error parsing jmri.inset.width: {}", e1.getMessage()); 716 } 717 } 718 String sh = System.getProperty("jmri.inset.height"); 719 if (sh != null) { 720 try { 721 heightInset = Integer.parseInt(sh); 722 } catch (NumberFormatException e1) { 723 log.error("Error parsing jmri.inset.height: {}", e1.getMessage()); 724 } 725 } 726 727 // calculate size as screen size minus space needed for offsets 728 log.trace("getMaximumSize returns normally {},{}", (sd.getBounds().width - widthInset), (sd.getBounds().height - heightInset)); 729 return new Dimension(sd.getBounds().width - widthInset, sd.getBounds().height - heightInset); 730 731 } catch (NoSuchMethodError e) { 732 Dimension screen = getToolkit().getScreenSize(); 733 log.trace("getMaximumSize returns approx due to failure {},{}", screen.width, screen.height); 734 return new Dimension(screen.width, screen.height - 45); // approximate this... 735 } 736 } catch (RuntimeException e2) { 737 // failed completely, fall back to standard method 738 log.trace("getMaximumSize returns super due to failure {}", super.getMaximumSize()); 739 return super.getMaximumSize(); 740 } 741 } 742 743 /** 744 * {@inheritDoc} 745 * The preferred size must fit on the physical screen, so calculate the 746 * lesser of either the preferred size from the layout or the screen size. 747 * 748 * @return the preferred size or the maximum size, whichever is smaller 749 */ 750 @Override 751 public Dimension getPreferredSize() { 752 // limit preferred size to size of screen (from getMaximumSize()) 753 Dimension screen = getMaximumSize(); 754 int width = Math.min(super.getPreferredSize().width, screen.width); 755 int height = Math.min(super.getPreferredSize().height, screen.height); 756 log.debug("getPreferredSize \"{}\" returns width {} height {}", getTitle(), width, height); 757 return new Dimension(width, height); 758 } 759 760 /** 761 * Get a List of the currently-existing JmriJFrame objects. The returned 762 * list is a copy made at the time of the call, so it can be manipulated as 763 * needed by the caller. 764 * 765 * @return a list of JmriJFrame instances. If there are no instances, an 766 * empty list is returned. 767 */ 768 @Nonnull 769 public static List<JmriJFrame> getFrameList() { 770 JmriJFrameManager m = getJmriJFrameManager(); 771 synchronized (m) { 772 return new ArrayList<>(m); 773 } 774 } 775 776 /** 777 * Get a list of currently-existing JmriJFrame objects that are specific 778 * sub-classes of JmriJFrame. 779 * <p> 780 * The returned list is a copy made at the time of the call, so it can be 781 * manipulated as needed by the caller. 782 * 783 * @param <T> generic JmriJframe. 784 * @param type The Class the list should be limited to. 785 * @return An ArrayList of Frames. 786 */ 787 @SuppressWarnings("unchecked") // cast in add() checked at run time 788 public static <T extends JmriJFrame> List<T> getFrameList(@Nonnull Class<T> type) { 789 List<T> result = new ArrayList<>(); 790 JmriJFrameManager m = getJmriJFrameManager(); 791 synchronized (m) { 792 m.stream().filter((f) -> (type.isInstance(f))).forEachOrdered((f) -> 793 { 794 result.add((T)f); 795 }); 796 } 797 return result; 798 } 799 800 /** 801 * Get a JmriJFrame of a particular name. If more than one exists, there's 802 * no guarantee as to which is returned. 803 * 804 * @param name the name of one or more JmriJFrame objects 805 * @return a JmriJFrame with the matching name or null if no matching frames 806 * exist 807 */ 808 public static JmriJFrame getFrame(String name) { 809 for (JmriJFrame j : getFrameList()) { 810 if (j.getTitle().equals(name)) { 811 return j; 812 } 813 } 814 return null; 815 } 816 817 /** 818 * Set whether the frame Position is saved or not after it has been created. 819 * 820 * @param save true if the frame position should be saved. 821 */ 822 public void setSavePosition(boolean save) { 823 reuseFrameSavedPosition = save; 824 InstanceManager.getOptionalDefault(UserPreferencesManager.class).ifPresent(prefsMgr -> { 825 prefsMgr.setSaveWindowLocation(windowFrameRef, save); 826 }); 827 } 828 829 /** 830 * Set whether the frame Size is saved or not after it has been created. 831 * 832 * @param save true if the frame size should be saved. 833 */ 834 public void setSaveSize(boolean save) { 835 reuseFrameSavedSized = save; 836 InstanceManager.getOptionalDefault(UserPreferencesManager.class).ifPresent(prefsMgr -> { 837 prefsMgr.setSaveWindowSize(windowFrameRef, save); 838 }); 839 } 840 841 /** 842 * Returns if the frame Position is saved or not. 843 * 844 * @return true if the frame position should be saved 845 */ 846 public boolean getSavePosition() { 847 return reuseFrameSavedPosition; 848 } 849 850 /** 851 * Returns if the frame Size is saved or not. 852 * 853 * @return true if the frame size should be saved 854 */ 855 public boolean getSaveSize() { 856 return reuseFrameSavedSized; 857 } 858 859 /** 860 * {@inheritDoc} 861 * A frame is considered "modified" if it has changes that have not been 862 * stored. 863 */ 864 @Override 865 public void setModifiedFlag(boolean flag) { 866 this.modifiedFlag = flag; 867 // mark the window in the GUI 868 markWindowModified(this.modifiedFlag); 869 } 870 871 /** {@inheritDoc} */ 872 @Override 873 public boolean getModifiedFlag() { 874 return modifiedFlag; 875 } 876 877 private boolean modifiedFlag = false; 878 879 /** 880 * Handle closing a window or quiting the program while the modified bit was 881 * set. 882 */ 883 protected void handleModified() { 884 if (getModifiedFlag()) { 885 this.setVisible(true); 886 int result = JmriJOptionPane.showOptionDialog(this, Bundle.getMessage("WarnChangedMsg"), 887 Bundle.getMessage("WarningTitle"), JmriJOptionPane.YES_NO_OPTION, 888 JmriJOptionPane.WARNING_MESSAGE, null, // icon 889 new String[]{Bundle.getMessage("WarnYesSave"), Bundle.getMessage("WarnNoClose")}, Bundle 890 .getMessage("WarnYesSave")); 891 if (result == 0 ) { // array option 0 , WarnYesSave 892 // user wants to save 893 storeValues(); 894 } 895 } 896 } 897 898 protected void storeValues() { 899 log.error("default storeValues does nothing for \"{}\"", getTitle()); 900 } 901 902 // For marking the window as modified on Mac OS X 903 // See: https://web.archive.org/web/20090712161630/http://developer.apple.com/qa/qa2001/qa1146.html 904 final static String WINDOW_MODIFIED = "windowModified"; 905 906 public void markWindowModified(boolean yes) { 907 getRootPane().putClientProperty(WINDOW_MODIFIED, yes ? Boolean.TRUE : Boolean.FALSE); 908 } 909 910 // Window methods 911 /** Does nothing in this class */ 912 @Override 913 public void windowOpened(java.awt.event.WindowEvent e) { 914 } 915 916 /** Does nothing in this class */ 917 @Override 918 public void windowClosed(java.awt.event.WindowEvent e) { 919 } 920 921 /** Does nothing in this class */ 922 @Override 923 public void windowActivated(java.awt.event.WindowEvent e) { 924 } 925 926 /** Does nothing in this class */ 927 @Override 928 public void windowDeactivated(java.awt.event.WindowEvent e) { 929 } 930 931 /** Does nothing in this class */ 932 @Override 933 public void windowIconified(java.awt.event.WindowEvent e) { 934 } 935 936 /** Does nothing in this class */ 937 @Override 938 public void windowDeiconified(java.awt.event.WindowEvent e) { 939 } 940 941 /** 942 * {@inheritDoc} 943 * 944 * The JmriJFrame implementation calls {@link #handleModified()}. 945 */ 946 @Override 947 @OverridingMethodsMustInvokeSuper 948 public void windowClosing(java.awt.event.WindowEvent e) { 949 handleModified(); 950 } 951 952 /** Does nothing in this class */ 953 @Override 954 public void componentHidden(java.awt.event.ComponentEvent e) { 955 } 956 957 /** {@inheritDoc} */ 958 @Override 959 public void componentMoved(java.awt.event.ComponentEvent e) { 960 InstanceManager.getOptionalDefault(UserPreferencesManager.class).ifPresent(p -> { 961 if (reuseFrameSavedPosition && isVisible()) { 962 p.setWindowLocation(windowFrameRef, this.getLocation()); 963 } 964 }); 965 } 966 967 /** {@inheritDoc} */ 968 @Override 969 public void componentResized(java.awt.event.ComponentEvent e) { 970 InstanceManager.getOptionalDefault(UserPreferencesManager.class).ifPresent(p -> { 971 if (reuseFrameSavedSized && isVisible()) { 972 saveWindowSize(p); 973 } 974 }); 975 } 976 977 /** Does nothing in this class */ 978 @Override 979 public void componentShown(java.awt.event.ComponentEvent e) { 980 } 981 982 private transient AbstractShutDownTask task = null; 983 984 protected void setShutDownTask() { 985 task = new AbstractShutDownTask(getTitle()) { 986 @Override 987 public Boolean call() { 988 handleModified(); 989 return Boolean.TRUE; 990 } 991 992 @Override 993 public void run() { 994 } 995 }; 996 InstanceManager.getDefault(ShutDownManager.class).register(task); 997 } 998 999 protected boolean reuseFrameSavedPosition = true; 1000 protected boolean reuseFrameSavedSized = true; 1001 1002 /** 1003 * {@inheritDoc} 1004 * 1005 * When window is finally destroyed, remove it from the list of windows. 1006 * <p> 1007 * Subclasses that over-ride this method must invoke this implementation 1008 * with super.dispose() right before returning. 1009 */ 1010 @OverridingMethodsMustInvokeSuper 1011 @Override 1012 public void dispose() { 1013 log.debug("JmriJFrame dispose invoked on {}", getTitle()); 1014 InstanceManager.getOptionalDefault(UserPreferencesManager.class).ifPresent(p -> { 1015 if (reuseFrameSavedPosition) { 1016 p.setWindowLocation(windowFrameRef, this.getLocation()); 1017 } 1018 if (reuseFrameSavedSized) { 1019 saveWindowSize(p); 1020 } 1021 }); 1022 log.debug("dispose \"{}\"", getTitle()); 1023 if (windowInterface != null) { 1024 windowInterface.dispose(); 1025 } 1026 if (task != null) { 1027 jmri.InstanceManager.getDefault(jmri.ShutDownManager.class).deregister(task); 1028 task = null; 1029 } 1030 JmriJFrameManager m = getJmriJFrameManager(); 1031 synchronized (m) { 1032 m.remove(this); 1033 } 1034 1035 // workaround for code that directly calls dispose() 1036 // instead of dispatching a WINDOW_CLOSED event. This 1037 // causes the windowClosing method to not be called. This in turn is an 1038 // issue because people have put code in the windowClosed method that 1039 // should really be in windowClosing. 1040 ThreadingUtil.runOnGUIDelayed(() -> { 1041 removeWindowListener(this); 1042 removeComponentListener(this); 1043 }, 500); 1044 1045 super.dispose(); 1046 } 1047 1048 /* 1049 * Save current window size, do not put adjustments here. Search elsewhere for the problem. 1050 */ 1051 private void saveWindowSize(jmri.UserPreferencesManager p) { 1052 p.setWindowSize(windowFrameRef, super.getSize()); 1053 } 1054 1055 /* 1056 * This field contains a list of properties that do not correspond to the JavaBeans properties coding pattern, or 1057 * known properties that do correspond to that pattern. The default JmriJFrame implementation of 1058 * BeanInstance.hasProperty checks this hashmap before using introspection to find properties corresponding to the 1059 * JavaBean properties coding pattern. 1060 */ 1061 protected HashMap<String, Object> properties = new HashMap<>(); 1062 1063 /** {@inheritDoc} */ 1064 @Override 1065 public void setIndexedProperty(String key, int index, Object value) { 1066 if (BeanUtil.hasIntrospectedProperty(this, key)) { 1067 BeanUtil.setIntrospectedIndexedProperty(this, key, index, value); 1068 } else { 1069 if (!properties.containsKey(key)) { 1070 properties.put(key, new Object[0]); 1071 } 1072 ((Object[]) properties.get(key))[index] = value; 1073 } 1074 } 1075 1076 /** {@inheritDoc} */ 1077 @Override 1078 public Object getIndexedProperty(String key, int index) { 1079 if (properties.containsKey(key) && properties.get(key).getClass().isArray()) { 1080 return ((Object[]) properties.get(key))[index]; 1081 } 1082 return BeanUtil.getIntrospectedIndexedProperty(this, key, index); 1083 } 1084 1085 /** {@inheritDoc} 1086 * Subclasses should override this method with something more direct and faster 1087 */ 1088 @Override 1089 public void setProperty(String key, Object value) { 1090 if (BeanUtil.hasIntrospectedProperty(this, key)) { 1091 BeanUtil.setIntrospectedProperty(this, key, value); 1092 } else { 1093 properties.put(key, value); 1094 } 1095 } 1096 1097 /** {@inheritDoc} 1098 * Subclasses should override this method with something more direct and faster 1099 */ 1100 @Override 1101 public Object getProperty(String key) { 1102 if (properties.containsKey(key)) { 1103 return properties.get(key); 1104 } 1105 return BeanUtil.getIntrospectedProperty(this, key); 1106 } 1107 1108 /** {@inheritDoc} */ 1109 @Override 1110 public boolean hasProperty(String key) { 1111 return (properties.containsKey(key) || BeanUtil.hasIntrospectedProperty(this, key)); 1112 } 1113 1114 /** {@inheritDoc} */ 1115 @Override 1116 public boolean hasIndexedProperty(String key) { 1117 return ((this.properties.containsKey(key) && this.properties.get(key).getClass().isArray()) 1118 || BeanUtil.hasIntrospectedIndexedProperty(this, key)); 1119 } 1120 1121 protected transient WindowInterface windowInterface = null; 1122 1123 /** {@inheritDoc} */ 1124 @Override 1125 public void show(JmriPanel child, JmriAbstractAction action) { 1126 if (null != windowInterface) { 1127 windowInterface.show(child, action); 1128 } 1129 } 1130 1131 /** {@inheritDoc} */ 1132 @Override 1133 public void show(JmriPanel child, JmriAbstractAction action, Hint hint) { 1134 if (null != windowInterface) { 1135 windowInterface.show(child, action, hint); 1136 } 1137 } 1138 1139 /** {@inheritDoc} */ 1140 @Override 1141 public boolean multipleInstances() { 1142 if (null != windowInterface) { 1143 return windowInterface.multipleInstances(); 1144 } 1145 return false; 1146 } 1147 1148 public void setWindowInterface(WindowInterface wi) { 1149 windowInterface = wi; 1150 } 1151 1152 public WindowInterface getWindowInterface() { 1153 return windowInterface; 1154 } 1155 1156 /** {@inheritDoc} */ 1157 @Override 1158 public Set<String> getPropertyNames() { 1159 Set<String> names = new HashSet<>(); 1160 names.addAll(properties.keySet()); 1161 names.addAll(BeanUtil.getIntrospectedPropertyNames(this)); 1162 return names; 1163 } 1164 1165 public void setAllowInFrameServlet(boolean allow) { 1166 allowInFrameServlet = allow; 1167 } 1168 1169 public boolean getAllowInFrameServlet() { 1170 return allowInFrameServlet; 1171 } 1172 1173 /** {@inheritDoc} */ 1174 @Override 1175 public Frame getFrame() { 1176 return this; 1177 } 1178 1179 private static JmriJFrameManager getJmriJFrameManager() { 1180 return InstanceManager.getOptionalDefault(JmriJFrameManager.class).orElseGet(() -> { 1181 return InstanceManager.setDefault(JmriJFrameManager.class, new JmriJFrameManager()); 1182 }); 1183 } 1184 1185 /** 1186 * A list container of JmriJFrame objects. Not a straight ArrayList, but a 1187 * specific class so that the {@link jmri.InstanceManager} can be used to 1188 * retain the reference to the list instead of relying on a static variable. 1189 */ 1190 private static class JmriJFrameManager extends ArrayList<JmriJFrame> { 1191 1192 } 1193 1194 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(JmriJFrame.class); 1195 1196}