001package jmri.util;
002
003import java.awt.Component;
004import java.awt.Dimension;
005import java.awt.DisplayMode;
006import java.awt.GraphicsDevice;
007import java.awt.GraphicsEnvironment;
008import java.awt.IllegalComponentStateException;
009import java.awt.Point;
010import java.awt.Window;
011//import java.util.ArrayList;
012import org.slf4j.Logger;
013import org.slf4j.LoggerFactory;
014
015import jmri.InstanceManager;
016import jmri.InstanceManagerAutoDefault;
017
018/**
019 * Position a Window relative to a component in another window so as
020 * to not obscure a component in that window. Typically, the Component
021 * is being edited by actions done in the target Window.\p
022 * Note the assumption in multiple screen environments is the screens
023 * are configured horizontally.
024 *
025 * @author Pete Cressman Copyright (C) 2018
026 * @since 4.13.1
027 */
028public class PlaceWindow implements InstanceManagerAutoDefault {
029    static GraphicsEnvironment _environ = GraphicsEnvironment.getLocalGraphicsEnvironment();
030    static Dimension _screenSize[];
031    static Dimension _totalScreenDim = new Dimension(0, 0);
032
033    public PlaceWindow() {
034        getScreens();
035    }
036    private void getScreens() {
037        GraphicsDevice[] gd = _environ.getScreenDevices();
038        _screenSize = new Dimension[gd.length];
039        int maxHeight = 0;
040        for (int i = 0; i < gd.length; i++) {
041            String deviceID = gd[i].getIDstring();
042            DisplayMode dm = gd[i].getDisplayMode();
043            _screenSize[i] = new Dimension(dm.getWidth(), dm.getHeight());
044            _totalScreenDim.width += dm.getWidth();          // assuming screens are horizontal
045            maxHeight = Math.max(maxHeight, dm.getHeight()); // use maximum height
046            if (log.isDebugEnabled()) {
047                log.debug("\"Screen # {} deviceID= {}: width= {}, height= {}",
048                        i, deviceID, dm.getWidth(), dm.getHeight());
049            }
050        }
051        _totalScreenDim.height = maxHeight;
052        if (log.isDebugEnabled()) {
053            try {
054                GraphicsDevice dgd = _environ.getDefaultScreenDevice();
055                DisplayMode dm = dgd.getDisplayMode();
056                log.debug("\"DefaultScreen= {}: width= {}, height= {}", dgd.getIDstring(), dm.getWidth(), dm.getHeight());
057                log.debug("\"Total Screen size: width= {}, height= {}", _totalScreenDim.width, _totalScreenDim.height);
058             } catch (java.awt.IllegalComponentStateException icse ) {
059                 log.debug( "unable to construct debug information due to illegal component state");
060             }
061        }
062    }
063
064    public static PlaceWindow getDefault() {
065        return InstanceManager.getOptionalDefault(PlaceWindow.class).orElseGet(() -> {
066            return InstanceManager.setDefault(PlaceWindow.class, new PlaceWindow());
067        });
068    }
069
070    /**
071     * In a possibly multi-monitor environment, find the screen displaying
072     * the window and return its dimensions.
073     * \p
074     * getLocation() and getLocationOnScreen() return the same Point which
075     * has coordinates in the total display area, i.e. all screens combined.
076     * Note DefaultScreen is NOT this total combined display area.
077     *
078     * We assume monitors are aligned horizontally - at least this is the only
079     * configuration possible from Windows settings.
080     *
081     * @param window a window
082     * @return Screen number of window location
083     */
084    public int getScreenNum(Window window) {
085        /* this always has window on device  #0 ??
086        GraphicsDevice windowDevice = window.getGraphicsConfiguration().getDevice();
087        DisplayMode windowDM = windowDevice.getDisplayMode();
088        GraphicsDevice[] gd = _environ.getScreenDevices();
089        for (int i = 0; i < gd.length; i++) {
090            if (gd[i].getDisplayMode().equals(windowDM)) {
091               return i;
092            }
093        }*/
094        int x = 0;
095        try {
096            for (int i = 0;  i < _screenSize.length; i++) {
097                x += _screenSize[i].width;
098                if (window.getLocation().x < x) {
099                    return i;
100                }
101            }
102
103        } catch (IllegalComponentStateException icse) {
104            return 0;
105        }
106        return 0;
107    }
108
109    public Dimension getScreenSize(int screenNum) {
110        if (screenNum >= 0 && screenNum <= _screenSize.length) {
111            return _screenSize[screenNum];
112        }
113        return new Dimension(0, 0);
114    }
115    /**
116     * Find the best place to position the target window next to the component but not
117     * obscuring it. Positions target to the Left, Right, Below or Above. Tries in
118     * that order to keep target within the parent window. If not possible, tries
119     * to keep the target window within the parent's screen. Failing that, will
120     * minimize the amount the target window is off screen.  The method guarantees
121     * a non-null component will not be obscured.\p
122     * If the component is null, the target window is placed beside the parent
123     * window, to the Left, Right, Below or Above it.\b
124     * Should be called after target is packed and <strong>before</strong> target is
125     * set visible.
126     * @param parent Window containing the Component
127     * @param comp Component contained in the parent Window. May be null.
128     * @param target a popup or some kind of window associated with the component
129     *
130     * @return the location Point to open the target window.
131     */
132    public Point nextTo(Window parent, Component comp, Window target) {
133        if (target == null || parent == null) {
134            return new Point(0, 0);
135        }
136        Point loc = findLocation(parent, comp, target);
137        if (log.isDebugEnabled()) {
138            log.debug("return target location: X= {}, Y= {}", loc.x, loc.y);
139        }
140        target.setLocation(loc);
141        return loc;
142    }
143
144    private Point findLocation(Window parent, Component comp, Window target) {
145        Point loc;
146        Point parentLoc = parent.getLocation();
147        Dimension parentDim = parent.getSize();
148        int screenNum = getScreenNum(parent);
149        Dimension parentScreen =getScreenSize(screenNum);
150        Dimension targetDim = target.getPreferredSize();
151        Point compLoc;
152        Dimension compDim;
153        int margin;
154        if (comp != null) {
155            try {
156                compLoc = new Point(comp.getLocationOnScreen());
157            } catch (IllegalComponentStateException icse) {
158                compLoc = comp.getLocation();
159                compLoc = new Point(compLoc.x + parentLoc.x, compLoc.y + parentLoc.y);
160            }
161            compDim = comp.getSize();
162            margin = 20;
163        } else {
164            compLoc = parentLoc;
165            compDim = parentDim;
166            margin = 0;
167        }
168        int num = screenNum - 1;
169        int screenLeft = 0;
170        while (num >= 0) {
171            screenLeft += getScreenSize(num).width;
172            num--;
173        }
174        int screenRight = screenLeft + parentScreen.width;
175        if (log.isDebugEnabled()) {
176            log.debug("parent at loc ({}, {}) is on screen #{}. Size: width= {}, height= {}",
177                    parentLoc.x, parentLoc.y, screenNum, parentDim.width, parentDim.height);
178            log.debug("Component at loc ({}, {}). Size: width= {}, height= {}",
179                    compLoc.x, compLoc.y, compDim.width, compDim.height);
180            log.debug("targetDim: width= {}, height= {}. screenLeft= {}, screen= {} x {}",
181                    targetDim.width, targetDim.height, screenLeft, parentScreen.width, parentScreen.height);
182        }
183
184        // try left or right of Component
185        int xr = compLoc.x + compDim.width + margin;
186        int xl = compLoc.x - targetDim.width - margin;
187        // compute the corresponding vertical offset
188        int hOff = compLoc.y + (compDim.height -  targetDim.height)/2;
189        if (hOff + targetDim.height > parentScreen.height) {
190            hOff = parentScreen.height - targetDim.height;
191        }
192        if (hOff < 0) {
193            hOff = 0;
194        }
195        // try above or below Component
196        int yb = compLoc.y + compDim.height + margin;
197        int ya = compLoc.y - targetDim.height - margin;
198        // compute the corresponding horizontal offset
199        int vOff = compLoc.x + (compDim.width -  targetDim.width)/2;
200        if (vOff + targetDim.width > parentScreen.width - targetDim.width) {
201            vOff = parentScreen.width - targetDim.width;
202        }
203        if (vOff < screenLeft) {
204            vOff = screenLeft;
205        }
206        if (log.isDebugEnabled()) {
207            log.debug("UpperleftCorners: xl=({},{}), xr=({},{}), yb=({},{}), ya=({},{})",
208                    xl,hOff, xr,hOff, vOff,yb, vOff,ya);
209        }
210
211        // try to keep completely within the parent window
212        if (xl >= parentLoc.x){
213            return new Point(xl, hOff);
214        } else if ((xr + targetDim.width <= parentLoc.x + parentDim.width)) {
215            return new Point(xr, hOff);
216        } else if (yb + targetDim.height <= parentLoc.y + parentDim.height) {
217            return new Point(vOff, yb);
218        } else if (ya >= parentLoc.y) {
219            if (ya < 0) {
220                ya = 0;
221            }
222            return new Point(vOff, ya);
223        }
224        // none were entirely within the parent window
225
226        // try to keep completely within the parent screen
227        if (log.isDebugEnabled()) {
228            log.debug("Off screen: left= {}, right = {}, below= {}, above= {}",
229                    xl, xr, yb, ya);
230        }
231        if (xl > screenLeft){
232            return new Point(xl, hOff);
233        } else if (xr + targetDim.width <= screenRight) {
234            return new Point(xr, hOff);
235        } else if (yb + targetDim.height <= parentScreen.height) {
236            return new Point(vOff, yb);
237        } else if (ya >= 0) {
238            return new Point(vOff, ya);
239        }
240
241        // none were entirely within the parent screen.
242        // position, but insure target stays on the total screen
243        if (log.isDebugEnabled()) log.debug("Outside: widthUpToParent= {},  _totalScreenWidth= {}, screenHeight={}",
244                parentLoc.x, _totalScreenDim.width, parentScreen.height);
245        int offScreen = screenLeft - xl;
246        int minOff = offScreen;
247        log.debug("offScreen= {} minOff= {}, xl= {}", offScreen, minOff, xl);
248        if (xl < 0) {
249            xl = 0;
250        }
251        loc = new Point(xl, hOff);
252
253        offScreen = xr + targetDim.width - screenRight;
254        xr = screenRight - targetDim.width;
255        log.debug("offScreen= {}  minOff= {}, xr= {}", offScreen, minOff, xr);
256        if (offScreen < minOff) {
257            minOff = offScreen;
258            loc = new Point(xr, hOff);
259        }
260
261        offScreen = (yb + targetDim.height) - parentScreen.height;
262        yb = parentScreen.height - targetDim.height;
263        log.debug("offScreen= {} minOff = {}, yb= {}", offScreen, minOff, yb);
264        if (offScreen < minOff) {
265            minOff = offScreen;
266            if (yb < 0) {
267                yb = 0;
268            }
269            loc = new Point(vOff, yb);
270        }
271
272        offScreen = -ya;        // !(ya >= 0)
273        log.debug("offScreen= {} minOff = {}, ya= {}", offScreen, minOff, ya);
274        if (offScreen < minOff) {
275            ya = 0;
276            loc = new Point(vOff, ya);
277        }
278
279        return loc;
280    }
281
282    private final static Logger log = LoggerFactory.getLogger(PlaceWindow.class);
283}