diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserFileListComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserFileListComp.java index 5ae5b5eb..b22251a5 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserFileListComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserFileListComp.java @@ -498,7 +498,7 @@ final class BrowserFileListComp extends SimpleComp { setAccessibleRole(AccessibleRole.TEXT); var textField = new LazyTextFieldComp(text).minWidth(USE_PREF_SIZE).createStructure().get(); - var quickAccess = new BrowserQuickAccessButtonComp(() -> getTableRow().getItem().getRawFileEntry(), fileList.getFileSystemModel(), fileEntry -> {}) + var quickAccess = new BrowserQuickAccessButtonComp(() -> getTableRow().getItem(), fileList.getFileSystemModel()) .hide(Bindings.createBooleanBinding(() -> { var notDir = getTableRow().getItem().getRawFileEntry().getKind() != FileKind.DIRECTORY; var isParentLink = getTableRow() diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserQuickAccessButtonComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserQuickAccessButtonComp.java index 462d0ae5..56d43bfa 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserQuickAccessButtonComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserQuickAccessButtonComp.java @@ -4,10 +4,12 @@ import io.xpipe.app.browser.icon.FileIconManager; import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.impl.IconButtonComp; import io.xpipe.app.fxcomps.impl.PrettyImageHelper; +import io.xpipe.app.util.BooleanTimer; import io.xpipe.app.util.ThreadHelper; import io.xpipe.core.store.FileKind; import io.xpipe.core.store.FileSystem; import javafx.application.Platform; +import javafx.beans.property.SimpleBooleanProperty; import javafx.geometry.Side; import javafx.scene.Node; import javafx.scene.control.ContextMenu; @@ -16,19 +18,19 @@ import javafx.scene.control.MenuItem; import javafx.scene.layout.Region; import java.util.ArrayList; -import java.util.function.Consumer; +import java.util.LinkedHashMap; +import java.util.List; import java.util.function.Supplier; +import java.util.stream.Collectors; public class BrowserQuickAccessButtonComp extends SimpleComp { - private final Supplier base; + private final Supplier base; private final OpenFileSystemModel model; - private final Consumer action; - public BrowserQuickAccessButtonComp(Supplier base, OpenFileSystemModel model, Consumer action) { + public BrowserQuickAccessButtonComp(Supplier base, OpenFileSystemModel model) { this.base = base; this.model = model; - this.action = action; } @Override @@ -43,15 +45,6 @@ public class BrowserQuickAccessButtonComp extends SimpleComp { } private void showMenu(Node anchor) { - ThreadHelper.runFailableAsync(() -> { - var children = model.getFileSystem().listFiles(base.get().getPath()); - try (var s = children) { - var list = s.toList(); - if (list.isEmpty()) { - return; - } - - Platform.runLater(() -> { var cm = new ContextMenu(); cm.addEventHandler(Menu.ON_SHOWING, e -> { Node content = cm.getSkin().getNode(); @@ -62,48 +55,94 @@ public class BrowserQuickAccessButtonComp extends SimpleComp { }); cm.setAutoHide(true); cm.getStyleClass().add("condensed"); - cm.getItems().addAll(list.stream().map(e -> recurse(cm, e)).toList()); - cm.show(anchor, Side.RIGHT, 0, 0); - }); - } - }); + + ThreadHelper.runFailableAsync(() -> { + var fileEntry = base.get().getRawFileEntry(); + if (fileEntry.getKind() != FileKind.DIRECTORY) { + return; + } + + var r = new Menu(); + var newItems = updateMenuItems(cm, r, fileEntry, true); + Platform.runLater(() -> { + cm.getItems().addAll(r.getItems()); + cm.show(anchor, Side.RIGHT, 0, 0); + }); + }); } - private MenuItem recurse(ContextMenu contextMenu, FileSystem.FileEntry fileEntry) { + private MenuItem createItem(ContextMenu contextMenu, FileSystem.FileEntry fileEntry) { + var browserCm = new BrowserContextMenu(model, new BrowserEntry(fileEntry,model.getFileList(), false)); + if (fileEntry.getKind() != FileKind.DIRECTORY) { - var m = new MenuItem( + var m = new Menu( fileEntry.getName(), - PrettyImageHelper.ofFixedSquare(FileIconManager.getFileIcon(fileEntry,false), 16).createRegion()); + PrettyImageHelper.ofFixedSizeSquare(FileIconManager.getFileIcon(fileEntry,false), 24).createRegion()); m.setMnemonicParsing(false); m.setOnAction(event -> { - action.accept(fileEntry); - event.consume(); + if (event.getTarget() != m) { + return; + } + + browserCm.show(m.getStyleableNode(), Side.RIGHT, 0, 0); }); return m; } var m = new Menu( fileEntry.getName(), - PrettyImageHelper.ofFixedSquare(FileIconManager.getFileIcon(fileEntry,false), 16).createRegion()); + PrettyImageHelper.ofFixedSizeSquare(FileIconManager.getFileIcon(fileEntry,false), 24).createRegion()); m.setMnemonicParsing(false); - m.setOnAction(event -> { - if (event.getTarget() == m) { - if (m.isShowing()) { - event.consume(); - return; - } + var empty = new MenuItem("..."); + m.getItems().add(empty); - ThreadHelper.runFailableAsync(() -> { - updateMenuItems(m, fileEntry); - }); - action.accept(fileEntry); - event.consume(); + var hover = new SimpleBooleanProperty(); + m.setOnShowing(event -> { + browserCm.hide(); + hover.set(true); + event.consume(); + }); + m.setOnHiding(event -> { + browserCm.hide(); + hover.set(false); + event.consume(); + }); + new BooleanTimer(hover,500, () -> { + if (m.isShowing() && !m.getItems().getFirst().equals(empty)) { + return; } + + List newItems = null; + try { + newItems = updateMenuItems(contextMenu, m, fileEntry, false); + m.getItems().setAll(newItems); + if (!browserCm.isShowing()) { + m.hide(); + m.show(); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + }).start(); + m.setOnAction(event -> { + if (event.getTarget() != m) { + return; + } + + if (browserCm.isShowing()) { + browserCm.hide(); + m.show(); + return; + } + + m.hide(); + browserCm.show(m.getStyleableNode(), Side.RIGHT, 0, 0); + event.consume(); }); return m; } - private void updateMenuItems(Menu m, FileSystem.FileEntry fileEntry) throws Exception { + private List updateMenuItems(ContextMenu contextMenu, Menu m, FileSystem.FileEntry fileEntry, boolean updateInstantly) throws Exception { var newFiles = model.getFileSystem().listFiles(fileEntry.getPath()); try (var s = newFiles) { var list = s.toList(); @@ -111,19 +150,26 @@ public class BrowserQuickAccessButtonComp extends SimpleComp { var newItems = new ArrayList(); if (list.isEmpty()) { newItems.add(new MenuItem("")); - } else if (list.size() == 1 && list.getFirst().getKind() == FileKind.DIRECTORY) { - var subMenu = recurse(m.getParentPopup(),list.getFirst()); - updateMenuItems(m, list.getFirst()); - newItems.add(subMenu); } else { - newItems.addAll(list.stream().map(e -> recurse(m.getParentPopup(), e)).toList()); + var menus = list.stream().sorted((o1, o2) -> { + if (o1.getKind() == FileKind.DIRECTORY && o2.getKind() != FileKind.DIRECTORY) { + return -1; + } + if (o2.getKind() == FileKind.DIRECTORY && o1.getKind() != FileKind.DIRECTORY) { + return 1; + } + return o1.getName().compareToIgnoreCase(o2.getName()); + }).collect(Collectors.toMap(e -> e, e -> createItem(contextMenu, e), (v1, v2) -> v2, LinkedHashMap::new)); + var dirs = list.stream().filter(e -> e.getKind() == FileKind.DIRECTORY).toList(); + if (dirs.size() == 1) { + updateMenuItems(contextMenu, (Menu) menus.get(dirs.getFirst()), list.getFirst(), updateInstantly); + } + newItems.addAll(menus.values()); } - - Platform.runLater(() -> { + if (updateInstantly) { m.getItems().setAll(newItems); - m.hide(); - m.show(); - }); + } + return newItems; } } } 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 index a88d2dd6..0aa842ee 100644 --- a/app/src/main/java/io/xpipe/app/browser/icon/BrowserIcons.java +++ b/app/src/main/java/io/xpipe/app/browser/icon/BrowserIcons.java @@ -7,18 +7,18 @@ import io.xpipe.core.store.FileSystem; public class BrowserIcons { public static Comp createDefaultFileIcon() { - return PrettyImageHelper.ofFixedSizeSquare("default_file.svg", 22); + return PrettyImageHelper.ofFixedSizeSquare("default_file.svg", 24); } public static Comp createDefaultDirectoryIcon() { - return PrettyImageHelper.ofFixedSizeSquare("default_folder.svg", 22); + return PrettyImageHelper.ofFixedSizeSquare("default_folder.svg", 24); } public static Comp createIcon(BrowserIconFileType type) { - return PrettyImageHelper.ofFixedSizeSquare(type.getIcon(), 22); + return PrettyImageHelper.ofFixedSizeSquare(type.getIcon(), 24); } public static Comp createIcon(FileSystem.FileEntry entry) { - return PrettyImageHelper.ofFixedSizeSquare(FileIconManager.getFileIcon(entry, false), 22); + return PrettyImageHelper.ofFixedSizeSquare(FileIconManager.getFileIcon(entry, false), 24); } } diff --git a/app/src/main/java/io/xpipe/app/util/BooleanTimer.java b/app/src/main/java/io/xpipe/app/util/BooleanTimer.java new file mode 100644 index 00000000..8bcd70c1 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/util/BooleanTimer.java @@ -0,0 +1,52 @@ +package io.xpipe.app.util; + +import javafx.animation.AnimationTimer; +import javafx.beans.value.ObservableBooleanValue; + +import java.util.concurrent.atomic.AtomicReference; + +public class BooleanTimer { + + private final ObservableBooleanValue value; + private final int duration; + private final Runnable toExecute; + + public BooleanTimer(ObservableBooleanValue value, int duration, Runnable toExecute) { + this.value = value; + this.duration = duration; + this.toExecute = toExecute; + } + + public void start() { + var timer = new AtomicReference(); + value.addListener((observable, oldValue, newValue) -> { + if (newValue) { + if (timer.get() == null) { + timer.set(new AnimationTimer() { + + long init =0; + + @Override + public void handle(long now) { + if (init == 0) { + init = now; + } + + var nowMs = now; + if ((nowMs - init) > duration * 1000L) { + toExecute.run(); + stop(); + } + } + }); + timer.get().start(); + } + } else { + if (timer.get() != null) { + timer.get().stop(); + timer.set(null); + } + } + }); + } +}