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