Improve window clamping

This commit is contained in:
crschnick 2023-12-02 06:58:43 +00:00
parent d04271f632
commit 2b95ea5d6e
4 changed files with 81 additions and 52 deletions

View file

@ -6,14 +6,11 @@ import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.util.ThreadHelper;
import javafx.application.Platform;
import javafx.beans.value.ObservableValue;
import javafx.geometry.BoundingBox;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.geometry.Rectangle2D;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
@ -23,7 +20,9 @@ import javafx.scene.layout.StackPane;
import javafx.scene.text.Text;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.Window;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
@ -76,12 +75,17 @@ public class AppWindowHelper {
});
centerToMainWindow(stage);
clampWindow(stage);
clampWindow(stage).ifPresent(rectangle2D -> {
stage.setX(rectangle2D.getMinX());
stage.setY(rectangle2D.getMinY());
stage.setWidth(rectangle2D.getWidth());
stage.setHeight(rectangle2D.getHeight());
});
});
return stage;
}
private static void centerToMainWindow(Stage childStage) {
private static void centerToMainWindow(Window childStage) {
if (App.getApp() == null) {
return;
}
@ -92,7 +96,7 @@ public class AppWindowHelper {
}
public static void showAlert(
Consumer<Alert> c, ObservableValue<Boolean> loading, Consumer<Optional<ButtonType>> bt) {
Consumer<Alert> c, Consumer<Optional<ButtonType>> bt) {
ThreadHelper.runAsync(() -> {
var r = showBlockingAlert(c);
if (bt != null) {
@ -101,30 +105,28 @@ public class AppWindowHelper {
});
}
public static void showAndWaitForWindow(Supplier<Stage> s) {
if (!Platform.isFxApplicationThread()) {
CountDownLatch latch = new CountDownLatch(1);
Platform.runLater(() -> {
s.get().showAndWait();
latch.countDown();
});
try {
latch.await();
} catch (InterruptedException ignored) {
}
} else {
s.get().showAndWait();
}
}
public static Optional<ButtonType> showBlockingAlert(Consumer<Alert> c) {
Supplier<Alert> supplier = () -> {
Alert a = AppWindowHelper.createEmptyAlert();
AppFont.normal(a.getDialogPane());
var s = (Stage) a.getDialogPane().getScene().getWindow();
a.getDialogPane().getScene().getWindow().setOnShown(event -> {
clampWindow((Stage) a.getDialogPane().getScene().getWindow()).ifPresent(rectangle2D -> {
s.setX(rectangle2D.getMinX());
s.setY(rectangle2D.getMinY());
// Somehow we have to set max size as setting the normal size does not work?
s.setMaxWidth(rectangle2D.getWidth());
s.setMaxHeight(rectangle2D.getHeight());
});
});
return a;
};
AtomicReference<Optional<ButtonType>> result = new AtomicReference<>();
if (!Platform.isFxApplicationThread()) {
CountDownLatch latch = new CountDownLatch(1);
Platform.runLater(() -> {
Alert a = AppWindowHelper.createEmptyAlert();
AppFont.normal(a.getDialogPane());
Alert a = supplier.get();
c.accept(a);
result.set(a.showAndWait());
latch.countDown();
@ -134,15 +136,8 @@ public class AppWindowHelper {
} catch (InterruptedException ignored) {
}
} else {
Alert a = createEmptyAlert();
AppFont.normal(a.getDialogPane());
Alert a = supplier.get();
c.accept(a);
Button button = (Button) a.getDialogPane().lookupButton(ButtonType.OK);
if (button != null) {
button.getStyleClass().add("ok-button");
}
result.set(a.showAndWait());
}
return result.get();
@ -200,32 +195,70 @@ public class AppWindowHelper {
});
}
public static void clampWindow(Stage stage) {
private static Optional<Rectangle2D> clampWindow(Stage stage) {
if (!areNumbersValid(stage.getWidth(), stage.getHeight())) {
return Optional.empty();
}
var allScreenBounds = computeWindowScreenBounds(stage);
double x = stage.getX();
if (x < allScreenBounds.getMinX()) {
stage.setX(x = allScreenBounds.getMinX());
if (!areNumbersValid(allScreenBounds.getMinX(), allScreenBounds.getMinY(), allScreenBounds.getMaxX(), allScreenBounds.getMaxY())) {
return Optional.empty();
}
double y = stage.getY();
if (y < allScreenBounds.getMinY()) {
stage.setY(y = allScreenBounds.getMinY());
// Alerts do not have a custom x/y set, but we are able to handle that
boolean changed = false;
double x = 0;
if (areNumbersValid(stage.getX())) {
x = stage.getX();
if (x < allScreenBounds.getMinX()) {
x = allScreenBounds.getMinX();
changed = true;
}
}
double y = 0;
if (areNumbersValid(stage.getY())) {
y = stage.getY();
if (y < allScreenBounds.getMinY()) {
y = allScreenBounds.getMinY();
changed = true;
}
}
double w = stage.getWidth();
double h = stage.getHeight();
if (x + w > allScreenBounds.getMaxX()) {
stage.setWidth(allScreenBounds.getMaxX() - x);
w = allScreenBounds.getMaxX() - x;
changed = true;
}
if (y + h > allScreenBounds.getMaxY()) {
stage.setHeight(allScreenBounds.getMaxY() - y);
h = allScreenBounds.getMaxY() - y;
changed = true;
}
return changed ? Optional.of(new Rectangle2D(x, y, w, h)) : Optional.empty();
}
private static Bounds computeWindowScreenBounds(Stage stage) {
private static boolean areNumbersValid(double... args) {
return Arrays.stream(args).allMatch(Double::isFinite);
}
private static List<Screen> getWindowScreens(Stage stage) {
if (!areNumbersValid(stage.getX(), stage.getY(), stage.getWidth(), stage.getHeight())) {
return stage.getOwner() != null && stage.getOwner() instanceof Stage ownerStage ? getWindowScreens(ownerStage) : List.of(Screen.getPrimary());
}
return Screen.getScreensForRectangle(new Rectangle2D(stage.getX(), stage.getY(), stage.getWidth(), stage.getHeight()));
}
private static Rectangle2D computeWindowScreenBounds(Stage stage) {
double minX = Double.POSITIVE_INFINITY ;
double minY = Double.POSITIVE_INFINITY ;
double maxX = Double.NEGATIVE_INFINITY ;
double maxY = Double.NEGATIVE_INFINITY ;
for (Screen screen : Screen.getScreensForRectangle(new Rectangle2D(stage.getX(), stage.getY(), stage.getWidth(), stage.getHeight()))) {
for (Screen screen : getWindowScreens(stage)) {
Rectangle2D screenBounds = screen.getBounds();
if (screenBounds.getMinX() < minX) {
minX = screenBounds.getMinX();
@ -242,7 +275,7 @@ public class AppWindowHelper {
}
// Taskbar adjustment
maxY -= 50;
return new BoundingBox(minX, minY, maxX-minX, maxY-minY);
return new Rectangle2D(minX, minY, maxX-minX, maxY-minY);
}
private static void bindSize(Stage stage, Region r) {

View file

@ -43,8 +43,6 @@ public class UpdateChangelogAlert {
alert.getDialogPane().setContent(markdown);
alert.getButtonTypes().add(new ButtonType(AppI18n.get("gotIt"), ButtonBar.ButtonData.OK_DONE));
},
null,
r -> r.filter(b -> b.getButtonData().isDefaultButton()).ifPresent(t -> {}));
}, r -> r.filter(b -> b.getButtonData().isDefaultButton()).ifPresent(t -> {}));
}
}

View file

@ -57,9 +57,7 @@ public class MacOsPermissions {
a.getButtonTypes().clear();
a.getButtonTypes().add(ButtonType.CANCEL);
alert.set(a);
},
null,
buttonType -> {
}, buttonType -> {
alert.get().close();
state.set(false);
});

View file

@ -13,7 +13,7 @@ dataSourceIntroText=Text data sources contain readable text that can\ncome in a
dataSourceIntroBinary=Binary data sources contain binary data. They\ncan be used when the data should be handled and preserved byte by byte.
dataSourceIntroCollection=Collection data sources contain multiple sub data sources. \nExamples are zip files or file system directories.
storeIntroTitle=Connection Hub
storeIntroDescription=Here you can manage all your shell connections, local and remote, in one place
storeIntroDescription=Here you can manage all your local and remote shell connections in one place
storeStreamDescription=Stream connections produce raw byte data\nthat can be used to construct data sources from.
storeMachineDescription=To start off, here you can quickly detect available\nconnections automatically and choose which ones to add.
detectConnections=Search for connections