diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserComp.java index 834a425c1..a4723c504 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserComp.java @@ -59,7 +59,7 @@ public class BrowserComp extends SimpleComp { return true; } - if (!model.getMode().equals(BrowserModel.Mode.BROWSER)) { + if (model.getMode().isChooser()) { return true; } @@ -89,7 +89,7 @@ public class BrowserComp extends SimpleComp { } private Region addBottomBar(Region r) { - if (model.getMode().equals(BrowserModel.Mode.BROWSER)) { + if (!model.getMode().isChooser()) { return r; } @@ -98,14 +98,19 @@ public class BrowserComp extends SimpleComp { var selected = new HBox(); selected.setAlignment(Pos.CENTER_LEFT); selected.setSpacing(10); - // model.getSelected().addListener((ListChangeListener) c -> { - // selected.getChildren().setAll(c.getList().stream().map(s -> { - // var field = new TextField(s.getPath()); - // field.setEditable(false); - // field.setPrefWidth(400); - // return field; - // }).toList()); - // }); + model.getSelection().addListener((ListChangeListener) c -> { + PlatformThread.runLaterIfNeeded(() -> { + selected.getChildren() + .setAll(c.getList().stream() + .map(s -> { + var field = new TextField(s.getRawFileEntry().getPath()); + field.setEditable(false); + field.setPrefWidth(400); + return field; + }) + .toList()); + }); + }); var spacer = new Spacer(Orientation.HORIZONTAL); var button = new Button("Select"); button.setOnAction(event -> model.finishChooser()); @@ -198,7 +203,7 @@ public class BrowserComp extends SimpleComp { continue; } - model.closeFileSystem(source.getKey()); + model.closeFileSystemAsync(source.getKey()); } } }); @@ -209,16 +214,9 @@ public class BrowserComp extends SimpleComp { var tabs = new TabPane(); tabs.setTabDragPolicy(TabPane.TabDragPolicy.REORDER); tabs.setTabMinWidth(Region.USE_COMPUTED_SIZE); - - if (!model.getMode().equals(BrowserModel.Mode.BROWSER)) { - tabs.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE); - tabs.getStyleClass().add("singular"); - } else { - tabs.setTabClosingPolicy(ALL_TABS); - Styles.toggleStyleClass(tabs, TabPane.STYLE_CLASS_FLOATING); - toggleStyleClass(tabs, DENSE); - } - + tabs.setTabClosingPolicy(ALL_TABS); + Styles.toggleStyleClass(tabs, TabPane.STYLE_CLASS_FLOATING); + toggleStyleClass(tabs, DENSE); return tabs; } @@ -257,12 +255,6 @@ public class BrowserComp extends SimpleComp { tab.setGraphic(label); GrowAugment.create(true, false).augment(new SimpleCompStructure<>(label)); - - if (!this.model.getMode().equals(BrowserModel.Mode.BROWSER)) { - label.setManaged(false); - label.setVisible(false); - } - tab.setContent(new OpenFileSystemComp(model).createSimple()); return tab; } diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserContextMenu.java b/app/src/main/java/io/xpipe/app/browser/BrowserContextMenu.java index 77cf1e062..756c70e57 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserContextMenu.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserContextMenu.java @@ -4,30 +4,34 @@ import io.xpipe.app.browser.action.BranchAction; import io.xpipe.app.browser.action.BrowserAction; import io.xpipe.app.browser.action.LeafAction; import io.xpipe.app.core.AppFont; -import javafx.collections.FXCollections; import javafx.scene.control.ContextMenu; import javafx.scene.control.Menu; import javafx.scene.control.SeparatorMenuItem; +import java.util.ArrayList; +import java.util.List; + final class BrowserContextMenu extends ContextMenu { private final OpenFileSystemModel model; - private final boolean empty; + private final BrowserEntry source; - public BrowserContextMenu(OpenFileSystemModel model, boolean empty) { - super(); + public BrowserContextMenu(OpenFileSystemModel model, BrowserEntry source) { this.model = model; - this.empty = empty; + this.source = source; createMenu(); } private void createMenu() { AppFont.normal(this.getStyleableNode()); - var selected = empty || model.getFileList().getSelected().isEmpty() - ? FXCollections.observableArrayList( - new BrowserEntry(model.getCurrentDirectory(), model.getFileList(), false)) - : model.getFileList().getSelected(); + var empty = source == null; + var selected = new ArrayList<>(empty ? List.of() : model.getFileList().getSelection()); + if (source != null && !selected.contains(source)) { + selected.add(source); + } else if (source == null && model.getFileList().getSelection().isEmpty()) { + selected.add(new BrowserEntry(model.getCurrentDirectory(), model.getFileList(), false)); + } for (BrowserAction.Category cat : BrowserAction.Category.values()) { var all = BrowserAction.ALL.stream() diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserFileListComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserFileListComp.java index 26ed44047..0bd6cddcd 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserFileListComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserFileListComp.java @@ -30,7 +30,10 @@ import javafx.scene.control.*; import javafx.scene.control.skin.TableViewSkin; import javafx.scene.control.skin.VirtualFlow; import javafx.scene.input.DragEvent; -import javafx.scene.layout.*; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; import java.time.Instant; import java.time.ZoneId; @@ -126,25 +129,28 @@ final class BrowserFileListComp extends SimpleComp { } private void prepareTableSelectionModel(TableView table) { - if (fileList.getMode().equals(BrowserModel.Mode.SINGLE_FILE_CHOOSER) - || fileList.getMode().equals(BrowserModel.Mode.DIRECTORY_CHOOSER)) { + if (!fileList.getMode().isMultiple()) { table.getSelectionModel().setSelectionMode(SelectionMode.SINGLE); } else { table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); } table.getSelectionModel().getSelectedItems().addListener((ListChangeListener) c -> { + var toSelect = new ArrayList<>(c.getList()); // Explicitly unselect synthetic entries since we can't use a custom selection model as that is bugged in // JavaFX - var toSelect = c.getList().stream() - .filter(entry -> fileList.getFileSystemModel().getCurrentParentDirectory() == null - || !entry.getRawFileEntry() - .getPath() - .equals(fileList.getFileSystemModel() - .getCurrentParentDirectory() - .getPath())) - .toList(); - fileList.getSelected().setAll(toSelect); + toSelect.removeIf(entry -> fileList.getFileSystemModel().getCurrentParentDirectory() != null + && entry.getRawFileEntry() + .getPath() + .equals(fileList.getFileSystemModel() + .getCurrentParentDirectory() + .getPath())); + // Remove unsuitable selection + toSelect.removeIf(browserEntry -> (browserEntry.getRawFileEntry().isDirectory() + && !fileList.getMode().isAcceptsDirectories()) + || (!browserEntry.getRawFileEntry().isDirectory() + && !fileList.getMode().isAcceptsFiles())); + fileList.getSelection().setAll(toSelect); Platform.runLater(() -> { var toUnselect = table.getSelectionModel().getSelectedItems().stream() @@ -155,7 +161,7 @@ final class BrowserFileListComp extends SimpleComp { }); }); - fileList.getSelected().addListener((ListChangeListener) c -> { + fileList.getSelection().addListener((ListChangeListener) c -> { if (c.getList().equals(table.getSelectionModel().getSelectedItems())) { return; } @@ -178,7 +184,7 @@ final class BrowserFileListComp extends SimpleComp { private void prepareTableShortcuts(TableView table) { table.setOnKeyPressed(event -> { - var selected = fileList.getSelected(); + var selected = fileList.getSelection(); BrowserAction.getFlattened().stream() .filter(browserAction -> browserAction.isApplicable(fileList.getFileSystemModel(), selected) && browserAction.isActive(fileList.getFileSystemModel(), selected)) @@ -218,7 +224,7 @@ final class BrowserFileListComp extends SimpleComp { return null; } - return new BrowserContextMenu(fileList.getFileSystemModel(), row.getItem() == null); + return new BrowserContextMenu(fileList.getFileSystemModel(), row.getItem()); }) .augment(new SimpleCompStructure<>(row)); var listEntry = Bindings.createObjectBinding( diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserFileListCompEntry.java b/app/src/main/java/io/xpipe/app/browser/BrowserFileListCompEntry.java index 619eba921..51434049f 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserFileListCompEntry.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserFileListCompEntry.java @@ -32,7 +32,7 @@ public class BrowserFileListCompEntry { @SuppressWarnings("unchecked") public void onMouseClick(MouseEvent t) { if (item == null) { - model.getSelected().clear(); + model.getSelection().clear(); return; } @@ -53,7 +53,7 @@ public class BrowserFileListCompEntry { var max = tv.getSelectionModel().getSelectedItems().stream().mapToInt(entry -> all.indexOf(entry)).max().orElse(all.size() - 1); var end = all.indexOf(item); var start = end > min ? min : max; - model.getSelected().setAll(all.subList(Math.min(start, end), Math.max(start, end) + 1)); + model.getSelection().setAll(all.subList(Math.min(start, end), Math.max(start, end) + 1)); t.consume(); return; } diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserFileListModel.java b/app/src/main/java/io/xpipe/app/browser/BrowserFileListModel.java index bcdd910ac..196286640 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserFileListModel.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserFileListModel.java @@ -35,9 +35,9 @@ public final class BrowserFileListModel { private final Property> shown = new SimpleObjectProperty<>(new ArrayList<>()); private final ObjectProperty> predicateProperty = new SimpleObjectProperty<>(path -> true); - private final ObservableList selected = FXCollections.observableArrayList(); + private final ObservableList selection = FXCollections.observableArrayList(); private final ObservableList selectedRaw = - BindingsHelper.mappedContentBinding(selected, entry -> entry.getRawFileEntry()); + BindingsHelper.mappedContentBinding(selection, entry -> entry.getRawFileEntry()); private final Property draggedOverDirectory = new SimpleObjectProperty(); private final Property draggedOverEmpty = new SimpleBooleanProperty(); diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserModel.java b/app/src/main/java/io/xpipe/app/browser/BrowserModel.java index dc1066996..c8d62f7d6 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserModel.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserModel.java @@ -1,5 +1,6 @@ package io.xpipe.app.browser; +import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.util.ThreadHelper; import io.xpipe.core.impl.FileStore; import io.xpipe.core.store.ShellStore; @@ -10,6 +11,7 @@ import javafx.collections.ObservableList; import lombok.Getter; import lombok.Setter; +import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.function.Consumer; @@ -19,14 +21,36 @@ public class BrowserModel { public BrowserModel(Mode mode) { this.mode = mode; + + selected.addListener((observable, oldValue, newValue) -> { + if (newValue == null) { + return; + } + + BindingsHelper.bindContent(selection, newValue.getFileList().getSelection()); + }); } + @Getter public static enum Mode { - BROWSER, - SINGLE_FILE_CHOOSER, - SINGLE_FILE_SAVE, - MULTIPLE_FILE_CHOOSER, - DIRECTORY_CHOOSER + BROWSER(false, true, true, true), + SINGLE_FILE_CHOOSER(true, false, true, false), + SINGLE_FILE_SAVE(true, false, true, false), + MULTIPLE_FILE_CHOOSER(true, true, true, false), + SINGLE_DIRECTORY_CHOOSER(true, false, false, true), + MULTIPLE_DIRECTORY_CHOOSER(true, true, false, true); + + private final boolean chooser; + private final boolean multiple; + private final boolean acceptsFiles; + private final boolean acceptsDirectories; + + Mode(boolean chooser, boolean multiple, boolean acceptsFiles, boolean acceptsDirectories) { + this.chooser = chooser; + this.multiple = multiple; + this.acceptsFiles = acceptsFiles; + this.acceptsDirectories = acceptsDirectories; + } } public static final BrowserModel DEFAULT = new BrowserModel(Mode.BROWSER); @@ -39,19 +63,23 @@ public class BrowserModel { private final ObservableList openFileSystems = FXCollections.observableArrayList(); private final Property selected = new SimpleObjectProperty<>(); private final BrowserTransferModel localTransfersStage = new BrowserTransferModel(); + private final ObservableList selection = FXCollections.observableArrayList(); public void finishChooser() { - if (getMode().equals(Mode.BROWSER)) { + if (!getMode().isChooser()) { throw new IllegalStateException(); } - var selectedFiles = openFileSystems.get(0).getFileList().getSelected(); - closeFileSystem(openFileSystems.get(0)); + var chosen = new ArrayList<>(selection); + for (OpenFileSystemModel openFileSystem : openFileSystems) { + closeFileSystemAsync(openFileSystem); + } - if (selectedFiles.size() == 0) { + if (chosen.size() == 0) { return; } - var stores = selectedFiles.stream() + + var stores = chosen.stream() .map(entry -> new FileStore( entry.getRawFileEntry().getFileSystem().getStore(), entry.getRawFileEntry().getPath())) @@ -59,7 +87,7 @@ public class BrowserModel { onFinish.accept(stores); } - public void closeFileSystem(OpenFileSystemModel open) { + public void closeFileSystemAsync(OpenFileSystemModel open) { ThreadHelper.runAsync(() -> { if (Objects.equals(selected.getValue(), open)) { selected.setValue(null); diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserStatusBarComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserStatusBarComp.java index e00984cc0..93bc4975f 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserStatusBarComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserStatusBarComp.java @@ -31,8 +31,8 @@ public class BrowserStatusBarComp extends SimpleComp { }, cc); var selectedCount = PlatformThread.sync(Bindings.createIntegerBinding(() -> { - return model.getFileList().getSelected().size(); - }, model.getFileList().getSelected())); + return model.getFileList().getSelection().size(); + }, model.getFileList().getSelection())); var allCount = PlatformThread.sync(Bindings.createIntegerBinding(() -> { return (int) model.getFileList().getAll().getValue().stream().filter(entry -> !entry.isSynthetic()).count(); @@ -60,7 +60,7 @@ public class BrowserStatusBarComp extends SimpleComp { AppFont.small(bar); // Use status bar as an extension of file list - new ContextMenuAugment<>(false, () -> new BrowserContextMenu(model, true)).augment(new SimpleCompStructure<>(bar)); + new ContextMenuAugment<>(false, () -> new BrowserContextMenu(model, null)).augment(new SimpleCompStructure<>(bar)); return bar; } diff --git a/app/src/main/java/io/xpipe/app/browser/OpenFileSystemComp.java b/app/src/main/java/io/xpipe/app/browser/OpenFileSystemComp.java index 225dc4c16..96fa018ed 100644 --- a/app/src/main/java/io/xpipe/app/browser/OpenFileSystemComp.java +++ b/app/src/main/java/io/xpipe/app/browser/OpenFileSystemComp.java @@ -57,7 +57,7 @@ public class OpenFileSystemComp extends SimpleComp { terminalBtn.disableProperty().bind(PlatformThread.sync(model.getNoDirectory())); var menuButton = new MenuButton(null, new FontIcon("mdral-folder_open")); - new ContextMenuAugment<>(true, () -> new BrowserContextMenu(model, true)).augment(new SimpleCompStructure<>(menuButton)); + new ContextMenuAugment<>(true, () -> new BrowserContextMenu(model, null)).augment(new SimpleCompStructure<>(menuButton)); var filter = new BrowserFilterComp(model.getFilter()).createStructure(); Shortcuts.addShortcut(filter.toggleButton(), new KeyCodeCombination(KeyCode.F, KeyCombination.SHORTCUT_DOWN)); @@ -69,9 +69,7 @@ public class OpenFileSystemComp extends SimpleComp { var directoryView = new BrowserFileListComp(model.getFileList()).createRegion(); var root = new VBox(topBar, directoryView); - if (model.getBrowserModel().getMode() == BrowserModel.Mode.BROWSER) { - root.getChildren().add(new BrowserStatusBarComp(model).createRegion()); - } + root.getChildren().add(new BrowserStatusBarComp(model).createRegion()); VBox.setVgrow(directoryView, Priority.ALWAYS); root.setPadding(Insets.EMPTY); model.getFileList().predicateProperty().set(PREDICATE_NOT_HIDDEN); diff --git a/app/src/main/java/io/xpipe/app/core/App.java b/app/src/main/java/io/xpipe/app/core/App.java index c80b2a5a5..5ed92e934 100644 --- a/app/src/main/java/io/xpipe/app/core/App.java +++ b/app/src/main/java/io/xpipe/app/core/App.java @@ -73,7 +73,7 @@ public class App extends Application { var titleBinding = Bindings.createStringBinding( () -> { var base = String.format( - "xpipe Desktop (%s)", AppProperties.get().getVersion()); + "XPipe Desktop (%s)", AppProperties.get().getVersion()); var prefix = AppProperties.get().isStaging() ? "[STAGE] " : ""; var suffix = XPipeDistributionType.get().getUpdateHandler().getPreparedUpdate().getValue() != null ? String.format( diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenInNativeManagerAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/BrowseInNativeManagerAction.java similarity index 86% rename from ext/base/src/main/java/io/xpipe/ext/base/browser/OpenInNativeManagerAction.java rename to ext/base/src/main/java/io/xpipe/ext/base/browser/BrowseInNativeManagerAction.java index 3efda31c1..583ecf42f 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenInNativeManagerAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/BrowseInNativeManagerAction.java @@ -9,7 +9,7 @@ import io.xpipe.core.process.ShellDialect; import java.util.List; -public class OpenInNativeManagerAction implements LeafAction { +public class BrowseInNativeManagerAction implements LeafAction { @Override public void execute(OpenFileSystemModel model, List entries) throws Exception { @@ -19,7 +19,11 @@ public class OpenInNativeManagerAction implements LeafAction { var e = entry.getRawFileEntry().getPath(); switch (OsType.getLocal()) { case OsType.Windows windows -> { - sc.executeSimpleCommand("explorer " + d.fileArgument(e)); + if (entry.getRawFileEntry().isDirectory()) { + sc.executeSimpleCommand("explorer " + d.fileArgument(e)); + } else { + sc.executeSimpleCommand("explorer /select," + d.fileArgument(e)); + } } case OsType.Linux linux -> { var action = entry.getRawFileEntry().isDirectory() ? "org.freedesktop.FileManager1.ShowFolders" : "org.freedesktop.FileManager1.ShowItems"; diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenDirectoryInNewTabAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenDirectoryInNewTabAction.java index 517c04fdc..708b9ef86 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenDirectoryInNewTabAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenDirectoryInNewTabAction.java @@ -1,7 +1,6 @@ package io.xpipe.ext.base.browser; import io.xpipe.app.browser.BrowserEntry; -import io.xpipe.app.browser.BrowserModel; import io.xpipe.app.browser.OpenFileSystemModel; import io.xpipe.app.browser.action.LeafAction; import javafx.scene.Node; @@ -31,7 +30,7 @@ public class OpenDirectoryInNewTabAction implements LeafAction { @Override public boolean isApplicable(OpenFileSystemModel model, List entries) { - return entries.size() == 1 && entries.stream().allMatch(entry -> entry.getRawFileEntry().isDirectory()) && model.getBrowserModel().getMode() == BrowserModel.Mode.BROWSER; + return entries.size() == 1 && entries.stream().allMatch(entry -> entry.getRawFileEntry().isDirectory()); } @Override diff --git a/ext/base/src/main/java/module-info.java b/ext/base/src/main/java/module-info.java index 6fbb581ba..453779eba 100644 --- a/ext/base/src/main/java/module-info.java +++ b/ext/base/src/main/java/module-info.java @@ -33,7 +33,7 @@ open module io.xpipe.ext.base { OpenDirectoryInNewTabAction, OpenTerminalAction, OpenNativeFileDetailsAction, - OpenInNativeManagerAction, + BrowseInNativeManagerAction, EditFileAction, RunAction, CopyAction,