Rework actions

This commit is contained in:
crschnick 2023-05-14 00:26:48 +00:00
parent 8038e88b28
commit 69ed60b611
59 changed files with 1949 additions and 622 deletions

View file

@ -3,7 +3,9 @@ package io.xpipe.app.browser;
import atlantafx.base.controls.RingProgressIndicator;
import atlantafx.base.controls.Spacer;
import atlantafx.base.theme.Styles;
import io.xpipe.app.browser.icon.DirectoryType;
import io.xpipe.app.browser.icon.FileIconManager;
import io.xpipe.app.browser.icon.FileType;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.augment.GrowAugment;
@ -12,7 +14,6 @@ import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.BusyProperty;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.FileSystem;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleBooleanProperty;
@ -42,6 +43,8 @@ public class FileBrowserComp extends SimpleComp {
@Override
protected Region createSimple() {
FileType.loadDefinitions();
DirectoryType.loadDefinitions();
ThreadHelper.runAsync( () -> {
FileIconManager.loadIfNecessary();
});
@ -78,14 +81,14 @@ public class FileBrowserComp extends SimpleComp {
var selected = new HBox();
selected.setAlignment(Pos.CENTER_LEFT);
selected.setSpacing(10);
model.getSelectedFiles().addListener((ListChangeListener<? super FileSystem.FileEntry>) 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.getSelected().addListener((ListChangeListener<? super FileSystem.FileEntry>) c -> {
// selected.getChildren().setAll(c.getList().stream().map(s -> {
// var field = new TextField(s.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());

View file

@ -0,0 +1,67 @@
package io.xpipe.app.browser;
import io.xpipe.app.browser.icon.DirectoryType;
import io.xpipe.app.browser.icon.FileType;
import io.xpipe.core.impl.FileNames;
import io.xpipe.core.store.FileSystem;
import lombok.Getter;
@Getter
public class FileBrowserEntry {
private final FileListModel model;
private final FileSystem.FileEntry rawFileEntry;
private final boolean synthetic;
private final FileType fileType;
private final DirectoryType directoryType;
public FileBrowserEntry(FileSystem.FileEntry rawFileEntry, FileListModel model, boolean synthetic) {
this.rawFileEntry = rawFileEntry;
this.model = model;
this.synthetic = synthetic;
this.fileType = fileType(rawFileEntry);
this.directoryType = directoryType(rawFileEntry);
}
private static FileType fileType(FileSystem.FileEntry rawFileEntry) {
if (rawFileEntry.isDirectory()) {
return null;
}
for (var f : FileType.ALL) {
if (f.matches(rawFileEntry)) {
return f;
}
}
return null;
}
private static DirectoryType directoryType(FileSystem.FileEntry rawFileEntry) {
if (!rawFileEntry.isDirectory()) {
return null;
}
for (var f : DirectoryType.ALL) {
if (f.matches(rawFileEntry)) {
return f;
}
}
return null;
}
public String getFileName() {
return FileNames.getFileName(getRawFileEntry().getPath());
}
public String getOptionallyQuotedFileName() {
var n = getFileName();
return FileNames.quoteIfNecessary(n);
}
public String getOptionallyQuotedFilePath() {
var n = rawFileEntry.getPath();
return FileNames.quoteIfNecessary(n);
}
}

View file

@ -2,7 +2,6 @@ package io.xpipe.app.browser;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.impl.FileStore;
import io.xpipe.core.store.FileSystem;
import io.xpipe.core.store.ShellStore;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
@ -33,7 +32,7 @@ public class FileBrowserModel {
public static final FileBrowserModel DEFAULT = new FileBrowserModel(Mode.BROWSER);
private final Mode mode;
private final ObservableList<FileSystem.FileEntry> selectedFiles = FXCollections.observableArrayList();
private final ObservableList<FileBrowserEntry> selectedFiles = FXCollections.observableArrayList();
@Setter
private Consumer<List<FileStore>> onFinish;
@ -52,7 +51,11 @@ public class FileBrowserModel {
if (selectedFiles.size() == 0) {
return;
}
var stores = selectedFiles.stream().map(entry -> new FileStore(entry.getFileSystem().getStore(), entry.getPath())).toList();
var stores = selectedFiles.stream()
.map(entry -> new FileStore(
entry.getRawFileEntry().getFileSystem().getStore(),
entry.getRawFileEntry().getPath()))
.toList();
onFinish.accept(stores);
}
@ -67,7 +70,9 @@ public class FileBrowserModel {
}
public void openExistingFileSystemIfPresent(ShellStore store) {
var found = openFileSystems.stream().filter(model -> Objects.equals(model.getStore().getValue(), store)).findFirst();
var found = openFileSystems.stream()
.filter(model -> Objects.equals(model.getStore().getValue(), store))
.findFirst();
if (found.isPresent()) {
selected.setValue(found.get());
} else {
@ -75,6 +80,14 @@ public class FileBrowserModel {
}
}
public void openFileSystemSync(ShellStore store, String path) throws Exception {
var model = new OpenFileSystemModel(this);
openFileSystems.add(model);
selected.setValue(model);
model.switchSync(store);
model.cd(path);
}
public void openFileSystemAsync(ShellStore store) {
// Prevent multiple tabs in non browser modes
if (!mode.equals(Mode.BROWSER)) {

View file

@ -33,7 +33,7 @@ public class FileBrowserStatusBarComp extends SimpleComp {
}, model.getFileList().getSelected()));
var allCount = PlatformThread.sync(Bindings.createIntegerBinding(() -> {
return model.getFileList().getAll().getValue().size();
return (int) model.getFileList().getAll().getValue().stream().filter(entry -> !entry.isSynthetic()).count();
}, model.getFileList().getAll()));
var selectedComp = new LabelComp(Bindings.createStringBinding(() -> {
@ -52,6 +52,13 @@ public class FileBrowserStatusBarComp extends SimpleComp {
);
bar.getStyleClass().add("status-bar");
AppFont.small(bar);
// Use status bar as an extension of file list
bar.setOnMouseClicked(event -> {
model.getFileList().getSelected().clear();
event.consume();
});
return bar;
}
}

View file

@ -2,192 +2,98 @@
package io.xpipe.app.browser;
import io.xpipe.app.comp.source.GuiDsCreatorMultiStep;
import io.xpipe.app.ext.DataSourceProvider;
import io.xpipe.app.util.FileOpener;
import io.xpipe.app.util.ScriptHelper;
import io.xpipe.app.util.TerminalHelper;
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.util.BusyProperty;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.impl.FileNames;
import io.xpipe.core.impl.FileStore;
import io.xpipe.core.process.OsType;
import io.xpipe.core.process.ShellControl;
import io.xpipe.core.store.FileSystem;
import javafx.beans.property.Property;
import javafx.collections.FXCollections;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SeparatorMenuItem;
import org.apache.commons.io.FilenameUtils;
import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
import java.util.List;
import java.util.function.UnaryOperator;
final class FileContextMenu extends ContextMenu {
public boolean isExecutable(FileSystem.FileEntry e) {
if (e.isDirectory()) {
return false;
}
if (e.getExecutable() != null && e.getExecutable()) {
return true;
}
var shell = e.getFileSystem().getShell();
if (shell.isEmpty()) {
return false;
}
var os = shell.get().getOsType();
var ending = FilenameUtils.getExtension(e.getPath()).toLowerCase();
if (os.equals(OsType.WINDOWS) && List.of("exe", "bat", "ps1", "cmd").contains(ending)) {
return true;
}
if (List.of("sh", "command").contains(ending)) {
return true;
}
return false;
}
private final OpenFileSystemModel model;
private final FileSystem.FileEntry entry;
private final Property<FileSystem.FileEntry> editing;
private final boolean empty;
public FileContextMenu(OpenFileSystemModel model, FileSystem.FileEntry entry, Property<FileSystem.FileEntry> editing) {
public FileContextMenu(OpenFileSystemModel model, boolean empty) {
super();
this.model = model;
this.entry = entry;
this.editing = editing;
this.empty = empty;
createMenu();
}
private void createMenu() {
if (entry.isDirectory()) {
var terminal = new MenuItem("Open terminal");
terminal.setOnAction(event -> {
event.consume();
model.openTerminalAsync(entry.getPath());
});
getItems().add(terminal);
} else {
if (isExecutable(entry)) {
var execute = new MenuItem("Run in terminal");
execute.setOnAction(event -> {
ThreadHelper.runFailableAsync(() -> {
ShellControl pc = model.getFileSystem().getShell().orElseThrow();
var e = pc.getShellDialect().getMakeExecutableCommand(entry.getPath());
if (e != null) {
pc.executeSimpleBooleanCommand(e);
}
var cmd = pc.command("\"" + entry.getPath() + "\"").prepareTerminalOpen("?");
TerminalHelper.open(FilenameUtils.getBaseName(entry.getPath()), cmd);
});
event.consume();
});
getItems().add(execute);
var selected = empty ? FXCollections.<FileBrowserEntry>observableArrayList() : model.getFileList().getSelected();
var executeInBackground = new MenuItem("Run in background");
executeInBackground.setOnAction(event -> {
ThreadHelper.runFailableAsync(() -> {
ShellControl pc = model.getFileSystem().getShell().orElseThrow();
var e = pc.getShellDialect().getMakeExecutableCommand(entry.getPath());
if (e != null) {
pc.executeSimpleBooleanCommand(e);
}
var cmd = ScriptHelper.createDetachCommand(pc, "\"" + entry.getPath() + "\"");
pc.executeSimpleBooleanCommand(cmd);
});
event.consume();
});
getItems().add(executeInBackground);
} else {
var open = new MenuItem("Open default");
open.setOnAction(event -> {
ThreadHelper.runFailableAsync(() -> {
FileOpener.openInDefaultApplication(entry);
});
event.consume();
});
getItems().add(open);
for (BrowserAction.Category cat : BrowserAction.Category.values()) {
var all = BrowserAction.ALL.stream().filter(browserAction -> browserAction.getCategory() == cat).filter(browserAction -> {
if (!browserAction.isApplicable(model, selected)) {
return false;
}
if (!browserAction.acceptsEmptySelection() && selected.isEmpty()) {
return false;
}
return true;
}).toList();
if (all.size() == 0) {
continue;
}
var pipe = new MenuItem("Pipe");
pipe.setOnAction(event -> {
var store = new FileStore(model.getFileSystem().getStore(), entry.getPath());
GuiDsCreatorMultiStep.showForStore(DataSourceProvider.Category.STREAM, store, null);
event.consume();
});
// getItems().add(pipe);
if (getItems().size() > 0) {
getItems().add(new SeparatorMenuItem());
}
var edit = new MenuItem("Edit");
edit.setOnAction(event -> {
ThreadHelper.runAsync(() -> FileOpener.openInTextEditor(entry));
event.consume();
});
getItems().add(edit);
}
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 = entry.isDirectory() ? entry : model.getCurrentDirectory();
model.dropFilesIntoAsync(target, files, true);
for (BrowserAction a : all) {
if (a instanceof LeafAction la) {
getItems().add(toItem(la, selected, s -> s));
}
event.consume();
});
getItems().add(paste);
if (a instanceof BranchAction la) {
var m = new Menu(a.getName(model, selected) + " ...");
for (LeafAction sub : la.getBranchingActions()) {
if (!sub.isApplicable(model, selected)) {
continue;
}
m.getItems().add(toItem(sub, selected, s -> "... " + s));
}
var graphic = a.getIcon(model, selected);
if (graphic != null) {
m.setGraphic(graphic);
}
m.setDisable(!a.isActive(model, selected));
getItems().add(m);
}
}
}
}
getItems().add(new SeparatorMenuItem());
var copyName = new MenuItem("Copy name");
copyName.setOnAction(event -> {
var selection = new StringSelection(FileNames.getFileName(entry.getPath()));
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
clipboard.setContents(selection, selection);
private MenuItem toItem(LeafAction a, List<FileBrowserEntry> selected, UnaryOperator<String> nameFunc) {
var mi = new MenuItem(nameFunc.apply(a.getName(model, selected)));
mi.setOnAction(event -> {
ThreadHelper.runFailableAsync(() -> {
BusyProperty.execute(model.getBusy(), () -> {
a.execute(model, selected);
});
});
event.consume();
});
getItems().add(copyName);
var copyPath = new MenuItem("Copy full path");
copyPath.setOnAction(event -> {
var selection = new StringSelection(entry.getPath());
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
clipboard.setContents(selection, selection);
event.consume();
});
getItems().add(copyPath);
var delete = new MenuItem("Delete");
delete.setOnAction(event -> {
model.deleteSelectionAsync();
event.consume();
});
var rename = new MenuItem("Rename");
rename.setOnAction(event -> {
event.consume();
editing.setValue(entry);
});
getItems().addAll(new SeparatorMenuItem(), rename, delete);
if (a.getShortcut() != null) {
mi.setAccelerator(a.getShortcut());
}
var graphic = a.getIcon(model, selected);
if (graphic != null) {
mi.setGraphic(graphic);
}
mi.setMnemonicParsing(false);
mi.setDisable(!a.isActive(model, selected));
return mi;
}
}

View file

@ -1,32 +1,23 @@
package io.xpipe.app.browser;
import atlantafx.base.theme.Styles;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.augment.GrowAugment;
import io.xpipe.app.fxcomps.impl.TextFieldComp;
import io.xpipe.app.fxcomps.util.Shortcuts;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.scene.control.Button;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import org.kordamp.ikonli.javafx.FontIcon;
public class FileFilterComp extends SimpleComp {
private final Property<String> filterString;
public FileFilterComp(Property<String> filterString) {
this.filterString = filterString;
}
public class FileFilterComp extends Comp<FileFilterComp.Structure> {
@Override
protected Region createSimple() {
public Structure createBase() {
var expanded = new SimpleBooleanProperty();
var text = new TextFieldComp(filterString, false).createRegion();
var button = new Button();
@ -57,7 +48,6 @@ public class FileFilterComp extends SimpleComp {
var fi = new FontIcon("mdi2m-magnify");
GrowAugment.create(false, true).augment(new SimpleCompStructure<>(button));
Shortcuts.addShortcut(button, new KeyCodeCombination(KeyCode.F, KeyCombination.SHORTCUT_DOWN));
button.setGraphic(fi);
button.setOnAction(event -> {
if (expanded.get()) {
@ -75,7 +65,6 @@ public class FileFilterComp extends SimpleComp {
text.setPrefWidth(0);
button.getStyleClass().add(Styles.FLAT);
expanded.addListener((observable, oldValue, val) -> {
System.out.println(val);
if (val) {
text.setPrefWidth(250);
button.getStyleClass().add(Styles.RIGHT_PILL);
@ -89,6 +78,20 @@ public class FileFilterComp extends SimpleComp {
var box = new HBox(text, button);
box.setFillHeight(true);
return box;
return new Structure(box, (TextField) text, button);
}
public record Structure(HBox box, TextField textField, Button toggleButton) implements CompStructure<HBox> {
@Override
public HBox get() {
return box;
}
}
private final Property<String> filterString;
public FileFilterComp(Property<String> filterString) {
this.filterString = filterString;
}
}

View file

@ -4,14 +4,18 @@ package io.xpipe.app.browser;
import atlantafx.base.theme.Styles;
import atlantafx.base.theme.Tweaks;
import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.browser.icon.FileIconManager;
import io.xpipe.app.comp.base.LazyTextFieldComp;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
import io.xpipe.app.fxcomps.impl.SvgCacheComp;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.app.util.BusyProperty;
import io.xpipe.app.util.Containers;
import io.xpipe.app.util.HumanReadableFormat;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.impl.FileNames;
import io.xpipe.core.process.OsType;
import io.xpipe.core.store.FileSystem;
@ -30,7 +34,6 @@ import javafx.scene.control.*;
import javafx.scene.control.skin.TableViewSkin;
import javafx.scene.control.skin.VirtualFlow;
import javafx.scene.input.DragEvent;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.*;
import java.time.Instant;
@ -56,7 +59,7 @@ final class FileListComp extends AnchorPane {
public FileListComp(FileListModel fileList) {
this.fileList = fileList;
TableView<FileSystem.FileEntry> table = createTable();
TableView<FileBrowserEntry> table = createTable();
SimpleChangeListener.apply(table.comparatorProperty(), (newValue) -> {
fileList.setComparator(newValue);
});
@ -67,64 +70,67 @@ final class FileListComp extends AnchorPane {
}
@SuppressWarnings("unchecked")
private TableView<FileSystem.FileEntry> createTable() {
var filenameCol = new TableColumn<FileSystem.FileEntry, String>("Name");
private TableView<FileBrowserEntry> createTable() {
var filenameCol = new TableColumn<FileBrowserEntry, String>("Name");
filenameCol.setCellValueFactory(param -> new SimpleStringProperty(
param.getValue() != null
? FileNames.getFileName(param.getValue().getPath())
? FileNames.getFileName(
param.getValue().getRawFileEntry().getPath())
: null));
filenameCol.setComparator(Comparator.comparing(String::toLowerCase));
filenameCol.setSortType(ASCENDING);
filenameCol.setCellFactory(col -> new FilenameCell(fileList.getEditing()));
var sizeCol = new TableColumn<FileSystem.FileEntry, Number>("Size");
sizeCol.setCellValueFactory(
param -> new SimpleLongProperty(param.getValue().getSize()));
var sizeCol = new TableColumn<FileBrowserEntry, Number>("Size");
sizeCol.setCellValueFactory(param ->
new SimpleLongProperty(param.getValue().getRawFileEntry().getSize()));
sizeCol.setCellFactory(col -> new FileSizeCell());
var mtimeCol = new TableColumn<FileSystem.FileEntry, Instant>("Modified");
mtimeCol.setCellValueFactory(
param -> new SimpleObjectProperty<>(param.getValue().getDate()));
var mtimeCol = new TableColumn<FileBrowserEntry, Instant>("Modified");
mtimeCol.setCellValueFactory(param ->
new SimpleObjectProperty<>(param.getValue().getRawFileEntry().getDate()));
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()));
var modeCol = new TableColumn<FileBrowserEntry, String>("Attributes");
modeCol.setCellValueFactory(param ->
new SimpleObjectProperty<>(param.getValue().getRawFileEntry().getMode()));
modeCol.setCellFactory(col -> new FileModeCell());
var table = new TableView<FileSystem.FileEntry>();
var table = new TableView<FileBrowserEntry>();
table.setPlaceholder(new Region());
table.getStyleClass().add(Styles.STRIPED);
table.getColumns().setAll(filenameCol, sizeCol, modeCol, mtimeCol);
table.getSortOrder().add(filenameCol);
table.setSortPolicy(param -> {
var comp = table.getComparator();
if (comp == null) {
return true;
}
var parentFirst = new Comparator<FileSystem.FileEntry>() {
@Override
public int compare(FileSystem.FileEntry o1, FileSystem.FileEntry o2) {
var c = fileList.getFileSystemModel().getCurrentParentDirectory();
if (c == null) {
return 0;
}
var syntheticFirst = Comparator.<FileBrowserEntry, Boolean>comparing(path -> !path.isSynthetic());
var dirsFirst = Comparator.<FileBrowserEntry, Boolean>comparing(
path -> !path.getRawFileEntry().isDirectory());
return o1.getPath().equals(c.getPath()) ? -1 : (o2.getPath().equals(c.getPath()) ? 1 : 0);
}
};
var dirsFirst = Comparator.<FileSystem.FileEntry, Boolean>comparing(path -> !path.isDirectory());
Comparator<? super FileSystem.FileEntry> us =
parentFirst.thenComparing(dirsFirst).thenComparing(comp);
FXCollections.sort(table.getItems(), us);
Comparator<? super FileBrowserEntry> us =
syntheticFirst.thenComparing(dirsFirst).thenComparing(comp);
FXCollections.sort(param.getItems(), us);
return true;
});
table.getSortOrder().add(filenameCol);
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
filenameCol.minWidthProperty().bind(table.widthProperty().multiply(0.5));
table.setFixedCellSize(34.0);
prepareTableSelectionModel(table);
prepareTableShortcuts(table);
prepareTableEntries(table);
prepareTableChanges(table, mtimeCol, modeCol);
return table;
}
private void prepareTableSelectionModel(TableView<FileBrowserEntry> table) {
if (fileList.getMode().equals(FileBrowserModel.Mode.SINGLE_FILE_CHOOSER)
|| fileList.getMode().equals(FileBrowserModel.Mode.DIRECTORY_CHOOSER)) {
table.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
@ -132,59 +138,68 @@ final class FileListComp extends AnchorPane {
table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
}
table.getSelectionModel().getSelectedItems().addListener((ListChangeListener<? super FileSystem.FileEntry>)
c -> {
// 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(toSelect);
table.getSelectionModel().getSelectedItems().addListener((ListChangeListener<? super FileBrowserEntry>) c -> {
// 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);
fileList.getFileSystemModel().getBrowserModel().getSelectedFiles().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 -> {
if (event.isControlDown()
&& event.getCode().equals(KeyCode.C)
&& table.getSelectionModel().getSelectedItems().size() > 0) {
FileBrowserClipboard.startCopy(
fileList.getFileSystemModel().getCurrentDirectory(),
table.getSelectionModel().getSelectedItems());
event.consume();
}
if (event.isControlDown() && event.getCode().equals(KeyCode.V)) {
var clipboard = FileBrowserClipboard.retrieveCopy();
if (clipboard != null) {
var files = clipboard.getEntries();
var target = fileList.getFileSystemModel().getCurrentDirectory();
fileList.getFileSystemModel().dropFilesIntoAsync(target, files, true);
event.consume();
}
}
Platform.runLater(() -> {
var toUnselect = table.getSelectionModel().getSelectedItems().stream()
.filter(entry -> !toSelect.contains(entry))
.toList();
toUnselect.forEach(entry -> table.getSelectionModel()
.clearSelection(table.getItems().indexOf(entry)));
});
});
prepareTableEntries(table);
prepareTableChanges(table, mtimeCol, modeCol);
fileList.getSelected().addListener((ListChangeListener<? super FileBrowserEntry>) c -> {
if (c.getList().equals(table.getSelectionModel().getSelectedItems())) {
return;
}
return table;
Platform.runLater(() -> {
if (c.getList().isEmpty()) {
table.getSelectionModel().clearSelection();
return;
}
var indices = c.getList().stream()
.skip(1)
.mapToInt(entry -> table.getItems().indexOf(entry))
.toArray();
table.getSelectionModel()
.selectIndices(table.getItems().indexOf(c.getList().get(0)), indices);
});
});
}
private void prepareTableEntries(TableView<FileSystem.FileEntry> table) {
private void prepareTableShortcuts(TableView<FileBrowserEntry> table) {
table.setOnKeyPressed(event -> {
var selected = fileList.getSelected();
BrowserAction.getFlattened().stream()
.filter(browserAction -> browserAction.isApplicable(fileList.getFileSystemModel(), selected)
&& browserAction.isActive(fileList.getFileSystemModel(), selected))
.filter(browserAction -> browserAction.getShortcut() != null)
.filter(browserAction -> browserAction.getShortcut().match(event))
.findAny()
.ifPresent(browserAction -> {
ThreadHelper.runFailableAsync(() -> {
browserAction.execute(fileList.getFileSystemModel(), selected);
});
});
});
}
private void prepareTableEntries(TableView<FileBrowserEntry> table) {
var emptyEntry = new FileListCompEntry(table, null, fileList);
table.setOnDragOver(event -> {
emptyEntry.onDragOver(event);
@ -203,7 +218,15 @@ final class FileListComp extends AnchorPane {
});
table.setRowFactory(param -> {
TableRow<FileSystem.FileEntry> row = new TableRow<>();
TableRow<FileBrowserEntry> row = new TableRow<>();
new ContextMenuAugment<>(false, () -> {
if (row.getItem() != null && row.getItem().isSynthetic()) {
return null;
}
return new FileContextMenu(fileList.getFileSystemModel(), row.getItem() == null);
})
.augment(new SimpleCompStructure<>(row));
var listEntry = Bindings.createObjectBinding(
() -> new FileListCompEntry(row, row.getItem(), fileList), row.itemProperty());
@ -214,8 +237,10 @@ final class FileListComp extends AnchorPane {
row.itemProperty().addListener((observable, oldValue, newValue) -> {
row.pseudoClassStateChanged(EMPTY, newValue == null);
row.pseudoClassStateChanged(FILE, newValue != null && !newValue.isDirectory());
row.pseudoClassStateChanged(FOLDER, newValue != null && newValue.isDirectory());
row.pseudoClassStateChanged(
FILE, newValue != null && !newValue.getRawFileEntry().isDirectory());
row.pseudoClassStateChanged(
FOLDER, newValue != null && newValue.getRawFileEntry().isDirectory());
});
fileList.getDraggedOverDirectory().addListener((observable, oldValue, newValue) -> {
@ -250,19 +275,19 @@ final class FileListComp extends AnchorPane {
return row;
});
}
private void prepareTableChanges(TableView<FileSystem.FileEntry> table, TableColumn<FileSystem.FileEntry, Instant> mtimeCol, TableColumn<FileSystem.FileEntry, String> modeCol) {
private void prepareTableChanges(
TableView<FileBrowserEntry> table,
TableColumn<FileBrowserEntry, Instant> mtimeCol,
TableColumn<FileBrowserEntry, String> modeCol) {
var lastDir = new SimpleObjectProperty<FileSystem.FileEntry>();
Runnable updateHandler = () -> {
PlatformThread.runLaterIfNeeded(() -> {
var newItems = new ArrayList<FileSystem.FileEntry>();
var parentEntry = fileList.getFileSystemModel().getCurrentParentDirectory();
if (parentEntry != null) {
newItems.add(parentEntry);
}
newItems.addAll(fileList.getShown().getValue());
var newItems = new ArrayList<>(fileList.getShown().getValue());
var hasModifiedDate =
newItems.size() == 0 || newItems.stream().anyMatch(entry -> entry.getDate() != null);
var hasModifiedDate = newItems.size() == 0
|| newItems.stream()
.anyMatch(entry -> entry.getRawFileEntry().getDate() != null);
if (!hasModifiedDate) {
table.getColumns().remove(mtimeCol);
} else {
@ -340,7 +365,7 @@ final class FileListComp extends AnchorPane {
}
}
private class FilenameCell extends TableCell<FileSystem.FileEntry, String> {
private class FilenameCell extends TableCell<FileBrowserEntry, String> {
private final StringProperty img = new SimpleStringProperty();
private final StringProperty text = new SimpleStringProperty();
@ -349,18 +374,17 @@ final class FileListComp extends AnchorPane {
.createRegion();
private final StackPane textField =
new LazyTextFieldComp(text).createStructure().get();
private final ChangeListener<String> listener;
private final BooleanProperty updating = new SimpleBooleanProperty();
public FilenameCell(Property<FileSystem.FileEntry> editing) {
public FilenameCell(Property<FileBrowserEntry> editing) {
editing.addListener((observable, oldValue, newValue) -> {
if (getTableRow().getItem() != null && getTableRow().getItem().equals(newValue)) {
textField.requestFocus();
}
});
listener = (observable, oldValue, newValue) -> {
ChangeListener<String> listener = (observable, oldValue, newValue) -> {
if (updating.get()) {
return;
}
@ -380,7 +404,7 @@ final class FileListComp extends AnchorPane {
return;
}
try (var b = new BusyProperty(updating)) {
try (var ignored = new BusyProperty(updating)) {
super.updateItem(fullPath, empty);
setText(null);
if (empty || getTableRow() == null || getTableRow().getItem() == null) {
@ -397,18 +421,20 @@ final class FileListComp extends AnchorPane {
var isParentLink = getTableRow()
.getItem()
.getRawFileEntry()
.equals(fileList.getFileSystemModel().getCurrentParentDirectory());
img.set(FileIconManager.getFileIcon(
isParentLink
? fileList.getFileSystemModel().getCurrentDirectory()
: getTableRow().getItem(),
: getTableRow().getItem().getRawFileEntry(),
isParentLink));
var isDirectory = getTableRow().getItem().isDirectory();
var isDirectory = getTableRow().getItem().getRawFileEntry().isDirectory();
pseudoClassStateChanged(FOLDER, isDirectory);
var fileName = isParentLink ? ".." : FileNames.getFileName(fullPath);
var hidden = !isParentLink && (getTableRow().getItem().isHidden() || fileName.startsWith("."));
var hidden = !isParentLink
&& (getTableRow().getItem().getRawFileEntry().isHidden() || fileName.startsWith("."));
getTableRow().pseudoClassStateChanged(HIDDEN, hidden);
text.set(fileName);
}
@ -416,7 +442,7 @@ final class FileListComp extends AnchorPane {
}
}
private class FileSizeCell extends TableCell<FileSystem.FileEntry, Number> {
private static class FileSizeCell extends TableCell<FileBrowserEntry, Number> {
@Override
protected void updateItem(Number fileSize, boolean empty) {
@ -425,7 +451,7 @@ final class FileListComp extends AnchorPane {
setText(null);
} else {
var path = getTableRow().getItem();
if (path.isDirectory()) {
if (path.getRawFileEntry().isDirectory()) {
setText("");
} else {
setText(byteCount(fileSize.longValue()));
@ -434,7 +460,7 @@ final class FileListComp extends AnchorPane {
}
}
private class FileModeCell extends TableCell<FileSystem.FileEntry, String> {
private static class FileModeCell extends TableCell<FileBrowserEntry, String> {
@Override
protected void updateItem(String mode, boolean empty) {
@ -447,7 +473,7 @@ final class FileListComp extends AnchorPane {
}
}
private static class FileTimeCell extends TableCell<FileSystem.FileEntry, Instant> {
private static class FileTimeCell extends TableCell<FileBrowserEntry, Instant> {
@Override
protected void updateItem(Instant fileTime, boolean empty) {

View file

@ -1,6 +1,5 @@
package io.xpipe.app.browser;
import io.xpipe.core.store.FileSystem;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.control.TableView;
@ -18,14 +17,13 @@ public class FileListCompEntry {
public static final Timer DROP_TIMER = new Timer("dnd", true);
private final Node row;
private final FileSystem.FileEntry item;
private final FileBrowserEntry item;
private final FileListModel model;
private Point2D lastOver = new Point2D(-1, -1);
private TimerTask activeTask;
private FileContextMenu currentContextMenu;
public FileListCompEntry(Node row, FileSystem.FileEntry item, FileListModel model) {
public FileListCompEntry(Node row, FileBrowserEntry item, FileListModel model) {
this.row = row;
this.item = item;
this.model = model;
@ -34,6 +32,7 @@ public class FileListCompEntry {
@SuppressWarnings("unchecked")
public void onMouseClick(MouseEvent t) {
if (item == null) {
model.getSelected().clear();
return;
}
@ -48,7 +47,7 @@ public class FileListCompEntry {
}
if (t.getButton() == MouseButton.PRIMARY && t.isShiftDown()) {
var tv = ((TableView<FileSystem.FileEntry>) row.getParent().getParent().getParent().getParent());
var tv = ((TableView<FileBrowserEntry>) row.getParent().getParent().getParent().getParent());
var all = tv.getItems();
var min = tv.getSelectionModel().getSelectedItems().stream().mapToInt(entry -> all.indexOf(entry)).min().orElse(1);
var max = tv.getSelectionModel().getSelectedItems().stream().mapToInt(entry -> all.indexOf(entry)).max().orElse(all.size() - 1);
@ -58,25 +57,10 @@ public class FileListCompEntry {
t.consume();
return;
}
if (currentContextMenu != null) {
currentContextMenu.hide();
currentContextMenu = null;
t.consume();
return;
}
if (t.getButton() == MouseButton.SECONDARY) {
var cm = new FileContextMenu(model.getFileSystemModel(), item, model.getEditing());
cm.show(row, t.getScreenX(), t.getScreenY());
currentContextMenu = cm;
t.consume();
return;
}
}
public boolean isSynthetic() {
return item != null && item.equals(model.getFileSystemModel().getCurrentParentDirectory());
return item != null && item.getRawFileEntry().equals(model.getFileSystemModel().getCurrentParentDirectory());
}
private boolean acceptsDrop(DragEvent event) {
@ -96,7 +80,7 @@ public class FileListCompEntry {
// Prevent drag and drops of files into the current directory
if (FileBrowserClipboard.currentDragClipboard
.getBaseDirectory().getPath()
.equals(model.getFileSystemModel().getCurrentDirectory().getPath()) && (item == null || !item.isDirectory())) {
.equals(model.getFileSystemModel().getCurrentDirectory().getPath()) && (item == null || !item.getRawFileEntry().isDirectory())) {
return false;
}
@ -116,8 +100,8 @@ public class FileListCompEntry {
if (event.getGestureSource() == null && event.getDragboard().hasFiles()) {
Dragboard db = event.getDragboard();
var list = db.getFiles().stream().map(File::toPath).toList();
var target = item != null && item.isDirectory()
? item
var target = item != null && item.getRawFileEntry().isDirectory()
? item.getRawFileEntry()
: model.getFileSystemModel().getCurrentDirectory();
model.getFileSystemModel().dropLocalFilesIntoAsync(target, list);
event.setDropCompleted(true);
@ -127,8 +111,8 @@ public class FileListCompEntry {
// Accept drops from inside the app window
if (event.getGestureSource() != null) {
var files = FileBrowserClipboard.retrieveDrag(event.getDragboard()).getEntries();
var target = item != null && item.isDirectory()
? item
var target = item != null && item.getRawFileEntry().isDirectory()
? item.getRawFileEntry()
: model.getFileSystemModel().getCurrentDirectory();
model.getFileSystemModel().dropFilesIntoAsync(target, files, false);
event.setDropCompleted(true);
@ -137,7 +121,7 @@ public class FileListCompEntry {
}
public void onDragExited(DragEvent event) {
if (item != null && item.isDirectory()) {
if (item != null && item.getRawFileEntry().isDirectory()) {
model.getDraggedOverDirectory().setValue(null);
} else {
model.getDraggedOverEmpty().setValue(false);
@ -154,7 +138,7 @@ public class FileListCompEntry {
return;
}
var selected = model.getSelected();
var selected = model.getSelectedRaw();
Dragboard db = row.startDragAndDrop(TransferMode.COPY);
db.setContent(FileBrowserClipboard.startDrag(model.getFileSystemModel().getCurrentDirectory(), selected));
@ -166,13 +150,13 @@ public class FileListCompEntry {
}
private void acceptDrag(DragEvent event) {
model.getDraggedOverEmpty().setValue(item == null || !item.isDirectory());
model.getDraggedOverEmpty().setValue(item == null || !item.getRawFileEntry().isDirectory());
model.getDraggedOverDirectory().setValue(item);
event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
}
private void handleHoverTimer(DragEvent event) {
if (item == null || !item.isDirectory()) {
if (item == null || !item.getRawFileEntry().isDirectory()) {
return;
}
@ -192,7 +176,7 @@ public class FileListCompEntry {
return;
}
model.getFileSystemModel().cd(item.getPath());
model.getFileSystemModel().cd(item.getRawFileEntry().getPath());
}
};
DROP_TIMER.schedule(activeTask, 1000);

View file

@ -2,6 +2,7 @@
package io.xpipe.app.browser;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.FileOpener;
import io.xpipe.core.impl.FileNames;
@ -22,25 +23,27 @@ import java.util.function.Predicate;
import java.util.stream.Stream;
@Getter
final class FileListModel {
public final class FileListModel {
static final Comparator<FileSystem.FileEntry> FILE_TYPE_COMPARATOR =
Comparator.comparing(path -> !path.isDirectory());
static final Predicate<FileSystem.FileEntry> PREDICATE_ANY = path -> true;
static final Predicate<FileSystem.FileEntry> PREDICATE_NOT_HIDDEN = path -> true;
static final Comparator<FileBrowserEntry> FILE_TYPE_COMPARATOR =
Comparator.comparing(path -> !path.getRawFileEntry().isDirectory());
static final Predicate<FileBrowserEntry> PREDICATE_ANY = path -> true;
static final Predicate<FileBrowserEntry> PREDICATE_NOT_HIDDEN = path -> true;
private final OpenFileSystemModel fileSystemModel;
private final Property<Comparator<FileSystem.FileEntry>> comparatorProperty =
private final Property<Comparator<FileBrowserEntry>> comparatorProperty =
new SimpleObjectProperty<>(FILE_TYPE_COMPARATOR);
private final Property<List<FileSystem.FileEntry>> all = new SimpleObjectProperty<>(new ArrayList<>());
private final Property<List<FileSystem.FileEntry>> shown = new SimpleObjectProperty<>(new ArrayList<>());
private final ObjectProperty<Predicate<FileSystem.FileEntry>> predicateProperty =
private final Property<List<FileBrowserEntry>> all = new SimpleObjectProperty<>(new ArrayList<>());
private final Property<List<FileBrowserEntry>> shown = new SimpleObjectProperty<>(new ArrayList<>());
private final ObjectProperty<Predicate<FileBrowserEntry>> predicateProperty =
new SimpleObjectProperty<>(path -> true);
private final ObservableList<FileSystem.FileEntry> selected = FXCollections.observableArrayList();
private final ObservableList<FileBrowserEntry> selected = FXCollections.observableArrayList();
private final ObservableList<FileSystem.FileEntry> selectedRaw =
BindingsHelper.mappedContentBinding(selected, entry -> entry.getRawFileEntry());
private final Property<FileSystem.FileEntry> draggedOverDirectory = new SimpleObjectProperty<FileSystem.FileEntry>();
private final Property<FileBrowserEntry> draggedOverDirectory = new SimpleObjectProperty<FileBrowserEntry>();
private final Property<Boolean> draggedOverEmpty = new SimpleBooleanProperty();
private final Property<FileSystem.FileEntry> editing = new SimpleObjectProperty<>();
private final Property<FileBrowserEntry> editing = new SimpleObjectProperty<>();
public FileListModel(OpenFileSystemModel fileSystemModel) {
this.fileSystemModel = fileSystemModel;
@ -54,35 +57,42 @@ final class FileListModel {
return fileSystemModel.getBrowserModel().getMode();
}
public void setAll(List<FileSystem.FileEntry> newFiles) {
all.setValue(newFiles);
refreshShown();
}
public void setAll(Stream<FileSystem.FileEntry> newFiles) {
try (var s = newFiles) {
var l = s.filter(entry -> entry != null).limit(5000).toList();
var parent = fileSystemModel.getCurrentParentDirectory();
var l = Stream.concat(
parent != null ? Stream.of(new FileBrowserEntry(parent, this, true)) : Stream.of(),
s.filter(entry -> entry != null)
.limit(5000)
.map(entry -> new FileBrowserEntry(entry, this, false)))
.toList();
all.setValue(l);
refreshShown();
}
}
public void setComparator(Comparator<FileSystem.FileEntry> comparator) {
public void setComparator(Comparator<FileBrowserEntry> comparator) {
comparatorProperty.setValue(comparator);
refreshShown();
}
private void refreshShown() {
List<FileSystem.FileEntry> filtered = fileSystemModel.getFilter().getValue() != null ? all.getValue().stream().filter(entry -> {
var name = FileNames.getFileName(entry.getPath()).toLowerCase(Locale.ROOT);
var filterString = fileSystemModel.getFilter().getValue().toLowerCase(Locale.ROOT);
return name.contains(filterString);
}).toList() : all.getValue();
List<FileBrowserEntry> filtered = fileSystemModel.getFilter().getValue() != null
? all.getValue().stream()
.filter(entry -> {
var name = FileNames.getFileName(
entry.getRawFileEntry().getPath())
.toLowerCase(Locale.ROOT);
var filterString =
fileSystemModel.getFilter().getValue().toLowerCase(Locale.ROOT);
return name.contains(filterString);
})
.toList()
: all.getValue();
Comparator<FileSystem.FileEntry> tableComparator = comparatorProperty.getValue();
var comparator = tableComparator != null
? FILE_TYPE_COMPARATOR.thenComparing(tableComparator)
: FILE_TYPE_COMPARATOR;
Comparator<FileBrowserEntry> tableComparator = comparatorProperty.getValue();
var comparator =
tableComparator != null ? FILE_TYPE_COMPARATOR.thenComparing(tableComparator) : FILE_TYPE_COMPARATOR;
var listCopy = new ArrayList<>(filtered);
listCopy.sort(comparator);
shown.setValue(listCopy);
@ -101,23 +111,23 @@ final class FileListModel {
}
}
public void onDoubleClick(FileSystem.FileEntry entry) {
if (!entry.isDirectory() && getMode().equals(FileBrowserModel.Mode.SINGLE_FILE_CHOOSER)) {
public void onDoubleClick(FileBrowserEntry entry) {
if (!entry.getRawFileEntry().isDirectory() && getMode().equals(FileBrowserModel.Mode.SINGLE_FILE_CHOOSER)) {
getFileSystemModel().getBrowserModel().finishChooser();
return;
}
if (entry.isDirectory()) {
var dir = fileSystemModel.cd(entry.getPath());
if (entry.getRawFileEntry().isDirectory()) {
var dir = fileSystemModel.cd(entry.getRawFileEntry().getPath());
if (dir.isPresent()) {
fileSystemModel.cd(dir.get());
}
} else {
FileOpener.openInTextEditor(entry);
FileOpener.openInTextEditor(entry.getRawFileEntry());
}
}
public ObjectProperty<Predicate<FileSystem.FileEntry>> predicateProperty() {
public ObjectProperty<Predicate<FileBrowserEntry>> predicateProperty() {
return predicateProperty;
}
}

View file

@ -0,0 +1,29 @@
package io.xpipe.app.browser;
import io.xpipe.app.util.ApplicationHelper;
import java.util.HashMap;
import java.util.Map;
public class OpenFileSystemCache {
private final OpenFileSystemModel model;
private final Map<String, Boolean> installedApplications = new HashMap<>();
public OpenFileSystemCache(OpenFileSystemModel model) {
this.model = model;
}
public boolean isApplicationInPath(String app) {
if (!installedApplications.containsKey(app)) {
try {
var b = ApplicationHelper.isInPath(model.getFileSystem().getShell().orElseThrow(), app);
installedApplications.put(app, b);
} catch (Exception e) {
installedApplications.put(app, false);
}
}
return installedApplications.get(app);
}
}

View file

@ -14,6 +14,7 @@ import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.layout.*;
import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon;
@ -66,7 +67,8 @@ public class OpenFileSystemComp extends SimpleComp {
addBtn.disableProperty().bind(PlatformThread.sync(model.getNoDirectory()));
Shortcuts.addShortcut(addBtn, new KeyCodeCombination(KeyCode.PLUS));
var filter = new FileFilterComp(model.getFilter()).createRegion();
var filter = new FileFilterComp(model.getFilter()).createStructure();
Shortcuts.addShortcut(filter.toggleButton(), new KeyCodeCombination(KeyCode.F, KeyCombination.SHORTCUT_DOWN));
var topBar = new ToolBar();
topBar.getItems().setAll(
@ -74,7 +76,7 @@ public class OpenFileSystemComp extends SimpleComp {
forthBtn,
new Spacer(10),
pathBar,
filter,
filter.get(),
refreshBtn,
terminalBtn,
addBtn

View file

@ -2,12 +2,15 @@
package io.xpipe.app.browser;
import io.xpipe.app.core.AppCache;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.BusyProperty;
import io.xpipe.app.util.TerminalHelper;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.app.util.XPipeDaemon;
import io.xpipe.core.impl.FileNames;
import io.xpipe.core.process.ShellControl;
import io.xpipe.core.store.ConnectionFileSystem;
import io.xpipe.core.store.FileSystem;
import io.xpipe.core.store.FileSystemStore;
@ -15,6 +18,7 @@ import io.xpipe.core.store.ShellStore;
import javafx.beans.property.*;
import lombok.Getter;
import lombok.SneakyThrows;
import org.apache.commons.lang3.function.FailableConsumer;
import java.io.IOException;
import java.nio.file.Path;
@ -22,9 +26,11 @@ import java.time.Instant;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Stream;
@Getter
final class OpenFileSystemModel {
public final class OpenFileSystemModel {
private Property<FileSystemStore> store = new SimpleObjectProperty<>();
private FileSystem fileSystem;
@ -35,21 +41,56 @@ final class OpenFileSystemModel {
private final BooleanProperty busy = new SimpleBooleanProperty();
private final FileBrowserModel browserModel;
private final BooleanProperty noDirectory = new SimpleBooleanProperty();
private final Property<OpenFileSystemSavedState> savedState = new SimpleObjectProperty<>();
private final OpenFileSystemCache cache = new OpenFileSystemCache(this);
public OpenFileSystemModel(FileBrowserModel browserModel) {
this.browserModel = browserModel;
fileList = new FileListModel(this);
addListeners();
}
public void withShell(FailableConsumer<ShellControl, Exception> c, boolean refresh) {
ThreadHelper.runFailableAsync(() -> {
if (fileSystem == null) {
return;
}
BusyProperty.execute(busy, () -> {
if (store.getValue() instanceof ShellStore s) {
c.accept(fileSystem.getShell().orElseThrow());
if (refresh) {
refreshSync();
}
}
});
});
}
private void addListeners() {
savedState.addListener((observable, oldValue, newValue) -> {
if (store.getValue() == null) {
return;
}
var storageEntry = DataStorage.get().getStoreEntryIfPresent(store.getValue());
storageEntry.ifPresent(entry -> AppCache.update("browser-state-" + entry.getUuid(), newValue));
});
currentPath.addListener((observable, oldValue, newValue) -> {
savedState.setValue(savedState.getValue().withLastDirectory(newValue));
});
}
@SneakyThrows
public void refresh() {
BusyProperty.execute(busy, () -> {
cdSync(currentPath.get());
cdSyncWithoutCheck(currentPath.get());
});
}
private void refreshInternal() throws Exception {
cdSync(currentPath.get());
public void refreshSync() throws Exception {
cdSyncWithoutCheck(currentPath.get());
}
public FileSystem.FileEntry getCurrentParentDirectory() {
@ -93,13 +134,13 @@ final class OpenFileSystemModel {
ThreadHelper.runFailableAsync(() -> {
try (var ignored = new BusyProperty(busy)) {
cdSync(path);
cdSyncWithoutCheck(path);
}
});
return Optional.empty();
}
private void cdSync(String path) throws Exception {
private void cdSyncWithoutCheck(String path) throws Exception {
if (fileSystem == null) {
var fs = store.getValue().createFileSystem();
fs.open();
@ -111,6 +152,7 @@ final class OpenFileSystemModel {
filter.setValue(null);
currentPath.set(path);
savedState.setValue(savedState.getValue().withLastDirectory(path));
history.updateCurrent(path);
loadFilesSync(path);
}
@ -130,7 +172,7 @@ final class OpenFileSystemModel {
}
return true;
} catch (Exception e) {
fileList.setAll(List.of());
fileList.setAll(Stream.of());
ErrorEvent.fromThrowable(e).handle();
return false;
}
@ -144,7 +186,7 @@ final class OpenFileSystemModel {
}
FileSystemHelper.dropLocalFilesInto(entry, files);
refreshInternal();
refreshSync();
});
});
}
@ -165,7 +207,7 @@ final class OpenFileSystemModel {
}
FileSystemHelper.dropFilesInto(target, files, explicitCopy);
refreshInternal();
refreshSync();
});
});
}
@ -191,7 +233,7 @@ final class OpenFileSystemModel {
}
fileSystem.mkdirs(abs);
refreshInternal();
refreshSync();
});
});
}
@ -213,7 +255,7 @@ final class OpenFileSystemModel {
var abs = FileNames.join(getCurrentDirectory().getPath(), name);
fileSystem.touch(abs);
refreshInternal();
refreshSync();
});
});
}
@ -225,12 +267,12 @@ final class OpenFileSystemModel {
return;
}
if (!FileBrowserAlerts.showDeleteAlert(fileList.getSelected())) {
if (!FileBrowserAlerts.showDeleteAlert(fileList.getSelectedRaw())) {
return;
}
FileSystemHelper.delete(fileList.getSelected());
refreshInternal();
FileSystemHelper.delete(fileList.getSelectedRaw());
refreshSync();
});
});
}
@ -257,8 +299,22 @@ final class OpenFileSystemModel {
fs.open();
this.fileSystem = fs;
var current = FileSystemHelper.getStartDirectory(this);
cdSync(current);
var storageEntry = DataStorage.get()
.getStoreEntryIfPresent(fileSystem)
.map(entry -> entry.getUuid())
.orElse(UUID.randomUUID());
this.savedState.setValue(
AppCache.get("browser-state-" + storageEntry, OpenFileSystemSavedState.class, () -> {
try {
return OpenFileSystemSavedState.builder()
.lastDirectory(FileSystemHelper.getStartDirectory(this))
.build();
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();
return null;
}
}));
cdSyncWithoutCheck(this.savedState.getValue().getLastDirectory());
});
}

View file

@ -0,0 +1,15 @@
package io.xpipe.app.browser;
import lombok.Builder;
import lombok.Value;
import lombok.With;
import lombok.extern.jackson.Jacksonized;
@Value
@With
@Jacksonized
@Builder
public class OpenFileSystemSavedState {
String lastDirectory;
}

View file

@ -0,0 +1,27 @@
package io.xpipe.app.browser.action;
import io.xpipe.app.browser.FileBrowserEntry;
import io.xpipe.app.browser.OpenFileSystemModel;
import java.util.List;
public interface ApplicationPathAction extends BrowserAction {
public abstract String getExecutable();
@Override
public default boolean isApplicable(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
if (entries.size() == 0) {
return false;
}
return entries.stream().allMatch(entry -> isApplicable(model, entry));
}
boolean isApplicable(OpenFileSystemModel model, FileBrowserEntry entry);
@Override
public default boolean isActive(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return model.getCache().isApplicationInPath(getExecutable());
}
}

View file

@ -0,0 +1,8 @@
package io.xpipe.app.browser.action;
import java.util.List;
public interface BranchAction extends BrowserAction {
List<LeafAction> getBranchingActions();
}

View file

@ -0,0 +1,87 @@
package io.xpipe.app.browser.action;
import io.xpipe.app.browser.FileBrowserEntry;
import io.xpipe.app.browser.OpenFileSystemModel;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.core.util.ModuleLayerLoader;
import javafx.scene.Node;
import javafx.scene.input.KeyCombination;
import java.util.ArrayList;
import java.util.List;
import java.util.ServiceLoader;
public interface BrowserAction {
static enum Category {
CUSTOM,
OPEN,
COPY_PASTE,
MUTATION
}
static List<BrowserAction> ALL = new ArrayList<>();
public static List<LeafAction> getFlattened() {
return ALL.stream()
.map(browserAction -> browserAction instanceof LeafAction
? List.of((LeafAction) browserAction)
: ((BranchAction) browserAction).getBranchingActions())
.flatMap(List::stream)
.toList();
}
default Node getIcon(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return null;
}
default Category getCategory() {
return null;
}
default KeyCombination getShortcut() {
return null;
}
default boolean acceptsEmptySelection() {
return false;
}
public abstract String getName(OpenFileSystemModel model, List<FileBrowserEntry> entries);
public default boolean isApplicable(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return true;
}
public default boolean isActive(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return true;
}
public static class Loader implements ModuleLayerLoader {
@Override
public void init(ModuleLayer layer) {
ALL.addAll(ServiceLoader.load(layer, BrowserAction.class).stream()
.map(actionProviderProvider -> actionProviderProvider.get())
.filter(provider -> {
try {
return true;
} catch (Throwable e) {
ErrorEvent.fromThrowable(e).handle();
return false;
}
})
.toList());
}
@Override
public boolean requiresFullDaemon() {
return true;
}
@Override
public boolean prioritizeLoading() {
return false;
}
}
}

View file

@ -0,0 +1,29 @@
package io.xpipe.app.browser.action;
import io.xpipe.app.browser.FileBrowserEntry;
import io.xpipe.app.browser.OpenFileSystemModel;
import io.xpipe.app.util.ScriptHelper;
import io.xpipe.core.process.ShellControl;
import java.util.List;
public abstract class ExecuteApplicationAction implements LeafAction, ApplicationPathAction {
@Override
public void execute(OpenFileSystemModel model, List<FileBrowserEntry> entries) throws Exception {
ShellControl sc = model.getFileSystem().getShell().orElseThrow();
for (FileBrowserEntry entry : entries) {
var command = detach() ? ScriptHelper.createDetachCommand(sc, createCommand(model, entry)) : createCommand(model, entry);
try (var cc = sc.command(command).workingDirectory(model.getCurrentDirectory().getPath()).start()) {
cc.discardOrThrow();
}
}
}
protected boolean detach() {
return false;
}
protected abstract String createCommand(OpenFileSystemModel model, FileBrowserEntry entry);
}

View file

@ -0,0 +1,12 @@
package io.xpipe.app.browser.action;
import io.xpipe.app.browser.FileBrowserEntry;
import io.xpipe.app.browser.OpenFileSystemModel;
import java.util.List;
public interface LeafAction extends BrowserAction {
public abstract void execute(OpenFileSystemModel model, List<FileBrowserEntry> entries) throws Exception;
}

View file

@ -0,0 +1,96 @@
package io.xpipe.app.browser.action;
import io.xpipe.app.browser.FileBrowserEntry;
import io.xpipe.app.browser.OpenFileSystemModel;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.ScriptHelper;
import io.xpipe.app.util.TerminalHelper;
import io.xpipe.core.impl.FileNames;
import io.xpipe.core.process.ShellControl;
import org.apache.commons.io.FilenameUtils;
import java.util.List;
public abstract class MultiExecuteAction implements BranchAction {
protected String filesArgument(List<FileBrowserEntry> entries) {
return entries.size() == 1 ? entries.get(0).getOptionallyQuotedFileName() : "(" + entries.size() + ")";
}
protected abstract String createCommand(ShellControl sc, OpenFileSystemModel model, FileBrowserEntry entry);
@Override
public List<LeafAction> getBranchingActions() {
return List.of(
new LeafAction() {
@Override
public void execute(OpenFileSystemModel model, List<FileBrowserEntry> entries) throws Exception {
model.withShell(
pc -> {
for (FileBrowserEntry entry : entries) {
var cmd = pc.command(createCommand(pc, model, entry))
.workingDirectory(model.getCurrentDirectory()
.getPath())
.prepareTerminalOpen(FileNames.getFileName(
entry.getRawFileEntry().getPath()));
TerminalHelper.open(
FilenameUtils.getBaseName(
entry.getRawFileEntry().getPath()),
cmd);
}
},
false);
}
@Override
public String getName(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return "in " + AppPrefs.get().terminalType().getValue().toTranslatedString();
}
},
new LeafAction() {
@Override
public void execute(OpenFileSystemModel model, List<FileBrowserEntry> entries) throws Exception {
model.withShell(
pc -> {
for (FileBrowserEntry entry : entries) {
var cmd = ScriptHelper.createDetachCommand(
pc, createCommand(pc, model, entry));
pc.command(cmd)
.workingDirectory(model.getCurrentDirectory()
.getPath())
.execute();
}
},
false);
}
@Override
public String getName(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return "in background";
}
},
new LeafAction() {
@Override
public void execute(OpenFileSystemModel model, List<FileBrowserEntry> entries) throws Exception {
model.withShell(
pc -> {
for (FileBrowserEntry entry : entries) {
pc.command(createCommand(pc, model, entry))
.workingDirectory(model.getCurrentDirectory()
.getPath())
.execute();
}
},
false);
}
@Override
public String getName(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return "wait for completion";
}
});
}
}

View file

@ -0,0 +1,101 @@
package io.xpipe.app.browser.icon;
import io.xpipe.app.core.AppResources;
import io.xpipe.core.impl.FileNames;
import io.xpipe.core.store.FileSystem;
import lombok.Getter;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public interface DirectoryType {
List<DirectoryType> ALL = new ArrayList<>();
static DirectoryType byId(String id) {
return ALL.stream().filter(fileType -> fileType.getId().equals(id)).findAny().orElseThrow();
}
public static void loadDefinitions() {
ALL.add(new Simple(
"default", new IconVariant("default_root_folder.svg"), new IconVariant("default_root_folder_opened.svg"), ""));
AppResources.with(AppResources.XPIPE_MODULE, "folder_list.txt", path -> {
try (var reader =
new BufferedReader(new InputStreamReader(Files.newInputStream(path), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
var split = line.split("\\|");
var id = split[0].trim();
var filter = Arrays.stream(split[1].split(","))
.map(s -> {
var r = s.trim();
if (r.startsWith(".")) {
return r;
}
if (r.contains(".")) {
return r;
}
return "." + r;
})
.toList();
var closedIcon = split[2].trim();
var openIcon = split[3].trim();
var lightClosedIcon = split.length > 4 ? split[4].trim() : closedIcon;
var lightOpenIcon = split.length > 4 ? split[5].trim() : openIcon;
ALL.add(new Simple(
id, new IconVariant(lightClosedIcon, closedIcon),
new IconVariant(lightOpenIcon, openIcon),
filter.toArray(String[]::new)));
}
}
});
}
class Simple implements DirectoryType {
@Getter
private final String id;
private final IconVariant closed;
private final IconVariant open;
private final String[] names;
public Simple(String id, IconVariant closed, IconVariant open, String... names) {
this.id = id;
this.closed = closed;
this.open = open;
this.names = names;
}
@Override
public boolean matches(FileSystem.FileEntry entry) {
if (!entry.isDirectory()) {
return false;
}
return Arrays.stream(names).anyMatch(name -> FileNames.getFileName(entry.getPath())
.equalsIgnoreCase(name));
}
@Override
public String getIcon(FileSystem.FileEntry entry, boolean open) {
return open ? this.open.getIcon() : this.closed.getIcon();
}
}
String getId();
boolean matches(FileSystem.FileEntry entry);
String getIcon(FileSystem.FileEntry entry, boolean open);
}

View file

@ -1,29 +0,0 @@
package io.xpipe.app.browser.icon;
import io.xpipe.core.store.FileSystem;
import java.util.Arrays;
public interface FileIconFactory {
class SimpleFile extends IconVariant implements FileIconFactory {
private final String[] endings;
public SimpleFile(String lightIcon, String darkIcon, String... endings) {
super(lightIcon, darkIcon);
this.endings = endings;
}
@Override
public String getIcon(FileSystem.FileEntry entry) {
if (entry.isDirectory()) {
return null;
}
return Arrays.stream(endings).anyMatch(ending -> entry.getPath().toLowerCase().endsWith(ending.toLowerCase())) ? getIcon() : null;
}
}
String getIcon(FileSystem.FileEntry entry);
}

View file

@ -7,89 +7,14 @@ import io.xpipe.core.store.FileSystem;
import javafx.scene.image.Image;
import lombok.Getter;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.*;
public class FileIconManager {
private static final List<FileIconFactory> factories = new ArrayList<>();
private static final List<FolderIconFactory> folderFactories = new ArrayList<>();
@Getter
private static SvgCache svgCache = createCache();
private static boolean loaded;
private static void loadDefinitions() {
AppResources.with(AppResources.XPIPE_MODULE, "browser_icons/file_list.txt", path -> {
try (var reader =
new BufferedReader(new InputStreamReader(Files.newInputStream(path), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
var split = line.split("\\|");
var id = split[0].trim();
var filter = Arrays.stream(split[1].split(","))
.map(s -> {
var r = s.trim();
if (r.startsWith(".")) {
return r;
}
if (r.contains(".")) {
return r;
}
return "." + r;
})
.toList();
var darkIcon = split[2].trim();
var lightIcon = split.length > 3 ? split[3].trim() : darkIcon;
factories.add(new FileIconFactory.SimpleFile(lightIcon, darkIcon, filter.toArray(String[]::new)));
}
}
});
folderFactories.addAll(List.of(new FolderIconFactory.SimpleDirectory(
new IconVariant("default_root_folder.svg"), new IconVariant("default_root_folder_opened.svg"), "")));
AppResources.with(AppResources.XPIPE_MODULE, "browser_icons/folder_list.txt", path -> {
try (var reader =
new BufferedReader(new InputStreamReader(Files.newInputStream(path), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
var split = line.split("\\|");
var id = split[0].trim();
var filter = Arrays.stream(split[1].split(","))
.map(s -> {
var r = s.trim();
if (r.startsWith(".")) {
return r;
}
if (r.contains(".")) {
return r;
}
return "." + r;
})
.toList();
var closedIcon = split[2].trim();
var openIcon = split[3].trim();
var lightClosedIcon = split.length > 4 ? split[4].trim() : closedIcon;
var lightOpenIcon = split.length > 4 ? split[5].trim() : openIcon;
folderFactories.add(new FolderIconFactory.SimpleDirectory(
new IconVariant(lightClosedIcon, closedIcon),
new IconVariant(lightOpenIcon, openIcon),
filter.toArray(String[]::new)));
}
}
});
}
private static SvgCache createCache() {
return new SvgCache() {
@ -109,7 +34,6 @@ public class FileIconManager {
public static synchronized void loadIfNecessary() {
if (!loaded) {
loadDefinitions();
AppImages.loadDirectory(AppResources.XPIPE_MODULE, "browser_icons");
loaded = true;
}
@ -123,17 +47,15 @@ public class FileIconManager {
loadIfNecessary();
if (!entry.isDirectory()) {
for (var f : factories) {
var icon = f.getIcon(entry);
if (icon != null) {
return getIconPath(icon);
for (var f : FileType.ALL) {
if (f.matches(entry)) {
return getIconPath(f.getIcon());
}
}
} else {
for (var f : folderFactories) {
var icon = f.getIcon(entry, open);
if (icon != null) {
return getIconPath(icon);
for (var f : DirectoryType.ALL) {
if (f.matches(entry)) {
return getIconPath(f.getIcon(entry, open));
}
}
}

View file

@ -6,6 +6,10 @@ import javafx.beans.property.SimpleStringProperty;
public class FileIcons {
public static PrettyImageComp createIcon(FileType type) {
return new PrettyImageComp(new SimpleStringProperty(type.getIcon()), 22, 22);
}
public static PrettyImageComp createIcon(FileSystem.FileEntry entry) {
return new PrettyImageComp(new SimpleStringProperty(FileIconManager.getFileIcon(entry, false)), 22, 22);
}

View file

@ -0,0 +1,87 @@
package io.xpipe.app.browser.icon;
import io.xpipe.app.core.AppResources;
import io.xpipe.core.store.FileSystem;
import lombok.Getter;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public interface FileType {
List<FileType> ALL = new ArrayList<>();
static FileType byId(String id) {
return ALL.stream().filter(fileType -> fileType.getId().equals(id)).findAny().orElseThrow();
}
public static void loadDefinitions() {
AppResources.with(AppResources.XPIPE_MODULE, "file_list.txt", path -> {
try (var reader =
new BufferedReader(new InputStreamReader(Files.newInputStream(path), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
var split = line.split("\\|");
var id = split[0].trim();
var filter = Arrays.stream(split[1].split(","))
.map(s -> {
var r = s.trim();
if (r.startsWith(".")) {
return r;
}
if (r.contains(".")) {
return r;
}
return "." + r;
})
.toList();
var darkIcon = split[2].trim();
var lightIcon = split.length > 3 ? split[3].trim() : darkIcon;
ALL.add(new FileType.Simple(id, lightIcon, darkIcon, filter.toArray(String[]::new)));
}
}
});
}
@Getter
class Simple implements FileType {
private final String id;
private final IconVariant icon;
private final String[] endings;
public Simple(String id, String lightIcon, String darkIcon, String... endings) {
this.icon = new IconVariant(lightIcon, darkIcon);
this.id = id;
this.endings = endings;
}
@Override
public boolean matches(FileSystem.FileEntry entry) {
if (entry.isDirectory()) {
return false;
}
return Arrays.stream(endings)
.anyMatch(ending -> entry.getPath().toLowerCase().endsWith(ending.toLowerCase()));
}
@Override
public String getIcon() {
return icon.getIcon();
}
}
String getId();
boolean matches(FileSystem.FileEntry entry);
String getIcon();
}

View file

@ -1,36 +0,0 @@
package io.xpipe.app.browser.icon;
import io.xpipe.core.impl.FileNames;
import io.xpipe.core.store.FileSystem;
import java.util.Arrays;
public interface FolderIconFactory {
class SimpleDirectory implements FolderIconFactory {
private final IconVariant closed;
private final IconVariant open;
private final String[] names;
public SimpleDirectory(IconVariant closed, IconVariant open, String... names) {
this.closed = closed;
this.open = open;
this.names = names;
}
@Override
public String getIcon(FileSystem.FileEntry entry, boolean open) {
if (!entry.isDirectory()) {
return null;
}
return Arrays.stream(names).anyMatch(name -> FileNames.getFileName(entry.getPath())
.equalsIgnoreCase(name))
? (open ? this.open.getIcon() : this.closed.getIcon())
: null;
}
}
String getIcon(FileSystem.FileEntry entry, boolean open);
}

View file

@ -78,7 +78,7 @@ public class AppLayoutComp extends Comp<CompStructure<BorderPane>> {
var pane = new BorderPane();
var sidebar = new SideMenuBarComp(selected, entries);
pane.setCenter(selected.getValue().comp().createRegion());
pane.setCenter(map.get(selected.getValue()));
pane.setRight(sidebar.createRegion());
selected.addListener((c, o, n) -> {
if (o != null && o.equals(entries.get(2))) {

View file

@ -3,7 +3,7 @@ package io.xpipe.app.comp.storage.collection;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppWindowHelper;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.augment.PopupMenuAugment;
import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.DesktopHelper;
import javafx.scene.control.Alert;
@ -13,19 +13,14 @@ import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.layout.Region;
import org.kordamp.ikonli.javafx.FontIcon;
public class SourceCollectionContextMenu<S extends CompStructure<?>> extends PopupMenuAugment<S> {
private final SourceCollectionWrapper group;
private final Region renameTextField;
public class SourceCollectionContextMenu<S extends CompStructure<?>> extends ContextMenuAugment<S> {
public SourceCollectionContextMenu(
boolean showOnPrimaryButton, SourceCollectionWrapper group, Region renameTextField) {
super(showOnPrimaryButton);
this.group = group;
this.renameTextField = renameTextField;
super(showOnPrimaryButton, () -> createContextMenu(group, renameTextField));
}
private void onDelete() {
private static void onDelete(SourceCollectionWrapper group) {
if (group.getEntries().size() > 0) {
AppWindowHelper.showBlockingAlert(alert -> {
alert.setTitle(AppI18n.get("confirmCollectionDeletionTitle"));
@ -44,7 +39,7 @@ public class SourceCollectionContextMenu<S extends CompStructure<?>> extends Pop
}
}
private void onClean() {
private static void onClean(SourceCollectionWrapper group) {
if (group.getEntries().size() > 0) {
AppWindowHelper.showBlockingAlert(alert -> {
alert.setTitle(AppI18n.get("confirmCollectionDeletionTitle"));
@ -63,8 +58,7 @@ public class SourceCollectionContextMenu<S extends CompStructure<?>> extends Pop
}
}
@Override
protected ContextMenu createContextMenu() {
protected static ContextMenu createContextMenu(SourceCollectionWrapper group, Region renameTextField) {
var cm = new ContextMenu();
var name = new MenuItem(group.getName());
name.setDisable(true);
@ -96,13 +90,13 @@ public class SourceCollectionContextMenu<S extends CompStructure<?>> extends Pop
if (group.isDeleteable()) {
var del = new MenuItem(AppI18n.get("delete"), new FontIcon("mdal-delete_outline"));
del.setOnAction(e -> {
onDelete();
onDelete(group);
});
cm.getItems().add(del);
} else {
var del = new MenuItem(AppI18n.get("clean"), new FontIcon("mdal-delete_outline"));
del.setOnAction(e -> {
onClean();
onClean(group);
});
cm.getItems().add(del);
}

View file

@ -3,7 +3,7 @@ package io.xpipe.app.comp.storage.source;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.augment.PopupMenuAugment;
import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataSourceEntry;
@ -16,19 +16,14 @@ import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.layout.Region;
import org.kordamp.ikonli.javafx.FontIcon;
public class SourceEntryContextMenu<S extends CompStructure<?>> extends PopupMenuAugment<S> {
public class SourceEntryContextMenu<S extends CompStructure<?>> extends ContextMenuAugment<S> {
private final SourceEntryWrapper entry;
private final Region renameTextField;
public SourceEntryContextMenu(boolean showOnPrimaryButton, SourceEntryWrapper entry, Region renameTextField) {
super(showOnPrimaryButton);
this.entry = entry;
this.renameTextField = renameTextField;
super(showOnPrimaryButton, () -> createContextMenu(entry, renameTextField));
}
@Override
protected ContextMenu createContextMenu() {
protected static ContextMenu createContextMenu(SourceEntryWrapper entry, Region renameTextField) {
var cm = new ContextMenu();
AppFont.normal(cm.getStyleableNode());

View file

@ -9,7 +9,7 @@ import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.augment.GrowAugment;
import io.xpipe.app.fxcomps.augment.PopupMenuAugment;
import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
import io.xpipe.app.fxcomps.impl.*;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
@ -164,12 +164,7 @@ public class StoreEntryComp extends SimpleComp {
});
});
new PopupMenuAugment<>(false) {
@Override
protected ContextMenu createContextMenu() {
return StoreEntryComp.this.createContextMenu();
}
}.augment(new SimpleCompStructure<>(button));
new ContextMenuAugment<>(false, () -> StoreEntryComp.this.createContextMenu()).augment(new SimpleCompStructure<>(button));
return button;
}
@ -218,12 +213,7 @@ public class StoreEntryComp extends SimpleComp {
private Comp<?> createSettingsButton() {
var settingsButton = new IconButtonComp("mdomz-settings");
settingsButton.styleClass("settings");
settingsButton.apply(new PopupMenuAugment<>(true) {
@Override
protected ContextMenu createContextMenu() {
return StoreEntryComp.this.createContextMenu();
}
});
settingsButton.apply(new ContextMenuAugment<>(true, () -> StoreEntryComp.this.createContextMenu()));
settingsButton.apply(GrowAugment.create(false, true));
settingsButton.apply(s -> {
s.get().prefWidthProperty().bind(Bindings.divide(s.get().heightProperty(), 1.35));

View file

@ -0,0 +1,45 @@
package io.xpipe.app.fxcomps.augment;
import io.xpipe.app.fxcomps.CompStructure;
import javafx.scene.control.ContextMenu;
import javafx.scene.input.MouseButton;
import java.util.function.Supplier;
public class ContextMenuAugment<S extends CompStructure<?>> implements Augment<S> {
private final boolean showOnPrimaryButton;
private final Supplier<ContextMenu> contextMenu;
public ContextMenuAugment(boolean showOnPrimaryButton, Supplier<ContextMenu> contextMenu) {
this.showOnPrimaryButton = showOnPrimaryButton;
this.contextMenu = contextMenu;
}
private static ContextMenu currentContextMenu;
@Override
public void augment(S struc) {
var r = struc.get();
r.setOnMousePressed(event -> {
if ((showOnPrimaryButton && event.getButton() == MouseButton.PRIMARY)
|| (!showOnPrimaryButton && event.getButton() == MouseButton.SECONDARY)) {
if (currentContextMenu != null && currentContextMenu.isShowing()) {
currentContextMenu.hide();
currentContextMenu = null;
}
if (event.getButton() == MouseButton.SECONDARY) {
var cm = contextMenu.get();
if (cm != null) {
cm.setAutoHide(true);
cm.show(r, event.getScreenX(), event.getScreenY());
currentContextMenu = cm;
}
}
event.consume();
}
});
}
}

View file

@ -1,31 +0,0 @@
package io.xpipe.app.fxcomps.augment;
import io.xpipe.app.fxcomps.CompStructure;
import javafx.scene.control.ContextMenu;
import javafx.scene.input.MouseButton;
public abstract class PopupMenuAugment<S extends CompStructure<?>> implements Augment<S> {
private final boolean showOnPrimaryButton;
protected PopupMenuAugment(boolean showOnPrimaryButton) {
this.showOnPrimaryButton = showOnPrimaryButton;
}
protected abstract ContextMenu createContextMenu();
@Override
public void augment(S struc) {
var cm = createContextMenu();
var r = struc.get();
r.setOnMousePressed(event -> {
if ((showOnPrimaryButton && event.getButton() == MouseButton.PRIMARY)
|| (!showOnPrimaryButton && event.getButton() == MouseButton.SECONDARY)) {
cm.show(r, event.getScreenX(), event.getScreenY());
event.consume();
} else {
cm.hide();
}
});
}
}

View file

@ -9,6 +9,7 @@ import javafx.scene.layout.Region;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
@ -21,7 +22,6 @@ public class Shortcuts {
}
public static <T extends Region> void addShortcut(T region, KeyCombination comb, Consumer<T> exec) {
AtomicReference<Scene> scene = new AtomicReference<>(region.getScene());
var filter = new EventHandler<KeyEvent>() {
public void handle(KeyEvent ke) {
if (comb.match(ke)) {
@ -30,21 +30,23 @@ public class Shortcuts {
}
}
};
SHORTCUTS.put(region, comb);
AtomicReference<Scene> scene = new AtomicReference<>();
SimpleChangeListener.apply(region.sceneProperty(), s -> {
if (Objects.equals(s, scene.get())) {
return;
}
if (scene.get() != null) {
scene.get().removeEventHandler(KeyEvent.KEY_PRESSED, filter);
SHORTCUTS.remove(region);
scene.set(null);
}
if (s != null) {
scene.set(s);
s.addEventHandler(KeyEvent.KEY_PRESSED, filter);
SHORTCUTS.put(region, comb);
} else {
if (scene.get() == null) {
return;
}
scene.get().removeEventHandler(KeyEvent.KEY_PRESSED, filter);
SHORTCUTS.remove(region);
scene.set(null);
}
});
}

View file

@ -23,6 +23,11 @@ public interface ExternalEditorType extends PrefsChoiceValue {
public static final ExternalEditorType VSCODE_WINDOWS = new WindowsFullPathType("app.vscode") {
@Override
public boolean canOpenDirectory() {
return true;
}
@Override
protected Optional<Path> determinePath() {
return Optional.of(Path.of(System.getenv("LOCALAPPDATA"))
@ -48,7 +53,13 @@ public interface ExternalEditorType extends PrefsChoiceValue {
}
};
public static final LinuxPathType VSCODE_LINUX = new LinuxPathType("app.vscode", "code");
public static final LinuxPathType VSCODE_LINUX = new LinuxPathType("app.vscode", "code") {
@Override
public boolean canOpenDirectory() {
return true;
}
};
public static final LinuxPathType KATE = new LinuxPathType("app.kate", "kate");
@ -81,7 +92,13 @@ public interface ExternalEditorType extends PrefsChoiceValue {
public static final ExternalEditorType SUBLIME_MACOS = new MacOsEditor("app.sublime", "Sublime Text");
public static final ExternalEditorType VSCODE_MACOS = new MacOsEditor("app.vscode", "Visual Studio Code");
public static final ExternalEditorType VSCODE_MACOS = new MacOsEditor("app.vscode", "Visual Studio Code") {
@Override
public boolean canOpenDirectory() {
return true;
}
};
public static final ExternalEditorType CUSTOM = new ExternalEditorType() {
@ -110,6 +127,10 @@ public interface ExternalEditorType extends PrefsChoiceValue {
public void launch(Path file) throws Exception;
default boolean canOpenDirectory() {
return false;
}
public static class LinuxPathType extends ExternalApplicationType.PathApplication implements ExternalEditorType {
public LinuxPathType(String id, String command) {

View file

@ -17,7 +17,7 @@ import java.util.stream.Stream;
public interface ExternalTerminalType extends PrefsChoiceValue {
public static final ExternalTerminalType CMD = new SimpleType("cmd", "cmd.exe", "cmd.exe") {
public static final ExternalTerminalType CMD = new SimpleType("app.cmd", "cmd.exe", "cmd.exe") {
@Override
protected String toCommand(String name, String file) {
@ -31,7 +31,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
};
public static final ExternalTerminalType POWERSHELL_WINDOWS =
new SimpleType("powershell", "powershell", "PowerShell") {
new SimpleType("app.powershell", "powershell", "PowerShell") {
@Override
protected String toCommand(String name, String file) {
@ -44,7 +44,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
}
};
public static final ExternalTerminalType PWSH_WINDOWS = new SimpleType("pwsh", "pwsh", "PowerShell Core") {
public static final ExternalTerminalType PWSH_WINDOWS = new SimpleType("app.pwsh", "pwsh", "PowerShell Core") {
@Override
protected String toCommand(String name, String file) {
@ -60,7 +60,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
};
public static final ExternalTerminalType WINDOWS_TERMINAL =
new SimpleType("windowsTerminal", "wt.exe", "Windows Terminal") {
new SimpleType("app.windowsTerminal", "wt.exe", "Windows Terminal") {
@Override
protected String toCommand(String name, String file) {
@ -78,7 +78,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
};
public static final ExternalTerminalType GNOME_TERMINAL =
new SimpleType("gnomeTerminal", "gnome-terminal", "Gnome Terminal") {
new SimpleType("app.gnomeTerminal", "gnome-terminal", "Gnome Terminal") {
@Override
public void launch(String name, String file, boolean elevated) throws Exception {
@ -105,7 +105,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
}
};
public static final ExternalTerminalType KONSOLE = new SimpleType("konsole", "konsole", "Konsole") {
public static final ExternalTerminalType KONSOLE = new SimpleType("app.konsole", "konsole", "Konsole") {
@Override
protected String toCommand(String name, String file) {
@ -120,7 +120,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
}
};
public static final ExternalTerminalType XFCE = new SimpleType("xfce", "xfce4-terminal", "Xfce") {
public static final ExternalTerminalType XFCE = new SimpleType("app.xfce", "xfce4-terminal", "Xfce") {
@Override
protected String toCommand(String name, String file) {
@ -169,7 +169,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
static class MacOsTerminalType extends ExternalApplicationType.MacApplication implements ExternalTerminalType {
public MacOsTerminalType() {
super("macosTerminal", "Terminal");
super("app.macosTerminal", "Terminal");
}
@Override
@ -193,7 +193,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
static class CustomType extends ExternalApplicationType implements ExternalTerminalType {
public CustomType() {
super("custom");
super("app.custom");
}
@Override
@ -229,7 +229,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
static class ITerm2Type extends ExternalApplicationType.MacApplication implements ExternalTerminalType {
public ITerm2Type() {
super("iterm2", "iTerm");
super("app.iterm2", "iTerm");
}
@Override
@ -263,7 +263,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
static class WarpType extends ExternalApplicationType.MacApplication implements ExternalTerminalType {
public WarpType() {
super("warp", "Warp");
super("app.warp", "Warp");
}
@Override

View file

@ -18,7 +18,7 @@ public class ScriptHelper {
public static String createDetachCommand(ShellControl pc, String command) {
if (pc.getOsType().equals(OsType.WINDOWS)) {
return "start \"\" " + command;
return "start \"\" /MIN " + command;
} else {
return "nohup " + command + " </dev/null &>/dev/null & disown";
}

View file

@ -1,3 +1,4 @@
import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.core.AppLogs;
import io.xpipe.app.exchange.*;
import io.xpipe.app.exchange.api.*;
@ -33,6 +34,7 @@ open module io.xpipe.app {
exports io.xpipe.app.fxcomps.util;
exports io.xpipe.app.fxcomps.augment;
exports io.xpipe.app.test;
exports io.xpipe.app.browser.action;
exports io.xpipe.app.browser;
exports io.xpipe.app.browser.icon;
@ -119,11 +121,13 @@ open module io.xpipe.app {
uses ProxyFunction;
uses ModuleLayerLoader;
uses ScanProvider;
uses BrowserAction;
provides ModuleLayerLoader with
DataSourceTarget.Loader,
ActionProvider.Loader,
PrefsProvider.Loader,
BrowserAction.Loader,
ScanProvider.Loader;
provides DataStateProvider with
DataStateProviderImpl;

View file

@ -66,6 +66,35 @@
-fx-padding: 0;
}
.browser .context-menu > * > * {
-fx-padding: 3px 10px 3px 10px;
-fx-background-radius: 1px;
-fx-spacing: 20px;
}
.browser .context-menu .separator {
-fx-padding: 0;
}
.browser .context-menu .separator .line {
-fx-padding: 0;
-fx-border-insets: 0px;
}
.browser .context-menu .accelerator-text {
-fx-padding: 3px 0px 3px 50px;
}
.browser .context-menu > * {
-fx-padding: 0;
}
.browser .context-menu {
-fx-padding: 0;
-fx-background-radius: 1px;
-fx-border-color: -color-neutral-muted;
}
.chooser-bar {
-fx-border-color: -color-neutral-emphasis;
-fx-border-width: 0.1em 0 0 0;

View file

@ -5,6 +5,10 @@ import java.util.List;
public class FileNames {
public static String quoteIfNecessary(String n) {
return n.contains(" ") ? "\"" + n + "\"" : n;
}
public static String toDirectory(String path) {
if (path.endsWith("/") || path.endsWith("\\")) {
return path;
@ -45,6 +49,19 @@ public class FileNames {
return components.get(components.size() - 1);
}
public static String getBaseName(String file) {
if (file == null || file.isEmpty()) {
return null;
}
var name = FileNames.getFileName(file);
var split = file.lastIndexOf("\\.");
if (split == -1) {
return name;
}
return name.substring(0, split);
}
public static String getExtension(String file) {
if (file == null || file.isEmpty()) {
return null;

View file

@ -4,7 +4,7 @@ import java.util.Locale;
import java.util.Map;
import java.util.stream.Collectors;
public interface OsType {
public sealed interface OsType permits OsType.Windows, OsType.Linux, OsType.MacOs {
Windows WINDOWS = new Windows();
Linux LINUX = new Linux();
@ -33,7 +33,7 @@ public interface OsType {
String determineOperatingSystemName(ShellControl pc) throws Exception;
static class Windows implements OsType {
static final class Windows implements OsType {
@Override
public String getHomeDirectory(ShellControl pc) throws Exception {
@ -80,7 +80,7 @@ public interface OsType {
}
}
static class Linux implements OsType {
static final class Linux implements OsType {
@Override
public String getHomeDirectory(ShellControl pc) throws Exception {
@ -138,7 +138,7 @@ public interface OsType {
}
}
static class MacOs implements OsType {
static final class MacOs implements OsType {
@Override
public String getHomeDirectory(ShellControl pc) throws Exception {

View file

@ -8,6 +8,12 @@ apply from: "$rootDir/gradle/gradle_scripts/commons.gradle"
apply from: "$rootDir/gradle/gradle_scripts/lombok.gradle"
apply from: "$rootDir/gradle/gradle_scripts/extension.gradle"
dependencies {
compileOnly group: 'org.kordamp.ikonli', name: 'ikonli-javafx', version: "12.2.0"
compileOnly 'net.java.dev.jna:jna-jpms:5.12.1'
compileOnly 'net.java.dev.jna:jna-platform-jpms:5.12.1'
}
compileJava {
doFirst {
options.compilerArgs += [

View file

@ -0,0 +1,42 @@
package io.xpipe.ext.base.browser;
import io.xpipe.app.browser.FileBrowserClipboard;
import io.xpipe.app.browser.FileBrowserEntry;
import io.xpipe.app.browser.OpenFileSystemModel;
import io.xpipe.app.browser.action.LeafAction;
import javafx.scene.Node;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
public class CopyAction implements LeafAction {
@Override
public void execute(OpenFileSystemModel model, List<FileBrowserEntry> entries) throws Exception {
FileBrowserClipboard.startCopy(
model.getCurrentDirectory(), entries.stream().map(entry -> entry.getRawFileEntry()).toList());
}
@Override
public Node getIcon(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return new FontIcon("mdi2c-content-copy");
}
@Override
public Category getCategory() {
return Category.COPY_PASTE;
}
@Override
public KeyCombination getShortcut() {
return new KeyCodeCombination(KeyCode.C, KeyCombination.SHORTCUT_DOWN);
}
@Override
public String getName(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return "Copy";
}
}

View file

@ -0,0 +1,108 @@
package io.xpipe.ext.base.browser;
import io.xpipe.app.browser.FileBrowserEntry;
import io.xpipe.app.browser.OpenFileSystemModel;
import io.xpipe.app.browser.action.BranchAction;
import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.browser.action.LeafAction;
import io.xpipe.core.impl.FileNames;
import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
import java.util.List;
import java.util.stream.Collectors;
public class CopyPathAction implements BrowserAction, BranchAction {
@Override
public String getName(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return "Copy location";
}
@Override
public Category getCategory() {
return Category.COPY_PASTE;
}
@Override
public List<LeafAction> getBranchingActions() {
return List.of(
new LeafAction() {
@Override
public String getName(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return "Absolute Path";
}
@Override
public void execute(OpenFileSystemModel model, List<FileBrowserEntry> entries) throws Exception {
var s = entries.stream()
.map(entry -> entry.getRawFileEntry().getPath())
.collect(Collectors.joining("\n"));
var selection = new StringSelection(s);
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
clipboard.setContents(selection, selection);
}
},
new LeafAction() {
@Override
public String getName(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return "Absolute Path (Quoted)";
}
@Override
public boolean isApplicable(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return entries.stream().anyMatch(entry -> entry.getRawFileEntry().getPath().contains(" "));
}
@Override
public void execute(OpenFileSystemModel model, List<FileBrowserEntry> entries) throws Exception {
var s = entries.stream()
.map(entry -> "\"" + entry.getRawFileEntry().getPath() + "\"")
.collect(Collectors.joining("\n"));
var selection = new StringSelection(s);
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
clipboard.setContents(selection, selection);
}
},
new LeafAction() {
@Override
public String getName(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return "File Name";
}
@Override
public void execute(OpenFileSystemModel model, List<FileBrowserEntry> entries) throws Exception {
var s = entries.stream()
.map(entry ->
FileNames.getFileName(entry.getRawFileEntry().getPath()))
.collect(Collectors.joining("\n"));
var selection = new StringSelection(s);
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
clipboard.setContents(selection, selection);
}
},
new LeafAction() {
@Override
public String getName(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return "File Name (Quoted)";
}
@Override
public boolean isApplicable(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return entries.stream().anyMatch(entry -> FileNames.getFileName(entry.getRawFileEntry().getPath()).contains(" "));
}
@Override
public void execute(OpenFileSystemModel model, List<FileBrowserEntry> entries) throws Exception {
var s = entries.stream()
.map(entry ->
"\"" + FileNames.getFileName(entry.getRawFileEntry().getPath()) + "\"")
.collect(Collectors.joining("\n"));
var selection = new StringSelection(s);
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
clipboard.setContents(selection, selection);
}
});
}
}

View file

@ -0,0 +1,48 @@
package io.xpipe.ext.base.browser;
import io.xpipe.app.browser.FileBrowserAlerts;
import io.xpipe.app.browser.FileBrowserEntry;
import io.xpipe.app.browser.FileSystemHelper;
import io.xpipe.app.browser.OpenFileSystemModel;
import io.xpipe.app.browser.action.LeafAction;
import javafx.scene.Node;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
public class DeleteAction implements LeafAction {
@Override
public void execute(OpenFileSystemModel model, List<FileBrowserEntry> entries) throws Exception {
var toDelete = entries.stream().map(entry -> entry.getRawFileEntry()).toList();
if (!FileBrowserAlerts.showDeleteAlert(toDelete)) {
return;
}
FileSystemHelper.delete(toDelete);
model.refreshSync();
}
@Override
public Category getCategory() {
return Category.MUTATION;
}
@Override
public Node getIcon(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return new FontIcon("mdi2d-delete");
}
@Override
public KeyCombination getShortcut() {
return new KeyCodeCombination(KeyCode.DELETE);
}
@Override
public String getName(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return "Delete";
}
}

View file

@ -0,0 +1,41 @@
package io.xpipe.ext.base.browser;
import io.xpipe.app.browser.FileBrowserEntry;
import io.xpipe.app.browser.OpenFileSystemModel;
import io.xpipe.app.browser.action.LeafAction;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.FileOpener;
import javafx.scene.Node;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
public class EditFileAction implements LeafAction {
@Override
public void execute(OpenFileSystemModel model, List<FileBrowserEntry> entries) throws Exception {
for (FileBrowserEntry entry : entries) {
FileOpener.openInTextEditor(entry.getRawFileEntry());
}
}
@Override
public Category getCategory() {
return Category.OPEN;
}
@Override
public Node getIcon(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return new FontIcon("mdi2p-pencil");
}
@Override
public boolean isApplicable(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return entries.stream().noneMatch(entry -> entry.getRawFileEntry().isDirectory());
}
@Override
public String getName(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return "Edit with " + AppPrefs.get().externalEditor().getValue().toTranslatedString();
}
}

View file

@ -0,0 +1,26 @@
package io.xpipe.ext.base.browser;
import io.xpipe.app.browser.FileBrowserEntry;
import io.xpipe.app.browser.OpenFileSystemModel;
import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.browser.icon.FileIcons;
import io.xpipe.app.browser.icon.FileType;
import javafx.scene.Node;
import java.util.List;
public interface FileTypeAction extends BrowserAction {
@Override
default boolean isApplicable(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
var t = getType();
return entries.stream().allMatch(entry -> t.matches(entry.getRawFileEntry()));
}
@Override
default Node getIcon(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return FileIcons.createIcon(getType()).createRegion();
}
FileType getType();
}

View file

@ -0,0 +1,41 @@
package io.xpipe.ext.base.browser;
import io.xpipe.app.browser.FileBrowserEntry;
import io.xpipe.app.browser.OpenFileSystemModel;
import io.xpipe.app.browser.icon.FileType;
import io.xpipe.core.process.ShellControl;
import java.util.List;
public class JarAction extends JavaAction implements FileTypeAction {
@Override
public Category getCategory() {
return Category.CUSTOM;
}
@Override
public boolean isApplicable(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return super.isApplicable(model, entries) && FileTypeAction.super.isApplicable(model, entries);
}
@Override
public boolean isApplicable(OpenFileSystemModel model, FileBrowserEntry entry) {
return entry.getFileName().endsWith(".jar");
}
@Override
protected String createCommand(ShellControl sc, OpenFileSystemModel model, FileBrowserEntry entry) {
return "java -jar " + entry.getOptionallyQuotedFileName();
}
@Override
public String getName(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return "java -jar " + filesArgument(entries);
}
@Override
public FileType getType() {
return FileType.byId("jar");
}
}

View file

@ -0,0 +1,21 @@
package io.xpipe.ext.base.browser;
import io.xpipe.app.browser.FileBrowserEntry;
import io.xpipe.app.browser.OpenFileSystemModel;
import io.xpipe.app.browser.action.ApplicationPathAction;
import io.xpipe.app.browser.action.MultiExecuteAction;
import java.util.List;
public abstract class JavaAction extends MultiExecuteAction implements ApplicationPathAction {
@Override
public String getName(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return "Java";
}
@Override
public String getExecutable() {
return "java";
}
}

View file

@ -0,0 +1,64 @@
package io.xpipe.ext.base.browser;
import io.xpipe.app.browser.FileBrowserEntry;
import io.xpipe.app.browser.OpenFileSystemModel;
import io.xpipe.app.browser.action.BranchAction;
import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.browser.action.LeafAction;
import javafx.scene.Node;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
public class NewItemAction implements BrowserAction, BranchAction {
@Override
public Node getIcon(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return new FontIcon("mdi2p-plus-box-outline");
}
@Override
public String getName(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return "Create new";
}
@Override
public boolean acceptsEmptySelection() {
return true;
}
@Override
public boolean isApplicable(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return entries.size() == 0;
}
@Override
public Category getCategory() {
return Category.MUTATION;
}
@Override
public List<LeafAction> getBranchingActions() {
return List.of(
new LeafAction() {
@Override
public String getName(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return "file";
}
@Override
public void execute(OpenFileSystemModel model, List<FileBrowserEntry> entries) throws Exception {
}
},
new LeafAction() {
@Override
public String getName(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return "directory";
}
@Override
public void execute(OpenFileSystemModel model, List<FileBrowserEntry> entries) throws Exception {
}
});
}
}

View file

@ -0,0 +1,45 @@
package io.xpipe.ext.base.browser;
import io.xpipe.app.browser.FileBrowserEntry;
import io.xpipe.app.browser.OpenFileSystemModel;
import io.xpipe.app.browser.action.LeafAction;
import javafx.scene.Node;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
public class OpenDirectoryAction implements LeafAction {
@Override
public void execute(OpenFileSystemModel model, List<FileBrowserEntry> entries) throws Exception {
model.cd(entries.get(0).getRawFileEntry().getPath());
}
@Override
public Category getCategory() {
return Category.OPEN;
}
@Override
public Node getIcon(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return new FontIcon("mdi2f-folder-open");
}
@Override
public boolean isApplicable(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return entries.size() == 1 && entries.stream().allMatch(entry -> entry.getRawFileEntry().isDirectory());
}
@Override
public KeyCombination getShortcut() {
return new KeyCodeCombination(KeyCode.ENTER);
}
@Override
public String getName(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return "Open";
}
}

View file

@ -0,0 +1,46 @@
package io.xpipe.ext.base.browser;
import io.xpipe.app.browser.FileBrowserEntry;
import io.xpipe.app.browser.FileBrowserModel;
import io.xpipe.app.browser.OpenFileSystemModel;
import io.xpipe.app.browser.action.LeafAction;
import javafx.scene.Node;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
public class OpenDirectoryInNewTabAction implements LeafAction {
@Override
public void execute(OpenFileSystemModel model, List<FileBrowserEntry> entries) throws Exception {
model.getBrowserModel().openFileSystemSync(model.getStore().getValue().asNeeded(), entries.get(0).getRawFileEntry().getPath());
}
@Override
public Category getCategory() {
return Category.OPEN;
}
@Override
public Node getIcon(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return new FontIcon("mdi2f-folder-open-outline");
}
@Override
public boolean isApplicable(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return entries.size() == 1 && entries.stream().allMatch(entry -> entry.getRawFileEntry().isDirectory()) && model.getBrowserModel().getMode() == FileBrowserModel.Mode.BROWSER;
}
@Override
public KeyCombination getShortcut() {
return new KeyCodeCombination(KeyCode.ENTER);
}
@Override
public String getName(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return "Open in new tab";
}
}

View file

@ -0,0 +1,48 @@
package io.xpipe.ext.base.browser;
import io.xpipe.app.browser.FileBrowserEntry;
import io.xpipe.app.browser.OpenFileSystemModel;
import io.xpipe.app.browser.action.LeafAction;
import io.xpipe.app.util.FileOpener;
import javafx.scene.Node;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
public class OpenFileDefaultAction implements LeafAction {
@Override
public void execute(OpenFileSystemModel model, List<FileBrowserEntry> entries) throws Exception {
for (var entry : entries) {
FileOpener.openInDefaultApplication(entry.getRawFileEntry());
}
}
@Override
public Category getCategory() {
return Category.OPEN;
}
@Override
public Node getIcon(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return new FontIcon("mdi2b-book-open-variant");
}
@Override
public boolean isApplicable(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return entries.stream().noneMatch(entry -> entry.getRawFileEntry().isDirectory());
}
@Override
public KeyCombination getShortcut() {
return new KeyCodeCombination(KeyCode.ENTER);
}
@Override
public String getName(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return "Open";
}
}

View file

@ -0,0 +1,62 @@
package io.xpipe.ext.base.browser;
import com.sun.jna.platform.win32.Shell32;
import com.sun.jna.platform.win32.WinUser;
import io.xpipe.app.browser.FileBrowserEntry;
import io.xpipe.app.browser.OpenFileSystemModel;
import io.xpipe.app.browser.action.LeafAction;
import io.xpipe.core.process.OsType;
import javafx.scene.Node;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
public class OpenFileWithAction implements LeafAction {
@Override
public void execute(OpenFileSystemModel model, List<FileBrowserEntry> entries) throws Exception {
switch (OsType.getLocal()) {
case OsType.Windows windows -> {
Shell32.INSTANCE.ShellExecute(
null,
"open",
"rundll32.exe",
"shell32.dll,OpenAs_RunDLL " + entries.get(0).getRawFileEntry().getPath(),
null,
WinUser.SW_SHOWNORMAL);
}
case OsType.Linux linux -> {
}
case OsType.MacOs macOs -> {
}
}
}
@Override
public Category getCategory() {
return Category.OPEN;
}
@Override
public Node getIcon(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return new FontIcon("mdi2b-book-open-page-variant-outline");
}
@Override
public boolean isApplicable(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return entries.size() == 1 && entries.stream().noneMatch(entry -> entry.getRawFileEntry().isDirectory());
}
@Override
public KeyCombination getShortcut() {
return new KeyCodeCombination(KeyCode.ENTER, KeyCombination.SHIFT_DOWN);
}
@Override
public String getName(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return "Open with ...";
}
}

View file

@ -0,0 +1,48 @@
package io.xpipe.ext.base.browser;
import io.xpipe.app.browser.FileBrowserEntry;
import io.xpipe.app.browser.OpenFileSystemModel;
import io.xpipe.app.browser.action.LeafAction;
import io.xpipe.app.prefs.AppPrefs;
import javafx.scene.Node;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
public class OpenTerminalAction implements LeafAction {
@Override
public void execute(OpenFileSystemModel model, List<FileBrowserEntry> entries) throws Exception {
for (var entry : entries) {
model.openTerminalAsync(entry.getRawFileEntry().getPath());
}
}
@Override
public Category getCategory() {
return Category.OPEN;
}
@Override
public Node getIcon(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return new FontIcon("mdi2c-console");
}
@Override
public boolean isApplicable(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return entries.stream().allMatch(entry -> entry.getRawFileEntry().isDirectory());
}
@Override
public KeyCombination getShortcut() {
return new KeyCodeCombination(KeyCode.T, KeyCombination.SHORTCUT_DOWN);
}
@Override
public String getName(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return "Open in " + AppPrefs.get().terminalType().getValue().toTranslatedString();
}
}

View file

@ -0,0 +1,63 @@
package io.xpipe.ext.base.browser;
import io.xpipe.app.browser.FileBrowserClipboard;
import io.xpipe.app.browser.FileBrowserEntry;
import io.xpipe.app.browser.OpenFileSystemModel;
import io.xpipe.app.browser.action.LeafAction;
import javafx.scene.Node;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
public class PasteAction implements LeafAction {
@Override
public void execute(OpenFileSystemModel model, List<FileBrowserEntry> entries) throws Exception {
var clipboard = FileBrowserClipboard.retrieveCopy();
if (clipboard == null) {
return;
}
var target = entries.size() == 1 && entries.get(0).getRawFileEntry().isDirectory() ? entries.get(0).getRawFileEntry() : model.getCurrentDirectory();
var files = clipboard.getEntries();
model.dropFilesIntoAsync(target, files, true);
}
@Override
public Category getCategory() {
return Category.COPY_PASTE;
}
@Override
public Node getIcon(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return new FontIcon("mdi2c-content-paste");
}
@Override
public boolean isApplicable(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return entries.size() < 2 && entries.stream().allMatch(entry -> entry.getRawFileEntry().isDirectory());
}
@Override
public boolean isActive(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return FileBrowserClipboard.retrieveCopy() != null;
}
@Override
public KeyCombination getShortcut() {
return new KeyCodeCombination(KeyCode.V, KeyCombination.SHORTCUT_DOWN);
}
@Override
public boolean acceptsEmptySelection() {
return true;
}
@Override
public String getName(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return "Paste";
}
}

View file

@ -0,0 +1,66 @@
package io.xpipe.ext.base.browser;
import io.xpipe.app.browser.FileBrowserEntry;
import io.xpipe.app.browser.OpenFileSystemModel;
import io.xpipe.app.browser.action.MultiExecuteAction;
import io.xpipe.core.process.OsType;
import io.xpipe.core.process.ShellControl;
import io.xpipe.core.store.FileSystem;
import javafx.scene.Node;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
public class RunAction extends MultiExecuteAction {
private boolean isExecutable(FileSystem.FileEntry e) {
if (e.isDirectory()) {
return false;
}
if (e.getExecutable() != null && e.getExecutable()) {
return true;
}
var shell = e.getFileSystem().getShell();
if (shell.isEmpty()) {
return false;
}
var os = shell.get().getOsType();
if (os.equals(OsType.WINDOWS) && List.of("exe", "bat", "ps1", "cmd").stream().anyMatch(s -> e.getPath().endsWith(s))) {
return true;
}
if (List.of("sh", "command").stream().anyMatch(s -> e.getPath().endsWith(s))) {
return true;
}
return false;
}
@Override
public Category getCategory() {
return Category.CUSTOM;
}
@Override
public Node getIcon(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return new FontIcon("mdi2p-play");
}
@Override
public String getName(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return "Run";
}
@Override
public boolean isApplicable(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return entries.stream().allMatch(entry -> isExecutable(entry.getRawFileEntry()));
}
@Override
protected String createCommand(ShellControl sc, OpenFileSystemModel model, FileBrowserEntry entry) {
return sc.getShellDialect().runScript(entry.getFileName());
}
}

View file

@ -0,0 +1,37 @@
package io.xpipe.ext.base.browser;
import io.xpipe.app.browser.FileBrowserEntry;
import io.xpipe.app.browser.OpenFileSystemModel;
import io.xpipe.app.browser.action.ExecuteApplicationAction;
import io.xpipe.core.impl.FileNames;
import io.xpipe.core.process.OsType;
import java.util.List;
public class UnzipAction extends ExecuteApplicationAction {
@Override
public String getExecutable() {
return "unzip";
}
@Override
public boolean isApplicable(OpenFileSystemModel model, FileBrowserEntry entry) {
return entry.getRawFileEntry().getPath().endsWith(".zip") && !OsType.getLocal().equals(OsType.WINDOWS);
}
@Override
protected String createCommand(OpenFileSystemModel model, FileBrowserEntry entry) {
return "unzip -o " + entry.getOptionallyQuotedFileName() + " -d " + FileNames.quoteIfNecessary(FileNames.getBaseName(entry.getFileName()));
}
@Override
public Category getCategory() {
return Category.CUSTOM;
}
@Override
public String getName(OpenFileSystemModel model, List<FileBrowserEntry> entries) {
return "unzip [...]";
}
}

View file

@ -1,3 +1,4 @@
import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.ext.ActionProvider;
import io.xpipe.app.ext.DataSourceProvider;
import io.xpipe.app.ext.DataSourceTarget;
@ -5,6 +6,7 @@ import io.xpipe.app.ext.DataStoreProvider;
import io.xpipe.ext.base.*;
import io.xpipe.ext.base.actions.*;
import io.xpipe.ext.base.apps.*;
import io.xpipe.ext.base.browser.*;
open module io.xpipe.ext.base {
exports io.xpipe.ext.base;
@ -20,7 +22,25 @@ open module io.xpipe.ext.base {
requires static net.synedra.validatorfx;
requires static io.xpipe.app;
requires org.apache.commons.lang3;
requires org.kordamp.ikonli.javafx;
requires com.sun.jna;
requires com.sun.jna.platform;
provides BrowserAction with
OpenFileDefaultAction,
OpenFileWithAction,
OpenDirectoryAction,
OpenDirectoryInNewTabAction,
OpenTerminalAction,
EditFileAction,
RunAction,
CopyAction,
CopyPathAction,
PasteAction,
NewItemAction,
DeleteAction,
UnzipAction,
JarAction;
provides ActionProvider with
DeleteStoreChildrenAction,
AddStoreAction,