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