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