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