diff --git a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreScanBarComp.java b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreScanBarComp.java new file mode 100644 index 00000000..aa308c23 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreScanBarComp.java @@ -0,0 +1,38 @@ +package io.xpipe.app.comp.storage.store; + +import atlantafx.base.theme.Styles; +import io.xpipe.app.comp.base.ButtonComp; +import io.xpipe.app.core.AppFont; +import io.xpipe.app.core.AppI18n; +import io.xpipe.app.fxcomps.SimpleComp; +import io.xpipe.app.fxcomps.impl.FancyTooltipAugment; +import io.xpipe.app.fxcomps.impl.VerticalComp; +import io.xpipe.app.util.ScanAlert; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyCodeCombination; +import javafx.scene.input.KeyCombination; +import javafx.scene.layout.Region; +import org.kordamp.ikonli.javafx.FontIcon; + +import java.util.List; + +public class StoreScanBarComp extends SimpleComp { + + @Override + protected Region createSimple() { + var newTunnelStore = new ButtonComp(AppI18n.observable("addAutomatically"), new FontIcon("mdi2e-eye-plus-outline"), () -> { + ScanAlert.showAsync(null); + }) + .styleClass(Styles.FLAT) + .shortcut(new KeyCodeCombination(KeyCode.A, KeyCombination.SHORTCUT_DOWN)) + .apply(new FancyTooltipAugment<>("addAutomatically")); + + var box = new VerticalComp(List.of(newTunnelStore)) + .apply(struc -> struc.get().setFillWidth(true)); + box.apply(s -> AppFont.medium(s.get())); + var bar = box.createRegion(); + bar.getStyleClass().add("bar"); + bar.getStyleClass().add("store-creation-bar"); + return bar; + } +} diff --git a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreSidebarComp.java b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreSidebarComp.java index 9a2fdabd..47e35c39 100644 --- a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreSidebarComp.java +++ b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreSidebarComp.java @@ -15,10 +15,11 @@ public class StoreSidebarComp extends SimpleComp { protected Region createSimple() { var sideBar = new VerticalComp(List.of( new StoreEntryListHeaderComp(), + new StoreScanBarComp(), new StoreCreationBarComp(), new StoreOrganizationComp(), Comp.of(() -> new Region()).styleClass("bar").styleClass("filler-bar"))); - sideBar.apply(s -> VBox.setVgrow(s.get().getChildren().get(3), Priority.ALWAYS)); + sideBar.apply(s -> VBox.setVgrow(s.get().getChildren().get(4), Priority.ALWAYS)); sideBar.styleClass("sidebar"); return sideBar.createRegion(); } diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/DataStoreChoiceComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/DataStoreChoiceComp.java index bc8c2cbb..34c74928 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/DataStoreChoiceComp.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/DataStoreChoiceComp.java @@ -16,6 +16,7 @@ import javafx.scene.control.Label; import javafx.scene.layout.Region; import lombok.AllArgsConstructor; +import java.util.Locale; import java.util.Optional; import java.util.function.Predicate; @@ -91,7 +92,7 @@ public class DataStoreChoiceComp extends SimpleComp { @SuppressWarnings("unchecked") protected Region createSimple() { var list = StoreEntryFlatMiniSectionComp.ALL; - var comboBox = new CustomComboBoxBuilder<>( + var comboBox = new CustomComboBoxBuilder( selected, t -> list.stream() .filter(e -> t.equals(e.getEntry().getStore())) @@ -100,6 +101,14 @@ public class DataStoreChoiceComp extends SimpleComp { .createRegion(), new Label(AppI18n.get("none")), n -> true); + + if (list.size() > 5) { + comboBox.addFilter((t, s) -> { + var entry = DataStorage.get().getStoreDisplayName(t).orElse("?"); + return entry.toLowerCase(Locale.ROOT).contains(s.toLowerCase(Locale.ROOT)); + }); + } + comboBox.setAccessibleNames(t -> toName(t)); comboBox.setSelectedDisplay(t -> createGraphic(t)); comboBox.setUnknownNode(t -> createGraphic(t)); diff --git a/app/src/main/java/io/xpipe/app/util/CustomComboBoxBuilder.java b/app/src/main/java/io/xpipe/app/util/CustomComboBoxBuilder.java index 89c08328..03087b9e 100644 --- a/app/src/main/java/io/xpipe/app/util/CustomComboBoxBuilder.java +++ b/app/src/main/java/io/xpipe/app/util/CustomComboBoxBuilder.java @@ -15,6 +15,7 @@ import javafx.scene.control.ComboBox; import javafx.scene.control.Label; import javafx.scene.control.ListCell; import javafx.scene.control.Separator; +import javafx.scene.input.MouseEvent; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; @@ -223,6 +224,14 @@ public class CustomComboBoxBuilder { private class Cell extends ListCell { + public Cell() { + addEventFilter(MouseEvent.MOUSE_PRESSED, event -> { + if (!nodeMap.containsKey(getItem())) { + event.consume(); + } + }); + } + @Override protected void updateItem(Node item, boolean empty) { setGraphic(item); 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 8f6e28ba..0b6441ed 100644 --- a/app/src/main/java/io/xpipe/app/util/ScanAlert.java +++ b/app/src/main/java/io/xpipe/app/util/ScanAlert.java @@ -6,16 +6,20 @@ import io.xpipe.app.core.AppI18n; import io.xpipe.app.core.AppWindowHelper; import io.xpipe.app.ext.ScanProvider; import io.xpipe.app.fxcomps.Comp; +import io.xpipe.app.fxcomps.impl.DataStoreChoiceComp; import io.xpipe.app.fxcomps.impl.LabelComp; import io.xpipe.app.fxcomps.impl.VerticalComp; import io.xpipe.app.issue.ErrorEvent; +import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.core.store.ShellStore; import javafx.application.Platform; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleListProperty; +import javafx.beans.property.SimpleObjectProperty; import javafx.collections.FXCollections; import javafx.event.ActionEvent; +import javafx.geometry.Insets; import javafx.scene.control.Alert; import javafx.scene.control.Button; import javafx.scene.control.ButtonType; @@ -25,32 +29,20 @@ import javafx.scene.layout.VBox; import java.util.ArrayList; import java.util.List; -import java.util.function.Supplier; +import java.util.function.Function; public class ScanAlert { public static void showAsync(DataStoreEntry entry) { ThreadHelper.runAsync(() -> { - if (entry.getStore() instanceof ShellStore) { + if (entry == null || entry.getStore() instanceof ShellStore) { showForShellStore(entry); - } else { - showForOtherStore(entry); } }); } - private static void showForOtherStore(DataStoreEntry entry) { - show(entry, () -> { - var providers = ScanProvider.getAll(); - return providers.stream() - .map(scanProvider -> scanProvider.create(entry.getStore())) - .filter(scanOperation -> scanOperation != null) - .toList(); - }); - } - - private static void showForShellStore(DataStoreEntry entry) { - show(entry, () -> { + private static void showForShellStore(DataStoreEntry initial) { + show(initial != null ? initial.getStore().asNeeded() : null, (DataStoreEntry entry) -> { try (var sc = ((ShellStore) entry.getStore()).control().start()) { var providers = ScanProvider.getAll(); var applicable = new ArrayList(); @@ -68,8 +60,10 @@ public class ScanAlert { }); } - private static void show(DataStoreEntry entry, Supplier> applicable) { + private static void show( + ShellStore initialStore, Function> applicable) { var busy = new SimpleBooleanProperty(); + var store = new SimpleObjectProperty(); var selected = new SimpleListProperty(FXCollections.observableArrayList()); AppWindowHelper.showAlert( alert -> { @@ -78,23 +72,40 @@ public class ScanAlert { alert.getButtonTypes().add(ButtonType.OK); var content = new LoadingOverlayComp( new VerticalComp(List.>of( - new LabelComp(AppI18n.get("scanAlertHeader")) + new LabelComp(AppI18n.get("scanAlertChoiceHeader")) .apply(struc -> struc.get().setWrapText(true)), + new DataStoreChoiceComp<>( + DataStoreChoiceComp.Mode.OTHER, + null, + store, + ShellStore.class, + store1 -> true) + .disable(new SimpleBooleanProperty(initialStore != null)), + new LabelComp(AppI18n.get("scanAlertHeader")) + .apply(struc -> + struc.get().setWrapText(true)) + .padding(new Insets(20, 0, 0, 0)), Comp.of(() -> new Region()))) .apply(struc -> struc.get().setSpacing(15)) .styleClass("window-content"), busy) .createRegion(); content.setPrefWidth(500); - content.setPrefHeight(450); + content.setPrefHeight(550); alert.getDialogPane().setContent(content); // Custom behavior for ok button var btOk = (Button) alert.getDialogPane().lookupButton(ButtonType.OK); btOk.addEventFilter(ActionEvent.ACTION, event -> { + if (store.get() == null) { + event.consume(); + return; + } + ThreadHelper.runAsync(() -> { BusyProperty.execute(busy, () -> { + var entry = DataStorage.get().getStoreEntry(store.get()); entry.setExpanded(true); for (var a : selected) { @@ -113,11 +124,21 @@ public class ScanAlert { }); }); - // Asynchronous loading of content - alert.setOnShown(event -> { + store.addListener((observable, oldValue, newValue) -> { + if (newValue == null) { + selected.clear(); + ((VBox) ((StackPane) alert.getDialogPane().getContent()) + .getChildren() + .get(0)) + .getChildren() + .set(3, new Region()); + return; + } + ThreadHelper.runAsync(() -> { BusyProperty.execute(busy, () -> { - var a = applicable.get(); + var entry = DataStorage.get().getStoreEntry(newValue); + var a = applicable.apply(entry); Platform.runLater(() -> { if (a == null) { @@ -139,11 +160,13 @@ public class ScanAlert { .getChildren() .get(0)) .getChildren() - .set(1, r); + .set(3, r); }); }); }); }); + + store.set(initialStore); }, busy, null); diff --git a/app/src/main/resources/io/xpipe/app/resources/lang/dscreation_en.properties b/app/src/main/resources/io/xpipe/app/resources/lang/dscreation_en.properties index 066226ef..53671f64 100644 --- a/app/src/main/resources/io/xpipe/app/resources/lang/dscreation_en.properties +++ b/app/src/main/resources/io/xpipe/app/resources/lang/dscreation_en.properties @@ -46,6 +46,7 @@ addTunnel=Add Tunnel ... addHost=Add Remote Host ... addShell=Add Environment ... addCommand=Add Command ... +addAutomatically=Add Automatically ... addOther=Add Other ... addStreamTitle=Add Stream Store addConnection=Add Connection diff --git a/app/src/main/resources/io/xpipe/app/resources/lang/preferences_en.properties b/app/src/main/resources/io/xpipe/app/resources/lang/preferences_en.properties index 5703d91d..04830884 100644 --- a/app/src/main/resources/io/xpipe/app/resources/lang/preferences_en.properties +++ b/app/src/main/resources/io/xpipe/app/resources/lang/preferences_en.properties @@ -64,7 +64,7 @@ logLevel=Log level appBehaviour=Application behaviour logLevelDescription=The log level that should be used when writing log files. developerMode=Developer mode -developerModeDescription=When enabled, you will have access to a variety of additional options that are useful for development. +developerModeDescription=When enabled, you will have access to a variety of additional options that are useful for development. Only active after a restart. editor=Editor custom=Custom passwordManagerCommand=Password manager command diff --git a/app/src/main/resources/io/xpipe/app/resources/lang/translations_en.properties b/app/src/main/resources/io/xpipe/app/resources/lang/translations_en.properties index dbb517b6..8a97cc63 100644 --- a/app/src/main/resources/io/xpipe/app/resources/lang/translations_en.properties +++ b/app/src/main/resources/io/xpipe/app/resources/lang/translations_en.properties @@ -55,6 +55,7 @@ connectionNameDescription=Give this connection a custom name openFileTitle=Open file unknown=Unknown scanAlertTitle=Add connections +scanAlertChoiceHeader=Choose from where to add connections: scanAlertHeader=Select types of connections you want to automatically add for the host system: namedHostFeatureUnsupported=$HOST$ does not support this feature namedHostNotActive=$HOST$ is not active