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