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