File browser improvements

This commit is contained in:
crschnick 2023-05-06 12:28:18 +00:00
parent 240d6698d6
commit 2a828721db
7 changed files with 158 additions and 32 deletions

View file

@ -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<FileSystem.FileEntry> entries;
}
public static Instance currentCopyClipboard;
public static Property<Instance> currentCopyClipboard = new SimpleObjectProperty<>();
public static Instance currentDragClipboard;
@SneakyThrows
@ -34,12 +36,12 @@ public class FileBrowserClipboard {
@SneakyThrows
public static void startCopy(FileSystem.FileEntry base, List<FileSystem.FileEntry> 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) {

View file

@ -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;
}
}

View file

@ -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()));

View file

@ -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<FileSystem.FileEntry, String>("Attributes");
modeCol.setCellValueFactory(
param -> new SimpleObjectProperty<>(param.getValue().getMode()));
@ -117,7 +117,8 @@ final class FileListComp extends AnchorPane {
};
var dirsFirst = Comparator.<FileSystem.FileEntry, Boolean>comparing(path -> !path.isDirectory());
Comparator<? super FileSystem.FileEntry> us = parentFirst.thenComparing(dirsFirst).thenComparing(comp);
Comparator<? super FileSystem.FileEntry> 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<? super FileSystem.FileEntry>)
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<String> listener;

View file

@ -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);

View file

@ -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 {

View file

@ -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 <pid>").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();
}
}
}
}