Window fixes

This commit is contained in:
crschnick 2024-06-20 18:33:51 +00:00
parent 05bb34eef5
commit 0587dea4ac
5 changed files with 204 additions and 175 deletions

View file

@ -10,7 +10,6 @@ import io.xpipe.app.fxcomps.augment.DragOverPseudoClassAugment;
import io.xpipe.app.fxcomps.impl.*;
import io.xpipe.app.fxcomps.util.DerivedObservableList;
import io.xpipe.app.fxcomps.util.PlatformThread;
import javafx.beans.binding.Bindings;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
@ -19,7 +18,6 @@ import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Region;
import org.kordamp.ikonli.javafx.FontIcon;
import java.io.File;
@ -192,6 +190,10 @@ public class BrowserTransferComp extends SimpleComp {
event.consume();
});
struc.get().setOnDragDone(event -> {
if (!event.isAccepted()) {
return;
}
model.clear();
event.consume();
});

View file

@ -114,7 +114,7 @@ public class AppMainWindow {
}
private void setupListeners() {
AppWindowHelper.fixInvalidStagePosition(stage);
AppWindowBounds.fixInvalidStagePosition(stage);
stage.xProperty().addListener((c, o, n) -> {
if (windowActive.get()) {
onChange();

View file

@ -0,0 +1,185 @@
package io.xpipe.app.core.window;
import io.xpipe.app.core.App;
import io.xpipe.core.process.OsType;
import javafx.geometry.Rectangle2D;
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.atomic.AtomicBoolean;
public class AppWindowBounds {
public static void fixInvalidStagePosition(Stage stage) {
if (OsType.getLocal() != OsType.LINUX) {
return;
}
var xSet = new AtomicBoolean();
stage.xProperty().addListener((observable, oldValue, newValue) -> {
var n = newValue.doubleValue();
var o = oldValue.doubleValue();
if (stage.isShowing() && areNumbersValid(o, n)) {
// Ignore rounding events
if (Math.abs(n - o) < 0.5) {
return;
}
if (!xSet.getAndSet(true) && !stage.isMaximized() && n <= 0.0 && o > 0.0 && Math.abs(n - o) > 100) {
stage.setX(o);
}
}
});
var ySet = new AtomicBoolean();
stage.yProperty().addListener((observable, oldValue, newValue) -> {
var n = newValue.doubleValue();
var o = oldValue.doubleValue();
if (stage.isShowing() && areNumbersValid(o, n)) {
// Ignore rounding events
if (Math.abs(n - o) < 0.5) {
return;
}
if (!ySet.getAndSet(true) && !stage.isMaximized() && n <= 0.0 && o > 0.0 && Math.abs(n - o) > 20) {
stage.setY(o);
}
}
});
}
public static Stage centerStage() {
var stage = new Stage() {
@Override
public void centerOnScreen() {
centerToMainWindow(this);
clampWindow(this).ifPresent(rectangle2D -> {
this.setX(rectangle2D.getMinX());
this.setY(rectangle2D.getMinY());
this.setWidth(rectangle2D.getWidth());
this.setHeight(rectangle2D.getHeight());
});
}
};
return stage;
}
public static void centerToMainWindow(Window childStage) {
if (App.getApp() == null) {
childStage.centerOnScreen();
return;
}
var stage = App.getApp().getStage();
childStage.setX(stage.getX() + stage.getWidth() / 2 - childStage.getWidth() / 2);
childStage.setY(stage.getY() + stage.getHeight() / 2 - childStage.getHeight() / 2);
}
public static Optional<Rectangle2D> clampWindow(Stage stage) {
if (!areNumbersValid(stage.getWidth(), stage.getHeight())) {
return Optional.empty();
}
var allScreenBounds = computeWindowScreenBounds(stage);
if (!areNumbersValid(
allScreenBounds.getMinX(),
allScreenBounds.getMinY(),
allScreenBounds.getMaxX(),
allScreenBounds.getMaxY())) {
return Optional.empty();
}
// 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()) {
w = allScreenBounds.getMaxX() - x;
changed = true;
}
if (y + h > allScreenBounds.getMaxY()) {
h = allScreenBounds.getMaxY() - y;
changed = true;
}
// This should not happen but on weird Linux systems nothing is impossible
if (w < 0 || h < 0) {
return Optional.empty();
}
return changed ? Optional.of(new Rectangle2D(x, y, w, h)) : Optional.empty();
}
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 : getWindowScreens(stage)) {
Rectangle2D screenBounds = screen.getBounds();
if (screenBounds.getMinX() < minX) {
minX = screenBounds.getMinX();
}
if (screenBounds.getMinY() < minY) {
minY = screenBounds.getMinY();
}
if (screenBounds.getMaxX() > maxX) {
maxX = screenBounds.getMaxX();
}
if (screenBounds.getMaxY() > maxY) {
maxY = screenBounds.getMaxY();
}
}
// Taskbar adjustment
maxY -= 50;
var w = maxX - minX;
var h = maxY - minY;
// This should not happen but on weird Linux systems nothing is impossible
if (w < 0 || h < 0) {
return new Rectangle2D(0, 0, 800, 600);
}
return new Rectangle2D(minX, minY, w, h);
}
}

View file

@ -8,12 +8,10 @@ import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.InputHelper;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.process.OsType;
import javafx.application.Platform;
import javafx.beans.value.ObservableValue;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Rectangle2D;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
@ -25,13 +23,12 @@ import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.text.Text;
import javafx.stage.*;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Function;
@ -39,44 +36,6 @@ import java.util.function.Supplier;
public class AppWindowHelper {
public static void fixInvalidStagePosition(Stage stage) {
if (OsType.getLocal() != OsType.LINUX) {
return;
}
var xSet = new AtomicBoolean();
stage.xProperty().addListener((observable, oldValue, newValue) -> {
var n = newValue.doubleValue();
var o = oldValue.doubleValue();
if (stage.isShowing() && areNumbersValid(o, n)) {
// Ignore rounding events
if (Math.abs(n - o) < 0.5) {
return;
}
if (!xSet.getAndSet(true) && !stage.isMaximized() && n <= 0.0 && o > 0.0 && Math.abs(n - o) > 100) {
stage.setX(o);
}
}
});
var ySet = new AtomicBoolean();
stage.yProperty().addListener((observable, oldValue, newValue) -> {
var n = newValue.doubleValue();
var o = oldValue.doubleValue();
if (stage.isShowing() && areNumbersValid(o, n)) {
// Ignore rounding events
if (Math.abs(n - o) < 0.5) {
return;
}
if (!ySet.getAndSet(true) && !stage.isMaximized() && n <= 0.0 && o > 0.0 && Math.abs(n - o) > 20) {
stage.setY(o);
}
}
});
}
public static Node alertContentText(String s) {
var text = new Text(s);
text.setWrappingWidth(450);
@ -104,7 +63,7 @@ public class AppWindowHelper {
public static Stage sideWindow(
String title, Function<Stage, Comp<?>> contentFunc, boolean bindSize, ObservableValue<Boolean> loading) {
var stage = new Stage();
var stage = AppWindowBounds.centerStage();
stage.initStyle(StageStyle.UNIFIED);
if (AppMainWindow.getInstance() != null) {
stage.initOwner(AppMainWindow.getInstance().getStage());
@ -114,7 +73,7 @@ public class AppWindowHelper {
addIcons(stage);
setupContent(stage, contentFunc, bindSize, loading);
setupStylesheets(stage.getScene());
fixInvalidStagePosition(stage);
AppWindowBounds.fixInvalidStagePosition(stage);
if (AppPrefs.get() != null && AppPrefs.get().enforceWindowModality().get()) {
stage.initModality(Modality.WINDOW_MODAL);
@ -122,30 +81,10 @@ public class AppWindowHelper {
stage.setOnShown(e -> {
AppTheme.initThemeHandlers(stage);
Platform.runLater(() -> {
centerToMainWindow(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(Window childStage) {
if (App.getApp() == null) {
childStage.centerOnScreen();
return;
}
var stage = App.getApp().getStage();
childStage.setX(stage.getX() + stage.getWidth() / 2 - childStage.getWidth() / 2);
childStage.setY(stage.getY() + stage.getHeight() / 2 - childStage.getHeight() / 2);
}
public static void showAlert(Consumer<Alert> c, Consumer<Optional<ButtonType>> bt) {
ThreadHelper.runAsync(() -> {
var r = showBlockingAlert(c);
@ -190,10 +129,9 @@ public class AppWindowHelper {
Alert a = AppWindowHelper.createEmptyAlert();
AppFont.normal(a.getDialogPane());
var s = (Stage) a.getDialogPane().getScene().getWindow();
fixInvalidStagePosition(s);
s.setOnShown(event -> {
Platform.runLater(() -> {
clampWindow(s).ifPresent(rectangle2D -> {
AppWindowBounds.clampWindow(s).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?
@ -203,6 +141,7 @@ public class AppWindowHelper {
});
event.consume();
});
AppWindowBounds.fixInvalidStagePosition(s);
a.getDialogPane().getScene().addEventFilter(KeyEvent.KEY_PRESSED, event -> {
if (event.getCode().equals(KeyCode.W) && event.isShortcutDown()) {
s.close();
@ -246,6 +185,9 @@ public class AppWindowHelper {
public static Alert createEmptyAlert() {
Alert alert = new Alert(Alert.AlertType.NONE);
if (AppMainWindow.getInstance() != null) {
alert.initOwner(AppMainWindow.getInstance().getStage());
}
alert.getDialogPane().getScene().setFill(Color.TRANSPARENT);
addIcons(((Stage) alert.getDialogPane().getScene().getWindow()));
setupStylesheets(alert.getDialogPane().getScene());
@ -324,110 +266,6 @@ public class AppWindowHelper {
});
}
private static Optional<Rectangle2D> clampWindow(Stage stage) {
if (!areNumbersValid(stage.getWidth(), stage.getHeight())) {
return Optional.empty();
}
var allScreenBounds = computeWindowScreenBounds(stage);
if (!areNumbersValid(
allScreenBounds.getMinX(),
allScreenBounds.getMinY(),
allScreenBounds.getMaxX(),
allScreenBounds.getMaxY())) {
return Optional.empty();
}
// 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()) {
w = allScreenBounds.getMaxX() - x;
changed = true;
}
if (y + h > allScreenBounds.getMaxY()) {
h = allScreenBounds.getMaxY() - y;
changed = true;
}
// This should not happen but on weird Linux systems nothing is impossible
if (w < 0 || h < 0) {
return Optional.empty();
}
return changed ? Optional.of(new Rectangle2D(x, y, w, h)) : Optional.empty();
}
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 : getWindowScreens(stage)) {
Rectangle2D screenBounds = screen.getBounds();
if (screenBounds.getMinX() < minX) {
minX = screenBounds.getMinX();
}
if (screenBounds.getMinY() < minY) {
minY = screenBounds.getMinY();
}
if (screenBounds.getMaxX() > maxX) {
maxX = screenBounds.getMaxX();
}
if (screenBounds.getMaxY() > maxY) {
maxY = screenBounds.getMaxY();
}
}
// Taskbar adjustment
maxY -= 50;
var w = maxX - minX;
var h = maxY - minY;
// This should not happen but on weird Linux systems nothing is impossible
if (w < 0 || h < 0) {
return new Rectangle2D(0, 0, 800, 600);
}
return new Rectangle2D(minX, minY, w, h);
}
private static void bindSize(Stage stage, Region r) {
if (r.getPrefWidth() == Region.USE_COMPUTED_SIZE) {
r.widthProperty().addListener((c, o, n) -> {

View file

@ -44,6 +44,10 @@ public class ModifiedStage extends Stage {
}
private static void applyModes(Stage stage) {
if (stage.getScene() == null) {
return;
}
if (OsType.getLocal() != OsType.WINDOWS || AppPrefs.get() == null) {
stage.getScene().getRoot().pseudoClassStateChanged(PseudoClass.getPseudoClass("seamless-frame"), false);
stage.getScene().getRoot().pseudoClassStateChanged(PseudoClass.getPseudoClass("separate-frame"), true);