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