diff --git a/app/build.gradle b/app/build.gradle index b6ceb5f3..6493dae3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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" diff --git a/app/src/main/java/io/xpipe/app/browser/BookmarkList.java b/app/src/main/java/io/xpipe/app/browser/BookmarkList.java deleted file mode 100644 index ed2c6af6..00000000 --- a/app/src/main/java/io/xpipe/app/browser/BookmarkList.java +++ /dev/null @@ -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); - } -} diff --git a/app/src/main/java/io/xpipe/app/browser/FileBrowserAlerts.java b/app/src/main/java/io/xpipe/app/browser/BrowserAlerts.java similarity index 98% rename from app/src/main/java/io/xpipe/app/browser/FileBrowserAlerts.java rename to app/src/main/java/io/xpipe/app/browser/BrowserAlerts.java index f28fd3fa..7d9c33b1 100644 --- a/app/src/main/java/io/xpipe/app/browser/FileBrowserAlerts.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserAlerts.java @@ -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 source, FileSystem.FileEntry target) { if (source.stream().noneMatch(entry -> entry.isDirectory())) { diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserBookmarkList.java b/app/src/main/java/io/xpipe/app/browser/BrowserBookmarkList.java new file mode 100644 index 00000000..80b5f9d1 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/BrowserBookmarkList.java @@ -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(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 getTreeViewItem( + TreeItem item, StoreEntryWrapper value) { + if (item.getValue() != null && item.getValue().equals(value)) { + return item; + } + + for (TreeItem child : item.getChildren()) { + TreeItem s = getTreeViewItem(child, value); + if (s != null) { + return s; + } + } + return null; + } + + private final class StoreCell extends TreeCell { + + 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); + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserBreadcrumbBar.java b/app/src/main/java/io/xpipe/app/browser/BrowserBreadcrumbBar.java new file mode 100644 index 00000000..9193a4d2 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/BrowserBreadcrumbBar.java @@ -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, 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, ButtonBase> crumbFactory, + Callback, ? extends Node> dividerFactory) { + + var breadcrumbs = new Breadcrumbs(); + 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 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; + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/FileBrowserClipboard.java b/app/src/main/java/io/xpipe/app/browser/BrowserClipboard.java similarity index 98% rename from app/src/main/java/io/xpipe/app/browser/FileBrowserClipboard.java rename to app/src/main/java/io/xpipe/app/browser/BrowserClipboard.java index 7a705970..85f6ca4b 100644 --- a/app/src/main/java/io/xpipe/app/browser/FileBrowserClipboard.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserClipboard.java @@ -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 { diff --git a/app/src/main/java/io/xpipe/app/browser/FileBrowserComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserComp.java similarity index 70% rename from app/src/main/java/io/xpipe/app/browser/FileBrowserComp.java rename to app/src/main/java/io/xpipe/app/browser/BrowserComp.java index dc72b1a8..a4723c50 100644 --- a/app/src/main/java/io/xpipe/app/browser/FileBrowserComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserComp.java @@ -3,7 +3,9 @@ package io.xpipe.app.browser; import atlantafx.base.controls.RingProgressIndicator; import atlantafx.base.controls.Spacer; import atlantafx.base.theme.Styles; +import io.xpipe.app.browser.icon.DirectoryType; import io.xpipe.app.browser.icon.FileIconManager; +import io.xpipe.app.browser.icon.FileType; import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.augment.GrowAugment; @@ -12,10 +14,10 @@ import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.util.BusyProperty; import io.xpipe.app.util.ThreadHelper; -import io.xpipe.core.store.FileSystem; import javafx.application.Platform; import javafx.beans.binding.Bindings; import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleStringProperty; import javafx.collections.ListChangeListener; import javafx.event.EventHandler; import javafx.geometry.Insets; @@ -32,29 +34,44 @@ import static atlantafx.base.theme.Styles.DENSE; import static atlantafx.base.theme.Styles.toggleStyleClass; import static javafx.scene.control.TabPane.TabClosingPolicy.ALL_TABS; -public class FileBrowserComp extends SimpleComp { +public class BrowserComp extends SimpleComp { - private final FileBrowserModel model; + private final BrowserModel model; - public FileBrowserComp(FileBrowserModel model) { + public BrowserComp(BrowserModel model) { this.model = model; } @Override protected Region createSimple() { - ThreadHelper.runAsync( () -> { + FileType.loadDefinitions(); + DirectoryType.loadDefinitions(); + ThreadHelper.runAsync(() -> { FileIconManager.loadIfNecessary(); }); - var bookmarksList = new BookmarkList(model).createRegion(); + var bookmarksList = new BrowserBookmarkList(model).createRegion(); VBox.setVgrow(bookmarksList, Priority.ALWAYS); - var localDownloadStage = new LocalFileTransferComp(model.getLocalTransfersStage()).hide(Bindings.createBooleanBinding(() -> { - if (model.getOpenFileSystems().size() == 0) { - return true; - } + var localDownloadStage = new BrowserTransferComp(model.getLocalTransfersStage()) + .hide(PlatformThread.sync(Bindings.createBooleanBinding( + () -> { + if (model.getOpenFileSystems().size() == 0) { + return true; + } - return !model.getMode().equals(FileBrowserModel.Mode.BROWSER); - }, PlatformThread.sync(model.getOpenFileSystems()))).createRegion(); + if (model.getMode().isChooser()) { + return true; + } + + if (model.getSelected().getValue() != null) { + return model.getSelected().getValue().isLocal(); + } + + return false; + }, + model.getOpenFileSystems(), + model.getSelected()))) + .createRegion(); var vertical = new VBox(bookmarksList, localDownloadStage); vertical.setFillWidth(true); @@ -63,13 +80,16 @@ public class FileBrowserComp extends SimpleComp { .widthProperty() .addListener( // set sidebar width in pixels depending on split pane width - (obs, old, val) -> splitPane.setDividerPosition(0, 230 / splitPane.getWidth())); + (obs, old, val) -> splitPane.setDividerPosition(0, 280 / splitPane.getWidth())); - return addBottomBar(splitPane); + var r = addBottomBar(splitPane); + r.getStyleClass().add("browser"); + // AppFont.small(r); + return r; } private Region addBottomBar(Region r) { - if (model.getMode().equals(FileBrowserModel.Mode.BROWSER)) { + if (!model.getMode().isChooser()) { return r; } @@ -78,13 +98,18 @@ public class FileBrowserComp extends SimpleComp { var selected = new HBox(); selected.setAlignment(Pos.CENTER_LEFT); selected.setSpacing(10); - model.getSelectedFiles().addListener((ListChangeListener) c -> { - selected.getChildren().setAll(c.getList().stream().map(s -> { - var field = new TextField(s.getPath()); - field.setEditable(false); - field.setPrefWidth(400); - return field; - }).toList()); + model.getSelection().addListener((ListChangeListener) c -> { + PlatformThread.runLaterIfNeeded(() -> { + selected.getChildren() + .setAll(c.getList().stream() + .map(s -> { + var field = new TextField(s.getRawFileEntry().getPath()); + field.setEditable(false); + field.setPrefWidth(400); + return field; + }) + .toList()); + }); }); var spacer = new Spacer(Orientation.HORIZONTAL); var button = new Button("Select"); @@ -114,7 +139,8 @@ public class FileBrowserComp extends SimpleComp { map.put(v, t); tabs.getTabs().add(t); }); - tabs.getSelectionModel().select(model.getOpenFileSystems().indexOf(model.getSelected().getValue())); + tabs.getSelectionModel() + .select(model.getOpenFileSystems().indexOf(model.getSelected().getValue())); // Used for ignoring changes by the tabpane when new tabs are added. We want to perform the selections manually! var modifying = new SimpleBooleanProperty(); @@ -177,12 +203,10 @@ public class FileBrowserComp extends SimpleComp { continue; } - model.closeFileSystem(source.getKey()); + model.closeFileSystemAsync(source.getKey()); } } }); - - stack.getStyleClass().add("browser"); return stack; } @@ -190,16 +214,9 @@ public class FileBrowserComp extends SimpleComp { var tabs = new TabPane(); tabs.setTabDragPolicy(TabPane.TabDragPolicy.REORDER); tabs.setTabMinWidth(Region.USE_COMPUTED_SIZE); - - if (!model.getMode().equals(FileBrowserModel.Mode.BROWSER)) { - tabs.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE); - tabs.getStyleClass().add("singular"); - } else { - tabs.setTabClosingPolicy(ALL_TABS); - Styles.toggleStyleClass(tabs, TabPane.STYLE_CLASS_FLOATING); - toggleStyleClass(tabs, DENSE); - } - + tabs.setTabClosingPolicy(ALL_TABS); + Styles.toggleStyleClass(tabs, TabPane.STYLE_CLASS_FLOATING); + toggleStyleClass(tabs, DENSE); return tabs; } @@ -214,29 +231,14 @@ public class FileBrowserComp extends SimpleComp { .bind(Bindings.createDoubleBinding( () -> model.getBusy().get() ? -1d : 0, PlatformThread.sync(model.getBusy()))); - var name = Bindings.createStringBinding( - () -> { - return model.getStore().getValue() != null - ? DataStorage.get() - .getStoreEntry(model.getStore().getValue()) - .getName() - : null; - }, - PlatformThread.sync(model.getStore())); - var image = Bindings.createStringBinding( - () -> { - return model.getStore().getValue() != null - ? DataStorage.get() - .getStoreEntry(model.getStore().getValue()) - .getProvider() - .getDisplayIconFileName(model.getStore().getValue()) - : null; - }, - model.getStore()); - var logo = new PrettyImageComp(image, 20, 20).createRegion(); + var name = DataStorage.get().getStoreEntry(model.getStore()).getName(); + var image = DataStorage.get() + .getStoreEntry(model.getStore()) + .getProvider() + .getDisplayIconFileName(model.getStore()); + var logo = new PrettyImageComp(new SimpleStringProperty(image), 20, 20).createRegion(); - var label = new Label(); - label.textProperty().bind(name); + var label = new Label(name); label.addEventHandler(DragEvent.DRAG_ENTERED, new EventHandler() { @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; } diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserContextMenu.java b/app/src/main/java/io/xpipe/app/browser/BrowserContextMenu.java new file mode 100644 index 00000000..756c70e5 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/BrowserContextMenu.java @@ -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); + } + } + } + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserEntry.java b/app/src/main/java/io/xpipe/app/browser/BrowserEntry.java new file mode 100644 index 00000000..680022cf --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/BrowserEntry.java @@ -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); + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/FileListComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserFileListComp.java similarity index 63% rename from app/src/main/java/io/xpipe/app/browser/FileListComp.java rename to app/src/main/java/io/xpipe/app/browser/BrowserFileListComp.java index 5a81cc63..0bd6cddc 100644 --- a/app/src/main/java/io/xpipe/app/browser/FileListComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserFileListComp.java @@ -1,17 +1,18 @@ -/* SPDX-License-Identifier: MIT */ - package io.xpipe.app.browser; import atlantafx.base.theme.Styles; -import atlantafx.base.theme.Tweaks; +import io.xpipe.app.browser.action.BrowserAction; import io.xpipe.app.browser.icon.FileIconManager; import io.xpipe.app.comp.base.LazyTextFieldComp; +import io.xpipe.app.fxcomps.SimpleComp; +import io.xpipe.app.fxcomps.SimpleCompStructure; +import io.xpipe.app.fxcomps.augment.ContextMenuAugment; import io.xpipe.app.fxcomps.impl.SvgCacheComp; import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.SimpleChangeListener; import io.xpipe.app.util.BusyProperty; -import io.xpipe.app.util.Containers; import io.xpipe.app.util.HumanReadableFormat; +import io.xpipe.app.util.ThreadHelper; import io.xpipe.core.impl.FileNames; import io.xpipe.core.process.OsType; import io.xpipe.core.store.FileSystem; @@ -23,15 +24,16 @@ import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.css.PseudoClass; import javafx.geometry.Bounds; -import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.control.*; import javafx.scene.control.skin.TableViewSkin; import javafx.scene.control.skin.VirtualFlow; import javafx.scene.input.DragEvent; -import javafx.scene.input.KeyCode; -import javafx.scene.layout.*; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; import java.time.Instant; import java.time.ZoneId; @@ -42,7 +44,7 @@ import java.util.Objects; import static io.xpipe.app.util.HumanReadableFormat.byteCount; import static javafx.scene.control.TableColumn.SortType.ASCENDING; -final class FileListComp extends AnchorPane { +final class BrowserFileListComp extends SimpleComp { private static final PseudoClass HIDDEN = PseudoClass.getPseudoClass("hidden"); private static final PseudoClass EMPTY = PseudoClass.getPseudoClass("empty"); @@ -52,140 +54,153 @@ final class FileListComp extends AnchorPane { private static final PseudoClass DRAG_OVER = PseudoClass.getPseudoClass("drag-over"); private static final PseudoClass DRAG_INTO_CURRENT = PseudoClass.getPseudoClass("drag-into-current"); - private final FileListModel fileList; + private final BrowserFileListModel fileList; - public FileListComp(FileListModel fileList) { + public BrowserFileListComp(BrowserFileListModel fileList) { this.fileList = fileList; - TableView table = createTable(); + } + + @Override + protected Region createSimple() { + TableView 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 createTable() { - var filenameCol = new TableColumn("Name"); + private TableView createTable() { + var filenameCol = new TableColumn("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("Size"); - sizeCol.setCellValueFactory( - param -> new SimpleLongProperty(param.getValue().getSize())); + var sizeCol = new TableColumn("Size"); + sizeCol.setCellValueFactory(param -> + new SimpleLongProperty(param.getValue().getRawFileEntry().getSize())); sizeCol.setCellFactory(col -> new FileSizeCell()); - var mtimeCol = new TableColumn("Modified"); - mtimeCol.setCellValueFactory( - param -> new SimpleObjectProperty<>(param.getValue().getDate())); + var mtimeCol = new TableColumn("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("Attributes"); - modeCol.setCellValueFactory( - param -> new SimpleObjectProperty<>(param.getValue().getMode())); + var modeCol = new TableColumn("Attributes"); + modeCol.setCellValueFactory(param -> + new SimpleObjectProperty<>(param.getValue().getRawFileEntry().getMode())); modeCol.setCellFactory(col -> new FileModeCell()); - var table = new TableView(); + var table = new TableView(); 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() { - @Override - public int compare(FileSystem.FileEntry o1, FileSystem.FileEntry o2) { - var c = fileList.getFileSystemModel().getCurrentParentDirectory(); - if (c == null) { - return 0; - } + var syntheticFirst = Comparator.comparing(path -> !path.isSynthetic()); + var dirsFirst = Comparator.comparing( + path -> !path.getRawFileEntry().isDirectory()); - return o1.getPath().equals(c.getPath()) ? -1 : (o2.getPath().equals(c.getPath()) ? 1 : 0); - } - }; - var dirsFirst = Comparator.comparing(path -> !path.isDirectory()); - - Comparator us = - parentFirst.thenComparing(dirsFirst).thenComparing(comp); - FXCollections.sort(table.getItems(), us); + Comparator us = + syntheticFirst.thenComparing(dirsFirst).thenComparing(comp); + FXCollections.sort(param.getItems(), us); return true; }); table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); filenameCol.minWidthProperty().bind(table.widthProperty().multiply(0.5)); - if (fileList.getMode().equals(FileBrowserModel.Mode.SINGLE_FILE_CHOOSER) - || fileList.getMode().equals(FileBrowserModel.Mode.DIRECTORY_CHOOSER)) { - table.getSelectionModel().setSelectionMode(SelectionMode.SINGLE); - } else { - table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); - } - - table.getSelectionModel().getSelectedItems().addListener((ListChangeListener) - c -> { - // Explicitly unselect synthetic entries since we can't use a custom selection model as that is bugged in JavaFX - var toSelect = c.getList().stream() - .filter(entry -> fileList.getFileSystemModel().getCurrentParentDirectory() == null - || !entry.getPath() - .equals(fileList.getFileSystemModel() - .getCurrentParentDirectory() - .getPath())) - .toList(); - fileList.getSelected().setAll(toSelect); - fileList.getFileSystemModel() - .getBrowserModel() - .getSelectedFiles() - .setAll(toSelect); - - Platform.runLater(() -> { - var toUnselect = table.getSelectionModel().getSelectedItems().stream() - .filter(entry -> !toSelect.contains(entry)) - .toList(); - toUnselect.forEach(entry -> table.getSelectionModel() - .clearSelection(table.getItems().indexOf(entry))); - }); - }); - - table.setOnKeyPressed(event -> { - if (event.isControlDown() - && event.getCode().equals(KeyCode.C) - && table.getSelectionModel().getSelectedItems().size() > 0) { - FileBrowserClipboard.startCopy( - fileList.getFileSystemModel().getCurrentDirectory(), - table.getSelectionModel().getSelectedItems()); - event.consume(); - } - - if (event.isControlDown() && event.getCode().equals(KeyCode.V)) { - var clipboard = FileBrowserClipboard.retrieveCopy(); - if (clipboard != null) { - var files = clipboard.getEntries(); - var target = fileList.getFileSystemModel().getCurrentDirectory(); - fileList.getFileSystemModel().dropFilesIntoAsync(target, files, true); - event.consume(); - } - } - }); + table.setFixedCellSize(34.0); + prepareTableSelectionModel(table); + prepareTableShortcuts(table); prepareTableEntries(table); prepareTableChanges(table, mtimeCol, modeCol); return table; } - private void prepareTableEntries(TableView table) { - var emptyEntry = new FileListCompEntry(table, null, fileList); + private void prepareTableSelectionModel(TableView table) { + if (!fileList.getMode().isMultiple()) { + table.getSelectionModel().setSelectionMode(SelectionMode.SINGLE); + } else { + table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); + } + + table.getSelectionModel().getSelectedItems().addListener((ListChangeListener) c -> { + var toSelect = new ArrayList<>(c.getList()); + // Explicitly unselect synthetic entries since we can't use a custom selection model as that is bugged in + // JavaFX + toSelect.removeIf(entry -> fileList.getFileSystemModel().getCurrentParentDirectory() != null + && entry.getRawFileEntry() + .getPath() + .equals(fileList.getFileSystemModel() + .getCurrentParentDirectory() + .getPath())); + // Remove unsuitable selection + toSelect.removeIf(browserEntry -> (browserEntry.getRawFileEntry().isDirectory() + && !fileList.getMode().isAcceptsDirectories()) + || (!browserEntry.getRawFileEntry().isDirectory() + && !fileList.getMode().isAcceptsFiles())); + fileList.getSelection().setAll(toSelect); + + Platform.runLater(() -> { + var toUnselect = table.getSelectionModel().getSelectedItems().stream() + .filter(entry -> !toSelect.contains(entry)) + .toList(); + toUnselect.forEach(entry -> table.getSelectionModel() + .clearSelection(table.getItems().indexOf(entry))); + }); + }); + + fileList.getSelection().addListener((ListChangeListener) c -> { + if (c.getList().equals(table.getSelectionModel().getSelectedItems())) { + return; + } + + Platform.runLater(() -> { + if (c.getList().isEmpty()) { + table.getSelectionModel().clearSelection(); + return; + } + + var indices = c.getList().stream() + .skip(1) + .mapToInt(entry -> table.getItems().indexOf(entry)) + .toArray(); + table.getSelectionModel() + .selectIndices(table.getItems().indexOf(c.getList().get(0)), indices); + }); + }); + } + + private void prepareTableShortcuts(TableView 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 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 row = new TableRow<>(); + TableRow 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 table, TableColumn mtimeCol, TableColumn modeCol) { + + private void prepareTableChanges( + TableView table, + TableColumn mtimeCol, + TableColumn modeCol) { var lastDir = new SimpleObjectProperty(); Runnable updateHandler = () -> { PlatformThread.runLaterIfNeeded(() -> { - var newItems = new ArrayList(); - 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 { + private class FilenameCell extends TableCell { 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 listener; private final BooleanProperty updating = new SimpleBooleanProperty(); - public FilenameCell(Property editing) { + public FilenameCell(Property 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 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 { + private static class FileSizeCell extends TableCell { @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 { + private static class FileModeCell extends TableCell { @Override protected void updateItem(String mode, boolean empty) { @@ -447,7 +475,7 @@ final class FileListComp extends AnchorPane { } } - private static class FileTimeCell extends TableCell { + private static class FileTimeCell extends TableCell { @Override protected void updateItem(Instant fileTime, boolean empty) { diff --git a/app/src/main/java/io/xpipe/app/browser/FileListCompEntry.java b/app/src/main/java/io/xpipe/app/browser/BrowserFileListCompEntry.java similarity index 70% rename from app/src/main/java/io/xpipe/app/browser/FileListCompEntry.java rename to app/src/main/java/io/xpipe/app/browser/BrowserFileListCompEntry.java index 3d0bf9b3..51434049 100644 --- a/app/src/main/java/io/xpipe/app/browser/FileListCompEntry.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserFileListCompEntry.java @@ -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) row.getParent().getParent().getParent().getParent()); + var tv = ((TableView) 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) row.getParent().getParent().getParent().getParent()); + tv.getSelectionModel().select(item); + } + public void onDragOver(DragEvent event) { event.consume(); if (!acceptsDrop(event)) { diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserFileListModel.java b/app/src/main/java/io/xpipe/app/browser/BrowserFileListModel.java new file mode 100644 index 00000000..19628664 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/BrowserFileListModel.java @@ -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 FILE_TYPE_COMPARATOR = + Comparator.comparing(path -> !path.getRawFileEntry().isDirectory()); + static final Predicate PREDICATE_ANY = path -> true; + static final Predicate PREDICATE_NOT_HIDDEN = path -> true; + + private final OpenFileSystemModel fileSystemModel; + private final Property> comparatorProperty = + new SimpleObjectProperty<>(FILE_TYPE_COMPARATOR); + private final Property> all = new SimpleObjectProperty<>(new ArrayList<>()); + private final Property> shown = new SimpleObjectProperty<>(new ArrayList<>()); + private final ObjectProperty> predicateProperty = + new SimpleObjectProperty<>(path -> true); + private final ObservableList selection = FXCollections.observableArrayList(); + private final ObservableList selectedRaw = + BindingsHelper.mappedContentBinding(selection, entry -> entry.getRawFileEntry()); + + private final Property draggedOverDirectory = new SimpleObjectProperty(); + private final Property draggedOverEmpty = new SimpleBooleanProperty(); + private final Property 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 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 comparator) { + comparatorProperty.setValue(comparator); + refreshShown(); + } + + private void refreshShown() { + List 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 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> predicateProperty() { + return predicateProperty; + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/FileFilterComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserFilterComp.java similarity index 76% rename from app/src/main/java/io/xpipe/app/browser/FileFilterComp.java rename to app/src/main/java/io/xpipe/app/browser/BrowserFilterComp.java index a939d6dc..37936640 100644 --- a/app/src/main/java/io/xpipe/app/browser/FileFilterComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserFilterComp.java @@ -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 filterString; - - public FileFilterComp(Property filterString) { - this.filterString = filterString; - } +public class BrowserFilterComp extends Comp { @Override - protected Region createSimple() { + public Structure createBase() { var expanded = new SimpleBooleanProperty(); var text = new TextFieldComp(filterString, false).createRegion(); var button = new Button(); @@ -56,8 +46,6 @@ public class FileFilterComp extends SimpleComp { }); var fi = new FontIcon("mdi2m-magnify"); - GrowAugment.create(false, true).augment(new SimpleCompStructure<>(button)); - Shortcuts.addShortcut(button, new KeyCodeCombination(KeyCode.F, KeyCombination.SHORTCUT_DOWN)); button.setGraphic(fi); button.setOnAction(event -> { if (expanded.get()) { @@ -75,7 +63,6 @@ public class FileFilterComp extends SimpleComp { text.setPrefWidth(0); button.getStyleClass().add(Styles.FLAT); expanded.addListener((observable, oldValue, val) -> { - System.out.println(val); if (val) { text.setPrefWidth(250); button.getStyleClass().add(Styles.RIGHT_PILL); @@ -86,9 +73,24 @@ public class FileFilterComp extends SimpleComp { button.getStyleClass().add(Styles.FLAT); } }); + button.prefHeightProperty().bind(text.heightProperty()); var box = new HBox(text, button); - box.setFillHeight(true); - return box; + box.setAlignment(Pos.CENTER); + return new Structure(box, (TextField) text, button); + } + + public record Structure(HBox box, TextField textField, Button toggleButton) implements CompStructure { + + @Override + public HBox get() { + return box; + } + } + + private final Property filterString; + + public BrowserFilterComp(Property filterString) { + this.filterString = filterString; } } diff --git a/app/src/main/java/io/xpipe/app/browser/FileBrowserHistory.java b/app/src/main/java/io/xpipe/app/browser/BrowserHistory.java similarity index 96% rename from app/src/main/java/io/xpipe/app/browser/FileBrowserHistory.java rename to app/src/main/java/io/xpipe/app/browser/BrowserHistory.java index 6fe19adb..ac507aa1 100644 --- a/app/src/main/java/io/xpipe/app/browser/FileBrowserHistory.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserHistory.java @@ -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 history = new ArrayList<>(); diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserModel.java b/app/src/main/java/io/xpipe/app/browser/BrowserModel.java new file mode 100644 index 00000000..c8d62f7d --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/BrowserModel.java @@ -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> onFinish; + + private final ObservableList openFileSystems = FXCollections.observableArrayList(); + private final Property selected = new SimpleObjectProperty<>(); + private final BrowserTransferModel localTransfersStage = new BrowserTransferModel(); + private final ObservableList 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(); + } + }); + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserNavBar.java b/app/src/main/java/io/xpipe/app/browser/BrowserNavBar.java new file mode 100644 index 00000000..912c65e5 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/BrowserNavBar.java @@ -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; + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/SelectedFileListComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserSelectionListComp.java similarity index 93% rename from app/src/main/java/io/xpipe/app/browser/SelectedFileListComp.java rename to app/src/main/java/io/xpipe/app/browser/BrowserSelectionListComp.java index 53e022cc..180098b7 100644 --- a/app/src/main/java/io/xpipe/app/browser/SelectedFileListComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserSelectionListComp.java @@ -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 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); diff --git a/app/src/main/java/io/xpipe/app/browser/FileBrowserStatusBarComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserStatusBarComp.java similarity index 69% rename from app/src/main/java/io/xpipe/app/browser/FileBrowserStatusBarComp.java rename to app/src/main/java/io/xpipe/app/browser/BrowserStatusBarComp.java index 549c84e0..93bc4975 100644 --- a/app/src/main/java/io/xpipe/app/browser/FileBrowserStatusBarComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserStatusBarComp.java @@ -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; } } diff --git a/app/src/main/java/io/xpipe/app/browser/LocalFileTransferComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserTransferComp.java similarity index 90% rename from app/src/main/java/io/xpipe/app/browser/LocalFileTransferComp.java rename to app/src/main/java/io/xpipe/app/browser/BrowserTransferComp.java index 00bb5013..b413c89d 100644 --- a/app/src/main/java/io/xpipe/app/browser/LocalFileTransferComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserTransferComp.java @@ -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); diff --git a/app/src/main/java/io/xpipe/app/browser/LocalFileTransferStage.java b/app/src/main/java/io/xpipe/app/browser/BrowserTransferModel.java similarity index 98% rename from app/src/main/java/io/xpipe/app/browser/LocalFileTransferStage.java rename to app/src/main/java/io/xpipe/app/browser/BrowserTransferModel.java index 1b8f869d..1c6ddecd 100644 --- a/app/src/main/java/io/xpipe/app/browser/LocalFileTransferStage.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserTransferModel.java @@ -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"); diff --git a/app/src/main/java/io/xpipe/app/browser/FileBrowserModel.java b/app/src/main/java/io/xpipe/app/browser/FileBrowserModel.java deleted file mode 100644 index ef37942f..00000000 --- a/app/src/main/java/io/xpipe/app/browser/FileBrowserModel.java +++ /dev/null @@ -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 selectedFiles = FXCollections.observableArrayList(); - - @Setter - private Consumer> onFinish; - - private final ObservableList openFileSystems = FXCollections.observableArrayList(); - private final Property 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); - }); - } -} diff --git a/app/src/main/java/io/xpipe/app/browser/FileContextMenu.java b/app/src/main/java/io/xpipe/app/browser/FileContextMenu.java deleted file mode 100644 index d6b7d90e..00000000 --- a/app/src/main/java/io/xpipe/app/browser/FileContextMenu.java +++ /dev/null @@ -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 editing; - - public FileContextMenu(OpenFileSystemModel model, FileSystem.FileEntry entry, Property 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); - } -} diff --git a/app/src/main/java/io/xpipe/app/browser/FileListModel.java b/app/src/main/java/io/xpipe/app/browser/FileListModel.java deleted file mode 100644 index b4723654..00000000 --- a/app/src/main/java/io/xpipe/app/browser/FileListModel.java +++ /dev/null @@ -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 FILE_TYPE_COMPARATOR = - Comparator.comparing(path -> !path.isDirectory()); - static final Predicate PREDICATE_ANY = path -> true; - static final Predicate PREDICATE_NOT_HIDDEN = path -> true; - - private final OpenFileSystemModel fileSystemModel; - private final Property> comparatorProperty = - new SimpleObjectProperty<>(FILE_TYPE_COMPARATOR); - private final Property> all = new SimpleObjectProperty<>(new ArrayList<>()); - private final Property> shown = new SimpleObjectProperty<>(new ArrayList<>()); - private final ObjectProperty> predicateProperty = - new SimpleObjectProperty<>(path -> true); - private final ObservableList selected = FXCollections.observableArrayList(); - - private final Property draggedOverDirectory = new SimpleObjectProperty(); - private final Property draggedOverEmpty = new SimpleBooleanProperty(); - private final Property 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 newFiles) { - all.setValue(newFiles); - refreshShown(); - } - - public void setAll(Stream newFiles) { - try (var s = newFiles) { - var l = s.filter(entry -> entry != null).limit(5000).toList(); - all.setValue(l); - refreshShown(); - } - } - - public void setComparator(Comparator comparator) { - comparatorProperty.setValue(comparator); - refreshShown(); - } - - private void refreshShown() { - List 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 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> predicateProperty() { - return predicateProperty; - } -} diff --git a/app/src/main/java/io/xpipe/app/browser/FileSystemHelper.java b/app/src/main/java/io/xpipe/app/browser/FileSystemHelper.java index bdf66992..1410aa4c 100644 --- a/app/src/main/java/io/xpipe/app/browser/FileSystemHelper.java +++ b/app/src/main/java/io/xpipe/app/browser/FileSystemHelper.java @@ -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); } diff --git a/app/src/main/java/io/xpipe/app/browser/OpenFileSystemCache.java b/app/src/main/java/io/xpipe/app/browser/OpenFileSystemCache.java new file mode 100644 index 00000000..603838e8 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/OpenFileSystemCache.java @@ -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 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); + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/OpenFileSystemComp.java b/app/src/main/java/io/xpipe/app/browser/OpenFileSystemComp.java index 028279b9..96fa018e 100644 --- a/app/src/main/java/io/xpipe/app/browser/OpenFileSystemComp.java +++ b/app/src/main/java/io/xpipe/app/browser/OpenFileSystemComp.java @@ -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; } } diff --git a/app/src/main/java/io/xpipe/app/browser/OpenFileSystemModel.java b/app/src/main/java/io/xpipe/app/browser/OpenFileSystemModel.java index fa81db1a..706473bc 100644 --- a/app/src/main/java/io/xpipe/app/browser/OpenFileSystemModel.java +++ b/app/src/main/java/io/xpipe/app/browser/OpenFileSystemModel.java @@ -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 store = new SimpleObjectProperty<>(); + private final FileSystemStore store; private FileSystem fileSystem; private final Property filter = new SimpleStringProperty(); - private final FileListModel fileList; + private final BrowserFileListModel fileList; private final ReadOnlyObjectWrapper 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 savedState = new SimpleObjectProperty<>(); + private final OpenFileSystemCache cache = new OpenFileSystemCache(this); + private final Property 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 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; } diff --git a/app/src/main/java/io/xpipe/app/browser/OpenFileSystemSavedState.java b/app/src/main/java/io/xpipe/app/browser/OpenFileSystemSavedState.java new file mode 100644 index 00000000..ef1eda35 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/OpenFileSystemSavedState.java @@ -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; +} diff --git a/app/src/main/java/io/xpipe/app/browser/StandaloneFileBrowser.java b/app/src/main/java/io/xpipe/app/browser/StandaloneFileBrowser.java index f7ff2dde..600fe4b3 100644 --- a/app/src/main/java/io/xpipe/app/browser/StandaloneFileBrowser.java +++ b/app/src/main/java/io/xpipe/app/browser/StandaloneFileBrowser.java @@ -37,8 +37,8 @@ public class StandaloneFileBrowser { public static void openSingleFile(Property 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 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); diff --git a/app/src/main/java/io/xpipe/app/browser/action/ApplicationPathAction.java b/app/src/main/java/io/xpipe/app/browser/action/ApplicationPathAction.java new file mode 100644 index 00000000..2e1b471c --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/action/ApplicationPathAction.java @@ -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 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 entries) { + return model.getCache().isApplicationInPath(getExecutable()); + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/action/BranchAction.java b/app/src/main/java/io/xpipe/app/browser/action/BranchAction.java new file mode 100644 index 00000000..17c69caf --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/action/BranchAction.java @@ -0,0 +1,8 @@ +package io.xpipe.app.browser.action; + +import java.util.List; + +public interface BranchAction extends BrowserAction { + + List getBranchingActions(); +} diff --git a/app/src/main/java/io/xpipe/app/browser/action/BrowserAction.java b/app/src/main/java/io/xpipe/app/browser/action/BrowserAction.java new file mode 100644 index 00000000..fa57213a --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/action/BrowserAction.java @@ -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 ALL = new ArrayList<>(); + + public static List 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 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 entries); + + public default boolean isApplicable(OpenFileSystemModel model, List entries) { + return true; + } + + public default boolean isActive(OpenFileSystemModel model, List 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; + } + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/action/ExecuteApplicationAction.java b/app/src/main/java/io/xpipe/app/browser/action/ExecuteApplicationAction.java new file mode 100644 index 00000000..348cb94e --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/action/ExecuteApplicationAction.java @@ -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 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); + +} diff --git a/app/src/main/java/io/xpipe/app/browser/action/LeafAction.java b/app/src/main/java/io/xpipe/app/browser/action/LeafAction.java new file mode 100644 index 00000000..4712996c --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/action/LeafAction.java @@ -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 entries) throws Exception; + + default MenuItem toItem(OpenFileSystemModel model, List selected, UnaryOperator 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; + } + +} diff --git a/app/src/main/java/io/xpipe/app/browser/action/MultiExecuteAction.java b/app/src/main/java/io/xpipe/app/browser/action/MultiExecuteAction.java new file mode 100644 index 00000000..4eea0503 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/action/MultiExecuteAction.java @@ -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 entries) { + return entries.size() == 1 ? entries.get(0).getOptionallyQuotedFileName() : "(" + entries.size() + ")"; + } + + protected abstract String createCommand(ShellControl sc, OpenFileSystemModel model, BrowserEntry entry); + + @Override + public List getBranchingActions() { + return List.of( + new LeafAction() { + + @Override + public void execute(OpenFileSystemModel model, List 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 entries) { + return "in " + AppPrefs.get().terminalType().getValue().toTranslatedString(); + } + }, + new LeafAction() { + + @Override + public void execute(OpenFileSystemModel model, List 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 entries) { + return "in background"; + } + }, + new LeafAction() { + + @Override + public void execute(OpenFileSystemModel model, List 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 entries) { + return "wait for completion"; + } + }); + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/icon/BrowserIcons.java b/app/src/main/java/io/xpipe/app/browser/icon/BrowserIcons.java new file mode 100644 index 00000000..3b3399f6 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/icon/BrowserIcons.java @@ -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); + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/icon/DirectoryType.java b/app/src/main/java/io/xpipe/app/browser/icon/DirectoryType.java new file mode 100644 index 00000000..a0d8fcd1 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/icon/DirectoryType.java @@ -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 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); +} diff --git a/app/src/main/java/io/xpipe/app/browser/icon/FileIconFactory.java b/app/src/main/java/io/xpipe/app/browser/icon/FileIconFactory.java deleted file mode 100644 index d65aaf13..00000000 --- a/app/src/main/java/io/xpipe/app/browser/icon/FileIconFactory.java +++ /dev/null @@ -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); -} diff --git a/app/src/main/java/io/xpipe/app/browser/icon/FileIconManager.java b/app/src/main/java/io/xpipe/app/browser/icon/FileIconManager.java index 8cc48f9c..07773d99 100644 --- a/app/src/main/java/io/xpipe/app/browser/icon/FileIconManager.java +++ b/app/src/main/java/io/xpipe/app/browser/icon/FileIconManager.java @@ -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 factories = new ArrayList<>(); - private static final List 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)); } } } diff --git a/app/src/main/java/io/xpipe/app/browser/icon/FileIcons.java b/app/src/main/java/io/xpipe/app/browser/icon/FileIcons.java deleted file mode 100644 index 89992de7..00000000 --- a/app/src/main/java/io/xpipe/app/browser/icon/FileIcons.java +++ /dev/null @@ -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); - } -} diff --git a/app/src/main/java/io/xpipe/app/browser/icon/FileType.java b/app/src/main/java/io/xpipe/app/browser/icon/FileType.java new file mode 100644 index 00000000..cbc44d0b --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/icon/FileType.java @@ -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 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(); +} diff --git a/app/src/main/java/io/xpipe/app/browser/icon/FolderIconFactory.java b/app/src/main/java/io/xpipe/app/browser/icon/FolderIconFactory.java deleted file mode 100644 index 3f8203bc..00000000 --- a/app/src/main/java/io/xpipe/app/browser/icon/FolderIconFactory.java +++ /dev/null @@ -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); -} diff --git a/app/src/main/java/io/xpipe/app/comp/AppLayoutComp.java b/app/src/main/java/io/xpipe/app/comp/AppLayoutComp.java index 9be90bed..a8b47494 100644 --- a/app/src/main/java/io/xpipe/app/comp/AppLayoutComp.java +++ b/app/src/main/java/io/xpipe/app/comp/AppLayoutComp.java @@ -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> { 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> { 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))) { diff --git a/app/src/main/java/io/xpipe/app/comp/PrefsComp.java b/app/src/main/java/io/xpipe/app/comp/PrefsComp.java index 841300c5..9c1ba13a 100644 --- a/app/src/main/java/io/xpipe/app/comp/PrefsComp.java +++ b/app/src/main/java/io/xpipe/app/comp/PrefsComp.java @@ -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); diff --git a/app/src/main/java/io/xpipe/app/comp/base/ErrorOverlayComp.java b/app/src/main/java/io/xpipe/app/comp/base/ErrorOverlayComp.java new file mode 100644 index 00000000..f0c7fbf2 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/comp/base/ErrorOverlayComp.java @@ -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 text; + + public ErrorOverlayComp(Comp background, Property text) { + this.background = background; + this.text = text; + } + + @Override + protected Region createSimple() { + var content = new SimpleObjectProperty(); + 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(); + } +} diff --git a/app/src/main/java/io/xpipe/app/comp/base/MessageComp.java b/app/src/main/java/io/xpipe/app/comp/base/MessageComp.java deleted file mode 100644 index 4e7e5b05..00000000 --- a/app/src/main/java/io/xpipe/app/comp/base/MessageComp.java +++ /dev/null @@ -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 shown = new SimpleBooleanProperty(); - - ObservableValue text; - int msShown; - - public MessageComp(ObservableValue 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; - } -} diff --git a/app/src/main/java/io/xpipe/app/comp/base/ModalOverlayComp.java b/app/src/main/java/io/xpipe/app/comp/base/ModalOverlayComp.java new file mode 100644 index 00000000..8d6f738c --- /dev/null +++ b/app/src/main/java/io/xpipe/app/comp/base/ModalOverlayComp.java @@ -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) { + 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; + + @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; + } +} diff --git a/app/src/main/java/io/xpipe/app/comp/source/store/GuiDsStoreCreator.java b/app/src/main/java/io/xpipe/app/comp/source/store/GuiDsStoreCreator.java index c59fe1e3..73df1e01 100644 --- a/app/src/main/java/io/xpipe/app/comp/source/store/GuiDsStoreCreator.java +++ b/app/src/main/java/io/xpipe/app/comp/source/store/GuiDsStoreCreator.java @@ -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> { BooleanProperty busy = new SimpleBooleanProperty(); Property validator = new SimpleObjectProperty<>(new SimpleValidator()); Property messageProp = new SimpleStringProperty(); - MessageComp message = new MessageComp(messageProp, 10000); BooleanProperty finished = new SimpleBooleanProperty(); Property entry = new SimpleObjectProperty<>(); BooleanProperty changedSinceError = new SimpleBooleanProperty(); @@ -188,7 +186,14 @@ public class GuiDsStoreCreator extends MultiStepComp.Step> { @Override public CompStructure 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> { 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> { .getText(); TrackEvent.info(msg); messageProp.setValue(msg); - message.show(); changedSinceError.setValue(false); return false; } @@ -287,7 +287,6 @@ public class GuiDsStoreCreator extends MultiStepComp.Step> { PlatformThread.runLaterIfNeeded(parent::next); } catch (Exception ex) { messageProp.setValue(ExceptionConverter.convertMessage(ex)); - message.show(); changedSinceError.setValue(false); ErrorEvent.fromThrowable(ex).omit().reportable(false).handle(); } diff --git a/app/src/main/java/io/xpipe/app/comp/storage/collection/SourceCollectionContextMenu.java b/app/src/main/java/io/xpipe/app/comp/storage/collection/SourceCollectionContextMenu.java index aaa53138..d18a3bbf 100644 --- a/app/src/main/java/io/xpipe/app/comp/storage/collection/SourceCollectionContextMenu.java +++ b/app/src/main/java/io/xpipe/app/comp/storage/collection/SourceCollectionContextMenu.java @@ -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> extends PopupMenuAugment { - - private final SourceCollectionWrapper group; - private final Region renameTextField; +public class SourceCollectionContextMenu> extends ContextMenuAugment { 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> 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> 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> 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); } diff --git a/app/src/main/java/io/xpipe/app/comp/storage/source/SourceEntryContextMenu.java b/app/src/main/java/io/xpipe/app/comp/storage/source/SourceEntryContextMenu.java index 13670fee..1b4b0722 100644 --- a/app/src/main/java/io/xpipe/app/comp/storage/source/SourceEntryContextMenu.java +++ b/app/src/main/java/io/xpipe/app/comp/storage/source/SourceEntryContextMenu.java @@ -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> extends PopupMenuAugment { +public class SourceEntryContextMenu> extends ContextMenuAugment { - 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()); diff --git a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryComp.java b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryComp.java index 19dc8353..b40c3f75 100644 --- a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryComp.java +++ b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryComp.java @@ -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)); diff --git a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryTree.java b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryTree.java new file mode 100644 index 00000000..2c264879 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryTree.java @@ -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 createTree() { + var topLevel = StoreSection.createTopLevel(); + var root = new TreeItem(); + root.setExpanded(true); + + // Listen for any entry list change, not only top level changes + StoreViewState.get().getAllEntries().addListener((ListChangeListener) 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 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); + } + } +} diff --git a/app/src/main/java/io/xpipe/app/core/App.java b/app/src/main/java/io/xpipe/app/core/App.java index db68063e..5ed92e93 100644 --- a/app/src/main/java/io/xpipe/app/core/App.java +++ b/app/src/main/java/io/xpipe/app/core/App.java @@ -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( diff --git a/app/src/main/java/io/xpipe/app/core/AppStyle.java b/app/src/main/java/io/xpipe/app/core/AppStyle.java index 10eb8e2f..f7cef72f 100644 --- a/app/src/main/java/io/xpipe/app/core/AppStyle.java +++ b/app/src/main/java/io/xpipe/app/core/AppStyle.java @@ -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(); - } - } } diff --git a/app/src/main/java/io/xpipe/app/core/AppTheme.java b/app/src/main/java/io/xpipe/app/core/AppTheme.java new file mode 100644 index 00000000..83e341ae --- /dev/null +++ b/app/src/main/java/io/xpipe/app/core/AppTheme.java @@ -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(); + } + } +} diff --git a/app/src/main/java/io/xpipe/app/core/mode/OperationMode.java b/app/src/main/java/io/xpipe/app/core/mode/OperationMode.java index bf101947..4e54bf84 100644 --- a/app/src/main/java/io/xpipe/app/core/mode/OperationMode.java +++ b/app/src/main/java/io/xpipe/app/core/mode/OperationMode.java @@ -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(); diff --git a/app/src/main/java/io/xpipe/app/core/mode/PlatformMode.java b/app/src/main/java/io/xpipe/app/core/mode/PlatformMode.java index 1b114707..63ceeddc 100644 --- a/app/src/main/java/io/xpipe/app/core/mode/PlatformMode.java +++ b/app/src/main/java/io/xpipe/app/core/mode/PlatformMode.java @@ -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"); diff --git a/app/src/main/java/io/xpipe/app/fxcomps/augment/ContextMenuAugment.java b/app/src/main/java/io/xpipe/app/fxcomps/augment/ContextMenuAugment.java new file mode 100644 index 00000000..ac9fd9cd --- /dev/null +++ b/app/src/main/java/io/xpipe/app/fxcomps/augment/ContextMenuAugment.java @@ -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> implements Augment { + + private final boolean showOnPrimaryButton; + private final Supplier contextMenu; + + public ContextMenuAugment(boolean showOnPrimaryButton, Supplier 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(); + } + }); + } +} diff --git a/app/src/main/java/io/xpipe/app/fxcomps/augment/PopupMenuAugment.java b/app/src/main/java/io/xpipe/app/fxcomps/augment/PopupMenuAugment.java deleted file mode 100644 index 7a8e2064..00000000 --- a/app/src/main/java/io/xpipe/app/fxcomps/augment/PopupMenuAugment.java +++ /dev/null @@ -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> implements Augment { - - 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(); - } - }); - } -} diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/SvgView.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/SvgView.java index 07fb5480..f8989fa6 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/SvgView.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/SvgView.java @@ -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; } diff --git a/app/src/main/java/io/xpipe/app/fxcomps/util/Shortcuts.java b/app/src/main/java/io/xpipe/app/fxcomps/util/Shortcuts.java index f80ec6b2..d6a952a3 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/util/Shortcuts.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/util/Shortcuts.java @@ -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 void addShortcut(T region, KeyCombination comb, Consumer exec) { - AtomicReference scene = new AtomicReference<>(region.getScene()); var filter = new EventHandler() { public void handle(KeyEvent ke) { if (comb.match(ke)) { @@ -30,21 +30,23 @@ public class Shortcuts { } } }; - SHORTCUTS.put(region, comb); + AtomicReference 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); } }); } diff --git a/app/src/main/java/io/xpipe/app/launcher/LauncherInput.java b/app/src/main/java/io/xpipe/app/launcher/LauncherInput.java index 258b376f..e2aa88a3 100644 --- a/app/src/main/java/io/xpipe/app/launcher/LauncherInput.java +++ b/app/src/main/java/io/xpipe/app/launcher/LauncherInput.java @@ -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 diff --git a/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java b/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java index 07973cc3..f7345b5e 100644 --- a/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java +++ b/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java @@ -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 languageList = new SimpleListProperty<>(FXCollections.observableArrayList(Arrays.asList(SupportedLocale.values()))); - private final SimpleListProperty themeList = - new SimpleListProperty<>(FXCollections.observableArrayList(Arrays.asList(AppStyle.Theme.values()))); + private final SimpleListProperty themeList = + new SimpleListProperty<>(FXCollections.observableArrayList(Arrays.asList(AppTheme.Theme.values()))); private final SimpleListProperty closeBehaviourList = new SimpleListProperty<>( FXCollections.observableArrayList(PrefsChoiceValue.getSupported(CloseBehaviour.class))); private final SimpleListProperty externalEditorList = new SimpleListProperty<>( @@ -90,11 +90,10 @@ public class AppPrefs { languageList, languageInternal) .render(() -> new TranslatableComboBoxControl<>()); - private final ObjectProperty themeInternal = - typed(new SimpleObjectProperty<>(AppStyle.Theme.LIGHT), AppStyle.Theme.class); - public final ReadOnlyProperty theme = themeInternal; - private final SingleSelectionField themeControl = - Field.ofSingleSelectionType(themeList, themeInternal).render(() -> new TranslatableComboBoxControl<>()); + public final ObjectProperty theme = + typed(new SimpleObjectProperty<>(), AppTheme.Theme.class); + private final SingleSelectionField 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))), diff --git a/app/src/main/java/io/xpipe/app/prefs/ExternalApplicationType.java b/app/src/main/java/io/xpipe/app/prefs/ExternalApplicationType.java index dbac96cd..94beeabf 100644 --- a/app/src/main/java/io/xpipe/app/prefs/ExternalApplicationType.java +++ b/app/src/main/java/io/xpipe/app/prefs/ExternalApplicationType.java @@ -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; diff --git a/app/src/main/java/io/xpipe/app/prefs/ExternalEditorType.java b/app/src/main/java/io/xpipe/app/prefs/ExternalEditorType.java index 8b18b7ce..2aa24c05 100644 --- a/app/src/main/java/io/xpipe/app/prefs/ExternalEditorType.java +++ b/app/src/main/java/io/xpipe/app/prefs/ExternalEditorType.java @@ -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 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) { diff --git a/app/src/main/java/io/xpipe/app/prefs/ExternalTerminalType.java b/app/src/main/java/io/xpipe/app/prefs/ExternalTerminalType.java index 88687c9f..e70b23a8 100644 --- a/app/src/main/java/io/xpipe/app/prefs/ExternalTerminalType.java +++ b/app/src/main/java/io/xpipe/app/prefs/ExternalTerminalType.java @@ -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 ALL = Stream.of( + List ALL = Stream.of( WINDOWS_TERMINAL, PWSH_WINDOWS, POWERSHELL_WINDOWS, @@ -156,7 +157,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue { .filter(terminalType -> terminalType.isSelectable()) .toList(); - public static ExternalTerminalType getDefault() { + static ExternalTerminalType getDefault() { return ALL.stream() .filter(externalTerminalType -> !externalTerminalType.equals(CUSTOM)) .filter(terminalType -> terminalType.isAvailable()) @@ -164,12 +165,12 @@ public interface ExternalTerminalType extends PrefsChoiceValue { .orElse(null); } - public abstract void launch(String name, String file, boolean elevated) throws Exception; + void launch(String name, String file, boolean elevated) throws Exception; - static class MacOsTerminalType extends ExternalApplicationType.MacApplication implements ExternalTerminalType { + class MacOsTerminalType extends ExternalApplicationType.MacApplication implements ExternalTerminalType { public MacOsTerminalType() { - super("macosTerminal", "Terminal"); + super("app.macosTerminal", "Terminal"); } @Override @@ -178,22 +179,21 @@ public interface ExternalTerminalType extends PrefsChoiceValue { var suffix = file.equals(pc.getShellDialect().getOpenCommand()) ? "\"\"" : "\"" + file.replaceAll("\"", "\\\\\"") + "\""; - var cmd = String.format( - """ - osascript - "$@" < 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); diff --git a/app/src/main/java/io/xpipe/app/util/Containers.java b/app/src/main/java/io/xpipe/app/util/Containers.java deleted file mode 100644 index f7a901d1..00000000 --- a/app/src/main/java/io/xpipe/app/util/Containers.java +++ /dev/null @@ -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); - } -} diff --git a/app/src/main/java/io/xpipe/app/util/Controls.java b/app/src/main/java/io/xpipe/app/util/Controls.java deleted file mode 100644 index 238aea07..00000000 --- a/app/src/main/java/io/xpipe/app/util/Controls.java +++ /dev/null @@ -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; - } -} diff --git a/app/src/main/java/io/xpipe/app/util/HumanReadableFormat.java b/app/src/main/java/io/xpipe/app/util/HumanReadableFormat.java index 77fc8922..c1544446 100644 --- a/app/src/main/java/io/xpipe/app/util/HumanReadableFormat.java +++ b/app/src/main/java/io/xpipe/app/util/HumanReadableFormat.java @@ -1,5 +1,3 @@ -/* SPDX-License-Identifier: MIT */ - package io.xpipe.app.util; import java.text.CharacterIterator; diff --git a/app/src/main/java/io/xpipe/app/util/MacOsPermissions.java b/app/src/main/java/io/xpipe/app/util/MacOsPermissions.java index 937ba673..6a1f4c98 100644 --- a/app/src/main/java/io/xpipe/app/util/MacOsPermissions.java +++ b/app/src/main/java/io/xpipe/app/util/MacOsPermissions.java @@ -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) { diff --git a/app/src/main/java/io/xpipe/app/util/ScriptHelper.java b/app/src/main/java/io/xpipe/app/util/ScriptHelper.java index 82c30477..eb30b576 100644 --- a/app/src/main/java/io/xpipe/app/util/ScriptHelper.java +++ b/app/src/main/java/io/xpipe/app/util/ScriptHelper.java @@ -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 & disown"; } diff --git a/app/src/main/java/module-info.java b/app/src/main/java/module-info.java index 8f29eb49..69dd3b9c 100644 --- a/app/src/main/java/module-info.java +++ b/app/src/main/java/module-info.java @@ -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; diff --git a/app/src/main/resources/io/xpipe/app/resources/browser_icons/file_list.txt b/app/src/main/resources/io/xpipe/app/resources/file_list.txt similarity index 100% rename from app/src/main/resources/io/xpipe/app/resources/browser_icons/file_list.txt rename to app/src/main/resources/io/xpipe/app/resources/file_list.txt diff --git a/app/src/main/resources/io/xpipe/app/resources/browser_icons/folder_list.txt b/app/src/main/resources/io/xpipe/app/resources/folder_list.txt similarity index 100% rename from app/src/main/resources/io/xpipe/app/resources/browser_icons/folder_list.txt rename to app/src/main/resources/io/xpipe/app/resources/folder_list.txt diff --git a/app/src/main/resources/io/xpipe/app/resources/lang/translations_en.properties b/app/src/main/resources/io/xpipe/app/resources/lang/translations_en.properties index 207fa984..470e509c 100644 --- a/app/src/main/resources/io/xpipe/app/resources/lang/translations_en.properties +++ b/app/src/main/resources/io/xpipe/app/resources/lang/translations_en.properties @@ -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 diff --git a/app/src/main/resources/io/xpipe/app/resources/style/browser.css b/app/src/main/resources/io/xpipe/app/resources/style/browser.css index 7d82d99e..a60e4a78 100644 --- a/app/src/main/resources/io/xpipe/app/resources/style/browser.css +++ b/app/src/main/resources/io/xpipe/app/resources/style/browser.css @@ -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; } diff --git a/app/src/main/resources/io/xpipe/app/resources/style/data-store-step.css b/app/src/main/resources/io/xpipe/app/resources/style/data-store-step.css index 114c9538..cafd6e18 100644 --- a/app/src/main/resources/io/xpipe/app/resources/style/data-store-step.css +++ b/app/src/main/resources/io/xpipe/app/resources/style/data-store-step.css @@ -1,5 +1,5 @@ .step { --fx-padding: 1em 1.5em 0.5em 1.5em; +-fx-padding: 0; } .spacer { diff --git a/app/src/main/resources/io/xpipe/app/resources/style/error-overlay-comp.css b/app/src/main/resources/io/xpipe/app/resources/style/error-overlay-comp.css new file mode 100644 index 00000000..6bc56f69 --- /dev/null +++ b/app/src/main/resources/io/xpipe/app/resources/style/error-overlay-comp.css @@ -0,0 +1,5 @@ +.error-overlay-comp { +-fx-padding: 1.0em; +-fx-border-width: 1px; +-fx-border-radius: 2px; +} diff --git a/app/src/main/resources/io/xpipe/app/resources/style/modal-overlay-comp.css b/app/src/main/resources/io/xpipe/app/resources/style/modal-overlay-comp.css new file mode 100644 index 00000000..229981cd --- /dev/null +++ b/app/src/main/resources/io/xpipe/app/resources/style/modal-overlay-comp.css @@ -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; +} + diff --git a/app/src/main/resources/io/xpipe/app/resources/style/multi-step-comp.css b/app/src/main/resources/io/xpipe/app/resources/style/multi-step-comp.css index ffc1ca8d..ba6a2a62 100644 --- a/app/src/main/resources/io/xpipe/app/resources/style/multi-step-comp.css +++ b/app/src/main/resources/io/xpipe/app/resources/style/multi-step-comp.css @@ -80,3 +80,7 @@ -fx-pref-height: 0; } +.multi-step-comp > .jfx-tab-pane .tab-content-area { +-fx-padding: 0; +} + diff --git a/app/src/main/resources/io/xpipe/app/resources/style/style.css b/app/src/main/resources/io/xpipe/app/resources/style/style.css index e4895d6f..22cd510f 100644 --- a/app/src/main/resources/io/xpipe/app/resources/style/style.css +++ b/app/src/main/resources/io/xpipe/app/resources/style/style.css @@ -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; } diff --git a/core/src/main/java/io/xpipe/core/impl/FileNames.java b/core/src/main/java/io/xpipe/core/impl/FileNames.java index 985f463b..6d020d3e 100644 --- a/core/src/main/java/io/xpipe/core/impl/FileNames.java +++ b/core/src/main/java/io/xpipe/core/impl/FileNames.java @@ -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 splitHierarchy(String file) { + if (file.isEmpty()) { + return List.of(); + } + + file = file + "/"; + var list = new ArrayList(); + 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; } diff --git a/core/src/main/java/io/xpipe/core/process/OsType.java b/core/src/main/java/io/xpipe/core/process/OsType.java index f58c3e67..206f519f 100644 --- a/core/src/main/java/io/xpipe/core/process/OsType.java +++ b/core/src/main/java/io/xpipe/core/process/OsType.java @@ -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"; diff --git a/core/src/main/java/io/xpipe/core/process/ShellControl.java b/core/src/main/java/io/xpipe/core/process/ShellControl.java index ffa0f91b..41b0c54e 100644 --- a/core/src/main/java/io/xpipe/core/process/ShellControl.java +++ b/core/src/main/java/io/xpipe/core/process/ShellControl.java @@ -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 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 - "$@" < elevationFunction); @@ -81,16 +96,17 @@ public interface ShellControl extends ProcessControl { default ShellControl subShell(@NonNull ShellDialect type) { return subShell(p -> type.getOpenCommand(), new TerminalOpenFunction() { - @Override - public boolean changesEnvironment() { - return false; - } + @Override + public boolean changesEnvironment() { + return false; + } - @Override - public String prepare(ShellControl sc, String command) throws Exception { - return command; - } - }).elevationPassword(getElevationPassword()); + @Override + public String prepare(ShellControl sc, String command) throws Exception { + return command; + } + }) + .elevationPassword(getElevationPassword()); } interface TerminalOpenFunction { @@ -102,16 +118,16 @@ public interface ShellControl extends ProcessControl { default ShellControl identicalSubShell() { return subShell(p -> p.getShellDialect().getOpenCommand(), new TerminalOpenFunction() { - @Override - public boolean changesEnvironment() { - return false; - } + @Override + public boolean changesEnvironment() { + return false; + } - @Override - public String prepare(ShellControl sc, String command) throws Exception { - return command; - } - }) + @Override + public String prepare(ShellControl sc, String command) throws Exception { + return command; + } + }) .elevationPassword(getElevationPassword()); } @@ -129,8 +145,17 @@ public interface ShellControl extends ProcessControl { }); } + default ShellControl enforcedDialect(ShellDialect type) throws Exception { + start(); + if (getShellDialect().equals(type)) { + return this; + } else { + return subShell(type).start(); + } + } + default T enforceDialect(@NonNull ShellDialect type, Function sc) throws Exception { - if (isRunning() && getShellDialect().equals(type)) { + if (isRunning() && getShellDialect().equals(type)) { return sc.apply(this); } else { try (var sub = subShell(type).start()) { @@ -140,8 +165,7 @@ public interface ShellControl extends ProcessControl { } ShellControl subShell( - FailableFunction command, - TerminalOpenFunction terminalCommand); + FailableFunction command, TerminalOpenFunction terminalCommand); void executeLine(String command) throws Exception; diff --git a/core/src/main/java/io/xpipe/core/util/XPipeSystemId.java b/core/src/main/java/io/xpipe/core/util/XPipeSystemId.java new file mode 100644 index 00000000..56ce5d54 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/util/XPipeSystemId.java @@ -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()); + } +} diff --git a/dist/changelogs/1.0.0.md b/dist/changelogs/1.0.0.md new file mode 100644 index 00000000..c61ebcc6 --- /dev/null +++ b/dist/changelogs/1.0.0.md @@ -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 \ No newline at end of file diff --git a/dist/licenses/java-annotations.license b/dist/licenses/java-annotations.license new file mode 100644 index 00000000..f433b1a5 --- /dev/null +++ b/dist/licenses/java-annotations.license @@ -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 diff --git a/dist/licenses/java-annotations.properties b/dist/licenses/java-annotations.properties new file mode 100644 index 00000000..784601a7 --- /dev/null +++ b/dist/licenses/java-annotations.properties @@ -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 \ No newline at end of file diff --git a/dist/licenses/jfa.license b/dist/licenses/jfa.license new file mode 100644 index 00000000..f433b1a5 --- /dev/null +++ b/dist/licenses/jfa.license @@ -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 diff --git a/dist/licenses/jfa.properties b/dist/licenses/jfa.properties new file mode 100644 index 00000000..40f52a0f --- /dev/null +++ b/dist/licenses/jfa.properties @@ -0,0 +1,4 @@ +name=Java Foundation Access +version=1.2.0 +license=Apache License 2.0 +link=https://github.com/0x4a616e/jfa \ No newline at end of file diff --git a/dist/licenses/jsystemthemedetector.license b/dist/licenses/jsystemthemedetector.license new file mode 100644 index 00000000..f433b1a5 --- /dev/null +++ b/dist/licenses/jsystemthemedetector.license @@ -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 diff --git a/dist/licenses/jsystemthemedetector.properties b/dist/licenses/jsystemthemedetector.properties new file mode 100644 index 00000000..3c10ec31 --- /dev/null +++ b/dist/licenses/jsystemthemedetector.properties @@ -0,0 +1,4 @@ +name=jSystemThemeDetector +version=3.8 +license=Apache License 2.0 +link=https://github.com/Dansoftowner/jSystemThemeDetector \ No newline at end of file diff --git a/dist/licenses/oshi.license b/dist/licenses/oshi.license new file mode 100644 index 00000000..c8d628e5 --- /dev/null +++ b/dist/licenses/oshi.license @@ -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. diff --git a/dist/licenses/oshi.properties b/dist/licenses/oshi.properties new file mode 100644 index 00000000..e7db3fdf --- /dev/null +++ b/dist/licenses/oshi.properties @@ -0,0 +1,4 @@ +name=oshi +version=6.4.2 +license=MIT License +link=https://github.com/oshi/oshi \ No newline at end of file diff --git a/ext/base/build.gradle b/ext/base/build.gradle index c8d2cbbb..7feaa585 100644 --- a/ext/base/build.gradle +++ b/ext/base/build.gradle @@ -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 += [ diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/BrowseInNativeManagerAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/BrowseInNativeManagerAction.java new file mode 100644 index 00000000..583ecf42 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/BrowseInNativeManagerAction.java @@ -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 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 entries) { + return model.isLocal(); + } + + @Override + public String getName(OpenFileSystemModel model, List 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"; + }; + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/CopyAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/CopyAction.java new file mode 100644 index 00000000..382446d2 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/CopyAction.java @@ -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 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 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 entries) { + return "Copy"; + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/CopyPathAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/CopyPathAction.java new file mode 100644 index 00000000..a1c899c8 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/CopyPathAction.java @@ -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 entries) { + return "Copy location"; + } + + @Override + public Category getCategory() { + return Category.COPY_PASTE; + } + + @Override + public boolean acceptsEmptySelection() { + return true; + } + + @Override + public List getBranchingActions() { + return List.of( + new LeafAction() { + @Override + public String getName(OpenFileSystemModel model, List entries) { + return "Absolute Path"; + } + + @Override + public void execute(OpenFileSystemModel model, List 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 entries) { + return "Absolute Path (Quoted)"; + } + + @Override + public boolean isApplicable(OpenFileSystemModel model, List entries) { + return entries.stream().anyMatch(entry -> entry.getRawFileEntry().getPath().contains(" ")); + } + + @Override + public void execute(OpenFileSystemModel model, List 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 entries) { + return "File Name"; + } + + @Override + public void execute(OpenFileSystemModel model, List 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 entries) { + return "File Name (Quoted)"; + } + + @Override + public boolean isApplicable(OpenFileSystemModel model, List entries) { + return entries.stream().anyMatch(entry -> FileNames.getFileName(entry.getRawFileEntry().getPath()).contains(" ")); + } + + @Override + public void execute(OpenFileSystemModel model, List 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); + } + }); + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/DeleteAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/DeleteAction.java new file mode 100644 index 00000000..e76cea07 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/DeleteAction.java @@ -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 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 entries) { + return new FontIcon("mdi2d-delete"); + } + + @Override + public KeyCombination getShortcut() { + return new KeyCodeCombination(KeyCode.DELETE); + } + + @Override + public String getName(OpenFileSystemModel model, List entries) { + return "Delete"; + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/EditFileAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/EditFileAction.java new file mode 100644 index 00000000..89275b81 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/EditFileAction.java @@ -0,0 +1,41 @@ +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.app.prefs.AppPrefs; +import io.xpipe.app.util.FileOpener; +import javafx.scene.Node; +import org.kordamp.ikonli.javafx.FontIcon; + +import java.util.List; + +public class EditFileAction implements LeafAction { + + @Override + public void execute(OpenFileSystemModel model, List entries) throws Exception { + for (BrowserEntry entry : entries) { + FileOpener.openInTextEditor(entry.getRawFileEntry()); + } + } + + @Override + public Category getCategory() { + return Category.OPEN; + } + + @Override + public Node getIcon(OpenFileSystemModel model, List entries) { + return new FontIcon("mdi2p-pencil"); + } + + @Override + public boolean isApplicable(OpenFileSystemModel model, List entries) { + return entries.stream().noneMatch(entry -> entry.getRawFileEntry().isDirectory()); + } + + @Override + public String getName(OpenFileSystemModel model, List entries) { + return "Edit with " + AppPrefs.get().externalEditor().getValue().toTranslatedString(); + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/FileTypeAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/FileTypeAction.java new file mode 100644 index 00000000..d0f8df9c --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/FileTypeAction.java @@ -0,0 +1,26 @@ +package io.xpipe.ext.base.browser; + +import io.xpipe.app.browser.BrowserEntry; +import io.xpipe.app.browser.OpenFileSystemModel; +import io.xpipe.app.browser.action.BrowserAction; +import io.xpipe.app.browser.icon.BrowserIcons; +import io.xpipe.app.browser.icon.FileType; +import javafx.scene.Node; + +import java.util.List; + +public interface FileTypeAction extends BrowserAction { + + @Override + default boolean isApplicable(OpenFileSystemModel model, List entries) { + var t = getType(); + return entries.stream().allMatch(entry -> t.matches(entry.getRawFileEntry())); + } + + @Override + default Node getIcon(OpenFileSystemModel model, List entries) { + return BrowserIcons.createIcon(getType()).createRegion(); + } + + FileType getType(); +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/JarAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/JarAction.java new file mode 100644 index 00000000..ae2b0441 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/JarAction.java @@ -0,0 +1,41 @@ +package io.xpipe.ext.base.browser; + +import io.xpipe.app.browser.BrowserEntry; +import io.xpipe.app.browser.OpenFileSystemModel; +import io.xpipe.app.browser.icon.FileType; +import io.xpipe.core.process.ShellControl; + +import java.util.List; + +public class JarAction extends JavaAction implements FileTypeAction { + + @Override + public Category getCategory() { + return Category.CUSTOM; + } + + @Override + public boolean isApplicable(OpenFileSystemModel model, List entries) { + return super.isApplicable(model, entries) && FileTypeAction.super.isApplicable(model, entries); + } + + @Override + public boolean isApplicable(OpenFileSystemModel model, BrowserEntry entry) { + return entry.getFileName().endsWith(".jar"); + } + + @Override + protected String createCommand(ShellControl sc, OpenFileSystemModel model, BrowserEntry entry) { + return "java -jar " + entry.getOptionallyQuotedFileName(); + } + + @Override + public String getName(OpenFileSystemModel model, List entries) { + return "java -jar " + filesArgument(entries); + } + + @Override + public FileType getType() { + return FileType.byId("jar"); + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/JavaAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/JavaAction.java new file mode 100644 index 00000000..7da62a50 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/JavaAction.java @@ -0,0 +1,21 @@ +package io.xpipe.ext.base.browser; + +import io.xpipe.app.browser.BrowserEntry; +import io.xpipe.app.browser.OpenFileSystemModel; +import io.xpipe.app.browser.action.ApplicationPathAction; +import io.xpipe.app.browser.action.MultiExecuteAction; + +import java.util.List; + +public abstract class JavaAction extends MultiExecuteAction implements ApplicationPathAction { + + @Override + public String getName(OpenFileSystemModel model, List entries) { + return "Java"; + } + + @Override + public String getExecutable() { + return "java"; + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/NewItemAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/NewItemAction.java new file mode 100644 index 00000000..be09d2a5 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/NewItemAction.java @@ -0,0 +1,94 @@ +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.app.browser.icon.BrowserIcons; +import io.xpipe.app.comp.base.ModalOverlayComp; +import io.xpipe.app.fxcomps.Comp; +import javafx.beans.property.SimpleStringProperty; +import javafx.scene.Node; +import javafx.scene.control.TextField; +import org.kordamp.ikonli.javafx.FontIcon; + +import java.util.List; + +public class NewItemAction implements BrowserAction, BranchAction { + + @Override + public Node getIcon(OpenFileSystemModel model, List entries) { + return new FontIcon("mdi2p-plus-box-outline"); + } + + @Override + public String getName(OpenFileSystemModel model, List entries) { + return "New"; + } + + @Override + public boolean acceptsEmptySelection() { + return true; + } + + @Override + public boolean isApplicable(OpenFileSystemModel model, List entries) { + return entries.size() == 1 && entries.get(0).getRawFileEntry().getPath().equals(model.getCurrentPath().get()); + } + + @Override + public Category getCategory() { + return Category.MUTATION; + } + + @Override + public List getBranchingActions() { + return List.of( + new LeafAction() { + @Override + public String getName(OpenFileSystemModel model, List entries) { + return "File"; + } + + @Override + public void execute(OpenFileSystemModel model, List entries) throws Exception { + var name = new SimpleStringProperty(); + model.getOverlay().setValue(new ModalOverlayComp.OverlayContent("newFile", Comp.of(() -> { + var creationName = new TextField(); + creationName.textProperty().bindBidirectional(name); + return creationName; + }), "finish", () -> { + model.createFileAsync(name.getValue()); + })); + } + + @Override + public Node getIcon(OpenFileSystemModel model, List entries) { + return BrowserIcons.createDefaultFileIcon().createRegion(); + } + }, + new LeafAction() { + @Override + public String getName(OpenFileSystemModel model, List entries) { + return "Directory"; + } + + @Override + public void execute(OpenFileSystemModel model, List entries) throws Exception { + var name = new SimpleStringProperty(); + model.getOverlay().setValue(new ModalOverlayComp.OverlayContent("newDirectory", Comp.of(() -> { + var creationName = new TextField(); + creationName.textProperty().bindBidirectional(name); + return creationName; + }), "finish", () -> { + model.createDirectoryAsync(name.getValue()); + })); + } + @Override + public Node getIcon(OpenFileSystemModel model, List entries) { + return BrowserIcons.createDefaultDirectoryIcon().createRegion(); + } + }); + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenDirectoryAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenDirectoryAction.java new file mode 100644 index 00000000..5aaf9336 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenDirectoryAction.java @@ -0,0 +1,45 @@ +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 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 OpenDirectoryAction implements LeafAction { + + @Override + public void execute(OpenFileSystemModel model, List entries) throws Exception { + model.cd(entries.get(0).getRawFileEntry().getPath()); + } + + @Override + public Category getCategory() { + return Category.OPEN; + } + + @Override + public Node getIcon(OpenFileSystemModel model, List entries) { + return new FontIcon("mdi2f-folder-open"); + } + + @Override + public boolean isApplicable(OpenFileSystemModel model, List entries) { + return entries.size() == 1 && entries.stream().allMatch(entry -> entry.getRawFileEntry().isDirectory()); + } + + @Override + public KeyCombination getShortcut() { + return new KeyCodeCombination(KeyCode.ENTER); + } + + @Override + public String getName(OpenFileSystemModel model, List entries) { + return "Open"; + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenDirectoryInNewTabAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenDirectoryInNewTabAction.java new file mode 100644 index 00000000..708b9ef8 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenDirectoryInNewTabAction.java @@ -0,0 +1,50 @@ +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 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 OpenDirectoryInNewTabAction implements LeafAction { + + @Override + public void execute(OpenFileSystemModel model, List entries) throws Exception { + model.getBrowserModel().openFileSystemAsync(model.getStore().asNeeded(), entries.get(0).getRawFileEntry().getPath()); + } + + @Override + public Category getCategory() { + return Category.OPEN; + } + + @Override + public Node getIcon(OpenFileSystemModel model, List entries) { + return new FontIcon("mdi2f-folder-open-outline"); + } + + @Override + public boolean isApplicable(OpenFileSystemModel model, List entries) { + return entries.size() == 1 && entries.stream().allMatch(entry -> entry.getRawFileEntry().isDirectory()); + } + + @Override + public boolean acceptsEmptySelection() { + return true; + } + + @Override + public KeyCombination getShortcut() { + return new KeyCodeCombination(KeyCode.ENTER); + } + + @Override + public String getName(OpenFileSystemModel model, List entries) { + return "Open in new tab"; + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenFileDefaultAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenFileDefaultAction.java new file mode 100644 index 00000000..52ba759d --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenFileDefaultAction.java @@ -0,0 +1,48 @@ +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.app.util.FileOpener; +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 OpenFileDefaultAction implements LeafAction { + + @Override + public void execute(OpenFileSystemModel model, List entries) throws Exception { + for (var entry : entries) { + FileOpener.openInDefaultApplication(entry.getRawFileEntry()); + } + } + + @Override + public Category getCategory() { + return Category.OPEN; + } + + @Override + public Node getIcon(OpenFileSystemModel model, List entries) { + return new FontIcon("mdi2b-book-open-variant"); + } + + @Override + public boolean isApplicable(OpenFileSystemModel model, List entries) { + return entries.stream().noneMatch(entry -> entry.getRawFileEntry().isDirectory()); + } + + @Override + public KeyCombination getShortcut() { + return new KeyCodeCombination(KeyCode.ENTER); + } + + @Override + public String getName(OpenFileSystemModel model, List entries) { + return "Open"; + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenFileWithAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenFileWithAction.java new file mode 100644 index 00000000..be3c3d5b --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenFileWithAction.java @@ -0,0 +1,73 @@ +package io.xpipe.ext.base.browser; + +import com.sun.jna.platform.win32.Shell32; +import com.sun.jna.platform.win32.WinUser; +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 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 OpenFileWithAction implements LeafAction { + + @Override + public void execute(OpenFileSystemModel model, List entries) throws Exception { + switch (OsType.getLocal()) { + case OsType.Windows windows -> { + Shell32.INSTANCE.ShellExecute( + null, + "open", + "rundll32.exe", + "shell32.dll,OpenAs_RunDLL " + + entries.get(0).getRawFileEntry().getPath(), + null, + WinUser.SW_SHOWNORMAL); + } + case OsType.Linux linux -> { + ShellControl sc = model.getFileSystem().getShell().get(); + ShellDialect d = sc.getShellDialect(); + sc.executeSimpleCommand("mimeopen -a " + d.fileArgument(entries.get(0).getRawFileEntry().getPath())); + } + case OsType.MacOs macOs -> { + throw new UnsupportedOperationException(); + } + } + } + + @Override + public Category getCategory() { + return Category.OPEN; + } + + @Override + public Node getIcon(OpenFileSystemModel model, List entries) { + return new FontIcon("mdi2b-book-open-page-variant-outline"); + } + + @Override + public boolean isApplicable(OpenFileSystemModel model, List entries) { + var os = model.getFileSystem().getShell(); + return os.isPresent() + && !os.get().getOsType().equals(OsType.MACOS) + && entries.size() == 1 + && entries.stream().noneMatch(entry -> entry.getRawFileEntry().isDirectory()); + } + + @Override + public KeyCombination getShortcut() { + return new KeyCodeCombination(KeyCode.ENTER, KeyCombination.SHIFT_DOWN); + } + + @Override + public String getName(OpenFileSystemModel model, List entries) { + return "Open with ..."; + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenNativeFileDetailsAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenNativeFileDetailsAction.java new file mode 100644 index 00000000..82879897 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenNativeFileDetailsAction.java @@ -0,0 +1,69 @@ +package io.xpipe.ext.base.browser; + +import io.xpipe.app.browser.BrowserEntry; +import io.xpipe.app.browser.OpenFileSystemModel; +import io.xpipe.app.browser.action.LeafAction; +import io.xpipe.core.impl.FileNames; +import io.xpipe.core.process.OsType; +import io.xpipe.core.process.ShellControl; +import io.xpipe.core.process.ShellDialects; + +import java.util.List; + +public class OpenNativeFileDetailsAction implements LeafAction { + + @Override + public void execute(OpenFileSystemModel model, List entries) throws Exception { + ShellControl sc = model.getFileSystem().getShell().get(); + for (BrowserEntry entry : entries) { + var e = entry.getRawFileEntry().getPath(); + switch (OsType.getLocal()) { + case OsType.Windows windows -> { + var content = String.format( + """ + $shell = New-Object -ComObject Shell.Application; $shell.NameSpace('%s').ParseName('%s').InvokeVerb('Properties') + """, + FileNames.getParent(e), FileNames.getFileName(e)); + try (var sub = sc.enforcedDialect(ShellDialects.POWERSHELL).start()) { + sub.command(content).notComplex().execute(); + } + } + case OsType.Linux linux -> { + var dbus = String.format(""" + dbus-send --session --print-reply --dest=org.freedesktop.FileManager1 --type=method_call /org/freedesktop/FileManager1 org.freedesktop.FileManager1.ShowItemProperties array:string:"file://%s" string:"" + """, entry.getRawFileEntry().getPath()); + sc.executeSimpleCommand(dbus); + } + case OsType.MacOs macOs -> { + sc.osascriptCommand(String.format( + """ + set fileEntry to (POSIX file "%s") as text + tell application "Finder" to open information window of file fileEntry + """, + entry.getRawFileEntry().getPath())).execute(); + } + } + } + } + + @Override + public boolean acceptsEmptySelection() { + return true; + } + + @Override + public Category getCategory() { + return Category.NATIVE; + } + + @Override + public boolean isApplicable(OpenFileSystemModel model, List entries) { + var sc = model.getFileSystem().getShell(); + return sc.isPresent() && !sc.get().getOsType().equals(OsType.WINDOWS); + } + + @Override + public String getName(OpenFileSystemModel model, List entries) { + return "Show details"; + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenTerminalAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenTerminalAction.java new file mode 100644 index 00000000..76253928 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenTerminalAction.java @@ -0,0 +1,53 @@ +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.app.prefs.AppPrefs; +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 OpenTerminalAction implements LeafAction { + + @Override + public void execute(OpenFileSystemModel model, List entries) throws Exception { + if (entries.size() == 0) { + model.openTerminalAsync(model.getCurrentDirectory().getPath()); + return; + } + + for (var entry : entries) { + model.openTerminalAsync(entry.getRawFileEntry().getPath()); + } + } + + @Override + public Category getCategory() { + return Category.OPEN; + } + + @Override + public Node getIcon(OpenFileSystemModel model, List entries) { + return new FontIcon("mdi2c-console"); + } + + @Override + public boolean isApplicable(OpenFileSystemModel model, List entries) { + return entries.stream().allMatch(entry -> entry.getRawFileEntry().isDirectory()); + } + + @Override + public KeyCombination getShortcut() { + return new KeyCodeCombination(KeyCode.T, KeyCombination.SHORTCUT_DOWN); + } + + @Override + public String getName(OpenFileSystemModel model, List entries) { + return "Open in " + AppPrefs.get().terminalType().getValue().toTranslatedString(); + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/PasteAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/PasteAction.java new file mode 100644 index 00000000..bf3bc178 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/PasteAction.java @@ -0,0 +1,63 @@ +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 PasteAction implements LeafAction { + + @Override + public void execute(OpenFileSystemModel model, List entries) throws Exception { + var clipboard = BrowserClipboard.retrieveCopy(); + if (clipboard == null) { + return; + } + + var target = entries.size() == 1 && entries.get(0).getRawFileEntry().isDirectory() ? entries.get(0).getRawFileEntry() : model.getCurrentDirectory(); + var files = clipboard.getEntries(); + model.dropFilesIntoAsync(target, files, true); + } + + @Override + public Category getCategory() { + return Category.COPY_PASTE; + } + + @Override + public Node getIcon(OpenFileSystemModel model, List entries) { + return new FontIcon("mdi2c-content-paste"); + } + + @Override + public boolean isApplicable(OpenFileSystemModel model, List entries) { + return entries.size() < 2 && entries.stream().allMatch(entry -> entry.getRawFileEntry().isDirectory()); + } + + @Override + public boolean isActive(OpenFileSystemModel model, List entries) { + return BrowserClipboard.retrieveCopy() != null; + } + + @Override + public KeyCombination getShortcut() { + return new KeyCodeCombination(KeyCode.V, KeyCombination.SHORTCUT_DOWN); + } + + @Override + public boolean acceptsEmptySelection() { + return true; + } + + @Override + public String getName(OpenFileSystemModel model, List entries) { + return "Paste"; + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/RenameAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/RenameAction.java new file mode 100644 index 00000000..a9e7ff2d --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/RenameAction.java @@ -0,0 +1,45 @@ +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 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 RenameAction implements LeafAction { + + @Override + public void execute(OpenFileSystemModel model, List entries) throws Exception { + model.getFileList().getEditing().setValue(entries.get(0)); + } + + @Override + public Category getCategory() { + return Category.MUTATION; + } + + @Override + public Node getIcon(OpenFileSystemModel model, List entries) { + return new FontIcon("mdi2r-rename-box"); + } + + @Override + public boolean isApplicable(OpenFileSystemModel model, List entries) { + return entries.size() == 1; + } + + @Override + public KeyCombination getShortcut() { + return new KeyCodeCombination(KeyCode.R, KeyCombination.SHORTCUT_DOWN); + } + + @Override + public String getName(OpenFileSystemModel model, List entries) { + return "Rename"; + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/RunAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/RunAction.java new file mode 100644 index 00000000..e924cd9b --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/RunAction.java @@ -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.MultiExecuteAction; +import io.xpipe.core.process.OsType; +import io.xpipe.core.process.ShellControl; +import io.xpipe.core.store.FileSystem; +import javafx.scene.Node; +import org.kordamp.ikonli.javafx.FontIcon; + +import java.util.List; + +public class RunAction extends MultiExecuteAction { + + private 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(); + if (os.equals(OsType.WINDOWS) && List.of("exe", "bat", "ps1", "cmd").stream().anyMatch(s -> e.getPath().endsWith(s))) { + return true; + } + + if (List.of("sh", "command").stream().anyMatch(s -> e.getPath().endsWith(s))) { + return true; + } + + return false; + } + + @Override + public Category getCategory() { + return Category.CUSTOM; + } + + @Override + public Node getIcon(OpenFileSystemModel model, List entries) { + return new FontIcon("mdi2p-play"); + } + + @Override + public String getName(OpenFileSystemModel model, List entries) { + return "Run"; + } + + @Override + public boolean isApplicable(OpenFileSystemModel model, List entries) { + return entries.stream().allMatch(entry -> isExecutable(entry.getRawFileEntry())); + } + + @Override + protected String createCommand(ShellControl sc, OpenFileSystemModel model, BrowserEntry entry) { + return sc.getShellDialect().runScript(entry.getFileName()); + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/UnzipAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/UnzipAction.java new file mode 100644 index 00000000..9ef22e35 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/UnzipAction.java @@ -0,0 +1,37 @@ +package io.xpipe.ext.base.browser; + +import io.xpipe.app.browser.BrowserEntry; +import io.xpipe.app.browser.OpenFileSystemModel; +import io.xpipe.app.browser.action.ExecuteApplicationAction; +import io.xpipe.core.impl.FileNames; +import io.xpipe.core.process.OsType; + +import java.util.List; + +public class UnzipAction extends ExecuteApplicationAction { + + @Override + public String getExecutable() { + return "unzip"; + } + + @Override + public boolean isApplicable(OpenFileSystemModel model, BrowserEntry entry) { + return entry.getRawFileEntry().getPath().endsWith(".zip") && !OsType.getLocal().equals(OsType.WINDOWS); + } + + @Override + protected String createCommand(OpenFileSystemModel model, BrowserEntry entry) { + return "unzip -o " + entry.getOptionallyQuotedFileName() + " -d " + FileNames.quoteIfNecessary(FileNames.getBaseName(entry.getFileName())); + } + + @Override + public Category getCategory() { + return Category.CUSTOM; + } + + @Override + public String getName(OpenFileSystemModel model, List entries) { + return "unzip [...]"; + } +} diff --git a/ext/base/src/main/java/module-info.java b/ext/base/src/main/java/module-info.java index 0cd23a0f..453779eb 100644 --- a/ext/base/src/main/java/module-info.java +++ b/ext/base/src/main/java/module-info.java @@ -1,3 +1,4 @@ +import io.xpipe.app.browser.action.BrowserAction; import io.xpipe.app.ext.ActionProvider; import io.xpipe.app.ext.DataSourceProvider; import io.xpipe.app.ext.DataSourceTarget; @@ -5,6 +6,7 @@ import io.xpipe.app.ext.DataStoreProvider; import io.xpipe.ext.base.*; import io.xpipe.ext.base.actions.*; import io.xpipe.ext.base.apps.*; +import io.xpipe.ext.base.browser.*; open module io.xpipe.ext.base { exports io.xpipe.ext.base; @@ -20,7 +22,28 @@ open module io.xpipe.ext.base { requires static net.synedra.validatorfx; requires static io.xpipe.app; requires org.apache.commons.lang3; + requires org.kordamp.ikonli.javafx; + requires com.sun.jna; + requires com.sun.jna.platform; + provides BrowserAction with + OpenFileDefaultAction, + OpenFileWithAction, + OpenDirectoryAction, + OpenDirectoryInNewTabAction, + OpenTerminalAction, + OpenNativeFileDetailsAction, + BrowseInNativeManagerAction, + EditFileAction, + RunAction, + CopyAction, + CopyPathAction, + PasteAction, + NewItemAction, + RenameAction, + DeleteAction, + UnzipAction, + JarAction; provides ActionProvider with DeleteStoreChildrenAction, AddStoreAction, diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/translations_en.properties b/ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/translations_en.properties index cb7b647f..99f99d29 100644 --- a/ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/translations_en.properties +++ b/ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/translations_en.properties @@ -9,6 +9,8 @@ destination=Destination configuration=Configuration selectOutput=Select Output options=Options +newFile=New file +newDirectory=New directory copyShareLink=Copy share link selectStore=Select Store saveSource=Save for later diff --git a/gradle/gradle_scripts/atlantafx-base-1.2.1.jar b/gradle/gradle_scripts/atlantafx-base-1.2.1.jar new file mode 100644 index 00000000..91e1b140 Binary files /dev/null and b/gradle/gradle_scripts/atlantafx-base-1.2.1.jar differ diff --git a/gradle/gradle_scripts/atlantafx-styles-1.2.1.jar b/gradle/gradle_scripts/atlantafx-styles-1.2.1.jar new file mode 100644 index 00000000..3b47eea2 Binary files /dev/null and b/gradle/gradle_scripts/atlantafx-styles-1.2.1.jar differ diff --git a/gradle/gradle_scripts/jSystemThemeDetector-3.8.jar b/gradle/gradle_scripts/jSystemThemeDetector-3.8.jar new file mode 100644 index 00000000..a8ac11f5 Binary files /dev/null and b/gradle/gradle_scripts/jSystemThemeDetector-3.8.jar differ diff --git a/gradle/gradle_scripts/versioncompare.gradle b/gradle/gradle_scripts/versioncompare.gradle new file mode 100644 index 00000000..b8a249e7 --- /dev/null +++ b/gradle/gradle_scripts/versioncompare.gradle @@ -0,0 +1,19 @@ +dependencies { + implementation files("$buildDir/generated-modules/versioncompare-1.5.0.jar") +} + +addDependenciesModuleInfo { + overwriteExistingFiles = true + jdepsExtraArgs = ['-q'] + outputDirectory = file("$buildDir/generated-modules") + modules { + module { + artifact "io.github.g00fy2:versioncompare:1.5.0" + moduleInfoSource = ''' + module versioncompare { + exports io.github.g00fy2.versioncompare; + } + ''' + } + } +} diff --git a/version b/version index 0e97bf48..afaf360d 100644 --- a/version +++ b/version @@ -1 +1 @@ -0.5.41 \ No newline at end of file +1.0.0 \ No newline at end of file