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}