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;
|
||||
|
||||
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) {
|
||||
|
|
|
@ -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());
|
||||
|
||||
{
|
||||
|
||||
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()));
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
@ -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
|
||||
try (CommandControl cc = sc.command(
|
||||
"""
|
||||
VAR = "value"
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue