From 7605a4331ab707e39e5e6a942ef1ec45b64edcb0 Mon Sep 17 00:00:00 2001 From: crschnick Date: Sun, 18 Jun 2023 17:49:28 +0000 Subject: [PATCH] More support for symlinks --- .../app/browser/BrowserBreadcrumbBar.java | 4 +- .../xpipe/app/browser/BrowserContextMenu.java | 29 +++++-- .../app/browser/BrowserFileListComp.java | 47 ++++------- .../app/browser/BrowserFileListModel.java | 29 ++++--- .../io/xpipe/app/browser/BrowserModel.java | 28 ++++--- .../io/xpipe/app/browser/BrowserNavBar.java | 2 +- .../xpipe/app/browser/FileSystemHelper.java | 53 ++++++++++-- .../app/browser/OpenFileSystemModel.java | 52 ++++++++---- .../app/browser/action/BrowserAction.java | 4 + .../app/browser/icon/FileIconManager.java | 11 +-- .../xpipe/app/comp/base/ModalOverlayComp.java | 45 +++------- ...ionButtonComp.java => TileButtonComp.java} | 4 +- .../java/io/xpipe/app/prefs/AboutComp.java | 22 ++--- .../xpipe/app/resources/style/scrollbar.css | 53 +----------- .../io/xpipe/core/process/ShellDialect.java | 4 +- .../java/io/xpipe/core/store/FileSystem.java | 29 +++++++ .../ext/base/browser/CopyPathAction.java | 84 +++++++++++++++++++ .../xpipe/ext/base/browser/DeleteAction.java | 3 +- .../ext/base/browser/FollowLinkAction.java | 46 ++++++++++ ext/base/src/main/java/module-info.java | 1 + 20 files changed, 357 insertions(+), 193 deletions(-) rename app/src/main/java/io/xpipe/app/comp/base/{DescriptionButtonComp.java => TileButtonComp.java} (94%) create mode 100644 ext/base/src/main/java/io/xpipe/ext/base/browser/FollowLinkAction.java diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserBreadcrumbBar.java b/app/src/main/java/io/xpipe/app/browser/BrowserBreadcrumbBar.java index 1703db3f..e955e76c 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserBreadcrumbBar.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserBreadcrumbBar.java @@ -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; diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserContextMenu.java b/app/src/main/java/io/xpipe/app/browser/BrowserContextMenu.java index 6b49d7f5..d4faff3a 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserContextMenu.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserContextMenu.java @@ -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 resolveIfNeeded(BrowserAction action, List selected) { + var used = action.automaticallyResolveLinks() + ? selected.stream() + .map(browserEntry -> new BrowserEntry( + browserEntry.getRawFileEntry().resolved(), + browserEntry.getModel(), + browserEntry.isSynthetic())) + .toList() + : selected; + return used; + } } diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserFileListComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserFileListComp.java index 1f84085a..c482cc20 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserFileListComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserFileListComp.java @@ -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 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("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("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("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 table, ObservableList list) { - var comp = table.getComparator(); - if (comp == null) { - return true; - } - - var syntheticFirst = Comparator.comparing(path -> !path.isSynthetic()); - var dirsFirst = Comparator.comparing( - path -> path.getRawFileEntry().getKind() != FileKind.DIRECTORY); - - Comparator us = - syntheticFirst.thenComparing(dirsFirst).thenComparing(comp); - FXCollections.sort(list, us); - return true; - } - private void prepareTableSelectionModel(TableView 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())); diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserFileListModel.java b/app/src/main/java/io/xpipe/app/browser/BrowserFileListModel.java index f30732a5..1b05d707 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserFileListModel.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserFileListModel.java @@ -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 FILE_TYPE_COMPARATOR = - Comparator.comparing(path -> path.getRawFileEntry().getKind() != FileKind.DIRECTORY); - static final Predicate PREDICATE_ANY = path -> true; - static final Predicate PREDICATE_NOT_HIDDEN = path -> true; + Comparator.comparing(path -> path.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY); private final OpenFileSystemModel fileSystemModel; private final Property> 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 l) { + var syntheticFirst = Comparator.comparing(path -> !path.isSynthetic()); + var dirsFirst = Comparator.comparing( + path -> path.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY); + var comp = comparatorProperty.getValue(); + + Comparator 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()); } } } diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserModel.java b/app/src/main/java/io/xpipe/app/browser/BrowserModel.java index a30d74d9..a254e371 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserModel.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserModel.java @@ -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(); 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,14 +165,17 @@ public class BrowserModel { ThreadHelper.runFailableAsync(() -> { OpenFileSystemModel model; - try (var b = new BusyProperty(externalBusy != null ? externalBusy : new SimpleBooleanProperty())) { - model = new OpenFileSystemModel(name, this, store); - model.initFileSystem(); - model.initSavedState(); - } + // 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(); + model.initSavedState(); + } - openFileSystems.add(model); - selected.setValue(model); + openFileSystems.add(model); + selected.setValue(model); + } if (path != null) { model.initWithGivenDirectory(path); } else { diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserNavBar.java b/app/src/main/java/io/xpipe/app/browser/BrowserNavBar.java index be4cf6b5..89880b89 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserNavBar.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserNavBar.java @@ -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); }); diff --git a/app/src/main/java/io/xpipe/app/browser/FileSystemHelper.java b/app/src/main/java/io/xpipe/app/browser/FileSystemHelper.java index 06dd9249..2b0ce571 100644 --- a/app/src/main/java/io/xpipe/app/browser/FileSystemHelper.java +++ b/app/src/main/java/io/xpipe/app/browser/FileSystemHelper.java @@ -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)); + public static String resolveDirectoryPath(OpenFileSystemModel model, String path) throws Exception { + if (path == null) { + return null; } - model.getFileSystem().directoryAccessible(normalized); - return FileNames.toDirectory(normalized); + 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; diff --git a/app/src/main/java/io/xpipe/app/browser/OpenFileSystemModel.java b/app/src/main/java/io/xpipe/app/browser/OpenFileSystemModel.java index e34eec01..0c3bef2c 100644 --- a/app/src/main/java/io/xpipe/app/browser/OpenFileSystemModel.java +++ b/app/src/main/java/io/xpipe/app/browser/OpenFileSystemModel.java @@ -104,61 +104,81 @@ public final class OpenFileSystemModel { return new FileSystem.FileEntry(fileSystem, currentPath.get(), null, false, false, 0, null, FileKind.DIRECTORY); } - public Optional cd(String path) { + public void cd(String path) { + cdOrRetry(path, false).ifPresent(s -> cdOrRetry(s, false)); + } + + public Optional 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(() -> { diff --git a/app/src/main/java/io/xpipe/app/browser/action/BrowserAction.java b/app/src/main/java/io/xpipe/app/browser/action/BrowserAction.java index ab5a5d95..77ca9bea 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/BrowserAction.java +++ b/app/src/main/java/io/xpipe/app/browser/action/BrowserAction.java @@ -61,6 +61,10 @@ public interface BrowserAction { return true; } + default boolean automaticallyResolveLinks() { + return true; + } + default boolean isActive(OpenFileSystemModel model, List entries) { return true; } diff --git a/app/src/main/java/io/xpipe/app/browser/icon/FileIconManager.java b/app/src/main/java/io/xpipe/app/browser/icon/FileIconManager.java index fa2dd25b..ccb7f46f 100644 --- a/app/src/main/java/io/xpipe/app/browser/icon/FileIconManager.java +++ b/app/src/main/java/io/xpipe/app/browser/icon/FileIconManager.java @@ -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"; } diff --git a/app/src/main/java/io/xpipe/app/comp/base/ModalOverlayComp.java b/app/src/main/java/io/xpipe/app/comp/base/ModalOverlayComp.java index e3b2737f..a4a9fc3f 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/ModalOverlayComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/ModalOverlayComp.java @@ -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(() -> { diff --git a/app/src/main/java/io/xpipe/app/comp/base/DescriptionButtonComp.java b/app/src/main/java/io/xpipe/app/comp/base/TileButtonComp.java similarity index 94% rename from app/src/main/java/io/xpipe/app/comp/base/DescriptionButtonComp.java rename to app/src/main/java/io/xpipe/app/comp/base/TileButtonComp.java index 668af3f8..cb354ece 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/DescriptionButtonComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/TileButtonComp.java @@ -24,14 +24,14 @@ import java.util.function.Consumer; @AllArgsConstructor @Getter -public class DescriptionButtonComp extends SimpleComp { +public class TileButtonComp extends SimpleComp { private final ObservableValue name; private final ObservableValue description; private final ObservableValue icon; private final Consumer action; - public DescriptionButtonComp(String nameKey, String descriptionKey, String icon, Consumer action) { + public TileButtonComp(String nameKey, String descriptionKey, String icon, Consumer action) { this.name = AppI18n.observable(nameKey); this.description = AppI18n.observable(descriptionKey); this.icon = new SimpleStringProperty(icon); diff --git a/app/src/main/java/io/xpipe/app/prefs/AboutComp.java b/app/src/main/java/io/xpipe/app/prefs/AboutComp.java index 7e4a98df..3bafd0b7 100644 --- a/app/src/main/java/io/xpipe/app/prefs/AboutComp.java +++ b/app/src/main/java/io/xpipe/app/prefs/AboutComp.java @@ -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> { 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> { .grow(true, false), null) .addComp( - new DescriptionButtonComp( + new TileButtonComp( "openCurrentLogFile", "openCurrentLogFileDescription", "mdmz-text_snippet", @@ -59,7 +59,7 @@ public class AboutComp extends Comp> { .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> { .grow(true, false), null) .addComp( - new DescriptionButtonComp( + new TileButtonComp( "openInstallationDirectory", "openInstallationDirectoryDescription", "mdomz-snippet_folder", @@ -101,7 +101,7 @@ public class AboutComp extends Comp> { 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> { .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> { .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(); }) diff --git a/app/src/main/resources/io/xpipe/app/resources/style/scrollbar.css b/app/src/main/resources/io/xpipe/app/resources/style/scrollbar.css index f05aa33a..a8b9913d 100644 --- a/app/src/main/resources/io/xpipe/app/resources/style/scrollbar.css +++ b/app/src/main/resources/io/xpipe/app/resources/style/scrollbar.css @@ -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; diff --git a/core/src/main/java/io/xpipe/core/process/ShellDialect.java b/core/src/main/java/io/xpipe/core/process/ShellDialect.java index a4e07582..55d2ec33 100644 --- a/core/src/main/java/io/xpipe/core/process/ShellDialect.java +++ b/core/src/main/java/io/xpipe/core/process/ShellDialect.java @@ -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); diff --git a/core/src/main/java/io/xpipe/core/store/FileSystem.java b/core/src/main/java/io/xpipe/core/store/FileSystem.java index 9e019e2c..46613ffe 100644 --- a/core/src/main/java/io/xpipe/core/store/FileSystem.java +++ b/core/src/main/java/io/xpipe/core/store/FileSystem.java @@ -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 getShell(); diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/CopyPathAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/CopyPathAction.java index b0fd85c7..1950f9c5 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/CopyPathAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/CopyPathAction.java @@ -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 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 entries) { + return entries.stream() + .allMatch(browserEntry -> + browserEntry.getRawFileEntry().getKind() == FileKind.LINK); + } + + @Override + public boolean automaticallyResolveLinks() { + return false; + } + + @Override + public void execute(OpenFileSystemModel model, List 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 entries) { @@ -126,6 +166,50 @@ public class CopyPathAction implements BrowserAction, BranchAction { clipboard.setContents(selection, selection); } }, + new LeafAction() { + @Override + public String getName(OpenFileSystemModel model, List 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 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 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 entries) { diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/DeleteAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/DeleteAction.java index e76cea07..1ed2e48e 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/DeleteAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/DeleteAction.java @@ -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 entries) { - return "Delete"; + return "Delete" + (entries.stream().allMatch(browserEntry -> browserEntry.getRawFileEntry().getKind() == FileKind.LINK) ? " link" : ""); } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/FollowLinkAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/FollowLinkAction.java new file mode 100644 index 00000000..d29ae03b --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/FollowLinkAction.java @@ -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 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 entries) { + return new FontIcon("mdi2a-arrow-top-right-thick"); + } + + @Override + public boolean isApplicable(OpenFileSystemModel model, List 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 entries) { + return "Follow link"; + } +} diff --git a/ext/base/src/main/java/module-info.java b/ext/base/src/main/java/module-info.java index 41115dcc..7e7a487d 100644 --- a/ext/base/src/main/java/module-info.java +++ b/ext/base/src/main/java/module-info.java @@ -27,6 +27,7 @@ open module io.xpipe.ext.base { requires com.sun.jna.platform; provides BrowserAction with + FollowLinkAction, BackAction, ForwardAction, RefreshAction,