mirror of
https://github.com/xpipe-io/xpipe.git
synced 2024-11-25 09:00:26 +00:00
More support for symlinks
This commit is contained in:
parent
f8b2afe44c
commit
7605a4331a
20 changed files with 357 additions and 193 deletions
|
@ -80,9 +80,7 @@ public class BrowserBreadcrumbBar extends SimpleComp {
|
|||
}
|
||||
|
||||
breadcrumbs.selectedCrumbProperty().addListener((obs, old, val) -> {
|
||||
model.cd(val != null ? val.getValue() : null).ifPresent(s -> {
|
||||
model.cd(s);
|
||||
});
|
||||
model.cd(val != null ? val.getValue() : null);
|
||||
});
|
||||
|
||||
return breadcrumbs;
|
||||
|
|
|
@ -38,7 +38,8 @@ final class BrowserContextMenu extends ContextMenu {
|
|||
var all = BrowserAction.ALL.stream()
|
||||
.filter(browserAction -> browserAction.getCategory() == cat)
|
||||
.filter(browserAction -> {
|
||||
if (!browserAction.isApplicable(model, selected)) {
|
||||
var used = resolveIfNeeded(browserAction, selected);
|
||||
if (!browserAction.isApplicable(model, used)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -58,26 +59,40 @@ final class BrowserContextMenu extends ContextMenu {
|
|||
}
|
||||
|
||||
for (BrowserAction a : all) {
|
||||
var used = resolveIfNeeded(a, selected);
|
||||
if (a instanceof LeafAction la) {
|
||||
getItems().add(la.toItem(model, selected, s -> s));
|
||||
getItems().add(la.toItem(model, used, s -> s));
|
||||
}
|
||||
|
||||
if (a instanceof BranchAction la) {
|
||||
var m = new Menu(a.getName(model, selected) + " ...");
|
||||
var m = new Menu(a.getName(model, used) + " ...");
|
||||
for (LeafAction sub : la.getBranchingActions()) {
|
||||
if (!sub.isApplicable(model, selected)) {
|
||||
var subUsed = resolveIfNeeded(sub, selected);
|
||||
if (!sub.isApplicable(model, subUsed)) {
|
||||
continue;
|
||||
}
|
||||
m.getItems().add(sub.toItem(model, selected, s -> s));
|
||||
m.getItems().add(sub.toItem(model, subUsed, s -> s));
|
||||
}
|
||||
var graphic = a.getIcon(model, selected);
|
||||
var graphic = a.getIcon(model, used);
|
||||
if (graphic != null) {
|
||||
m.setGraphic(graphic);
|
||||
}
|
||||
m.setDisable(!a.isActive(model, selected));
|
||||
m.setDisable(!a.isActive(model, used));
|
||||
getItems().add(m);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static List<BrowserEntry> resolveIfNeeded(BrowserAction action, List<BrowserEntry> selected) {
|
||||
var used = action.automaticallyResolveLinks()
|
||||
? selected.stream()
|
||||
.map(browserEntry -> new BrowserEntry(
|
||||
browserEntry.getRawFileEntry().resolved(),
|
||||
browserEntry.getModel(),
|
||||
browserEntry.isSynthetic()))
|
||||
.toList()
|
||||
: selected;
|
||||
return used;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@ 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.HumanReadableFormat;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
|
@ -23,7 +22,6 @@ import javafx.beans.property.*;
|
|||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Bounds;
|
||||
import javafx.geometry.Pos;
|
||||
|
@ -67,9 +65,6 @@ final class BrowserFileListComp extends SimpleComp {
|
|||
@Override
|
||||
protected Region createSimple() {
|
||||
TableView<BrowserEntry> table = createTable();
|
||||
SimpleChangeListener.apply(table.comparatorProperty(), (newValue) -> {
|
||||
fileList.setComparator(newValue);
|
||||
});
|
||||
return table;
|
||||
}
|
||||
|
||||
|
@ -87,17 +82,17 @@ final class BrowserFileListComp extends SimpleComp {
|
|||
|
||||
var sizeCol = new TableColumn<BrowserEntry, Number>("Size");
|
||||
sizeCol.setCellValueFactory(param ->
|
||||
new SimpleLongProperty(param.getValue().getRawFileEntry().getSize()));
|
||||
new SimpleLongProperty(param.getValue().getRawFileEntry().resolved().getSize()));
|
||||
sizeCol.setCellFactory(col -> new FileSizeCell());
|
||||
|
||||
var mtimeCol = new TableColumn<BrowserEntry, Instant>("Modified");
|
||||
mtimeCol.setCellValueFactory(param ->
|
||||
new SimpleObjectProperty<>(param.getValue().getRawFileEntry().getDate()));
|
||||
new SimpleObjectProperty<>(param.getValue().getRawFileEntry().resolved().getDate()));
|
||||
mtimeCol.setCellFactory(col -> new FileTimeCell());
|
||||
|
||||
var modeCol = new TableColumn<BrowserEntry, String>("Attributes");
|
||||
modeCol.setCellValueFactory(param ->
|
||||
new SimpleObjectProperty<>(param.getValue().getRawFileEntry().getMode()));
|
||||
new SimpleObjectProperty<>(param.getValue().getRawFileEntry().resolved().getMode()));
|
||||
modeCol.setCellFactory(col -> new FileModeCell());
|
||||
modeCol.setSortable(false);
|
||||
|
||||
|
@ -109,7 +104,8 @@ final class BrowserFileListComp extends SimpleComp {
|
|||
table.getSortOrder().add(filenameCol);
|
||||
table.setFocusTraversable(true);
|
||||
table.setSortPolicy(param -> {
|
||||
return sort(table, param.getItems());
|
||||
fileList.setComparator(table.getComparator());
|
||||
return true;
|
||||
});
|
||||
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN);
|
||||
filenameCol.minWidthProperty().bind(table.widthProperty().multiply(0.5));
|
||||
|
@ -124,22 +120,6 @@ final class BrowserFileListComp extends SimpleComp {
|
|||
return table;
|
||||
}
|
||||
|
||||
private boolean sort(TableView<BrowserEntry> table, ObservableList<BrowserEntry> list) {
|
||||
var comp = table.getComparator();
|
||||
if (comp == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var syntheticFirst = Comparator.<BrowserEntry, Boolean>comparing(path -> !path.isSynthetic());
|
||||
var dirsFirst = Comparator.<BrowserEntry, Boolean>comparing(
|
||||
path -> path.getRawFileEntry().getKind() != FileKind.DIRECTORY);
|
||||
|
||||
Comparator<? super BrowserEntry> us =
|
||||
syntheticFirst.thenComparing(dirsFirst).thenComparing(comp);
|
||||
FXCollections.sort(list, us);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void prepareTableSelectionModel(TableView<BrowserEntry> table) {
|
||||
if (!fileList.getMode().isMultiple()) {
|
||||
table.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
|
||||
|
@ -256,12 +236,12 @@ final class BrowserFileListComp extends SimpleComp {
|
|||
}
|
||||
|
||||
if (row.getItem() != null
|
||||
&& row.getItem().getRawFileEntry().getKind() == FileKind.DIRECTORY) {
|
||||
&& row.getItem().getRawFileEntry().resolved().getKind() == FileKind.DIRECTORY) {
|
||||
return event.getButton() == MouseButton.SECONDARY;
|
||||
}
|
||||
|
||||
if (row.getItem() != null
|
||||
&& row.getItem().getRawFileEntry().getKind() != FileKind.DIRECTORY) {
|
||||
&& row.getItem().getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY) {
|
||||
return event.getButton() == MouseButton.SECONDARY
|
||||
|| event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2;
|
||||
}
|
||||
|
@ -365,7 +345,6 @@ final class BrowserFileListComp extends SimpleComp {
|
|||
if (!table.getItems().equals(newItems)) {
|
||||
// Sort the list ourselves as sorting the table would incur a lot of cell updates
|
||||
var obs = FXCollections.observableList(newItems);
|
||||
sort(table, obs);
|
||||
table.getItems().setAll(obs);
|
||||
// table.sort();
|
||||
}
|
||||
|
@ -497,7 +476,15 @@ final class BrowserFileListComp extends SimpleComp {
|
|||
var isDirectory = getTableRow().getItem().getRawFileEntry().getKind() == FileKind.DIRECTORY;
|
||||
pseudoClassStateChanged(FOLDER, isDirectory);
|
||||
|
||||
var fileName = isParentLink ? ".." : FileNames.getFileName(newName);
|
||||
var normalName = getTableRow().getItem().getRawFileEntry().getKind() == FileKind.LINK
|
||||
? getTableRow().getItem().getFileName() + " -> "
|
||||
+ getTableRow()
|
||||
.getItem()
|
||||
.getRawFileEntry()
|
||||
.resolved()
|
||||
.getPath()
|
||||
: getTableRow().getItem().getFileName();
|
||||
var fileName = isParentLink ? ".." : normalName;
|
||||
var hidden = !isParentLink
|
||||
&& (getTableRow().getItem().getRawFileEntry().isHidden() || fileName.startsWith("."));
|
||||
getTableRow().pseudoClassStateChanged(HIDDEN, hidden);
|
||||
|
@ -519,7 +506,7 @@ final class BrowserFileListComp extends SimpleComp {
|
|||
setText(null);
|
||||
} else {
|
||||
var path = getTableRow().getItem();
|
||||
if (path.getRawFileEntry().getKind() == FileKind.DIRECTORY) {
|
||||
if (path.getRawFileEntry().resolved().getKind() == FileKind.DIRECTORY) {
|
||||
setText("");
|
||||
} else {
|
||||
setText(byteCount(fileSize.longValue()));
|
||||
|
|
|
@ -17,16 +17,13 @@ import java.util.ArrayList;
|
|||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@Getter
|
||||
public final class BrowserFileListModel {
|
||||
|
||||
static final Comparator<BrowserEntry> FILE_TYPE_COMPARATOR =
|
||||
Comparator.comparing(path -> path.getRawFileEntry().getKind() != FileKind.DIRECTORY);
|
||||
static final Predicate<BrowserEntry> PREDICATE_ANY = path -> true;
|
||||
static final Predicate<BrowserEntry> PREDICATE_NOT_HIDDEN = path -> true;
|
||||
Comparator.comparing(path -> path.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY);
|
||||
|
||||
private final OpenFileSystemModel fileSystemModel;
|
||||
private final Property<Comparator<BrowserEntry>> comparatorProperty =
|
||||
|
@ -95,10 +92,21 @@ public final class BrowserFileListModel {
|
|||
var comparator =
|
||||
tableComparator != null ? FILE_TYPE_COMPARATOR.thenComparing(tableComparator) : FILE_TYPE_COMPARATOR;
|
||||
var listCopy = new ArrayList<>(filtered);
|
||||
listCopy.sort(comparator);
|
||||
sort(listCopy);
|
||||
shown.setValue(listCopy);
|
||||
}
|
||||
|
||||
private void sort(List<BrowserEntry> l) {
|
||||
var syntheticFirst = Comparator.<BrowserEntry, Boolean>comparing(path -> !path.isSynthetic());
|
||||
var dirsFirst = Comparator.<BrowserEntry, Boolean>comparing(
|
||||
path -> path.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY);
|
||||
var comp = comparatorProperty.getValue();
|
||||
|
||||
Comparator<? super BrowserEntry> us =
|
||||
comp != null ? syntheticFirst.thenComparing(dirsFirst).thenComparing(comp) : syntheticFirst.thenComparing(dirsFirst);
|
||||
l.sort(us);
|
||||
}
|
||||
|
||||
public boolean rename(String filename, String newName) {
|
||||
var fullPath = FileNames.join(fileSystemModel.getCurrentPath().get(), filename);
|
||||
var newFullPath = FileNames.join(fileSystemModel.getCurrentPath().get(), newName);
|
||||
|
@ -113,19 +121,14 @@ public final class BrowserFileListModel {
|
|||
}
|
||||
|
||||
public void onDoubleClick(BrowserEntry entry) {
|
||||
if (entry.getRawFileEntry().getKind() != FileKind.DIRECTORY
|
||||
if (entry.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY
|
||||
&& getMode().equals(BrowserModel.Mode.SINGLE_FILE_CHOOSER)) {
|
||||
getFileSystemModel().getBrowserModel().finishChooser();
|
||||
return;
|
||||
}
|
||||
|
||||
if (entry.getRawFileEntry().getKind() == FileKind.DIRECTORY) {
|
||||
var dir = fileSystemModel.cd(entry.getRawFileEntry().getPath());
|
||||
if (dir.isPresent()) {
|
||||
fileSystemModel.cd(dir.get());
|
||||
}
|
||||
} else {
|
||||
// FileOpener.openInTextEditor(entry.getRawFileEntry());
|
||||
if (entry.getRawFileEntry().resolved().getKind() == FileKind.DIRECTORY) {
|
||||
fileSystemModel.cd(entry.getRawFileEntry().resolved().getPath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,9 @@ import javafx.collections.ObservableList;
|
|||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@Getter
|
||||
|
@ -71,7 +73,8 @@ public class BrowserModel {
|
|||
state.getLastSystems().forEach(e -> {
|
||||
var storageEntry = DataStorage.get().getStoreEntry(e.getUuid());
|
||||
storageEntry.ifPresent(entry -> {
|
||||
openFileSystemAsync(entry.getName(), entry.getStore().asNeeded(), e.getPath(), new SimpleBooleanProperty());
|
||||
openFileSystemAsync(
|
||||
entry.getName(), entry.getStore().asNeeded(), e.getPath(), new SimpleBooleanProperty());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -80,8 +83,8 @@ public class BrowserModel {
|
|||
var list = new ArrayList<BrowserSavedState.Entry>();
|
||||
openFileSystems.forEach(model -> {
|
||||
var storageEntry = DataStorage.get().getStoreEntryIfPresent(model.getStore());
|
||||
storageEntry.ifPresent(
|
||||
entry -> list.add(new BrowserSavedState.Entry(entry.getUuid(), model.getCurrentPath().get())));
|
||||
storageEntry.ifPresent(entry -> list.add(new BrowserSavedState.Entry(
|
||||
entry.getUuid(), model.getCurrentPath().get())));
|
||||
});
|
||||
|
||||
// Don't override state if it is empty
|
||||
|
@ -162,6 +165,8 @@ public class BrowserModel {
|
|||
ThreadHelper.runFailableAsync(() -> {
|
||||
OpenFileSystemModel model;
|
||||
|
||||
// Prevent multiple calls from interfering with each other
|
||||
synchronized (BrowserModel.this) {
|
||||
try (var b = new BusyProperty(externalBusy != null ? externalBusy : new SimpleBooleanProperty())) {
|
||||
model = new OpenFileSystemModel(name, this, store);
|
||||
model.initFileSystem();
|
||||
|
@ -170,6 +175,7 @@ public class BrowserModel {
|
|||
|
||||
openFileSystems.add(model);
|
||||
selected.setValue(model);
|
||||
}
|
||||
if (path != null) {
|
||||
model.initWithGivenDirectory(path);
|
||||
} else {
|
||||
|
|
|
@ -42,7 +42,7 @@ public class BrowserNavBar extends SimpleComp {
|
|||
path.set(newValue);
|
||||
});
|
||||
path.addListener((observable, oldValue, newValue) -> {
|
||||
var changed = model.cd(newValue);
|
||||
var changed = model.cdOrRetry(newValue, true);
|
||||
changed.ifPresent(path::set);
|
||||
});
|
||||
|
||||
|
|
|
@ -32,10 +32,12 @@ public class FileSystemHelper {
|
|||
.get()
|
||||
.getOsType()
|
||||
.getHomeDirectory(fileSystem.getShell().get());
|
||||
return validateDirectoryPath(model, resolvePath(model, current));
|
||||
var r = resolveDirectoryPath(model, evaluatePath(model, adjustPath(model, current)));
|
||||
validateDirectoryPath(model, r);
|
||||
return r;
|
||||
}
|
||||
|
||||
public static String resolvePath(OpenFileSystemModel model, String path) {
|
||||
public static String adjustPath(OpenFileSystemModel model, String path) {
|
||||
if (path == null) {
|
||||
return null;
|
||||
}
|
||||
|
@ -62,7 +64,7 @@ public class FileSystemHelper {
|
|||
return path;
|
||||
}
|
||||
|
||||
public static String validateDirectoryPath(OpenFileSystemModel model, String path) throws Exception {
|
||||
public static String evaluatePath(OpenFileSystemModel model, String path) throws Exception {
|
||||
if (path == null) {
|
||||
return null;
|
||||
}
|
||||
|
@ -72,17 +74,50 @@ public class FileSystemHelper {
|
|||
return path;
|
||||
}
|
||||
|
||||
var normalized = shell.get()
|
||||
return shell.get()
|
||||
.getShellDialect()
|
||||
.normalizeDirectory(shell.get(), path)
|
||||
.evaluateExpression(shell.get(), path)
|
||||
.readStdoutOrThrow();
|
||||
|
||||
if (!model.getFileSystem().directoryExists(normalized)) {
|
||||
throw new IllegalArgumentException(String.format("Directory %s does not exist", normalized));
|
||||
}
|
||||
|
||||
model.getFileSystem().directoryAccessible(normalized);
|
||||
return FileNames.toDirectory(normalized);
|
||||
public static String resolveDirectoryPath(OpenFileSystemModel model, String path) throws Exception {
|
||||
if (path == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var shell = model.getFileSystem().getShell();
|
||||
if (shell.isEmpty()) {
|
||||
return path;
|
||||
}
|
||||
|
||||
var resolved = shell.get()
|
||||
.getShellDialect()
|
||||
.resolveDirectory(shell.get(), path)
|
||||
.withWorkingDirectory(model.getCurrentPath().get())
|
||||
.readStdoutOrThrow();
|
||||
|
||||
if (!FileNames.isAbsolute(resolved)) {
|
||||
throw new IllegalArgumentException(String.format("Directory %s is not absolute", resolved));
|
||||
}
|
||||
|
||||
return FileNames.toDirectory(resolved);
|
||||
}
|
||||
|
||||
public static void validateDirectoryPath(OpenFileSystemModel model, String path) throws Exception {
|
||||
if (path == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var shell = model.getFileSystem().getShell();
|
||||
if (shell.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!model.getFileSystem().directoryExists(path)) {
|
||||
throw new IllegalArgumentException(String.format("Directory %s does not exist", path));
|
||||
}
|
||||
|
||||
model.getFileSystem().directoryAccessible(path);
|
||||
}
|
||||
|
||||
private static FileSystem localFileSystem;
|
||||
|
|
|
@ -104,61 +104,81 @@ public final class OpenFileSystemModel {
|
|||
return new FileSystem.FileEntry(fileSystem, currentPath.get(), null, false, false, 0, null, FileKind.DIRECTORY);
|
||||
}
|
||||
|
||||
public Optional<String> cd(String path) {
|
||||
public void cd(String path) {
|
||||
cdOrRetry(path, false).ifPresent(s -> cdOrRetry(s, false));
|
||||
}
|
||||
|
||||
public Optional<String> cdOrRetry(String path, boolean allowCommands) {
|
||||
if (Objects.equals(path, currentPath.get())) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
// Fix common issues with paths
|
||||
var normalizedPath = FileSystemHelper.resolvePath(this, path);
|
||||
if (!Objects.equals(path, normalizedPath)) {
|
||||
return Optional.of(normalizedPath);
|
||||
var adjustedPath = FileSystemHelper.adjustPath(this, path);
|
||||
if (!Objects.equals(path, adjustedPath)) {
|
||||
return Optional.of(adjustedPath);
|
||||
}
|
||||
|
||||
// Evaluate optional expressions
|
||||
String evaluatedPath;
|
||||
try {
|
||||
evaluatedPath = FileSystemHelper.evaluatePath(this, adjustedPath);
|
||||
} catch (Exception ex) {
|
||||
ErrorEvent.fromThrowable(ex).handle();
|
||||
return Optional.ofNullable(currentPath.get());
|
||||
}
|
||||
|
||||
// Handle commands typed into navigation bar
|
||||
if (normalizedPath != null
|
||||
&& !FileNames.isAbsolute(normalizedPath)
|
||||
if (allowCommands && evaluatedPath != null && !FileNames.isAbsolute(evaluatedPath)
|
||||
&& fileSystem.getShell().isPresent()) {
|
||||
var directory = currentPath.get();
|
||||
var name = normalizedPath + " - "
|
||||
var name = adjustedPath + " - "
|
||||
+ XPipeDaemon.getInstance().getStoreName(store).orElse("?");
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
if (ShellDialects.ALL.stream()
|
||||
.anyMatch(dialect -> normalizedPath.startsWith(dialect.getOpenCommand()))) {
|
||||
.anyMatch(dialect -> adjustedPath.startsWith(dialect.getOpenCommand()))) {
|
||||
var cmd = fileSystem
|
||||
.getShell()
|
||||
.get()
|
||||
.subShell(normalizedPath)
|
||||
.subShell(adjustedPath)
|
||||
.initWith(fileSystem
|
||||
.getShell()
|
||||
.get()
|
||||
.getShellDialect()
|
||||
.getCdCommand(currentPath.get()))
|
||||
.prepareTerminalOpen(name);
|
||||
TerminalHelper.open(normalizedPath, cmd);
|
||||
TerminalHelper.open(adjustedPath, cmd);
|
||||
} else {
|
||||
var cmd = fileSystem
|
||||
.getShell()
|
||||
.get()
|
||||
.command(normalizedPath)
|
||||
.command(adjustedPath)
|
||||
.withWorkingDirectory(directory)
|
||||
.prepareTerminalOpen(name);
|
||||
TerminalHelper.open(normalizedPath, cmd);
|
||||
TerminalHelper.open(adjustedPath, cmd);
|
||||
}
|
||||
});
|
||||
return Optional.of(currentPath.get());
|
||||
}
|
||||
|
||||
String dirPath;
|
||||
// Evaluate optional links
|
||||
String resolvedPath;
|
||||
try {
|
||||
dirPath = FileSystemHelper.validateDirectoryPath(this, normalizedPath);
|
||||
resolvedPath = FileSystemHelper.resolveDirectoryPath(this, evaluatedPath);
|
||||
} catch (Exception ex) {
|
||||
ErrorEvent.fromThrowable(ex).handle();
|
||||
return Optional.ofNullable(currentPath.get());
|
||||
}
|
||||
|
||||
if (!Objects.equals(path, dirPath)) {
|
||||
return Optional.of(dirPath);
|
||||
if (!Objects.equals(path, resolvedPath)) {
|
||||
return Optional.ofNullable(resolvedPath);
|
||||
}
|
||||
|
||||
try {
|
||||
FileSystemHelper.validateDirectoryPath(this, resolvedPath);
|
||||
} catch (Exception ex) {
|
||||
ErrorEvent.fromThrowable(ex).handle();
|
||||
return Optional.ofNullable(currentPath.get());
|
||||
}
|
||||
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
|
|
|
@ -61,6 +61,10 @@ public interface BrowserAction {
|
|||
return true;
|
||||
}
|
||||
|
||||
default boolean automaticallyResolveLinks() {
|
||||
return true;
|
||||
}
|
||||
|
||||
default boolean isActive(OpenFileSystemModel model, List<BrowserEntry> entries) {
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -50,21 +50,22 @@ public class FileIconManager {
|
|||
|
||||
loadIfNecessary();
|
||||
|
||||
if (entry.getKind() != FileKind.DIRECTORY) {
|
||||
var r = entry.resolved();
|
||||
if (r.getKind() != FileKind.DIRECTORY) {
|
||||
for (var f : FileType.ALL) {
|
||||
if (f.matches(entry)) {
|
||||
if (f.matches(r)) {
|
||||
return getIconPath(f.getIcon());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (var f : DirectoryType.ALL) {
|
||||
if (f.matches(entry)) {
|
||||
return getIconPath(f.getIcon(entry, open));
|
||||
if (f.matches(r)) {
|
||||
return getIconPath(f.getIcon(r, open));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return entry.getKind() == FileKind.DIRECTORY
|
||||
return r.getKind() == FileKind.DIRECTORY
|
||||
? (open ? "default_folder_opened.svg" : "default_folder.svg")
|
||||
: "default_file.svg";
|
||||
}
|
||||
|
|
|
@ -1,27 +1,22 @@
|
|||
package io.xpipe.app.comp.base;
|
||||
|
||||
import atlantafx.base.controls.ModalPane;
|
||||
import atlantafx.base.layout.ModalBox;
|
||||
import atlantafx.base.theme.Styles;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.fxcomps.Comp;
|
||||
import io.xpipe.app.fxcomps.SimpleComp;
|
||||
import io.xpipe.app.fxcomps.util.PlatformThread;
|
||||
import io.xpipe.app.fxcomps.util.Shortcuts;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ButtonBar;
|
||||
import javafx.scene.control.TitledPane;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyCodeCombination;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import lombok.Value;
|
||||
import org.kordamp.ikonli.javafx.FontIcon;
|
||||
|
||||
public class ModalOverlayComp extends SimpleComp {
|
||||
|
||||
|
@ -55,8 +50,9 @@ public class ModalOverlayComp extends SimpleComp {
|
|||
}
|
||||
|
||||
if (newValue != null) {
|
||||
var l = new Label(AppI18n.get(newValue.titleKey));
|
||||
var r = newValue.content.createRegion();
|
||||
var box = new VBox(r);
|
||||
var box = new VBox(l, r);
|
||||
box.setSpacing(15);
|
||||
box.setPadding(new Insets(15));
|
||||
|
||||
|
@ -73,33 +69,16 @@ public class ModalOverlayComp extends SimpleComp {
|
|||
box.getChildren().add(buttonBar);
|
||||
}
|
||||
|
||||
var tp = new TitledPane(AppI18n.get(newValue.titleKey), box);
|
||||
tp.setCollapsible(false);
|
||||
|
||||
var closeButton = new Button(null, new FontIcon("mdi2w-window-close"));
|
||||
closeButton.setOnAction(event -> {
|
||||
var modalBox = new ModalBox(box);
|
||||
modalBox.setOnClose(event -> {
|
||||
overlayContent.setValue(null);
|
||||
event.consume();
|
||||
});
|
||||
Shortcuts.addShortcut(closeButton, new KeyCodeCombination(KeyCode.ESCAPE));
|
||||
Styles.toggleStyleClass(closeButton, Styles.FLAT);
|
||||
var close = new AnchorPane(closeButton);
|
||||
close.setPickOnBounds(false);
|
||||
AnchorPane.setTopAnchor(closeButton, 10.0);
|
||||
AnchorPane.setRightAnchor(closeButton, 10.0);
|
||||
|
||||
var stack = new StackPane(tp, close);
|
||||
stack.setPadding(new Insets(10));
|
||||
stack.setOnMouseClicked(event -> {
|
||||
if (overlayContent.getValue() != null) {
|
||||
overlayContent.setValue(null);
|
||||
}
|
||||
});
|
||||
stack.setAlignment(Pos.CENTER);
|
||||
close.maxWidthProperty().bind(tp.widthProperty());
|
||||
close.maxHeightProperty().bind(tp.heightProperty());
|
||||
tp.maxWidthProperty().bind(stack.widthProperty().add(-100));
|
||||
|
||||
modal.show(stack);
|
||||
modalBox.prefWidthProperty().bind(box.widthProperty());
|
||||
modalBox.prefHeightProperty().bind(box.heightProperty());
|
||||
modalBox.maxWidthProperty().bind(box.widthProperty());
|
||||
modalBox.maxHeightProperty().bind(box.heightProperty());
|
||||
modal.show(modalBox);
|
||||
|
||||
// Wait 2 pulses before focus so that the scene can be assigned to r
|
||||
Platform.runLater(() -> {
|
||||
|
|
|
@ -24,14 +24,14 @@ import java.util.function.Consumer;
|
|||
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public class DescriptionButtonComp extends SimpleComp {
|
||||
public class TileButtonComp extends SimpleComp {
|
||||
|
||||
private final ObservableValue<String> name;
|
||||
private final ObservableValue<String> description;
|
||||
private final ObservableValue<String> icon;
|
||||
private final Consumer<ActionEvent> action;
|
||||
|
||||
public DescriptionButtonComp(String nameKey, String descriptionKey, String icon, Consumer<ActionEvent> action) {
|
||||
public TileButtonComp(String nameKey, String descriptionKey, String icon, Consumer<ActionEvent> action) {
|
||||
this.name = AppI18n.observable(nameKey);
|
||||
this.description = AppI18n.observable(descriptionKey);
|
||||
this.icon = new SimpleStringProperty(icon);
|
|
@ -1,6 +1,6 @@
|
|||
package io.xpipe.app.prefs;
|
||||
|
||||
import io.xpipe.app.comp.base.DescriptionButtonComp;
|
||||
import io.xpipe.app.comp.base.TileButtonComp;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.core.AppLogs;
|
||||
import io.xpipe.app.core.AppWindowHelper;
|
||||
|
@ -34,7 +34,7 @@ public class AboutComp extends Comp<CompStructure<?>> {
|
|||
return new OptionsBuilder()
|
||||
.addTitle("usefulActions")
|
||||
.addComp(
|
||||
new DescriptionButtonComp("reportIssue", "reportIssueDescription", "mdal-bug_report", e -> {
|
||||
new TileButtonComp("reportIssue", "reportIssueDescription", "mdal-bug_report", e -> {
|
||||
var event = ErrorEvent.fromMessage("User Report");
|
||||
if (AppLogs.get().isWriteToFile()) {
|
||||
event.attachment(AppLogs.get().getSessionLogsDirectory());
|
||||
|
@ -45,7 +45,7 @@ public class AboutComp extends Comp<CompStructure<?>> {
|
|||
.grow(true, false),
|
||||
null)
|
||||
.addComp(
|
||||
new DescriptionButtonComp(
|
||||
new TileButtonComp(
|
||||
"openCurrentLogFile",
|
||||
"openCurrentLogFileDescription",
|
||||
"mdmz-text_snippet",
|
||||
|
@ -59,7 +59,7 @@ public class AboutComp extends Comp<CompStructure<?>> {
|
|||
.grow(true, false),
|
||||
null)
|
||||
.addComp(
|
||||
new DescriptionButtonComp(
|
||||
new TileButtonComp(
|
||||
"launchDebugMode", "launchDebugModeDescription", "mdmz-refresh", e -> {
|
||||
OperationMode.executeAfterShutdown(() -> {
|
||||
try (var sc = ShellStore.createLocal()
|
||||
|
@ -84,7 +84,7 @@ public class AboutComp extends Comp<CompStructure<?>> {
|
|||
.grow(true, false),
|
||||
null)
|
||||
.addComp(
|
||||
new DescriptionButtonComp(
|
||||
new TileButtonComp(
|
||||
"openInstallationDirectory",
|
||||
"openInstallationDirectoryDescription",
|
||||
"mdomz-snippet_folder",
|
||||
|
@ -101,7 +101,7 @@ public class AboutComp extends Comp<CompStructure<?>> {
|
|||
private Comp<?> createLinks() {
|
||||
return new OptionsBuilder()
|
||||
.addComp(
|
||||
new DescriptionButtonComp(
|
||||
new TileButtonComp(
|
||||
"securityPolicy", "securityPolicyDescription", "mdrmz-security", e -> {
|
||||
Hyperlinks.open(Hyperlinks.SECURITY);
|
||||
e.consume();
|
||||
|
@ -109,14 +109,14 @@ public class AboutComp extends Comp<CompStructure<?>> {
|
|||
.grow(true, false),
|
||||
null)
|
||||
.addComp(
|
||||
new DescriptionButtonComp("privacy", "privacyDescription", "mdomz-privacy_tip", e -> {
|
||||
new TileButtonComp("privacy", "privacyDescription", "mdomz-privacy_tip", e -> {
|
||||
Hyperlinks.open(Hyperlinks.PRIVACY);
|
||||
e.consume();
|
||||
})
|
||||
.grow(true, false),
|
||||
null)
|
||||
.addComp(
|
||||
new DescriptionButtonComp(
|
||||
new TileButtonComp(
|
||||
"thirdParty", "thirdPartyDescription", "mdi2o-open-source-initiative", e -> {
|
||||
AppWindowHelper.sideWindow(
|
||||
AppI18n.get("openSourceNotices"),
|
||||
|
@ -129,21 +129,21 @@ public class AboutComp extends Comp<CompStructure<?>> {
|
|||
.grow(true, false),
|
||||
null)
|
||||
.addComp(
|
||||
new DescriptionButtonComp("discord", "discordDescription", "mdi2d-discord", e -> {
|
||||
new TileButtonComp("discord", "discordDescription", "mdi2d-discord", e -> {
|
||||
Hyperlinks.open(Hyperlinks.DISCORD);
|
||||
e.consume();
|
||||
})
|
||||
.grow(true, false),
|
||||
null)
|
||||
.addComp(
|
||||
new DescriptionButtonComp("slack", "slackDescription", "mdi2s-slack", e -> {
|
||||
new TileButtonComp("slack", "slackDescription", "mdi2s-slack", e -> {
|
||||
Hyperlinks.open(Hyperlinks.SLACK);
|
||||
e.consume();
|
||||
})
|
||||
.grow(true, false),
|
||||
null)
|
||||
.addComp(
|
||||
new DescriptionButtonComp("github", "githubDescription", "mdi2g-github", e -> {
|
||||
new TileButtonComp("github", "githubDescription", "mdi2g-github", e -> {
|
||||
Hyperlinks.open(Hyperlinks.GITHUB);
|
||||
e.consume();
|
||||
})
|
||||
|
|
|
@ -1,59 +1,12 @@
|
|||
.scroll-bar:vertical {
|
||||
-fx-pref-width: 0.4em;
|
||||
-fx-background-color: transparent;
|
||||
-fx-padding: 0 1px 0 0;
|
||||
-fx-pref-width: 0.3em;
|
||||
-fx-padding: 0.3em 0 0.3em 0;
|
||||
}
|
||||
|
||||
.scroll-bar:vertical .track {
|
||||
-fx-padding: 12px;
|
||||
}
|
||||
|
||||
.scroll-bar:vertical > .track-background, .scroll-bar:horizontal > .track-background {
|
||||
-fx-padding: 12px;
|
||||
-fx-background-color: transparent;
|
||||
}
|
||||
|
||||
.scroll-bar:vertical > .thumb, .scroll-bar:horizontal > .thumb {
|
||||
-fx-background-color: #CCC;
|
||||
-fx-background-insets: 0;
|
||||
-fx-background-radius: 2em;
|
||||
}
|
||||
|
||||
.scroll-bar:vertical > .increment-button, .scroll-bar:vertical > .decrement-button {
|
||||
-fx-max-height: 0;
|
||||
-fx-padding: 1px;
|
||||
-fx-opacity: 0;
|
||||
}
|
||||
|
||||
.scroll-bar:vertical .increment-arrow, .scroll-bar:vertical .decrement-arrow {
|
||||
fx-max-height: 0;
|
||||
-fx-padding: 1px;
|
||||
-fx-opacity: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.scroll-bar:horizontal {
|
||||
-fx-pref-height: 0.4em;
|
||||
-fx-pref-height: 0.3em;
|
||||
}
|
||||
|
||||
.scroll-bar:horizontal .track {
|
||||
-fx-padding: 12px;
|
||||
}
|
||||
|
||||
.scroll-bar:horizontal > .increment-button, .scroll-bar:horizontal > .decrement-button {
|
||||
-fx-max-width: 0;
|
||||
-fx-padding: 1px;
|
||||
-fx-opacity: 0;
|
||||
}
|
||||
|
||||
.scroll-bar:horizontal .increment-arrow, .scroll-bar:horizontal .decrement-arrow {
|
||||
fx-max-width: 0;
|
||||
-fx-padding: 1px;
|
||||
-fx-opacity: 0;
|
||||
}
|
||||
|
||||
|
||||
.scroll-pane {
|
||||
-fx-background-insets: 0;
|
||||
-fx-padding: 0;
|
||||
|
|
|
@ -31,7 +31,9 @@ public interface ShellDialect {
|
|||
|
||||
CommandControl directoryExists(ShellControl shellControl, String directory);
|
||||
|
||||
CommandControl normalizeDirectory(ShellControl shellControl, String directory);
|
||||
CommandControl evaluateExpression(ShellControl shellControl, String s);
|
||||
|
||||
CommandControl resolveDirectory(ShellControl shellControl, String directory);
|
||||
|
||||
String fileArgument(String s);
|
||||
|
||||
|
|
|
@ -2,8 +2,11 @@ package io.xpipe.core.store;
|
|||
|
||||
import io.xpipe.core.impl.FileNames;
|
||||
import io.xpipe.core.process.ShellControl;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NonNull;
|
||||
import lombok.Setter;
|
||||
import lombok.Value;
|
||||
import lombok.experimental.NonFinal;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.InputStream;
|
||||
|
@ -17,11 +20,14 @@ import java.util.stream.Stream;
|
|||
public interface FileSystem extends Closeable, AutoCloseable {
|
||||
|
||||
@Value
|
||||
@NonFinal
|
||||
class FileEntry {
|
||||
@NonNull
|
||||
FileSystem fileSystem;
|
||||
|
||||
@NonNull
|
||||
@NonFinal
|
||||
@Setter
|
||||
String path;
|
||||
|
||||
Instant date;
|
||||
|
@ -52,11 +58,34 @@ public interface FileSystem extends Closeable, AutoCloseable {
|
|||
this.size = size;
|
||||
}
|
||||
|
||||
public FileEntry resolved() {
|
||||
return this;
|
||||
}
|
||||
|
||||
public static FileEntry ofDirectory(FileSystem fileSystem, String path) {
|
||||
return new FileEntry(fileSystem, path, Instant.now(), true, false, 0, null, FileKind.DIRECTORY);
|
||||
}
|
||||
}
|
||||
|
||||
@Value
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
class LinkFileEntry extends FileEntry {
|
||||
|
||||
@NonNull
|
||||
FileEntry target;
|
||||
|
||||
public LinkFileEntry(
|
||||
@NonNull FileSystem fileSystem, @NonNull String path, Instant date, boolean hidden, Boolean executable, long size, String mode, @NonNull FileEntry target
|
||||
) {
|
||||
super(fileSystem, path, date, hidden, executable, size, mode, FileKind.LINK);
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
public FileEntry resolved() {
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
FileSystemStore getStore();
|
||||
|
||||
Optional<ShellControl> getShell();
|
||||
|
|
|
@ -7,6 +7,7 @@ import io.xpipe.app.browser.action.BrowserAction;
|
|||
import io.xpipe.app.browser.action.BrowserActionFormatter;
|
||||
import io.xpipe.app.browser.action.LeafAction;
|
||||
import io.xpipe.core.impl.FileNames;
|
||||
import io.xpipe.core.store.FileKind;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyCodeCombination;
|
||||
import javafx.scene.input.KeyCombination;
|
||||
|
@ -49,6 +50,11 @@ public class CopyPathAction implements BrowserAction, BranchAction {
|
|||
return "Absolute Path";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean automaticallyResolveLinks() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyCombination getShortcut() {
|
||||
return new KeyCodeCombination(KeyCode.C, KeyCombination.ALT_DOWN, KeyCombination.SHORTCUT_DOWN);
|
||||
|
@ -64,6 +70,40 @@ public class CopyPathAction implements BrowserAction, BranchAction {
|
|||
clipboard.setContents(selection, selection);
|
||||
}
|
||||
},
|
||||
new LeafAction() {
|
||||
@Override
|
||||
public String getName(OpenFileSystemModel model, List<BrowserEntry> entries) {
|
||||
if (entries.size() == 1) {
|
||||
return " "
|
||||
+ BrowserActionFormatter.centerEllipsis(
|
||||
entries.get(0).getRawFileEntry().getPath(), 50);
|
||||
}
|
||||
|
||||
return "Absolute Link Path";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(OpenFileSystemModel model, List<BrowserEntry> entries) {
|
||||
return entries.stream()
|
||||
.allMatch(browserEntry ->
|
||||
browserEntry.getRawFileEntry().getKind() == FileKind.LINK);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean automaticallyResolveLinks() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(OpenFileSystemModel model, List<BrowserEntry> entries) {
|
||||
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<BrowserEntry> entries) {
|
||||
|
@ -126,6 +166,50 @@ public class CopyPathAction implements BrowserAction, BranchAction {
|
|||
clipboard.setContents(selection, selection);
|
||||
}
|
||||
},
|
||||
new LeafAction() {
|
||||
@Override
|
||||
public String getName(OpenFileSystemModel model, List<BrowserEntry> entries) {
|
||||
if (entries.size() == 1) {
|
||||
return " "
|
||||
+ BrowserActionFormatter.centerEllipsis(
|
||||
FileNames.getFileName(entries.get(0)
|
||||
.getRawFileEntry()
|
||||
.getPath()),
|
||||
50);
|
||||
}
|
||||
|
||||
return "Link File Name";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(OpenFileSystemModel model, List<BrowserEntry> entries) {
|
||||
return entries.stream()
|
||||
.allMatch(browserEntry ->
|
||||
browserEntry.getRawFileEntry().getKind() == FileKind.LINK)
|
||||
&& entries.stream().anyMatch(browserEntry -> !browserEntry
|
||||
.getFileName()
|
||||
.equals(FileNames.getFileName(browserEntry
|
||||
.getRawFileEntry()
|
||||
.resolved()
|
||||
.getPath())));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean automaticallyResolveLinks() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(OpenFileSystemModel model, List<BrowserEntry> entries) {
|
||||
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<BrowserEntry> entries) {
|
||||
|
|
|
@ -5,6 +5,7 @@ import io.xpipe.app.browser.BrowserEntry;
|
|||
import io.xpipe.app.browser.FileSystemHelper;
|
||||
import io.xpipe.app.browser.OpenFileSystemModel;
|
||||
import io.xpipe.app.browser.action.LeafAction;
|
||||
import io.xpipe.core.store.FileKind;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyCodeCombination;
|
||||
|
@ -43,6 +44,6 @@ public class DeleteAction implements LeafAction {
|
|||
|
||||
@Override
|
||||
public String getName(OpenFileSystemModel model, List<BrowserEntry> entries) {
|
||||
return "Delete";
|
||||
return "Delete" + (entries.stream().allMatch(browserEntry -> browserEntry.getRawFileEntry().getKind() == FileKind.LINK) ? " link" : "");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
package io.xpipe.ext.base.browser;
|
||||
|
||||
import io.xpipe.app.browser.BrowserEntry;
|
||||
import io.xpipe.app.browser.OpenFileSystemModel;
|
||||
import io.xpipe.app.browser.action.LeafAction;
|
||||
import io.xpipe.core.impl.FileNames;
|
||||
import io.xpipe.core.store.FileKind;
|
||||
import javafx.scene.Node;
|
||||
import org.kordamp.ikonli.javafx.FontIcon;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class FollowLinkAction implements LeafAction {
|
||||
|
||||
@Override
|
||||
public boolean automaticallyResolveLinks() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(OpenFileSystemModel model, List<BrowserEntry> entries) {
|
||||
var target = FileNames.getParent(entries.get(0).getRawFileEntry().resolved().getPath());
|
||||
model.cd(target);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Category getCategory() {
|
||||
return Category.OPEN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Node getIcon(OpenFileSystemModel model, List<BrowserEntry> entries) {
|
||||
return new FontIcon("mdi2a-arrow-top-right-thick");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(OpenFileSystemModel model, List<BrowserEntry> entries) {
|
||||
return entries.size() == 1
|
||||
&& entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.LINK && entry.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName(OpenFileSystemModel model, List<BrowserEntry> entries) {
|
||||
return "Follow link";
|
||||
}
|
||||
}
|
|
@ -27,6 +27,7 @@ open module io.xpipe.ext.base {
|
|||
requires com.sun.jna.platform;
|
||||
|
||||
provides BrowserAction with
|
||||
FollowLinkAction,
|
||||
BackAction,
|
||||
ForwardAction,
|
||||
RefreshAction,
|
||||
|
|
Loading…
Reference in a new issue