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