diff --git a/app/src/main/java/io/xpipe/app/browser/FileBrowserBreadcrumbBar.java b/app/src/main/java/io/xpipe/app/browser/FileBrowserBreadcrumbBar.java new file mode 100644 index 00000000..12438bb0 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/FileBrowserBreadcrumbBar.java @@ -0,0 +1,65 @@ +package io.xpipe.app.browser; + +import atlantafx.base.controls.Breadcrumbs; +import atlantafx.base.theme.Styles; +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.layout.Region; +import javafx.util.Callback; + +public class FileBrowserBreadcrumbBar extends SimpleComp { + + private final OpenFileSystemModel model; + + public FileBrowserBreadcrumbBar(OpenFileSystemModel model) { + this.model = model; + } + + @Override + protected Region createSimple() { + Callback, ButtonBase> crumbFactory = crumb -> { + var btn = new Button(FileNames.getFileName(crumb.getValue()), null); + btn.getStyleClass().add(Styles.FLAT); + 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 elements = FileNames.splitHierarchy(val); + Breadcrumbs.BreadCrumbItem items = Breadcrumbs.buildTreeModel(elements.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.getValue()).ifPresent(s -> { + model.cd(s); + }); + }); + + return breadcrumbs; + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/FileBrowserModel.java b/app/src/main/java/io/xpipe/app/browser/FileBrowserModel.java index 1cd3028e..b8cb678e 100644 --- a/app/src/main/java/io/xpipe/app/browser/FileBrowserModel.java +++ b/app/src/main/java/io/xpipe/app/browser/FileBrowserModel.java @@ -32,7 +32,6 @@ public class FileBrowserModel { public static final FileBrowserModel DEFAULT = new FileBrowserModel(Mode.BROWSER); private final Mode mode; - private final ObservableList selectedFiles = FXCollections.observableArrayList(); @Setter private Consumer> onFinish; @@ -46,6 +45,7 @@ public class FileBrowserModel { throw new IllegalStateException(); } + var selectedFiles = openFileSystems.get(0).getFileList().getSelected(); closeFileSystem(openFileSystems.get(0)); if (selectedFiles.size() == 0) { diff --git a/app/src/main/java/io/xpipe/app/browser/FileBrowserStatusBarComp.java b/app/src/main/java/io/xpipe/app/browser/FileBrowserStatusBarComp.java index c15ddffa..5fb9b625 100644 --- a/app/src/main/java/io/xpipe/app/browser/FileBrowserStatusBarComp.java +++ b/app/src/main/java/io/xpipe/app/browser/FileBrowserStatusBarComp.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; @@ -54,10 +56,7 @@ public class FileBrowserStatusBarComp extends SimpleComp { AppFont.small(bar); // Use status bar as an extension of file list - bar.setOnMouseClicked(event -> { - model.getFileList().getSelected().clear(); - event.consume(); - }); + new ContextMenuAugment<>(false, () -> new FileContextMenu(model,true)).augment(new SimpleCompStructure<>(bar)); return bar; } diff --git a/app/src/main/java/io/xpipe/app/browser/FileContextMenu.java b/app/src/main/java/io/xpipe/app/browser/FileContextMenu.java index 6ce71f1d..be51880d 100644 --- a/app/src/main/java/io/xpipe/app/browser/FileContextMenu.java +++ b/app/src/main/java/io/xpipe/app/browser/FileContextMenu.java @@ -5,17 +5,12 @@ 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.util.BusyProperty; -import io.xpipe.app.util.ThreadHelper; +import io.xpipe.app.core.AppFont; import javafx.collections.FXCollections; import javafx.scene.control.ContextMenu; import javafx.scene.control.Menu; -import javafx.scene.control.MenuItem; import javafx.scene.control.SeparatorMenuItem; -import java.util.List; -import java.util.function.UnaryOperator; - final class FileContextMenu extends ContextMenu { private final OpenFileSystemModel model; @@ -29,6 +24,8 @@ final class FileContextMenu extends ContextMenu { } private void createMenu() { + AppFont.normal(this.getStyleableNode()); + var selected = empty ? FXCollections.observableArrayList() : model.getFileList().getSelected(); for (BrowserAction.Category cat : BrowserAction.Category.values()) { @@ -53,7 +50,7 @@ final class FileContextMenu extends ContextMenu { for (BrowserAction a : all) { if (a instanceof LeafAction la) { - getItems().add(toItem(la, selected, s -> s)); + getItems().add(la.toItem(model, selected, s -> s)); } if (a instanceof BranchAction la) { @@ -62,7 +59,7 @@ final class FileContextMenu extends ContextMenu { if (!sub.isApplicable(model, selected)) { continue; } - m.getItems().add(toItem(sub, selected, s -> "... " + s)); + m.getItems().add(sub.toItem(model, selected, s -> s)); } var graphic = a.getIcon(model, selected); if (graphic != null) { @@ -74,26 +71,4 @@ final class FileContextMenu extends ContextMenu { } } } - - private MenuItem toItem(LeafAction a, List selected, UnaryOperator nameFunc) { - var mi = new MenuItem(nameFunc.apply(a.getName(model, selected))); - mi.setOnAction(event -> { - ThreadHelper.runFailableAsync(() -> { - BusyProperty.execute(model.getBusy(), () -> { - a.execute(model, selected); - }); - }); - event.consume(); - }); - if (a.getShortcut() != null) { - mi.setAccelerator(a.getShortcut()); - } - var graphic = a.getIcon(model, selected); - if (graphic != null) { - mi.setGraphic(graphic); - } - mi.setMnemonicParsing(false); - mi.setDisable(!a.isActive(model, selected)); - return mi; - } } diff --git a/app/src/main/java/io/xpipe/app/browser/FileListComp.java b/app/src/main/java/io/xpipe/app/browser/FileListComp.java index 72c6601b..5b465f06 100644 --- a/app/src/main/java/io/xpipe/app/browser/FileListComp.java +++ b/app/src/main/java/io/xpipe/app/browser/FileListComp.java @@ -150,7 +150,6 @@ final class FileListComp extends AnchorPane { .getPath())) .toList(); fileList.getSelected().setAll(toSelect); - fileList.getFileSystemModel().getBrowserModel().getSelectedFiles().setAll(toSelect); Platform.runLater(() -> { var toUnselect = table.getSelectionModel().getSelectedItems().stream() @@ -380,7 +379,7 @@ final class FileListComp extends AnchorPane { public FilenameCell(Property editing) { editing.addListener((observable, oldValue, newValue) -> { if (getTableRow().getItem() != null && getTableRow().getItem().equals(newValue)) { - textField.requestFocus(); + PlatformThread.runLaterIfNeeded(() -> textField.requestFocus()); } }); 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 1b8f6a3d..56c6a972 100644 --- a/app/src/main/java/io/xpipe/app/browser/OpenFileSystemComp.java +++ b/app/src/main/java/io/xpipe/app/browser/OpenFileSystemComp.java @@ -1,24 +1,31 @@ package io.xpipe.app.browser; import atlantafx.base.controls.Spacer; +import io.xpipe.app.browser.action.BranchAction; +import io.xpipe.app.browser.action.BrowserAction; +import io.xpipe.app.comp.base.AlertOverlayComp; +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.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.input.KeyCombination; -import javafx.scene.layout.*; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; import org.kordamp.ikonli.feather.Feather; import org.kordamp.ikonli.javafx.FontIcon; +import java.util.function.UnaryOperator; + import static io.xpipe.app.browser.FileListModel.PREDICATE_NOT_HIDDEN; import static io.xpipe.app.util.Controls.iconButton; @@ -32,7 +39,13 @@ public class OpenFileSystemComp extends SimpleComp { @Override protected Region createSimple() { - var creatingProperty = new SimpleBooleanProperty(); + var alertOverlay = new AlertOverlayComp( + Comp.of(() -> createContent()), + model.getOverlay()); + return alertOverlay.createRegion(); + } + + private Region createContent() { var backBtn = iconButton(Feather.ARROW_LEFT, false); backBtn.setOnAction(e -> model.back()); backBtn.disableProperty().bind(model.getHistory().canGoBackProperty().not()); @@ -57,30 +70,23 @@ public class OpenFileSystemComp extends SimpleComp { 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); + var addBtn = new MenuButton(null, new FontIcon("mdmz-plus")); + var s = model.getFileList().getSelected(); + var action = (BranchAction) BrowserAction.ALL.stream().filter(browserAction -> browserAction.getName(model, s).equals("New")).findFirst().orElseThrow(); + action.getBranchingActions().forEach(action1 -> { + addBtn.getItems().add(action1.toItem(model, s, UnaryOperator.identity())); }); - addBtn.disableProperty().bind(PlatformThread.sync(model.getNoDirectory())); - Shortcuts.addShortcut(addBtn, new KeyCodeCombination(KeyCode.PLUS)); var filter = new FileFilterComp(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.get(), - refreshBtn, - terminalBtn, - addBtn - ); + topBar.getItems() + .setAll(backBtn, forthBtn, new Spacer(10), new FileBrowserBreadcrumbBar(model).createRegion(), filter.get(), refreshBtn, terminalBtn, addBtn); // ~ @@ -93,56 +99,6 @@ public class OpenFileSystemComp extends SimpleComp { 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 146fb555..60560347 100644 --- a/app/src/main/java/io/xpipe/app/browser/OpenFileSystemModel.java +++ b/app/src/main/java/io/xpipe/app/browser/OpenFileSystemModel.java @@ -2,6 +2,7 @@ package io.xpipe.app.browser; +import io.xpipe.app.comp.base.AlertOverlayComp; import io.xpipe.app.core.AppCache; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.storage.DataStorage; @@ -43,6 +44,7 @@ public final class OpenFileSystemModel { 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<>(); public OpenFileSystemModel(FileBrowserModel browserModel) { this.browserModel = browserModel; @@ -239,7 +241,7 @@ public final class OpenFileSystemModel { } public void createFileAsync(String name) { - if (name.isBlank()) { + if (name == null || name.isBlank()) { return; } 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 index 3e6a9903..6e257c1d 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/LeafAction.java +++ b/app/src/main/java/io/xpipe/app/browser/action/LeafAction.java @@ -2,11 +2,37 @@ package io.xpipe.app.browser.action; import io.xpipe.app.browser.FileBrowserEntry; 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/icon/FileIcons.java b/app/src/main/java/io/xpipe/app/browser/icon/FileBrowserIcons.java similarity index 60% rename from app/src/main/java/io/xpipe/app/browser/icon/FileIcons.java rename to app/src/main/java/io/xpipe/app/browser/icon/FileBrowserIcons.java index 1057cb59..36f760d3 100644 --- a/app/src/main/java/io/xpipe/app/browser/icon/FileIcons.java +++ b/app/src/main/java/io/xpipe/app/browser/icon/FileBrowserIcons.java @@ -4,8 +4,13 @@ import io.xpipe.app.fxcomps.impl.PrettyImageComp; import io.xpipe.core.store.FileSystem; import javafx.beans.property.SimpleStringProperty; -public class FileIcons { - +public class FileBrowserIcons { + 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); } diff --git a/app/src/main/java/io/xpipe/app/comp/base/AlertOverlayComp.java b/app/src/main/java/io/xpipe/app/comp/base/AlertOverlayComp.java new file mode 100644 index 00000000..65217559 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/comp/base/AlertOverlayComp.java @@ -0,0 +1,99 @@ +package io.xpipe.app.comp.base; + +import atlantafx.base.theme.Styles; +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.beans.value.ObservableValue; +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 AlertOverlayComp extends SimpleComp { + + + public AlertOverlayComp(Comp background, Property overlayContent) { + this.background = background; + this.overlayContent = overlayContent; + } + + @Value + public static class OverlayContent { + + ObservableValue title; + Comp content; + Runnable onFinish; + } + + private final Comp background; + private final Property overlayContent; + + @Override + protected Region createSimple() { + var bgRegion = background.createRegion(); + var pane = new StackPane(bgRegion); + pane.setPickOnBounds(false); + PlatformThread.sync(overlayContent).addListener((observable, oldValue, newValue) -> { + if (oldValue != null) { + pane.getChildren().remove(1); + } + + if (newValue != null) { + var r = newValue.content.createRegion(); + + var finishButton = new Button("Finish"); + Styles.toggleStyleClass(finishButton, Styles.FLAT); + finishButton.setOnAction(event -> { + newValue.onFinish.run(); + overlayContent.setValue(null); + }); + + var buttonBar = new ButtonBar(); + buttonBar.getButtons().addAll(finishButton); + var box = new VBox(r, buttonBar); + box.setSpacing(15); + box.setPadding(new Insets(15)); + var tp = new TitledPane(newValue.title.getValue(), box); + tp.setMaxWidth(400); + tp.setCollapsible(false); + tp.getStyleClass().add("elevated-3"); + + 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.setOnMouseClicked(event -> { + if (overlayContent.getValue() != null) { + overlayContent.setValue(null); + } + }); + stack.setAlignment(Pos.CENTER); + close.maxWidthProperty().bind(tp.widthProperty()); + close.maxHeightProperty().bind(tp.heightProperty()); + + pane.getChildren().add(stack); + } + }); + return pane; + } +} 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 index 02534df9..ac9fd9cd 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/augment/ContextMenuAugment.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/augment/ContextMenuAugment.java @@ -22,20 +22,18 @@ public class ContextMenuAugment> implements Augment { + if (currentContextMenu != null && currentContextMenu.isShowing()) { + currentContextMenu.hide(); + currentContextMenu = null; + } + if ((showOnPrimaryButton && event.getButton() == MouseButton.PRIMARY) || (!showOnPrimaryButton && event.getButton() == MouseButton.SECONDARY)) { - if (currentContextMenu != null && currentContextMenu.isShowing()) { - currentContextMenu.hide(); - currentContextMenu = null; - } - - if (event.getButton() == MouseButton.SECONDARY) { - var cm = contextMenu.get(); - if (cm != null) { - cm.setAutoHide(true); - cm.show(r, event.getScreenX(), event.getScreenY()); - currentContextMenu = cm; - } + 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/resources/io/xpipe/app/resources/style/browser.css b/app/src/main/resources/io/xpipe/app/resources/style/browser.css index 443f6550..c8ac0b39 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 @@ -81,6 +81,10 @@ -fx-border-insets: 0px; } +.browser .breadcrumbs .button { +-fx-padding: 0; +} + .browser .context-menu .accelerator-text { -fx-padding: 3px 0px 3px 50px; } 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 8ac5b6b5..b28b8f1c 100644 --- a/core/src/main/java/io/xpipe/core/impl/FileNames.java +++ b/core/src/main/java/io/xpipe/core/impl/FileNames.java @@ -1,5 +1,6 @@ package io.xpipe.core.impl; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -49,6 +50,26 @@ 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; 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 index dbc5db1a..a7be615c 100644 --- 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 @@ -3,7 +3,7 @@ package io.xpipe.ext.base.browser; import io.xpipe.app.browser.FileBrowserEntry; import io.xpipe.app.browser.OpenFileSystemModel; import io.xpipe.app.browser.action.BrowserAction; -import io.xpipe.app.browser.icon.FileIcons; +import io.xpipe.app.browser.icon.FileBrowserIcons; import io.xpipe.app.browser.icon.FileType; import javafx.scene.Node; @@ -19,7 +19,7 @@ public interface FileTypeAction extends BrowserAction { @Override default Node getIcon(OpenFileSystemModel model, List entries) { - return FileIcons.createIcon(getType()).createRegion(); + return FileBrowserIcons.createIcon(getType()).createRegion(); } FileType getType(); 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 index a26ac5be..31f10e56 100644 --- 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 @@ -5,7 +5,13 @@ 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.FileBrowserIcons; +import io.xpipe.app.comp.base.AlertOverlayComp; +import io.xpipe.app.core.AppI18n; +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; @@ -19,7 +25,7 @@ public class NewItemAction implements BrowserAction, BranchAction { @Override public String getName(OpenFileSystemModel model, List entries) { - return "Create new"; + return "New"; } @Override @@ -43,21 +49,46 @@ public class NewItemAction implements BrowserAction, BranchAction { new LeafAction() { @Override public String getName(OpenFileSystemModel model, List entries) { - return "file"; + return "File"; } @Override public void execute(OpenFileSystemModel model, List entries) throws Exception { + var name = new SimpleStringProperty(); + model.getOverlay().setValue(new AlertOverlayComp.OverlayContent(AppI18n.observable("newFile"), Comp.of(() -> { + var creationName = new TextField(); + creationName.textProperty().bindBidirectional(name); + return creationName; + }), () -> { + model.createFileAsync(name.getValue()); + })); + } + + @Override + public Node getIcon(OpenFileSystemModel model, List entries) { + return FileBrowserIcons.createDefaultFileIcon().createRegion(); } }, new LeafAction() { @Override public String getName(OpenFileSystemModel model, List entries) { - return "directory"; + return "Directory"; } @Override public void execute(OpenFileSystemModel model, List entries) throws Exception { + var name = new SimpleStringProperty(); + model.getOverlay().setValue(new AlertOverlayComp.OverlayContent(AppI18n.observable("newDirectory"), Comp.of(() -> { + var creationName = new TextField(); + creationName.textProperty().bindBidirectional(name); + return creationName; + }), () -> { + model.createDirectoryAsync(name.getValue()); + })); + } + @Override + public Node getIcon(OpenFileSystemModel model, List entries) { + return FileBrowserIcons.createDefaultDirectoryIcon().createRegion(); } }); } 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 index 10be38e7..b1352b73 100644 --- 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 @@ -16,11 +16,21 @@ 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 boolean acceptsEmptySelection() { + return true; + } + @Override public Category getCategory() { return Category.OPEN; 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..6c875f5f --- /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.FileBrowserEntry; +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/module-info.java b/ext/base/src/main/java/module-info.java index 8f519955..4dc9f5a5 100644 --- a/ext/base/src/main/java/module-info.java +++ b/ext/base/src/main/java/module-info.java @@ -38,6 +38,7 @@ open module io.xpipe.ext.base { CopyPathAction, PasteAction, NewItemAction, + RenameAction, DeleteAction, UnzipAction, JarAction; 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