001    // License: GPL. For details, see LICENSE file.
002    package org.openstreetmap.josm.tools;
003    
004    import static org.openstreetmap.josm.tools.I18n.tr;
005    
006    import java.awt.Component;
007    import java.awt.Dimension;
008    import java.awt.GraphicsConfiguration;
009    import java.awt.GraphicsDevice;
010    import java.awt.GraphicsEnvironment;
011    import java.awt.Point;
012    import java.awt.Rectangle;
013    import java.awt.Toolkit;
014    import java.awt.Window;
015    import java.util.regex.Matcher;
016    import java.util.regex.Pattern;
017    
018    import org.openstreetmap.josm.Main;
019    
020    /**
021     * This is a helper class for persisting the geometry of a JOSM window to the preference store
022     * and for restoring it from the preference store.
023     *
024     */
025    public class WindowGeometry {
026    
027        /**
028         * Replies a window geometry object for a window with a specific size which is
029         * centered on screen, where main window is
030         *
031         * @param extent  the size
032         * @return the geometry object
033         */
034        static public WindowGeometry centerOnScreen(Dimension extent) {
035            return centerOnScreen(extent, "gui.geometry");
036        }
037    
038        /**
039         * Replies a window geometry object for a window with a specific size which is
040         * centered on screen where the corresponding window is.
041         *
042         * @param extent  the size
043         * @param preferenceKey the key to get window size and position from, null value format
044         * for whole virtual screen
045         * @return the geometry object
046         */
047        static public WindowGeometry centerOnScreen(Dimension extent, String preferenceKey) {
048            Rectangle size = preferenceKey != null ? getScreenInfo(preferenceKey)
049                : getFullScreenInfo();
050            Point topLeft = new Point(
051                    size.x + Math.max(0, (size.width - extent.width) /2),
052                    size.y + Math.max(0, (size.height - extent.height) /2)
053            );
054            return new WindowGeometry(topLeft, extent);
055        }
056    
057        /**
058         * Replies a window geometry object for a window with a specific size which is centered
059         * relative to the parent window of a reference component.
060         *
061         * @param reference the reference component.
062         * @param extent the size
063         * @return the geometry object
064         */
065        static public WindowGeometry centerInWindow(Component reference, Dimension extent) {
066            Window parentWindow = null;
067            while(reference != null && ! (reference instanceof Window) ) {
068                reference = reference.getParent();
069            }
070            if (reference == null)
071                return new WindowGeometry(new Point(0,0), extent);
072            parentWindow = (Window)reference;
073            Point topLeft = new Point(
074                    Math.max(0, (parentWindow.getSize().width - extent.width) /2),
075                    Math.max(0, (parentWindow.getSize().height - extent.height) /2)
076            );
077            topLeft.x += parentWindow.getLocation().x;
078            topLeft.y += parentWindow.getLocation().y;
079            return new WindowGeometry(topLeft, extent);
080        }
081    
082        /**
083         * Exception thrown by the WindowGeometry class if something goes wrong
084         */
085        static public class WindowGeometryException extends Exception {
086            public WindowGeometryException(String message, Throwable cause) {
087                super(message, cause);
088            }
089    
090            public WindowGeometryException(String message) {
091                super(message);
092            }
093        }
094    
095        /** the top left point */
096        private Point topLeft;
097        /** the size */
098        private Dimension extent;
099    
100        /**
101         * Creates a window geometry from a position and dimension
102         *
103         * @param topLeft the top left point
104         * @param extent the extent
105         */
106        public WindowGeometry(Point topLeft, Dimension extent) {
107            this.topLeft = topLeft;
108            this.extent = extent;
109        }
110    
111        /**
112         * Creates a window geometry from a rectangle
113         * 
114         * @param rect the position
115         */
116        public WindowGeometry(Rectangle rect) {
117            this.topLeft = rect.getLocation();
118            this.extent = rect.getSize();
119        }
120    
121        /**
122         * Creates a window geometry from the position and the size of a window.
123         *
124         * @param window the window
125         */
126        public WindowGeometry(Window window)  {
127            this(window.getLocationOnScreen(), window.getSize());
128        }
129    
130        /**
131         * Fixes a window geometry to shift to the correct screen.
132         *
133         * @param window the window
134         */
135        public void fixScreen(Window window)  {
136            Rectangle oldScreen = getScreenInfo(getRectangle());
137            Rectangle newScreen = getScreenInfo(new Rectangle(window.getLocationOnScreen(), window.getSize()));
138            if(oldScreen.x != newScreen.x) {
139                this.topLeft.x += newScreen.x - oldScreen.x;
140            }
141            if(oldScreen.y != newScreen.y) {
142                this.topLeft.y += newScreen.y - oldScreen.y;
143            }
144        }
145    
146        protected int parseField(String preferenceKey, String preferenceValue, String field) throws WindowGeometryException {
147            String v = "";
148            try {
149                Pattern p = Pattern.compile(field + "=(-?\\d+)",Pattern.CASE_INSENSITIVE);
150                Matcher m = p.matcher(preferenceValue);
151                if (!m.find())
152                    throw new WindowGeometryException(tr("Preference with key ''{0}'' does not include ''{1}''. Cannot restore window geometry from preferences.", preferenceKey, field));
153                v = m.group(1);
154                return Integer.parseInt(v);
155            } catch(WindowGeometryException e) {
156                throw e;
157            } catch(NumberFormatException e) {
158                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));
159            } catch(Exception e) {
160                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);
161            }
162        }
163    
164        protected void initFromPreferences(String preferenceKey) throws WindowGeometryException {
165            String value = Main.pref.get(preferenceKey);
166            if (value == null || value.equals(""))
167                throw new WindowGeometryException(tr("Preference with key ''{0}'' does not exist. Cannot restore window geometry from preferences.", preferenceKey));
168            topLeft = new Point();
169            extent = new Dimension();
170            topLeft.x = parseField(preferenceKey, value, "x");
171            topLeft.y = parseField(preferenceKey, value, "y");
172            extent.width = parseField(preferenceKey, value, "width");
173            extent.height = parseField(preferenceKey, value, "height");
174        }
175    
176        protected void initFromWindowGeometry(WindowGeometry other) {
177            this.topLeft = other.topLeft;
178            this.extent = other.extent;
179        }
180    
181        static public WindowGeometry mainWindow(String preferenceKey, String arg, boolean maximize) {
182            Rectangle screenDimension = getScreenInfo("gui.geometry");
183            if (arg != null) {
184                final Matcher m = Pattern.compile("(\\d+)x(\\d+)(([+-])(\\d+)([+-])(\\d+))?").matcher(arg);
185                if (m.matches()) {
186                    int w = Integer.valueOf(m.group(1));
187                    int h = Integer.valueOf(m.group(2));
188                    int x = screenDimension.x, y = screenDimension.y;
189                    if (m.group(3) != null) {
190                        x = Integer.valueOf(m.group(5));
191                        y = Integer.valueOf(m.group(7));
192                        if (m.group(4).equals("-")) {
193                            x = screenDimension.x + screenDimension.width - x - w;
194                        }
195                        if (m.group(6).equals("-")) {
196                            y = screenDimension.y + screenDimension.height - y - h;
197                        }
198                    }
199                    return new WindowGeometry(new Point(x,y), new Dimension(w,h));
200                } else {
201                    Main.warn(tr("Ignoring malformed geometry: {0}", arg));
202                }
203            }
204            WindowGeometry def;
205            if(maximize) {
206                def = new WindowGeometry(screenDimension);
207            } else {
208                Point p = screenDimension.getLocation();
209                p.x += (screenDimension.width-1000)/2;
210                p.y += (screenDimension.height-740)/2;
211                def = new WindowGeometry(p, new Dimension(1000, 740));
212            }
213            return new WindowGeometry(preferenceKey, def);
214        }
215    
216        /**
217         * Creates a window geometry from the values kept in the preference store under the
218         * key <code>preferenceKey</code>
219         *
220         * @param preferenceKey the preference key
221         * @throws WindowGeometryException thrown if no such key exist or if the preference value has
222         * an illegal format
223         */
224        public WindowGeometry(String preferenceKey) throws WindowGeometryException {
225            initFromPreferences(preferenceKey);
226        }
227    
228        /**
229         * Creates a window geometry from the values kept in the preference store under the
230         * key <code>preferenceKey</code>. Falls back to the <code>defaultGeometry</code> if
231         * something goes wrong.
232         *
233         * @param preferenceKey the preference key
234         * @param defaultGeometry the default geometry
235         *
236         */
237        public WindowGeometry(String preferenceKey, WindowGeometry defaultGeometry) {
238            try {
239                initFromPreferences(preferenceKey);
240            } catch(WindowGeometryException e) {
241                initFromWindowGeometry(defaultGeometry);
242            }
243        }
244    
245        /**
246         * Remembers a window geometry under a specific preference key
247         *
248         * @param preferenceKey the preference key
249         */
250        public void remember(String preferenceKey) {
251            StringBuffer value = new StringBuffer();
252            value.append("x=").append(topLeft.x).append(",")
253            .append("y=").append(topLeft.y).append(",")
254            .append("width=").append(extent.width).append(",")
255            .append("height=").append(extent.height);
256            Main.pref.put(preferenceKey, value.toString());
257        }
258    
259        /**
260         * Replies the top left point for the geometry
261         *
262         * @return  the top left point for the geometry
263         */
264        public Point getTopLeft() {
265            return topLeft;
266        }
267    
268        /**
269         * Replies the size specified by the geometry
270         *
271         * @return the size specified by the geometry
272         */
273        public Dimension getSize() {
274            return extent;
275        }
276    
277        /**
278         * Replies the size and position specified by the geometry
279         *
280         * @return the size and position specified by the geometry
281         */
282        private Rectangle getRectangle() {
283            return new Rectangle(topLeft, extent);
284        }
285    
286        /**
287         * Applies this geometry to a window. Makes sure that the window is not
288         * placed outside of the coordinate range of all available screens.
289         * 
290         * @param window the window
291         */
292        public void applySafe(Window window) {
293            Point p = new Point(topLeft);
294    
295            Rectangle virtualBounds = new Rectangle();
296            GraphicsEnvironment ge = GraphicsEnvironment
297                    .getLocalGraphicsEnvironment();
298            GraphicsDevice[] gs = ge.getScreenDevices();
299            for (int j = 0; j < gs.length; j++) {
300                GraphicsDevice gd = gs[j];
301                if (gd.getType() == GraphicsDevice.TYPE_RASTER_SCREEN) {
302                    virtualBounds = virtualBounds.union(gd.getDefaultConfiguration().getBounds());
303                }
304            }
305    
306            if (p.x < virtualBounds.x) {
307                p.x = virtualBounds.x;
308            } else if (p.x > virtualBounds.x + virtualBounds.width - extent.width) {
309                p.x = virtualBounds.x + virtualBounds.width - extent.width;
310            }
311    
312            if (p.y < virtualBounds.y) {
313                p.y = virtualBounds.y;
314            } else if (p.y > virtualBounds.y + virtualBounds.height - extent.height) {
315                p.y = virtualBounds.y + virtualBounds.height - extent.height;
316            }
317    
318            window.setLocation(p);
319            window.setSize(extent);
320        }
321    
322        /**
323         * Find the size and position of the screen for given coordinates. Use first screen,
324         * when no coordinates are stored or null is passed.
325         * 
326         * @param preferenceKey the key to get size and position from
327         * @return bounds of the screen
328         */
329        public static Rectangle getScreenInfo(String preferenceKey) {
330            Rectangle g = new WindowGeometry(preferenceKey,
331                /* default: something on screen 1 */
332                new WindowGeometry(new Point(0,0), new Dimension(10,10))).getRectangle();
333            return getScreenInfo(g);
334        }
335    
336        /**
337         * Find the size and position of the screen for given coordinates. Use first screen,
338         * when no coordinates are stored or null is passed.
339         * 
340         * @param g coordinates to check
341         * @return bounds of the screen
342         */
343        private static Rectangle getScreenInfo(Rectangle g) {
344            GraphicsEnvironment ge = GraphicsEnvironment
345                    .getLocalGraphicsEnvironment();
346            GraphicsDevice[] gs = ge.getScreenDevices();
347            int intersect = 0;
348            Rectangle bounds = null;
349            for (int j = 0; j < gs.length; j++) {
350                GraphicsDevice gd = gs[j];
351                if (gd.getType() == GraphicsDevice.TYPE_RASTER_SCREEN) {
352                    Rectangle b = gd.getDefaultConfiguration().getBounds();
353                    if (b.height > 0 && b.width/b.height >= 3) /* multiscreen with wrong definition */
354                    {
355                        b.width /= 2;
356                        Rectangle is = b.intersection(g);
357                        int s = is.width*is.height;
358                        if(bounds == null || intersect < s) {
359                            intersect = s;
360                            bounds = b;
361                        }
362                        b = new Rectangle(b);
363                        b.x += b.width;
364                        is = b.intersection(g);
365                        s = is.width*is.height;
366                        if(bounds == null || intersect < s) {
367                            intersect = s;
368                            bounds = b;
369                        }
370                    }
371                    else
372                    {
373                        Rectangle is = b.intersection(g);
374                        int s = is.width*is.height;
375                        if(bounds == null || intersect < s) {
376                            intersect = s;
377                            bounds = b;
378                        }
379                    }
380                }
381            }
382            return bounds;
383        }
384    
385        /**
386         * Find the size of the full virtual screen.
387         * @return size of the full virtual screen
388         */
389        public static Rectangle getFullScreenInfo() {
390            return new Rectangle(new Point(0,0), Toolkit.getDefaultToolkit().getScreenSize());
391        }
392    
393        public String toString() {
394            return "WindowGeometry{topLeft="+topLeft+",extent="+extent+"}";
395        }
396    }