mirror of
https://github.com/xpipe-io/xpipe.git
synced 2024-11-22 07:30:24 +00:00
More support for symlinks
This commit is contained in:
parent
f8b2afe44c
commit
7605a4331a
20 changed files with 357 additions and 193 deletions
|
@ -80,9 +80,7 @@ public class BrowserBreadcrumbBar extends SimpleComp {
|
||||||
}
|
}
|
||||||
|
|
||||||
breadcrumbs.selectedCrumbProperty().addListener((obs, old, val) -> {
|
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;
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()));
|
||||||
|
|
|
@ -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());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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(() -> {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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";
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(() -> {
|
||||||
|
|
|
@ -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);
|
|
@ -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();
|
||||||
})
|
})
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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" : "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
package io.xpipe.ext.base.browser;
|
||||||
|
|
||||||
|
import io.xpipe.app.browser.BrowserEntry;
|
||||||
|
import io.xpipe.app.browser.OpenFileSystemModel;
|
||||||
|
import io.xpipe.app.browser.action.LeafAction;
|
||||||
|
import io.xpipe.core.impl.FileNames;
|
||||||
|
import io.xpipe.core.store.FileKind;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import org.kordamp.ikonli.javafx.FontIcon;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class FollowLinkAction implements LeafAction {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean automaticallyResolveLinks() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(OpenFileSystemModel model, List<BrowserEntry> entries) {
|
||||||
|
var target = FileNames.getParent(entries.get(0).getRawFileEntry().resolved().getPath());
|
||||||
|
model.cd(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Category getCategory() {
|
||||||
|
return Category.OPEN;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Node getIcon(OpenFileSystemModel model, List<BrowserEntry> entries) {
|
||||||
|
return new FontIcon("mdi2a-arrow-top-right-thick");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isApplicable(OpenFileSystemModel model, List<BrowserEntry> entries) {
|
||||||
|
return entries.size() == 1
|
||||||
|
&& entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.LINK && entry.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName(OpenFileSystemModel model, List<BrowserEntry> entries) {
|
||||||
|
return "Follow link";
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,6 +27,7 @@ open module io.xpipe.ext.base {
|
||||||
requires com.sun.jna.platform;
|
requires com.sun.jna.platform;
|
||||||
|
|
||||||
provides BrowserAction with
|
provides BrowserAction with
|
||||||
|
FollowLinkAction,
|
||||||
BackAction,
|
BackAction,
|
||||||
ForwardAction,
|
ForwardAction,
|
||||||
RefreshAction,
|
RefreshAction,
|
||||||
|
|
Loading…
Reference in a new issue