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