// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.tools;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.Window;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.JComponent;
import org.openstreetmap.josm.Main;
/**
* This is a helper class for persisting the geometry of a JOSM window to the preference store
* and for restoring it from the preference store.
*
*/
public class WindowGeometry {
/**
* Replies a window geometry object for a window with a specific size which is
* centered on screen, where main window is
*
* @param extent the size
* @return the geometry object
*/
public static WindowGeometry centerOnScreen(Dimension extent) {
return centerOnScreen(extent, "gui.geometry");
}
/**
* Replies a window geometry object for a window with a specific size which is
* centered on screen where the corresponding window is.
*
* @param extent the size
* @param preferenceKey the key to get window size and position from, null value format
* for whole virtual screen
* @return the geometry object
*/
public static WindowGeometry centerOnScreen(Dimension extent, String preferenceKey) {
Rectangle size = preferenceKey != null ? getScreenInfo(preferenceKey)
: getFullScreenInfo();
Point topLeft = new Point(
size.x + Math.max(0, (size.width - extent.width) /2),
size.y + Math.max(0, (size.height - extent.height) /2)
);
return new WindowGeometry(topLeft, extent);
}
/**
* Replies a window geometry object for a window with a specific size which is centered
* relative to the parent window of a reference component.
*
* @param reference the reference component.
* @param extent the size
* @return the geometry object
*/
public static WindowGeometry centerInWindow(Component reference, Dimension extent) {
Window parentWindow = null;
while (reference != null && !(reference instanceof Window)) {
reference = reference.getParent();
}
if (reference == null)
return new WindowGeometry(new Point(0, 0), extent);
parentWindow = (Window) reference;
Point topLeft = new Point(
Math.max(0, (parentWindow.getSize().width - extent.width) /2),
Math.max(0, (parentWindow.getSize().height - extent.height) /2)
);
topLeft.x += parentWindow.getLocation().x;
topLeft.y += parentWindow.getLocation().y;
return new WindowGeometry(topLeft, extent);
}
/**
* Exception thrown by the WindowGeometry class if something goes wrong
*/
public static class WindowGeometryException extends Exception {
public WindowGeometryException(String message, Throwable cause) {
super(message, cause);
}
public WindowGeometryException(String message) {
super(message);
}
}
/** the top left point */
private Point topLeft;
/** the size */
private Dimension extent;
/**
* Creates a window geometry from a position and dimension
*
* @param topLeft the top left point
* @param extent the extent
*/
public WindowGeometry(Point topLeft, Dimension extent) {
this.topLeft = topLeft;
this.extent = extent;
}
/**
* Creates a window geometry from a rectangle
*
* @param rect the position
*/
public WindowGeometry(Rectangle rect) {
this.topLeft = rect.getLocation();
this.extent = rect.getSize();
}
/**
* Creates a window geometry from the position and the size of a window.
*
* @param window the window
*/
public WindowGeometry(Window window) {
this(window.getLocationOnScreen(), window.getSize());
}
/**
* Fixes a window geometry to shift to the correct screen.
*
* @param window the window
*/
public void fixScreen(Window window) {
Rectangle oldScreen = getScreenInfo(getRectangle());
Rectangle newScreen = getScreenInfo(new Rectangle(window.getLocationOnScreen(), window.getSize()));
if (oldScreen.x != newScreen.x) {
this.topLeft.x += newScreen.x - oldScreen.x;
}
if (oldScreen.y != newScreen.y) {
this.topLeft.y += newScreen.y - oldScreen.y;
}
}
protected int parseField(String preferenceKey, String preferenceValue, String field) throws WindowGeometryException {
String v = "";
try {
Pattern p = Pattern.compile(field + "=(-?\\d+)", Pattern.CASE_INSENSITIVE);
Matcher m = p.matcher(preferenceValue);
if (!m.find())
throw new WindowGeometryException(
tr("Preference with key ''{0}'' does not include ''{1}''. Cannot restore window geometry from preferences.",
preferenceKey, field));
v = m.group(1);
return Integer.parseInt(v);
} catch (WindowGeometryException e) {
throw e;
} catch (NumberFormatException e) {
throw new WindowGeometryException(
tr("Preference with key ''{0}'' does not provide an int value for ''{1}''. Got {2}. " +
"Cannot restore window geometry from preferences.",
preferenceKey, field, v), e);
} catch (Exception e) {
throw new WindowGeometryException(
tr("Failed to parse field ''{1}'' in preference with key ''{0}''. Exception was: {2}. " +
"Cannot restore window geometry from preferences.",
preferenceKey, field, e.toString()), e);
}
}
protected final void initFromPreferences(String preferenceKey) throws WindowGeometryException {
String value = Main.pref.get(preferenceKey);
if (value == null || value.isEmpty())
throw new WindowGeometryException(
tr("Preference with key ''{0}'' does not exist. Cannot restore window geometry from preferences.", preferenceKey));
topLeft = new Point();
extent = new Dimension();
topLeft.x = parseField(preferenceKey, value, "x");
topLeft.y = parseField(preferenceKey, value, "y");
extent.width = parseField(preferenceKey, value, "width");
extent.height = parseField(preferenceKey, value, "height");
}
protected final void initFromWindowGeometry(WindowGeometry other) {
this.topLeft = other.topLeft;
this.extent = other.extent;
}
public static WindowGeometry mainWindow(String preferenceKey, String arg, boolean maximize) {
Rectangle screenDimension = getScreenInfo("gui.geometry");
if (arg != null) {
final Matcher m = Pattern.compile("(\\d+)x(\\d+)(([+-])(\\d+)([+-])(\\d+))?").matcher(arg);
if (m.matches()) {
int w = Integer.parseInt(m.group(1));
int h = Integer.parseInt(m.group(2));
int x = screenDimension.x, y = screenDimension.y;
if (m.group(3) != null) {
x = Integer.parseInt(m.group(5));
y = Integer.parseInt(m.group(7));
if ("-".equals(m.group(4))) {
x = screenDimension.x + screenDimension.width - x - w;
}
if ("-".equals(m.group(6))) {
y = screenDimension.y + screenDimension.height - y - h;
}
}
return new WindowGeometry(new Point(x, y), new Dimension(w, h));
} else {
Main.warn(tr("Ignoring malformed geometry: {0}", arg));
}
}
WindowGeometry def;
if (maximize) {
def = new WindowGeometry(screenDimension);
} else {
Point p = screenDimension.getLocation();
p.x += (screenDimension.width-1000)/2;
p.y += (screenDimension.height-740)/2;
def = new WindowGeometry(p, new Dimension(1000, 740));
}
return new WindowGeometry(preferenceKey, def);
}
/**
* Creates a window geometry from the values kept in the preference store under the
* key preferenceKey
*
* @param preferenceKey the preference key
* @throws WindowGeometryException if no such key exist or if the preference value has
* an illegal format
*/
public WindowGeometry(String preferenceKey) throws WindowGeometryException {
initFromPreferences(preferenceKey);
}
/**
* Creates a window geometry from the values kept in the preference store under the
* key preferenceKey
. Falls back to the defaultGeometry
if
* something goes wrong.
*
* @param preferenceKey the preference key
* @param defaultGeometry the default geometry
*
*/
public WindowGeometry(String preferenceKey, WindowGeometry defaultGeometry) {
try {
initFromPreferences(preferenceKey);
} catch (WindowGeometryException e) {
initFromWindowGeometry(defaultGeometry);
}
}
/**
* Remembers a window geometry under a specific preference key
*
* @param preferenceKey the preference key
*/
public void remember(String preferenceKey) {
StringBuilder value = new StringBuilder();
value.append("x=").append(topLeft.x).append(",y=").append(topLeft.y)
.append(",width=").append(extent.width).append(",height=").append(extent.height);
Main.pref.put(preferenceKey, value.toString());
}
/**
* Replies the top left point for the geometry
*
* @return the top left point for the geometry
*/
public Point getTopLeft() {
return topLeft;
}
/**
* Replies the size specified by the geometry
*
* @return the size specified by the geometry
*/
public Dimension getSize() {
return extent;
}
/**
* Replies the size and position specified by the geometry
*
* @return the size and position specified by the geometry
*/
private Rectangle getRectangle() {
return new Rectangle(topLeft, extent);
}
/**
* Applies this geometry to a window. Makes sure that the window is not
* placed outside of the coordinate range of all available screens.
*
* @param window the window
*/
public void applySafe(Window window) {
Point p = new Point(topLeft);
Dimension size = new Dimension(extent);
Rectangle virtualBounds = getVirtualScreenBounds();
// Ensure window fit on screen
if (p.x < virtualBounds.x) {
p.x = virtualBounds.x;
} else if (p.x > virtualBounds.x + virtualBounds.width - size.width) {
p.x = virtualBounds.x + virtualBounds.width - size.width;
}
if (p.y < virtualBounds.y) {
p.y = virtualBounds.y;
} else if (p.y > virtualBounds.y + virtualBounds.height - size.height) {
p.y = virtualBounds.y + virtualBounds.height - size.height;
}
int deltax = (p.x + size.width) - (virtualBounds.x + virtualBounds.width);
if (deltax > 0) {
size.width -= deltax;
}
int deltay = (p.y + size.height) - (virtualBounds.y + virtualBounds.height);
if (deltay > 0) {
size.height -= deltay;
}
// Ensure window does not hide taskbar
Rectangle maxbounds = GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds();
if (!isBugInMaximumWindowBounds(maxbounds)) {
deltax = size.width - maxbounds.width;
if (deltax > 0) {
size.width -= deltax;
}
deltay = size.height - maxbounds.height;
if (deltay > 0) {
size.height -= deltay;
}
}
window.setLocation(p);
window.setSize(size);
}
/**
* Determines if the bug affecting getMaximumWindowBounds() occured.
*
* @param maxbounds result of getMaximumWindowBounds()
* @return {@code true} if the bug happened, {@code false otherwise}
*
* @see JOSM-9699
* @see Ubuntu-1171563
* @see IcedTea-1669
* @see JI-9010334
*/
protected static boolean isBugInMaximumWindowBounds(Rectangle maxbounds) {
return maxbounds.width <= 0 || maxbounds.height <= 0;
}
/**
* Computes the virtual bounds of graphics environment, as an union of all screen bounds.
* @return The virtual bounds of graphics environment, as an union of all screen bounds.
* @since 6522
*/
public static Rectangle getVirtualScreenBounds() {
Rectangle virtualBounds = new Rectangle();
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
for (GraphicsDevice gd : ge.getScreenDevices()) {
if (gd.getType() == GraphicsDevice.TYPE_RASTER_SCREEN) {
virtualBounds = virtualBounds.union(gd.getDefaultConfiguration().getBounds());
}
}
return virtualBounds;
}
/**
* Computes the maximum dimension for a component to fit in screen displaying {@code component}.
* @param component The component to get current screen info from. Must not be {@code null}
* @return the maximum dimension for a component to fit in current screen
* @throws IllegalArgumentException if {@code component} is null
* @since 7463
*/
public static Dimension getMaxDimensionOnScreen(JComponent component) {
CheckParameterUtil.ensureParameterNotNull(component, "component");
// Compute max dimension of current screen
Dimension result = new Dimension();
GraphicsConfiguration gc = component.getGraphicsConfiguration();
if (gc == null && Main.parent != null) {
gc = Main.parent.getGraphicsConfiguration();
}
if (gc != null) {
// Max displayable dimension (max screen dimension - insets)
Rectangle bounds = gc.getBounds();
Insets insets = component.getToolkit().getScreenInsets(gc);
result.width = bounds.width - insets.left - insets.right;
result.height = bounds.height - insets.top - insets.bottom;
}
return result;
}
/**
* Find the size and position of the screen for given coordinates. Use first screen,
* when no coordinates are stored or null is passed.
*
* @param preferenceKey the key to get size and position from
* @return bounds of the screen
*/
public static Rectangle getScreenInfo(String preferenceKey) {
Rectangle g = new WindowGeometry(preferenceKey,
/* default: something on screen 1 */
new WindowGeometry(new Point(0, 0), new Dimension(10, 10))).getRectangle();
return getScreenInfo(g);
}
/**
* Find the size and position of the screen for given coordinates. Use first screen,
* when no coordinates are stored or null is passed.
*
* @param g coordinates to check
* @return bounds of the screen
*/
private static Rectangle getScreenInfo(Rectangle g) {
GraphicsEnvironment ge = GraphicsEnvironment
.getLocalGraphicsEnvironment();
GraphicsDevice[] gs = ge.getScreenDevices();
int intersect = 0;
Rectangle bounds = null;
for (GraphicsDevice gd : gs) {
if (gd.getType() == GraphicsDevice.TYPE_RASTER_SCREEN) {
Rectangle b = gd.getDefaultConfiguration().getBounds();
if (b.height > 0 && b.width / b.height >= 3) /* multiscreen with wrong definition */ {
b.width /= 2;
Rectangle is = b.intersection(g);
int s = is.width * is.height;
if (bounds == null || intersect < s) {
intersect = s;
bounds = b;
}
b = new Rectangle(b);
b.x += b.width;
is = b.intersection(g);
s = is.width * is.height;
if (bounds == null || intersect < s) {
intersect = s;
bounds = b;
}
} else {
Rectangle is = b.intersection(g);
int s = is.width * is.height;
if (bounds == null || intersect < s) {
intersect = s;
bounds = b;
}
}
}
}
return bounds;
}
/**
* Find the size of the full virtual screen.
* @return size of the full virtual screen
*/
public static Rectangle getFullScreenInfo() {
return new Rectangle(new Point(0, 0), Toolkit.getDefaultToolkit().getScreenSize());
}
@Override
public String toString() {
return "WindowGeometry{topLeft="+topLeft+",extent="+extent+'}';
}
}