diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserFileChooserSessionComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserFileChooserSessionComp.java index 992e14d96..0c014d33c 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserFileChooserSessionComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserFileChooserSessionComp.java @@ -6,11 +6,9 @@ import io.xpipe.app.browser.file.BrowserEntry; import io.xpipe.app.browser.file.BrowserFileSystemTabComp; import io.xpipe.app.browser.file.BrowserFileSystemTabModel; import io.xpipe.app.comp.Comp; -import io.xpipe.app.comp.base.DialogComp; -import io.xpipe.app.comp.base.LeftSplitPaneComp; -import io.xpipe.app.comp.base.StackComp; -import io.xpipe.app.comp.base.VerticalComp; +import io.xpipe.app.comp.base.*; import io.xpipe.app.comp.store.StoreEntryWrapper; +import io.xpipe.app.core.AppFontSizes; import io.xpipe.app.core.AppLayoutModel; import io.xpipe.app.ext.ShellStore; import io.xpipe.app.storage.DataStoreEntryRef; @@ -21,16 +19,11 @@ import io.xpipe.app.util.ThreadHelper; import io.xpipe.core.store.FileSystemStore; import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleStringProperty; import javafx.collections.ListChangeListener; -import javafx.geometry.Pos; -import javafx.scene.control.TextField; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; import javafx.scene.shape.Rectangle; -import javafx.stage.Stage; -import javafx.stage.Window; -import javafx.stage.WindowEvent; import java.util.List; import java.util.function.BiConsumer; @@ -38,65 +31,51 @@ import java.util.function.Consumer; import java.util.function.Predicate; import java.util.function.Supplier; -public class BrowserFileChooserSessionComp extends DialogComp { +public class BrowserFileChooserSessionComp extends ModalOverlayContentComp { - private final Stage stage; private final BrowserFileChooserSessionModel model; - public BrowserFileChooserSessionComp(Stage stage, BrowserFileChooserSessionModel model) { - this.stage = stage; + public BrowserFileChooserSessionComp(BrowserFileChooserSessionModel model) { this.model = model; } public static void openSingleFile( Supplier> store, Consumer file, boolean save) { - PlatformThread.runLaterIfNeeded(() -> { - var lastWindow = Window.getWindows().stream() - .filter(window -> window.isFocused()) - .findFirst(); - var model = new BrowserFileChooserSessionModel(BrowserFileSystemTabModel.SelectionMode.SINGLE_FILE); - DialogComp.showWindow(save ? "saveFileTitle" : "openFileTitle", stage -> { - stage.addEventFilter(WindowEvent.WINDOW_HIDDEN, event -> { - lastWindow.ifPresent(window -> window.requestFocus()); - }); - var comp = new BrowserFileChooserSessionComp(stage, model); - comp.apply(struc -> struc.get().setPrefSize(1200, 700)) - .styleClass("browser") - .styleClass("chooser"); - return comp; - }); - model.setOnFinish(fileStores -> { - file.accept(fileStores.size() > 0 ? fileStores.getFirst() : null); - }); - ThreadHelper.runAsync(() -> { - model.openFileSystemAsync(store.get(), null, null); - }); + var model = new BrowserFileChooserSessionModel(BrowserFileSystemTabModel.SelectionMode.SINGLE_FILE); + model.setOnFinish(fileStores -> { + file.accept(fileStores.size() > 0 ? fileStores.getFirst() : null); + }); + var comp = new BrowserFileChooserSessionComp(model) + .styleClass("browser") + .styleClass("chooser"); + var selection = new SimpleStringProperty(); + model.getFileSelection().addListener((ListChangeListener) c -> { + selection.set(c.getList().size() > 0 ? c.getList().getFirst().getRawFileEntry().getPath().toString() : null); + }); + var selectionField = new TextFieldComp(selection); + selectionField.apply(struc -> { + struc.get().setEditable(false); + AppFontSizes.base(struc.get()); + }); + selectionField.styleClass("chooser-selection"); + selectionField.hgrow(); + var modal = ModalOverlay.of(save ? "saveFileTitle" : "openFileTitle", comp); + modal.setRequireCloseButtonForClose(true); + modal.addButtonBarComp(selectionField); + modal.addButton(new ModalButton("select", () -> model.finishChooser(), true, true)); + modal.show(); + ThreadHelper.runAsync(() -> { + model.openFileSystemAsync(store.get(), null, null); }); } @Override - protected String finishKey() { - return "select"; + protected void onClose() { + model.closeFileSystem(); } @Override - protected Comp pane(Comp content) { - return content; - } - - @Override - protected void finish() { - stage.close(); - model.finishChooser(); - } - - @Override - protected void discard() { - model.finishWithoutChoice(); - } - - @Override - public Comp content() { + protected Region createSimple() { Predicate applicable = storeEntryWrapper -> { return (storeEntryWrapper.getEntry().getStore() instanceof ShellStore) && storeEntryWrapper.getEntry().getValidity().isUsable(); @@ -163,33 +142,6 @@ public class BrowserFileChooserSessionComp extends DialogComp { struc.getLeft().setMinWidth(200); struc.getLeft().setMaxWidth(500); }); - return splitPane; - } - - @Override - public Comp bottom() { - return Comp.of(() -> { - var selected = new HBox(); - selected.setAlignment(Pos.CENTER_LEFT); - model.getFileSelection().addListener((ListChangeListener) c -> { - PlatformThread.runLaterIfNeeded(() -> { - selected.getChildren() - .setAll(c.getList().stream() - .map(s -> { - var field = new TextField( - s.getRawFileEntry().getPath().toString()); - field.setEditable(false); - field.getStyleClass().add("chooser-selection"); - HBox.setHgrow(field, Priority.ALWAYS); - return field; - }) - .toList()); - }); - }); - var bottomBar = new HBox(selected); - HBox.setHgrow(selected, Priority.ALWAYS); - bottomBar.setAlignment(Pos.CENTER); - return bottomBar; - }); + return splitPane.createRegion(); } } diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserFileChooserSessionModel.java b/app/src/main/java/io/xpipe/app/browser/BrowserFileChooserSessionModel.java index e817e7382..7ad724896 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserFileChooserSessionModel.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserFileChooserSessionModel.java @@ -65,7 +65,7 @@ public class BrowserFileChooserSessionModel extends BrowserAbstractSessionModel< onFinish.accept(stores); } - public void finishWithoutChoice() { + public void closeFileSystem() { synchronized (BrowserFileChooserSessionModel.this) { var open = selectedEntry.getValue(); if (open != null) { diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserNavBarComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserNavBarComp.java index a1f407266..c95576f28 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserNavBarComp.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserNavBarComp.java @@ -46,7 +46,7 @@ public class BrowserNavBarComp extends Comp { ? BrowserIconManager.getFileIcon(model.getCurrentDirectory()) : null; }, - model.getCurrentPath()); + PlatformThread.sync(model.getCurrentPath())); var breadcrumbsGraphic = PrettyImageHelper.ofFixedSize(graphic, 24, 24) .styleClass("path-graphic") .createRegion(); @@ -85,7 +85,7 @@ public class BrowserNavBarComp extends Comp { && !model.getInOverview().get(); }, pathRegion.focusedProperty(), - model.getInOverview())); + PlatformThread.sync(model.getInOverview()))); var stack = new StackPane(pathRegion, breadcrumbsRegion); stack.setAlignment(Pos.CENTER_LEFT); pathRegion.prefHeightProperty().bind(stack.heightProperty()); @@ -135,7 +135,9 @@ public class BrowserNavBarComp extends Comp { private Comp> createPathBar() { var path = new SimpleStringProperty(); model.getCurrentPath().subscribe((newValue) -> { - path.set(newValue != null ? newValue.toString() : null); + PlatformThread.runLaterIfNeeded(() -> { + path.set(newValue != null ? newValue.toString() : null); + }); }); path.addListener((observable, oldValue, newValue) -> { ThreadHelper.runFailableAsync(() -> { diff --git a/app/src/main/java/io/xpipe/app/comp/base/DialogComp.java b/app/src/main/java/io/xpipe/app/comp/base/DialogComp.java index d198f16ee..03102f434 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/DialogComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/DialogComp.java @@ -25,29 +25,6 @@ import java.util.function.Function; public abstract class DialogComp extends Comp> { - public static void showWindow(String titleKey, Function f) { - var loading = new SimpleBooleanProperty(); - var dialog = new AtomicReference(); - Platform.runLater(() -> { - var stage = AppWindowHelper.sideWindow( - AppI18n.get(titleKey), - window -> { - var c = f.apply(window); - dialog.set(c); - loading.bind(c.busy()); - return c; - }, - false, - loading); - stage.setOnCloseRequest(event -> { - if (dialog.get() != null) { - dialog.get().discard(); - } - }); - stage.show(); - }); - } - protected Region createNavigation() { HBox buttons = new HBox(); buttons.setFillHeight(true); diff --git a/app/src/main/java/io/xpipe/app/comp/base/ModalOverlayComp.java b/app/src/main/java/io/xpipe/app/comp/base/ModalOverlayComp.java index 60a5eb24a..681431fff 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/ModalOverlayComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/ModalOverlayComp.java @@ -189,6 +189,7 @@ public class ModalOverlayComp extends SimpleComp { if (newValue.getButtons().size() > 0) { var buttonBar = new HBox(); + buttonBar.getStyleClass().add("button-bar"); buttonBar.setSpacing(10); buttonBar.setAlignment(Pos.CENTER_RIGHT); for (var o : newValue.getButtons()) { @@ -199,7 +200,7 @@ public class ModalOverlayComp extends SimpleComp { } } content.getChildren().add(buttonBar); - AppFontSizes.xs(buttonBar); + AppFontSizes.base(buttonBar); } var modalBox = new ModalBox(pane, content) { diff --git a/app/src/main/java/io/xpipe/app/core/AppProperties.java b/app/src/main/java/io/xpipe/app/core/AppProperties.java index fcec18b03..08fdedca2 100644 --- a/app/src/main/java/io/xpipe/app/core/AppProperties.java +++ b/app/src/main/java/io/xpipe/app/core/AppProperties.java @@ -57,6 +57,8 @@ public class AppProperties { boolean aotTrainMode; + boolean debugPlatformThreadAccess; + AppArguments arguments; XPipeDaemonMode explicitMode; @@ -105,6 +107,9 @@ public class AppProperties { debugThreads = Optional.ofNullable(System.getProperty("io.xpipe.app.debugThreads")) .map(Boolean::parseBoolean) .orElse(false); + debugPlatformThreadAccess = Optional.ofNullable(System.getProperty("io.xpipe.app.debugPlatformThreadAccess")) + .map(Boolean::parseBoolean) + .orElse(false); defaultDataDir = Path.of(System.getProperty("user.home"), isStaging() ? ".xpipe-ptb" : ".xpipe"); dataDir = Optional.ofNullable(System.getProperty("io.xpipe.app.dataDir")) .map(s -> { 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 30eef346d..6d2a489a0 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 @@ -10,6 +10,7 @@ import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.prefs.CloseBehaviourDialog; import io.xpipe.app.resources.AppImages; import io.xpipe.app.util.LicenseProvider; +import io.xpipe.app.util.NodeCallback; import io.xpipe.app.util.PlatformThread; import io.xpipe.app.util.ThreadHelper; import io.xpipe.core.process.OsType; @@ -97,6 +98,9 @@ public class AppMainWindow { return stage.isFocused() ? 1.0 : 0.8; }, stage.focusedProperty())); + if (AppProperties.get().isDebugPlatformThreadAccess()) { + NodeCallback.watchPlatformThreadChanges(content); + } var scene = new Scene(content, -1, -1, false); content.prefWidthProperty().bind(scene.widthProperty()); content.prefHeightProperty().bind(scene.heightProperty()); 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 7e9e03113..22f807283 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 @@ -84,32 +84,6 @@ public class AppWindowHelper { }); } - public static Stage sideWindow( - String title, Function> contentFunc, boolean bindSize, ObservableValue loading) { - var stage = AppWindowBounds.centerStage(); - ModifiedStage.prepareStage(stage); - if (AppMainWindow.getInstance() != null) { - stage.initOwner(AppMainWindow.getInstance().getStage()); - } - stage.setTitle(title); - - addIcons(stage); - setupContent(stage, contentFunc, bindSize, loading); - setupStylesheets(stage.getScene()); - AppWindowHelper.setupClickShield(stage); - AppWindowBounds.fixInvalidStagePosition(stage); - AppWindowHelper.addFontSize(stage); - - if (AppPrefs.get() != null && AppPrefs.get().enforceWindowModality().get()) { - stage.initModality(Modality.WINDOW_MODAL); - } - - stage.setOnShown(e -> { - AppTheme.initThemeHandlers(stage); - }); - return stage; - } - public static void setContent(Alert alert, String s) { alert.getDialogPane().setMinWidth(505); alert.getDialogPane().setPrefWidth(505); @@ -267,52 +241,4 @@ public class AppWindowHelper { } }); } - - public static void setupContent( - Stage stage, Function> contentFunc, boolean bindSize, ObservableValue loading) { - var baseComp = contentFunc.apply(stage); - var content = loading != null ? LoadingOverlayComp.noProgress(baseComp, loading) : baseComp; - var contentR = content.createRegion(); - var scene = new Scene(bindSize ? new Pane(contentR) : contentR, -1, -1, false); - scene.setFill(Color.TRANSPARENT); - stage.setScene(scene); - contentR.requestFocus(); - if (bindSize) { - bindSize(stage, contentR); - stage.setResizable(false); - } - - scene.addEventHandler(KeyEvent.KEY_PRESSED, event -> { - if (new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN).match(event)) { - stage.close(); - event.consume(); - } - }); - } - - private static void bindSize(Stage stage, Region r) { - if (r.getPrefWidth() == Region.USE_COMPUTED_SIZE) { - r.widthProperty().addListener((c, o, n) -> { - stage.sizeToScene(); - }); - } else { - stage.setWidth(r.getPrefWidth()); - r.prefWidthProperty().addListener((c, o, n) -> { - stage.sizeToScene(); - }); - } - - if (r.getPrefHeight() == Region.USE_COMPUTED_SIZE) { - r.heightProperty().addListener((c, o, n) -> { - stage.sizeToScene(); - }); - } else { - stage.setHeight(r.getPrefHeight()); - r.prefHeightProperty().addListener((c, o, n) -> { - stage.sizeToScene(); - }); - } - - stage.sizeToScene(); - } } diff --git a/app/src/main/java/io/xpipe/app/util/NodeCallback.java b/app/src/main/java/io/xpipe/app/util/NodeCallback.java new file mode 100644 index 000000000..2a7d348a6 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/util/NodeCallback.java @@ -0,0 +1,53 @@ +package io.xpipe.app.util; + +import javafx.application.Platform; +import javafx.collections.ListChangeListener; +import javafx.scene.Node; +import javafx.scene.Parent; + +import java.util.function.Consumer; + +public class NodeCallback { + + public static void watchGraph(Node node, Consumer callback) { + if (node instanceof Parent p) { + for (Node c : p.getChildrenUnmodifiable()) { + watchGraph(c, callback); + } + p.getChildrenUnmodifiable().addListener((ListChangeListener) change -> { + for (Node c : change.getList()) { + watchGraph(c, callback); + } + }); + } + callback.accept(node); + } + + public static void watchPlatformThreadChanges(Node node) { + watchGraph(node, c -> { + if (c instanceof Parent p) { + p.getChildrenUnmodifiable().addListener((ListChangeListener) change -> { + checkPlatformThread(); + }); + } + c.visibleProperty().addListener((observable, oldValue, newValue) -> { + checkPlatformThread(); + }); + c.boundsInParentProperty().addListener((observable, oldValue, newValue) -> { + checkPlatformThread(); + }); + c.managedProperty().addListener((observable, oldValue, newValue) -> { + checkPlatformThread(); + }); + c.opacityProperty().addListener((observable, oldValue, newValue) -> { + checkPlatformThread(); + }); + }); + } + + private static void checkPlatformThread() { + if (!Platform.isFxApplicationThread()) { + throw new IllegalStateException("Not in Fx application thread"); + } + } +} diff --git a/app/src/main/resources/io/xpipe/app/resources/style/browser.css b/app/src/main/resources/io/xpipe/app/resources/style/browser.css index 4f8b8a8ef..7ff7c146d 100644 --- a/app/src/main/resources/io/xpipe/app/resources/style/browser.css +++ b/app/src/main/resources/io/xpipe/app/resources/style/browser.css @@ -191,8 +191,13 @@ -fx-border-color: -color-border-default; } -.browser .chooser-selection { - -fx-background-color: -color-bg-default; +.browser.chooser { + -fx-padding: -11 -3 -10 -3; +} + +.chooser-selection { + -fx-background-color: -color-bg-subtle; + -fx-background-radius: 2; } .browser .singular { diff --git a/dist/logo/ico/logo_48x48.png b/dist/logo/ico/logo_48x48.png index b9ebbc06a..eb7356e6f 100644 Binary files a/dist/logo/ico/logo_48x48.png and b/dist/logo/ico/logo_48x48.png differ diff --git a/dist/logo/logo.ico b/dist/logo/logo.ico index 9462b4808..ac9e98347 100644 Binary files a/dist/logo/logo.ico and b/dist/logo/logo.ico differ