mirror of
https://github.com/xpipe-io/xpipe.git
synced 2024-11-22 07:30:24 +00:00
File browser improvements
This commit is contained in:
parent
240d6698d6
commit
2a828721db
7 changed files with 158 additions and 32 deletions
|
@ -1,6 +1,8 @@
|
||||||
package io.xpipe.app.browser;
|
package io.xpipe.app.browser;
|
||||||
|
|
||||||
import io.xpipe.core.store.FileSystem;
|
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.ClipboardContent;
|
||||||
import javafx.scene.input.Dragboard;
|
import javafx.scene.input.Dragboard;
|
||||||
import lombok.SneakyThrows;
|
import lombok.SneakyThrows;
|
||||||
|
@ -19,7 +21,7 @@ public class FileBrowserClipboard {
|
||||||
List<FileSystem.FileEntry> entries;
|
List<FileSystem.FileEntry> entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Instance currentCopyClipboard;
|
public static Property<Instance> currentCopyClipboard = new SimpleObjectProperty<>();
|
||||||
public static Instance currentDragClipboard;
|
public static Instance currentDragClipboard;
|
||||||
|
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
|
@ -34,12 +36,12 @@ public class FileBrowserClipboard {
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
public static void startCopy(FileSystem.FileEntry base, List<FileSystem.FileEntry> selected) {
|
public static void startCopy(FileSystem.FileEntry base, List<FileSystem.FileEntry> selected) {
|
||||||
var id = UUID.randomUUID();
|
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() {
|
public static Instance retrieveCopy() {
|
||||||
var current = currentCopyClipboard;
|
var current = currentCopyClipboard;
|
||||||
return current;
|
return current.getValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Instance retrieveDrag(Dragboard dragboard) {
|
public static Instance retrieveDrag(Dragboard dragboard) {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -133,6 +133,31 @@ final class FileContextMenu extends ContextMenu {
|
||||||
|
|
||||||
getItems().add(new SeparatorMenuItem());
|
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");
|
var copyName = new MenuItem("Copy name");
|
||||||
copyName.setOnAction(event -> {
|
copyName.setOnAction(event -> {
|
||||||
var selection = new StringSelection(FileNames.getFileName(entry.getPath()));
|
var selection = new StringSelection(FileNames.getFileName(entry.getPath()));
|
||||||
|
|
|
@ -15,6 +15,7 @@ import io.xpipe.app.util.HumanReadableFormat;
|
||||||
import io.xpipe.core.impl.FileNames;
|
import io.xpipe.core.impl.FileNames;
|
||||||
import io.xpipe.core.process.OsType;
|
import io.xpipe.core.process.OsType;
|
||||||
import io.xpipe.core.store.FileSystem;
|
import io.xpipe.core.store.FileSystem;
|
||||||
|
import javafx.application.Platform;
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
import javafx.beans.property.*;
|
import javafx.beans.property.*;
|
||||||
import javafx.beans.value.ChangeListener;
|
import javafx.beans.value.ChangeListener;
|
||||||
|
@ -87,7 +88,6 @@ final class FileListComp extends AnchorPane {
|
||||||
mtimeCol.setCellFactory(col -> new FileTimeCell());
|
mtimeCol.setCellFactory(col -> new FileTimeCell());
|
||||||
mtimeCol.getStyleClass().add(Tweaks.ALIGN_RIGHT);
|
mtimeCol.getStyleClass().add(Tweaks.ALIGN_RIGHT);
|
||||||
|
|
||||||
|
|
||||||
var modeCol = new TableColumn<FileSystem.FileEntry, String>("Attributes");
|
var modeCol = new TableColumn<FileSystem.FileEntry, String>("Attributes");
|
||||||
modeCol.setCellValueFactory(
|
modeCol.setCellValueFactory(
|
||||||
param -> new SimpleObjectProperty<>(param.getValue().getMode()));
|
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());
|
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);
|
FXCollections.sort(table.getItems(), us);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
@ -133,11 +134,27 @@ final class FileListComp extends AnchorPane {
|
||||||
|
|
||||||
table.getSelectionModel().getSelectedItems().addListener((ListChangeListener<? super FileSystem.FileEntry>)
|
table.getSelectionModel().getSelectedItems().addListener((ListChangeListener<? super FileSystem.FileEntry>)
|
||||||
c -> {
|
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()
|
fileList.getFileSystemModel()
|
||||||
.getBrowserModel()
|
.getBrowserModel()
|
||||||
.getSelectedFiles()
|
.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 -> {
|
table.setOnKeyPressed(event -> {
|
||||||
|
@ -183,12 +200,6 @@ final class FileListComp extends AnchorPane {
|
||||||
var listEntry = Bindings.createObjectBinding(
|
var listEntry = Bindings.createObjectBinding(
|
||||||
() -> new FileListCompEntry(row, row.getItem(), fileList), row.itemProperty());
|
() -> 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.itemProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
row.pseudoClassStateChanged(DRAG, false);
|
row.pseudoClassStateChanged(DRAG, false);
|
||||||
row.pseudoClassStateChanged(DRAG_OVER, 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) {
|
if (!hasAttributes) {
|
||||||
table.getColumns().remove(modeCol);
|
table.getColumns().remove(modeCol);
|
||||||
} else {
|
} else {
|
||||||
|
@ -310,7 +327,9 @@ final class FileListComp extends AnchorPane {
|
||||||
|
|
||||||
private final StringProperty img = new SimpleStringProperty();
|
private final StringProperty img = new SimpleStringProperty();
|
||||||
private final StringProperty text = 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 =
|
private final StackPane textField =
|
||||||
new LazyTextFieldComp(text).createStructure().get();
|
new LazyTextFieldComp(text).createStructure().get();
|
||||||
private final ChangeListener<String> listener;
|
private final ChangeListener<String> listener;
|
||||||
|
|
|
@ -84,7 +84,7 @@ public class OpenFileSystemComp extends SimpleComp {
|
||||||
|
|
||||||
FileListComp directoryView = new FileListComp(model.getFileList());
|
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);
|
VBox.setVgrow(directoryView, Priority.ALWAYS);
|
||||||
root.setPadding(Insets.EMPTY);
|
root.setPadding(Insets.EMPTY);
|
||||||
model.getFileList().predicateProperty().set(PREDICATE_NOT_HIDDEN);
|
model.getFileList().predicateProperty().set(PREDICATE_NOT_HIDDEN);
|
||||||
|
|
|
@ -46,6 +46,12 @@
|
||||||
|
|
||||||
.browser .tool-bar {
|
.browser .tool-bar {
|
||||||
-fx-border-width: 1 0 1 0;
|
-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 {
|
.browser .breadcrumbs >.divider {
|
||||||
|
|
|
@ -4,8 +4,10 @@ import io.xpipe.app.core.AppI18n;
|
||||||
import io.xpipe.app.ext.ActionProvider;
|
import io.xpipe.app.ext.ActionProvider;
|
||||||
import io.xpipe.app.storage.DataStorage;
|
import io.xpipe.app.storage.DataStorage;
|
||||||
import io.xpipe.app.storage.DataStoreEntry;
|
import io.xpipe.app.storage.DataStoreEntry;
|
||||||
|
import io.xpipe.core.impl.LocalStore;
|
||||||
import io.xpipe.core.process.CommandControl;
|
import io.xpipe.core.process.CommandControl;
|
||||||
import io.xpipe.core.process.ShellControl;
|
import io.xpipe.core.process.ShellControl;
|
||||||
|
import io.xpipe.core.process.ShellDialects;
|
||||||
import io.xpipe.core.store.ShellStore;
|
import io.xpipe.core.store.ShellStore;
|
||||||
import javafx.beans.value.ObservableValue;
|
import javafx.beans.value.ObservableValue;
|
||||||
import lombok.Value;
|
import lombok.Value;
|
||||||
|
@ -28,12 +30,20 @@ public class SampleAction implements ActionProvider {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void execute() throws Exception {
|
public void execute() throws Exception {
|
||||||
// Start a shell control from the shell connection store
|
var docker = new LocalStore();
|
||||||
try (ShellControl sc = ((ShellStore) entry.getStore()).control().start()) {
|
// 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
|
// Simple commands can be executed in one line
|
||||||
// The shell dialects also provide the proper command syntax for common commands like echo
|
// The shell dialects also provide the appropriate commands for common operations like echo for all supported shells
|
||||||
String echoOut =
|
String echoOut = sc.executeSimpleStringCommand(sc.getShellDialect().getEchoCommand("hello!", false));
|
||||||
sc.executeSimpleStringCommand(sc.getShellDialect().getEchoCommand("hello!", false));
|
|
||||||
|
|
||||||
// You can also implement custom handling for more complex commands
|
// You can also implement custom handling for more complex commands
|
||||||
try (CommandControl cc = sc.command("ls").start()) {
|
try (CommandControl cc = sc.command("ls").start()) {
|
||||||
|
@ -42,7 +52,8 @@ public class SampleAction implements ActionProvider {
|
||||||
|
|
||||||
// Read the stdout lines as a stream
|
// Read the stdout lines as a stream
|
||||||
BufferedReader reader = new BufferedReader(new InputStreamReader(cc.getStdout(), cc.getCharset()));
|
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);
|
System.out.println(s);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -56,25 +67,33 @@ public class SampleAction implements ActionProvider {
|
||||||
// In this case, X-Pipe will internally write a command to a script file and then execute the script
|
// In this case, X-Pipe will internally write a command to a script file and then execute the script
|
||||||
try (CommandControl cc = sc.command(
|
try (CommandControl cc = sc.command(
|
||||||
"""
|
"""
|
||||||
VAR = "value"
|
VAR="value"
|
||||||
echo "$VAR"
|
echo "$VAR"
|
||||||
"""
|
"""
|
||||||
).start()) {
|
).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();
|
var output = cc.readOrThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
// More customization options
|
// More customization options
|
||||||
// If the command should be run as root, the command will be executed with
|
// 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.
|
// sudo and the optional sudo password automatically provided by X-Pipe
|
||||||
// You can also set a custom working directory
|
// 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()) {
|
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();
|
cc.discardOrThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start a bash sub shell. Useful if the login shell is different
|
// 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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue