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) -> {
model.cd(val != null ? val.getValue() : null).ifPresent(s -> {
model.cd(s);
});
model.cd(val != null ? val.getValue() : null);
});
return breadcrumbs;

View file

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

View file

@ -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()));

View file

@ -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());
}
}
}

View file

@ -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 {

View file

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

View file

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

View file

@ -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(() -> {

View file

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

View file

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

View file

@ -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(() -> {

View file

@ -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);

View file

@ -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();
})

View file

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

View file

@ -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);

View file

@ -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();

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.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) {

View file

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

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;
provides BrowserAction with
FollowLinkAction,
BackAction,
ForwardAction,
RefreshAction,