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