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