From fee3c8ca34f736caf26b1a64ac78faa3e1a1cf15 Mon Sep 17 00:00:00 2001 From: crschnick Date: Wed, 19 Nov 2025 20:59:10 +0000 Subject: [PATCH] Improve filter shortcuts and sftp handling --- .../BrowserFileChooserSessionComp.java | 12 ++++++++- .../app/browser/BrowserFullSessionComp.java | 18 ++++++++++--- .../file/BrowserConnectionListFilterComp.java | 5 ++++ .../file/BrowserFileTransferOperation.java | 6 +++++ .../browser/file/BrowserStatusBarComp.java | 11 ++++++++ .../io/xpipe/app/comp/base/AppLayoutComp.java | 1 + .../xpipe/app/comp/base/DelayedInitComp.java | 5 ++++ .../io/xpipe/app/comp/base/FilterComp.java | 1 - .../xpipe/app/comp/base/MultiContentComp.java | 18 +++++++++++++ .../xpipe/app/ext/ConnectionFileSystem.java | 5 ++++ .../java/io/xpipe/app/ext/FileSystem.java | 2 ++ .../io/xpipe/app/ext/WrapperFileSystem.java | 5 ++++ .../hub/comp/StoreEntryListOverviewComp.java | 13 ++++++++++ .../xpipe/app/hub/comp/StoreLayoutComp.java | 14 +++++++++- .../xpipe/app/hub/comp/StoreSidebarComp.java | 7 ++++- .../xpipe/app/util/ObservableSubscriber.java | 26 +++++++++++++++++++ 16 files changed, 142 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/io/xpipe/app/util/ObservableSubscriber.java 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 d33950d62..cc9cd4904 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserFileChooserSessionComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserFileChooserSessionComp.java @@ -14,10 +14,12 @@ import io.xpipe.app.ext.ShellStore; import io.xpipe.app.hub.comp.StoreEntryWrapper; import io.xpipe.app.hub.comp.StoreViewState; import io.xpipe.app.platform.BindingsHelper; +import io.xpipe.app.platform.InputHelper; import io.xpipe.app.platform.PlatformThread; import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.storage.DataStoreEntryRef; import io.xpipe.app.util.FileReference; +import io.xpipe.app.util.ObservableSubscriber; import io.xpipe.app.util.ThreadHelper; import io.xpipe.core.FilePath; @@ -25,6 +27,9 @@ import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.collections.ListChangeListener; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyCodeCombination; +import javafx.scene.input.KeyCombination; import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; import javafx.scene.shape.Rectangle; @@ -116,7 +121,8 @@ public class BrowserFileChooserSessionComp extends ModalOverlayContentComp { var category = new SimpleObjectProperty<>( StoreViewState.get().getActiveCategory().getValue()); var filter = new SimpleStringProperty(); - var bookmarkTopBar = new BrowserConnectionListFilterComp(category, filter); + var filterTrigger = new ObservableSubscriber(); + var bookmarkTopBar = new BrowserConnectionListFilterComp(filterTrigger, category, filter); var bookmarksList = new BrowserConnectionListComp( BindingsHelper.map( model.getSelectedEntry(), v -> v != null ? v.getEntry().get() : null), @@ -147,6 +153,10 @@ public class BrowserFileChooserSessionComp extends ModalOverlayContentComp { } }); }); + InputHelper.onKeyCombination(s, new KeyCodeCombination(KeyCode.F, KeyCombination.SHORTCUT_DOWN), false, keyEvent -> { + filterTrigger.trigger(); + keyEvent.consume(); + }); return s; }); diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserFullSessionComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserFullSessionComp.java index ceaa21c53..8f82cfc06 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserFullSessionComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserFullSessionComp.java @@ -13,7 +13,9 @@ import io.xpipe.app.ext.ShellStore; import io.xpipe.app.hub.comp.StoreEntryWrapper; import io.xpipe.app.hub.comp.StoreViewState; import io.xpipe.app.platform.BindingsHelper; +import io.xpipe.app.platform.InputHelper; import io.xpipe.app.platform.PlatformThread; +import io.xpipe.app.util.ObservableSubscriber; import io.xpipe.app.util.ThreadHelper; import javafx.application.Platform; @@ -23,6 +25,9 @@ import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.geometry.Insets; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyCodeCombination; +import javafx.scene.input.KeyCombination; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.Region; import javafx.scene.shape.Rectangle; @@ -42,7 +47,8 @@ public class BrowserFullSessionComp extends SimpleComp { @Override protected Region createSimple() { - var left = Comp.of(() -> createLeftSide()); + var filterTrigger = new ObservableSubscriber(); + var left = Comp.of(() -> createLeftSide(filterTrigger)); var leftSplit = new SimpleDoubleProperty(); var rightSplit = new SimpleDoubleProperty(); @@ -103,12 +109,18 @@ public class BrowserFullSessionComp extends SimpleComp { } }); }); + splitPane.apply(struc -> { + InputHelper.onKeyCombination(struc.get(), new KeyCodeCombination(KeyCode.F, KeyCombination.SHORTCUT_DOWN), false, keyEvent -> { + filterTrigger.trigger(); + keyEvent.consume(); + }); + }); splitPane.styleClass("browser"); var r = splitPane.createRegion(); return r; } - private Region createLeftSide() { + private Region createLeftSide(ObservableSubscriber filterTrigger) { Predicate applicable = storeEntryWrapper -> { if (!storeEntryWrapper.getEntry().getValidity().isUsable()) { return false; @@ -138,7 +150,7 @@ public class BrowserFullSessionComp extends SimpleComp { var category = new SimpleObjectProperty<>( StoreViewState.get().getActiveCategory().getValue()); var filter = new SimpleStringProperty(); - var bookmarkTopBar = new BrowserConnectionListFilterComp(category, filter); + var bookmarkTopBar = new BrowserConnectionListFilterComp(filterTrigger, category, filter); var bookmarksList = new BrowserConnectionListComp( BindingsHelper.map( model.getSelectedEntry(), diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserConnectionListFilterComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserConnectionListFilterComp.java index d8505ea68..e164cfde5 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserConnectionListFilterComp.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserConnectionListFilterComp.java @@ -8,6 +8,7 @@ import io.xpipe.app.hub.comp.DataStoreCategoryChoiceComp; import io.xpipe.app.hub.comp.StoreCategoryWrapper; import io.xpipe.app.hub.comp.StoreViewState; +import io.xpipe.app.util.ObservableSubscriber; import javafx.beans.property.Property; import javafx.scene.layout.Region; @@ -21,6 +22,7 @@ import java.util.List; @AllArgsConstructor public final class BrowserConnectionListFilterComp extends SimpleComp { + private final ObservableSubscriber filterTrigger; private final Property category; private final Property filter; @@ -40,6 +42,9 @@ public final class BrowserConnectionListFilterComp extends SimpleComp { .hgrow() .apply(struc -> { AppFontSizes.base(struc.get()); + filterTrigger.subscribe(() -> { + struc.get().requestFocus(); + }); }); var top = new HorizontalComp(List.of(category, filter)) diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileTransferOperation.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileTransferOperation.java index 5379ea474..d54592b6b 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileTransferOperation.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileTransferOperation.java @@ -383,6 +383,12 @@ public class BrowserFileTransferOperation { var fileSize = sourceFs.getFileSize(sourceFile); + updateProgress(new BrowserTransferProgress(sourceFile.getFileName(), 0, 0)); + if (targetFs.writeInstantIfPossible(sourceFs, sourceFile, targetFile)) { + updateProgress(BrowserTransferProgress.finished(sourceFile.getFileName(), fileSize)); + return; + } + InputStream inputStream = null; OutputStream outputStream = null; try { diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserStatusBarComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserStatusBarComp.java index 58d9781f9..f21d82e6c 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserStatusBarComp.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserStatusBarComp.java @@ -100,6 +100,7 @@ public class BrowserStatusBarComp extends SimpleComp { return null; } + // Handle unknown transfers if (p.getTotal() == 0) { return HumanReadableFormat.byteCount(p.getTransferred()); } @@ -132,6 +133,16 @@ public class BrowserStatusBarComp extends SimpleComp { return null; } else { var transferred = HumanReadableFormat.progressByteCount(p.getTransferred()); + + // Handle unknown transfers + if (p.getTotal() == 0) { + if (p.getTransferred() == 0) { + return "..."; + } else { + return transferred; + } + } + var all = HumanReadableFormat.byteCount(p.getTotal()); return transferred + " / " + all; } diff --git a/app/src/main/java/io/xpipe/app/comp/base/AppLayoutComp.java b/app/src/main/java/io/xpipe/app/comp/base/AppLayoutComp.java index f7e162dab..d015eb750 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/AppLayoutComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/AppLayoutComp.java @@ -90,6 +90,7 @@ public class AppLayoutComp extends Comp { } PlatformThread.runNestedLoopIteration(); sidebar.setDisable(false); + stack.requestFocus(); } @Override diff --git a/app/src/main/java/io/xpipe/app/comp/base/DelayedInitComp.java b/app/src/main/java/io/xpipe/app/comp/base/DelayedInitComp.java index c170a8369..9a1b77ce1 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/DelayedInitComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/DelayedInitComp.java @@ -33,6 +33,11 @@ public class DelayedInitComp extends SimpleComp { }); return true; }); + stack.focusedProperty().addListener((observable, oldValue, newValue) -> { + if (newValue && !stack.getChildren().isEmpty()) { + stack.getChildren().getFirst().requestFocus(); + } + }); return stack; } } diff --git a/app/src/main/java/io/xpipe/app/comp/base/FilterComp.java b/app/src/main/java/io/xpipe/app/comp/base/FilterComp.java index 06368ecb5..26ace5fbf 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/FilterComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/FilterComp.java @@ -60,7 +60,6 @@ public class FilterComp extends Comp> { filter.addEventFilter(KeyEvent.KEY_PRESSED, event -> { if (new KeyCodeCombination(KeyCode.ESCAPE).match(event)) { filter.clear(); - filter.getScene().getRoot().requestFocus(); event.consume(); } }); diff --git a/app/src/main/java/io/xpipe/app/comp/base/MultiContentComp.java b/app/src/main/java/io/xpipe/app/comp/base/MultiContentComp.java index 5dc57e799..d138da7d3 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/MultiContentComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/MultiContentComp.java @@ -5,6 +5,7 @@ import io.xpipe.app.comp.SimpleComp; import io.xpipe.app.issue.TrackEvent; import io.xpipe.app.platform.PlatformThread; +import javafx.application.Platform; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.collections.MapChangeListener; @@ -36,6 +37,18 @@ public class MultiContentComp extends SimpleComp { } }); + stack.focusedProperty().addListener((observable, oldValue, newValue) -> { + if (newValue) { + var selected = content.entrySet().stream() + .filter(e -> e.getValue().getValue()) + .map(e -> m.get(e.getKey())) + .findFirst(); + if (selected.isPresent()) { + selected.get().requestFocus(); + } + } + }); + for (Map.Entry, ObservableValue> e : content.entrySet()) { var name = e.getKey().getClass().getSimpleName(); if (log) { @@ -49,6 +62,11 @@ public class MultiContentComp extends SimpleComp { PlatformThread.runLaterIfNeeded(() -> { r.setManaged(val); r.setVisible(val); + if (val) { + Platform.runLater(() -> { + r.requestFocus(); + }); + } }); }); m.put(e.getKey(), r); diff --git a/app/src/main/java/io/xpipe/app/ext/ConnectionFileSystem.java b/app/src/main/java/io/xpipe/app/ext/ConnectionFileSystem.java index 8d8ca7639..61ce2ec72 100644 --- a/app/src/main/java/io/xpipe/app/ext/ConnectionFileSystem.java +++ b/app/src/main/java/io/xpipe/app/ext/ConnectionFileSystem.java @@ -33,6 +33,11 @@ public class ConnectionFileSystem implements FileSystem { this.shellControl = shellControl; } + @Override + public boolean writeInstantIfPossible(FileSystem sourceFs, FilePath sourceFile, FilePath targetFile) { + return false; + } + @Override public String getSuffix() { return null; diff --git a/app/src/main/java/io/xpipe/app/ext/FileSystem.java b/app/src/main/java/io/xpipe/app/ext/FileSystem.java index e0630b24e..6df59f010 100644 --- a/app/src/main/java/io/xpipe/app/ext/FileSystem.java +++ b/app/src/main/java/io/xpipe/app/ext/FileSystem.java @@ -14,6 +14,8 @@ import java.util.stream.Stream; public interface FileSystem extends Closeable, AutoCloseable { + boolean writeInstantIfPossible(FileSystem sourceFs, FilePath sourceFile, FilePath targetFile) throws Exception; + String getSuffix(); boolean isRunning(); diff --git a/app/src/main/java/io/xpipe/app/ext/WrapperFileSystem.java b/app/src/main/java/io/xpipe/app/ext/WrapperFileSystem.java index 945ecf4c6..caa05b557 100644 --- a/app/src/main/java/io/xpipe/app/ext/WrapperFileSystem.java +++ b/app/src/main/java/io/xpipe/app/ext/WrapperFileSystem.java @@ -24,6 +24,11 @@ public class WrapperFileSystem implements FileSystem { this.runningCheck = runningCheck; } + @Override + public boolean writeInstantIfPossible(FileSystem sourceFs, FilePath sourceFile, FilePath targetFile) throws Exception { + return fs.writeInstantIfPossible(sourceFs, sourceFile, targetFile); + } + @Override public String getSuffix() { return fs.getSuffix(); diff --git a/app/src/main/java/io/xpipe/app/hub/comp/StoreEntryListOverviewComp.java b/app/src/main/java/io/xpipe/app/hub/comp/StoreEntryListOverviewComp.java index 67b43b065..fbb5b39d7 100644 --- a/app/src/main/java/io/xpipe/app/hub/comp/StoreEntryListOverviewComp.java +++ b/app/src/main/java/io/xpipe/app/hub/comp/StoreEntryListOverviewComp.java @@ -8,7 +8,9 @@ import io.xpipe.app.comp.base.IconButtonComp; import io.xpipe.app.core.AppFontSizes; import io.xpipe.app.core.AppI18n; import io.xpipe.app.platform.BindingsHelper; +import io.xpipe.app.platform.InputHelper; import io.xpipe.app.platform.LabelGraphic; +import io.xpipe.app.util.ObservableSubscriber; import io.xpipe.core.OsType; import javafx.beans.binding.Bindings; @@ -19,6 +21,9 @@ import javafx.geometry.Pos; import javafx.scene.control.Label; import javafx.scene.control.MenuButton; import javafx.scene.control.Separator; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyCodeCombination; +import javafx.scene.input.KeyCombination; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; @@ -26,12 +31,17 @@ import javafx.scene.layout.VBox; import javafx.scene.text.TextAlignment; import atlantafx.base.theme.Styles; +import javafx.util.Subscription; import org.kordamp.ikonli.javafx.FontIcon; import java.util.function.Function; public class StoreEntryListOverviewComp extends SimpleComp { + private final ObservableSubscriber filterTrigger; + + public StoreEntryListOverviewComp(ObservableSubscriber filterTrigger) {this.filterTrigger = filterTrigger;} + private Region createGroupListHeader() { var label = new Label(); var name = BindingsHelper.flatMap( @@ -88,6 +98,9 @@ public class StoreEntryListOverviewComp extends SimpleComp { private Region createGroupListFilter() { var filter = new FilterComp(StoreViewState.get().getFilterString()).createRegion(); + filterTrigger.subscribe(() -> { + filter.requestFocus(); + }); var add = createAddButton(); var batchMode = createBatchModeButton().createRegion(); var hbox = new HBox(add, filter, batchMode); diff --git a/app/src/main/java/io/xpipe/app/hub/comp/StoreLayoutComp.java b/app/src/main/java/io/xpipe/app/hub/comp/StoreLayoutComp.java index 59e683ba1..d3312e2c7 100644 --- a/app/src/main/java/io/xpipe/app/hub/comp/StoreLayoutComp.java +++ b/app/src/main/java/io/xpipe/app/hub/comp/StoreLayoutComp.java @@ -7,6 +7,11 @@ import io.xpipe.app.comp.base.LeftSplitPaneComp; import io.xpipe.app.core.AppLayoutModel; import io.xpipe.app.core.window.AppMainWindow; +import io.xpipe.app.platform.InputHelper; +import io.xpipe.app.util.ObservableSubscriber; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyCodeCombination; +import javafx.scene.input.KeyCombination; import javafx.scene.layout.Region; public class StoreLayoutComp extends SimpleComp { @@ -20,7 +25,8 @@ public class StoreLayoutComp extends SimpleComp { } private Region createContent() { - var left = new StoreSidebarComp(); + var filterTrigger = new ObservableSubscriber(); + var left = new StoreSidebarComp(filterTrigger); left.hide(AppMainWindow.get().getStage().widthProperty().lessThan(1000)); left.minWidth(270); left.maxWidth(500); @@ -35,6 +41,12 @@ public class StoreLayoutComp extends SimpleComp { AppLayoutModel.get().getSavedState().setSidebarWidth(aDouble); }); comp.styleClass("store-layout"); + comp.apply(struc -> { + InputHelper.onKeyCombination(struc.get(), new KeyCodeCombination(KeyCode.F, KeyCombination.SHORTCUT_DOWN), false, keyEvent -> { + filterTrigger.trigger(); + keyEvent.consume(); + }); + }); return comp.createRegion(); } } diff --git a/app/src/main/java/io/xpipe/app/hub/comp/StoreSidebarComp.java b/app/src/main/java/io/xpipe/app/hub/comp/StoreSidebarComp.java index df3f624eb..540e9453d 100644 --- a/app/src/main/java/io/xpipe/app/hub/comp/StoreSidebarComp.java +++ b/app/src/main/java/io/xpipe/app/hub/comp/StoreSidebarComp.java @@ -4,16 +4,21 @@ import io.xpipe.app.comp.Comp; import io.xpipe.app.comp.SimpleComp; import io.xpipe.app.comp.base.VerticalComp; +import io.xpipe.app.util.ObservableSubscriber; import javafx.scene.layout.Region; import java.util.List; public class StoreSidebarComp extends SimpleComp { + private final ObservableSubscriber filterTrigger; + + public StoreSidebarComp(ObservableSubscriber filterTrigger) {this.filterTrigger = filterTrigger;} + @Override protected Region createSimple() { var sideBar = new VerticalComp(List.of( - new StoreEntryListOverviewComp() + new StoreEntryListOverviewComp(filterTrigger) .styleClass("color-box") .styleClass("gray") .styleClass("bar"), diff --git a/app/src/main/java/io/xpipe/app/util/ObservableSubscriber.java b/app/src/main/java/io/xpipe/app/util/ObservableSubscriber.java new file mode 100644 index 000000000..9079a7b50 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/util/ObservableSubscriber.java @@ -0,0 +1,26 @@ +package io.xpipe.app.util; + +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.SimpleIntegerProperty; + +public class ObservableSubscriber implements Observable { + + private final IntegerProperty property = new SimpleIntegerProperty(); + + public void trigger() { + property.set(property.get() + 1); + property.getValue(); + } + + @Override + public void addListener(InvalidationListener listener) { + property.addListener(listener); + } + + @Override + public void removeListener(InvalidationListener listener) { + property.removeListener(listener); + } +}