Add changelog

This commit is contained in:
crschnick 2023-05-20 13:49:58 +00:00
parent 8038e88b28
commit 19f4b0abc4
122 changed files with 4263 additions and 1589 deletions

View file

@ -24,6 +24,7 @@ apply from: "$rootDir/gradle/gradle_scripts/lombok.gradle"
apply from: "$projectDir/gradle_scripts/github-api.gradle"
apply from: "$projectDir/gradle_scripts/flexmark.gradle"
apply from: "$rootDir/gradle/gradle_scripts/picocli.gradle"
apply from: "$rootDir/gradle/gradle_scripts/versioncompare.gradle"
configurations {
implementation.extendsFrom(dep)
@ -38,8 +39,8 @@ dependencies {
compileOnly 'org.junit.jupiter:junit-jupiter-api:5.9.0'
compileOnly 'org.junit.jupiter:junit-jupiter-params:5.9.0'
implementation 'net.java.dev.jna:jna-jpms:5.12.1'
implementation 'net.java.dev.jna:jna-platform-jpms:5.12.1'
implementation 'net.java.dev.jna:jna-jpms:5.13.0'
implementation 'net.java.dev.jna:jna-platform-jpms:5.13.0'
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.13.0"
implementation group: 'com.fasterxml.jackson.module', name: 'jackson-module-parameter-names', version: "2.13.0"
implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: "2.13.0"
@ -56,7 +57,14 @@ dependencies {
implementation 'com.jfoenix:jfoenix:9.0.10'
implementation 'org.controlsfx:controlsfx:11.1.1'
implementation 'net.synedra:validatorfx:0.3.1'
implementation 'io.github.mkpaz:atlantafx-base:1.2.0'
implementation name: 'atlantafx-base-1.2.1'
implementation name: 'atlantafx-styles-1.2.1'
implementation name: 'jSystemThemeDetector-3.8'
implementation group: 'com.github.oshi', name: 'oshi-core-java11', version: '6.4.2'
implementation 'org.jetbrains:annotations:24.0.1'
implementation ('de.jangassen:jfa:1.2.0') {
exclude group: 'net.java.dev.jna', module: 'jna'
}
}
apply from: "$rootDir/gradle/gradle_scripts/junit.gradle"

View file

@ -1,84 +0,0 @@
package io.xpipe.app.browser;
import io.xpipe.app.comp.base.ListBoxViewComp;
import io.xpipe.app.comp.storage.store.StoreEntryFlatMiniSectionComp;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.augment.DragPseudoClassAugment;
import io.xpipe.app.fxcomps.augment.GrowAugment;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.ShellStore;
import javafx.application.Platform;
import javafx.geometry.Point2D;
import javafx.scene.control.Button;
import javafx.scene.input.DragEvent;
import javafx.scene.layout.Region;
import java.util.Timer;
import java.util.TimerTask;
final class BookmarkList extends SimpleComp {
public static final Timer DROP_TIMER = new Timer("dnd", true);
private Point2D lastOver = new Point2D(-1, -1);
private TimerTask activeTask;
private final FileBrowserModel model;
BookmarkList(FileBrowserModel model) {
this.model = model;
}
@Override
protected Region createSimple() {
var observableList = BindingsHelper.filteredContentBinding(StoreEntryFlatMiniSectionComp.ALL, e -> e.getEntry().getState().isUsable());
var list = new ListBoxViewComp<>(observableList, observableList, e -> {
return Comp.of(() -> {
var button = new Button(null, e.createRegion());
if (!(e.getEntry().getStore() instanceof ShellStore)) {
button.setDisable(true);
}
button.setOnAction(event -> {
var fileSystem = ((ShellStore) e.getEntry().getStore());
model.openFileSystemAsync(fileSystem);
event.consume();
});
GrowAugment.create(true, false).augment(new SimpleCompStructure<>(button));
DragPseudoClassAugment.create().augment(new SimpleCompStructure<>(button));
button.addEventHandler(
DragEvent.DRAG_OVER,
mouseEvent -> handleHoverTimer(e.getEntry().getStore(), mouseEvent));
button.addEventHandler(
DragEvent.DRAG_EXITED,
mouseEvent -> activeTask = null);
return button;
});
}).styleClass("bookmark-list").createRegion();
return list;
}
private void handleHoverTimer(DataStore store, DragEvent event) {
if (lastOver.getX() == event.getX() && lastOver.getY() == event.getY()) {
return;
}
lastOver = (new Point2D(event.getX(), event.getY()));
activeTask = new TimerTask() {
@Override
public void run() {
if (activeTask != this) {
return;
}
Platform.runLater(() -> model.openExistingFileSystemIfPresent(store.asNeeded()));
}
};
DROP_TIMER.schedule(activeTask, 500);
}
}

View file

@ -8,7 +8,7 @@ import javafx.scene.control.Alert;
import java.util.List;
import java.util.stream.Collectors;
public class FileBrowserAlerts {
public class BrowserAlerts {
public static boolean showMoveAlert(List<FileSystem.FileEntry> source, FileSystem.FileEntry target) {
if (source.stream().noneMatch(entry -> entry.isDirectory())) {

View file

@ -0,0 +1,155 @@
package io.xpipe.app.browser;
import io.xpipe.app.comp.storage.store.StoreEntryTree;
import io.xpipe.app.comp.storage.store.StoreEntryWrapper;
import io.xpipe.app.comp.storage.store.StoreViewState;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.PrettyImageComp;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.ShellStore;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.input.DragEvent;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Region;
import java.util.Timer;
import java.util.TimerTask;
final class BrowserBookmarkList extends SimpleComp {
public static final Timer DROP_TIMER = new Timer("dnd", true);
private Point2D lastOver = new Point2D(-1, -1);
private TimerTask activeTask;
private final BrowserModel model;
BrowserBookmarkList(BrowserModel model) {
this.model = model;
}
@Override
protected Region createSimple() {
var root = StoreEntryTree.createTree();
var view = new TreeView<StoreEntryWrapper>(root);
view.setShowRoot(false);
view.getStyleClass().add("bookmark-list");
view.setCellFactory(param -> {
return new StoreCell();
});
model.getSelected().addListener((observable, oldValue, newValue) -> {
if (newValue == null) {
view.getSelectionModel().clearSelection();
return;
}
view.getSelectionModel()
.select(getTreeViewItem(
root,
StoreViewState.get().getAllEntries().stream()
.filter(storeEntryWrapper -> storeEntryWrapper
.getState()
.getValue()
.isUsable()
&& storeEntryWrapper
.getEntry()
.getStore()
.equals(newValue.getStore()))
.findAny()
.orElse(null)));
});
return view;
}
private static TreeItem<StoreEntryWrapper> getTreeViewItem(
TreeItem<StoreEntryWrapper> item, StoreEntryWrapper value) {
if (item.getValue() != null && item.getValue().equals(value)) {
return item;
}
for (TreeItem<StoreEntryWrapper> child : item.getChildren()) {
TreeItem<StoreEntryWrapper> s = getTreeViewItem(child, value);
if (s != null) {
return s;
}
}
return null;
}
private final class StoreCell extends TreeCell<StoreEntryWrapper> {
private final StringProperty img = new SimpleStringProperty();
private final Node imageView = new PrettyImageComp(img, 20, 20).createRegion();
private StoreCell() {
setGraphic(imageView);
addEventHandler(DragEvent.DRAG_OVER, mouseEvent -> {
if (getItem() == null) {
return;
}
handleHoverTimer(getItem().getEntry().getStore(), mouseEvent);
mouseEvent.consume();
});
addEventHandler(DragEvent.DRAG_EXITED, mouseEvent -> {
activeTask = null;
mouseEvent.consume();
});
addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
if (getItem() == null || event.getButton() != MouseButton.PRIMARY) {
return;
}
var fileSystem = ((ShellStore) getItem().getEntry().getStore());
model.openFileSystemAsync(fileSystem, null);
event.consume();
});
}
@Override
public void updateItem(StoreEntryWrapper item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
// Don't set image as that would trigger image comp update
// and cells are emptied on each change, leading to unnecessary changes
// img.set(null);
setGraphic(null);
} else {
setText(item.getName());
img.set(item.getEntry()
.getProvider()
.getDisplayIconFileName(item.getEntry().getStore()));
setGraphic(imageView);
}
}
}
private void handleHoverTimer(DataStore store, DragEvent event) {
if (lastOver.getX() == event.getX() && lastOver.getY() == event.getY()) {
return;
}
lastOver = (new Point2D(event.getX(), event.getY()));
activeTask = new TimerTask() {
@Override
public void run() {
if (activeTask != this) {
return;
}
Platform.runLater(() -> model.openExistingFileSystemIfPresent(store.asNeeded()));
}
};
DROP_TIMER.schedule(activeTask, 500);
}
}

View file

@ -0,0 +1,92 @@
package io.xpipe.app.browser;
import atlantafx.base.controls.Breadcrumbs;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.core.impl.FileNames;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonBase;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import javafx.util.Callback;
import java.util.ArrayList;
public class BrowserBreadcrumbBar extends SimpleComp {
private final OpenFileSystemModel model;
public BrowserBreadcrumbBar(OpenFileSystemModel model) {
this.model = model;
}
@Override
protected Region createSimple() {
Callback<Breadcrumbs.BreadCrumbItem<String>, ButtonBase> crumbFactory = crumb -> {
var name = crumb.getValue().equals("/")
? "/"
: FileNames.getFileName(crumb.getValue());
var btn = new Button(name, null);
btn.setMnemonicParsing(false);
btn.setFocusTraversable(false);
return btn;
};
return createBreadcrumbs(crumbFactory, null);
}
private Region createBreadcrumbs(
Callback<Breadcrumbs.BreadCrumbItem<String>, ButtonBase> crumbFactory,
Callback<Breadcrumbs.BreadCrumbItem<String>, ? extends Node> dividerFactory) {
var breadcrumbs = new Breadcrumbs<String>();
SimpleChangeListener.apply(PlatformThread.sync(model.getCurrentPath()), val -> {
if (val == null) {
breadcrumbs.setSelectedCrumb(null);
return;
}
var sc = model.getFileSystem().getShell();
if (sc.isEmpty()) {
breadcrumbs.setDividerFactory(item -> item != null && !item.isLast() ? new Label("/") : null);
} else {
breadcrumbs.setDividerFactory(item -> {
if (item == null) {
return null;
}
if (item.isFirst() && item.getValue().equals("/")) {
return new Label("");
}
return new Label(sc.get().getOsType().getFileSystemSeparator());
});
}
var elements = FileNames.splitHierarchy(val);
var modifiedElements = new ArrayList<>(elements);
if (val.startsWith("/")) {
modifiedElements.add(0, "/");
}
Breadcrumbs.BreadCrumbItem<String> items =
Breadcrumbs.buildTreeModel(modifiedElements.toArray(String[]::new));
breadcrumbs.setSelectedCrumb(items);
});
if (crumbFactory != null) {
breadcrumbs.setCrumbFactory(crumbFactory);
}
if (dividerFactory != null) {
breadcrumbs.setDividerFactory(dividerFactory);
}
breadcrumbs.selectedCrumbProperty().addListener((obs, old, val) -> {
model.cd(val != null ? val.getValue() : null).ifPresent(s -> {
model.cd(s);
});
});
return breadcrumbs;
}
}

View file

@ -12,7 +12,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class FileBrowserClipboard {
public class BrowserClipboard {
@Value
public static class Instance {

View file

@ -3,7 +3,9 @@ package io.xpipe.app.browser;
import atlantafx.base.controls.RingProgressIndicator;
import atlantafx.base.controls.Spacer;
import atlantafx.base.theme.Styles;
import io.xpipe.app.browser.icon.DirectoryType;
import io.xpipe.app.browser.icon.FileIconManager;
import io.xpipe.app.browser.icon.FileType;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.augment.GrowAugment;
@ -12,10 +14,10 @@ import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.BusyProperty;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.FileSystem;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.ListChangeListener;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
@ -32,29 +34,44 @@ import static atlantafx.base.theme.Styles.DENSE;
import static atlantafx.base.theme.Styles.toggleStyleClass;
import static javafx.scene.control.TabPane.TabClosingPolicy.ALL_TABS;
public class FileBrowserComp extends SimpleComp {
public class BrowserComp extends SimpleComp {
private final FileBrowserModel model;
private final BrowserModel model;
public FileBrowserComp(FileBrowserModel model) {
public BrowserComp(BrowserModel model) {
this.model = model;
}
@Override
protected Region createSimple() {
ThreadHelper.runAsync( () -> {
FileType.loadDefinitions();
DirectoryType.loadDefinitions();
ThreadHelper.runAsync(() -> {
FileIconManager.loadIfNecessary();
});
var bookmarksList = new BookmarkList(model).createRegion();
var bookmarksList = new BrowserBookmarkList(model).createRegion();
VBox.setVgrow(bookmarksList, Priority.ALWAYS);
var localDownloadStage = new LocalFileTransferComp(model.getLocalTransfersStage()).hide(Bindings.createBooleanBinding(() -> {
if (model.getOpenFileSystems().size() == 0) {
return true;
}
var localDownloadStage = new BrowserTransferComp(model.getLocalTransfersStage())
.hide(PlatformThread.sync(Bindings.createBooleanBinding(
() -> {
if (model.getOpenFileSystems().size() == 0) {
return true;
}
return !model.getMode().equals(FileBrowserModel.Mode.BROWSER);
}, PlatformThread.sync(model.getOpenFileSystems()))).createRegion();
if (model.getMode().isChooser()) {
return true;
}
if (model.getSelected().getValue() != null) {
return model.getSelected().getValue().isLocal();
}
return false;
},
model.getOpenFileSystems(),
model.getSelected())))
.createRegion();
var vertical = new VBox(bookmarksList, localDownloadStage);
vertical.setFillWidth(true);
@ -63,13 +80,16 @@ public class FileBrowserComp extends SimpleComp {
.widthProperty()
.addListener(
// set sidebar width in pixels depending on split pane width
(obs, old, val) -> splitPane.setDividerPosition(0, 230 / splitPane.getWidth()));
(obs, old, val) -> splitPane.setDividerPosition(0, 280 / splitPane.getWidth()));
return addBottomBar(splitPane);
var r = addBottomBar(splitPane);
r.getStyleClass().add("browser");
// AppFont.small(r);
return r;
}
private Region addBottomBar(Region r) {
if (model.getMode().equals(FileBrowserModel.Mode.BROWSER)) {
if (!model.getMode().isChooser()) {
return r;
}
@ -78,13 +98,18 @@ public class FileBrowserComp extends SimpleComp {
var selected = new HBox();
selected.setAlignment(Pos.CENTER_LEFT);
selected.setSpacing(10);
model.getSelectedFiles().addListener((ListChangeListener<? super FileSystem.FileEntry>) c -> {
selected.getChildren().setAll(c.getList().stream().map(s -> {
var field = new TextField(s.getPath());
field.setEditable(false);
field.setPrefWidth(400);
return field;
}).toList());
model.getSelection().addListener((ListChangeListener<? super BrowserEntry>) c -> {
PlatformThread.runLaterIfNeeded(() -> {
selected.getChildren()
.setAll(c.getList().stream()
.map(s -> {
var field = new TextField(s.getRawFileEntry().getPath());
field.setEditable(false);
field.setPrefWidth(400);
return field;
})
.toList());
});
});
var spacer = new Spacer(Orientation.HORIZONTAL);
var button = new Button("Select");
@ -114,7 +139,8 @@ public class FileBrowserComp extends SimpleComp {
map.put(v, t);
tabs.getTabs().add(t);
});
tabs.getSelectionModel().select(model.getOpenFileSystems().indexOf(model.getSelected().getValue()));
tabs.getSelectionModel()
.select(model.getOpenFileSystems().indexOf(model.getSelected().getValue()));
// Used for ignoring changes by the tabpane when new tabs are added. We want to perform the selections manually!
var modifying = new SimpleBooleanProperty();
@ -177,12 +203,10 @@ public class FileBrowserComp extends SimpleComp {
continue;
}
model.closeFileSystem(source.getKey());
model.closeFileSystemAsync(source.getKey());
}
}
});
stack.getStyleClass().add("browser");
return stack;
}
@ -190,16 +214,9 @@ public class FileBrowserComp extends SimpleComp {
var tabs = new TabPane();
tabs.setTabDragPolicy(TabPane.TabDragPolicy.REORDER);
tabs.setTabMinWidth(Region.USE_COMPUTED_SIZE);
if (!model.getMode().equals(FileBrowserModel.Mode.BROWSER)) {
tabs.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE);
tabs.getStyleClass().add("singular");
} else {
tabs.setTabClosingPolicy(ALL_TABS);
Styles.toggleStyleClass(tabs, TabPane.STYLE_CLASS_FLOATING);
toggleStyleClass(tabs, DENSE);
}
tabs.setTabClosingPolicy(ALL_TABS);
Styles.toggleStyleClass(tabs, TabPane.STYLE_CLASS_FLOATING);
toggleStyleClass(tabs, DENSE);
return tabs;
}
@ -214,29 +231,14 @@ public class FileBrowserComp extends SimpleComp {
.bind(Bindings.createDoubleBinding(
() -> model.getBusy().get() ? -1d : 0, PlatformThread.sync(model.getBusy())));
var name = Bindings.createStringBinding(
() -> {
return model.getStore().getValue() != null
? DataStorage.get()
.getStoreEntry(model.getStore().getValue())
.getName()
: null;
},
PlatformThread.sync(model.getStore()));
var image = Bindings.createStringBinding(
() -> {
return model.getStore().getValue() != null
? DataStorage.get()
.getStoreEntry(model.getStore().getValue())
.getProvider()
.getDisplayIconFileName(model.getStore().getValue())
: null;
},
model.getStore());
var logo = new PrettyImageComp(image, 20, 20).createRegion();
var name = DataStorage.get().getStoreEntry(model.getStore()).getName();
var image = DataStorage.get()
.getStoreEntry(model.getStore())
.getProvider()
.getDisplayIconFileName(model.getStore());
var logo = new PrettyImageComp(new SimpleStringProperty(image), 20, 20).createRegion();
var label = new Label();
label.textProperty().bind(name);
var label = new Label(name);
label.addEventHandler(DragEvent.DRAG_ENTERED, new EventHandler<DragEvent>() {
@Override
public void handle(DragEvent mouseEvent) {
@ -253,12 +255,6 @@ public class FileBrowserComp extends SimpleComp {
tab.setGraphic(label);
GrowAugment.create(true, false).augment(new SimpleCompStructure<>(label));
if (!this.model.getMode().equals(FileBrowserModel.Mode.BROWSER)) {
label.setManaged(false);
label.setVisible(false);
}
tab.setContent(new OpenFileSystemComp(model).createSimple());
return tab;
}

View file

@ -0,0 +1,82 @@
package io.xpipe.app.browser;
import io.xpipe.app.browser.action.BranchAction;
import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.browser.action.LeafAction;
import io.xpipe.app.core.AppFont;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Menu;
import javafx.scene.control.SeparatorMenuItem;
import java.util.ArrayList;
import java.util.List;
final class BrowserContextMenu extends ContextMenu {
private final OpenFileSystemModel model;
private final BrowserEntry source;
public BrowserContextMenu(OpenFileSystemModel model, BrowserEntry source) {
this.model = model;
this.source = source;
createMenu();
}
private void createMenu() {
AppFont.normal(this.getStyleableNode());
var empty = source == null;
var selected = new ArrayList<>(empty ? List.of() : model.getFileList().getSelection());
if (source != null && !selected.contains(source)) {
selected.add(source);
} else if (source == null && model.getFileList().getSelection().isEmpty()) {
selected.add(new BrowserEntry(model.getCurrentDirectory(), model.getFileList(), false));
}
for (BrowserAction.Category cat : BrowserAction.Category.values()) {
var all = BrowserAction.ALL.stream()
.filter(browserAction -> browserAction.getCategory() == cat)
.filter(browserAction -> {
if (!browserAction.isApplicable(model, selected)) {
return false;
}
if (!browserAction.acceptsEmptySelection() && empty) {
return false;
}
return true;
})
.toList();
if (all.size() == 0) {
continue;
}
if (getItems().size() > 0) {
getItems().add(new SeparatorMenuItem());
}
for (BrowserAction a : all) {
if (a instanceof LeafAction la) {
getItems().add(la.toItem(model, selected, s -> s));
}
if (a instanceof BranchAction la) {
var m = new Menu(a.getName(model, selected) + " ...");
for (LeafAction sub : la.getBranchingActions()) {
if (!sub.isApplicable(model, selected)) {
continue;
}
m.getItems().add(sub.toItem(model, selected, s -> s));
}
var graphic = a.getIcon(model, selected);
if (graphic != null) {
m.setGraphic(graphic);
}
m.setDisable(!a.isActive(model, selected));
getItems().add(m);
}
}
}
}
}

View file

@ -0,0 +1,67 @@
package io.xpipe.app.browser;
import io.xpipe.app.browser.icon.DirectoryType;
import io.xpipe.app.browser.icon.FileType;
import io.xpipe.core.impl.FileNames;
import io.xpipe.core.store.FileSystem;
import lombok.Getter;
@Getter
public class BrowserEntry {
private final BrowserFileListModel model;
private final FileSystem.FileEntry rawFileEntry;
private final boolean synthetic;
private final FileType fileType;
private final DirectoryType directoryType;
public BrowserEntry(FileSystem.FileEntry rawFileEntry, BrowserFileListModel model, boolean synthetic) {
this.rawFileEntry = rawFileEntry;
this.model = model;
this.synthetic = synthetic;
this.fileType = fileType(rawFileEntry);
this.directoryType = directoryType(rawFileEntry);
}
private static FileType fileType(FileSystem.FileEntry rawFileEntry) {
if (rawFileEntry.isDirectory()) {
return null;
}
for (var f : FileType.ALL) {
if (f.matches(rawFileEntry)) {
return f;
}
}
return null;
}
private static DirectoryType directoryType(FileSystem.FileEntry rawFileEntry) {
if (!rawFileEntry.isDirectory()) {
return null;
}
for (var f : DirectoryType.ALL) {
if (f.matches(rawFileEntry)) {
return f;
}
}
return null;
}
public String getFileName() {
return FileNames.getFileName(getRawFileEntry().getPath());
}
public String getOptionallyQuotedFileName() {
var n = getFileName();
return FileNames.quoteIfNecessary(n);
}
public String getOptionallyQuotedFilePath() {
var n = rawFileEntry.getPath();
return FileNames.quoteIfNecessary(n);
}
}

View file

@ -1,17 +1,18 @@
/* SPDX-License-Identifier: MIT */
package io.xpipe.app.browser;
import atlantafx.base.theme.Styles;
import atlantafx.base.theme.Tweaks;
import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.browser.icon.FileIconManager;
import io.xpipe.app.comp.base.LazyTextFieldComp;
import io.xpipe.app.fxcomps.SimpleComp;
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.Containers;
import io.xpipe.app.util.HumanReadableFormat;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.impl.FileNames;
import io.xpipe.core.process.OsType;
import io.xpipe.core.store.FileSystem;
@ -23,15 +24,16 @@ import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.css.PseudoClass;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.control.skin.TableViewSkin;
import javafx.scene.control.skin.VirtualFlow;
import javafx.scene.input.DragEvent;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.*;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import java.time.Instant;
import java.time.ZoneId;
@ -42,7 +44,7 @@ import java.util.Objects;
import static io.xpipe.app.util.HumanReadableFormat.byteCount;
import static javafx.scene.control.TableColumn.SortType.ASCENDING;
final class FileListComp extends AnchorPane {
final class BrowserFileListComp extends SimpleComp {
private static final PseudoClass HIDDEN = PseudoClass.getPseudoClass("hidden");
private static final PseudoClass EMPTY = PseudoClass.getPseudoClass("empty");
@ -52,140 +54,153 @@ final class FileListComp extends AnchorPane {
private static final PseudoClass DRAG_OVER = PseudoClass.getPseudoClass("drag-over");
private static final PseudoClass DRAG_INTO_CURRENT = PseudoClass.getPseudoClass("drag-into-current");
private final FileListModel fileList;
private final BrowserFileListModel fileList;
public FileListComp(FileListModel fileList) {
public BrowserFileListComp(BrowserFileListModel fileList) {
this.fileList = fileList;
TableView<FileSystem.FileEntry> table = createTable();
}
@Override
protected Region createSimple() {
TableView<BrowserEntry> table = createTable();
SimpleChangeListener.apply(table.comparatorProperty(), (newValue) -> {
fileList.setComparator(newValue);
});
getChildren().setAll(table);
getStyleClass().addAll("table-directory-view");
Containers.setAnchors(table, Insets.EMPTY);
return table;
}
@SuppressWarnings("unchecked")
private TableView<FileSystem.FileEntry> createTable() {
var filenameCol = new TableColumn<FileSystem.FileEntry, String>("Name");
private TableView<BrowserEntry> createTable() {
var filenameCol = new TableColumn<BrowserEntry, String>("Name");
filenameCol.setCellValueFactory(param -> new SimpleStringProperty(
param.getValue() != null
? FileNames.getFileName(param.getValue().getPath())
? FileNames.getFileName(
param.getValue().getRawFileEntry().getPath())
: null));
filenameCol.setComparator(Comparator.comparing(String::toLowerCase));
filenameCol.setSortType(ASCENDING);
filenameCol.setCellFactory(col -> new FilenameCell(fileList.getEditing()));
var sizeCol = new TableColumn<FileSystem.FileEntry, Number>("Size");
sizeCol.setCellValueFactory(
param -> new SimpleLongProperty(param.getValue().getSize()));
var sizeCol = new TableColumn<BrowserEntry, Number>("Size");
sizeCol.setCellValueFactory(param ->
new SimpleLongProperty(param.getValue().getRawFileEntry().getSize()));
sizeCol.setCellFactory(col -> new FileSizeCell());
var mtimeCol = new TableColumn<FileSystem.FileEntry, Instant>("Modified");
mtimeCol.setCellValueFactory(
param -> new SimpleObjectProperty<>(param.getValue().getDate()));
var mtimeCol = new TableColumn<BrowserEntry, Instant>("Modified");
mtimeCol.setCellValueFactory(param ->
new SimpleObjectProperty<>(param.getValue().getRawFileEntry().getDate()));
mtimeCol.setCellFactory(col -> new FileTimeCell());
mtimeCol.getStyleClass().add(Tweaks.ALIGN_RIGHT);
var modeCol = new TableColumn<FileSystem.FileEntry, String>("Attributes");
modeCol.setCellValueFactory(
param -> new SimpleObjectProperty<>(param.getValue().getMode()));
var modeCol = new TableColumn<BrowserEntry, String>("Attributes");
modeCol.setCellValueFactory(param ->
new SimpleObjectProperty<>(param.getValue().getRawFileEntry().getMode()));
modeCol.setCellFactory(col -> new FileModeCell());
var table = new TableView<FileSystem.FileEntry>();
var table = new TableView<BrowserEntry>();
table.setPlaceholder(new Region());
table.getStyleClass().add(Styles.STRIPED);
table.getColumns().setAll(filenameCol, sizeCol, modeCol, mtimeCol);
table.getSortOrder().add(filenameCol);
table.setSortPolicy(param -> {
var comp = table.getComparator();
if (comp == null) {
return true;
}
var parentFirst = new Comparator<FileSystem.FileEntry>() {
@Override
public int compare(FileSystem.FileEntry o1, FileSystem.FileEntry o2) {
var c = fileList.getFileSystemModel().getCurrentParentDirectory();
if (c == null) {
return 0;
}
var syntheticFirst = Comparator.<BrowserEntry, Boolean>comparing(path -> !path.isSynthetic());
var dirsFirst = Comparator.<BrowserEntry, Boolean>comparing(
path -> !path.getRawFileEntry().isDirectory());
return o1.getPath().equals(c.getPath()) ? -1 : (o2.getPath().equals(c.getPath()) ? 1 : 0);
}
};
var dirsFirst = Comparator.<FileSystem.FileEntry, Boolean>comparing(path -> !path.isDirectory());
Comparator<? super FileSystem.FileEntry> us =
parentFirst.thenComparing(dirsFirst).thenComparing(comp);
FXCollections.sort(table.getItems(), us);
Comparator<? super BrowserEntry> us =
syntheticFirst.thenComparing(dirsFirst).thenComparing(comp);
FXCollections.sort(param.getItems(), us);
return true;
});
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
filenameCol.minWidthProperty().bind(table.widthProperty().multiply(0.5));
if (fileList.getMode().equals(FileBrowserModel.Mode.SINGLE_FILE_CHOOSER)
|| fileList.getMode().equals(FileBrowserModel.Mode.DIRECTORY_CHOOSER)) {
table.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
} else {
table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
}
table.getSelectionModel().getSelectedItems().addListener((ListChangeListener<? super FileSystem.FileEntry>)
c -> {
// Explicitly unselect synthetic entries since we can't use a custom selection model as that is bugged in JavaFX
var toSelect = c.getList().stream()
.filter(entry -> fileList.getFileSystemModel().getCurrentParentDirectory() == null
|| !entry.getPath()
.equals(fileList.getFileSystemModel()
.getCurrentParentDirectory()
.getPath()))
.toList();
fileList.getSelected().setAll(toSelect);
fileList.getFileSystemModel()
.getBrowserModel()
.getSelectedFiles()
.setAll(toSelect);
Platform.runLater(() -> {
var toUnselect = table.getSelectionModel().getSelectedItems().stream()
.filter(entry -> !toSelect.contains(entry))
.toList();
toUnselect.forEach(entry -> table.getSelectionModel()
.clearSelection(table.getItems().indexOf(entry)));
});
});
table.setOnKeyPressed(event -> {
if (event.isControlDown()
&& event.getCode().equals(KeyCode.C)
&& table.getSelectionModel().getSelectedItems().size() > 0) {
FileBrowserClipboard.startCopy(
fileList.getFileSystemModel().getCurrentDirectory(),
table.getSelectionModel().getSelectedItems());
event.consume();
}
if (event.isControlDown() && event.getCode().equals(KeyCode.V)) {
var clipboard = FileBrowserClipboard.retrieveCopy();
if (clipboard != null) {
var files = clipboard.getEntries();
var target = fileList.getFileSystemModel().getCurrentDirectory();
fileList.getFileSystemModel().dropFilesIntoAsync(target, files, true);
event.consume();
}
}
});
table.setFixedCellSize(34.0);
prepareTableSelectionModel(table);
prepareTableShortcuts(table);
prepareTableEntries(table);
prepareTableChanges(table, mtimeCol, modeCol);
return table;
}
private void prepareTableEntries(TableView<FileSystem.FileEntry> table) {
var emptyEntry = new FileListCompEntry(table, null, fileList);
private void prepareTableSelectionModel(TableView<BrowserEntry> table) {
if (!fileList.getMode().isMultiple()) {
table.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
} else {
table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
}
table.getSelectionModel().getSelectedItems().addListener((ListChangeListener<? super BrowserEntry>) c -> {
var toSelect = new ArrayList<>(c.getList());
// Explicitly unselect synthetic entries since we can't use a custom selection model as that is bugged in
// JavaFX
toSelect.removeIf(entry -> fileList.getFileSystemModel().getCurrentParentDirectory() != null
&& entry.getRawFileEntry()
.getPath()
.equals(fileList.getFileSystemModel()
.getCurrentParentDirectory()
.getPath()));
// Remove unsuitable selection
toSelect.removeIf(browserEntry -> (browserEntry.getRawFileEntry().isDirectory()
&& !fileList.getMode().isAcceptsDirectories())
|| (!browserEntry.getRawFileEntry().isDirectory()
&& !fileList.getMode().isAcceptsFiles()));
fileList.getSelection().setAll(toSelect);
Platform.runLater(() -> {
var toUnselect = table.getSelectionModel().getSelectedItems().stream()
.filter(entry -> !toSelect.contains(entry))
.toList();
toUnselect.forEach(entry -> table.getSelectionModel()
.clearSelection(table.getItems().indexOf(entry)));
});
});
fileList.getSelection().addListener((ListChangeListener<? super BrowserEntry>) c -> {
if (c.getList().equals(table.getSelectionModel().getSelectedItems())) {
return;
}
Platform.runLater(() -> {
if (c.getList().isEmpty()) {
table.getSelectionModel().clearSelection();
return;
}
var indices = c.getList().stream()
.skip(1)
.mapToInt(entry -> table.getItems().indexOf(entry))
.toArray();
table.getSelectionModel()
.selectIndices(table.getItems().indexOf(c.getList().get(0)), indices);
});
});
}
private void prepareTableShortcuts(TableView<BrowserEntry> table) {
table.setOnKeyPressed(event -> {
var selected = fileList.getSelection();
BrowserAction.getFlattened().stream()
.filter(browserAction -> browserAction.isApplicable(fileList.getFileSystemModel(), selected)
&& browserAction.isActive(fileList.getFileSystemModel(), selected))
.filter(browserAction -> browserAction.getShortcut() != null)
.filter(browserAction -> browserAction.getShortcut().match(event))
.findAny()
.ifPresent(browserAction -> {
ThreadHelper.runFailableAsync(() -> {
browserAction.execute(fileList.getFileSystemModel(), selected);
});
});
});
}
private void prepareTableEntries(TableView<BrowserEntry> table) {
var emptyEntry = new BrowserFileListCompEntry(table, null, fileList);
table.setOnDragOver(event -> {
emptyEntry.onDragOver(event);
});
@ -203,9 +218,17 @@ final class FileListComp extends AnchorPane {
});
table.setRowFactory(param -> {
TableRow<FileSystem.FileEntry> row = new TableRow<>();
TableRow<BrowserEntry> row = new TableRow<>();
new ContextMenuAugment<>(false, () -> {
if (row.getItem() != null && row.getItem().isSynthetic()) {
return null;
}
return new BrowserContextMenu(fileList.getFileSystemModel(), row.getItem());
})
.augment(new SimpleCompStructure<>(row));
var listEntry = Bindings.createObjectBinding(
() -> new FileListCompEntry(row, row.getItem(), fileList), row.itemProperty());
() -> new BrowserFileListCompEntry(row, row.getItem(), fileList), row.itemProperty());
row.itemProperty().addListener((observable, oldValue, newValue) -> {
row.pseudoClassStateChanged(DRAG, false);
@ -214,8 +237,10 @@ final class FileListComp extends AnchorPane {
row.itemProperty().addListener((observable, oldValue, newValue) -> {
row.pseudoClassStateChanged(EMPTY, newValue == null);
row.pseudoClassStateChanged(FILE, newValue != null && !newValue.isDirectory());
row.pseudoClassStateChanged(FOLDER, newValue != null && newValue.isDirectory());
row.pseudoClassStateChanged(
FILE, newValue != null && !newValue.getRawFileEntry().isDirectory());
row.pseudoClassStateChanged(
FOLDER, newValue != null && newValue.getRawFileEntry().isDirectory());
});
fileList.getDraggedOverDirectory().addListener((observable, oldValue, newValue) -> {
@ -229,7 +254,9 @@ final class FileListComp extends AnchorPane {
row.setOnMouseClicked(e -> {
listEntry.get().onMouseClick(e);
});
row.setOnMouseDragEntered(event -> {
listEntry.get().onMouseDragEntered(event);
});
row.setOnDragEntered(event -> {
listEntry.get().onDragEntered(event);
});
@ -250,19 +277,19 @@ final class FileListComp extends AnchorPane {
return row;
});
}
private void prepareTableChanges(TableView<FileSystem.FileEntry> table, TableColumn<FileSystem.FileEntry, Instant> mtimeCol, TableColumn<FileSystem.FileEntry, String> modeCol) {
private void prepareTableChanges(
TableView<BrowserEntry> table,
TableColumn<BrowserEntry, Instant> mtimeCol,
TableColumn<BrowserEntry, String> modeCol) {
var lastDir = new SimpleObjectProperty<FileSystem.FileEntry>();
Runnable updateHandler = () -> {
PlatformThread.runLaterIfNeeded(() -> {
var newItems = new ArrayList<FileSystem.FileEntry>();
var parentEntry = fileList.getFileSystemModel().getCurrentParentDirectory();
if (parentEntry != null) {
newItems.add(parentEntry);
}
newItems.addAll(fileList.getShown().getValue());
var newItems = new ArrayList<>(fileList.getShown().getValue());
var hasModifiedDate =
newItems.size() == 0 || newItems.stream().anyMatch(entry -> entry.getDate() != null);
var hasModifiedDate = newItems.size() == 0
|| newItems.stream()
.anyMatch(entry -> entry.getRawFileEntry().getDate() != null);
if (!hasModifiedDate) {
table.getColumns().remove(mtimeCol);
} else {
@ -340,7 +367,7 @@ final class FileListComp extends AnchorPane {
}
}
private class FilenameCell extends TableCell<FileSystem.FileEntry, String> {
private class FilenameCell extends TableCell<BrowserEntry, String> {
private final StringProperty img = new SimpleStringProperty();
private final StringProperty text = new SimpleStringProperty();
@ -349,18 +376,17 @@ final class FileListComp extends AnchorPane {
.createRegion();
private final StackPane textField =
new LazyTextFieldComp(text).createStructure().get();
private final ChangeListener<String> listener;
private final BooleanProperty updating = new SimpleBooleanProperty();
public FilenameCell(Property<FileSystem.FileEntry> editing) {
public FilenameCell(Property<BrowserEntry> editing) {
editing.addListener((observable, oldValue, newValue) -> {
if (getTableRow().getItem() != null && getTableRow().getItem().equals(newValue)) {
textField.requestFocus();
PlatformThread.runLaterIfNeeded(() -> textField.requestFocus());
}
});
listener = (observable, oldValue, newValue) -> {
ChangeListener<String> listener = (observable, oldValue, newValue) -> {
if (updating.get()) {
return;
}
@ -380,7 +406,7 @@ final class FileListComp extends AnchorPane {
return;
}
try (var b = new BusyProperty(updating)) {
try (var ignored = new BusyProperty(updating)) {
super.updateItem(fullPath, empty);
setText(null);
if (empty || getTableRow() == null || getTableRow().getItem() == null) {
@ -397,18 +423,20 @@ final class FileListComp extends AnchorPane {
var isParentLink = getTableRow()
.getItem()
.getRawFileEntry()
.equals(fileList.getFileSystemModel().getCurrentParentDirectory());
img.set(FileIconManager.getFileIcon(
isParentLink
? fileList.getFileSystemModel().getCurrentDirectory()
: getTableRow().getItem(),
: getTableRow().getItem().getRawFileEntry(),
isParentLink));
var isDirectory = getTableRow().getItem().isDirectory();
var isDirectory = getTableRow().getItem().getRawFileEntry().isDirectory();
pseudoClassStateChanged(FOLDER, isDirectory);
var fileName = isParentLink ? ".." : FileNames.getFileName(fullPath);
var hidden = !isParentLink && (getTableRow().getItem().isHidden() || fileName.startsWith("."));
var hidden = !isParentLink
&& (getTableRow().getItem().getRawFileEntry().isHidden() || fileName.startsWith("."));
getTableRow().pseudoClassStateChanged(HIDDEN, hidden);
text.set(fileName);
}
@ -416,7 +444,7 @@ final class FileListComp extends AnchorPane {
}
}
private class FileSizeCell extends TableCell<FileSystem.FileEntry, Number> {
private static class FileSizeCell extends TableCell<BrowserEntry, Number> {
@Override
protected void updateItem(Number fileSize, boolean empty) {
@ -425,7 +453,7 @@ final class FileListComp extends AnchorPane {
setText(null);
} else {
var path = getTableRow().getItem();
if (path.isDirectory()) {
if (path.getRawFileEntry().isDirectory()) {
setText("");
} else {
setText(byteCount(fileSize.longValue()));
@ -434,7 +462,7 @@ final class FileListComp extends AnchorPane {
}
}
private class FileModeCell extends TableCell<FileSystem.FileEntry, String> {
private static class FileModeCell extends TableCell<BrowserEntry, String> {
@Override
protected void updateItem(String mode, boolean empty) {
@ -447,7 +475,7 @@ final class FileListComp extends AnchorPane {
}
}
private static class FileTimeCell extends TableCell<FileSystem.FileEntry, Instant> {
private static class FileTimeCell extends TableCell<BrowserEntry, Instant> {
@Override
protected void updateItem(Instant fileTime, boolean empty) {

View file

@ -1,6 +1,5 @@
package io.xpipe.app.browser;
import io.xpipe.core.store.FileSystem;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.control.TableView;
@ -13,19 +12,18 @@ import java.util.Timer;
import java.util.TimerTask;
@Getter
public class FileListCompEntry {
public class BrowserFileListCompEntry {
public static final Timer DROP_TIMER = new Timer("dnd", true);
private final Node row;
private final FileSystem.FileEntry item;
private final FileListModel model;
private final BrowserEntry item;
private final BrowserFileListModel model;
private Point2D lastOver = new Point2D(-1, -1);
private TimerTask activeTask;
private FileContextMenu currentContextMenu;
public FileListCompEntry(Node row, FileSystem.FileEntry item, FileListModel model) {
public BrowserFileListCompEntry(Node row, BrowserEntry item, BrowserFileListModel model) {
this.row = row;
this.item = item;
this.model = model;
@ -34,6 +32,7 @@ public class FileListCompEntry {
@SuppressWarnings("unchecked")
public void onMouseClick(MouseEvent t) {
if (item == null) {
model.getSelection().clear();
return;
}
@ -48,35 +47,20 @@ public class FileListCompEntry {
}
if (t.getButton() == MouseButton.PRIMARY && t.isShiftDown()) {
var tv = ((TableView<FileSystem.FileEntry>) row.getParent().getParent().getParent().getParent());
var tv = ((TableView<BrowserEntry>) row.getParent().getParent().getParent().getParent());
var all = tv.getItems();
var min = tv.getSelectionModel().getSelectedItems().stream().mapToInt(entry -> all.indexOf(entry)).min().orElse(1);
var max = tv.getSelectionModel().getSelectedItems().stream().mapToInt(entry -> all.indexOf(entry)).max().orElse(all.size() - 1);
var end = all.indexOf(item);
var start = end > min ? min : max;
model.getSelected().setAll(all.subList(Math.min(start, end), Math.max(start, end) + 1));
t.consume();
return;
}
if (currentContextMenu != null) {
currentContextMenu.hide();
currentContextMenu = null;
t.consume();
return;
}
if (t.getButton() == MouseButton.SECONDARY) {
var cm = new FileContextMenu(model.getFileSystemModel(), item, model.getEditing());
cm.show(row, t.getScreenX(), t.getScreenY());
currentContextMenu = cm;
model.getSelection().setAll(all.subList(Math.min(start, end), Math.max(start, end) + 1));
t.consume();
return;
}
}
public boolean isSynthetic() {
return item != null && item.equals(model.getFileSystemModel().getCurrentParentDirectory());
return item != null && item.getRawFileEntry().equals(model.getFileSystemModel().getCurrentParentDirectory());
}
private boolean acceptsDrop(DragEvent event) {
@ -85,7 +69,7 @@ public class FileListCompEntry {
return true;
}
if (FileBrowserClipboard.currentDragClipboard == null) {
if (BrowserClipboard.currentDragClipboard == null) {
return false;
}
@ -94,14 +78,14 @@ public class FileListCompEntry {
}
// Prevent drag and drops of files into the current directory
if (FileBrowserClipboard.currentDragClipboard
if (BrowserClipboard.currentDragClipboard
.getBaseDirectory().getPath()
.equals(model.getFileSystemModel().getCurrentDirectory().getPath()) && (item == null || !item.isDirectory())) {
.equals(model.getFileSystemModel().getCurrentDirectory().getPath()) && (item == null || !item.getRawFileEntry().isDirectory())) {
return false;
}
// Prevent dropping items onto themselves
if (item != null && FileBrowserClipboard.currentDragClipboard.getEntries().contains(item)) {
if (item != null && BrowserClipboard.currentDragClipboard.getEntries().contains(item)) {
return false;
}
@ -116,8 +100,8 @@ public class FileListCompEntry {
if (event.getGestureSource() == null && event.getDragboard().hasFiles()) {
Dragboard db = event.getDragboard();
var list = db.getFiles().stream().map(File::toPath).toList();
var target = item != null && item.isDirectory()
? item
var target = item != null && item.getRawFileEntry().isDirectory()
? item.getRawFileEntry()
: model.getFileSystemModel().getCurrentDirectory();
model.getFileSystemModel().dropLocalFilesIntoAsync(target, list);
event.setDropCompleted(true);
@ -126,9 +110,9 @@ public class FileListCompEntry {
// Accept drops from inside the app window
if (event.getGestureSource() != null) {
var files = FileBrowserClipboard.retrieveDrag(event.getDragboard()).getEntries();
var target = item != null && item.isDirectory()
? item
var files = BrowserClipboard.retrieveDrag(event.getDragboard()).getEntries();
var target = item != null && item.getRawFileEntry().isDirectory()
? item.getRawFileEntry()
: model.getFileSystemModel().getCurrentDirectory();
model.getFileSystemModel().dropFilesIntoAsync(target, files, false);
event.setDropCompleted(true);
@ -137,7 +121,7 @@ public class FileListCompEntry {
}
public void onDragExited(DragEvent event) {
if (item != null && item.isDirectory()) {
if (item != null && item.getRawFileEntry().isDirectory()) {
model.getDraggedOverDirectory().setValue(null);
} else {
model.getDraggedOverEmpty().setValue(false);
@ -147,6 +131,7 @@ public class FileListCompEntry {
public void startDrag(MouseEvent event) {
if (item == null) {
row.startFullDrag();
return;
}
@ -154,11 +139,11 @@ public class FileListCompEntry {
return;
}
var selected = model.getSelected();
var selected = model.getSelectedRaw();
Dragboard db = row.startDragAndDrop(TransferMode.COPY);
db.setContent(FileBrowserClipboard.startDrag(model.getFileSystemModel().getCurrentDirectory(), selected));
db.setContent(BrowserClipboard.startDrag(model.getFileSystemModel().getCurrentDirectory(), selected));
Image image = SelectedFileListComp.snapshot(selected);
Image image = BrowserSelectionListComp.snapshot(selected);
db.setDragView(image, -20, 15);
event.setDragDetect(true);
@ -166,13 +151,13 @@ public class FileListCompEntry {
}
private void acceptDrag(DragEvent event) {
model.getDraggedOverEmpty().setValue(item == null || !item.isDirectory());
model.getDraggedOverEmpty().setValue(item == null || !item.getRawFileEntry().isDirectory());
model.getDraggedOverDirectory().setValue(item);
event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
}
private void handleHoverTimer(DragEvent event) {
if (item == null || !item.isDirectory()) {
if (item == null || !item.getRawFileEntry().isDirectory()) {
return;
}
@ -192,7 +177,7 @@ public class FileListCompEntry {
return;
}
model.getFileSystemModel().cd(item.getPath());
model.getFileSystemModel().cd(item.getRawFileEntry().getPath());
}
};
DROP_TIMER.schedule(activeTask, 1000);
@ -207,6 +192,22 @@ public class FileListCompEntry {
acceptDrag(event);
}
@SuppressWarnings("unchecked")
public void onMouseDragEntered(MouseDragEvent event) {
event.consume();
if (model.getFileSystemModel().getCurrentDirectory() == null) {
return;
}
if (item == null || item.isSynthetic()) {
return;
}
var tv = ((TableView<BrowserEntry>) row.getParent().getParent().getParent().getParent());
tv.getSelectionModel().select(item);
}
public void onDragOver(DragEvent event) {
event.consume();
if (!acceptsDrop(event)) {

View file

@ -0,0 +1,131 @@
package io.xpipe.app.browser;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.FileOpener;
import io.xpipe.core.impl.FileNames;
import io.xpipe.core.store.FileSystem;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import lombok.Getter;
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().isDirectory());
static final Predicate<BrowserEntry> PREDICATE_ANY = path -> true;
static final Predicate<BrowserEntry> PREDICATE_NOT_HIDDEN = path -> true;
private final OpenFileSystemModel fileSystemModel;
private final Property<Comparator<BrowserEntry>> comparatorProperty =
new SimpleObjectProperty<>(FILE_TYPE_COMPARATOR);
private final Property<List<BrowserEntry>> all = new SimpleObjectProperty<>(new ArrayList<>());
private final Property<List<BrowserEntry>> shown = new SimpleObjectProperty<>(new ArrayList<>());
private final ObjectProperty<Predicate<BrowserEntry>> predicateProperty =
new SimpleObjectProperty<>(path -> true);
private final ObservableList<BrowserEntry> selection = FXCollections.observableArrayList();
private final ObservableList<FileSystem.FileEntry> selectedRaw =
BindingsHelper.mappedContentBinding(selection, entry -> entry.getRawFileEntry());
private final Property<BrowserEntry> draggedOverDirectory = new SimpleObjectProperty<BrowserEntry>();
private final Property<Boolean> draggedOverEmpty = new SimpleBooleanProperty();
private final Property<BrowserEntry> editing = new SimpleObjectProperty<>();
public BrowserFileListModel(OpenFileSystemModel fileSystemModel) {
this.fileSystemModel = fileSystemModel;
fileSystemModel.getFilter().addListener((observable, oldValue, newValue) -> {
refreshShown();
});
}
public BrowserModel.Mode getMode() {
return fileSystemModel.getBrowserModel().getMode();
}
public void setAll(Stream<FileSystem.FileEntry> newFiles) {
try (var s = newFiles) {
var parent = fileSystemModel.getCurrentParentDirectory();
var l = Stream.concat(
parent != null ? Stream.of(new BrowserEntry(parent, this, true)) : Stream.of(),
s.filter(entry -> entry != null)
.limit(5000)
.map(entry -> new BrowserEntry(entry, this, false)))
.toList();
all.setValue(l);
refreshShown();
}
}
public void setComparator(Comparator<BrowserEntry> comparator) {
comparatorProperty.setValue(comparator);
refreshShown();
}
private void refreshShown() {
List<BrowserEntry> filtered = fileSystemModel.getFilter().getValue() != null
? all.getValue().stream()
.filter(entry -> {
var name = FileNames.getFileName(
entry.getRawFileEntry().getPath())
.toLowerCase(Locale.ROOT);
var filterString =
fileSystemModel.getFilter().getValue().toLowerCase(Locale.ROOT);
return name.contains(filterString);
})
.toList()
: all.getValue();
Comparator<BrowserEntry> tableComparator = comparatorProperty.getValue();
var comparator =
tableComparator != null ? FILE_TYPE_COMPARATOR.thenComparing(tableComparator) : FILE_TYPE_COMPARATOR;
var listCopy = new ArrayList<>(filtered);
listCopy.sort(comparator);
shown.setValue(listCopy);
}
public boolean rename(String filename, String newName) {
var fullPath = FileNames.join(fileSystemModel.getCurrentPath().get(), filename);
var newFullPath = FileNames.join(fileSystemModel.getCurrentPath().get(), newName);
try {
fileSystemModel.getFileSystem().move(fullPath, newFullPath);
fileSystemModel.refresh();
return true;
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();
return false;
}
}
public void onDoubleClick(BrowserEntry entry) {
if (!entry.getRawFileEntry().isDirectory() && getMode().equals(BrowserModel.Mode.SINGLE_FILE_CHOOSER)) {
getFileSystemModel().getBrowserModel().finishChooser();
return;
}
if (entry.getRawFileEntry().isDirectory()) {
var dir = fileSystemModel.cd(entry.getRawFileEntry().getPath());
if (dir.isPresent()) {
fileSystemModel.cd(dir.get());
}
} else {
FileOpener.openInTextEditor(entry.getRawFileEntry());
}
}
public ObjectProperty<Predicate<BrowserEntry>> predicateProperty() {
return predicateProperty;
}
}

View file

@ -1,32 +1,22 @@
package io.xpipe.app.browser;
import atlantafx.base.theme.Styles;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.augment.GrowAugment;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.impl.TextFieldComp;
import io.xpipe.app.fxcomps.util.Shortcuts;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import org.kordamp.ikonli.javafx.FontIcon;
public class FileFilterComp extends SimpleComp {
private final Property<String> filterString;
public FileFilterComp(Property<String> filterString) {
this.filterString = filterString;
}
public class BrowserFilterComp extends Comp<BrowserFilterComp.Structure> {
@Override
protected Region createSimple() {
public Structure createBase() {
var expanded = new SimpleBooleanProperty();
var text = new TextFieldComp(filterString, false).createRegion();
var button = new Button();
@ -56,8 +46,6 @@ public class FileFilterComp extends SimpleComp {
});
var fi = new FontIcon("mdi2m-magnify");
GrowAugment.create(false, true).augment(new SimpleCompStructure<>(button));
Shortcuts.addShortcut(button, new KeyCodeCombination(KeyCode.F, KeyCombination.SHORTCUT_DOWN));
button.setGraphic(fi);
button.setOnAction(event -> {
if (expanded.get()) {
@ -75,7 +63,6 @@ public class FileFilterComp extends SimpleComp {
text.setPrefWidth(0);
button.getStyleClass().add(Styles.FLAT);
expanded.addListener((observable, oldValue, val) -> {
System.out.println(val);
if (val) {
text.setPrefWidth(250);
button.getStyleClass().add(Styles.RIGHT_PILL);
@ -86,9 +73,24 @@ public class FileFilterComp extends SimpleComp {
button.getStyleClass().add(Styles.FLAT);
}
});
button.prefHeightProperty().bind(text.heightProperty());
var box = new HBox(text, button);
box.setFillHeight(true);
return box;
box.setAlignment(Pos.CENTER);
return new Structure(box, (TextField) text, button);
}
public record Structure(HBox box, TextField textField, Button toggleButton) implements CompStructure<HBox> {
@Override
public HBox get() {
return box;
}
}
private final Property<String> filterString;
public BrowserFilterComp(Property<String> filterString) {
this.filterString = filterString;
}
}

View file

@ -1,5 +1,3 @@
/* SPDX-License-Identifier: MIT */
package io.xpipe.app.browser;
import javafx.beans.binding.Bindings;
@ -12,7 +10,7 @@ import java.util.List;
import java.util.Objects;
import java.util.Optional;
final class FileBrowserHistory {
final class BrowserHistory {
private final IntegerProperty cursor = new SimpleIntegerProperty(0);
private final List<String> history = new ArrayList<>();

View file

@ -0,0 +1,141 @@
package io.xpipe.app.browser;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.impl.FileStore;
import io.xpipe.core.store.ShellStore;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import lombok.Getter;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
@Getter
public class BrowserModel {
public BrowserModel(Mode mode) {
this.mode = mode;
selected.addListener((observable, oldValue, newValue) -> {
if (newValue == null) {
return;
}
BindingsHelper.bindContent(selection, newValue.getFileList().getSelection());
});
}
@Getter
public static enum Mode {
BROWSER(false, true, true, true),
SINGLE_FILE_CHOOSER(true, false, true, false),
SINGLE_FILE_SAVE(true, false, true, false),
MULTIPLE_FILE_CHOOSER(true, true, true, false),
SINGLE_DIRECTORY_CHOOSER(true, false, false, true),
MULTIPLE_DIRECTORY_CHOOSER(true, true, false, true);
private final boolean chooser;
private final boolean multiple;
private final boolean acceptsFiles;
private final boolean acceptsDirectories;
Mode(boolean chooser, boolean multiple, boolean acceptsFiles, boolean acceptsDirectories) {
this.chooser = chooser;
this.multiple = multiple;
this.acceptsFiles = acceptsFiles;
this.acceptsDirectories = acceptsDirectories;
}
}
public static final BrowserModel DEFAULT = new BrowserModel(Mode.BROWSER);
private final Mode mode;
@Setter
private Consumer<List<FileStore>> onFinish;
private final ObservableList<OpenFileSystemModel> openFileSystems = FXCollections.observableArrayList();
private final Property<OpenFileSystemModel> selected = new SimpleObjectProperty<>();
private final BrowserTransferModel localTransfersStage = new BrowserTransferModel();
private final ObservableList<BrowserEntry> selection = FXCollections.observableArrayList();
public void finishChooser() {
if (!getMode().isChooser()) {
throw new IllegalStateException();
}
var chosen = new ArrayList<>(selection);
for (OpenFileSystemModel openFileSystem : openFileSystems) {
closeFileSystemAsync(openFileSystem);
}
if (chosen.size() == 0) {
return;
}
var stores = chosen.stream()
.map(entry -> new FileStore(
entry.getRawFileEntry().getFileSystem().getStore(),
entry.getRawFileEntry().getPath()))
.toList();
onFinish.accept(stores);
}
public void closeFileSystemAsync(OpenFileSystemModel open) {
ThreadHelper.runAsync(() -> {
if (Objects.equals(selected.getValue(), open)) {
selected.setValue(null);
}
open.closeSync();
openFileSystems.remove(open);
});
}
public void openExistingFileSystemIfPresent(ShellStore store) {
var found = openFileSystems.stream()
.filter(model -> Objects.equals(model.getStore(), store))
.findFirst();
if (found.isPresent()) {
selected.setValue(found.get());
} else {
openFileSystemAsync(store, null);
}
}
public void openFileSystemAsync(ShellStore store, String path) {
// // Prevent multiple tabs in non browser modes
// if (!mode.equals(Mode.BROWSER)) {
// ThreadHelper.runFailableAsync(() -> {
// var open = openFileSystems.size() > 0 ? openFileSystems.get(0) : null;
// if (open != null) {
// open.closeSync();
// openFileSystems.remove(open);
// }
//
// var model = new OpenFileSystemModel(this, store);
// openFileSystems.add(model);
// selected.setValue(model);
// model.switchSync(store);
// });
// return;
// }
ThreadHelper.runFailableAsync(() -> {
var model = new OpenFileSystemModel(this, store);
model.initFileSystem();
openFileSystems.add(model);
selected.setValue(model);
if (path != null) {
model.initWithGivenDirectory(path);
} else {
model.initWithDefaultDirectory();
}
});
}
}

View file

@ -0,0 +1,57 @@
package io.xpipe.app.browser;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.TextFieldComp;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.css.PseudoClass;
import javafx.geometry.Pos;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
public class BrowserNavBar extends SimpleComp {
private static final PseudoClass INVISIBLE = PseudoClass.getPseudoClass("invisible");
private final OpenFileSystemModel model;
public BrowserNavBar(OpenFileSystemModel model) {
this.model = model;
}
@Override
protected Region createSimple() {
var path = new SimpleStringProperty(model.getCurrentPath().get());
path.addListener((observable, oldValue, newValue) -> {
var changed = model.cd(newValue);
changed.ifPresent(path::set);
});
var pathBar = new TextFieldComp(path, true).createStructure().get();
pathBar.getStyleClass().add("path-text");
model.getCurrentPath().addListener((observable, oldValue, newValue) -> {
path.set(newValue);
});
SimpleChangeListener.apply(pathBar.focusedProperty(), val -> {
pathBar.pseudoClassStateChanged(INVISIBLE, !val);
if (val) {
Platform.runLater(() -> {
pathBar.selectAll();
});
}
});
var breadcrumbs = new BrowserBreadcrumbBar(model)
.hide(pathBar.focusedProperty())
.createRegion();
var stack = new StackPane(pathBar, breadcrumbs);
breadcrumbs.prefHeightProperty().bind(pathBar.heightProperty());
HBox.setHgrow(stack, Priority.ALWAYS);
stack.setAlignment(Pos.CENTER_LEFT);
return stack;
}
}

View file

@ -22,10 +22,10 @@ import lombok.Value;
@Value
@EqualsAndHashCode(callSuper = true)
public class SelectedFileListComp extends SimpleComp {
public class BrowserSelectionListComp extends SimpleComp {
public static Image snapshot(ObservableList<FileSystem.FileEntry> list) {
var r = new SelectedFileListComp(list).styleClass("drag").createRegion();
var r = new BrowserSelectionListComp(list).styleClass("drag").createRegion();
var scene = new Scene(r);
AppWindowHelper.setupStylesheets(scene);
AppStyle.addStylesheets(scene);

View file

@ -3,6 +3,8 @@ package io.xpipe.app.browser;
import atlantafx.base.controls.Spacer;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
import io.xpipe.app.fxcomps.impl.LabelComp;
import io.xpipe.app.fxcomps.util.PlatformThread;
import javafx.beans.binding.Bindings;
@ -13,13 +15,13 @@ import lombok.Value;
@Value
@EqualsAndHashCode(callSuper = true)
public class FileBrowserStatusBarComp extends SimpleComp {
public class BrowserStatusBarComp extends SimpleComp {
OpenFileSystemModel model;
@Override
protected Region createSimple() {
var cc = PlatformThread.sync(FileBrowserClipboard.currentCopyClipboard);
var cc = PlatformThread.sync(BrowserClipboard.currentCopyClipboard);
var ccCount = Bindings.createStringBinding(() -> {
if (cc.getValue() != null && cc.getValue().getEntries().size() > 0) {
return cc.getValue().getEntries().size() + " file" + (cc.getValue().getEntries().size() > 1 ? "s" : "") + " in clipboard";
@ -29,11 +31,11 @@ public class FileBrowserStatusBarComp extends SimpleComp {
}, cc);
var selectedCount = PlatformThread.sync(Bindings.createIntegerBinding(() -> {
return model.getFileList().getSelected().size();
}, model.getFileList().getSelected()));
return model.getFileList().getSelection().size();
}, model.getFileList().getSelection()));
var allCount = PlatformThread.sync(Bindings.createIntegerBinding(() -> {
return model.getFileList().getAll().getValue().size();
return (int) model.getFileList().getAll().getValue().stream().filter(entry -> !entry.isSynthetic()).count();
}, model.getFileList().getAll()));
var selectedComp = new LabelComp(Bindings.createStringBinding(() -> {
@ -51,7 +53,15 @@ public class FileBrowserStatusBarComp extends SimpleComp {
selectedComp.createRegion()
);
bar.getStyleClass().add("status-bar");
bar.setOnDragDetected(event -> {
event.consume();
bar.startFullDrag();
});
AppFont.small(bar);
// Use status bar as an extension of file list
new ContextMenuAugment<>(false, () -> new BrowserContextMenu(model, null)).augment(new SimpleCompStructure<>(bar));
return bar;
}
}

View file

@ -24,11 +24,11 @@ import org.kordamp.ikonli.javafx.FontIcon;
import java.io.IOException;
import java.util.List;
public class LocalFileTransferComp extends SimpleComp {
public class BrowserTransferComp extends SimpleComp {
private final LocalFileTransferStage stage;
private final BrowserTransferModel stage;
public LocalFileTransferComp(LocalFileTransferStage stage) {
public BrowserTransferComp(BrowserTransferModel stage) {
this.stage = stage;
}
@ -41,7 +41,7 @@ public class LocalFileTransferComp extends SimpleComp {
new StackComp(List.of(background)).grow(true, true).styleClass("download-background");
var binding = BindingsHelper.mappedContentBinding(stage.getItems(), item -> item.getFileEntry());
var list = new SelectedFileListComp(binding).apply(struc -> struc.get().setMinHeight(150)).grow(false, true);
var list = new BrowserSelectionListComp(binding).apply(struc -> struc.get().setMinHeight(150)).grow(false, true);
var dragNotice = new LabelComp(AppI18n.observable("dragFiles"))
.apply(struc -> struc.get().setGraphic(new FontIcon("mdi2e-export")))
.hide(BindingsHelper.persist(Bindings.isEmpty(stage.getItems())))
@ -71,7 +71,7 @@ public class LocalFileTransferComp extends SimpleComp {
});
struc.get().setOnDragDropped(event -> {
if (event.getGestureSource() != null) {
var files = FileBrowserClipboard.retrieveDrag(event.getDragboard())
var files = BrowserClipboard.retrieveDrag(event.getDragboard())
.getEntries();
stage.drop(files);
event.setDropCompleted(true);
@ -97,7 +97,7 @@ public class LocalFileTransferComp extends SimpleComp {
cc.putFiles(files);
db.setContent(cc);
var image = SelectedFileListComp.snapshot(FXCollections.observableList(stage.getItems().stream()
var image = BrowserSelectionListComp.snapshot(FXCollections.observableList(stage.getItems().stream()
.map(item -> item.getFileEntry())
.toList()));
db.setDragView(image, -20, 15);

View file

@ -17,7 +17,7 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Value
public class LocalFileTransferStage {
public class BrowserTransferModel {
private static final Path TEMP =
FileUtils.getTempDirectory().toPath().resolve("xpipe").resolve("download");

View file

@ -1,103 +0,0 @@
package io.xpipe.app.browser;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.impl.FileStore;
import io.xpipe.core.store.FileSystem;
import io.xpipe.core.store.ShellStore;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
@Getter
public class FileBrowserModel {
public FileBrowserModel(Mode mode) {
this.mode = mode;
}
public static enum Mode {
BROWSER,
SINGLE_FILE_CHOOSER,
SINGLE_FILE_SAVE,
MULTIPLE_FILE_CHOOSER,
DIRECTORY_CHOOSER
}
public static final FileBrowserModel DEFAULT = new FileBrowserModel(Mode.BROWSER);
private final Mode mode;
private final ObservableList<FileSystem.FileEntry> selectedFiles = FXCollections.observableArrayList();
@Setter
private Consumer<List<FileStore>> onFinish;
private final ObservableList<OpenFileSystemModel> openFileSystems = FXCollections.observableArrayList();
private final Property<OpenFileSystemModel> selected = new SimpleObjectProperty<>();
private final LocalFileTransferStage localTransfersStage = new LocalFileTransferStage();
public void finishChooser() {
if (getMode().equals(Mode.BROWSER)) {
throw new IllegalStateException();
}
closeFileSystem(openFileSystems.get(0));
if (selectedFiles.size() == 0) {
return;
}
var stores = selectedFiles.stream().map(entry -> new FileStore(entry.getFileSystem().getStore(), entry.getPath())).toList();
onFinish.accept(stores);
}
public void closeFileSystem(OpenFileSystemModel open) {
ThreadHelper.runAsync(() -> {
if (Objects.equals(selected.getValue(), open)) {
selected.setValue(null);
}
open.closeSync();
openFileSystems.remove(open);
});
}
public void openExistingFileSystemIfPresent(ShellStore store) {
var found = openFileSystems.stream().filter(model -> Objects.equals(model.getStore().getValue(), store)).findFirst();
if (found.isPresent()) {
selected.setValue(found.get());
} else {
openFileSystemAsync(store);
}
}
public void openFileSystemAsync(ShellStore store) {
// Prevent multiple tabs in non browser modes
if (!mode.equals(Mode.BROWSER)) {
ThreadHelper.runFailableAsync(() -> {
var open = openFileSystems.size() > 0 ? openFileSystems.get(0) : null;
if (open != null) {
open.closeSync();
openFileSystems.remove(open);
}
var model = new OpenFileSystemModel(this);
openFileSystems.add(model);
selected.setValue(model);
model.switchSync(store);
});
return;
}
ThreadHelper.runFailableAsync(() -> {
var model = new OpenFileSystemModel(this);
openFileSystems.add(model);
selected.setValue(model);
model.switchSync(store);
});
}
}

View file

@ -1,193 +0,0 @@
/* SPDX-License-Identifier: MIT */
package io.xpipe.app.browser;
import io.xpipe.app.comp.source.GuiDsCreatorMultiStep;
import io.xpipe.app.ext.DataSourceProvider;
import io.xpipe.app.util.FileOpener;
import io.xpipe.app.util.ScriptHelper;
import io.xpipe.app.util.TerminalHelper;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.impl.FileNames;
import io.xpipe.core.impl.FileStore;
import io.xpipe.core.process.OsType;
import io.xpipe.core.process.ShellControl;
import io.xpipe.core.store.FileSystem;
import javafx.beans.property.Property;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SeparatorMenuItem;
import org.apache.commons.io.FilenameUtils;
import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
import java.util.List;
final class FileContextMenu extends ContextMenu {
public boolean isExecutable(FileSystem.FileEntry e) {
if (e.isDirectory()) {
return false;
}
if (e.getExecutable() != null && e.getExecutable()) {
return true;
}
var shell = e.getFileSystem().getShell();
if (shell.isEmpty()) {
return false;
}
var os = shell.get().getOsType();
var ending = FilenameUtils.getExtension(e.getPath()).toLowerCase();
if (os.equals(OsType.WINDOWS) && List.of("exe", "bat", "ps1", "cmd").contains(ending)) {
return true;
}
if (List.of("sh", "command").contains(ending)) {
return true;
}
return false;
}
private final OpenFileSystemModel model;
private final FileSystem.FileEntry entry;
private final Property<FileSystem.FileEntry> editing;
public FileContextMenu(OpenFileSystemModel model, FileSystem.FileEntry entry, Property<FileSystem.FileEntry> editing) {
super();
this.model = model;
this.entry = entry;
this.editing = editing;
createMenu();
}
private void createMenu() {
if (entry.isDirectory()) {
var terminal = new MenuItem("Open terminal");
terminal.setOnAction(event -> {
event.consume();
model.openTerminalAsync(entry.getPath());
});
getItems().add(terminal);
} else {
if (isExecutable(entry)) {
var execute = new MenuItem("Run in terminal");
execute.setOnAction(event -> {
ThreadHelper.runFailableAsync(() -> {
ShellControl pc = model.getFileSystem().getShell().orElseThrow();
var e = pc.getShellDialect().getMakeExecutableCommand(entry.getPath());
if (e != null) {
pc.executeSimpleBooleanCommand(e);
}
var cmd = pc.command("\"" + entry.getPath() + "\"").prepareTerminalOpen("?");
TerminalHelper.open(FilenameUtils.getBaseName(entry.getPath()), cmd);
});
event.consume();
});
getItems().add(execute);
var executeInBackground = new MenuItem("Run in background");
executeInBackground.setOnAction(event -> {
ThreadHelper.runFailableAsync(() -> {
ShellControl pc = model.getFileSystem().getShell().orElseThrow();
var e = pc.getShellDialect().getMakeExecutableCommand(entry.getPath());
if (e != null) {
pc.executeSimpleBooleanCommand(e);
}
var cmd = ScriptHelper.createDetachCommand(pc, "\"" + entry.getPath() + "\"");
pc.executeSimpleBooleanCommand(cmd);
});
event.consume();
});
getItems().add(executeInBackground);
} else {
var open = new MenuItem("Open default");
open.setOnAction(event -> {
ThreadHelper.runFailableAsync(() -> {
FileOpener.openInDefaultApplication(entry);
});
event.consume();
});
getItems().add(open);
}
var pipe = new MenuItem("Pipe");
pipe.setOnAction(event -> {
var store = new FileStore(model.getFileSystem().getStore(), entry.getPath());
GuiDsCreatorMultiStep.showForStore(DataSourceProvider.Category.STREAM, store, null);
event.consume();
});
// getItems().add(pipe);
var edit = new MenuItem("Edit");
edit.setOnAction(event -> {
ThreadHelper.runAsync(() -> FileOpener.openInTextEditor(entry));
event.consume();
});
getItems().add(edit);
}
getItems().add(new SeparatorMenuItem());
{
var copy = new MenuItem("Copy");
copy.setOnAction(event -> {
FileBrowserClipboard.startCopy(
model.getCurrentDirectory(), model.getFileList().getSelected());
event.consume();
});
getItems().add(copy);
var paste = new MenuItem("Paste");
paste.setOnAction(event -> {
var clipboard = FileBrowserClipboard.retrieveCopy();
if (clipboard != null) {
var files = clipboard.getEntries();
var target = entry.isDirectory() ? entry : model.getCurrentDirectory();
model.dropFilesIntoAsync(target, files, true);
}
event.consume();
});
getItems().add(paste);
}
getItems().add(new SeparatorMenuItem());
var copyName = new MenuItem("Copy name");
copyName.setOnAction(event -> {
var selection = new StringSelection(FileNames.getFileName(entry.getPath()));
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
clipboard.setContents(selection, selection);
event.consume();
});
getItems().add(copyName);
var copyPath = new MenuItem("Copy full path");
copyPath.setOnAction(event -> {
var selection = new StringSelection(entry.getPath());
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
clipboard.setContents(selection, selection);
event.consume();
});
getItems().add(copyPath);
var delete = new MenuItem("Delete");
delete.setOnAction(event -> {
model.deleteSelectionAsync();
event.consume();
});
var rename = new MenuItem("Rename");
rename.setOnAction(event -> {
event.consume();
editing.setValue(entry);
});
getItems().addAll(new SeparatorMenuItem(), rename, delete);
}
}

View file

@ -1,123 +0,0 @@
/* SPDX-License-Identifier: MIT */
package io.xpipe.app.browser;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.FileOpener;
import io.xpipe.core.impl.FileNames;
import io.xpipe.core.store.FileSystem;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import lombok.Getter;
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
final class FileListModel {
static final Comparator<FileSystem.FileEntry> FILE_TYPE_COMPARATOR =
Comparator.comparing(path -> !path.isDirectory());
static final Predicate<FileSystem.FileEntry> PREDICATE_ANY = path -> true;
static final Predicate<FileSystem.FileEntry> PREDICATE_NOT_HIDDEN = path -> true;
private final OpenFileSystemModel fileSystemModel;
private final Property<Comparator<FileSystem.FileEntry>> comparatorProperty =
new SimpleObjectProperty<>(FILE_TYPE_COMPARATOR);
private final Property<List<FileSystem.FileEntry>> all = new SimpleObjectProperty<>(new ArrayList<>());
private final Property<List<FileSystem.FileEntry>> shown = new SimpleObjectProperty<>(new ArrayList<>());
private final ObjectProperty<Predicate<FileSystem.FileEntry>> predicateProperty =
new SimpleObjectProperty<>(path -> true);
private final ObservableList<FileSystem.FileEntry> selected = FXCollections.observableArrayList();
private final Property<FileSystem.FileEntry> draggedOverDirectory = new SimpleObjectProperty<FileSystem.FileEntry>();
private final Property<Boolean> draggedOverEmpty = new SimpleBooleanProperty();
private final Property<FileSystem.FileEntry> editing = new SimpleObjectProperty<>();
public FileListModel(OpenFileSystemModel fileSystemModel) {
this.fileSystemModel = fileSystemModel;
fileSystemModel.getFilter().addListener((observable, oldValue, newValue) -> {
refreshShown();
});
}
public FileBrowserModel.Mode getMode() {
return fileSystemModel.getBrowserModel().getMode();
}
public void setAll(List<FileSystem.FileEntry> newFiles) {
all.setValue(newFiles);
refreshShown();
}
public void setAll(Stream<FileSystem.FileEntry> newFiles) {
try (var s = newFiles) {
var l = s.filter(entry -> entry != null).limit(5000).toList();
all.setValue(l);
refreshShown();
}
}
public void setComparator(Comparator<FileSystem.FileEntry> comparator) {
comparatorProperty.setValue(comparator);
refreshShown();
}
private void refreshShown() {
List<FileSystem.FileEntry> filtered = fileSystemModel.getFilter().getValue() != null ? all.getValue().stream().filter(entry -> {
var name = FileNames.getFileName(entry.getPath()).toLowerCase(Locale.ROOT);
var filterString = fileSystemModel.getFilter().getValue().toLowerCase(Locale.ROOT);
return name.contains(filterString);
}).toList() : all.getValue();
Comparator<FileSystem.FileEntry> tableComparator = comparatorProperty.getValue();
var comparator = tableComparator != null
? FILE_TYPE_COMPARATOR.thenComparing(tableComparator)
: FILE_TYPE_COMPARATOR;
var listCopy = new ArrayList<>(filtered);
listCopy.sort(comparator);
shown.setValue(listCopy);
}
public boolean rename(String filename, String newName) {
var fullPath = FileNames.join(fileSystemModel.getCurrentPath().get(), filename);
var newFullPath = FileNames.join(fileSystemModel.getCurrentPath().get(), newName);
try {
fileSystemModel.getFileSystem().move(fullPath, newFullPath);
fileSystemModel.refresh();
return true;
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();
return false;
}
}
public void onDoubleClick(FileSystem.FileEntry entry) {
if (!entry.isDirectory() && getMode().equals(FileBrowserModel.Mode.SINGLE_FILE_CHOOSER)) {
getFileSystemModel().getBrowserModel().finishChooser();
return;
}
if (entry.isDirectory()) {
var dir = fileSystemModel.cd(entry.getPath());
if (dir.isPresent()) {
fileSystemModel.cd(dir.get());
}
} else {
FileOpener.openInTextEditor(entry);
}
}
public ObjectProperty<Predicate<FileSystem.FileEntry>> predicateProperty() {
return predicateProperty;
}
}

View file

@ -21,7 +21,7 @@ public class FileSystemHelper {
}
ConnectionFileSystem fileSystem = (ConnectionFileSystem) model.getFileSystem();
var current = !(model.getStore().getValue() instanceof LocalStore)
var current = !model.isLocal()
? fileSystem
.getShellControl()
.executeSimpleStringCommand(
@ -31,10 +31,10 @@ public class FileSystemHelper {
.get()
.getOsType()
.getHomeDirectory(fileSystem.getShell().get());
return FileSystemHelper.resolveDirectoryPath(model, current);
return validateDirectoryPath(model, resolvePath(model, current));
}
public static String resolveDirectoryPath(OpenFileSystemModel model, String path) throws Exception {
public static String resolvePath(OpenFileSystemModel model, String path) {
if (path == null) {
return null;
}
@ -58,6 +58,19 @@ public class FileSystemHelper {
return path + "\\";
}
return path;
}
public static String validateDirectoryPath(OpenFileSystemModel model, String path) throws Exception {
if (path == null) {
return null;
}
var shell = model.getFileSystem().getShell();
if (shell.isEmpty()) {
return path;
}
var normalized = shell.get()
.getShellDialect()
.normalizeDirectory(shell.get(), path)
@ -68,7 +81,6 @@ public class FileSystemHelper {
}
model.getFileSystem().directoryAccessible(normalized);
return FileNames.toDirectory(normalized);
}

View file

@ -0,0 +1,29 @@
package io.xpipe.app.browser;
import io.xpipe.app.util.ApplicationHelper;
import java.util.HashMap;
import java.util.Map;
public class OpenFileSystemCache {
private final OpenFileSystemModel model;
private final Map<String, Boolean> installedApplications = new HashMap<>();
public OpenFileSystemCache(OpenFileSystemModel model) {
this.model = model;
}
public boolean isApplicationInPath(String app) {
if (!installedApplications.containsKey(app)) {
try {
var b = ApplicationHelper.isInPath(model.getFileSystem().getShell().orElseThrow(), app);
installedApplications.put(app, b);
} catch (Exception e) {
installedApplications.put(app, false);
}
}
return installedApplications.get(app);
}
}

View file

@ -1,25 +1,26 @@
package io.xpipe.app.browser;
import atlantafx.base.controls.Spacer;
import io.xpipe.app.comp.base.ModalOverlayComp;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.PrettyImageComp;
import io.xpipe.app.fxcomps.impl.TextFieldComp;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.Shortcuts;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.control.Button;
import javafx.scene.control.MenuButton;
import javafx.scene.control.ToolBar;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.layout.*;
import org.kordamp.ikonli.feather.Feather;
import javafx.scene.input.KeyCombination;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import org.kordamp.ikonli.javafx.FontIcon;
import static io.xpipe.app.browser.FileListModel.PREDICATE_NOT_HIDDEN;
import static io.xpipe.app.util.Controls.iconButton;
import static io.xpipe.app.browser.BrowserFileListModel.PREDICATE_NOT_HIDDEN;
public class OpenFileSystemComp extends SimpleComp {
@ -31,116 +32,47 @@ public class OpenFileSystemComp extends SimpleComp {
@Override
protected Region createSimple() {
var creatingProperty = new SimpleBooleanProperty();
var backBtn = iconButton(Feather.ARROW_LEFT, false);
var alertOverlay = new ModalOverlayComp(
Comp.of(() -> createContent()),
model.getOverlay());
return alertOverlay.createRegion();
}
private Region createContent() {
var backBtn = new Button(null, new FontIcon("fth-arrow-left"));
backBtn.setOnAction(e -> model.back());
backBtn.disableProperty().bind(model.getHistory().canGoBackProperty().not());
var forthBtn = iconButton(Feather.ARROW_RIGHT, false);
var forthBtn = new Button(null, new FontIcon("fth-arrow-right"));
forthBtn.setOnAction(e -> model.forth());
forthBtn.disableProperty().bind(model.getHistory().canGoForthProperty().not());
var path = new SimpleStringProperty(model.getCurrentPath().get());
var pathBar = new TextFieldComp(path, true).createRegion();
path.addListener((observable, oldValue, newValue) -> {
var changed = model.cd(newValue);
changed.ifPresent(path::set);
});
model.getCurrentPath().addListener((observable, oldValue, newValue) -> {
path.set(newValue);
});
HBox.setHgrow(pathBar, Priority.ALWAYS);
var refreshBtn = new Button(null, new FontIcon("mdmz-refresh"));
refreshBtn.setOnAction(e -> model.refresh());
Shortcuts.addShortcut(refreshBtn, new KeyCodeCombination(KeyCode.F5));
var terminalBtn = new Button(null, new FontIcon("mdi2c-code-greater-than"));
terminalBtn.setOnAction(e -> model.openTerminalAsync(model.getCurrentPath().get()));
terminalBtn.setOnAction(
e -> model.openTerminalAsync(model.getCurrentPath().get()));
terminalBtn.disableProperty().bind(PlatformThread.sync(model.getNoDirectory()));
var addBtn = new Button(null, new FontIcon("mdmz-plus"));
addBtn.setOnAction(e -> {
creatingProperty.set(true);
});
addBtn.disableProperty().bind(PlatformThread.sync(model.getNoDirectory()));
Shortcuts.addShortcut(addBtn, new KeyCodeCombination(KeyCode.PLUS));
var menuButton = new MenuButton(null, new FontIcon("mdral-folder_open"));
new ContextMenuAugment<>(true, () -> new BrowserContextMenu(model, null)).augment(new SimpleCompStructure<>(menuButton));
var filter = new FileFilterComp(model.getFilter()).createRegion();
var filter = new BrowserFilterComp(model.getFilter()).createStructure();
Shortcuts.addShortcut(filter.toggleButton(), new KeyCodeCombination(KeyCode.F, KeyCombination.SHORTCUT_DOWN));
var topBar = new ToolBar();
topBar.getItems().setAll(
backBtn,
forthBtn,
new Spacer(10),
pathBar,
filter,
refreshBtn,
terminalBtn,
addBtn
);
topBar.getItems()
.setAll(backBtn, forthBtn, new Spacer(10), new BrowserNavBar(model).createRegion(), filter.get(), refreshBtn, terminalBtn, menuButton);
// ~
FileListComp directoryView = new FileListComp(model.getFileList());
var directoryView = new BrowserFileListComp(model.getFileList()).createRegion();
var root = new VBox(topBar, directoryView);
if (model.getBrowserModel().getMode() == FileBrowserModel.Mode.BROWSER) {
root.getChildren().add(new FileBrowserStatusBarComp(model).createRegion());
}
root.getChildren().add(new BrowserStatusBarComp(model).createRegion());
VBox.setVgrow(directoryView, Priority.ALWAYS);
root.setPadding(Insets.EMPTY);
model.getFileList().predicateProperty().set(PREDICATE_NOT_HIDDEN);
var pane = new StackPane();
pane.getChildren().add(root);
var creation = createCreationWindow(creatingProperty);
var creationPane = new StackPane(creation);
creationPane.setAlignment(Pos.CENTER);
creationPane.setOnMouseClicked(event -> {
creatingProperty.set(false);
});
pane.getChildren().add(creationPane);
creationPane.visibleProperty().bind(creatingProperty);
creationPane.managedProperty().bind(creatingProperty);
return pane;
}
private Region createCreationWindow(BooleanProperty creating) {
var creationName = new TextField();
creating.addListener((observable, oldValue, newValue) -> {
if (!newValue) {
creationName.setText("");
}
});
var createFileButton = new Button("File", new PrettyImageComp(new SimpleStringProperty("file_drag_icon.png"), 20, 20).createRegion());
createFileButton.setOnAction(event -> {
model.createFileAsync(creationName.getText());
creating.set(false);
});
var createDirectoryButton = new Button("Directory", new PrettyImageComp(new SimpleStringProperty("folder_closed.svg"), 20, 20).createRegion());
createDirectoryButton.setOnAction(event -> {
model.createDirectoryAsync(creationName.getText());
creating.set(false);
});
var buttonBar = new ButtonBar();
buttonBar.getButtons().addAll(createFileButton, createDirectoryButton);
var creationContent = new VBox(creationName, buttonBar);
creationContent.setSpacing(15);
var creation = new TitledPane("New ...", creationContent);
creation.setMaxWidth(400);
creation.setCollapsible(false);
creationContent.setPadding(new Insets(15));
creation.getStyleClass().add("elevated-3");
creating.addListener((observable, oldValue, newValue) -> {
if (newValue) {
creationName.requestFocus();
}
});
return creation;
return root;
}
}

View file

@ -1,13 +1,16 @@
/* SPDX-License-Identifier: MIT */
package io.xpipe.app.browser;
import io.xpipe.app.comp.base.ModalOverlayComp;
import io.xpipe.app.core.AppCache;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.BusyProperty;
import io.xpipe.app.util.TerminalHelper;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.app.util.XPipeDaemon;
import io.xpipe.core.impl.FileNames;
import io.xpipe.core.process.ShellControl;
import io.xpipe.core.process.ShellDialects;
import io.xpipe.core.store.ConnectionFileSystem;
import io.xpipe.core.store.FileSystem;
import io.xpipe.core.store.FileSystemStore;
@ -15,6 +18,7 @@ import io.xpipe.core.store.ShellStore;
import javafx.beans.property.*;
import lombok.Getter;
import lombok.SneakyThrows;
import org.apache.commons.lang3.function.FailableConsumer;
import java.io.IOException;
import java.nio.file.Path;
@ -22,34 +26,74 @@ import java.time.Instant;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Stream;
@Getter
final class OpenFileSystemModel {
public final class OpenFileSystemModel {
private Property<FileSystemStore> store = new SimpleObjectProperty<>();
private final FileSystemStore store;
private FileSystem fileSystem;
private final Property<String> filter = new SimpleStringProperty();
private final FileListModel fileList;
private final BrowserFileListModel fileList;
private final ReadOnlyObjectWrapper<String> currentPath = new ReadOnlyObjectWrapper<>();
private final FileBrowserHistory history = new FileBrowserHistory();
private final BrowserHistory history = new BrowserHistory();
private final BooleanProperty busy = new SimpleBooleanProperty();
private final FileBrowserModel browserModel;
private final BrowserModel browserModel;
private final BooleanProperty noDirectory = new SimpleBooleanProperty();
private final Property<OpenFileSystemSavedState> savedState = new SimpleObjectProperty<>();
private final OpenFileSystemCache cache = new OpenFileSystemCache(this);
private final Property<ModalOverlayComp.OverlayContent> overlay = new SimpleObjectProperty<>();
private boolean local;
public OpenFileSystemModel(FileBrowserModel browserModel) {
public OpenFileSystemModel(BrowserModel browserModel, FileSystemStore store) {
this.browserModel = browserModel;
fileList = new FileListModel(this);
this.store = store;
fileList = new BrowserFileListModel(this);
addListeners();
}
public void withShell(FailableConsumer<ShellControl, Exception> c, boolean refresh) {
ThreadHelper.runFailableAsync(() -> {
if (fileSystem == null) {
return;
}
BusyProperty.execute(busy, () -> {
if (store instanceof ShellStore s) {
c.accept(fileSystem.getShell().orElseThrow());
if (refresh) {
refreshSync();
}
}
});
});
}
private void addListeners() {
savedState.addListener((observable, oldValue, newValue) -> {
if (store == null) {
return;
}
var storageEntry = DataStorage.get().getStoreEntryIfPresent(store);
storageEntry.ifPresent(entry -> AppCache.update("browser-state-" + entry.getUuid(), newValue));
});
currentPath.addListener((observable, oldValue, newValue) -> {
savedState.setValue(savedState.getValue().withLastDirectory(newValue));
});
}
@SneakyThrows
public void refresh() {
BusyProperty.execute(busy, () -> {
cdSync(currentPath.get());
cdSyncWithoutCheck(currentPath.get());
});
}
private void refreshInternal() throws Exception {
cdSync(currentPath.get());
public void refreshSync() throws Exception {
cdSyncWithoutCheck(currentPath.get());
}
public FileSystem.FileEntry getCurrentParentDirectory() {
@ -79,29 +123,66 @@ final class OpenFileSystemModel {
return Optional.empty();
}
String newPath = null;
// Fix common issues with paths
var normalizedPath = FileSystemHelper.resolvePath(this, path);
if (!Objects.equals(path, normalizedPath)) {
return Optional.of(normalizedPath);
}
// Handle commands typed into navigation bar
if (normalizedPath != null && !FileNames.isAbsolute(normalizedPath) && fileSystem.getShell().isPresent()) {
var directory = currentPath.get();
var name = normalizedPath + " - "
+ XPipeDaemon.getInstance().getStoreName(store).orElse("?");
ThreadHelper.runFailableAsync(() -> {
if (ShellDialects.ALL.stream().anyMatch(dialect -> normalizedPath.startsWith(dialect.getOpenCommand()))) {
var cmd = fileSystem
.getShell()
.get()
.subShell(normalizedPath)
.initWith(fileSystem
.getShell()
.get()
.getShellDialect()
.getCdCommand(currentPath.get()))
.prepareTerminalOpen(name);
TerminalHelper.open(normalizedPath, cmd);
} else {
var cmd = fileSystem
.getShell()
.get()
.command(normalizedPath)
.workingDirectory(directory)
.prepareTerminalOpen(name);
TerminalHelper.open(normalizedPath, cmd);
}
});
return Optional.of(currentPath.get());
}
String dirPath = null;
try {
newPath = FileSystemHelper.resolveDirectoryPath(this, path);
dirPath = FileSystemHelper.validateDirectoryPath(this, normalizedPath);
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle();
return Optional.of(currentPath.get());
}
if (!Objects.equals(path, newPath)) {
return Optional.of(newPath);
if (!Objects.equals(path, dirPath)) {
return Optional.of(dirPath);
}
ThreadHelper.runFailableAsync(() -> {
try (var ignored = new BusyProperty(busy)) {
cdSync(path);
cdSyncWithoutCheck(path);
}
});
return Optional.empty();
}
private void cdSync(String path) throws Exception {
private void cdSyncWithoutCheck(String path) throws Exception {
if (fileSystem == null) {
var fs = store.getValue().createFileSystem();
var fs = store.createFileSystem();
fs.open();
this.fileSystem = fs;
}
@ -111,6 +192,7 @@ final class OpenFileSystemModel {
filter.setValue(null);
currentPath.set(path);
savedState.setValue(savedState.getValue().withLastDirectory(path));
history.updateCurrent(path);
loadFilesSync(path);
}
@ -130,7 +212,7 @@ final class OpenFileSystemModel {
}
return true;
} catch (Exception e) {
fileList.setAll(List.of());
fileList.setAll(Stream.of());
ErrorEvent.fromThrowable(e).handle();
return false;
}
@ -144,7 +226,7 @@ final class OpenFileSystemModel {
}
FileSystemHelper.dropLocalFilesInto(entry, files);
refreshInternal();
refreshSync();
});
});
}
@ -159,13 +241,13 @@ final class OpenFileSystemModel {
var same = files.get(0).getFileSystem().equals(target.getFileSystem());
if (same) {
if (!FileBrowserAlerts.showMoveAlert(files, target)) {
if (!BrowserAlerts.showMoveAlert(files, target)) {
return;
}
}
FileSystemHelper.dropFilesInto(target, files, explicitCopy);
refreshInternal();
refreshSync();
});
});
}
@ -191,13 +273,13 @@ final class OpenFileSystemModel {
}
fileSystem.mkdirs(abs);
refreshInternal();
refreshSync();
});
});
}
public void createFileAsync(String name) {
if (name.isBlank()) {
if (name == null || name.isBlank()) {
return;
}
@ -213,7 +295,7 @@ final class OpenFileSystemModel {
var abs = FileNames.join(getCurrentDirectory().getPath(), name);
fileSystem.touch(abs);
refreshInternal();
refreshSync();
});
});
}
@ -225,12 +307,12 @@ final class OpenFileSystemModel {
return;
}
if (!FileBrowserAlerts.showDeleteAlert(fileList.getSelected())) {
if (!BrowserAlerts.showDeleteAlert(fileList.getSelectedRaw())) {
return;
}
FileSystemHelper.delete(fileList.getSelected());
refreshInternal();
FileSystemHelper.delete(fileList.getSelectedRaw());
refreshSync();
});
});
}
@ -246,22 +328,46 @@ final class OpenFileSystemModel {
ErrorEvent.fromThrowable(e).handle();
}
fileSystem = null;
store = null;
}
public void switchSync(FileSystemStore fileSystem) throws Exception {
public void initFileSystem() throws Exception {
BusyProperty.execute(busy, () -> {
closeSync();
this.store.setValue(fileSystem);
var fs = fileSystem.createFileSystem();
var fs = store.createFileSystem();
fs.open();
this.fileSystem = fs;
var current = FileSystemHelper.getStartDirectory(this);
cdSync(current);
this.local = fs.getShell().map(shellControl -> shellControl.isLocal()).orElse(false);
});
}
public void initWithGivenDirectory(String dir) throws Exception {
initSavedState(dir);
cdSyncWithoutCheck(dir);
}
public void initWithDefaultDirectory() throws Exception {
var dir = FileSystemHelper.getStartDirectory(this);
initSavedState(dir);
cdSyncWithoutCheck(dir);
}
private void initSavedState(String path) {
var storageEntry = DataStorage.get()
.getStoreEntryIfPresent(store)
.map(entry -> entry.getUuid())
.orElse(UUID.randomUUID());
this.savedState.setValue(
AppCache.get("browser-state-" + storageEntry, OpenFileSystemSavedState.class, () -> {
try {
return OpenFileSystemSavedState.builder()
.lastDirectory(path)
.build();
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();
return null;
}
}));
}
public void openTerminalAsync(String directory) {
ThreadHelper.runFailableAsync(() -> {
if (fileSystem == null) {
@ -269,13 +375,13 @@ final class OpenFileSystemModel {
}
BusyProperty.execute(busy, () -> {
if (store.getValue() instanceof ShellStore s) {
if (store instanceof ShellStore s) {
var connection = ((ConnectionFileSystem) fileSystem).getShellControl();
var command = s.control()
.initWith(connection.getShellDialect().getCdCommand(directory))
.prepareTerminalOpen(directory + " - "
+ XPipeDaemon.getInstance()
.getStoreName(store.getValue())
.getStoreName(store)
.orElse("?"));
TerminalHelper.open(directory, command);
}
@ -283,7 +389,7 @@ final class OpenFileSystemModel {
});
}
public FileBrowserHistory getHistory() {
public BrowserHistory getHistory() {
return history;
}

View file

@ -0,0 +1,15 @@
package io.xpipe.app.browser;
import lombok.Builder;
import lombok.Value;
import lombok.With;
import lombok.extern.jackson.Jacksonized;
@Value
@With
@Jacksonized
@Builder
public class OpenFileSystemSavedState {
String lastDirectory;
}

View file

@ -37,8 +37,8 @@ public class StandaloneFileBrowser {
public static void openSingleFile(Property<FileStore> file) {
PlatformThread.runLaterIfNeeded(() -> {
var model = new FileBrowserModel(FileBrowserModel.Mode.SINGLE_FILE_CHOOSER);
var comp = new FileBrowserComp(model)
var model = new BrowserModel(BrowserModel.Mode.SINGLE_FILE_CHOOSER);
var comp = new BrowserComp(model)
.apply(struc -> struc.get().setPrefSize(1200, 700))
.apply(struc -> AppFont.normal(struc.get()));
var window = AppWindowHelper.sideWindow(AppI18n.get("openFileTitle"), stage -> comp, true, null);
@ -52,8 +52,8 @@ public class StandaloneFileBrowser {
public static void saveSingleFile(Property<FileStore> file) {
PlatformThread.runLaterIfNeeded(() -> {
var model = new FileBrowserModel(FileBrowserModel.Mode.SINGLE_FILE_SAVE);
var comp = new FileBrowserComp(model)
var model = new BrowserModel(BrowserModel.Mode.SINGLE_FILE_SAVE);
var comp = new BrowserComp(model)
.apply(struc -> struc.get().setPrefSize(1200, 700))
.apply(struc -> AppFont.normal(struc.get()));
var window = AppWindowHelper.sideWindow(AppI18n.get("saveFileTitle"), stage -> comp, true, null);

View file

@ -0,0 +1,27 @@
package io.xpipe.app.browser.action;
import io.xpipe.app.browser.BrowserEntry;
import io.xpipe.app.browser.OpenFileSystemModel;
import java.util.List;
public interface ApplicationPathAction extends BrowserAction {
public abstract String getExecutable();
@Override
public default boolean isApplicable(OpenFileSystemModel model, List<BrowserEntry> entries) {
if (entries.size() == 0) {
return false;
}
return entries.stream().allMatch(entry -> isApplicable(model, entry));
}
boolean isApplicable(OpenFileSystemModel model, BrowserEntry entry);
@Override
public default boolean isActive(OpenFileSystemModel model, List<BrowserEntry> entries) {
return model.getCache().isApplicationInPath(getExecutable());
}
}

View file

@ -0,0 +1,8 @@
package io.xpipe.app.browser.action;
import java.util.List;
public interface BranchAction extends BrowserAction {
List<LeafAction> getBranchingActions();
}

View file

@ -0,0 +1,88 @@
package io.xpipe.app.browser.action;
import io.xpipe.app.browser.BrowserEntry;
import io.xpipe.app.browser.OpenFileSystemModel;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.core.util.ModuleLayerLoader;
import javafx.scene.Node;
import javafx.scene.input.KeyCombination;
import java.util.ArrayList;
import java.util.List;
import java.util.ServiceLoader;
public interface BrowserAction {
static enum Category {
CUSTOM,
OPEN,
NATIVE,
COPY_PASTE,
MUTATION
}
static List<BrowserAction> ALL = new ArrayList<>();
public static List<LeafAction> getFlattened() {
return ALL.stream()
.map(browserAction -> browserAction instanceof LeafAction
? List.of((LeafAction) browserAction)
: ((BranchAction) browserAction).getBranchingActions())
.flatMap(List::stream)
.toList();
}
default Node getIcon(OpenFileSystemModel model, List<BrowserEntry> entries) {
return null;
}
default Category getCategory() {
return null;
}
default KeyCombination getShortcut() {
return null;
}
default boolean acceptsEmptySelection() {
return false;
}
public abstract String getName(OpenFileSystemModel model, List<BrowserEntry> entries);
public default boolean isApplicable(OpenFileSystemModel model, List<BrowserEntry> entries) {
return true;
}
public default boolean isActive(OpenFileSystemModel model, List<BrowserEntry> entries) {
return true;
}
public static class Loader implements ModuleLayerLoader {
@Override
public void init(ModuleLayer layer) {
ALL.addAll(ServiceLoader.load(layer, BrowserAction.class).stream()
.map(actionProviderProvider -> actionProviderProvider.get())
.filter(provider -> {
try {
return true;
} catch (Throwable e) {
ErrorEvent.fromThrowable(e).handle();
return false;
}
})
.toList());
}
@Override
public boolean requiresFullDaemon() {
return true;
}
@Override
public boolean prioritizeLoading() {
return false;
}
}
}

View file

@ -0,0 +1,29 @@
package io.xpipe.app.browser.action;
import io.xpipe.app.browser.BrowserEntry;
import io.xpipe.app.browser.OpenFileSystemModel;
import io.xpipe.app.util.ScriptHelper;
import io.xpipe.core.process.ShellControl;
import java.util.List;
public abstract class ExecuteApplicationAction implements LeafAction, ApplicationPathAction {
@Override
public void execute(OpenFileSystemModel model, List<BrowserEntry> entries) throws Exception {
ShellControl sc = model.getFileSystem().getShell().orElseThrow();
for (BrowserEntry entry : entries) {
var command = detach() ? ScriptHelper.createDetachCommand(sc, createCommand(model, entry)) : createCommand(model, entry);
try (var cc = sc.command(command).workingDirectory(model.getCurrentDirectory().getPath()).start()) {
cc.discardOrThrow();
}
}
}
protected boolean detach() {
return false;
}
protected abstract String createCommand(OpenFileSystemModel model, BrowserEntry entry);
}

View file

@ -0,0 +1,38 @@
package io.xpipe.app.browser.action;
import io.xpipe.app.browser.BrowserEntry;
import io.xpipe.app.browser.OpenFileSystemModel;
import io.xpipe.app.util.BusyProperty;
import io.xpipe.app.util.ThreadHelper;
import javafx.scene.control.MenuItem;
import java.util.List;
import java.util.function.UnaryOperator;
public interface LeafAction extends BrowserAction {
public abstract void execute(OpenFileSystemModel model, List<BrowserEntry> entries) throws Exception;
default MenuItem toItem(OpenFileSystemModel model, List<BrowserEntry> selected, UnaryOperator<String> nameFunc) {
var mi = new MenuItem(nameFunc.apply(getName(model, selected)));
mi.setOnAction(event -> {
ThreadHelper.runFailableAsync(() -> {
BusyProperty.execute(model.getBusy(), () -> {
execute(model, selected);
});
});
event.consume();
});
if (getShortcut() != null) {
mi.setAccelerator(getShortcut());
}
var graphic = getIcon(model, selected);
if (graphic != null) {
mi.setGraphic(graphic);
}
mi.setMnemonicParsing(false);
mi.setDisable(!isActive(model, selected));
return mi;
}
}

View file

@ -0,0 +1,96 @@
package io.xpipe.app.browser.action;
import io.xpipe.app.browser.BrowserEntry;
import io.xpipe.app.browser.OpenFileSystemModel;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.ScriptHelper;
import io.xpipe.app.util.TerminalHelper;
import io.xpipe.core.impl.FileNames;
import io.xpipe.core.process.ShellControl;
import org.apache.commons.io.FilenameUtils;
import java.util.List;
public abstract class MultiExecuteAction implements BranchAction {
protected String filesArgument(List<BrowserEntry> entries) {
return entries.size() == 1 ? entries.get(0).getOptionallyQuotedFileName() : "(" + entries.size() + ")";
}
protected abstract String createCommand(ShellControl sc, OpenFileSystemModel model, BrowserEntry entry);
@Override
public List<LeafAction> getBranchingActions() {
return List.of(
new LeafAction() {
@Override
public void execute(OpenFileSystemModel model, List<BrowserEntry> entries) throws Exception {
model.withShell(
pc -> {
for (BrowserEntry entry : entries) {
var cmd = pc.command(createCommand(pc, model, entry))
.workingDirectory(model.getCurrentDirectory()
.getPath())
.prepareTerminalOpen(FileNames.getFileName(
entry.getRawFileEntry().getPath()));
TerminalHelper.open(
FilenameUtils.getBaseName(
entry.getRawFileEntry().getPath()),
cmd);
}
},
false);
}
@Override
public String getName(OpenFileSystemModel model, List<BrowserEntry> entries) {
return "in " + AppPrefs.get().terminalType().getValue().toTranslatedString();
}
},
new LeafAction() {
@Override
public void execute(OpenFileSystemModel model, List<BrowserEntry> entries) throws Exception {
model.withShell(
pc -> {
for (BrowserEntry entry : entries) {
var cmd = ScriptHelper.createDetachCommand(
pc, createCommand(pc, model, entry));
pc.command(cmd)
.workingDirectory(model.getCurrentDirectory()
.getPath())
.execute();
}
},
false);
}
@Override
public String getName(OpenFileSystemModel model, List<BrowserEntry> entries) {
return "in background";
}
},
new LeafAction() {
@Override
public void execute(OpenFileSystemModel model, List<BrowserEntry> entries) throws Exception {
model.withShell(
pc -> {
for (BrowserEntry entry : entries) {
pc.command(createCommand(pc, model, entry))
.workingDirectory(model.getCurrentDirectory()
.getPath())
.execute();
}
},
false);
}
@Override
public String getName(OpenFileSystemModel model, List<BrowserEntry> entries) {
return "wait for completion";
}
});
}
}

View file

@ -0,0 +1,21 @@
package io.xpipe.app.browser.icon;
import io.xpipe.app.fxcomps.impl.PrettyImageComp;
import io.xpipe.core.store.FileSystem;
import javafx.beans.property.SimpleStringProperty;
public class BrowserIcons {
public static PrettyImageComp createDefaultFileIcon() {
return new PrettyImageComp(new SimpleStringProperty("default_file.svg"), 22, 22);
}
public static PrettyImageComp createDefaultDirectoryIcon() {
return new PrettyImageComp(new SimpleStringProperty("default_folder.svg"), 22, 22);
}
public static PrettyImageComp createIcon(FileType type) {
return new PrettyImageComp(new SimpleStringProperty(type.getIcon()), 22, 22);
}
public static PrettyImageComp createIcon(FileSystem.FileEntry entry) {
return new PrettyImageComp(new SimpleStringProperty(FileIconManager.getFileIcon(entry, false)), 22, 22);
}
}

View file

@ -0,0 +1,101 @@
package io.xpipe.app.browser.icon;
import io.xpipe.app.core.AppResources;
import io.xpipe.core.impl.FileNames;
import io.xpipe.core.store.FileSystem;
import lombok.Getter;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public interface DirectoryType {
List<DirectoryType> ALL = new ArrayList<>();
static DirectoryType byId(String id) {
return ALL.stream().filter(fileType -> fileType.getId().equals(id)).findAny().orElseThrow();
}
public static void loadDefinitions() {
ALL.add(new Simple(
"default", new IconVariant("default_root_folder.svg"), new IconVariant("default_root_folder_opened.svg"), ""));
AppResources.with(AppResources.XPIPE_MODULE, "folder_list.txt", path -> {
try (var reader =
new BufferedReader(new InputStreamReader(Files.newInputStream(path), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
var split = line.split("\\|");
var id = split[0].trim();
var filter = Arrays.stream(split[1].split(","))
.map(s -> {
var r = s.trim();
if (r.startsWith(".")) {
return r;
}
if (r.contains(".")) {
return r;
}
return "." + r;
})
.toList();
var closedIcon = split[2].trim();
var openIcon = split[3].trim();
var lightClosedIcon = split.length > 4 ? split[4].trim() : closedIcon;
var lightOpenIcon = split.length > 4 ? split[5].trim() : openIcon;
ALL.add(new Simple(
id, new IconVariant(lightClosedIcon, closedIcon),
new IconVariant(lightOpenIcon, openIcon),
filter.toArray(String[]::new)));
}
}
});
}
class Simple implements DirectoryType {
@Getter
private final String id;
private final IconVariant closed;
private final IconVariant open;
private final String[] names;
public Simple(String id, IconVariant closed, IconVariant open, String... names) {
this.id = id;
this.closed = closed;
this.open = open;
this.names = names;
}
@Override
public boolean matches(FileSystem.FileEntry entry) {
if (!entry.isDirectory()) {
return false;
}
return Arrays.stream(names).anyMatch(name -> FileNames.getFileName(entry.getPath())
.equalsIgnoreCase(name));
}
@Override
public String getIcon(FileSystem.FileEntry entry, boolean open) {
return open ? this.open.getIcon() : this.closed.getIcon();
}
}
String getId();
boolean matches(FileSystem.FileEntry entry);
String getIcon(FileSystem.FileEntry entry, boolean open);
}

View file

@ -1,29 +0,0 @@
package io.xpipe.app.browser.icon;
import io.xpipe.core.store.FileSystem;
import java.util.Arrays;
public interface FileIconFactory {
class SimpleFile extends IconVariant implements FileIconFactory {
private final String[] endings;
public SimpleFile(String lightIcon, String darkIcon, String... endings) {
super(lightIcon, darkIcon);
this.endings = endings;
}
@Override
public String getIcon(FileSystem.FileEntry entry) {
if (entry.isDirectory()) {
return null;
}
return Arrays.stream(endings).anyMatch(ending -> entry.getPath().toLowerCase().endsWith(ending.toLowerCase())) ? getIcon() : null;
}
}
String getIcon(FileSystem.FileEntry entry);
}

View file

@ -7,89 +7,14 @@ import io.xpipe.core.store.FileSystem;
import javafx.scene.image.Image;
import lombok.Getter;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.*;
public class FileIconManager {
private static final List<FileIconFactory> factories = new ArrayList<>();
private static final List<FolderIconFactory> folderFactories = new ArrayList<>();
@Getter
private static SvgCache svgCache = createCache();
private static boolean loaded;
private static void loadDefinitions() {
AppResources.with(AppResources.XPIPE_MODULE, "browser_icons/file_list.txt", path -> {
try (var reader =
new BufferedReader(new InputStreamReader(Files.newInputStream(path), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
var split = line.split("\\|");
var id = split[0].trim();
var filter = Arrays.stream(split[1].split(","))
.map(s -> {
var r = s.trim();
if (r.startsWith(".")) {
return r;
}
if (r.contains(".")) {
return r;
}
return "." + r;
})
.toList();
var darkIcon = split[2].trim();
var lightIcon = split.length > 3 ? split[3].trim() : darkIcon;
factories.add(new FileIconFactory.SimpleFile(lightIcon, darkIcon, filter.toArray(String[]::new)));
}
}
});
folderFactories.addAll(List.of(new FolderIconFactory.SimpleDirectory(
new IconVariant("default_root_folder.svg"), new IconVariant("default_root_folder_opened.svg"), "")));
AppResources.with(AppResources.XPIPE_MODULE, "browser_icons/folder_list.txt", path -> {
try (var reader =
new BufferedReader(new InputStreamReader(Files.newInputStream(path), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
var split = line.split("\\|");
var id = split[0].trim();
var filter = Arrays.stream(split[1].split(","))
.map(s -> {
var r = s.trim();
if (r.startsWith(".")) {
return r;
}
if (r.contains(".")) {
return r;
}
return "." + r;
})
.toList();
var closedIcon = split[2].trim();
var openIcon = split[3].trim();
var lightClosedIcon = split.length > 4 ? split[4].trim() : closedIcon;
var lightOpenIcon = split.length > 4 ? split[5].trim() : openIcon;
folderFactories.add(new FolderIconFactory.SimpleDirectory(
new IconVariant(lightClosedIcon, closedIcon),
new IconVariant(lightOpenIcon, openIcon),
filter.toArray(String[]::new)));
}
}
});
}
private static SvgCache createCache() {
return new SvgCache() {
@ -109,7 +34,6 @@ public class FileIconManager {
public static synchronized void loadIfNecessary() {
if (!loaded) {
loadDefinitions();
AppImages.loadDirectory(AppResources.XPIPE_MODULE, "browser_icons");
loaded = true;
}
@ -123,17 +47,15 @@ public class FileIconManager {
loadIfNecessary();
if (!entry.isDirectory()) {
for (var f : factories) {
var icon = f.getIcon(entry);
if (icon != null) {
return getIconPath(icon);
for (var f : FileType.ALL) {
if (f.matches(entry)) {
return getIconPath(f.getIcon());
}
}
} else {
for (var f : folderFactories) {
var icon = f.getIcon(entry, open);
if (icon != null) {
return getIconPath(icon);
for (var f : DirectoryType.ALL) {
if (f.matches(entry)) {
return getIconPath(f.getIcon(entry, open));
}
}
}

View file

@ -1,12 +0,0 @@
package io.xpipe.app.browser.icon;
import io.xpipe.app.fxcomps.impl.PrettyImageComp;
import io.xpipe.core.store.FileSystem;
import javafx.beans.property.SimpleStringProperty;
public class FileIcons {
public static PrettyImageComp createIcon(FileSystem.FileEntry entry) {
return new PrettyImageComp(new SimpleStringProperty(FileIconManager.getFileIcon(entry, false)), 22, 22);
}
}

View file

@ -0,0 +1,87 @@
package io.xpipe.app.browser.icon;
import io.xpipe.app.core.AppResources;
import io.xpipe.core.store.FileSystem;
import lombok.Getter;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public interface FileType {
List<FileType> ALL = new ArrayList<>();
static FileType byId(String id) {
return ALL.stream().filter(fileType -> fileType.getId().equals(id)).findAny().orElseThrow();
}
public static void loadDefinitions() {
AppResources.with(AppResources.XPIPE_MODULE, "file_list.txt", path -> {
try (var reader =
new BufferedReader(new InputStreamReader(Files.newInputStream(path), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
var split = line.split("\\|");
var id = split[0].trim();
var filter = Arrays.stream(split[1].split(","))
.map(s -> {
var r = s.trim();
if (r.startsWith(".")) {
return r;
}
if (r.contains(".")) {
return r;
}
return "." + r;
})
.toList();
var darkIcon = split[2].trim();
var lightIcon = split.length > 3 ? split[3].trim() : darkIcon;
ALL.add(new FileType.Simple(id, lightIcon, darkIcon, filter.toArray(String[]::new)));
}
}
});
}
@Getter
class Simple implements FileType {
private final String id;
private final IconVariant icon;
private final String[] endings;
public Simple(String id, String lightIcon, String darkIcon, String... endings) {
this.icon = new IconVariant(lightIcon, darkIcon);
this.id = id;
this.endings = endings;
}
@Override
public boolean matches(FileSystem.FileEntry entry) {
if (entry.isDirectory()) {
return false;
}
return Arrays.stream(endings)
.anyMatch(ending -> entry.getPath().toLowerCase().endsWith(ending.toLowerCase()));
}
@Override
public String getIcon() {
return icon.getIcon();
}
}
String getId();
boolean matches(FileSystem.FileEntry entry);
String getIcon();
}

View file

@ -1,36 +0,0 @@
package io.xpipe.app.browser.icon;
import io.xpipe.core.impl.FileNames;
import io.xpipe.core.store.FileSystem;
import java.util.Arrays;
public interface FolderIconFactory {
class SimpleDirectory implements FolderIconFactory {
private final IconVariant closed;
private final IconVariant open;
private final String[] names;
public SimpleDirectory(IconVariant closed, IconVariant open, String... names) {
this.closed = closed;
this.open = open;
this.names = names;
}
@Override
public String getIcon(FileSystem.FileEntry entry, boolean open) {
if (!entry.isDirectory()) {
return null;
}
return Arrays.stream(names).anyMatch(name -> FileNames.getFileName(entry.getPath())
.equalsIgnoreCase(name))
? (open ? this.open.getIcon() : this.closed.getIcon())
: null;
}
}
String getIcon(FileSystem.FileEntry entry, boolean open);
}

View file

@ -1,7 +1,7 @@
package io.xpipe.app.comp;
import io.xpipe.app.browser.FileBrowserComp;
import io.xpipe.app.browser.FileBrowserModel;
import io.xpipe.app.browser.BrowserComp;
import io.xpipe.app.browser.BrowserModel;
import io.xpipe.app.comp.about.AboutTabComp;
import io.xpipe.app.comp.base.SideMenuBarComp;
import io.xpipe.app.comp.storage.store.StoreLayoutComp;
@ -47,7 +47,7 @@ public class AppLayoutComp extends Comp<CompStructure<BorderPane>> {
new SideMenuBarComp.Entry(
AppI18n.observable("browser"),
"mdi2f-file-cabinet",
new FileBrowserComp(FileBrowserModel.DEFAULT)),
new BrowserComp(BrowserModel.DEFAULT)),
// new SideMenuBarComp.Entry(AppI18n.observable("data"), "mdsal-dvr", new SourceCollectionLayoutComp()),
new SideMenuBarComp.Entry(
AppI18n.observable("settings"), "mdsmz-miscellaneous_services", new PrefsComp(this)),
@ -78,7 +78,7 @@ public class AppLayoutComp extends Comp<CompStructure<BorderPane>> {
var pane = new BorderPane();
var sidebar = new SideMenuBarComp(selected, entries);
pane.setCenter(selected.getValue().comp().createRegion());
pane.setCenter(map.get(selected.getValue()));
pane.setRight(sidebar.createRegion());
selected.addListener((c, o, n) -> {
if (o != null && o.equals(entries.get(2))) {

View file

@ -6,7 +6,6 @@ import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.prefs.ClearCacheAlert;
import javafx.beans.binding.Bindings;
import javafx.geometry.Pos;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.HBox;
@ -33,27 +32,6 @@ public class PrefsComp extends SimpleComp {
MasterDetailPane p = (MasterDetailPane) pfx.getCenter();
p.dividerPositionProperty().setValue(0.27);
var cancel = new ButtonComp(AppI18n.observable("cancel"), null, () -> {
AppPrefs.get().cancel();
layout.selectedProperty().setValue(layout.getEntries().get(0));
})
.createRegion();
var apply = new ButtonComp(AppI18n.observable("apply"), null, () -> {
AppPrefs.get().save();
layout.selectedProperty().setValue(layout.getEntries().get(0));
})
.createRegion();
var maxWidth = Bindings.max(cancel.widthProperty(), apply.widthProperty());
cancel.minWidthProperty().bind(maxWidth);
apply.minWidthProperty().bind(maxWidth);
var rightButtons = new HBox(apply, cancel);
rightButtons.setSpacing(8);
var rightPane = new AnchorPane(rightButtons);
rightPane.setPickOnBounds(false);
AnchorPane.setBottomAnchor(rightButtons, 15.0);
AnchorPane.setRightAnchor(rightButtons, 55.0);
var clearCaches = new ButtonComp(AppI18n.observable("clearCaches"), null, ClearCacheAlert::show).createRegion();
// var reload = new ButtonComp(AppI18n.observable("reload"), null, () -> OperationMode.reload()).createRegion();
var leftButtons = new HBox(clearCaches);
@ -65,7 +43,7 @@ public class PrefsComp extends SimpleComp {
AnchorPane.setBottomAnchor(leftButtons, 15.0);
AnchorPane.setLeftAnchor(leftButtons, 15.0);
var stack = new StackPane(pfx, rightPane, leftPane);
var stack = new StackPane(pfx, leftPane);
stack.setPickOnBounds(false);
AppFont.medium(stack);

View file

@ -0,0 +1,44 @@
package io.xpipe.app.comp.base;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.control.TextArea;
import javafx.scene.layout.Region;
import lombok.AccessLevel;
import lombok.experimental.FieldDefaults;
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
public class ErrorOverlayComp extends SimpleComp {
Comp<?> background;
Property<String> text;
public ErrorOverlayComp(Comp<?> background, Property<String> text) {
this.background = background;
this.text = text;
}
@Override
protected Region createSimple() {
var content = new SimpleObjectProperty<ModalOverlayComp.OverlayContent>();
this.text.addListener((observable, oldValue, newValue) -> {
var comp = Comp.of(() -> {
var l = new TextArea();
l.textProperty().bind(text);
l.setWrapText(true);
l.getStyleClass().add("error-overlay-comp");
l.setEditable(false);
return l;
});
content.set(new ModalOverlayComp.OverlayContent("error", comp, null, () -> {}));
});
content.addListener((observable, oldValue, newValue) -> {
if (newValue == null) {
this.text.setValue(null);
}
});
return new ModalOverlayComp(background, content).createRegion();
}
}

View file

@ -1,77 +0,0 @@
package io.xpipe.app.comp.base;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.app.util.ThreadHelper;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.TextArea;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import lombok.AccessLevel;
import lombok.experimental.FieldDefaults;
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
public class MessageComp extends SimpleComp {
Property<Boolean> shown = new SimpleBooleanProperty();
ObservableValue<String> text;
int msShown;
public MessageComp(ObservableValue<String> text, int msShown) {
this.text = PlatformThread.sync(text);
this.msShown = msShown;
}
public void show() {
shown.setValue(true);
if (msShown != -1) {
ThreadHelper.runAsync(() -> {
try {
Thread.sleep(msShown);
} catch (InterruptedException ignored) {
}
shown.setValue(false);
});
}
}
@Override
protected Region createSimple() {
var l = new TextArea();
l.textProperty().bind(text);
l.setWrapText(true);
l.getStyleClass().add("message");
l.setEditable(false);
var sp = new StackPane(l);
sp.getStyleClass().add("message-comp");
SimpleChangeListener.apply(PlatformThread.sync(shown), n -> {
if (n) {
l.setMinHeight(Region.USE_PREF_SIZE);
l.setPrefHeight(Region.USE_COMPUTED_SIZE);
l.setMaxHeight(Region.USE_PREF_SIZE);
sp.setMinHeight(Region.USE_PREF_SIZE);
sp.setPrefHeight(Region.USE_COMPUTED_SIZE);
sp.setMaxHeight(Region.USE_PREF_SIZE);
} else {
l.setMinHeight(0);
l.setPrefHeight(0);
l.setMaxHeight(0);
sp.setMinHeight(0);
sp.setPrefHeight(0);
sp.setMaxHeight(0);
}
});
return sp;
}
}

View file

@ -0,0 +1,107 @@
package io.xpipe.app.comp.base;
import atlantafx.base.controls.ModalPane;
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.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.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 {
public ModalOverlayComp(Comp<?> background, Property<OverlayContent> overlayContent) {
this.background = background;
this.overlayContent = overlayContent;
}
@Value
public static class OverlayContent {
String titleKey;
Comp<?> content;
String finishKey;
Runnable onFinish;
}
private final Comp<?> background;
private final Property<OverlayContent> overlayContent;
@Override
protected Region createSimple() {
var bgRegion = background.createRegion();
var modal = new ModalPane();
modal.getStyleClass().add("modal-overlay-comp");
var pane = new StackPane(bgRegion, modal);
pane.setPickOnBounds(false);
PlatformThread.sync(overlayContent).addListener((observable, oldValue, newValue) -> {
if (oldValue != null) {
modal.hide(true);
}
if (newValue != null) {
var r = newValue.content.createRegion();
var box = new VBox(r);
box.setSpacing(15);
box.setPadding(new Insets(15));
if (newValue.finishKey != null) {
var finishButton = new Button(AppI18n.get(newValue.finishKey));
Styles.toggleStyleClass(finishButton, Styles.FLAT);
finishButton.setOnAction(event -> {
newValue.onFinish.run();
overlayContent.setValue(null);
});
var buttonBar = new ButtonBar();
buttonBar.getButtons().addAll(finishButton);
box.getChildren().add(buttonBar);
}
var tp = new TitledPane(AppI18n.get(newValue.titleKey), box);
tp.setMaxWidth(400);
tp.setCollapsible(false);
var closeButton = new Button(null, new FontIcon("mdi2w-window-close"));
closeButton.setOnAction(event -> {
overlayContent.setValue(null);
});
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());
modal.show(stack);
}
});
return pane;
}
}

View file

@ -1,7 +1,6 @@
package io.xpipe.app.comp.source.store;
import io.xpipe.app.comp.base.InstallExtensionComp;
import io.xpipe.app.comp.base.MessageComp;
import io.xpipe.app.comp.base.ErrorOverlayComp;
import io.xpipe.app.comp.base.MultiStepComp;
import io.xpipe.app.core.AppExtensionManager;
import io.xpipe.app.core.AppFont;
@ -9,7 +8,6 @@ import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppWindowHelper;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.ext.DataStoreProvider;
import io.xpipe.app.ext.DownloadModuleInstall;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.augment.GrowAugment;
@ -25,6 +23,7 @@ import io.xpipe.app.util.*;
import io.xpipe.core.store.DataStore;
import javafx.application.Platform;
import javafx.beans.property.*;
import javafx.geometry.Insets;
import javafx.scene.control.Alert;
import javafx.scene.control.Separator;
import javafx.scene.layout.BorderPane;
@ -48,7 +47,6 @@ public class GuiDsStoreCreator extends MultiStepComp.Step<CompStructure<?>> {
BooleanProperty busy = new SimpleBooleanProperty();
Property<Validator> validator = new SimpleObjectProperty<>(new SimpleValidator());
Property<String> messageProp = new SimpleStringProperty();
MessageComp message = new MessageComp(messageProp, 10000);
BooleanProperty finished = new SimpleBooleanProperty();
Property<DataStoreEntry> entry = new SimpleObjectProperty<>();
BooleanProperty changedSinceError = new SimpleBooleanProperty();
@ -188,7 +186,14 @@ public class GuiDsStoreCreator extends MultiStepComp.Step<CompStructure<?>> {
@Override
public CompStructure<? extends Region> createBase() {
var back = Comp.of(this::createLayout);
var message = new ErrorOverlayComp(back, messageProp);
return message.createStructure();
}
private Region createLayout() {
var layout = new BorderPane();
layout.setPadding(new Insets(20));
var providerChoice = new DsStoreProviderChoiceComp(filter, provider);
if (provider.getValue() != null) {
providerChoice.apply(struc -> struc.get().setDisable(true));
@ -197,37 +202,33 @@ public class GuiDsStoreCreator extends MultiStepComp.Step<CompStructure<?>> {
SimpleChangeListener.apply(provider, n -> {
if (n != null) {
var install = n.getRequiredAdditionalInstallation();
if (install != null && AppExtensionManager.getInstance().isInstalled(install)) {
layout.setCenter(new InstallExtensionComp((DownloadModuleInstall) install).createRegion());
validator.setValue(new SimpleValidator());
return;
}
// var install = n.getRequiredAdditionalInstallation();
// if (install != null && AppExtensionManager.getInstance().isInstalled(install)) {
// layout.setCenter(new InstallExtensionComp((DownloadModuleInstall)
// install).createRegion());
// validator.setValue(new SimpleValidator());
// return;
// }
var d = n.guiDialog(input);
var propVal = new SimpleValidator();
var propR = createStoreProperties(d == null || d.getComp() == null ? null : d.getComp(), propVal);
var box = new VBox(propR);
box.setSpacing(7);
layout.setCenter(propR);
layout.setCenter(box);
validator.setValue(new ChainedValidator(List.of(d != null && d.getValidator() != null ? d.getValidator() : new SimpleValidator(), propVal)));
validator.setValue(new ChainedValidator(List.of(
d != null && d.getValidator() != null ? d.getValidator() : new SimpleValidator(), propVal)));
} else {
layout.setCenter(null);
validator.setValue(new SimpleValidator());
}
});
layout.setBottom(message.createRegion());
var sep = new Separator();
sep.getStyleClass().add("spacer");
var top = new VBox(providerChoice.createRegion(), sep);
top.getStyleClass().add("top");
layout.setTop(top);
// layout.getStyleClass().add("data-input-creation-step");
return Comp.of(() -> layout).createStructure();
return layout;
}
@Override
@ -275,7 +276,6 @@ public class GuiDsStoreCreator extends MultiStepComp.Step<CompStructure<?>> {
.getText();
TrackEvent.info(msg);
messageProp.setValue(msg);
message.show();
changedSinceError.setValue(false);
return false;
}
@ -287,7 +287,6 @@ public class GuiDsStoreCreator extends MultiStepComp.Step<CompStructure<?>> {
PlatformThread.runLaterIfNeeded(parent::next);
} catch (Exception ex) {
messageProp.setValue(ExceptionConverter.convertMessage(ex));
message.show();
changedSinceError.setValue(false);
ErrorEvent.fromThrowable(ex).omit().reportable(false).handle();
}

View file

@ -3,7 +3,7 @@ package io.xpipe.app.comp.storage.collection;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppWindowHelper;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.augment.PopupMenuAugment;
import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.DesktopHelper;
import javafx.scene.control.Alert;
@ -13,19 +13,14 @@ import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.layout.Region;
import org.kordamp.ikonli.javafx.FontIcon;
public class SourceCollectionContextMenu<S extends CompStructure<?>> extends PopupMenuAugment<S> {
private final SourceCollectionWrapper group;
private final Region renameTextField;
public class SourceCollectionContextMenu<S extends CompStructure<?>> extends ContextMenuAugment<S> {
public SourceCollectionContextMenu(
boolean showOnPrimaryButton, SourceCollectionWrapper group, Region renameTextField) {
super(showOnPrimaryButton);
this.group = group;
this.renameTextField = renameTextField;
super(showOnPrimaryButton, () -> createContextMenu(group, renameTextField));
}
private void onDelete() {
private static void onDelete(SourceCollectionWrapper group) {
if (group.getEntries().size() > 0) {
AppWindowHelper.showBlockingAlert(alert -> {
alert.setTitle(AppI18n.get("confirmCollectionDeletionTitle"));
@ -44,7 +39,7 @@ public class SourceCollectionContextMenu<S extends CompStructure<?>> extends Pop
}
}
private void onClean() {
private static void onClean(SourceCollectionWrapper group) {
if (group.getEntries().size() > 0) {
AppWindowHelper.showBlockingAlert(alert -> {
alert.setTitle(AppI18n.get("confirmCollectionDeletionTitle"));
@ -63,8 +58,7 @@ public class SourceCollectionContextMenu<S extends CompStructure<?>> extends Pop
}
}
@Override
protected ContextMenu createContextMenu() {
protected static ContextMenu createContextMenu(SourceCollectionWrapper group, Region renameTextField) {
var cm = new ContextMenu();
var name = new MenuItem(group.getName());
name.setDisable(true);
@ -96,13 +90,13 @@ public class SourceCollectionContextMenu<S extends CompStructure<?>> extends Pop
if (group.isDeleteable()) {
var del = new MenuItem(AppI18n.get("delete"), new FontIcon("mdal-delete_outline"));
del.setOnAction(e -> {
onDelete();
onDelete(group);
});
cm.getItems().add(del);
} else {
var del = new MenuItem(AppI18n.get("clean"), new FontIcon("mdal-delete_outline"));
del.setOnAction(e -> {
onClean();
onClean(group);
});
cm.getItems().add(del);
}

View file

@ -3,7 +3,7 @@ package io.xpipe.app.comp.storage.source;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.augment.PopupMenuAugment;
import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataSourceEntry;
@ -16,19 +16,14 @@ import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.layout.Region;
import org.kordamp.ikonli.javafx.FontIcon;
public class SourceEntryContextMenu<S extends CompStructure<?>> extends PopupMenuAugment<S> {
public class SourceEntryContextMenu<S extends CompStructure<?>> extends ContextMenuAugment<S> {
private final SourceEntryWrapper entry;
private final Region renameTextField;
public SourceEntryContextMenu(boolean showOnPrimaryButton, SourceEntryWrapper entry, Region renameTextField) {
super(showOnPrimaryButton);
this.entry = entry;
this.renameTextField = renameTextField;
super(showOnPrimaryButton, () -> createContextMenu(entry, renameTextField));
}
@Override
protected ContextMenu createContextMenu() {
protected static ContextMenu createContextMenu(SourceEntryWrapper entry, Region renameTextField) {
var cm = new ContextMenu();
AppFont.normal(cm.getStyleableNode());

View file

@ -9,7 +9,7 @@ import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.augment.GrowAugment;
import io.xpipe.app.fxcomps.augment.PopupMenuAugment;
import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
import io.xpipe.app.fxcomps.impl.*;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
@ -164,12 +164,7 @@ public class StoreEntryComp extends SimpleComp {
});
});
new PopupMenuAugment<>(false) {
@Override
protected ContextMenu createContextMenu() {
return StoreEntryComp.this.createContextMenu();
}
}.augment(new SimpleCompStructure<>(button));
new ContextMenuAugment<>(false, () -> StoreEntryComp.this.createContextMenu()).augment(new SimpleCompStructure<>(button));
return button;
}
@ -218,12 +213,7 @@ public class StoreEntryComp extends SimpleComp {
private Comp<?> createSettingsButton() {
var settingsButton = new IconButtonComp("mdomz-settings");
settingsButton.styleClass("settings");
settingsButton.apply(new PopupMenuAugment<>(true) {
@Override
protected ContextMenu createContextMenu() {
return StoreEntryComp.this.createContextMenu();
}
});
settingsButton.apply(new ContextMenuAugment<>(true, () -> StoreEntryComp.this.createContextMenu()));
settingsButton.apply(GrowAugment.create(false, true));
settingsButton.apply(s -> {
s.get().prefWidthProperty().bind(Bindings.divide(s.get().heightProperty(), 1.35));

View file

@ -0,0 +1,36 @@
package io.xpipe.app.comp.storage.store;
import javafx.collections.ListChangeListener;
import javafx.scene.control.TreeItem;
public class StoreEntryTree {
public static TreeItem<StoreEntryWrapper> createTree() {
var topLevel = StoreSection.createTopLevel();
var root = new TreeItem<StoreEntryWrapper>();
root.setExpanded(true);
// Listen for any entry list change, not only top level changes
StoreViewState.get().getAllEntries().addListener((ListChangeListener<? super StoreEntryWrapper>) c -> {
root.getChildren().clear();
for (StoreSection v : topLevel.getChildren()) {
add(root, v);
}
});
for (StoreSection v : topLevel.getChildren()) {
add(root, v);
}
return root;
}
private static void add(TreeItem<StoreEntryWrapper> parent, StoreSection section) {
var item = new TreeItem<>(section.getWrapper());
item.setExpanded(section.getWrapper().getExpanded().getValue());
parent.getChildren().add(item);
for (StoreSection child : section.getChildren()) {
add(item, child);
}
}
}

View file

@ -73,7 +73,7 @@ public class App extends Application {
var titleBinding = Bindings.createStringBinding(
() -> {
var base = String.format(
"X-Pipe Desktop (%s)", AppProperties.get().getVersion());
"XPipe Desktop (%s)", AppProperties.get().getVersion());
var prefix = AppProperties.get().isStaging() ? "[STAGE] " : "";
var suffix = XPipeDistributionType.get().getUpdateHandler().getPreparedUpdate().getValue() != null
? String.format(

View file

@ -1,17 +1,9 @@
package io.xpipe.app.core;
import atlantafx.base.theme.NordDark;
import atlantafx.base.theme.NordLight;
import atlantafx.base.theme.PrimerDark;
import atlantafx.base.theme.PrimerLight;
import io.xpipe.app.ext.PrefsChoiceValue;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.prefs.AppPrefs;
import javafx.application.Application;
import javafx.scene.Scene;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.io.IOException;
import java.nio.file.FileVisitResult;
@ -36,9 +28,6 @@ public class AppStyle {
loadStylesheets();
if (AppPrefs.get() != null) {
AppPrefs.get().theme.addListener((c, o, n) -> {
changeTheme(o, n);
});
AppPrefs.get().useSystemFont.addListener((c, o, n) -> {
changeFontUsage(n);
});
@ -78,12 +67,6 @@ public class AppStyle {
}
}
private static void changeTheme(Theme oldTheme, Theme newTheme) {
scenes.forEach(scene -> {
Application.setUserAgentStylesheet(newTheme.getTheme().getUserAgentStylesheet());
});
}
private static void changeFontUsage(boolean use) {
if (!use) {
scenes.forEach(scene -> {
@ -106,10 +89,6 @@ public class AppStyle {
}
public static void addStylesheets(Scene scene) {
var t = AppPrefs.get() != null ? AppPrefs.get().theme.getValue() : Theme.LIGHT;
Application.setUserAgentStylesheet(t.getTheme().getUserAgentStylesheet());
TrackEvent.debug("Set theme " + t.getId() + " for scene");
if (AppPrefs.get() != null && !AppPrefs.get().useSystemFont.get()) {
scene.getStylesheets().add(FONT_CONTENTS);
}
@ -122,21 +101,4 @@ public class AppStyle {
scenes.add(scene);
}
@AllArgsConstructor
@Getter
public enum Theme implements PrefsChoiceValue {
LIGHT("light", new PrimerLight()),
DARK("dark", new PrimerDark()),
NORD_LIGHT("nordLight", new NordLight()),
NORD_DARK("nordDark", new NordDark());
// DARK("dark");
private final String id;
private final atlantafx.base.theme.Theme theme;
@Override
public String toTranslatedString() {
return theme.getName();
}
}
}

View file

@ -0,0 +1,142 @@
package io.xpipe.app.core;
import atlantafx.base.theme.*;
import com.jthemedetecor.OsThemeDetector;
import io.xpipe.app.ext.PrefsChoiceValue;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.core.process.OsType;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.css.PseudoClass;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.stage.Window;
import javafx.util.Duration;
import lombok.AllArgsConstructor;
import lombok.Getter;
public class AppTheme {
public record AccentColor(Color primaryColor, PseudoClass pseudoClass) {
public static AccentColor xpipeBlue() {
return new AccentColor(Color.web("#11B4B4"), PseudoClass.getPseudoClass("accent-primer-purple"));
}
}
public static void init() {
if (AppPrefs.get() == null) {
return;
}
OsThemeDetector detector = OsThemeDetector.getDetector();
if (AppPrefs.get().theme.getValue() == null) {
try {
setDefault(detector.isDark());
} catch (Throwable ex) {
ErrorEvent.fromThrowable(ex).omit().handle();
setDefault(false);
}
}
var t = AppPrefs.get().theme.getValue();
Application.setUserAgentStylesheet(t.getTheme().getUserAgentStylesheet());
TrackEvent.debug("Set theme " + t.getId() + " for scene");
detector.registerListener(dark -> {
PlatformThread.runLaterIfNeeded(() -> {
if (dark && !AppPrefs.get().theme.getValue().getTheme().isDarkMode()) {
AppPrefs.get().theme.setValue(Theme.getDefaultDarkTheme());
}
if (!dark && AppPrefs.get().theme.getValue().getTheme().isDarkMode()) {
AppPrefs.get().theme.setValue(Theme.getDefaultLightTheme());
}
});
});
AppPrefs.get().theme.addListener((c, o, n) -> {
changeTheme(n);
});
}
private static void setDefault(boolean dark) {
if (dark) {
AppPrefs.get().theme.setValue(Theme.getDefaultDarkTheme());
} else {
AppPrefs.get().theme.setValue(Theme.getDefaultLightTheme());
}
}
private static void changeTheme(Theme newTheme) {
if (newTheme == null) {
return;
}
PlatformThread.runLaterIfNeeded(() -> {
for (Window window : Window.getWindows()) {
var scene = window.getScene();
Image snapshot = scene.snapshot(null);
Pane root = (Pane) scene.getRoot();
ImageView imageView = new ImageView(snapshot);
root.getChildren().add(imageView);
// Animate!
var transition = new Timeline(
new KeyFrame(Duration.ZERO, new KeyValue(imageView.opacityProperty(), 1, Interpolator.EASE_OUT)),
new KeyFrame(
Duration.millis(1250), new KeyValue(imageView.opacityProperty(), 0, Interpolator.EASE_OUT)));
transition.setOnFinished(e -> root.getChildren().remove(imageView));
transition.play();
}
Application.setUserAgentStylesheet(newTheme.getTheme().getUserAgentStylesheet());
TrackEvent.debug("Set theme " + newTheme.getId() + " for scene");
});
}
@AllArgsConstructor
@Getter
public enum Theme implements PrefsChoiceValue {
PRIMER_LIGHT("light", new PrimerLight()),
PRIMER_DARK("dark", new PrimerDark()),
NORD_LIGHT("nordLight", new NordLight()),
NORD_DARK("nordDark", new NordDark()),
CUPERTINO_LIGHT("cupertinoLight", new CupertinoLight()),
CUPERTINO_DARK("cupertinoDark", new CupertinoDark()),
DRACULA("dracula", new Dracula());
static Theme getDefaultLightTheme() {
return switch (OsType.getLocal()) {
case OsType.Windows windows -> PRIMER_LIGHT;
case OsType.Linux linux -> NORD_LIGHT;
case OsType.MacOs macOs -> CUPERTINO_LIGHT;
};
}
static Theme getDefaultDarkTheme() {
return switch (OsType.getLocal()) {
case OsType.Windows windows -> PRIMER_DARK;
case OsType.Linux linux -> NORD_DARK;
case OsType.MacOs macOs -> CUPERTINO_DARK;
};
}
private final String id;
private final atlantafx.base.theme.Theme theme;
@Override
public String toTranslatedString() {
return theme.getName();
}
}
}

View file

@ -8,6 +8,7 @@ import io.xpipe.app.launcher.LauncherCommand;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.app.util.XPipeSession;
import io.xpipe.core.util.XPipeDaemonMode;
import io.xpipe.core.util.XPipeSystemId;
import org.apache.commons.lang3.function.FailableRunnable;
import java.util.ArrayList;
@ -91,6 +92,7 @@ public abstract class OperationMode {
AppProperties.logArguments(args);
AppProperties.logSystemProperties();
AppProperties.logPassedProperties();
XPipeSystemId.init();
TrackEvent.info("mode", "Finished initial setup");
} catch (Throwable ex) {
ErrorEvent.fromThrowable(ex).term().handle();

View file

@ -60,6 +60,7 @@ public abstract class PlatformMode extends OperationMode {
TrackEvent.info("mode", "Platform mode initial setup");
AppI18n.init();
AppFont.loadFonts();
AppTheme.init();
AppStyle.init();
AppImages.init();
TrackEvent.info("mode", "Finished essential component initialization before platform");

View file

@ -0,0 +1,43 @@
package io.xpipe.app.fxcomps.augment;
import io.xpipe.app.fxcomps.CompStructure;
import javafx.scene.control.ContextMenu;
import javafx.scene.input.MouseButton;
import java.util.function.Supplier;
public class ContextMenuAugment<S extends CompStructure<?>> implements Augment<S> {
private final boolean showOnPrimaryButton;
private final Supplier<ContextMenu> contextMenu;
public ContextMenuAugment(boolean showOnPrimaryButton, Supplier<ContextMenu> contextMenu) {
this.showOnPrimaryButton = showOnPrimaryButton;
this.contextMenu = contextMenu;
}
private static ContextMenu currentContextMenu;
@Override
public void augment(S struc) {
var r = struc.get();
r.setOnMousePressed(event -> {
if (currentContextMenu != null && currentContextMenu.isShowing()) {
currentContextMenu.hide();
currentContextMenu = null;
}
if ((showOnPrimaryButton && event.getButton() == MouseButton.PRIMARY)
|| (!showOnPrimaryButton && event.getButton() == MouseButton.SECONDARY)) {
var cm = contextMenu.get();
if (cm != null) {
cm.setAutoHide(true);
cm.show(r, event.getScreenX(), event.getScreenY());
currentContextMenu = cm;
}
event.consume();
}
});
}
}

View file

@ -1,31 +0,0 @@
package io.xpipe.app.fxcomps.augment;
import io.xpipe.app.fxcomps.CompStructure;
import javafx.scene.control.ContextMenu;
import javafx.scene.input.MouseButton;
public abstract class PopupMenuAugment<S extends CompStructure<?>> implements Augment<S> {
private final boolean showOnPrimaryButton;
protected PopupMenuAugment(boolean showOnPrimaryButton) {
this.showOnPrimaryButton = showOnPrimaryButton;
}
protected abstract ContextMenu createContextMenu();
@Override
public void augment(S struc) {
var cm = createContextMenu();
var r = struc.get();
r.setOnMousePressed(event -> {
if ((showOnPrimaryButton && event.getButton() == MouseButton.PRIMARY)
|| (!showOnPrimaryButton && event.getButton() == MouseButton.SECONDARY)) {
cm.show(r, event.getScreenX(), event.getScreenY());
event.consume();
} else {
cm.hide();
}
});
}
}

View file

@ -52,7 +52,7 @@ public class SvgView {
var widthProperty = new SimpleIntegerProperty();
var heightProperty = new SimpleIntegerProperty();
SimpleChangeListener.apply(content, val -> {
if (val == null) {
if (val == null || val.isBlank()) {
return;
}

View file

@ -9,6 +9,7 @@ import javafx.scene.layout.Region;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
@ -21,7 +22,6 @@ public class Shortcuts {
}
public static <T extends Region> void addShortcut(T region, KeyCombination comb, Consumer<T> exec) {
AtomicReference<Scene> scene = new AtomicReference<>(region.getScene());
var filter = new EventHandler<KeyEvent>() {
public void handle(KeyEvent ke) {
if (comb.match(ke)) {
@ -30,21 +30,23 @@ public class Shortcuts {
}
}
};
SHORTCUTS.put(region, comb);
AtomicReference<Scene> scene = new AtomicReference<>();
SimpleChangeListener.apply(region.sceneProperty(), s -> {
if (Objects.equals(s, scene.get())) {
return;
}
if (scene.get() != null) {
scene.get().removeEventHandler(KeyEvent.KEY_PRESSED, filter);
SHORTCUTS.remove(region);
scene.set(null);
}
if (s != null) {
scene.set(s);
s.addEventHandler(KeyEvent.KEY_PRESSED, filter);
SHORTCUTS.put(region, comb);
} else {
if (scene.get() == null) {
return;
}
scene.get().removeEventHandler(KeyEvent.KEY_PRESSED, filter);
SHORTCUTS.remove(region);
scene.set(null);
}
});
}

View file

@ -1,9 +1,11 @@
package io.xpipe.app.launcher;
import io.xpipe.app.browser.BrowserModel;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.ext.ActionProvider;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.core.store.ShellStore;
import lombok.Getter;
import lombok.Value;
@ -114,7 +116,8 @@ public abstract class LauncherInput {
return;
}
// GuiDsCreatorMultiStep.showForStore(DataSourceProvider.Category.STREAM, FileStore.local(file), null);
var dir = Files.isDirectory(file) ? file : file.getParent();
BrowserModel.DEFAULT.openFileSystemAsync(ShellStore.createLocal(), dir.toString());
}
@Override

View file

@ -11,7 +11,7 @@ import com.dlsc.preferencesfx.util.VisibilityProperty;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppProperties;
import io.xpipe.app.core.AppStyle;
import io.xpipe.app.core.AppTheme;
import io.xpipe.app.ext.PrefsChoiceValue;
import io.xpipe.app.ext.PrefsHandler;
import io.xpipe.app.ext.PrefsProvider;
@ -68,8 +68,8 @@ public class AppPrefs {
private static AppPrefs INSTANCE;
private final SimpleListProperty<SupportedLocale> languageList =
new SimpleListProperty<>(FXCollections.observableArrayList(Arrays.asList(SupportedLocale.values())));
private final SimpleListProperty<AppStyle.Theme> themeList =
new SimpleListProperty<>(FXCollections.observableArrayList(Arrays.asList(AppStyle.Theme.values())));
private final SimpleListProperty<AppTheme.Theme> themeList =
new SimpleListProperty<>(FXCollections.observableArrayList(Arrays.asList(AppTheme.Theme.values())));
private final SimpleListProperty<CloseBehaviour> closeBehaviourList = new SimpleListProperty<>(
FXCollections.observableArrayList(PrefsChoiceValue.getSupported(CloseBehaviour.class)));
private final SimpleListProperty<ExternalEditorType> externalEditorList = new SimpleListProperty<>(
@ -90,11 +90,10 @@ public class AppPrefs {
languageList, languageInternal)
.render(() -> new TranslatableComboBoxControl<>());
private final ObjectProperty<AppStyle.Theme> themeInternal =
typed(new SimpleObjectProperty<>(AppStyle.Theme.LIGHT), AppStyle.Theme.class);
public final ReadOnlyProperty<AppStyle.Theme> theme = themeInternal;
private final SingleSelectionField<AppStyle.Theme> themeControl =
Field.ofSingleSelectionType(themeList, themeInternal).render(() -> new TranslatableComboBoxControl<>());
public final ObjectProperty<AppTheme.Theme> theme =
typed(new SimpleObjectProperty<>(), AppTheme.Theme.class);
private final SingleSelectionField<AppTheme.Theme> themeControl =
Field.ofSingleSelectionType(themeList, theme).render(() -> new TranslatableComboBoxControl<>());
private final BooleanProperty useSystemFontInternal = typed(new SimpleBooleanProperty(true), Boolean.class);
public final ReadOnlyBooleanProperty useSystemFont = useSystemFontInternal;
private final IntegerProperty tooltipDelayInternal = typed(new SimpleIntegerProperty(1000), Integer.class);
@ -512,7 +511,7 @@ public class AppPrefs {
Group.of(
"uiOptions",
Setting.of("language", languageControl, languageInternal),
Setting.of("theme", themeControl, themeInternal),
Setting.of("theme", themeControl, theme),
Setting.of("useSystemFont", useSystemFontInternal),
Setting.of("tooltipDelay", tooltipDelayInternal, tooltipDelayMin, tooltipDelayMax)),
Group.of("windowOptions", Setting.of("saveWindowLocation", saveWindowLocationInternal))),

View file

@ -27,6 +27,11 @@ public abstract class ExternalApplicationType implements PrefsChoiceValue {
public abstract boolean isAvailable();
@Override
public String toString() {
return getId();
}
public static class MacApplication extends ExternalApplicationType {
protected final String applicationName;

View file

@ -23,6 +23,11 @@ public interface ExternalEditorType extends PrefsChoiceValue {
public static final ExternalEditorType VSCODE_WINDOWS = new WindowsFullPathType("app.vscode") {
@Override
public boolean canOpenDirectory() {
return true;
}
@Override
protected Optional<Path> determinePath() {
return Optional.of(Path.of(System.getenv("LOCALAPPDATA"))
@ -48,7 +53,13 @@ public interface ExternalEditorType extends PrefsChoiceValue {
}
};
public static final LinuxPathType VSCODE_LINUX = new LinuxPathType("app.vscode", "code");
public static final LinuxPathType VSCODE_LINUX = new LinuxPathType("app.vscode", "code") {
@Override
public boolean canOpenDirectory() {
return true;
}
};
public static final LinuxPathType KATE = new LinuxPathType("app.kate", "kate");
@ -81,7 +92,13 @@ public interface ExternalEditorType extends PrefsChoiceValue {
public static final ExternalEditorType SUBLIME_MACOS = new MacOsEditor("app.sublime", "Sublime Text");
public static final ExternalEditorType VSCODE_MACOS = new MacOsEditor("app.vscode", "Visual Studio Code");
public static final ExternalEditorType VSCODE_MACOS = new MacOsEditor("app.vscode", "Visual Studio Code") {
@Override
public boolean canOpenDirectory() {
return true;
}
};
public static final ExternalEditorType CUSTOM = new ExternalEditorType() {
@ -110,6 +127,10 @@ public interface ExternalEditorType extends PrefsChoiceValue {
public void launch(Path file) throws Exception;
default boolean canOpenDirectory() {
return false;
}
public static class LinuxPathType extends ExternalApplicationType.PathApplication implements ExternalEditorType {
public LinuxPathType(String id, String command) {

View file

@ -17,7 +17,7 @@ import java.util.stream.Stream;
public interface ExternalTerminalType extends PrefsChoiceValue {
public static final ExternalTerminalType CMD = new SimpleType("cmd", "cmd.exe", "cmd.exe") {
ExternalTerminalType CMD = new SimpleType("app.cmd", "cmd.exe", "cmd.exe") {
@Override
protected String toCommand(String name, String file) {
@ -30,8 +30,8 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
}
};
public static final ExternalTerminalType POWERSHELL_WINDOWS =
new SimpleType("powershell", "powershell", "PowerShell") {
ExternalTerminalType POWERSHELL_WINDOWS =
new SimpleType("app.powershell", "powershell", "PowerShell") {
@Override
protected String toCommand(String name, String file) {
@ -44,13 +44,13 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
}
};
public static final ExternalTerminalType PWSH_WINDOWS = new SimpleType("pwsh", "pwsh", "PowerShell Core") {
ExternalTerminalType PWSH_WINDOWS = new SimpleType("app.pwsh", "pwsh", "PowerShell Core") {
@Override
protected String toCommand(String name, String file) {
// Fix for https://github.com/PowerShell/PowerShell/issues/18530#issuecomment-1325691850
var script = ScriptHelper.createLocalExecScript("set \"PSModulePath=\"\r\n\"" + file + "\"\npause");
return "-ExecutionPolicy Bypass -NoProfile -Command cmd /C '" +script + "'";
return "-ExecutionPolicy Bypass -NoProfile -Command cmd /C '" + script + "'";
}
@Override
@ -59,8 +59,8 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
}
};
public static final ExternalTerminalType WINDOWS_TERMINAL =
new SimpleType("windowsTerminal", "wt.exe", "Windows Terminal") {
ExternalTerminalType WINDOWS_TERMINAL =
new SimpleType("app.windowsTerminal", "wt.exe", "Windows Terminal") {
@Override
protected String toCommand(String name, String file) {
@ -77,8 +77,8 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
}
};
public static final ExternalTerminalType GNOME_TERMINAL =
new SimpleType("gnomeTerminal", "gnome-terminal", "Gnome Terminal") {
ExternalTerminalType GNOME_TERMINAL =
new SimpleType("app.gnomeTerminal", "gnome-terminal", "Gnome Terminal") {
@Override
public void launch(String name, String file, boolean elevated) throws Exception {
@ -105,11 +105,12 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
}
};
public static final ExternalTerminalType KONSOLE = new SimpleType("konsole", "konsole", "Konsole") {
ExternalTerminalType KONSOLE = new SimpleType("app.konsole", "konsole", "Konsole") {
@Override
protected String toCommand(String name, String file) {
// Note for later: When debugging konsole launches, it will always open as a child process of IntelliJ/X-Pipe even though we try to detach it.
// Note for later: When debugging konsole launches, it will always open as a child process of
// IntelliJ/X-Pipe even though we try to detach it.
// This is not the case for production where it works as expected
return "--new-tab -e \"" + file + "\"";
}
@ -120,7 +121,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
}
};
public static final ExternalTerminalType XFCE = new SimpleType("xfce", "xfce4-terminal", "Xfce") {
ExternalTerminalType XFCE = new SimpleType("app.xfce", "xfce4-terminal", "Xfce") {
@Override
protected String toCommand(String name, String file) {
@ -133,15 +134,15 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
}
};
public static final ExternalTerminalType MACOS_TERMINAL = new MacOsTerminalType();
ExternalTerminalType MACOS_TERMINAL = new MacOsTerminalType();
public static final ExternalTerminalType ITERM2 = new ITerm2Type();
ExternalTerminalType ITERM2 = new ITerm2Type();
public static final ExternalTerminalType WARP = new WarpType();
ExternalTerminalType WARP = new WarpType();
public static final ExternalTerminalType CUSTOM = new CustomType();
ExternalTerminalType CUSTOM = new CustomType();
public static final List<ExternalTerminalType> ALL = Stream.of(
List<ExternalTerminalType> ALL = Stream.of(
WINDOWS_TERMINAL,
PWSH_WINDOWS,
POWERSHELL_WINDOWS,
@ -156,7 +157,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
.filter(terminalType -> terminalType.isSelectable())
.toList();
public static ExternalTerminalType getDefault() {
static ExternalTerminalType getDefault() {
return ALL.stream()
.filter(externalTerminalType -> !externalTerminalType.equals(CUSTOM))
.filter(terminalType -> terminalType.isAvailable())
@ -164,12 +165,12 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
.orElse(null);
}
public abstract void launch(String name, String file, boolean elevated) throws Exception;
void launch(String name, String file, boolean elevated) throws Exception;
static class MacOsTerminalType extends ExternalApplicationType.MacApplication implements ExternalTerminalType {
class MacOsTerminalType extends ExternalApplicationType.MacApplication implements ExternalTerminalType {
public MacOsTerminalType() {
super("macosTerminal", "Terminal");
super("app.macosTerminal", "Terminal");
}
@Override
@ -178,22 +179,21 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
var suffix = file.equals(pc.getShellDialect().getOpenCommand())
? "\"\""
: "\"" + file.replaceAll("\"", "\\\\\"") + "\"";
var cmd = String.format(
"""
osascript - "$@" <<EOF
pc.osascriptCommand(String.format(
"""
activate application "Terminal"
tell app "Terminal" to do script %s
EOF""",
suffix);
pc.executeSimpleCommand(cmd);
""",
suffix))
.execute();
}
}
}
static class CustomType extends ExternalApplicationType implements ExternalTerminalType {
class CustomType extends ExternalApplicationType implements ExternalTerminalType {
public CustomType() {
super("custom");
super("app.custom");
}
@Override
@ -226,18 +226,17 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
}
}
static class ITerm2Type extends ExternalApplicationType.MacApplication implements ExternalTerminalType {
class ITerm2Type extends ExternalApplicationType.MacApplication implements ExternalTerminalType {
public ITerm2Type() {
super("iterm2", "iTerm");
super("app.iterm2", "iTerm");
}
@Override
public void launch(String name, String file, boolean elevated) throws Exception {
try (ShellControl pc = LocalStore.getShell()) {
var cmd = String.format(
"""
osascript - "$@" <<EOF
pc.osascriptCommand(String.format(
"""
if application "iTerm" is running then
tell application "iTerm"
create window with profile "Default" command "%s"
@ -253,17 +252,17 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
end tell
end tell
end if
EOF""",
file.replaceAll("\"", "\\\\\""), file.replaceAll("\"", "\\\\\""));
pc.executeSimpleCommand(cmd);
""",
file.replaceAll("\"", "\\\\\""), file.replaceAll("\"", "\\\\\"")))
.execute();
}
}
}
static class WarpType extends ExternalApplicationType.MacApplication implements ExternalTerminalType {
class WarpType extends ExternalApplicationType.MacApplication implements ExternalTerminalType {
public WarpType() {
super("warp", "Warp");
super("app.warp", "Warp");
}
@Override
@ -273,28 +272,26 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
}
try (ShellControl pc = LocalStore.getShell()) {
var cmd = String.format(
"""
osascript - "$@" <<EOF
tell application "Warp" to activate
tell application "System Events" to tell process "Warp" to keystroke "t" using command down
delay 1
tell application "System Events"
tell process "Warp"
keystroke "%s"
key code 36
end tell
end tell
EOF
""",
file.replaceAll("\"", "\\\\\""));
pc.executeSimpleCommand(cmd);
pc.osascriptCommand(String.format(
"""
tell application "Warp" to activate
tell application "System Events" to tell process "Warp" to keystroke "t" using command down
delay 1
tell application "System Events"
tell process "Warp"
keystroke "%s"
key code 36
end tell
end tell
""",
file.replaceAll("\"", "\\\\\"")))
.execute();
}
}
}
@Getter
public abstract static class SimpleType extends ExternalApplicationType.PathApplication
abstract class SimpleType extends ExternalApplicationType.PathApplication
implements ExternalTerminalType {
private final String displayName;
@ -308,9 +305,12 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
public void launch(String name, String file, boolean elevated) throws Exception {
if (elevated) {
if (OsType.getLocal().equals(OsType.WINDOWS)) {
try (ShellControl pc = LocalStore.getShell().subShell(ShellDialects.POWERSHELL).start()) {
try (ShellControl pc = LocalStore.getShell()
.subShell(ShellDialects.POWERSHELL)
.start()) {
ApplicationHelper.checkSupport(pc, executable, displayName);
var toExecute = "Start-Process \"" + executable + "\" -Verb RunAs -ArgumentList \"" + toCommand(name, file).replaceAll("\"", "`\"") + "\"";
var toExecute = "Start-Process \"" + executable + "\" -Verb RunAs -ArgumentList \""
+ toCommand(name, file).replaceAll("\"", "`\"") + "\"";
pc.executeSimpleCommand(toExecute);
}
return;

View file

@ -42,7 +42,7 @@ public class TranslatableComboBoxControl<V extends Translatable>
readOnlyLabel.getStyleClass().add("read-only-label");
comboBox.setMaxWidth(Double.MAX_VALUE);
comboBox.setVisibleRowCount(4);
comboBox.setVisibleRowCount(10);
node.setAlignment(Pos.CENTER_LEFT);
node.getChildren().addAll(comboBox, readOnlyLabel);

View file

@ -1,64 +0,0 @@
/* SPDX-License-Identifier: MIT */
package io.xpipe.app.util;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import static javafx.scene.layout.Region.USE_COMPUTED_SIZE;
import static javafx.scene.layout.Region.USE_PREF_SIZE;
public final class Containers {
public static final ColumnConstraints H_GROW_NEVER = columnConstraints(Priority.NEVER);
public static void setAnchors(Node node, Insets insets) {
if (insets.getTop() >= 0) {
AnchorPane.setTopAnchor(node, insets.getTop());
}
if (insets.getRight() >= 0) {
AnchorPane.setRightAnchor(node, insets.getRight());
}
if (insets.getBottom() >= 0) {
AnchorPane.setBottomAnchor(node, insets.getBottom());
}
if (insets.getLeft() >= 0) {
AnchorPane.setLeftAnchor(node, insets.getLeft());
}
}
public static void setScrollConstraints(ScrollPane scrollPane,
ScrollPane.ScrollBarPolicy vbarPolicy, boolean fitHeight,
ScrollPane.ScrollBarPolicy hbarPolicy, boolean fitWidth) {
scrollPane.setVbarPolicy(vbarPolicy);
scrollPane.setFitToHeight(fitHeight);
scrollPane.setHbarPolicy(hbarPolicy);
scrollPane.setFitToWidth(fitWidth);
}
public static ColumnConstraints columnConstraints(Priority hgrow) {
return columnConstraints(USE_COMPUTED_SIZE, hgrow);
}
public static ColumnConstraints columnConstraints(double minWidth, Priority hgrow) {
double maxWidth = hgrow == Priority.ALWAYS ? Double.MAX_VALUE : USE_PREF_SIZE;
ColumnConstraints constraints = new ColumnConstraints(minWidth, USE_COMPUTED_SIZE, maxWidth);
constraints.setHgrow(hgrow);
return constraints;
}
public static void usePrefWidth(Region region) {
region.setMinWidth(USE_PREF_SIZE);
region.setMaxWidth(USE_PREF_SIZE);
}
public static void usePrefHeight(Region region) {
region.setMinHeight(USE_PREF_SIZE);
region.setMaxHeight(USE_PREF_SIZE);
}
}

View file

@ -1,73 +0,0 @@
/* SPDX-License-Identifier: MIT */
package io.xpipe.app.util;
import javafx.scene.control.*;
import javafx.scene.input.KeyCombination;
import org.kordamp.ikonli.Ikon;
import org.kordamp.ikonli.javafx.FontIcon;
import java.net.URI;
import static atlantafx.base.theme.Styles.BUTTON_ICON;
public final class Controls {
public static Button iconButton(Ikon icon, boolean disable) {
return button("", icon, disable, BUTTON_ICON);
}
public static Button button(String text, Ikon icon, boolean disable, String... styleClasses) {
var button = new Button(text);
if (icon != null) {
button.setGraphic(new FontIcon(icon));
}
button.setDisable(disable);
button.getStyleClass().addAll(styleClasses);
return button;
}
public static MenuItem menuItem(String text, Ikon graphic, KeyCombination accelerator) {
return menuItem(text, graphic, accelerator, false);
}
public static MenuItem menuItem(String text, Ikon graphic, KeyCombination accelerator, boolean disable) {
var item = new MenuItem(text);
if (graphic != null) {
item.setGraphic(new FontIcon(graphic));
}
if (accelerator != null) {
item.setAccelerator(accelerator);
}
item.setDisable(disable);
return item;
}
public static ToggleButton toggleButton(String text,
Ikon icon,
ToggleGroup group,
boolean selected,
String... styleClasses) {
var toggleButton = new ToggleButton(text);
if (icon != null) {
toggleButton.setGraphic(new FontIcon(icon));
}
if (group != null) {
toggleButton.setToggleGroup(group);
}
toggleButton.setSelected(selected);
toggleButton.getStyleClass().addAll(styleClasses);
return toggleButton;
}
public static Hyperlink hyperlink(String text, URI uri) {
var hyperlink = new Hyperlink(text);
if (uri != null) {
hyperlink.setOnAction(event -> Hyperlinks.open(uri.toString()));
}
return hyperlink;
}
}

View file

@ -1,5 +1,3 @@
/* SPDX-License-Identifier: MIT */
package io.xpipe.app.util;
import java.text.CharacterIterator;

View file

@ -17,8 +17,11 @@ public class MacOsPermissions {
var state = new SimpleBooleanProperty(true);
try (var pc = LocalStore.getShell().start()) {
while (state.get()) {
var success = pc.executeSimpleBooleanCommand(
"osascript -e 'tell application \"System Events\" to keystroke \"t\"'");
var success = pc.osascriptCommand(
"""
tell application "System Events" to keystroke "t"
""")
.executeAndCheck();
if (success) {
Platform.runLater(() -> {
if (alert.get() != null) {

View file

@ -17,8 +17,13 @@ import java.util.Random;
public class ScriptHelper {
public static String createDetachCommand(ShellControl pc, String command) {
if (pc.getShellDialect().equals(ShellDialects.POWERSHELL)) {
var script = ScriptHelper.createExecScript(pc, command);
return String.format("Start-Process -WindowStyle Minimized -FilePath powershell.exe -ArgumentList \"-NoProfile\", \"-File\", %s", ShellDialects.POWERSHELL.fileArgument(script));
}
if (pc.getOsType().equals(OsType.WINDOWS)) {
return "start \"\" " + command;
return "start \"\" /MIN " + command;
} else {
return "nohup " + command + " </dev/null &>/dev/null & disown";
}

View file

@ -1,3 +1,4 @@
import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.core.AppLogs;
import io.xpipe.app.exchange.*;
import io.xpipe.app.exchange.api.*;
@ -33,6 +34,7 @@ open module io.xpipe.app {
exports io.xpipe.app.fxcomps.util;
exports io.xpipe.app.fxcomps.augment;
exports io.xpipe.app.test;
exports io.xpipe.app.browser.action;
exports io.xpipe.app.browser;
exports io.xpipe.app.browser.icon;
@ -81,6 +83,8 @@ open module io.xpipe.app {
requires java.management;
requires jdk.management;
requires jdk.management.agent;
requires com.jthemedetector;
requires versioncompare;
// Required by extensions
requires commons.math3;
@ -119,11 +123,13 @@ open module io.xpipe.app {
uses ProxyFunction;
uses ModuleLayerLoader;
uses ScanProvider;
uses BrowserAction;
provides ModuleLayerLoader with
DataSourceTarget.Loader,
ActionProvider.Loader,
PrefsProvider.Loader,
BrowserAction.Loader,
ScanProvider.Loader;
provides DataStateProvider with
DataStateProviderImpl;

View file

@ -9,6 +9,11 @@ setLock=Set lock
changeLock=Change lock
lockCreationAlertTitle=Create Lock
lockCreationAlertHeader=Set your new lock password
finish=Finish
error=Error
ok=Ok
newFile=New file
newDirectory=New directory
password=Password
unlockAlertTitle=Unlock workspace
unlockAlertHeader=Enter your lock password to continue

View file

@ -49,7 +49,22 @@
}
.browser .bookmark-list {
-fx-border-width: 0 0 1 1;
-fx-border-width: 0;
}
.browser .bookmark-list *.scroll-bar:horizontal,
.browser .bookmark-list *.scroll-bar:horizontal *.track,
.browser .bookmark-list *.scroll-bar:horizontal *.track-background,
.browser .bookmark-list *.scroll-bar:horizontal *.thumb,
.browser .bookmark-list *.scroll-bar:horizontal *.increment-button,
.browser .bookmark-list *.scroll-bar:horizontal *.decrement-button,
.browser .bookmark-list *.scroll-bar:horizontal *.increment-arrow,
.browser .bookmark-list *.scroll-bar:horizontal *.decrement-arrow {
-fx-background-color: null;
-fx-background-radius: 0;
-fx-background-insets: 0;
-fx-padding: 0;
-fx-shape: null;
}
.browser .tool-bar {
@ -58,7 +73,7 @@
}
.browser .status-bar {
-fx-border-width: 1 0 1 0;
-fx-border-width: 1 0 0 0;
-fx-border-color: -color-neutral-muted;
}
@ -66,6 +81,56 @@
-fx-padding: 0;
}
.browser .context-menu > * > * {
-fx-padding: 3px 10px 3px 10px;
-fx-background-radius: 1px;
-fx-spacing: 20px;
}
.browser .context-menu .separator {
-fx-padding: 0;
}
.browser .breadcrumbs {
-fx-padding: 2px 10px 2px 10px;
}
.browser .context-menu .separator .line {
-fx-padding: 0;
-fx-border-insets: 0px;
}
.browser .breadcrumbs .button {
-fx-padding: 3px 1px 3px 1px;
-fx-background-color: transparent;
}
.browser .breadcrumbs .button:hover {
-fx-background-color: -color-neutral-muted;
}
.browser .path-text:invisible {
-fx-text-fill: transparent;
}
.browser .context-menu .accelerator-text {
-fx-padding: 3px 0px 3px 50px;
}
.browser .context-menu > * {
-fx-padding: 0;
}
.browser .context-menu {
-fx-padding: 0;
-fx-background-radius: 1px;
-fx-border-color: -color-neutral-muted;
}
.browser .tab-pane {
-fx-border-width: 0 0 0 1px;
-fx-border-color: -color-neutral-emphasis;
}
.chooser-bar {
-fx-border-color: -color-neutral-emphasis;
-fx-border-width: 0.1em 0 0 0;
@ -83,7 +148,7 @@
visibility: hidden ;
}
.browser .table-directory-view .table-view {
.browser .table-view {
-color-header-bg: -color-bg-default;
-color-cell-bg-selected: -color-neutral-emphasis;
-color-cell-fg-selected: -color-fg-emphasis;
@ -96,7 +161,7 @@
-fx-opacity: 0.75;
}
.browser .table-directory-view .table-view:drag-into-current .table-row-cell {
.browser .table-view:drag-into-current .table-row-cell {
-fx-opacity: 0.8;
}

View file

@ -1,5 +1,5 @@
.step {
-fx-padding: 1em 1.5em 0.5em 1.5em;
-fx-padding: 0;
}
.spacer {

View file

@ -0,0 +1,5 @@
.error-overlay-comp {
-fx-padding: 1.0em;
-fx-border-width: 1px;
-fx-border-radius: 2px;
}

View file

@ -0,0 +1,18 @@
.modal-overlay-comp .titled-pane {
-fx-padding: 0;
-fx-border-radius: 0;
}
.modal-overlay-comp {
-fx-border-radius: 0;
-fx-background-radius: 0;
}
.modal-overlay-comp .titled-pane > * {
-fx-border-radius: 0;
}
.modal-overlay-comp .titled-pane > * {
-fx-background-radius: 0;
}

View file

@ -80,3 +80,7 @@
-fx-pref-height: 0;
}
.multi-step-comp > .jfx-tab-pane .tab-content-area {
-fx-padding: 0;
}

View file

@ -13,21 +13,6 @@
-fx-padding: 1.2em;
}
.message-comp {
-fx-background-color: #FF9999AA;
-fx-border-width: 1px;
-fx-border-color:-color-accent-fg;
-fx-border-radius: 2px;
}
.message {
-fx-padding: 0.0em;
-fx-background-color: transparent;
-fx-border-width: 1px;
-fx-border-color:-color-accent-fg;
-fx-border-radius: 2px;
}
.radio-button {
-fx-background-color:transparent;
}

View file

@ -1,10 +1,15 @@
package io.xpipe.core.impl;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class FileNames {
public static String quoteIfNecessary(String n) {
return n.contains(" ") ? "\"" + n + "\"" : n;
}
public static String toDirectory(String path) {
if (path.endsWith("/") || path.endsWith("\\")) {
return path;
@ -45,6 +50,39 @@ public class FileNames {
return components.get(components.size() - 1);
}
public static List<String> splitHierarchy(String file) {
if (file.isEmpty()) {
return List.of();
}
file = file + "/";
var list = new ArrayList<String>();
int lastElementStart = 0;
for (int i = 0; i < file.length(); i++) {
if (file.charAt(i) == '\\' || file.charAt(i) == '/') {
if (i - lastElementStart > 0) {
list.add(file.substring(0, i));
}
lastElementStart = i + 1;
}
}
return list;
}
public static String getBaseName(String file) {
if (file == null || file.isEmpty()) {
return null;
}
var name = FileNames.getFileName(file);
var split = file.lastIndexOf("\\.");
if (split == -1) {
return name;
}
return name.substring(0, split);
}
public static String getExtension(String file) {
if (file == null || file.isEmpty()) {
return null;
@ -68,7 +106,7 @@ public class FileNames {
return false;
}
if (!file.startsWith("/") && !file.startsWith("~") && !file.matches("^\\w:.*")) {
if (!file.startsWith("\\") && !file.startsWith("/") && !file.startsWith("~") && !file.matches("^\\w:.*")) {
return false;
}

View file

@ -1,10 +1,12 @@
package io.xpipe.core.process;
import io.xpipe.core.impl.FileNames;
import java.util.Locale;
import java.util.Map;
import java.util.stream.Collectors;
public interface OsType {
public sealed interface OsType permits OsType.Windows, OsType.Linux, OsType.MacOs {
Windows WINDOWS = new Windows();
Linux LINUX = new Linux();
@ -23,8 +25,18 @@ public interface OsType {
}
}
default String getXPipeHomeDirectory(ShellControl pc) throws Exception {
return FileNames.join(getHomeDirectory(pc), ".xpipe");
}
default String getSystemIdFile(ShellControl pc) throws Exception {
return FileNames.join(getXPipeHomeDirectory(pc), "system_id");
}
String getHomeDirectory(ShellControl pc) throws Exception;
String getFileSystemSeparator();
String getName();
String getTempDirectory(ShellControl pc) throws Exception;
@ -33,7 +45,7 @@ public interface OsType {
String determineOperatingSystemName(ShellControl pc) throws Exception;
static class Windows implements OsType {
static final class Windows implements OsType {
@Override
public String getHomeDirectory(ShellControl pc) throws Exception {
@ -41,6 +53,11 @@ public interface OsType {
pc.getShellDialect().getPrintEnvironmentVariableCommand("USERPROFILE"));
}
@Override
public String getFileSystemSeparator() {
return "\\";
}
@Override
public String getName() {
return "Windows";
@ -80,13 +97,18 @@ public interface OsType {
}
}
static class Linux implements OsType {
static final class Linux implements OsType {
@Override
public String getHomeDirectory(ShellControl pc) throws Exception {
return pc.executeSimpleStringCommand(pc.getShellDialect().getPrintEnvironmentVariableCommand("HOME"));
}
@Override
public String getFileSystemSeparator() {
return "/";
}
@Override
public String getTempDirectory(ShellControl pc) throws Exception {
return "/tmp/";
@ -138,7 +160,7 @@ public interface OsType {
}
}
static class MacOs implements OsType {
static final class MacOs implements OsType {
@Override
public String getHomeDirectory(ShellControl pc) throws Exception {
@ -157,6 +179,11 @@ public interface OsType {
return found;
}
@Override
public String getFileSystemSeparator() {
return "/";
}
@Override
public String getName() {
return "Mac";

View file

@ -2,17 +2,25 @@ package io.xpipe.core.process;
import io.xpipe.core.util.FailableFunction;
import io.xpipe.core.util.SecretValue;
import io.xpipe.core.util.XPipeSystemId;
import lombok.NonNull;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.Semaphore;
import java.util.function.Consumer;
import java.util.function.Function;
public interface ShellControl extends ProcessControl {
default boolean isLocal() {
return getSystemId().equals(XPipeSystemId.getLocal());
}
UUID getSystemId();
Semaphore getCommandLock();
ShellControl onInit(Consumer<ShellControl> pc);
@ -29,6 +37,15 @@ public interface ShellControl extends ProcessControl {
public void checkRunning() throws Exception;
default CommandControl osascriptCommand(String script) {
return command(String.format(
"""
osascript - "$@" <<EOF
%s
EOF
""", script));
}
default String executeSimpleStringCommand(String command) throws Exception {
try (CommandControl c = command(command).start()) {
return c.readOrThrow();
@ -63,8 +80,6 @@ public interface ShellControl extends ProcessControl {
void restart() throws Exception;
boolean isLocal();
OsType getOsType();
ShellControl elevated(FailableFunction<ShellControl, Boolean, Exception> elevationFunction);
@ -81,16 +96,17 @@ public interface ShellControl extends ProcessControl {
default ShellControl subShell(@NonNull ShellDialect type) {
return subShell(p -> type.getOpenCommand(), new TerminalOpenFunction() {
@Override
public boolean changesEnvironment() {
return false;
}
@Override
public boolean changesEnvironment() {
return false;
}
@Override
public String prepare(ShellControl sc, String command) throws Exception {
return command;
}
}).elevationPassword(getElevationPassword());
@Override
public String prepare(ShellControl sc, String command) throws Exception {
return command;
}
})
.elevationPassword(getElevationPassword());
}
interface TerminalOpenFunction {
@ -102,16 +118,16 @@ public interface ShellControl extends ProcessControl {
default ShellControl identicalSubShell() {
return subShell(p -> p.getShellDialect().getOpenCommand(), new TerminalOpenFunction() {
@Override
public boolean changesEnvironment() {
return false;
}
@Override
public boolean changesEnvironment() {
return false;
}
@Override
public String prepare(ShellControl sc, String command) throws Exception {
return command;
}
})
@Override
public String prepare(ShellControl sc, String command) throws Exception {
return command;
}
})
.elevationPassword(getElevationPassword());
}
@ -129,8 +145,17 @@ public interface ShellControl extends ProcessControl {
});
}
default ShellControl enforcedDialect(ShellDialect type) throws Exception {
start();
if (getShellDialect().equals(type)) {
return this;
} else {
return subShell(type).start();
}
}
default <T> T enforceDialect(@NonNull ShellDialect type, Function<ShellControl, T> sc) throws Exception {
if (isRunning() && getShellDialect().equals(type)) {
if (isRunning() && getShellDialect().equals(type)) {
return sc.apply(this);
} else {
try (var sub = subShell(type).start()) {
@ -140,8 +165,7 @@ public interface ShellControl extends ProcessControl {
}
ShellControl subShell(
FailableFunction<ShellControl, String, Exception> command,
TerminalOpenFunction terminalCommand);
FailableFunction<ShellControl, String, Exception> command, TerminalOpenFunction terminalCommand);
void executeLine(String command) throws Exception;

View file

@ -0,0 +1,44 @@
package io.xpipe.core.util;
import io.xpipe.core.impl.FileNames;
import io.xpipe.core.process.ShellControl;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.UUID;
public class XPipeSystemId {
private static UUID localId;
public static void init() {
try {
var file = Path.of(System.getProperty("user.home")).resolve(".xpipe").resolve("system_id");
if (!Files.exists(file)) {
Files.writeString(file, UUID.randomUUID().toString());
}
localId = UUID.fromString(Files.readString(file).trim());
} catch (Exception ex) {
localId = UUID.randomUUID();
}
}
public static UUID getLocal() {
return localId;
}
public static UUID getSystemId(ShellControl proc) throws Exception {
var file = proc.getOsType().getSystemIdFile(proc);
if (!proc.getShellDialect().createFileExistsCommand(proc, file).executeAndCheck()) {
proc.executeSimpleCommand(
proc.getShellDialect().getMkdirsCommand(FileNames.getParent(file)),
"Unable to access or create directory " + file);
var id = UUID.randomUUID();
proc.getShellDialect().createTextFileWriteCommand(proc, id.toString(), file).execute();
return id;
}
return UUID.fromString(proc.executeSimpleStringCommand(proc.getShellDialect().getFileReadCommand(file)).trim());
}
}

6
dist/changelogs/1.0.0.md vendored Normal file
View file

@ -0,0 +1,6 @@
## Changes in 1.0.0
- Completely revamp file browser
- Add more appearance themes to choose from
- Add arm64 support for homebrew release
- A lot of bug fixes

177
dist/licenses/java-annotations.license vendored Normal file
View file

@ -0,0 +1,177 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

View file

@ -0,0 +1,4 @@
name=JetBrains Annotations for JVM-based languages
version=24.0.1
license=Apache License 2.0
link=https://github.com/JetBrains/java-annotations

177
dist/licenses/jfa.license vendored Normal file
View file

@ -0,0 +1,177 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

4
dist/licenses/jfa.properties vendored Normal file
View file

@ -0,0 +1,4 @@
name=Java Foundation Access
version=1.2.0
license=Apache License 2.0
link=https://github.com/0x4a616e/jfa

View file

@ -0,0 +1,177 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

View file

@ -0,0 +1,4 @@
name=jSystemThemeDetector
version=3.8
license=Apache License 2.0
link=https://github.com/Dansoftowner/jSystemThemeDetector

21
dist/licenses/oshi.license vendored Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2010-2023 The OSHI Project Contributors: https://github.com/oshi/oshi/graphs/contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

4
dist/licenses/oshi.properties vendored Normal file
View file

@ -0,0 +1,4 @@
name=oshi
version=6.4.2
license=MIT License
link=https://github.com/oshi/oshi

View file

@ -8,6 +8,12 @@ apply from: "$rootDir/gradle/gradle_scripts/commons.gradle"
apply from: "$rootDir/gradle/gradle_scripts/lombok.gradle"
apply from: "$rootDir/gradle/gradle_scripts/extension.gradle"
dependencies {
compileOnly group: 'org.kordamp.ikonli', name: 'ikonli-javafx', version: "12.2.0"
compileOnly 'net.java.dev.jna:jna-jpms:5.12.1'
compileOnly 'net.java.dev.jna:jna-platform-jpms:5.12.1'
}
compileJava {
doFirst {
options.compilerArgs += [

View file

@ -0,0 +1,66 @@
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.process.OsType;
import io.xpipe.core.process.ShellControl;
import io.xpipe.core.process.ShellDialect;
import java.util.List;
public class BrowseInNativeManagerAction implements LeafAction {
@Override
public void execute(OpenFileSystemModel model, List<BrowserEntry> entries) throws Exception {
ShellControl sc = model.getFileSystem().getShell().get();
ShellDialect d = sc.getShellDialect();
for (BrowserEntry entry : entries) {
var e = entry.getRawFileEntry().getPath();
switch (OsType.getLocal()) {
case OsType.Windows windows -> {
if (entry.getRawFileEntry().isDirectory()) {
sc.executeSimpleCommand("explorer " + d.fileArgument(e));
} else {
sc.executeSimpleCommand("explorer /select," + d.fileArgument(e));
}
}
case OsType.Linux linux -> {
var action = entry.getRawFileEntry().isDirectory() ? "org.freedesktop.FileManager1.ShowFolders" : "org.freedesktop.FileManager1.ShowItems";
var dbus = String.format("""
dbus-send --session --print-reply --dest=org.freedesktop.FileManager1 --type=method_call /org/freedesktop/FileManager1 %s array:string:"file://%s" string:""
""", action, entry.getRawFileEntry().getPath());
sc.executeSimpleCommand(dbus);
}
case OsType.MacOs macOs -> {
sc.executeSimpleCommand("open " + (entry.getRawFileEntry().isDirectory() ? "" : "-R ")
+ d.fileArgument(entry.getRawFileEntry().getPath()));
}
}
}
}
@Override
public Category getCategory() {
return Category.NATIVE;
}
@Override
public boolean acceptsEmptySelection() {
return true;
}
@Override
public boolean isApplicable(OpenFileSystemModel model, List<BrowserEntry> entries) {
return model.isLocal();
}
@Override
public String getName(OpenFileSystemModel model, List<BrowserEntry> entries) {
return switch (OsType.getLocal()) {
case OsType.Windows windows -> "Browse in Windows Explorer";
case OsType.Linux linux -> "Browse in default file manager";
case OsType.MacOs macOs -> "Browse in Finder";
};
}
}

View file

@ -0,0 +1,47 @@
package io.xpipe.ext.base.browser;
import io.xpipe.app.browser.BrowserClipboard;
import io.xpipe.app.browser.BrowserEntry;
import io.xpipe.app.browser.OpenFileSystemModel;
import io.xpipe.app.browser.action.LeafAction;
import javafx.scene.Node;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
public class CopyAction implements LeafAction {
@Override
public void execute(OpenFileSystemModel model, List<BrowserEntry> entries) throws Exception {
BrowserClipboard.startCopy(
model.getCurrentDirectory(), entries.stream().map(entry -> entry.getRawFileEntry()).toList());
}
@Override
public boolean acceptsEmptySelection() {
return true;
}
@Override
public Node getIcon(OpenFileSystemModel model, List<BrowserEntry> entries) {
return new FontIcon("mdi2c-content-copy");
}
@Override
public Category getCategory() {
return Category.COPY_PASTE;
}
@Override
public KeyCombination getShortcut() {
return new KeyCodeCombination(KeyCode.C, KeyCombination.SHORTCUT_DOWN);
}
@Override
public String getName(OpenFileSystemModel model, List<BrowserEntry> entries) {
return "Copy";
}
}

View file

@ -0,0 +1,113 @@
package io.xpipe.ext.base.browser;
import io.xpipe.app.browser.BrowserEntry;
import io.xpipe.app.browser.OpenFileSystemModel;
import io.xpipe.app.browser.action.BranchAction;
import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.browser.action.LeafAction;
import io.xpipe.core.impl.FileNames;
import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
import java.util.List;
import java.util.stream.Collectors;
public class CopyPathAction implements BrowserAction, BranchAction {
@Override
public String getName(OpenFileSystemModel model, List<BrowserEntry> entries) {
return "Copy location";
}
@Override
public Category getCategory() {
return Category.COPY_PASTE;
}
@Override
public boolean acceptsEmptySelection() {
return true;
}
@Override
public List<LeafAction> getBranchingActions() {
return List.of(
new LeafAction() {
@Override
public String getName(OpenFileSystemModel model, List<BrowserEntry> entries) {
return "Absolute Path";
}
@Override
public void execute(OpenFileSystemModel model, List<BrowserEntry> entries) throws Exception {
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) {
return "Absolute Path (Quoted)";
}
@Override
public boolean isApplicable(OpenFileSystemModel model, List<BrowserEntry> entries) {
return entries.stream().anyMatch(entry -> entry.getRawFileEntry().getPath().contains(" "));
}
@Override
public void execute(OpenFileSystemModel model, List<BrowserEntry> entries) throws Exception {
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) {
return "File Name";
}
@Override
public void execute(OpenFileSystemModel model, List<BrowserEntry> entries) throws Exception {
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) {
return "File Name (Quoted)";
}
@Override
public boolean isApplicable(OpenFileSystemModel model, List<BrowserEntry> entries) {
return entries.stream().anyMatch(entry -> FileNames.getFileName(entry.getRawFileEntry().getPath()).contains(" "));
}
@Override
public void execute(OpenFileSystemModel model, List<BrowserEntry> entries) throws Exception {
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);
}
});
}
}

View file

@ -0,0 +1,48 @@
package io.xpipe.ext.base.browser;
import io.xpipe.app.browser.BrowserAlerts;
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 javafx.scene.Node;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
public class DeleteAction implements LeafAction {
@Override
public void execute(OpenFileSystemModel model, List<BrowserEntry> entries) throws Exception {
var toDelete = entries.stream().map(entry -> entry.getRawFileEntry()).toList();
if (!BrowserAlerts.showDeleteAlert(toDelete)) {
return;
}
FileSystemHelper.delete(toDelete);
model.refreshSync();
}
@Override
public Category getCategory() {
return Category.MUTATION;
}
@Override
public Node getIcon(OpenFileSystemModel model, List<BrowserEntry> entries) {
return new FontIcon("mdi2d-delete");
}
@Override
public KeyCombination getShortcut() {
return new KeyCodeCombination(KeyCode.DELETE);
}
@Override
public String getName(OpenFileSystemModel model, List<BrowserEntry> entries) {
return "Delete";
}
}

Some files were not shown because too many files have changed in this diff Show more