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