001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.tools;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Component;
007import java.awt.Dimension;
008import java.awt.GraphicsDevice;
009import java.awt.GraphicsEnvironment;
010import java.awt.Point;
011import java.awt.Rectangle;
012import java.awt.Toolkit;
013import java.awt.Window;
014import java.util.regex.Matcher;
015import java.util.regex.Pattern;
016
017import org.openstreetmap.josm.Main;
018
019/**
020 * This is a helper class for persisting the geometry of a JOSM window to the preference store
021 * and for restoring it from the preference store.
022 *
023 */
024public class WindowGeometry {
025
026    /**
027     * Replies a window geometry object for a window with a specific size which is
028     * centered on screen, where main window is
029     *
030     * @param extent  the size
031     * @return the geometry object
032     */
033    public static WindowGeometry centerOnScreen(Dimension extent) {
034        return centerOnScreen(extent, "gui.geometry");
035    }
036
037    /**
038     * Replies a window geometry object for a window with a specific size which is
039     * centered on screen where the corresponding window is.
040     *
041     * @param extent  the size
042     * @param preferenceKey the key to get window size and position from, null value format
043     * for whole virtual screen
044     * @return the geometry object
045     */
046    public static WindowGeometry centerOnScreen(Dimension extent, String preferenceKey) {
047        Rectangle size = preferenceKey != null ? getScreenInfo(preferenceKey)
048            : getFullScreenInfo();
049        Point topLeft = new Point(
050                size.x + Math.max(0, (size.width - extent.width) /2),
051                size.y + Math.max(0, (size.height - extent.height) /2)
052        );
053        return new WindowGeometry(topLeft, extent);
054    }
055
056    /**
057     * Replies a window geometry object for a window with a specific size which is centered
058     * relative to the parent window of a reference component.
059     *
060     * @param reference the reference component.
061     * @param extent the size
062     * @return the geometry object
063     */
064    public static WindowGeometry centerInWindow(Component reference, Dimension extent) {
065        Window parentWindow = null;
066        while(reference != null && ! (reference instanceof Window) ) {
067            reference = reference.getParent();
068        }
069        if (reference == null)
070            return new WindowGeometry(new Point(0,0), extent);
071        parentWindow = (Window)reference;
072        Point topLeft = new Point(
073                Math.max(0, (parentWindow.getSize().width - extent.width) /2),
074                Math.max(0, (parentWindow.getSize().height - extent.height) /2)
075        );
076        topLeft.x += parentWindow.getLocation().x;
077        topLeft.y += parentWindow.getLocation().y;
078        return new WindowGeometry(topLeft, extent);
079    }
080
081    /**
082     * Exception thrown by the WindowGeometry class if something goes wrong
083     */
084    public static class WindowGeometryException extends Exception {
085        public WindowGeometryException(String message, Throwable cause) {
086            super(message, cause);
087        }
088
089        public WindowGeometryException(String message) {
090            super(message);
091        }
092    }
093
094    /** the top left point */
095    private Point topLeft;
096    /** the size */
097    private Dimension extent;
098
099    /**
100     * Creates a window geometry from a position and dimension
101     *
102     * @param topLeft the top left point
103     * @param extent the extent
104     */
105    public WindowGeometry(Point topLeft, Dimension extent) {
106        this.topLeft = topLeft;
107        this.extent = extent;
108    }
109
110    /**
111     * Creates a window geometry from a rectangle
112     *
113     * @param rect the position
114     */
115    public WindowGeometry(Rectangle rect) {
116        this.topLeft = rect.getLocation();
117        this.extent = rect.getSize();
118    }
119
120    /**
121     * Creates a window geometry from the position and the size of a window.
122     *
123     * @param window the window
124     */
125    public WindowGeometry(Window window)  {
126        this(window.getLocationOnScreen(), window.getSize());
127    }
128
129    /**
130     * Fixes a window geometry to shift to the correct screen.
131     *
132     * @param window the window
133     */
134    public void fixScreen(Window window)  {
135        Rectangle oldScreen = getScreenInfo(getRectangle());
136        Rectangle newScreen = getScreenInfo(new Rectangle(window.getLocationOnScreen(), window.getSize()));
137        if(oldScreen.x != newScreen.x) {
138            this.topLeft.x += newScreen.x - oldScreen.x;
139        }
140        if(oldScreen.y != newScreen.y) {
141            this.topLeft.y += newScreen.y - oldScreen.y;
142        }
143    }
144
145    protected int parseField(String preferenceKey, String preferenceValue, String field) throws WindowGeometryException {
146        String v = "";
147        try {
148            Pattern p = Pattern.compile(field + "=(-?\\d+)",Pattern.CASE_INSENSITIVE);
149            Matcher m = p.matcher(preferenceValue);
150            if (!m.find())
151                throw new WindowGeometryException(tr("Preference with key ''{0}'' does not include ''{1}''. Cannot restore window geometry from preferences.", preferenceKey, field));
152            v = m.group(1);
153            return Integer.parseInt(v);
154        } catch(WindowGeometryException e) {
155            throw e;
156        } catch(NumberFormatException e) {
157            throw new WindowGeometryException(tr("Preference with key ''{0}'' does not provide an int value for ''{1}''. Got {2}. Cannot restore window geometry from preferences.", preferenceKey, field, v));
158        } catch(Exception e) {
159            throw new WindowGeometryException(tr("Failed to parse field ''{1}'' in preference with key ''{0}''. Exception was: {2}. Cannot restore window geometry from preferences.", preferenceKey, field, e.toString()), e);
160        }
161    }
162
163    protected final void initFromPreferences(String preferenceKey) throws WindowGeometryException {
164        String value = Main.pref.get(preferenceKey);
165        if (value == null || value.isEmpty())
166            throw new WindowGeometryException(tr("Preference with key ''{0}'' does not exist. Cannot restore window geometry from preferences.", preferenceKey));
167        topLeft = new Point();
168        extent = new Dimension();
169        topLeft.x = parseField(preferenceKey, value, "x");
170        topLeft.y = parseField(preferenceKey, value, "y");
171        extent.width = parseField(preferenceKey, value, "width");
172        extent.height = parseField(preferenceKey, value, "height");
173    }
174
175    protected final void initFromWindowGeometry(WindowGeometry other) {
176        this.topLeft = other.topLeft;
177        this.extent = other.extent;
178    }
179
180    public static WindowGeometry mainWindow(String preferenceKey, String arg, boolean maximize) {
181        Rectangle screenDimension = getScreenInfo("gui.geometry");
182        if (arg != null) {
183            final Matcher m = Pattern.compile("(\\d+)x(\\d+)(([+-])(\\d+)([+-])(\\d+))?").matcher(arg);
184            if (m.matches()) {
185                int w = Integer.valueOf(m.group(1));
186                int h = Integer.valueOf(m.group(2));
187                int x = screenDimension.x, y = screenDimension.y;
188                if (m.group(3) != null) {
189                    x = Integer.valueOf(m.group(5));
190                    y = Integer.valueOf(m.group(7));
191                    if ("-".equals(m.group(4))) {
192                        x = screenDimension.x + screenDimension.width - x - w;
193                    }
194                    if ("-".equals(m.group(6))) {
195                        y = screenDimension.y + screenDimension.height - y - h;
196                    }
197                }
198                return new WindowGeometry(new Point(x,y), new Dimension(w,h));
199            } else {
200                Main.warn(tr("Ignoring malformed geometry: {0}", arg));
201            }
202        }
203        WindowGeometry def;
204        if(maximize) {
205            def = new WindowGeometry(screenDimension);
206        } else {
207            Point p = screenDimension.getLocation();
208            p.x += (screenDimension.width-1000)/2;
209            p.y += (screenDimension.height-740)/2;
210            def = new WindowGeometry(p, new Dimension(1000, 740));
211        }
212        return new WindowGeometry(preferenceKey, def);
213    }
214
215    /**
216     * Creates a window geometry from the values kept in the preference store under the
217     * key <code>preferenceKey</code>
218     *
219     * @param preferenceKey the preference key
220     * @throws WindowGeometryException thrown if no such key exist or if the preference value has
221     * an illegal format
222     */
223    public WindowGeometry(String preferenceKey) throws WindowGeometryException {
224        initFromPreferences(preferenceKey);
225    }
226
227    /**
228     * Creates a window geometry from the values kept in the preference store under the
229     * key <code>preferenceKey</code>. Falls back to the <code>defaultGeometry</code> if
230     * something goes wrong.
231     *
232     * @param preferenceKey the preference key
233     * @param defaultGeometry the default geometry
234     *
235     */
236    public WindowGeometry(String preferenceKey, WindowGeometry defaultGeometry) {
237        try {
238            initFromPreferences(preferenceKey);
239        } catch(WindowGeometryException e) {
240            initFromWindowGeometry(defaultGeometry);
241        }
242    }
243
244    /**
245     * Remembers a window geometry under a specific preference key
246     *
247     * @param preferenceKey the preference key
248     */
249    public void remember(String preferenceKey) {
250        StringBuilder value = new StringBuilder();
251        value.append("x=").append(topLeft.x).append(",")
252        .append("y=").append(topLeft.y).append(",")
253        .append("width=").append(extent.width).append(",")
254        .append("height=").append(extent.height);
255        Main.pref.put(preferenceKey, value.toString());
256    }
257
258    /**
259     * Replies the top left point for the geometry
260     *
261     * @return  the top left point for the geometry
262     */
263    public Point getTopLeft() {
264        return topLeft;
265    }
266
267    /**
268     * Replies the size specified by the geometry
269     *
270     * @return the size specified by the geometry
271     */
272    public Dimension getSize() {
273        return extent;
274    }
275
276    /**
277     * Replies the size and position specified by the geometry
278     *
279     * @return the size and position specified by the geometry
280     */
281    private Rectangle getRectangle() {
282        return new Rectangle(topLeft, extent);
283    }
284
285    /**
286     * Applies this geometry to a window. Makes sure that the window is not
287     * placed outside of the coordinate range of all available screens.
288     *
289     * @param window the window
290     */
291    public void applySafe(Window window) {
292        Point p = new Point(topLeft);
293        Dimension size = new Dimension(extent);
294
295        Rectangle virtualBounds = getVirtualScreenBounds();
296
297        // Ensure window fit on screen
298
299        if (p.x < virtualBounds.x) {
300            p.x = virtualBounds.x;
301        } else if (p.x > virtualBounds.x + virtualBounds.width - size.width) {
302            p.x = virtualBounds.x + virtualBounds.width - size.width;
303        }
304
305        if (p.y < virtualBounds.y) {
306            p.y = virtualBounds.y;
307        } else if (p.y > virtualBounds.y + virtualBounds.height - size.height) {
308            p.y = virtualBounds.y + virtualBounds.height - size.height;
309        }
310
311        int deltax = (p.x + size.width) - (virtualBounds.x + virtualBounds.width);
312        if (deltax > 0) {
313            size.width -= deltax;
314        }
315
316        int deltay = (p.y + size.height) - (virtualBounds.y + virtualBounds.height);
317        if (deltay > 0) {
318            size.height -= deltay;
319        }
320
321        // Ensure window does not hide taskbar
322
323        Rectangle maxbounds = GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds();
324
325        if (!isBugInMaximumWindowBounds(maxbounds)) {
326            deltax = size.width - maxbounds.width;
327            if (deltax > 0) {
328                size.width -= deltax;
329            }
330
331            deltay = size.height - maxbounds.height;
332            if (deltay > 0) {
333                size.height -= deltay;
334            }
335        }
336        window.setLocation(p);
337        window.setSize(size);
338    }
339
340    /**
341     * Determines if the bug affecting getMaximumWindowBounds() occured.
342     *
343     * @param maxbounds result of getMaximumWindowBounds()
344     * @return {@code true} if the bug happened, {@code false otherwise}
345     *
346     * @see <a href="https://josm.openstreetmap.de/ticket/9699">JOSM-9699</a>
347     * @see <a href="https://bugs.launchpad.net/ubuntu/+source/openjdk-7/+bug/1171563">Ubuntu-1171563</a>
348     * @see <a href="http://icedtea.classpath.org/bugzilla/show_bug.cgi?id=1669">IcedTea-1669</a>
349     * @see <a href="https://bugs.openjdk.java.net/browse/JI-9010334">JI-9010334</a>
350     */
351    protected static boolean isBugInMaximumWindowBounds(Rectangle maxbounds) {
352        return maxbounds.width <= 0 || maxbounds.height <= 0;
353    }
354
355    /**
356     * Computes the virtual bounds of graphics environment, as an union of all screen bounds.
357     * @return The virtual bounds of graphics environment, as an union of all screen bounds.
358     * @since 6522
359     */
360    public static Rectangle getVirtualScreenBounds() {
361        Rectangle virtualBounds = new Rectangle();
362        GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
363        for (GraphicsDevice gd : ge.getScreenDevices()) {
364            if (gd.getType() == GraphicsDevice.TYPE_RASTER_SCREEN) {
365                virtualBounds = virtualBounds.union(gd.getDefaultConfiguration().getBounds());
366            }
367        }
368        return virtualBounds;
369    }
370
371    /**
372     * Find the size and position of the screen for given coordinates. Use first screen,
373     * when no coordinates are stored or null is passed.
374     *
375     * @param preferenceKey the key to get size and position from
376     * @return bounds of the screen
377     */
378    public static Rectangle getScreenInfo(String preferenceKey) {
379        Rectangle g = new WindowGeometry(preferenceKey,
380            /* default: something on screen 1 */
381            new WindowGeometry(new Point(0,0), new Dimension(10,10))).getRectangle();
382        return getScreenInfo(g);
383    }
384
385    /**
386     * Find the size and position of the screen for given coordinates. Use first screen,
387     * when no coordinates are stored or null is passed.
388     *
389     * @param g coordinates to check
390     * @return bounds of the screen
391     */
392    private static Rectangle getScreenInfo(Rectangle g) {
393        GraphicsEnvironment ge = GraphicsEnvironment
394                .getLocalGraphicsEnvironment();
395        GraphicsDevice[] gs = ge.getScreenDevices();
396        int intersect = 0;
397        Rectangle bounds = null;
398        for (GraphicsDevice gd : gs) {
399            if (gd.getType() == GraphicsDevice.TYPE_RASTER_SCREEN) {
400                Rectangle b = gd.getDefaultConfiguration().getBounds();
401                if (b.height > 0 && b.width / b.height >= 3) /* multiscreen with wrong definition */ {
402                    b.width /= 2;
403                    Rectangle is = b.intersection(g);
404                    int s = is.width * is.height;
405                    if (bounds == null || intersect < s) {
406                        intersect = s;
407                        bounds = b;
408                    }
409                    b = new Rectangle(b);
410                    b.x += b.width;
411                    is = b.intersection(g);
412                    s = is.width * is.height;
413                    if (bounds == null || intersect < s) {
414                        intersect = s;
415                        bounds = b;
416                    }
417                } else {
418                    Rectangle is = b.intersection(g);
419                    int s = is.width * is.height;
420                    if (bounds == null || intersect < s) {
421                        intersect = s;
422                        bounds = b;
423                    }
424                }
425            }
426        }
427        return bounds;
428    }
429
430    /**
431     * Find the size of the full virtual screen.
432     * @return size of the full virtual screen
433     */
434    public static Rectangle getFullScreenInfo() {
435        return new Rectangle(new Point(0,0), Toolkit.getDefaultToolkit().getScreenSize());
436    }
437
438    @Override
439    public String toString() {
440        return "WindowGeometry{topLeft="+topLeft+",extent="+extent+"}";
441    }
442}