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