|
9 | 9 |
|
10 | 10 | import static org.phoebus.ui.application.PhoebusApplication.logger; |
11 | 11 |
|
| 12 | +import java.util.Comparator; |
12 | 13 | import java.util.List; |
13 | 14 | import java.util.Objects; |
| 15 | +import java.util.Optional; |
14 | 16 | import java.util.function.Consumer; |
15 | 17 | import java.util.logging.Level; |
16 | 18 | import java.util.prefs.BackingStoreException; |
17 | 19 | import java.util.prefs.Preferences; |
| 20 | +import java.util.stream.Collectors; |
18 | 21 |
|
19 | 22 | import javafx.application.Platform; |
20 | 23 | import javafx.geometry.Bounds; |
| 24 | +import javafx.geometry.Point2D; |
21 | 25 | import javafx.geometry.Rectangle2D; |
22 | 26 | import javafx.scene.Node; |
23 | 27 | import javafx.scene.control.Dialog; |
@@ -103,6 +107,48 @@ public void run() { |
103 | 107 | }); |
104 | 108 | } |
105 | 109 |
|
| 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 | + |
106 | 152 | /** Position the given {@code dialog} initially relative to {@code owner}, |
107 | 153 | * then it saves/restore the dialog's position and size into/from the |
108 | 154 | * provided {@link Preferences}. |
@@ -233,9 +279,19 @@ public static void positionAndSize(final Dialog<?> dialog, final Node owner, fin |
233 | 279 | if (owner != null) { |
234 | 280 | // Position relative to owner |
235 | 281 | 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 | + ); |
236 | 292 |
|
237 | | - dialog.setX(pos.getMinX() - prefWidth); |
238 | | - dialog.setY(pos.getMinY() - prefHeight/3); |
| 293 | + dialog.setX(clampedPos.getMinX()); |
| 294 | + dialog.setY(clampedPos.getMinY()); |
239 | 295 | } |
240 | 296 |
|
241 | 297 | if (!Double.isNaN(prefWidth) && !Double.isNaN(prefHeight)) |
|
0 commit comments