From 5b25f349880893d802ba0be434aa6dd52cc8ec0d Mon Sep 17 00:00:00 2001 From: crschnick Date: Sat, 21 Sep 2024 18:01:23 +0000 Subject: [PATCH] Rework dialogs --- .../browser/session/BrowserChooserComp.java | 136 ++++++------- .../io/xpipe/app/comp/base/DialogComp.java | 10 + .../app/comp/store/StoreCreationComp.java | 6 +- .../comp/store/StoreIconChoiceDialogComp.java | 3 + .../xpipe/app/comp/store/StoreNotesComp.java | 6 +- .../io/xpipe/app/core/AppLayoutModel.java | 15 +- .../java/io/xpipe/app/util/ScanAlert.java | 181 +----------------- .../java/io/xpipe/app/util/ScanDialog.java | 180 +++++++++++++++++ 8 files changed, 269 insertions(+), 268 deletions(-) create mode 100644 app/src/main/java/io/xpipe/app/util/ScanDialog.java diff --git a/app/src/main/java/io/xpipe/app/browser/session/BrowserChooserComp.java b/app/src/main/java/io/xpipe/app/browser/session/BrowserChooserComp.java index 647ed26c3..8590a8605 100644 --- a/app/src/main/java/io/xpipe/app/browser/session/BrowserChooserComp.java +++ b/app/src/main/java/io/xpipe/app/browser/session/BrowserChooserComp.java @@ -9,11 +9,8 @@ import io.xpipe.app.comp.base.DialogComp; import io.xpipe.app.comp.base.SideSplitPaneComp; import io.xpipe.app.comp.store.StoreEntryWrapper; import io.xpipe.app.core.AppFont; -import io.xpipe.app.core.AppI18n; import io.xpipe.app.core.AppLayoutModel; -import io.xpipe.app.core.window.AppWindowHelper; import io.xpipe.app.fxcomps.Comp; -import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.impl.StackComp; import io.xpipe.app.fxcomps.impl.VerticalComp; import io.xpipe.app.fxcomps.util.BindingsHelper; @@ -23,14 +20,12 @@ import io.xpipe.app.util.FileReference; import io.xpipe.app.util.ThreadHelper; import io.xpipe.core.store.FileSystemStore; import io.xpipe.core.store.ShellStore; - import javafx.beans.property.BooleanProperty; 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; @@ -40,7 +35,7 @@ import java.util.function.Consumer; import java.util.function.Predicate; import java.util.function.Supplier; -public class BrowserChooserComp extends SimpleComp { +public class BrowserChooserComp extends DialogComp { private final BrowserFileChooserModel model; @@ -52,24 +47,16 @@ public class BrowserChooserComp extends SimpleComp { Supplier> store, Consumer file, boolean save) { PlatformThread.runLaterIfNeeded(() -> { var model = new BrowserFileChooserModel(OpenFileSystemModel.SelectionMode.SINGLE_FILE); - var comp = new BrowserChooserComp(model) - .apply(struc -> struc.get().setPrefSize(1200, 700)) - .apply(struc -> AppFont.normal(struc.get())); - var window = AppWindowHelper.sideWindow( - AppI18n.get(save ? "saveFileTitle" : "openFileTitle"), - stage -> { - return comp; - }, - false, - null); + DialogComp.showWindow(save ? "saveFileTitle" : "openFileTitle", stage -> { + var comp = new BrowserChooserComp(model); + comp.apply(struc -> struc.get().setPrefSize(1200, 700)) + .apply(struc -> AppFont.normal(struc.get())) + .styleClass("browser") + .styleClass("chooser"); + return comp; + }); model.setOnFinish(fileStores -> { file.accept(fileStores.size() > 0 ? fileStores.getFirst() : null); - window.close(); - }); - window.show(); - window.setOnHidden(event -> { - model.finishWithoutChoice(); - event.consume(); }); ThreadHelper.runAsync(() -> { model.openFileSystemAsync(store.get(), null, null); @@ -77,8 +64,32 @@ public class BrowserChooserComp extends SimpleComp { }); } + + + + @Override - protected Region createSimple() { + protected String finishKey() { + return "select"; + } + + @Override + protected Comp pane(Comp content) { + return content; + } + + @Override + protected void finish() { + model.finishChooser(); + } + + @Override + protected void discard() { + model.finishWithoutChoice(); + } + + @Override + public Comp content() { Predicate applicable = storeEntryWrapper -> { return (storeEntryWrapper.getEntry().getStore() instanceof ShellStore) && storeEntryWrapper.getEntry().getValidity().isUsable(); @@ -144,60 +155,33 @@ public class BrowserChooserComp extends SimpleComp { struc.getLeft().setMinWidth(200); struc.getLeft().setMaxWidth(500); }); + return splitPane; + } - var dialogPane = new DialogComp() { - - @Override - protected String finishKey() { - return "select"; - } - - @Override - protected Comp pane(Comp content) { - return content; - } - - @Override - protected void finish() { - model.finishChooser(); - } - - @Override - public Comp content() { - 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()); - 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; + @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()); + field.setEditable(false); + field.getStyleClass().add("chooser-selection"); + HBox.setHgrow(field, Priority.ALWAYS); + return field; + }) + .toList()); }); - } - }; - - var r = dialogPane.createRegion(); - r.getStyleClass().add("browser"); - r.getStyleClass().add("chooser"); - return r; + }); + var bottomBar = new HBox(selected); + HBox.setHgrow(selected, Priority.ALWAYS); + bottomBar.setAlignment(Pos.CENTER); + return bottomBar; + }); } } 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 962355810..782bc981b 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 @@ -20,22 +20,30 @@ import javafx.stage.Stage; import atlantafx.base.theme.Styles; import java.util.List; +import java.util.concurrent.atomic.AtomicReference; 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(); }); } @@ -97,6 +105,8 @@ public abstract class DialogComp extends Comp> { protected abstract void finish(); + protected abstract void discard(); + public abstract Comp content(); protected Comp pane(Comp content) { diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreCreationComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreCreationComp.java index 3f34c7353..91dc48db4 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreCreationComp.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreCreationComp.java @@ -323,9 +323,6 @@ public class StoreCreationComp extends DialogComp { try (var ignored = new BooleanScope(busy).start()) { DataStorage.get().addStoreEntryInProgress(entry.getValue()); var context = entry.getValue().validateAndKeepOpenOrThrowAndClose(null); - if (context == null) { - entry.getValue().validateRefreshChildrenOrThrow(); - } commit(context, true); } catch (Throwable ex) { if (ex instanceof ValidationException) { @@ -354,6 +351,9 @@ public class StoreCreationComp extends DialogComp { }); } + @Override + protected void discard() {} + @Override public Comp content() { return Comp.of(this::createLayout); diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreIconChoiceDialogComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreIconChoiceDialogComp.java index a3a7b3ca5..85ddbf4bb 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreIconChoiceDialogComp.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreIconChoiceDialogComp.java @@ -63,6 +63,9 @@ public class StoreIconChoiceDialogComp extends SimpleComp { dialogStage.close(); } + @Override + protected void discard() {} + @Override public Comp content() { return new StoreIconChoiceComp(selected, SystemIcons.getSystemIcons(), 5, filterText, () -> { diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreNotesComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreNotesComp.java index 35431fb16..d6590184b 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreNotesComp.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreNotesComp.java @@ -96,6 +96,9 @@ public class StoreNotesComp extends Comp { ref.get().hide(); } + @Override + protected void discard() {} + @Override protected String finishKey() { return "apply"; @@ -130,8 +133,7 @@ public class StoreNotesComp extends Comp { popover.setTitle(wrapper.getName().getValue()); popover.showingProperty().addListener((observable, oldValue, newValue) -> { if (!newValue) { - n.setValue( - new StoreNotes(n.getValue().getCommited(), n.getValue().getCommited())); + n.setValue(new StoreNotes(n.getValue().getCommited(), n.getValue().getCommited())); DataStorage.get().saveAsync(); ref.set(null); } diff --git a/app/src/main/java/io/xpipe/app/core/AppLayoutModel.java b/app/src/main/java/io/xpipe/app/core/AppLayoutModel.java index 38572a52d..b63102895 100644 --- a/app/src/main/java/io/xpipe/app/core/AppLayoutModel.java +++ b/app/src/main/java/io/xpipe/app/core/AppLayoutModel.java @@ -115,13 +115,14 @@ public class AppLayoutModel { null, () -> Hyperlinks.open( "http://localhost:" + AppBeaconServer.get().getPort()), - null), - new Entry( - AppI18n.observable("webtop"), - "mdi2d-desktop-mac", - null, - () -> Hyperlinks.open(Hyperlinks.GITHUB_WEBTOP), - null))); + null) +// new Entry( +// AppI18n.observable("webtop"), +// "mdi2d-desktop-mac", +// null, +// () -> Hyperlinks.open(Hyperlinks.GITHUB_WEBTOP), +// null) + )); return l; } diff --git a/app/src/main/java/io/xpipe/app/util/ScanAlert.java b/app/src/main/java/io/xpipe/app/util/ScanAlert.java index 5ba633d30..5845180c1 100644 --- a/app/src/main/java/io/xpipe/app/util/ScanAlert.java +++ b/app/src/main/java/io/xpipe/app/util/ScanAlert.java @@ -1,16 +1,9 @@ package io.xpipe.app.util; import io.xpipe.app.comp.base.DialogComp; -import io.xpipe.app.comp.base.ListSelectorComp; -import io.xpipe.app.comp.store.StoreViewState; -import io.xpipe.app.core.AppI18n; import io.xpipe.app.ext.ScanProvider; -import io.xpipe.app.fxcomps.Comp; -import io.xpipe.app.fxcomps.impl.DataStoreChoiceComp; import io.xpipe.app.issue.ErrorEvent; -import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreEntry; -import io.xpipe.app.storage.DataStoreEntryRef; import io.xpipe.core.process.ShellControl; import io.xpipe.core.process.ShellStoreState; import io.xpipe.core.process.ShellTtyState; @@ -18,21 +11,9 @@ import io.xpipe.core.store.ShellStore; import io.xpipe.core.store.ShellValidationContext; import io.xpipe.core.store.ValidationContext; -import javafx.application.Platform; -import javafx.beans.property.*; -import javafx.beans.value.ObservableValue; -import javafx.collections.FXCollections; -import javafx.geometry.Insets; -import javafx.scene.layout.StackPane; -import javafx.scene.layout.VBox; -import javafx.stage.Stage; - import java.util.ArrayList; import java.util.List; import java.util.function.BiFunction; -import java.util.function.Function; - -import static javafx.scene.layout.Priority.ALWAYS; public class ScanAlert { @@ -90,168 +71,8 @@ public class ScanAlert { ShellValidationContext shellValidationContext) { DialogComp.showWindow( "scanAlertTitle", - stage -> new Dialog( + stage -> new ScanDialog( stage, initialStore != null ? initialStore.ref() : null, applicable, shellValidationContext)); } - private static class Dialog extends DialogComp { - - private final DataStoreEntryRef initialStore; - private final BiFunction> applicable; - private final Stage window; - private final ObjectProperty> entry; - private final ListProperty selected = - new SimpleListProperty<>(FXCollections.observableArrayList()); - private final BooleanProperty busy = new SimpleBooleanProperty(); - private ShellValidationContext shellValidationContext; - - private Dialog( - Stage window, - DataStoreEntryRef entry, - BiFunction> applicable, - ShellValidationContext shellValidationContext) { - this.window = window; - this.initialStore = entry; - this.entry = new SimpleObjectProperty<>(entry); - this.applicable = applicable; - this.shellValidationContext = shellValidationContext; - } - - @Override - protected ObservableValue busy() { - return busy; - } - - @Override - protected void finish() { - ThreadHelper.runFailableAsync(() -> { - try { - if (entry.get() == null) { - return; - } - - Platform.runLater(() -> { - window.close(); - }); - - BooleanScope.executeExclusive(busy, () -> { - entry.get().get().setExpanded(true); - var copy = new ArrayList<>(selected); - for (var a : copy) { - // If the user decided to remove the selected entry - // while the scan is running, just return instantly - if (!DataStorage.get() - .getStoreEntriesSet() - .contains(entry.get().get())) { - return; - } - - // Previous scan operation could have exited the shell - shellValidationContext.get().start(); - - try { - a.getScanner().run(); - } catch (Throwable ex) { - ErrorEvent.fromThrowable(ex).handle(); - } - } - }); - } finally { - shellValidationContext.close(); - shellValidationContext = null; - } - }); - } - - @Override - protected Comp pane(Comp content) { - return content; - } - - @Override - public Comp content() { - StackPane stackPane = new StackPane(); - stackPane.getStyleClass().add("scan-list"); - - var b = new OptionsBuilder() - .name("scanAlertChoiceHeader") - .description("scanAlertChoiceHeaderDescription") - .addComp(new DataStoreChoiceComp<>( - DataStoreChoiceComp.Mode.OTHER, - null, - entry, - ShellStore.class, - store1 -> true, - StoreViewState.get().getAllConnectionsCategory()) - .disable(new SimpleBooleanProperty(initialStore != null))) - .name("scanAlertHeader") - .description("scanAlertHeaderDescription") - .addComp(Comp.of(() -> stackPane).vgrow()) - .buildComp() - .prefWidth(500) - .prefHeight(680) - .apply(struc -> { - VBox.setVgrow(struc.get().getChildren().get(1), ALWAYS); - }) - .padding(new Insets(5, 20, 20, 20)); - - entry.subscribe(newValue -> { - selected.clear(); - stackPane.getChildren().clear(); - - if (newValue == null) { - return; - } - - ThreadHelper.runFailableAsync(() -> { - BooleanScope.executeExclusive(busy, () -> { - if (shellValidationContext != null) { - shellValidationContext.close(); - shellValidationContext = null; - } - - shellValidationContext = new ShellValidationContext(newValue.getStore() - .control() - .withoutLicenseCheck() - .start()); - var a = applicable.apply(entry.get().get(), shellValidationContext.get()); - - Platform.runLater(() -> { - if (a == null) { - window.close(); - return; - } - - selected.setAll(a.stream() - .filter(scanOperation -> - scanOperation.isDefaultSelected() && !scanOperation.isDisabled()) - .toList()); - Function nameFunc = (ScanProvider.ScanOperation s) -> { - var n = AppI18n.get(s.getNameKey()); - if (s.getLicensedFeatureId() == null) { - return n; - } - - var suffix = LicenseProvider.get().getFeature(s.getLicensedFeatureId()); - return n - + suffix.getDescriptionSuffix() - .map(d -> " (" + d + ")") - .orElse(""); - }; - var r = new ListSelectorComp<>( - a, - nameFunc, - selected, - scanOperation -> scanOperation.isDisabled(), - a.size() > 3) - .createRegion(); - stackPane.getChildren().add(r); - }); - }); - }); - }); - - return b; - } - } } diff --git a/app/src/main/java/io/xpipe/app/util/ScanDialog.java b/app/src/main/java/io/xpipe/app/util/ScanDialog.java new file mode 100644 index 000000000..e0f04530a --- /dev/null +++ b/app/src/main/java/io/xpipe/app/util/ScanDialog.java @@ -0,0 +1,180 @@ +package io.xpipe.app.util; + +import io.xpipe.app.comp.base.DialogComp; +import io.xpipe.app.comp.base.ListSelectorComp; +import io.xpipe.app.comp.store.StoreViewState; +import io.xpipe.app.core.AppI18n; +import io.xpipe.app.ext.ScanProvider; +import io.xpipe.app.fxcomps.Comp; +import io.xpipe.app.fxcomps.impl.DataStoreChoiceComp; +import io.xpipe.app.issue.ErrorEvent; +import io.xpipe.app.storage.DataStorage; +import io.xpipe.app.storage.DataStoreEntry; +import io.xpipe.app.storage.DataStoreEntryRef; +import io.xpipe.core.process.ShellControl; +import io.xpipe.core.store.ShellStore; +import io.xpipe.core.store.ShellValidationContext; +import javafx.application.Platform; +import javafx.beans.property.*; +import javafx.beans.value.ObservableValue; +import javafx.collections.FXCollections; +import javafx.geometry.Insets; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import javafx.stage.Stage; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Function; + +import static javafx.scene.layout.Priority.ALWAYS; + +class ScanDialog extends DialogComp { + + private final DataStoreEntryRef initialStore; + private final BiFunction> applicable; + private final Stage window; + private final ObjectProperty> entry; + private final ListProperty selected = new SimpleListProperty<>(FXCollections.observableArrayList()); + private final BooleanProperty busy = new SimpleBooleanProperty(); + private ShellValidationContext shellValidationContext; + + ScanDialog( + Stage window, DataStoreEntryRef entry, BiFunction> applicable, + ShellValidationContext shellValidationContext + ) { + this.window = window; + this.initialStore = entry; + this.entry = new SimpleObjectProperty<>(entry); + this.applicable = applicable; + this.shellValidationContext = shellValidationContext; + } + + @Override + protected ObservableValue busy() { + return busy; + } + + @Override + protected void finish() { + ThreadHelper.runFailableAsync(() -> { + try { + if (entry.get() == null) { + return; + } + + Platform.runLater(() -> { + window.close(); + }); + + BooleanScope.executeExclusive(busy, () -> { + entry.get().get().setExpanded(true); + var copy = new ArrayList<>(selected); + for (var a : copy) { + // If the user decided to remove the selected entry + // while the scan is running, just return instantly + if (!DataStorage.get().getStoreEntriesSet().contains(entry.get().get())) { + return; + } + + // Previous scan operation could have exited the shell + shellValidationContext.get().start(); + + try { + a.getScanner().run(); + } catch (Throwable ex) { + ErrorEvent.fromThrowable(ex).handle(); + } + } + }); + } finally { + shellValidationContext.close(); + shellValidationContext = null; + } + }); + } + + @Override + protected void discard() { + ThreadHelper.runAsync(() -> { + shellValidationContext.close(); + shellValidationContext = null; + }); + } + + @Override + protected Comp pane(Comp content) { + return content; + } + + @Override + public Comp content() { + StackPane stackPane = new StackPane(); + stackPane.getStyleClass().add("scan-list"); + + var b = new OptionsBuilder().name("scanAlertChoiceHeader") + .description("scanAlertChoiceHeaderDescription") + .addComp(new DataStoreChoiceComp<>(DataStoreChoiceComp.Mode.OTHER, null, entry, ShellStore.class, store1 -> true, + StoreViewState.get().getAllConnectionsCategory()).disable(new SimpleBooleanProperty(initialStore != null))) + .name("scanAlertHeader") + .description("scanAlertHeaderDescription") + .addComp(Comp.of(() -> stackPane).vgrow()) + .buildComp() + .prefWidth(500) + .prefHeight(680) + .apply(struc -> { + VBox.setVgrow(struc.get().getChildren().get(1), ALWAYS); + }) + .padding(new Insets(5, 20, 20, 20)); + + entry.subscribe(newValue -> { + onUpdate(newValue, stackPane); + }); + + return b; + } + + private void onUpdate(DataStoreEntryRef newValue, StackPane stackPane) { + selected.clear(); + stackPane.getChildren().clear(); + + if (newValue == null) { + return; + } + + ThreadHelper.runFailableAsync(() -> { + BooleanScope.executeExclusive(busy, () -> { + if (shellValidationContext != null) { + shellValidationContext.close(); + shellValidationContext = null; + } + + shellValidationContext = new ShellValidationContext(newValue.getStore().control().withoutLicenseCheck().start()); + var a = applicable.apply(entry.get().get(), shellValidationContext.get()); + + Platform.runLater(() -> { + if (a == null) { + window.close(); + return; + } + + selected.setAll( + a.stream().filter(scanOperation -> scanOperation.isDefaultSelected() && !scanOperation.isDisabled()).toList()); + Function nameFunc = (ScanProvider.ScanOperation s) -> { + var n = AppI18n.get(s.getNameKey()); + if (s.getLicensedFeatureId() == null) { + return n; + } + + var suffix = LicenseProvider.get().getFeature(s.getLicensedFeatureId()); + return n + suffix.getDescriptionSuffix().map(d -> " (" + d + ")").orElse(""); + }; + var r = new ListSelectorComp<>(a, nameFunc, selected, scanOperation -> scanOperation.isDisabled(), + a.size() > 3).createRegion(); + stackPane.getChildren().add(r); + }); + }); + }); + } +}