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