diff --git a/app/src/main/java/io/xpipe/app/comp/base/ListSelectorComp.java b/app/src/main/java/io/xpipe/app/comp/base/ListSelectorComp.java index 81a297460..ddd6e01fb 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/ListSelectorComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/ListSelectorComp.java @@ -3,7 +3,9 @@ package io.xpipe.app.comp.base; import io.xpipe.app.comp.SimpleComp; import io.xpipe.app.core.AppI18n; +import io.xpipe.app.util.PlatformThread; import javafx.beans.property.ListProperty; +import javafx.collections.ObservableList; import javafx.geometry.Orientation; import javafx.scene.control.CheckBox; import javafx.scene.control.Label; @@ -19,16 +21,17 @@ import java.util.ArrayList; import java.util.List; import java.util.function.Function; import java.util.function.Predicate; +import java.util.function.Supplier; @Value @EqualsAndHashCode(callSuper = true) public class ListSelectorComp extends SimpleComp { - List values; + ObservableList values; Function toString; ListProperty selected; Predicate disable; - boolean showAllSelector; + Supplier showAllSelector; @Override protected Region createSimple() { @@ -36,56 +39,62 @@ public class ListSelectorComp extends SimpleComp { vbox.setSpacing(8); vbox.getStyleClass().add("list-content"); var cbs = new ArrayList(); - for (var v : values) { - var cb = new CheckBox(null); - if (disable.test(v)) { - cb.setDisable(true); - } - cbs.add(cb); - cb.setAccessibleText(toString.apply(v)); - cb.setSelected(selected.contains(v)); - cb.selectedProperty().addListener((c, o, n) -> { - if (n) { - selected.add(v); - } else { - selected.remove(v); - } - }); - var l = new Label(toString.apply(v), cb); - l.setGraphicTextGap(9); - l.setOnMouseClicked(event -> { - if (disable.test(v)) { - return; - } - - cb.setSelected(!cb.isSelected()); - event.consume(); - }); - l.opacityProperty().bind(cb.opacityProperty()); - vbox.getChildren().add(l); - } - - if (showAllSelector) { - var allSelector = new CheckBox(null); - allSelector.setSelected( - values.stream().filter(t -> !disable.test(t)).count() == selected.size()); - allSelector.selectedProperty().addListener((observable, oldValue, newValue) -> { - cbs.forEach(checkBox -> { - if (checkBox.isDisabled()) { - return; + values.subscribe(() -> { + var currentVals = new ArrayList<>(values); + PlatformThread.runLaterIfNeeded(() -> { + vbox.getChildren().clear(); + cbs.clear(); + for (var v : currentVals) { + var cb = new CheckBox(null); + if (disable.test(v)) { + cb.setDisable(true); } + cbs.add(cb); + cb.setAccessibleText(toString.apply(v)); + cb.setSelected(selected.contains(v)); + cb.selectedProperty().addListener((c, o, n) -> { + if (n) { + selected.add(v); + } else { + selected.remove(v); + } + }); + var l = new Label(toString.apply(v), cb); + l.setGraphicTextGap(9); + l.setOnMouseClicked(event -> { + if (disable.test(v)) { + return; + } - checkBox.setSelected(newValue); - }); + cb.setSelected(!cb.isSelected()); + event.consume(); + }); + l.opacityProperty().bind(cb.opacityProperty()); + vbox.getChildren().add(l); + } + + if (showAllSelector.get()) { + var allSelector = new CheckBox(null); + allSelector.setSelected( + values.stream().filter(t -> !disable.test(t)).count() == selected.size()); + allSelector.selectedProperty().addListener((observable, oldValue, newValue) -> { + cbs.forEach(checkBox -> { + if (checkBox.isDisabled()) { + return; + } + + checkBox.setSelected(newValue); + }); + }); + var l = new Label(null, allSelector); + l.textProperty().bind(AppI18n.observable("selectAll")); + l.setGraphicTextGap(9); + l.setOnMouseClicked(event -> allSelector.setSelected(!allSelector.isSelected())); + vbox.getChildren().add(new Separator(Orientation.HORIZONTAL)); + vbox.getChildren().add(l); + } }); - var l = new Label(null, allSelector); - l.textProperty().bind(AppI18n.observable("selectAll")); - l.setGraphicTextGap(9); - l.setOnMouseClicked(event -> allSelector.setSelected(!allSelector.isSelected())); - vbox.getChildren().add(new Separator(Orientation.HORIZONTAL)); - vbox.getChildren().add(l); - } - + }); var sp = new ScrollPane(vbox); sp.setFitToWidth(true); sp.getStyleClass().add("list-selector-comp"); diff --git a/app/src/main/java/io/xpipe/app/comp/base/LoadingOverlayComp.java b/app/src/main/java/io/xpipe/app/comp/base/LoadingOverlayComp.java index 3e767e989..29130bdb0 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/LoadingOverlayComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/LoadingOverlayComp.java @@ -86,8 +86,8 @@ public class LoadingOverlayComp extends Comp> { r.heightProperty())); loading.prefHeightProperty().bind(loading.prefWidthProperty()); - stack.maxWidthProperty().bind(r.prefWidthProperty()); - stack.maxHeightProperty().bind(r.prefHeightProperty()); + stack.prefWidthProperty().bind(r.widthProperty()); + stack.prefHeightProperty().bind(r.heightProperty()); return new SimpleCompStructure<>(stack); } 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 403234696..bfc1e7638 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 @@ -117,6 +117,9 @@ public class ModalOverlayComp extends SimpleComp { if (runnable != null) { runnable.run(); } + if (oldValue.getContent() instanceof ModalOverlayContentComp mocc) { + mocc.setModalOverlay(null); + } } if (newValue != null) { @@ -146,13 +149,6 @@ public class ModalOverlayComp extends SimpleComp { closeButton.setVisible(false); } } - - // Wait 2 pulses before focus so that the scene can be assigned to r - Platform.runLater(() -> { - Platform.runLater(() -> { - modalBox.requestFocus(); - }); - }); } private Region toBox(ModalPane pane, ModalOverlay newValue) { diff --git a/app/src/main/java/io/xpipe/app/comp/base/ModalOverlayStackComp.java b/app/src/main/java/io/xpipe/app/comp/base/ModalOverlayStackComp.java index d46b7d494..7a9a0d733 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/ModalOverlayStackComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/ModalOverlayStackComp.java @@ -29,10 +29,16 @@ public class ModalOverlayStackComp extends SimpleComp { private Comp buildModalOverlay(Comp current, int index) { var prop = new SimpleObjectProperty(); modalOverlay.subscribe(() -> { + var ex = prop.get(); + // Don't shift just for an index change + if (ex != null && modalOverlay.contains(ex)) { + return; + } + prop.set(modalOverlay.size() > index ? modalOverlay.get(index) : null); }); prop.addListener((observable, oldValue, newValue) -> { - if (newValue == null) { + if (newValue == null && modalOverlay.indexOf(oldValue) == index) { modalOverlay.remove(oldValue); } }); 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 7f67fc2d7..b230435ea 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 @@ -234,7 +234,7 @@ public class StoreCreationComp extends DialogComp { && AppPrefs.get() .openConnectionSearchWindowOnConnectionCreation() .get()) { - ScanAlert.showAsync(e); + ScanDialog.showAsync(e); } if (selectCategory) { diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreCreationMenu.java b/app/src/main/java/io/xpipe/app/comp/store/StoreCreationMenu.java index 952505ab8..f83a99c38 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreCreationMenu.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreCreationMenu.java @@ -4,7 +4,7 @@ import io.xpipe.app.comp.base.PrettyImageHelper; import io.xpipe.app.core.AppI18n; import io.xpipe.app.ext.DataStoreCreationCategory; import io.xpipe.app.ext.DataStoreProviders; -import io.xpipe.app.util.ScanAlert; +import io.xpipe.app.util.ScanDialog; import javafx.scene.control.Menu; import javafx.scene.control.MenuButton; @@ -22,7 +22,7 @@ public class StoreCreationMenu { automatically.setGraphic(new FontIcon("mdi2e-eye-plus-outline")); automatically.textProperty().bind(AppI18n.observable("addAutomatically")); automatically.setOnAction(event -> { - ScanAlert.showAsync(null); + ScanDialog.showAsync(null); event.consume(); }); menu.getItems().add(automatically); diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreIntroComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreIntroComp.java index ec0d7fd96..8db75aca0 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreIntroComp.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreIntroComp.java @@ -6,7 +6,7 @@ import io.xpipe.app.core.AppFont; import io.xpipe.app.core.AppI18n; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.storage.DataStorage; -import io.xpipe.app.util.ScanAlert; +import io.xpipe.app.util.ScanDialog; import io.xpipe.core.process.OsType; import javafx.geometry.Insets; @@ -38,7 +38,7 @@ public class StoreIntroComp extends SimpleComp { var scanButton = new Button(null, new FontIcon("mdi2m-magnify")); scanButton.textProperty().bind(AppI18n.observable("detectConnections")); - scanButton.setOnAction(event -> ScanAlert.showAsync(DataStorage.get().local())); + scanButton.setOnAction(event -> ScanDialog.showAsync(DataStorage.get().local())); scanButton.setDefaultButton(true); var scanPane = new StackPane(scanButton); scanPane.setAlignment(Pos.CENTER); diff --git a/app/src/main/java/io/xpipe/app/core/window/AppDialog.java b/app/src/main/java/io/xpipe/app/core/window/AppDialog.java index 75a87aff4..3c9fd0810 100644 --- a/app/src/main/java/io/xpipe/app/core/window/AppDialog.java +++ b/app/src/main/java/io/xpipe/app/core/window/AppDialog.java @@ -33,7 +33,9 @@ public class AppDialog { } public static void closeDialog(ModalOverlay overlay) { - modalOverlay.remove(overlay); + PlatformThread.runLaterIfNeeded(() -> { + modalOverlay.remove(overlay); + }); } public static void waitForClose() { diff --git a/app/src/main/java/io/xpipe/app/issue/UserReportComp.java b/app/src/main/java/io/xpipe/app/issue/UserReportComp.java index 82184581d..b0898864d 100644 --- a/app/src/main/java/io/xpipe/app/issue/UserReportComp.java +++ b/app/src/main/java/io/xpipe/app/issue/UserReportComp.java @@ -59,7 +59,7 @@ public class UserReportComp extends SimpleComp { private Comp createAttachments() { var list = new ListSelectorComp<>( - event.getAttachments(), + FXCollections.observableList(event.getAttachments()), file -> { if (file.equals(AppLogs.get().getSessionLogsDirectory())) { return AppI18n.get("logFilesAttachment"); @@ -69,7 +69,7 @@ public class UserReportComp extends SimpleComp { }, includedDiagnostics, file -> false, - false) + () -> false) .styleClass("attachment-list"); return new TitledPaneComp(AppI18n.observable("additionalErrorAttachments"), list, 100) .apply(struc -> struc.get().setExpanded(true)) diff --git a/app/src/main/java/io/xpipe/app/util/CommandSupport.java b/app/src/main/java/io/xpipe/app/util/CommandSupport.java index 951bbe77f..13bcd80d5 100644 --- a/app/src/main/java/io/xpipe/app/util/CommandSupport.java +++ b/app/src/main/java/io/xpipe/app/util/CommandSupport.java @@ -11,27 +11,6 @@ import java.util.Optional; public class CommandSupport { - public static String getPath(ShellControl sc) throws Exception { - var path = sc.command(sc.getShellDialect().getPrintEnvironmentVariableCommand("PATH")) - .readStdoutOrThrow(); - return path; - } - - public static String getLibraryPath(ShellControl sc) throws Exception { - var path = sc.command(sc.getShellDialect().getPrintEnvironmentVariableCommand("LD_LIBRARY_PATH")) - .readStdoutOrThrow(); - return path; - } - - public static boolean isRoot(ShellControl shellControl) throws Exception { - if (shellControl.getOsType() == OsType.WINDOWS) { - return false; - } - - var isRoot = shellControl.executeSimpleBooleanCommand("test \"${EUID:-$(id -u)}\" -eq 0"); - return isRoot; - } - public static Optional findProgram(ShellControl processControl, String name) throws Exception { var out = processControl .command(processControl.getShellDialect().getWhichCommand(name)) diff --git a/app/src/main/java/io/xpipe/app/util/ScanAlert.java b/app/src/main/java/io/xpipe/app/util/ScanAlert.java deleted file mode 100644 index ed390854b..000000000 --- a/app/src/main/java/io/xpipe/app/util/ScanAlert.java +++ /dev/null @@ -1,74 +0,0 @@ -package io.xpipe.app.util; - -import io.xpipe.app.comp.base.ModalButton; -import io.xpipe.app.comp.base.ModalOverlay; -import io.xpipe.app.ext.ScanProvider; -import io.xpipe.app.ext.ShellStore; -import io.xpipe.app.issue.ErrorEvent; -import io.xpipe.app.storage.DataStoreEntry; -import io.xpipe.core.process.ShellControl; -import io.xpipe.core.process.ShellTtyState; -import io.xpipe.core.process.SystemState; - -import java.util.ArrayList; -import java.util.List; -import java.util.function.BiFunction; - -public class ScanAlert { - - public static void showAsync(DataStoreEntry entry) { - ThreadHelper.runAsync(() -> { - var showForCon = entry == null - || (entry.getStore() instanceof ShellStore - && (!(entry.getStorePersistentState() instanceof SystemState systemState) - || systemState.getTtyState() == null - || systemState.getTtyState() == ShellTtyState.NONE)); - if (showForCon) { - showForShellStore(entry); - } - }); - } - - public static void showForShellStore(DataStoreEntry initial) { - show(initial, (DataStoreEntry entry, ShellControl sc) -> { - if (!sc.canHaveSubshells()) { - return null; - } - - if (!sc.getShellDialect().getDumbMode().supportsAnyPossibleInteraction()) { - return null; - } - - if (sc.getTtyState() != ShellTtyState.NONE) { - return null; - } - - var providers = ScanProvider.getAll(); - var applicable = new ArrayList(); - for (ScanProvider scanProvider : providers) { - try { - // Previous scan operation could have exited the shell - sc.start(); - ScanProvider.ScanOpportunity operation = scanProvider.create(entry, sc); - if (operation != null) { - applicable.add(operation); - } - } catch (Exception ex) { - ErrorEvent.fromThrowable(ex).handle(); - } - } - return applicable; - }); - } - - private static void show( - DataStoreEntry initialStore, - BiFunction> applicable) { - var comp = new ScanDialog(initialStore != null ? initialStore.ref() : null, applicable); - var modal = ModalOverlay.of("scanAlertTitle", comp); - modal.addButton(ModalButton.ok(() -> { - comp.finish(); - })); - modal.showAndWait(); - } -} diff --git a/app/src/main/java/io/xpipe/app/util/ScanDialog.java b/app/src/main/java/io/xpipe/app/util/ScanDialog.java index 227374fc6..e2b3ce02d 100644 --- a/app/src/main/java/io/xpipe/app/util/ScanDialog.java +++ b/app/src/main/java/io/xpipe/app/util/ScanDialog.java @@ -1,167 +1,82 @@ package io.xpipe.app.util; -import io.xpipe.app.comp.Comp; -import io.xpipe.app.comp.base.ListSelectorComp; -import io.xpipe.app.comp.base.ModalOverlayContentComp; -import io.xpipe.app.comp.store.StoreChoiceComp; -import io.xpipe.app.comp.store.StoreViewState; -import io.xpipe.app.core.AppI18n; +import io.xpipe.app.comp.base.ModalButton; +import io.xpipe.app.comp.base.ModalOverlay; import io.xpipe.app.ext.ScanProvider; import io.xpipe.app.ext.ShellStore; 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 javafx.application.Platform; -import javafx.beans.property.*; -import javafx.beans.value.ObservableValue; -import javafx.collections.FXCollections; -import javafx.scene.layout.Region; -import javafx.scene.layout.StackPane; -import javafx.scene.layout.VBox; +import io.xpipe.core.process.ShellTtyState; +import io.xpipe.core.process.SystemState; +import javafx.collections.ObservableList; 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 ScanDialog { -class ScanDialog extends ModalOverlayContentComp { - - private final DataStoreEntryRef initialStore; - private final BiFunction> applicable; - private final ObjectProperty> entry; - private final ListProperty selected = - new SimpleListProperty<>(FXCollections.observableArrayList()); - private final BooleanProperty busy = new SimpleBooleanProperty(); - - ScanDialog( - DataStoreEntryRef entry, - BiFunction> applicable) { - this.initialStore = entry; - this.entry = new SimpleObjectProperty<>(entry); - this.applicable = applicable; - } - - @Override - protected ObservableValue busy() { - return busy; - } - - protected void finish() { - ThreadHelper.runFailableAsync(() -> { - if (entry.get() == null) { - return; + public static void showAsync(DataStoreEntry entry) { + ThreadHelper.runAsync(() -> { + var showForCon = entry == null + || (entry.getStore() instanceof ShellStore + && (!(entry.getStorePersistentState() instanceof SystemState systemState) + || systemState.getTtyState() == null + || systemState.getTtyState() == ShellTtyState.NONE)); + if (showForCon) { + showForShellStore(entry); } + }); + } - Platform.runLater(() -> { - var modal = getModalOverlay(); - modal.close(); - }); + public static void showForShellStore(DataStoreEntry initial) { + var action = new ScanDialogAction() { - 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; - } + @Override + public boolean scan(ObservableList all, ObservableList selected, DataStoreEntry entry, ShellControl sc) { + if (!sc.canHaveSubshells()) { + return false; + } - // Previous scan operation could have exited the shell - var sc = entry.get().getStore().getOrStartSession(); + if (!sc.getShellDialect().getDumbMode().supportsAnyPossibleInteraction()) { + return false; + } + if (sc.getTtyState() != ShellTtyState.NONE) { + return false; + } + + var providers = ScanProvider.getAll(); + for (ScanProvider scanProvider : providers) { try { - a.getProvider().scan(entry.get().get(), sc); - } catch (Throwable ex) { + // Previous scan operation could have exited the shell + sc.start(); + ScanProvider.ScanOpportunity operation = scanProvider.create(entry, sc); + if (operation != null) { + if (!operation.isDisabled() && operation.isDefaultSelected()) { + selected.add(operation); + } + all.add(operation); + } + } catch (Exception ex) { ErrorEvent.fromThrowable(ex).handle(); } } - }); - }); + return true; + } + }; + show(initial, action); } - private void onUpdate(DataStoreEntryRef newValue, StackPane stackPane) { - selected.clear(); - stackPane.getChildren().clear(); - - if (newValue == null) { - return; - } - - ThreadHelper.runFailableAsync(() -> { - BooleanScope.executeExclusive(busy, () -> { - var sc = entry.get().getStore().getOrStartSession(); - var a = applicable.apply(entry.get().get(), sc); - Platform.runLater(() -> { - if (a == null) { - var modal = getModalOverlay(); - if (modal != null) { - modal.close(); - } - return; - } - - selected.setAll(a.stream() - .filter(scanOperation -> scanOperation.isDefaultSelected() && !scanOperation.isDisabled()) - .toList()); - Function nameFunc = (ScanProvider.ScanOpportunity 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); - }); - }); - }); - } - - @Override - protected Region createSimple() { - StackPane stackPane = new StackPane(); - stackPane.getStyleClass().add("scan-list"); - - var b = new OptionsBuilder() - .name("scanAlertChoiceHeader") - .description("scanAlertChoiceHeaderDescription") - .addComp(new StoreChoiceComp<>( - StoreChoiceComp.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); - }); - - entry.subscribe(newValue -> { - onUpdate(newValue, stackPane); - }); - - return b.createRegion(); + private static void show( + DataStoreEntry initialStore, + ScanDialogAction action) { + var comp = new ScanDialogComp(initialStore != null ? initialStore.ref() : null, action); + var modal = ModalOverlay.of("scanAlertTitle", comp); + modal.addButton(ModalButton.ok(() -> { + comp.finish(); + })); + modal.showAndWait(); } } diff --git a/app/src/main/java/io/xpipe/app/util/ScanDialogAction.java b/app/src/main/java/io/xpipe/app/util/ScanDialogAction.java new file mode 100644 index 000000000..e90a28da5 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/util/ScanDialogAction.java @@ -0,0 +1,11 @@ +package io.xpipe.app.util; + +import io.xpipe.app.ext.ScanProvider; +import io.xpipe.app.storage.DataStoreEntry; +import io.xpipe.core.process.ShellControl; +import javafx.collections.ObservableList; + +public interface ScanDialogAction { + + boolean scan(ObservableList all, ObservableList selected, DataStoreEntry entry, ShellControl shellControl); +} diff --git a/app/src/main/java/io/xpipe/app/util/ScanDialogComp.java b/app/src/main/java/io/xpipe/app/util/ScanDialogComp.java new file mode 100644 index 000000000..bf02eaf08 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/util/ScanDialogComp.java @@ -0,0 +1,172 @@ +package io.xpipe.app.util; + +import io.xpipe.app.comp.Comp; +import io.xpipe.app.comp.base.ListSelectorComp; +import io.xpipe.app.comp.base.ModalOverlayContentComp; +import io.xpipe.app.comp.store.StoreChoiceComp; +import io.xpipe.app.comp.store.StoreViewState; +import io.xpipe.app.core.AppI18n; +import io.xpipe.app.ext.ScanProvider; +import io.xpipe.app.ext.ShellStore; +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 javafx.application.Platform; +import javafx.beans.property.*; +import javafx.beans.value.ObservableValue; +import javafx.collections.FXCollections; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; + +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 ScanDialogComp extends ModalOverlayContentComp { + + private final DataStoreEntryRef initialStore; + private final ScanDialogAction action; + private final ObjectProperty> entry; + private final ListProperty available = + new SimpleListProperty<>(FXCollections.observableArrayList()); + private final ListProperty selected = + new SimpleListProperty<>(FXCollections.observableArrayList()); + private final BooleanProperty busy = new SimpleBooleanProperty(); + + ScanDialogComp( + DataStoreEntryRef entry, + ScanDialogAction action) { + this.initialStore = entry; + this.entry = new SimpleObjectProperty<>(entry); + this.action = action; + } + + @Override + protected ObservableValue busy() { + return busy; + } + + protected void finish() { + ThreadHelper.runFailableAsync(() -> { + if (entry.get() == null) { + return; + } + + Platform.runLater(() -> { + var modal = getModalOverlay(); + modal.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 + var sc = entry.get().getStore().getOrStartSession(); + + try { + a.getProvider().scan(entry.get().get(), sc); + } catch (Throwable ex) { + ErrorEvent.fromThrowable(ex).handle(); + } + } + }); + }); + } + + private void onUpdate(DataStoreEntryRef newValue) { + available.clear(); + selected.clear(); + + if (newValue == null) { + return; + } + + ThreadHelper.runFailableAsync(() -> { + BooleanScope.executeExclusive(busy, () -> { + boolean r; + try { + var sc = entry.get().getStore().getOrStartSession(); + r = action.scan(available, selected, newValue.get(), sc); + } catch (Throwable t) { + var modal = getModalOverlay(); + if (initialStore != null && modal != null) { + modal.close(); + } + throw t; + } + if (!r) { + var modal = getModalOverlay(); + if (initialStore != null && modal != null) { + modal.close(); + } + } + }); + }); + } + + @Override + protected Region createSimple() { + StackPane stackPane = new StackPane(); + stackPane.getStyleClass().add("scan-list"); + + var b = new OptionsBuilder() + .name("scanAlertChoiceHeader") + .description("scanAlertChoiceHeaderDescription") + .addComp(new StoreChoiceComp<>( + StoreChoiceComp.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); + }); + + Function nameFunc = (ScanProvider.ScanOpportunity 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<>( + available, nameFunc, selected, scanOperation -> scanOperation.isDisabled(), () -> available.size() > 3) + .createRegion(); + stackPane.getChildren().add(r); + + entry.subscribe(newValue -> { + onUpdate(newValue); + }); + + return b.createRegion(); + } +} diff --git a/core/src/main/java/io/xpipe/core/process/ShellView.java b/core/src/main/java/io/xpipe/core/process/ShellView.java index 2dfca9eca..1a6a89f45 100644 --- a/core/src/main/java/io/xpipe/core/process/ShellView.java +++ b/core/src/main/java/io/xpipe/core/process/ShellView.java @@ -3,6 +3,7 @@ package io.xpipe.core.process; import io.xpipe.core.store.FilePath; import java.io.InputStream; +import java.util.Optional; public class ShellView { @@ -45,4 +46,38 @@ public class ShellView { public String user() throws Exception { return getDialect().printUsernameCommand(shellControl).readStdoutOrThrow(); } + + + public String getPath() throws Exception { + var path = shellControl.command(shellControl.getShellDialect().getPrintEnvironmentVariableCommand("PATH")) + .readStdoutOrThrow(); + return path; + } + + public String getLibraryPath() throws Exception { + var path = shellControl.command(shellControl.getShellDialect().getPrintEnvironmentVariableCommand("LD_LIBRARY_PATH")) + .readStdoutOrThrow(); + return path; + } + + public boolean isRoot() throws Exception { + if (shellControl.getOsType() == OsType.WINDOWS) { + return false; + } + + var isRoot = shellControl.executeSimpleBooleanCommand("test \"${EUID:-$(id -u)}\" -eq 0"); + return isRoot; + } + + public Optional findProgram(String name) throws Exception { + var out = shellControl + .command(shellControl.getShellDialect().getWhichCommand(name)) + .readStdoutIfPossible(); + return out.flatMap(s -> s.lines().findFirst()).map(String::trim); + } + + public boolean isInPath(String executable) throws Exception { + return shellControl.executeSimpleBooleanCommand( + shellControl.getShellDialect().getWhichCommand(executable)); + } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/action/ScanStoreAction.java b/ext/base/src/main/java/io/xpipe/ext/base/action/ScanStoreAction.java index 9ccd2ff07..0de324938 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/action/ScanStoreAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/action/ScanStoreAction.java @@ -5,7 +5,7 @@ import io.xpipe.app.ext.ActionProvider; import io.xpipe.app.ext.ShellStore; import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.storage.DataStoreEntryRef; -import io.xpipe.app.util.ScanAlert; +import io.xpipe.app.util.ScanDialog; import io.xpipe.core.process.ShellTtyState; import io.xpipe.core.process.SystemState; @@ -69,7 +69,7 @@ public class ScanStoreAction implements ActionProvider { @Override public void execute() { if (entry == null || entry.getStore() instanceof ShellStore) { - ScanAlert.showForShellStore(entry); + ScanDialog.showForShellStore(entry); } } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/identity/SshIdentityStrategy.java b/ext/base/src/main/java/io/xpipe/ext/base/identity/SshIdentityStrategy.java index bfc4a2d13..5f025db72 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/identity/SshIdentityStrategy.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/identity/SshIdentityStrategy.java @@ -301,10 +301,10 @@ public interface SshIdentityStrategy { var file = getFile(sc); var dir = FileNames.getParent(file); if (sc.getOsType() == OsType.WINDOWS) { - var path = CommandSupport.getPath(sc); + var path = sc.view().getPath(); builder.fixedEnvrironment("PATH", dir + ";" + path); } else { - var path = CommandSupport.getLibraryPath(sc); + var path = sc.view().getLibraryPath(); builder.fixedEnvrironment("LD_LIBRARY_PATH", dir + ":" + path); } }) @@ -348,10 +348,10 @@ public interface SshIdentityStrategy { var file = getFile(); var dir = FileNames.getParent(file); if (sc.getOsType() == OsType.WINDOWS) { - var path = CommandSupport.getPath(sc); + var path = sc.view().getPath(); builder.fixedEnvrironment("PATH", dir + ";" + path); } else { - var path = CommandSupport.getLibraryPath(sc); + var path = sc.view().getLibraryPath(); builder.fixedEnvrironment("LD_LIBRARY_PATH", dir + ":" + path); } }); diff --git a/ext/base/src/main/java/io/xpipe/ext/base/script/SimpleScriptStoreProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/script/SimpleScriptStoreProvider.java index 062b3c550..0ad0da1c1 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/script/SimpleScriptStoreProvider.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/script/SimpleScriptStoreProvider.java @@ -146,7 +146,7 @@ public class SimpleScriptStoreProvider implements EnabledParentStoreProvider, Da } }; var selectedExecTypes = new SimpleListProperty<>(FXCollections.observableList(selectedStart)); - var selectorComp = new ListSelectorComp<>(vals, name, selectedExecTypes, v -> false, false); + var selectorComp = new ListSelectorComp<>(FXCollections.observableList(vals), name, selectedExecTypes, v -> false, () -> false); return new OptionsBuilder() .name("snippets") diff --git a/gradle/gradle_scripts/atlantafx-base-2.0.2.jar b/gradle/gradle_scripts/atlantafx-base-2.0.2.jar index 93d4a9213..27ad4d895 100644 Binary files a/gradle/gradle_scripts/atlantafx-base-2.0.2.jar and b/gradle/gradle_scripts/atlantafx-base-2.0.2.jar differ