Reputation: 20059
Occasionally I have to display a popup or dialog relative to an existing component (prime example is a date input control with a calendar button beside it).
It worked beautifully for years, but always had the bug that the calendar could partially appear outside the screen (it was hardcoded to appear just to the right of the field). Just nobody ever noticed because there was never a date control at the far right in a window. Well that changed recently with the addition of a new window.
Well then, I thought, lets just fix a windows position (after I positioned it where it should be) to be completely on screen. I wrote a simple utility method to do just that:
public static void correctWindowLocationForScreen(Window window) {
GraphicsConfiguration gc = window.getGraphicsConfiguration();
Rectangle screenRect = gc.getBounds();
Rectangle windowRect = window.getBounds();
Rectangle newRect = new Rectangle(windowRect);
if (windowRect.x + windowRect.width > screenRect.x + screenRect.width)
newRect.x = screenRect.x + screenRect.width - windowRect.width;
if (windowRect.y + windowRect.height > screenRect.y + screenRect.height)
newRect.y = screenRect.y + screenRect.height - windowRect.height;
if (windowRect.x < screenRect.x)
newRect.x = screenRect.x;
if (windowRect.y < screenRect.y)
newRect.y = screenRect.y;
if (!newRect.equals(windowRect))
window.setLocation(newRect.x, newRect.y);
}
Problem solved. Or not. I position my window using the on-screen coordinates from the triggering component (the button that makes the calendar appear):
JComponent invoker = ... // passed in from the date field (a JButton)
Window owner = SwingUtilities.getWindowAncestor(invoker);
JDialog dialog = new JDialog(owner);
dialog.setLocation(invoker.getLocationOnScreen());
correctWindowLocationForScreen(dialog);
Havoc breaks out if the "invoker" component is located in a window that spans two screens. Apparently "window.getGraphicsConfiguration()" returns whatever graphic configuration the windows top left corner happens to be in. Thats not necessarily the screen where the date component within the window is located.
So how can I position my dialog properly in this case?
Upvotes: 0
Views: 933
Reputation: 20059
Ok, here is what I ended up with (a wall of code to handle the odd edge case).
correctWindowLocationForScreen() will reposition a window if it is not completely within the visible screen area (simplest case, its completely on one screen. Hard case, it spans multiple screens). If the window leaves the complete screen area by just one pixel, it is repositioned using the first screen rectangle found. If the window doesn't fit the screen, its positioned at the top left and extends over the screen to bottom right (its implied by the order in which positionInsideRectangle() checks/alters coordinates).
Its quite complicated considering the requirement is pretty simple.
/**
* Check that window is completely on screen, if not correct position.
* Will not ensure the window fits completely onto the screen.
*/
public static void correctWindowLocationForScreen(final Window window) {
correctComponentLocation(window, getScreenRectangles());
}
/**
* Set the component location so that it is completely inside the available
* regions (if possible).
* Although the method will make some effort to place the component
* nicely, it may end up partially outside the regions (either because it
* doesn't fit at all, or the regions are placed badly).
*/
public static void correctComponentLocation(final Component component, final Rectangle ... availableRegions) {
// check the simple cases (component completely inside one region, no regions available)
final Rectangle bounds = component.getBounds();
if (availableRegions == null || availableRegions.length <= 0)
return;
final List<Rectangle> intersecting = new ArrayList<>(3);
for (final Rectangle region : availableRegions) {
if (region.contains(bounds)) {
return;
} else if (region.intersects(bounds)) {
// partial overlap
intersecting.add(region);
}
}
switch (intersecting.size()) {
case 0:
// position component in the first available region
positionInsideRectangle(component, availableRegions[0]);
return;
case 1:
// position component in the only intersecting region
positionInsideRectangle(component, intersecting.get(0));
return;
default:
// uuuh oooh...
break;
}
// build area containing all detected intersections
// and check if the bounds fall completely into the intersection area
final Area area = new Area();
for (final Rectangle region : intersecting) {
final Rectangle2D r2d = new Rectangle2D.Double(region.x, region.y, region.width, region.height);
area.add(new Area(r2d));
}
final Rectangle2D boundsRect = new Rectangle2D.Double(bounds.x, bounds.y, bounds.width, bounds.height);
if (area.contains(boundsRect))
return;
// bah, just place it in the first intersecting region...
positionInsideRectangle(component, intersecting.get(0));
}
/**
* Position component so that its completely inside the rectangle.
* If the component is larger than the rectangle, component will
* exceed to rectangle bounds to the right and bottom, e.g.
* the component is placed at the rectangles x respectively y.
*/
public static void positionInsideRectangle(final Component component, final Rectangle region) {
final Rectangle bounds = component.getBounds();
int x = bounds.x;
int y = bounds.y;
if (x + bounds.width > region.x + region.width)
x = region.x + region.width - bounds.width;
if (y + bounds.height > region.y + region.height)
y = region.y + region.height - bounds.height;
if (region.x < region.x)
x = region.x;
if (y < region.y)
y = region.y;
if (x != bounds.x || y != bounds.y)
component.setLocation(x, y);
}
/**
* Gets the available display space as an arrays of rectangles
* (there is one rectangle for each screen, if the environment is
* headless the resulting array will be empty).
*/
public static Rectangle[] getScreenRectangles() {
try {
Rectangle[] result;
final GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
final GraphicsDevice[] devices = ge.getScreenDevices();
result = new Rectangle[devices.length];
for (int i=0; i<devices.length; ++i) {
final GraphicsDevice gd = devices[i];
result[i] = gd.getDefaultConfiguration().getBounds();
}
return result;
} catch (final Exception e) {
return new Rectangle[0];
}
}
Upvotes: 1
Reputation: 109547
One can iterate over all devices, and find the monitor where the point is in. Then keep to that Rectangle.
See GraphicsEnvironment.getScreenDevices.
This will not use the current Window, but you already found out that a window may be shown in several monitors.
Useful might be Component.getLocationOnScreen.
Upvotes: 1