diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserTransferComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserTransferComp.java index c31dab7f..259f3ac5 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserTransferComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserTransferComp.java @@ -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(); }); diff --git a/app/src/main/java/io/xpipe/app/core/window/AppMainWindow.java b/app/src/main/java/io/xpipe/app/core/window/AppMainWindow.java index f0747e69..3cd4b1ef 100644 --- a/app/src/main/java/io/xpipe/app/core/window/AppMainWindow.java +++ b/app/src/main/java/io/xpipe/app/core/window/AppMainWindow.java @@ -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(); diff --git a/app/src/main/java/io/xpipe/app/core/window/AppWindowBounds.java b/app/src/main/java/io/xpipe/app/core/window/AppWindowBounds.java new file mode 100644 index 00000000..5e90e622 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/core/window/AppWindowBounds.java @@ -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 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 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); + } +} diff --git a/app/src/main/java/io/xpipe/app/core/window/AppWindowHelper.java b/app/src/main/java/io/xpipe/app/core/window/AppWindowHelper.java index 6a943ca4..1b2d4282 100644 --- a/app/src/main/java/io/xpipe/app/core/window/AppWindowHelper.java +++ b/app/src/main/java/io/xpipe/app/core/window/AppWindowHelper.java @@ -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> contentFunc, boolean bindSize, ObservableValue 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 c, Consumer> 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 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 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) -> { diff --git a/app/src/main/java/io/xpipe/app/core/window/ModifiedStage.java b/app/src/main/java/io/xpipe/app/core/window/ModifiedStage.java index 0d64a65f..209415b9 100644 --- a/app/src/main/java/io/xpipe/app/core/window/ModifiedStage.java +++ b/app/src/main/java/io/xpipe/app/core/window/ModifiedStage.java @@ -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);