More support for symlinks

This commit is contained in:
crschnick 2023-06-18 17:49:28 +00:00
parent f8b2afe44c
commit 7605a4331a
20 changed files with 357 additions and 193 deletions

View file

@ -80,9 +80,7 @@ public class BrowserBreadcrumbBar extends SimpleComp {
} }
breadcrumbs.selectedCrumbProperty().addListener((obs, old, val) -> { breadcrumbs.selectedCrumbProperty().addListener((obs, old, val) -> {
model.cd(val != null ? val.getValue() : null).ifPresent(s -> { model.cd(val != null ? val.getValue() : null);
model.cd(s);
});
}); });
return breadcrumbs; return breadcrumbs;

View file

@ -38,7 +38,8 @@ final class BrowserContextMenu extends ContextMenu {
var all = BrowserAction.ALL.stream() var all = BrowserAction.ALL.stream()
.filter(browserAction -> browserAction.getCategory() == cat) .filter(browserAction -> browserAction.getCategory() == cat)
.filter(browserAction -> { .filter(browserAction -> {
if (!browserAction.isApplicable(model, selected)) { var used = resolveIfNeeded(browserAction, selected);
if (!browserAction.isApplicable(model, used)) {
return false; return false;
} }
@ -58,26 +59,40 @@ final class BrowserContextMenu extends ContextMenu {
} }
for (BrowserAction a : all) { for (BrowserAction a : all) {
var used = resolveIfNeeded(a, selected);
if (a instanceof LeafAction la) { 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) { 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()) { for (LeafAction sub : la.getBranchingActions()) {
if (!sub.isApplicable(model, selected)) { var subUsed = resolveIfNeeded(sub, selected);
if (!sub.isApplicable(model, subUsed)) {
continue; 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) { if (graphic != null) {
m.setGraphic(graphic); m.setGraphic(graphic);
} }
m.setDisable(!a.isActive(model, selected)); m.setDisable(!a.isActive(model, used));
getItems().add(m); 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;
}
} }

View file

@ -9,7 +9,6 @@ import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.augment.ContextMenuAugment; import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
import io.xpipe.app.fxcomps.impl.SvgCacheComp; import io.xpipe.app.fxcomps.impl.SvgCacheComp;
import io.xpipe.app.fxcomps.util.PlatformThread; 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.BusyProperty;
import io.xpipe.app.util.HumanReadableFormat; import io.xpipe.app.util.HumanReadableFormat;
import io.xpipe.app.util.ThreadHelper; import io.xpipe.app.util.ThreadHelper;
@ -23,7 +22,6 @@ import javafx.beans.property.*;
import javafx.beans.value.ChangeListener; import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener; import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass; import javafx.css.PseudoClass;
import javafx.geometry.Bounds; import javafx.geometry.Bounds;
import javafx.geometry.Pos; import javafx.geometry.Pos;
@ -67,9 +65,6 @@ final class BrowserFileListComp extends SimpleComp {
@Override @Override
protected Region createSimple() { protected Region createSimple() {
TableView<BrowserEntry> table = createTable(); TableView<BrowserEntry> table = createTable();
SimpleChangeListener.apply(table.comparatorProperty(), (newValue) -> {
fileList.setComparator(newValue);
});
return table; return table;
} }
@ -87,17 +82,17 @@ final class BrowserFileListComp extends SimpleComp {
var sizeCol = new TableColumn<BrowserEntry, Number>("Size"); var sizeCol = new TableColumn<BrowserEntry, Number>("Size");
sizeCol.setCellValueFactory(param -> sizeCol.setCellValueFactory(param ->
new SimpleLongProperty(param.getValue().getRawFileEntry().getSize())); new SimpleLongProperty(param.getValue().getRawFileEntry().resolved().getSize()));
sizeCol.setCellFactory(col -> new FileSizeCell()); sizeCol.setCellFactory(col -> new FileSizeCell());
var mtimeCol = new TableColumn<BrowserEntry, Instant>("Modified"); var mtimeCol = new TableColumn<BrowserEntry, Instant>("Modified");
mtimeCol.setCellValueFactory(param -> mtimeCol.setCellValueFactory(param ->
new SimpleObjectProperty<>(param.getValue().getRawFileEntry().getDate())); new SimpleObjectProperty<>(param.getValue().getRawFileEntry().resolved().getDate()));
mtimeCol.setCellFactory(col -> new FileTimeCell()); mtimeCol.setCellFactory(col -> new FileTimeCell());
var modeCol = new TableColumn<BrowserEntry, String>("Attributes"); var modeCol = new TableColumn<BrowserEntry, String>("Attributes");
modeCol.setCellValueFactory(param -> modeCol.setCellValueFactory(param ->
new SimpleObjectProperty<>(param.getValue().getRawFileEntry().getMode())); new SimpleObjectProperty<>(param.getValue().getRawFileEntry().resolved().getMode()));
modeCol.setCellFactory(col -> new FileModeCell()); modeCol.setCellFactory(col -> new FileModeCell());
modeCol.setSortable(false); modeCol.setSortable(false);
@ -109,7 +104,8 @@ final class BrowserFileListComp extends SimpleComp {
table.getSortOrder().add(filenameCol); table.getSortOrder().add(filenameCol);
table.setFocusTraversable(true); table.setFocusTraversable(true);
table.setSortPolicy(param -> { table.setSortPolicy(param -> {
return sort(table, param.getItems()); fileList.setComparator(table.getComparator());
return true;
}); });
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN); table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN);
filenameCol.minWidthProperty().bind(table.widthProperty().multiply(0.5)); filenameCol.minWidthProperty().bind(table.widthProperty().multiply(0.5));
@ -124,22 +120,6 @@ final class BrowserFileListComp extends SimpleComp {
return table; 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) { private void prepareTableSelectionModel(TableView<BrowserEntry> table) {
if (!fileList.getMode().isMultiple()) { if (!fileList.getMode().isMultiple()) {
table.getSelectionModel().setSelectionMode(SelectionMode.SINGLE); table.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
@ -256,12 +236,12 @@ final class BrowserFileListComp extends SimpleComp {
} }
if (row.getItem() != null if (row.getItem() != null
&& row.getItem().getRawFileEntry().getKind() == FileKind.DIRECTORY) { && row.getItem().getRawFileEntry().resolved().getKind() == FileKind.DIRECTORY) {
return event.getButton() == MouseButton.SECONDARY; return event.getButton() == MouseButton.SECONDARY;
} }
if (row.getItem() != null if (row.getItem() != null
&& row.getItem().getRawFileEntry().getKind() != FileKind.DIRECTORY) { && row.getItem().getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY) {
return event.getButton() == MouseButton.SECONDARY return event.getButton() == MouseButton.SECONDARY
|| event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2; || event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2;
} }
@ -365,7 +345,6 @@ final class BrowserFileListComp extends SimpleComp {
if (!table.getItems().equals(newItems)) { if (!table.getItems().equals(newItems)) {
// Sort the list ourselves as sorting the table would incur a lot of cell updates // Sort the list ourselves as sorting the table would incur a lot of cell updates
var obs = FXCollections.observableList(newItems); var obs = FXCollections.observableList(newItems);
sort(table, obs);
table.getItems().setAll(obs); table.getItems().setAll(obs);
// table.sort(); // table.sort();
} }
@ -497,7 +476,15 @@ final class BrowserFileListComp extends SimpleComp {
var isDirectory = getTableRow().getItem().getRawFileEntry().getKind() == FileKind.DIRECTORY; var isDirectory = getTableRow().getItem().getRawFileEntry().getKind() == FileKind.DIRECTORY;
pseudoClassStateChanged(FOLDER, isDirectory); 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 var hidden = !isParentLink
&& (getTableRow().getItem().getRawFileEntry().isHidden() || fileName.startsWith(".")); && (getTableRow().getItem().getRawFileEntry().isHidden() || fileName.startsWith("."));
getTableRow().pseudoClassStateChanged(HIDDEN, hidden); getTableRow().pseudoClassStateChanged(HIDDEN, hidden);
@ -519,7 +506,7 @@ final class BrowserFileListComp extends SimpleComp {
setText(null); setText(null);
} else { } else {
var path = getTableRow().getItem(); var path = getTableRow().getItem();
if (path.getRawFileEntry().getKind() == FileKind.DIRECTORY) { if (path.getRawFileEntry().resolved().getKind() == FileKind.DIRECTORY) {
setText(""); setText("");
} else { } else {
setText(byteCount(fileSize.longValue())); setText(byteCount(fileSize.longValue()));

View file

@ -17,16 +17,13 @@ import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.function.Predicate;
import java.util.stream.Stream; import java.util.stream.Stream;
@Getter @Getter
public final class BrowserFileListModel { public final class BrowserFileListModel {
static final Comparator<BrowserEntry> FILE_TYPE_COMPARATOR = static final Comparator<BrowserEntry> FILE_TYPE_COMPARATOR =
Comparator.comparing(path -> path.getRawFileEntry().getKind() != FileKind.DIRECTORY); Comparator.comparing(path -> path.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY);
static final Predicate<BrowserEntry> PREDICATE_ANY = path -> true;
static final Predicate<BrowserEntry> PREDICATE_NOT_HIDDEN = path -> true;
private final OpenFileSystemModel fileSystemModel; private final OpenFileSystemModel fileSystemModel;
private final Property<Comparator<BrowserEntry>> comparatorProperty = private final Property<Comparator<BrowserEntry>> comparatorProperty =
@ -95,10 +92,21 @@ public final class BrowserFileListModel {
var comparator = var comparator =
tableComparator != null ? FILE_TYPE_COMPARATOR.thenComparing(tableComparator) : FILE_TYPE_COMPARATOR; tableComparator != null ? FILE_TYPE_COMPARATOR.thenComparing(tableComparator) : FILE_TYPE_COMPARATOR;
var listCopy = new ArrayList<>(filtered); var listCopy = new ArrayList<>(filtered);
listCopy.sort(comparator); sort(listCopy);
shown.setValue(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) { public boolean rename(String filename, String newName) {
var fullPath = FileNames.join(fileSystemModel.getCurrentPath().get(), filename); var fullPath = FileNames.join(fileSystemModel.getCurrentPath().get(), filename);
var newFullPath = FileNames.join(fileSystemModel.getCurrentPath().get(), newName); var newFullPath = FileNames.join(fileSystemModel.getCurrentPath().get(), newName);
@ -113,19 +121,14 @@ public final class BrowserFileListModel {
} }
public void onDoubleClick(BrowserEntry entry) { 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)) { && getMode().equals(BrowserModel.Mode.SINGLE_FILE_CHOOSER)) {
getFileSystemModel().getBrowserModel().finishChooser(); getFileSystemModel().getBrowserModel().finishChooser();
return; return;
} }
if (entry.getRawFileEntry().getKind() == FileKind.DIRECTORY) { if (entry.getRawFileEntry().resolved().getKind() == FileKind.DIRECTORY) {
var dir = fileSystemModel.cd(entry.getRawFileEntry().getPath()); fileSystemModel.cd(entry.getRawFileEntry().resolved().getPath());
if (dir.isPresent()) {
fileSystemModel.cd(dir.get());
}
} else {
// FileOpener.openInTextEditor(entry.getRawFileEntry());
} }
} }
} }

View file

@ -15,7 +15,9 @@ import javafx.collections.ObservableList;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import java.util.*; import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer; import java.util.function.Consumer;
@Getter @Getter
@ -71,7 +73,8 @@ public class BrowserModel {
state.getLastSystems().forEach(e -> { state.getLastSystems().forEach(e -> {
var storageEntry = DataStorage.get().getStoreEntry(e.getUuid()); var storageEntry = DataStorage.get().getStoreEntry(e.getUuid());
storageEntry.ifPresent(entry -> { 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>(); var list = new ArrayList<BrowserSavedState.Entry>();
openFileSystems.forEach(model -> { openFileSystems.forEach(model -> {
var storageEntry = DataStorage.get().getStoreEntryIfPresent(model.getStore()); var storageEntry = DataStorage.get().getStoreEntryIfPresent(model.getStore());
storageEntry.ifPresent( storageEntry.ifPresent(entry -> list.add(new BrowserSavedState.Entry(
entry -> list.add(new BrowserSavedState.Entry(entry.getUuid(), model.getCurrentPath().get()))); entry.getUuid(), model.getCurrentPath().get())));
}); });
// Don't override state if it is empty // Don't override state if it is empty
@ -162,6 +165,8 @@ public class BrowserModel {
ThreadHelper.runFailableAsync(() -> { ThreadHelper.runFailableAsync(() -> {
OpenFileSystemModel model; OpenFileSystemModel model;
// Prevent multiple calls from interfering with each other
synchronized (BrowserModel.this) {
try (var b = new BusyProperty(externalBusy != null ? externalBusy : new SimpleBooleanProperty())) { try (var b = new BusyProperty(externalBusy != null ? externalBusy : new SimpleBooleanProperty())) {
model = new OpenFileSystemModel(name, this, store); model = new OpenFileSystemModel(name, this, store);
model.initFileSystem(); model.initFileSystem();
@ -170,6 +175,7 @@ public class BrowserModel {
openFileSystems.add(model); openFileSystems.add(model);
selected.setValue(model); selected.setValue(model);
}
if (path != null) { if (path != null) {
model.initWithGivenDirectory(path); model.initWithGivenDirectory(path);
} else { } else {

View file

@ -42,7 +42,7 @@ public class BrowserNavBar extends SimpleComp {
path.set(newValue); path.set(newValue);
}); });
path.addListener((observable, oldValue, newValue) -> { path.addListener((observable, oldValue, newValue) -> {
var changed = model.cd(newValue); var changed = model.cdOrRetry(newValue, true);
changed.ifPresent(path::set); changed.ifPresent(path::set);
}); });

View file

@ -32,10 +32,12 @@ public class FileSystemHelper {
.get() .get()
.getOsType() .getOsType()
.getHomeDirectory(fileSystem.getShell().get()); .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) { if (path == null) {
return null; return null;
} }
@ -62,7 +64,7 @@ public class FileSystemHelper {
return path; 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) { if (path == null) {
return null; return null;
} }
@ -72,17 +74,50 @@ public class FileSystemHelper {
return path; return path;
} }
var normalized = shell.get() return shell.get()
.getShellDialect() .getShellDialect()
.normalizeDirectory(shell.get(), path) .evaluateExpression(shell.get(), path)
.readStdoutOrThrow(); .readStdoutOrThrow();
if (!model.getFileSystem().directoryExists(normalized)) {
throw new IllegalArgumentException(String.format("Directory %s does not exist", normalized));
} }
model.getFileSystem().directoryAccessible(normalized); public static String resolveDirectoryPath(OpenFileSystemModel model, String path) throws Exception {
return FileNames.toDirectory(normalized); 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; private static FileSystem localFileSystem;

View file

@ -104,61 +104,81 @@ public final class OpenFileSystemModel {
return new FileSystem.FileEntry(fileSystem, currentPath.get(), null, false, false, 0, null, FileKind.DIRECTORY); 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())) { if (Objects.equals(path, currentPath.get())) {
return Optional.empty(); return Optional.empty();
} }
// Fix common issues with paths // Fix common issues with paths
var normalizedPath = FileSystemHelper.resolvePath(this, path); var adjustedPath = FileSystemHelper.adjustPath(this, path);
if (!Objects.equals(path, normalizedPath)) { if (!Objects.equals(path, adjustedPath)) {
return Optional.of(normalizedPath); 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 // Handle commands typed into navigation bar
if (normalizedPath != null if (allowCommands && evaluatedPath != null && !FileNames.isAbsolute(evaluatedPath)
&& !FileNames.isAbsolute(normalizedPath)
&& fileSystem.getShell().isPresent()) { && fileSystem.getShell().isPresent()) {
var directory = currentPath.get(); var directory = currentPath.get();
var name = normalizedPath + " - " var name = adjustedPath + " - "
+ XPipeDaemon.getInstance().getStoreName(store).orElse("?"); + XPipeDaemon.getInstance().getStoreName(store).orElse("?");
ThreadHelper.runFailableAsync(() -> { ThreadHelper.runFailableAsync(() -> {
if (ShellDialects.ALL.stream() if (ShellDialects.ALL.stream()
.anyMatch(dialect -> normalizedPath.startsWith(dialect.getOpenCommand()))) { .anyMatch(dialect -> adjustedPath.startsWith(dialect.getOpenCommand()))) {
var cmd = fileSystem var cmd = fileSystem
.getShell() .getShell()
.get() .get()
.subShell(normalizedPath) .subShell(adjustedPath)
.initWith(fileSystem .initWith(fileSystem
.getShell() .getShell()
.get() .get()
.getShellDialect() .getShellDialect()
.getCdCommand(currentPath.get())) .getCdCommand(currentPath.get()))
.prepareTerminalOpen(name); .prepareTerminalOpen(name);
TerminalHelper.open(normalizedPath, cmd); TerminalHelper.open(adjustedPath, cmd);
} else { } else {
var cmd = fileSystem var cmd = fileSystem
.getShell() .getShell()
.get() .get()
.command(normalizedPath) .command(adjustedPath)
.withWorkingDirectory(directory) .withWorkingDirectory(directory)
.prepareTerminalOpen(name); .prepareTerminalOpen(name);
TerminalHelper.open(normalizedPath, cmd); TerminalHelper.open(adjustedPath, cmd);
} }
}); });
return Optional.of(currentPath.get()); return Optional.of(currentPath.get());
} }
String dirPath; // Evaluate optional links
String resolvedPath;
try { try {
dirPath = FileSystemHelper.validateDirectoryPath(this, normalizedPath); resolvedPath = FileSystemHelper.resolveDirectoryPath(this, evaluatedPath);
} catch (Exception ex) { } catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle(); ErrorEvent.fromThrowable(ex).handle();
return Optional.ofNullable(currentPath.get()); return Optional.ofNullable(currentPath.get());
} }
if (!Objects.equals(path, dirPath)) { if (!Objects.equals(path, resolvedPath)) {
return Optional.of(dirPath); return Optional.ofNullable(resolvedPath);
}
try {
FileSystemHelper.validateDirectoryPath(this, resolvedPath);
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle();
return Optional.ofNullable(currentPath.get());
} }
ThreadHelper.runFailableAsync(() -> { ThreadHelper.runFailableAsync(() -> {

View file

@ -61,6 +61,10 @@ public interface BrowserAction {
return true; return true;
} }
default boolean automaticallyResolveLinks() {
return true;
}
default boolean isActive(OpenFileSystemModel model, List<BrowserEntry> entries) { default boolean isActive(OpenFileSystemModel model, List<BrowserEntry> entries) {
return true; return true;
} }

View file

@ -50,21 +50,22 @@ public class FileIconManager {
loadIfNecessary(); loadIfNecessary();
if (entry.getKind() != FileKind.DIRECTORY) { var r = entry.resolved();
if (r.getKind() != FileKind.DIRECTORY) {
for (var f : FileType.ALL) { for (var f : FileType.ALL) {
if (f.matches(entry)) { if (f.matches(r)) {
return getIconPath(f.getIcon()); return getIconPath(f.getIcon());
} }
} }
} else { } else {
for (var f : DirectoryType.ALL) { for (var f : DirectoryType.ALL) {
if (f.matches(entry)) { if (f.matches(r)) {
return getIconPath(f.getIcon(entry, open)); return getIconPath(f.getIcon(r, open));
} }
} }
} }
return entry.getKind() == FileKind.DIRECTORY return r.getKind() == FileKind.DIRECTORY
? (open ? "default_folder_opened.svg" : "default_folder.svg") ? (open ? "default_folder_opened.svg" : "default_folder.svg")
: "default_file.svg"; : "default_file.svg";
} }

View file

@ -1,27 +1,22 @@
package io.xpipe.app.comp.base; package io.xpipe.app.comp.base;
import atlantafx.base.controls.ModalPane; import atlantafx.base.controls.ModalPane;
import atlantafx.base.layout.ModalBox;
import atlantafx.base.theme.Styles; import atlantafx.base.theme.Styles;
import io.xpipe.app.core.AppI18n; import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.Shortcuts;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.property.Property; import javafx.beans.property.Property;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.ButtonBar; import javafx.scene.control.ButtonBar;
import javafx.scene.control.TitledPane; import javafx.scene.control.Label;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import lombok.Value; import lombok.Value;
import org.kordamp.ikonli.javafx.FontIcon;
public class ModalOverlayComp extends SimpleComp { public class ModalOverlayComp extends SimpleComp {
@ -55,8 +50,9 @@ public class ModalOverlayComp extends SimpleComp {
} }
if (newValue != null) { if (newValue != null) {
var l = new Label(AppI18n.get(newValue.titleKey));
var r = newValue.content.createRegion(); var r = newValue.content.createRegion();
var box = new VBox(r); var box = new VBox(l, r);
box.setSpacing(15); box.setSpacing(15);
box.setPadding(new Insets(15)); box.setPadding(new Insets(15));
@ -73,33 +69,16 @@ public class ModalOverlayComp extends SimpleComp {
box.getChildren().add(buttonBar); box.getChildren().add(buttonBar);
} }
var tp = new TitledPane(AppI18n.get(newValue.titleKey), box); var modalBox = new ModalBox(box);
tp.setCollapsible(false); modalBox.setOnClose(event -> {
var closeButton = new Button(null, new FontIcon("mdi2w-window-close"));
closeButton.setOnAction(event -> {
overlayContent.setValue(null); overlayContent.setValue(null);
event.consume();
}); });
Shortcuts.addShortcut(closeButton, new KeyCodeCombination(KeyCode.ESCAPE)); modalBox.prefWidthProperty().bind(box.widthProperty());
Styles.toggleStyleClass(closeButton, Styles.FLAT); modalBox.prefHeightProperty().bind(box.heightProperty());
var close = new AnchorPane(closeButton); modalBox.maxWidthProperty().bind(box.widthProperty());
close.setPickOnBounds(false); modalBox.maxHeightProperty().bind(box.heightProperty());
AnchorPane.setTopAnchor(closeButton, 10.0); modal.show(modalBox);
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);
// Wait 2 pulses before focus so that the scene can be assigned to r // Wait 2 pulses before focus so that the scene can be assigned to r
Platform.runLater(() -> { Platform.runLater(() -> {

View file

@ -24,14 +24,14 @@ import java.util.function.Consumer;
@AllArgsConstructor @AllArgsConstructor
@Getter @Getter
public class DescriptionButtonComp extends SimpleComp { public class TileButtonComp extends SimpleComp {
private final ObservableValue<String> name; private final ObservableValue<String> name;
private final ObservableValue<String> description; private final ObservableValue<String> description;
private final ObservableValue<String> icon; private final ObservableValue<String> icon;
private final Consumer<ActionEvent> action; 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.name = AppI18n.observable(nameKey);
this.description = AppI18n.observable(descriptionKey); this.description = AppI18n.observable(descriptionKey);
this.icon = new SimpleStringProperty(icon); this.icon = new SimpleStringProperty(icon);

View file

@ -1,6 +1,6 @@
package io.xpipe.app.prefs; 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.AppI18n;
import io.xpipe.app.core.AppLogs; import io.xpipe.app.core.AppLogs;
import io.xpipe.app.core.AppWindowHelper; import io.xpipe.app.core.AppWindowHelper;
@ -34,7 +34,7 @@ public class AboutComp extends Comp<CompStructure<?>> {
return new OptionsBuilder() return new OptionsBuilder()
.addTitle("usefulActions") .addTitle("usefulActions")
.addComp( .addComp(
new DescriptionButtonComp("reportIssue", "reportIssueDescription", "mdal-bug_report", e -> { new TileButtonComp("reportIssue", "reportIssueDescription", "mdal-bug_report", e -> {
var event = ErrorEvent.fromMessage("User Report"); var event = ErrorEvent.fromMessage("User Report");
if (AppLogs.get().isWriteToFile()) { if (AppLogs.get().isWriteToFile()) {
event.attachment(AppLogs.get().getSessionLogsDirectory()); event.attachment(AppLogs.get().getSessionLogsDirectory());
@ -45,7 +45,7 @@ public class AboutComp extends Comp<CompStructure<?>> {
.grow(true, false), .grow(true, false),
null) null)
.addComp( .addComp(
new DescriptionButtonComp( new TileButtonComp(
"openCurrentLogFile", "openCurrentLogFile",
"openCurrentLogFileDescription", "openCurrentLogFileDescription",
"mdmz-text_snippet", "mdmz-text_snippet",
@ -59,7 +59,7 @@ public class AboutComp extends Comp<CompStructure<?>> {
.grow(true, false), .grow(true, false),
null) null)
.addComp( .addComp(
new DescriptionButtonComp( new TileButtonComp(
"launchDebugMode", "launchDebugModeDescription", "mdmz-refresh", e -> { "launchDebugMode", "launchDebugModeDescription", "mdmz-refresh", e -> {
OperationMode.executeAfterShutdown(() -> { OperationMode.executeAfterShutdown(() -> {
try (var sc = ShellStore.createLocal() try (var sc = ShellStore.createLocal()
@ -84,7 +84,7 @@ public class AboutComp extends Comp<CompStructure<?>> {
.grow(true, false), .grow(true, false),
null) null)
.addComp( .addComp(
new DescriptionButtonComp( new TileButtonComp(
"openInstallationDirectory", "openInstallationDirectory",
"openInstallationDirectoryDescription", "openInstallationDirectoryDescription",
"mdomz-snippet_folder", "mdomz-snippet_folder",
@ -101,7 +101,7 @@ public class AboutComp extends Comp<CompStructure<?>> {
private Comp<?> createLinks() { private Comp<?> createLinks() {
return new OptionsBuilder() return new OptionsBuilder()
.addComp( .addComp(
new DescriptionButtonComp( new TileButtonComp(
"securityPolicy", "securityPolicyDescription", "mdrmz-security", e -> { "securityPolicy", "securityPolicyDescription", "mdrmz-security", e -> {
Hyperlinks.open(Hyperlinks.SECURITY); Hyperlinks.open(Hyperlinks.SECURITY);
e.consume(); e.consume();
@ -109,14 +109,14 @@ public class AboutComp extends Comp<CompStructure<?>> {
.grow(true, false), .grow(true, false),
null) null)
.addComp( .addComp(
new DescriptionButtonComp("privacy", "privacyDescription", "mdomz-privacy_tip", e -> { new TileButtonComp("privacy", "privacyDescription", "mdomz-privacy_tip", e -> {
Hyperlinks.open(Hyperlinks.PRIVACY); Hyperlinks.open(Hyperlinks.PRIVACY);
e.consume(); e.consume();
}) })
.grow(true, false), .grow(true, false),
null) null)
.addComp( .addComp(
new DescriptionButtonComp( new TileButtonComp(
"thirdParty", "thirdPartyDescription", "mdi2o-open-source-initiative", e -> { "thirdParty", "thirdPartyDescription", "mdi2o-open-source-initiative", e -> {
AppWindowHelper.sideWindow( AppWindowHelper.sideWindow(
AppI18n.get("openSourceNotices"), AppI18n.get("openSourceNotices"),
@ -129,21 +129,21 @@ public class AboutComp extends Comp<CompStructure<?>> {
.grow(true, false), .grow(true, false),
null) null)
.addComp( .addComp(
new DescriptionButtonComp("discord", "discordDescription", "mdi2d-discord", e -> { new TileButtonComp("discord", "discordDescription", "mdi2d-discord", e -> {
Hyperlinks.open(Hyperlinks.DISCORD); Hyperlinks.open(Hyperlinks.DISCORD);
e.consume(); e.consume();
}) })
.grow(true, false), .grow(true, false),
null) null)
.addComp( .addComp(
new DescriptionButtonComp("slack", "slackDescription", "mdi2s-slack", e -> { new TileButtonComp("slack", "slackDescription", "mdi2s-slack", e -> {
Hyperlinks.open(Hyperlinks.SLACK); Hyperlinks.open(Hyperlinks.SLACK);
e.consume(); e.consume();
}) })
.grow(true, false), .grow(true, false),
null) null)
.addComp( .addComp(
new DescriptionButtonComp("github", "githubDescription", "mdi2g-github", e -> { new TileButtonComp("github", "githubDescription", "mdi2g-github", e -> {
Hyperlinks.open(Hyperlinks.GITHUB); Hyperlinks.open(Hyperlinks.GITHUB);
e.consume(); e.consume();
}) })

View file

@ -1,59 +1,12 @@
.scroll-bar:vertical { .scroll-bar:vertical {
-fx-pref-width: 0.4em; -fx-pref-width: 0.3em;
-fx-background-color: transparent; -fx-padding: 0.3em 0 0.3em 0;
-fx-padding: 0 1px 0 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 { .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 { .scroll-pane {
-fx-background-insets: 0; -fx-background-insets: 0;
-fx-padding: 0; -fx-padding: 0;

View file

@ -31,7 +31,9 @@ public interface ShellDialect {
CommandControl directoryExists(ShellControl shellControl, String directory); 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); String fileArgument(String s);

View file

@ -2,8 +2,11 @@ package io.xpipe.core.store;
import io.xpipe.core.impl.FileNames; import io.xpipe.core.impl.FileNames;
import io.xpipe.core.process.ShellControl; import io.xpipe.core.process.ShellControl;
import lombok.EqualsAndHashCode;
import lombok.NonNull; import lombok.NonNull;
import lombok.Setter;
import lombok.Value; import lombok.Value;
import lombok.experimental.NonFinal;
import java.io.Closeable; import java.io.Closeable;
import java.io.InputStream; import java.io.InputStream;
@ -17,11 +20,14 @@ import java.util.stream.Stream;
public interface FileSystem extends Closeable, AutoCloseable { public interface FileSystem extends Closeable, AutoCloseable {
@Value @Value
@NonFinal
class FileEntry { class FileEntry {
@NonNull @NonNull
FileSystem fileSystem; FileSystem fileSystem;
@NonNull @NonNull
@NonFinal
@Setter
String path; String path;
Instant date; Instant date;
@ -52,11 +58,34 @@ public interface FileSystem extends Closeable, AutoCloseable {
this.size = size; this.size = size;
} }
public FileEntry resolved() {
return this;
}
public static FileEntry ofDirectory(FileSystem fileSystem, String path) { public static FileEntry ofDirectory(FileSystem fileSystem, String path) {
return new FileEntry(fileSystem, path, Instant.now(), true, false, 0, null, FileKind.DIRECTORY); 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(); FileSystemStore getStore();
Optional<ShellControl> getShell(); Optional<ShellControl> getShell();

View file

@ -7,6 +7,7 @@ import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.browser.action.BrowserActionFormatter; import io.xpipe.app.browser.action.BrowserActionFormatter;
import io.xpipe.app.browser.action.LeafAction; import io.xpipe.app.browser.action.LeafAction;
import io.xpipe.core.impl.FileNames; import io.xpipe.core.impl.FileNames;
import io.xpipe.core.store.FileKind;
import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination; import javafx.scene.input.KeyCombination;
@ -49,6 +50,11 @@ public class CopyPathAction implements BrowserAction, BranchAction {
return "Absolute Path"; return "Absolute Path";
} }
@Override
public boolean automaticallyResolveLinks() {
return true;
}
@Override @Override
public KeyCombination getShortcut() { public KeyCombination getShortcut() {
return new KeyCodeCombination(KeyCode.C, KeyCombination.ALT_DOWN, KeyCombination.SHORTCUT_DOWN); 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); 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() { new LeafAction() {
@Override @Override
public String getName(OpenFileSystemModel model, List<BrowserEntry> entries) { public String getName(OpenFileSystemModel model, List<BrowserEntry> entries) {
@ -126,6 +166,50 @@ public class CopyPathAction implements BrowserAction, BranchAction {
clipboard.setContents(selection, selection); 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() { new LeafAction() {
@Override @Override
public String getName(OpenFileSystemModel model, List<BrowserEntry> entries) { public String getName(OpenFileSystemModel model, List<BrowserEntry> entries) {

View file

@ -5,6 +5,7 @@ import io.xpipe.app.browser.BrowserEntry;
import io.xpipe.app.browser.FileSystemHelper; import io.xpipe.app.browser.FileSystemHelper;
import io.xpipe.app.browser.OpenFileSystemModel; import io.xpipe.app.browser.OpenFileSystemModel;
import io.xpipe.app.browser.action.LeafAction; import io.xpipe.app.browser.action.LeafAction;
import io.xpipe.core.store.FileKind;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyCodeCombination;
@ -43,6 +44,6 @@ public class DeleteAction implements LeafAction {
@Override @Override
public String getName(OpenFileSystemModel model, List<BrowserEntry> entries) { public String getName(OpenFileSystemModel model, List<BrowserEntry> entries) {
return "Delete"; return "Delete" + (entries.stream().allMatch(browserEntry -> browserEntry.getRawFileEntry().getKind() == FileKind.LINK) ? " link" : "");
} }
} }

View file

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

View file

@ -27,6 +27,7 @@ open module io.xpipe.ext.base {
requires com.sun.jna.platform; requires com.sun.jna.platform;
provides BrowserAction with provides BrowserAction with
FollowLinkAction,
BackAction, BackAction,
ForwardAction, ForwardAction,
RefreshAction, RefreshAction,