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