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.Main; 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() occured. 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 && Main.parent != null) { 413 gc = Main.parent.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}