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 io.xpipe.app.util.ThreadHelper;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.geometry.BoundingBox;
import javafx.geometry.Bounds;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Rectangle2D; import javafx.geometry.Rectangle2D;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.scene.control.Alert; import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType; import javafx.scene.control.ButtonType;
import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent; import javafx.scene.input.KeyEvent;
@ -23,7 +20,9 @@ import javafx.scene.layout.StackPane;
import javafx.scene.text.Text; import javafx.scene.text.Text;
import javafx.stage.Screen; import javafx.stage.Screen;
import javafx.stage.Stage; import javafx.stage.Stage;
import javafx.stage.Window;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
@ -76,12 +75,17 @@ public class AppWindowHelper {
}); });
centerToMainWindow(stage); 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; return stage;
} }
private static void centerToMainWindow(Stage childStage) { private static void centerToMainWindow(Window childStage) {
if (App.getApp() == null) { if (App.getApp() == null) {
return; return;
} }
@ -92,7 +96,7 @@ public class AppWindowHelper {
} }
public static void showAlert( public static void showAlert(
Consumer<Alert> c, ObservableValue<Boolean> loading, Consumer<Optional<ButtonType>> bt) { Consumer<Alert> c, Consumer<Optional<ButtonType>> bt) {
ThreadHelper.runAsync(() -> { ThreadHelper.runAsync(() -> {
var r = showBlockingAlert(c); var r = showBlockingAlert(c);
if (bt != null) { 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) { 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<>(); AtomicReference<Optional<ButtonType>> result = new AtomicReference<>();
if (!Platform.isFxApplicationThread()) { if (!Platform.isFxApplicationThread()) {
CountDownLatch latch = new CountDownLatch(1); CountDownLatch latch = new CountDownLatch(1);
Platform.runLater(() -> { Platform.runLater(() -> {
Alert a = AppWindowHelper.createEmptyAlert(); Alert a = supplier.get();
AppFont.normal(a.getDialogPane());
c.accept(a); c.accept(a);
result.set(a.showAndWait()); result.set(a.showAndWait());
latch.countDown(); latch.countDown();
@ -134,15 +136,8 @@ public class AppWindowHelper {
} catch (InterruptedException ignored) { } catch (InterruptedException ignored) {
} }
} else { } else {
Alert a = createEmptyAlert(); Alert a = supplier.get();
AppFont.normal(a.getDialogPane());
c.accept(a); c.accept(a);
Button button = (Button) a.getDialogPane().lookupButton(ButtonType.OK);
if (button != null) {
button.getStyleClass().add("ok-button");
}
result.set(a.showAndWait()); result.set(a.showAndWait());
} }
return result.get(); 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); var allScreenBounds = computeWindowScreenBounds(stage);
double x = stage.getX(); if (!areNumbersValid(allScreenBounds.getMinX(), allScreenBounds.getMinY(), allScreenBounds.getMaxX(), allScreenBounds.getMaxY())) {
if (x < allScreenBounds.getMinX()) { return Optional.empty();
stage.setX(x = allScreenBounds.getMinX());
} }
double y = stage.getY();
if (y < allScreenBounds.getMinY()) { // Alerts do not have a custom x/y set, but we are able to handle that
stage.setY(y = allScreenBounds.getMinY());
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 w = stage.getWidth();
double h = stage.getHeight(); double h = stage.getHeight();
if (x + w > allScreenBounds.getMaxX()) { if (x + w > allScreenBounds.getMaxX()) {
stage.setWidth(allScreenBounds.getMaxX() - x); w = allScreenBounds.getMaxX() - x;
changed = true;
} }
if (y + h > allScreenBounds.getMaxY()) { 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 minX = Double.POSITIVE_INFINITY ;
double minY = Double.POSITIVE_INFINITY ; double minY = Double.POSITIVE_INFINITY ;
double maxX = Double.NEGATIVE_INFINITY ; double maxX = Double.NEGATIVE_INFINITY ;
double maxY = 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(); Rectangle2D screenBounds = screen.getBounds();
if (screenBounds.getMinX() < minX) { if (screenBounds.getMinX() < minX) {
minX = screenBounds.getMinX(); minX = screenBounds.getMinX();
@ -242,7 +275,7 @@ public class AppWindowHelper {
} }
// Taskbar adjustment // Taskbar adjustment
maxY -= 50; 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) { private static void bindSize(Stage stage, Region r) {

View file

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

View file

@ -57,9 +57,7 @@ public class MacOsPermissions {
a.getButtonTypes().clear(); a.getButtonTypes().clear();
a.getButtonTypes().add(ButtonType.CANCEL); a.getButtonTypes().add(ButtonType.CANCEL);
alert.set(a); alert.set(a);
}, }, buttonType -> {
null,
buttonType -> {
alert.get().close(); alert.get().close();
state.set(false); 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. 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. dataSourceIntroCollection=Collection data sources contain multiple sub data sources. \nExamples are zip files or file system directories.
storeIntroTitle=Connection Hub 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. 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. storeMachineDescription=To start off, here you can quickly detect available\nconnections automatically and choose which ones to add.
detectConnections=Search for connections detectConnections=Search for connections