diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserNavBar.java b/app/src/main/java/io/xpipe/app/browser/BrowserNavBar.java index 3fb86d9e..9c9d14a8 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserNavBar.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserNavBar.java @@ -3,6 +3,7 @@ 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; @@ -28,13 +29,18 @@ public class BrowserNavBar extends SimpleComp { var changed = model.cd(newValue); changed.ifPresent(path::set); }); - var pathBar = new TextFieldComp(path, true).createRegion(); + 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 FileBrowserBreadcrumbBar(model) 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 5fb9b625..0acb62bc 100644 --- a/app/src/main/java/io/xpipe/app/browser/FileBrowserStatusBarComp.java +++ b/app/src/main/java/io/xpipe/app/browser/FileBrowserStatusBarComp.java @@ -53,6 +53,10 @@ 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 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 be51880d..92ed3731 100644 --- a/app/src/main/java/io/xpipe/app/browser/FileContextMenu.java +++ b/app/src/main/java/io/xpipe/app/browser/FileContextMenu.java @@ -26,20 +26,26 @@ final class FileContextMenu extends ContextMenu { private void createMenu() { AppFont.normal(this.getStyleableNode()); - var selected = empty ? FXCollections.observableArrayList() : model.getFileList().getSelected(); + var selected = empty || model.getFileList().getSelected().isEmpty() + ? FXCollections.observableArrayList( + new FileBrowserEntry(model.getCurrentDirectory(), model.getFileList(), false)) + : model.getFileList().getSelected(); 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; - } + var all = BrowserAction.ALL.stream() + .filter(browserAction -> browserAction.getCategory() == cat) + .filter(browserAction -> { + if (!browserAction.isApplicable(model, selected)) { + return false; + } - if (!browserAction.acceptsEmptySelection() && selected.isEmpty()) { - return false; - } + if (!browserAction.acceptsEmptySelection() && empty) { + return false; + } - return true; - }).toList(); + return true; + }) + .toList(); if (all.size() == 0) { continue; } 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 5b465f06..91591a87 100644 --- a/app/src/main/java/io/xpipe/app/browser/FileListComp.java +++ b/app/src/main/java/io/xpipe/app/browser/FileListComp.java @@ -3,7 +3,6 @@ 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; @@ -90,7 +89,6 @@ final class FileListComp extends AnchorPane { 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 -> @@ -116,7 +114,6 @@ final class FileListComp extends AnchorPane { FXCollections.sort(param.getItems(), us); return true; }); - table.getSortOrder().add(filenameCol); table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); filenameCol.minWidthProperty().bind(table.widthProperty().multiply(0.5)); @@ -253,7 +250,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); }); diff --git a/app/src/main/java/io/xpipe/app/browser/FileListCompEntry.java b/app/src/main/java/io/xpipe/app/browser/FileListCompEntry.java index e39bef36..da11dbcf 100644 --- a/app/src/main/java/io/xpipe/app/browser/FileListCompEntry.java +++ b/app/src/main/java/io/xpipe/app/browser/FileListCompEntry.java @@ -131,6 +131,7 @@ public class FileListCompEntry { public void startDrag(MouseEvent event) { if (item == null) { + row.startFullDrag(); return; } @@ -191,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/OpenFileSystemComp.java b/app/src/main/java/io/xpipe/app/browser/OpenFileSystemComp.java index 749af2a2..b6bea9bc 100644 --- a/app/src/main/java/io/xpipe/app/browser/OpenFileSystemComp.java +++ b/app/src/main/java/io/xpipe/app/browser/OpenFileSystemComp.java @@ -1,11 +1,11 @@ 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.ModalOverlayComp; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.SimpleComp; +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.geometry.Insets; @@ -21,8 +21,6 @@ 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; @@ -60,19 +58,15 @@ public class OpenFileSystemComp extends SimpleComp { e -> model.openTerminalAsync(model.getCurrentPath().get())); terminalBtn.disableProperty().bind(PlatformThread.sync(model.getNoDirectory())); - 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())); - }); + var menuButton = new MenuButton(null, new FontIcon("mdral-folder_open")); + new ContextMenuAugment<>(true, () -> new FileContextMenu(model, true)).augment(new SimpleCompStructure<>(menuButton)); 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), new BrowserNavBar(model).createRegion(), filter.get(), refreshBtn, terminalBtn, addBtn); + .setAll(backBtn, forthBtn, new Spacer(10), new BrowserNavBar(model).createRegion(), filter.get(), refreshBtn, terminalBtn, menuButton); // ~ 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 5f66744d..71ff74e2 100644 --- a/app/src/main/java/io/xpipe/app/browser/OpenFileSystemModel.java +++ b/app/src/main/java/io/xpipe/app/browser/OpenFileSystemModel.java @@ -12,6 +12,7 @@ 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; @@ -124,8 +125,34 @@ public final class OpenFileSystemModel { } // Handle commands typed into navigation bar - if (!FileNames.isAbsolute(path)) { - + if (!FileNames.isAbsolute(path) && fileSystem.getShell().isPresent()) { + var directory = currentPath.get(); + var name = path + " - " + + XPipeDaemon.getInstance().getStoreName(store.getValue()).orElse("?"); + ThreadHelper.runFailableAsync(() -> { + if (ShellDialects.ALL.stream().anyMatch(dialect -> path.startsWith(dialect.getOpenCommand()))) { + var cmd = fileSystem + .getShell() + .get() + .subShell(path) + .initWith(fileSystem + .getShell() + .get() + .getShellDialect() + .getCdCommand(currentPath.get())) + .prepareTerminalOpen(name); + TerminalHelper.open(path, cmd); + } else { + var cmd = fileSystem + .getShell() + .get() + .command(path) + .workingDirectory(directory) + .prepareTerminalOpen(name); + TerminalHelper.open(path, cmd); + } + }); + return Optional.of(currentPath.get()); } String newPath = null; @@ -306,7 +333,8 @@ public final class OpenFileSystemModel { var fs = fileSystem.createFileSystem(); fs.open(); this.fileSystem = fs; - this.local.set(fs.getShell().map(shellControl -> shellControl.isLocal()).orElse(false)); + this.local.set( + fs.getShell().map(shellControl -> shellControl.isLocal()).orElse(false)); var storageEntry = DataStorage.get() .getStoreEntryIfPresent(fileSystem) 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 index 30c4ffa8..07fafda5 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/BrowserAction.java +++ b/app/src/main/java/io/xpipe/app/browser/action/BrowserAction.java @@ -16,6 +16,7 @@ public interface BrowserAction { static enum Category { CUSTOM, OPEN, + NATIVE, COPY_PASTE, MUTATION } 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 3d424422..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,6 +17,11 @@ 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 \"\" /MIN " + command; } else { 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 ea6f108d..2618368c 100644 --- a/core/src/main/java/io/xpipe/core/process/ShellControl.java +++ b/core/src/main/java/io/xpipe/core/process/ShellControl.java @@ -135,6 +135,15 @@ public interface ShellControl extends ProcessControl { }); } + default ShellControl enforcedDialect(ShellDialect type) throws Exception { + start(); + if (getShellDialect().equals(type)) { + return this; + } else { + return subShell(type).start(); + } + } + default T enforceDialect(@NonNull ShellDialect type, Function sc) throws Exception { if (isRunning() && getShellDialect().equals(type)) { return sc.apply(this); 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 index 26342b21..b497d642 100644 --- 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 @@ -20,6 +20,11 @@ public class CopyAction implements LeafAction { 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"); 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 index d5d2ca75..5c3e59fd 100644 --- 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 @@ -25,6 +25,11 @@ public class CopyPathAction implements BrowserAction, BranchAction { return Category.COPY_PASTE; } + @Override + public boolean acceptsEmptySelection() { + return true; + } + @Override public List getBranchingActions() { return List.of( 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 08403240..9fd19752 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 @@ -35,7 +35,7 @@ public class NewItemAction implements BrowserAction, BranchAction { @Override public boolean isApplicable(OpenFileSystemModel model, List entries) { - return entries.size() == 0; + return entries.size() == 1 && entries.get(0).getRawFileEntry().getPath().equals(model.getCurrentPath().get()); } @Override 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 index 21f8284b..3f18b306 100644 --- 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 @@ -34,6 +34,11 @@ public class OpenDirectoryInNewTabAction implements LeafAction { return entries.size() == 1 && entries.stream().allMatch(entry -> entry.getRawFileEntry().isDirectory()) && model.getBrowserModel().getMode() == FileBrowserModel.Mode.BROWSER; } + @Override + public boolean acceptsEmptySelection() { + return true; + } + @Override public KeyCombination getShortcut() { return new KeyCodeCombination(KeyCode.ENTER); diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenInNativeManagerAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenInNativeManagerAction.java new file mode 100644 index 00000000..b5d1f3b9 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenInNativeManagerAction.java @@ -0,0 +1,57 @@ +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 io.xpipe.core.process.OsType; + +import java.util.List; + +public class OpenInNativeManagerAction implements LeafAction { + + @Override + public void execute(OpenFileSystemModel model, List entries) throws Exception { + for (FileBrowserEntry entry : entries) { + var e = entry.getRawFileEntry().getPath(); + switch (OsType.getLocal()) { + case OsType.Windows windows -> { + model.getFileSystem() + .getShell() + .get() + .executeSimpleCommand("explorer " + + model.getFileSystem() + .getShell() + .get() + .getShellDialect() + .fileArgument(e)); + } + case OsType.Linux linux -> {} + case OsType.MacOs macOs -> {} + } + } + } + + @Override + public Category getCategory() { + return Category.NATIVE; + } + + @Override + public boolean acceptsEmptySelection() { + return true; + } + + @Override + public boolean isApplicable(OpenFileSystemModel model, List entries) { + return entries.stream().allMatch(entry -> entry.getRawFileEntry().isDirectory()); + } + + @Override + public String getName(OpenFileSystemModel model, List entries) { + return switch (OsType.getLocal()) { + case OsType.Windows windows -> "Open in Windows Explorer"; + case OsType.Linux linux -> "Open in Windows Explorer"; + case OsType.MacOs macOs -> "Open in Windows Explorer"; + }; + } +} 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..d933747b --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenNativeFileDetailsAction.java @@ -0,0 +1,55 @@ +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 io.xpipe.core.impl.FileNames; +import io.xpipe.core.process.OsType; +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 { + for (FileBrowserEntry 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 = model.getFileSystem().getShell().get().enforcedDialect(ShellDialects.POWERSHELL).start()) { + sub.command(content).notComplex().execute(); + } + } + case OsType.Linux linux -> {} + case OsType.MacOs macOs -> {} + } + } + } + + @Override + public boolean acceptsEmptySelection() { + return true; + } + + @Override + public Category getCategory() { + return Category.NATIVE; + } + + @Override + public boolean isApplicable(OpenFileSystemModel model, List entries) { + return true; + } + + @Override + public String getName(OpenFileSystemModel model, List entries) { + return "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 index b1352b73..e3a73464 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 @@ -26,11 +26,6 @@ public class OpenTerminalAction implements LeafAction { } } - @Override - public boolean acceptsEmptySelection() { - return true; - } - @Override public Category getCategory() { return Category.OPEN; diff --git a/ext/base/src/main/java/module-info.java b/ext/base/src/main/java/module-info.java index 4dc9f5a5..6fbb581b 100644 --- a/ext/base/src/main/java/module-info.java +++ b/ext/base/src/main/java/module-info.java @@ -32,6 +32,8 @@ open module io.xpipe.ext.base { OpenDirectoryAction, OpenDirectoryInNewTabAction, OpenTerminalAction, + OpenNativeFileDetailsAction, + OpenInNativeManagerAction, EditFileAction, RunAction, CopyAction,