Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 58 additions & 2 deletions core/ui/src/main/java/org/phoebus/ui/dialog/DialogHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,19 @@

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

import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.prefs.BackingStoreException;
import java.util.prefs.Preferences;
import java.util.stream.Collectors;

import javafx.application.Platform;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.geometry.Rectangle2D;
import javafx.scene.Node;
import javafx.scene.control.Dialog;
Expand Down Expand Up @@ -103,6 +107,48 @@ public void run() {
});
}

/**
* Clamp a rectangle to the closest rectangle in a list of screens. This is used to prevent a dialog from going
* off screen completely, and losing any control over the entire application.
*
* @param rect The rectangle to be clamped.
* @param screens A list of screen regions to clamp to.
* */
static private Rectangle2D clampToClosest(final Rectangle2D rect, final List<Rectangle2D> screens) {
Point2D center = new Point2D(
rect.getMinX() + rect.getWidth() / 2,
rect.getMinY() + rect.getHeight() / 2
);

Optional<Rectangle2D> closestOpt = screens.stream().min(
Comparator.comparingDouble(screen -> {
// if the dialog center is inside of a screen, it will always be the closest
if (screen.contains(center))
return -1;

// get distance to closest edge
double dx = Math.max(0, Math.max(screen.getMinX() - center.getX(), center.getX() - screen.getMaxX()));
double dy = Math.max(0, Math.max(screen.getMinY() - center.getY(), center.getY() - screen.getMaxY()));

return dx * dx + dy * dy;
})
);

if (closestOpt.isEmpty()) {
// no available screens (unlikely)
return rect;
}

// clamp position to screen, note that this will move the rectangle into the screen in its entirety,
// with a preference for the top left corner
Rectangle2D closest = closestOpt.get();
double newMinX = Math.max(closest.getMinX(), Math.min(rect.getMinX(), closest.getMaxX() - rect.getWidth()));
double newMinY = Math.max(closest.getMinY(), Math.min(rect.getMinY(), closest.getMaxY() - rect.getHeight()));
return new Rectangle2D(
newMinX, newMinY, rect.getWidth(), rect.getHeight()
);
}

/** Position the given {@code dialog} initially relative to {@code owner},
* then it saves/restore the dialog's position and size into/from the
* provided {@link Preferences}.
Expand Down Expand Up @@ -233,9 +279,19 @@ public static void positionAndSize(final Dialog<?> dialog, final Node owner, fin
if (owner != null) {
// Position relative to owner
final Bounds pos = owner.localToScreen(owner.getBoundsInLocal());
final Rectangle2D prefPos = new Rectangle2D(
pos.getMinX() - prefWidth,
pos.getMinY() - prefHeight/3,
prefWidth,
prefHeight
);
List<Screen> screens = Screen.getScreens();
Rectangle2D clampedPos = clampToClosest(
prefPos, screens.stream().map(Screen::getVisualBounds).collect(Collectors.toList())
);

dialog.setX(pos.getMinX() - prefWidth);
dialog.setY(pos.getMinY() - prefHeight/3);
dialog.setX(clampedPos.getMinX());
dialog.setY(clampedPos.getMinY());
}

if (!Double.isNaN(prefWidth) && !Double.isNaN(prefHeight))
Expand Down