Skip to content

Commit 83ffaa3

Browse files
authored
Merge pull request #3316 from High-Voltage-Engineering/master
Clamp dialogs to closest screen
2 parents c45273b + d1c799b commit 83ffaa3

File tree

1 file changed

+58
-2
lines changed

1 file changed

+58
-2
lines changed

core/ui/src/main/java/org/phoebus/ui/dialog/DialogHelper.java

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,19 @@
99

1010
import static org.phoebus.ui.application.PhoebusApplication.logger;
1111

12+
import java.util.Comparator;
1213
import java.util.List;
1314
import java.util.Objects;
15+
import java.util.Optional;
1416
import java.util.function.Consumer;
1517
import java.util.logging.Level;
1618
import java.util.prefs.BackingStoreException;
1719
import java.util.prefs.Preferences;
20+
import java.util.stream.Collectors;
1821

1922
import javafx.application.Platform;
2023
import javafx.geometry.Bounds;
24+
import javafx.geometry.Point2D;
2125
import javafx.geometry.Rectangle2D;
2226
import javafx.scene.Node;
2327
import javafx.scene.control.Dialog;
@@ -103,6 +107,48 @@ public void run() {
103107
});
104108
}
105109

110+
/**
111+
* Clamp a rectangle to the closest rectangle in a list of screens. This is used to prevent a dialog from going
112+
* off screen completely, and losing any control over the entire application.
113+
*
114+
* @param rect The rectangle to be clamped.
115+
* @param screens A list of screen regions to clamp to.
116+
* */
117+
static private Rectangle2D clampToClosest(final Rectangle2D rect, final List<Rectangle2D> screens) {
118+
Point2D center = new Point2D(
119+
rect.getMinX() + rect.getWidth() / 2,
120+
rect.getMinY() + rect.getHeight() / 2
121+
);
122+
123+
Optional<Rectangle2D> closestOpt = screens.stream().min(
124+
Comparator.comparingDouble(screen -> {
125+
// if the dialog center is inside of a screen, it will always be the closest
126+
if (screen.contains(center))
127+
return -1;
128+
129+
// get distance to closest edge
130+
double dx = Math.max(0, Math.max(screen.getMinX() - center.getX(), center.getX() - screen.getMaxX()));
131+
double dy = Math.max(0, Math.max(screen.getMinY() - center.getY(), center.getY() - screen.getMaxY()));
132+
133+
return dx * dx + dy * dy;
134+
})
135+
);
136+
137+
if (closestOpt.isEmpty()) {
138+
// no available screens (unlikely)
139+
return rect;
140+
}
141+
142+
// clamp position to screen, note that this will move the rectangle into the screen in its entirety,
143+
// with a preference for the top left corner
144+
Rectangle2D closest = closestOpt.get();
145+
double newMinX = Math.max(closest.getMinX(), Math.min(rect.getMinX(), closest.getMaxX() - rect.getWidth()));
146+
double newMinY = Math.max(closest.getMinY(), Math.min(rect.getMinY(), closest.getMaxY() - rect.getHeight()));
147+
return new Rectangle2D(
148+
newMinX, newMinY, rect.getWidth(), rect.getHeight()
149+
);
150+
}
151+
106152
/** Position the given {@code dialog} initially relative to {@code owner},
107153
* then it saves/restore the dialog's position and size into/from the
108154
* provided {@link Preferences}.
@@ -233,9 +279,19 @@ public static void positionAndSize(final Dialog<?> dialog, final Node owner, fin
233279
if (owner != null) {
234280
// Position relative to owner
235281
final Bounds pos = owner.localToScreen(owner.getBoundsInLocal());
282+
final Rectangle2D prefPos = new Rectangle2D(
283+
pos.getMinX() - prefWidth,
284+
pos.getMinY() - prefHeight/3,
285+
prefWidth,
286+
prefHeight
287+
);
288+
List<Screen> screens = Screen.getScreens();
289+
Rectangle2D clampedPos = clampToClosest(
290+
prefPos, screens.stream().map(Screen::getVisualBounds).collect(Collectors.toList())
291+
);
236292

237-
dialog.setX(pos.getMinX() - prefWidth);
238-
dialog.setY(pos.getMinY() - prefHeight/3);
293+
dialog.setX(clampedPos.getMinX());
294+
dialog.setY(clampedPos.getMinY());
239295
}
240296

241297
if (!Double.isNaN(prefWidth) && !Double.isNaN(prefHeight))

0 commit comments

Comments
 (0)