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