From 2a828721db574a733af5fdbf9d83ea74799016aa Mon Sep 17 00:00:00 2001 From: crschnick Date: Sat, 6 May 2023 12:28:18 +0000 Subject: [PATCH] File browser improvements --- .../app/browser/FileBrowserClipboard.java | 8 ++- .../app/browser/FileBrowserStatusBarComp.java | 55 +++++++++++++++++++ .../io/xpipe/app/browser/FileContextMenu.java | 25 +++++++++ .../io/xpipe/app/browser/FileListComp.java | 43 +++++++++++---- .../xpipe/app/browser/OpenFileSystemComp.java | 2 +- .../io/xpipe/app/resources/style/browser.css | 6 ++ .../xpipe/ext/base/actions/SampleAction.java | 51 +++++++++++------ 7 files changed, 158 insertions(+), 32 deletions(-) create mode 100644 app/src/main/java/io/xpipe/app/browser/FileBrowserStatusBarComp.java diff --git a/app/src/main/java/io/xpipe/app/browser/FileBrowserClipboard.java b/app/src/main/java/io/xpipe/app/browser/FileBrowserClipboard.java index 9abcc928e..7a7059701 100644 --- a/app/src/main/java/io/xpipe/app/browser/FileBrowserClipboard.java +++ b/app/src/main/java/io/xpipe/app/browser/FileBrowserClipboard.java @@ -1,6 +1,8 @@ package io.xpipe.app.browser; import io.xpipe.core.store.FileSystem; +import javafx.beans.property.Property; +import javafx.beans.property.SimpleObjectProperty; import javafx.scene.input.ClipboardContent; import javafx.scene.input.Dragboard; import lombok.SneakyThrows; @@ -19,7 +21,7 @@ public class FileBrowserClipboard { List entries; } - public static Instance currentCopyClipboard; + public static Property currentCopyClipboard = new SimpleObjectProperty<>(); public static Instance currentDragClipboard; @SneakyThrows @@ -34,12 +36,12 @@ public class FileBrowserClipboard { @SneakyThrows public static void startCopy(FileSystem.FileEntry base, List selected) { var id = UUID.randomUUID(); - currentCopyClipboard = new Instance(id, base, new ArrayList<>(selected)); + currentCopyClipboard.setValue(new Instance(id, base, new ArrayList<>(selected))); } public static Instance retrieveCopy() { var current = currentCopyClipboard; - return current; + return current.getValue(); } public static Instance retrieveDrag(Dragboard dragboard) { diff --git a/app/src/main/java/io/xpipe/app/browser/FileBrowserStatusBarComp.java b/app/src/main/java/io/xpipe/app/browser/FileBrowserStatusBarComp.java new file mode 100644 index 000000000..10d6b4b3f --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/FileBrowserStatusBarComp.java @@ -0,0 +1,55 @@ +package io.xpipe.app.browser; + +import atlantafx.base.controls.Spacer; +import io.xpipe.app.core.AppFont; +import io.xpipe.app.fxcomps.SimpleComp; +import io.xpipe.app.fxcomps.impl.LabelComp; +import io.xpipe.app.fxcomps.util.PlatformThread; +import javafx.beans.binding.Bindings; +import javafx.scene.control.ToolBar; +import javafx.scene.layout.Region; +import lombok.Value; + +@Value +public class FileBrowserStatusBarComp extends SimpleComp { + + OpenFileSystemModel model; + + @Override + protected Region createSimple() { + var cc = PlatformThread.sync(FileBrowserClipboard.currentCopyClipboard); + var ccCount = Bindings.createStringBinding(() -> { + if (cc.getValue() != null && cc.getValue().getEntries().size() > 0) { + return String.valueOf(cc.getValue().getEntries().size()) + " file" + (cc.getValue().getEntries().size() > 1 ? "s" : "") + " in clipboard"; + } else { + return null; + } + }, cc); + + var selectedCount = PlatformThread.sync(Bindings.createIntegerBinding(() -> { + return model.getFileList().getSelected().size(); + }, model.getFileList().getSelected())); + + var allCount = PlatformThread.sync(Bindings.createIntegerBinding(() -> { + return model.getFileList().getAll().getValue().size(); + }, model.getFileList().getAll())); + + var selectedComp = new LabelComp(Bindings.createStringBinding(() -> { + if (selectedCount.getValue().intValue() == 0) { + return null; + } else { + return selectedCount.getValue() + " / " + allCount.getValue() + " selected"; + } + }, selectedCount, allCount)); + + var bar = new ToolBar(); + bar.getItems().setAll( + new LabelComp(ccCount).createRegion(), + new Spacer(), + selectedComp.createRegion() + ); + bar.getStyleClass().add("status-bar"); + AppFont.small(bar); + return bar; + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/FileContextMenu.java b/app/src/main/java/io/xpipe/app/browser/FileContextMenu.java index 39f27db4f..f3546fc6a 100644 --- a/app/src/main/java/io/xpipe/app/browser/FileContextMenu.java +++ b/app/src/main/java/io/xpipe/app/browser/FileContextMenu.java @@ -133,6 +133,31 @@ final class FileContextMenu extends ContextMenu { getItems().add(new SeparatorMenuItem()); + { + + var copy = new MenuItem("Copy"); + copy.setOnAction(event -> { + FileBrowserClipboard.startCopy( + model.getCurrentDirectory(), model.getFileList().getSelected()); + event.consume(); + }); + getItems().add(copy); + + var paste = new MenuItem("Paste"); + paste.setOnAction(event -> { + var clipboard = FileBrowserClipboard.retrieveCopy(); + if (clipboard != null) { + var files = clipboard.getEntries(); + var target = model.getCurrentDirectory(); + model.dropFilesIntoAsync(target, files, true); + } + event.consume(); + }); + getItems().add(paste); + } + + getItems().add(new SeparatorMenuItem()); + var copyName = new MenuItem("Copy name"); copyName.setOnAction(event -> { var selection = new StringSelection(FileNames.getFileName(entry.getPath())); diff --git a/app/src/main/java/io/xpipe/app/browser/FileListComp.java b/app/src/main/java/io/xpipe/app/browser/FileListComp.java index 67d6b3cd2..20305311e 100644 --- a/app/src/main/java/io/xpipe/app/browser/FileListComp.java +++ b/app/src/main/java/io/xpipe/app/browser/FileListComp.java @@ -15,6 +15,7 @@ import io.xpipe.app.util.HumanReadableFormat; import io.xpipe.core.impl.FileNames; import io.xpipe.core.process.OsType; import io.xpipe.core.store.FileSystem; +import javafx.application.Platform; import javafx.beans.binding.Bindings; import javafx.beans.property.*; import javafx.beans.value.ChangeListener; @@ -87,7 +88,6 @@ final class FileListComp extends AnchorPane { mtimeCol.setCellFactory(col -> new FileTimeCell()); mtimeCol.getStyleClass().add(Tweaks.ALIGN_RIGHT); - var modeCol = new TableColumn("Attributes"); modeCol.setCellValueFactory( param -> new SimpleObjectProperty<>(param.getValue().getMode())); @@ -117,7 +117,8 @@ final class FileListComp extends AnchorPane { }; var dirsFirst = Comparator.comparing(path -> !path.isDirectory()); - Comparator us = parentFirst.thenComparing(dirsFirst).thenComparing(comp); + Comparator us = + parentFirst.thenComparing(dirsFirst).thenComparing(comp); FXCollections.sort(table.getItems(), us); return true; }); @@ -133,11 +134,27 @@ final class FileListComp extends AnchorPane { table.getSelectionModel().getSelectedItems().addListener((ListChangeListener) c -> { - fileList.getSelected().setAll(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.getPath() + .equals(fileList.getFileSystemModel() + .getCurrentParentDirectory() + .getPath())) + .toList(); + fileList.getSelected().setAll(toSelect); fileList.getFileSystemModel() .getBrowserModel() .getSelectedFiles() - .setAll(c.getList()); + .setAll(toSelect); + + Platform.runLater(() -> { + var toUnselect = table.getSelectionModel().getSelectedItems().stream() + .filter(entry -> !toSelect.contains(entry)) + .toList(); + toUnselect.forEach(entry -> table.getSelectionModel() + .clearSelection(table.getItems().indexOf(entry))); + }); }); table.setOnKeyPressed(event -> { @@ -183,12 +200,6 @@ final class FileListComp extends AnchorPane { var listEntry = Bindings.createObjectBinding( () -> new FileListCompEntry(row, row.getItem(), fileList), row.itemProperty()); - row.selectedProperty().addListener((observable, oldValue, newValue) -> { - if (newValue && listEntry.get().isSynthetic()) { - row.updateSelected(false); - } - }); - row.itemProperty().addListener((observable, oldValue, newValue) -> { row.pseudoClassStateChanged(DRAG, false); row.pseudoClassStateChanged(DRAG_OVER, false); @@ -252,7 +263,13 @@ final class FileListComp extends AnchorPane { } } - var hasAttributes = fileList.getFileSystemModel().getFileSystem() != null && !fileList.getFileSystemModel().getFileSystem().getShell().orElseThrow().getOsType().equals(OsType.WINDOWS); + var hasAttributes = fileList.getFileSystemModel().getFileSystem() != null + && !fileList.getFileSystemModel() + .getFileSystem() + .getShell() + .orElseThrow() + .getOsType() + .equals(OsType.WINDOWS); if (!hasAttributes) { table.getColumns().remove(modeCol); } else { @@ -310,7 +327,9 @@ final class FileListComp extends AnchorPane { private final StringProperty img = new SimpleStringProperty(); private final StringProperty text = new SimpleStringProperty(); - private final Node imageView = new SvgCacheComp(new SimpleDoubleProperty(24), new SimpleDoubleProperty(24), img, FileIconManager.getSvgCache()).createRegion(); + private final Node imageView = new SvgCacheComp( + new SimpleDoubleProperty(24), new SimpleDoubleProperty(24), img, FileIconManager.getSvgCache()) + .createRegion(); private final StackPane textField = new LazyTextFieldComp(text).createStructure().get(); private final ChangeListener listener; 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 4ee5252c3..0ebbf02f7 100644 --- a/app/src/main/java/io/xpipe/app/browser/OpenFileSystemComp.java +++ b/app/src/main/java/io/xpipe/app/browser/OpenFileSystemComp.java @@ -84,7 +84,7 @@ public class OpenFileSystemComp extends SimpleComp { FileListComp directoryView = new FileListComp(model.getFileList()); - var root = new VBox(topBar, directoryView); + var root = new VBox(topBar, directoryView, new FileBrowserStatusBarComp(model).createRegion()); VBox.setVgrow(directoryView, Priority.ALWAYS); root.setPadding(Insets.EMPTY); model.getFileList().predicateProperty().set(PREDICATE_NOT_HIDDEN); diff --git a/app/src/main/resources/io/xpipe/app/resources/style/browser.css b/app/src/main/resources/io/xpipe/app/resources/style/browser.css index 7aefa3fb7..a5808189a 100644 --- a/app/src/main/resources/io/xpipe/app/resources/style/browser.css +++ b/app/src/main/resources/io/xpipe/app/resources/style/browser.css @@ -46,6 +46,12 @@ .browser .tool-bar { -fx-border-width: 1 0 1 0; + -fx-padding: 5px 10px ; +} + +.browser .status-bar { + -fx-border-width: 1 0 1 0; +-fx-border-color: -color-neutral-muted; } .browser .breadcrumbs >.divider { diff --git a/ext/base/src/main/java/io/xpipe/ext/base/actions/SampleAction.java b/ext/base/src/main/java/io/xpipe/ext/base/actions/SampleAction.java index f57696a5d..f208e7699 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/actions/SampleAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/actions/SampleAction.java @@ -4,8 +4,10 @@ import io.xpipe.app.core.AppI18n; import io.xpipe.app.ext.ActionProvider; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreEntry; +import io.xpipe.core.impl.LocalStore; import io.xpipe.core.process.CommandControl; import io.xpipe.core.process.ShellControl; +import io.xpipe.core.process.ShellDialects; import io.xpipe.core.store.ShellStore; import javafx.beans.value.ObservableValue; import lombok.Value; @@ -28,12 +30,20 @@ public class SampleAction implements ActionProvider { @Override public void execute() throws Exception { - // Start a shell control from the shell connection store - try (ShellControl sc = ((ShellStore) entry.getStore()).control().start()) { + var docker = new LocalStore(); + // Start a shell control from the docker connection store + try (ShellControl sc = docker.control().start()) { + // Once we are here, the shell connection is initialized and we can query all kinds of information + + // Query the detected shell dialect, e.g. cmd, powershell, sh, bash, etc. + System.out.println(sc.getShellDialect()); + + // Query the os type + System.out.println(sc.getOsType()); + // Simple commands can be executed in one line - // The shell dialects also provide the proper command syntax for common commands like echo - String echoOut = - sc.executeSimpleStringCommand(sc.getShellDialect().getEchoCommand("hello!", false)); + // The shell dialects also provide the appropriate commands for common operations like echo for all supported shells + String echoOut = sc.executeSimpleStringCommand(sc.getShellDialect().getEchoCommand("hello!", false)); // You can also implement custom handling for more complex commands try (CommandControl cc = sc.command("ls").start()) { @@ -42,7 +52,8 @@ public class SampleAction implements ActionProvider { // Read the stdout lines as a stream BufferedReader reader = new BufferedReader(new InputStreamReader(cc.getStdout(), cc.getCharset())); - reader.lines().filter(s -> s != null).forEach(s -> { + // We don't have to close this stream here, that will be automatically done by the command control after the try-with block + reader.lines().filter(s -> !s.isBlank()).forEach(s -> { System.out.println(s); }); @@ -55,26 +66,34 @@ public class SampleAction implements ActionProvider { // Commands can also be more complex and span multiple lines. // In this case, X-Pipe will internally write a command to a script file and then execute the script try (CommandControl cc = sc.command( - """ - VAR = "value" - echo "$VAR" - """ - ).start()) { + """ + VAR="value" + echo "$VAR" + """ + ).start()) { + // Reads stdout, stashes stderr. If the exit code is not 0, it will throw an exception with the stderr contents. var output = cc.readOrThrow(); } // More customization options // If the command should be run as root, the command will be executed with - // sudo and the optional sudo password automatically provided by X-Pipe. - // You can also set a custom working directory + // sudo and the optional sudo password automatically provided by X-Pipe + // by using the information from the connection store. + // You can also set a custom working directory. try (CommandControl cc = sc.command("kill ").elevated().workingDirectory("/").start()) { - // Discard any output but throw an exception the exit code is not 0 + // Discard any output but throw an exception with the stderr contents if the exit code is not 0 cc.discardOrThrow(); } // Start a bash sub shell. Useful if the login shell is different - try (ShellControl bash = sc.subShell("bash").start()) { - // ... + try (ShellControl bash = sc.subShell(ShellDialects.BASH).start()) { + // Let's write to a file + try (CommandControl cc = bash.command("cat > myfile.txt").start()) { + // Writing into stdin can also easily be done + cc.getStdin().write("my file content".getBytes(cc.getCharset())); + // Close stdin to send EOF. It will be reopened by the shell control after the command is done + cc.closeStdin(); + } } } }