mirror of
https://github.com/xpipe-io/xpipe.git
synced 2024-11-22 07:30:24 +00:00
Add changelog
This commit is contained in:
parent
8038e88b28
commit
19f4b0abc4
122 changed files with 4263 additions and 1589 deletions
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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())) {
|
155
app/src/main/java/io/xpipe/app/browser/BrowserBookmarkList.java
Normal file
155
app/src/main/java/io/xpipe/app/browser/BrowserBookmarkList.java
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 {
|
|
@ -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(() -> {
|
||||
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());
|
||||
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());
|
||||
})
|
||||
.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);
|
||||
}
|
||||
|
||||
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())
|
||||
var name = DataStorage.get().getStoreEntry(model.getStore()).getName();
|
||||
var image = DataStorage.get()
|
||||
.getStoreEntry(model.getStore())
|
||||
.getProvider()
|
||||
.getDisplayIconFileName(model.getStore().getValue())
|
||||
: null;
|
||||
},
|
||||
model.getStore());
|
||||
var logo = new PrettyImageComp(image, 20, 20).createRegion();
|
||||
.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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
67
app/src/main/java/io/xpipe/app/browser/BrowserEntry.java
Normal file
67
app/src/main/java/io/xpipe/app/browser/BrowserEntry.java
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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,101 +54,103 @@ 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.setFixedCellSize(34.0);
|
||||
|
||||
prepareTableSelectionModel(table);
|
||||
prepareTableShortcuts(table);
|
||||
prepareTableEntries(table);
|
||||
prepareTableChanges(table, mtimeCol, modeCol);
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
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 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()
|
||||
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()))
|
||||
.toList();
|
||||
fileList.getSelected().setAll(toSelect);
|
||||
fileList.getFileSystemModel()
|
||||
.getBrowserModel()
|
||||
.getSelectedFiles()
|
||||
.setAll(toSelect);
|
||||
.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()
|
||||
|
@ -157,35 +161,46 @@ final class FileListComp extends AnchorPane {
|
|||
});
|
||||
});
|
||||
|
||||
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();
|
||||
fileList.getSelection().addListener((ListChangeListener<? super BrowserEntry>) c -> {
|
||||
if (c.getList().equals(table.getSelectionModel().getSelectedItems())) {
|
||||
return;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
prepareTableEntries(table);
|
||||
prepareTableChanges(table, mtimeCol, modeCol);
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
private void prepareTableEntries(TableView<FileSystem.FileEntry> table) {
|
||||
var emptyEntry = new FileListCompEntry(table, null, fileList);
|
||||
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) {
|
|
@ -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)) {
|
131
app/src/main/java/io/xpipe/app/browser/BrowserFileListModel.java
Normal file
131
app/src/main/java/io/xpipe/app/browser/BrowserFileListModel.java
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -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<>();
|
141
app/src/main/java/io/xpipe/app/browser/BrowserModel.java
Normal file
141
app/src/main/java/io/xpipe/app/browser/BrowserModel.java
Normal 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
57
app/src/main/java/io/xpipe/app/browser/BrowserNavBar.java
Normal file
57
app/src/main/java/io/xpipe/app/browser/BrowserNavBar.java
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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);
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
|
@ -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");
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package io.xpipe.app.browser.action;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface BranchAction extends BrowserAction {
|
||||
|
||||
List<LeafAction> getBranchingActions();
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
101
app/src/main/java/io/xpipe/app/browser/icon/DirectoryType.java
Normal file
101
app/src/main/java/io/xpipe/app/browser/icon/DirectoryType.java
Normal 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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
87
app/src/main/java/io/xpipe/app/browser/icon/FileType.java
Normal file
87
app/src/main/java/io/xpipe/app/browser/icon/FileType.java
Normal 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();
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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))) {
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
107
app/src/main/java/io/xpipe/app/comp/base/ModalOverlayComp.java
Normal file
107
app/src/main/java/io/xpipe/app/comp/base/ModalOverlayComp.java
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
142
app/src/main/java/io/xpipe/app/core/AppTheme.java
Normal file
142
app/src/main/java/io/xpipe/app/core/AppTheme.java
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))),
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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(
|
||||
pc.osascriptCommand(String.format(
|
||||
"""
|
||||
osascript - "$@" <<EOF
|
||||
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(
|
||||
pc.osascriptCommand(String.format(
|
||||
"""
|
||||
osascript - "$@" <<EOF
|
||||
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,9 +272,8 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
}
|
||||
|
||||
try (ShellControl pc = LocalStore.getShell()) {
|
||||
var cmd = String.format(
|
||||
pc.osascriptCommand(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
|
||||
|
@ -285,16 +283,15 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
key code 36
|
||||
end tell
|
||||
end tell
|
||||
EOF
|
||||
""",
|
||||
file.replaceAll("\"", "\\\\\""));
|
||||
pc.executeSimpleCommand(cmd);
|
||||
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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,3 @@
|
|||
/* SPDX-License-Identifier: MIT */
|
||||
|
||||
package io.xpipe.app.util;
|
||||
|
||||
import java.text.CharacterIterator;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
.step {
|
||||
-fx-padding: 1em 1.5em 0.5em 1.5em;
|
||||
-fx-padding: 0;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
.error-overlay-comp {
|
||||
-fx-padding: 1.0em;
|
||||
-fx-border-width: 1px;
|
||||
-fx-border-radius: 2px;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -80,3 +80,7 @@
|
|||
-fx-pref-height: 0;
|
||||
}
|
||||
|
||||
.multi-step-comp > .jfx-tab-pane .tab-content-area {
|
||||
-fx-padding: 0;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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);
|
||||
|
@ -90,7 +105,8 @@ public interface ShellControl extends ProcessControl {
|
|||
public String prepare(ShellControl sc, String command) throws Exception {
|
||||
return command;
|
||||
}
|
||||
}).elevationPassword(getElevationPassword());
|
||||
})
|
||||
.elevationPassword(getElevationPassword());
|
||||
}
|
||||
|
||||
interface TerminalOpenFunction {
|
||||
|
@ -129,6 +145,15 @@ 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)) {
|
||||
return sc.apply(this);
|
||||
|
@ -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;
|
||||
|
||||
|
|
44
core/src/main/java/io/xpipe/core/util/XPipeSystemId.java
Normal file
44
core/src/main/java/io/xpipe/core/util/XPipeSystemId.java
Normal 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
6
dist/changelogs/1.0.0.md
vendored
Normal 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
177
dist/licenses/java-annotations.license
vendored
Normal 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/java-annotations.properties
vendored
Normal file
4
dist/licenses/java-annotations.properties
vendored
Normal 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
177
dist/licenses/jfa.license
vendored
Normal 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
4
dist/licenses/jfa.properties
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
name=Java Foundation Access
|
||||
version=1.2.0
|
||||
license=Apache License 2.0
|
||||
link=https://github.com/0x4a616e/jfa
|
177
dist/licenses/jsystemthemedetector.license
vendored
Normal file
177
dist/licenses/jsystemthemedetector.license
vendored
Normal 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/jsystemthemedetector.properties
vendored
Normal file
4
dist/licenses/jsystemthemedetector.properties
vendored
Normal 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
21
dist/licenses/oshi.license
vendored
Normal 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
4
dist/licenses/oshi.properties
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
name=oshi
|
||||
version=6.4.2
|
||||
license=MIT License
|
||||
link=https://github.com/oshi/oshi
|
|
@ -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 += [
|
||||
|
|
|
@ -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";
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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
Loading…
Reference in a new issue