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