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