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