mirror of
https://github.com/xpipe-io/xpipe.git
synced 2024-11-21 23:20:23 +00:00
Browser quick access improvements
This commit is contained in:
parent
061dbe1cf3
commit
9ad5b6f7f5
7 changed files with 386 additions and 244 deletions
|
@ -9,6 +9,7 @@ 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.PrettyImageHelper;
|
||||
import io.xpipe.app.fxcomps.util.BindingsHelper;
|
||||
import io.xpipe.app.fxcomps.util.PlatformThread;
|
||||
import io.xpipe.app.util.BooleanScope;
|
||||
import io.xpipe.app.util.HumanReadableFormat;
|
||||
|
@ -503,20 +504,16 @@ final class BrowserFileListComp extends SimpleComp {
|
|||
.get();
|
||||
var quickAccess = new BrowserQuickAccessButtonComp(
|
||||
() -> getTableRow().getItem(), fileList.getFileSystemModel())
|
||||
.hide(Bindings.createBooleanBinding(
|
||||
.hide(BindingsHelper.persist(Bindings.createBooleanBinding(
|
||||
() -> {
|
||||
var notDir = getTableRow()
|
||||
.getItem()
|
||||
.getRawFileEntry()
|
||||
.getKind()
|
||||
!= FileKind.DIRECTORY;
|
||||
var isParentLink = getTableRow()
|
||||
.getItem()
|
||||
var item = getTableRow().getItem();
|
||||
var notDir = item.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY;
|
||||
var isParentLink = item
|
||||
.getRawFileEntry()
|
||||
.equals(fileList.getFileSystemModel().getCurrentParentDirectory());
|
||||
return notDir || isParentLink;
|
||||
},
|
||||
itemProperty()))
|
||||
itemProperty())))
|
||||
.createRegion();
|
||||
|
||||
editing.addListener((observable, oldValue, newValue) -> {
|
||||
|
|
|
@ -87,20 +87,20 @@ public final class BrowserFileListModel {
|
|||
: all.getValue();
|
||||
|
||||
var listCopy = new ArrayList<>(filtered);
|
||||
sort(listCopy);
|
||||
listCopy.sort(order());
|
||||
shown.setValue(listCopy);
|
||||
}
|
||||
|
||||
private void sort(List<BrowserEntry> l) {
|
||||
public Comparator<BrowserEntry> order() {
|
||||
var syntheticFirst = Comparator.<BrowserEntry, Boolean>comparing(path -> !path.isSynthetic());
|
||||
var dirsFirst = Comparator.<BrowserEntry, Boolean>comparing(
|
||||
path -> path.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY);
|
||||
var comp = comparatorProperty.getValue();
|
||||
|
||||
Comparator<? super BrowserEntry> us = comp != null
|
||||
Comparator<BrowserEntry> us = comp != null
|
||||
? syntheticFirst.thenComparing(dirsFirst).thenComparing(comp)
|
||||
: syntheticFirst.thenComparing(dirsFirst);
|
||||
l.sort(us);
|
||||
return us;
|
||||
}
|
||||
|
||||
public boolean rename(String filename, String newName) {
|
||||
|
|
|
@ -1,28 +1,12 @@
|
|||
package io.xpipe.app.browser;
|
||||
|
||||
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.BooleanAnimationTimer;
|
||||
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;
|
||||
import javafx.scene.control.Menu;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.layout.Region;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class BrowserQuickAccessButtonComp extends SimpleComp {
|
||||
|
||||
|
@ -36,188 +20,28 @@ public class BrowserQuickAccessButtonComp extends SimpleComp {
|
|||
|
||||
@Override
|
||||
protected Region createSimple() {
|
||||
var cm = new ContextMenu();
|
||||
var cm = new BrowserQuickAccessContextMenu(base, model);
|
||||
var button = new IconButtonComp("mdi2c-chevron-double-right");
|
||||
button.apply(struc -> {
|
||||
struc.get().setOnAction(event -> {
|
||||
if (!cm.isShowing()) {
|
||||
showMenu(cm, struc.get());
|
||||
cm.showMenu(struc.get());
|
||||
} else {
|
||||
cm.hide();
|
||||
}
|
||||
event.consume();
|
||||
});
|
||||
cm.addEventFilter(Menu.ON_HIDDEN, e -> {
|
||||
Platform.runLater(() -> {
|
||||
struc.get().requestFocus();
|
||||
});
|
||||
});
|
||||
BrowserQuickAccessContextMenu.onRight(struc.get(), false, keyEvent -> {
|
||||
cm.showMenu(struc.get());
|
||||
keyEvent.consume();
|
||||
});
|
||||
});
|
||||
button.styleClass("quick-access-button");
|
||||
return button.createRegion();
|
||||
}
|
||||
|
||||
private void showMenu(ContextMenu cm, Node anchor) {
|
||||
cm.getItems().clear();
|
||||
cm.addEventHandler(Menu.ON_SHOWING, e -> {
|
||||
Node content = cm.getSkin().getNode();
|
||||
if (content instanceof Region r) {
|
||||
r.setMaxWidth(500);
|
||||
}
|
||||
});
|
||||
cm.setAutoHide(true);
|
||||
cm.getStyleClass().add("condensed");
|
||||
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
var fileEntry = base.get().getRawFileEntry();
|
||||
if (fileEntry.getKind() != FileKind.DIRECTORY) {
|
||||
return;
|
||||
}
|
||||
|
||||
var actionsMenu = new AtomicReference<ContextMenu>();
|
||||
var r = new Menu();
|
||||
var newItems = updateMenuItems(cm, r, fileEntry, true, actionsMenu);
|
||||
Platform.runLater(() -> {
|
||||
cm.getItems().addAll(r.getItems());
|
||||
cm.show(anchor, Side.RIGHT, 0, 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private MenuItem createItem(
|
||||
ContextMenu contextMenu, FileSystem.FileEntry fileEntry, AtomicReference<ContextMenu> showingActionsMenu) {
|
||||
var browserCm = new BrowserContextMenu(model, new BrowserEntry(fileEntry, model.getFileList(), false));
|
||||
browserCm.setOnAction(e -> {
|
||||
contextMenu.hide();
|
||||
});
|
||||
|
||||
if (fileEntry.getKind() != FileKind.DIRECTORY) {
|
||||
var m = new Menu(
|
||||
fileEntry.getName(),
|
||||
PrettyImageHelper.ofFixedSizeSquare(FileIconManager.getFileIcon(fileEntry, false), 24)
|
||||
.createRegion());
|
||||
m.setMnemonicParsing(false);
|
||||
m.setOnAction(event -> {
|
||||
if (event.getTarget() != m) {
|
||||
return;
|
||||
}
|
||||
|
||||
browserCm.show(m.getStyleableNode(), Side.RIGHT, 0, 0);
|
||||
showingActionsMenu.set(browserCm);
|
||||
});
|
||||
m.getStyleClass().add("leaf");
|
||||
return m;
|
||||
}
|
||||
|
||||
var m = new Menu(
|
||||
fileEntry.getName(),
|
||||
PrettyImageHelper.ofFixedSizeSquare(FileIconManager.getFileIcon(fileEntry, false), 24)
|
||||
.createRegion());
|
||||
m.setMnemonicParsing(false);
|
||||
var empty = new MenuItem("...");
|
||||
m.getItems().add(empty);
|
||||
|
||||
var hover = new SimpleBooleanProperty();
|
||||
m.setOnShowing(event -> {
|
||||
var actionsMenu = showingActionsMenu.get();
|
||||
if (actionsMenu != null) {
|
||||
actionsMenu.hide();
|
||||
showingActionsMenu.set(null);
|
||||
}
|
||||
hover.set(true);
|
||||
event.consume();
|
||||
});
|
||||
m.setOnHiding(event -> {
|
||||
var actionsMenu = showingActionsMenu.get();
|
||||
if (actionsMenu != null) {
|
||||
actionsMenu.hide();
|
||||
showingActionsMenu.set(null);
|
||||
}
|
||||
hover.set(false);
|
||||
event.consume();
|
||||
});
|
||||
new BooleanAnimationTimer(hover, 100, () -> {
|
||||
if (m.isShowing() && !m.getItems().getFirst().equals(empty)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
var newItems = updateMenuItems(contextMenu, m, fileEntry, false, showingActionsMenu);
|
||||
Platform.runLater(() -> {
|
||||
m.getItems().setAll(newItems);
|
||||
if (!browserCm.isShowing() && m.isShowing()) {
|
||||
m.hide();
|
||||
m.show();
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
.start();
|
||||
m.setOnAction(event -> {
|
||||
if (event.getTarget() != m) {
|
||||
return;
|
||||
}
|
||||
|
||||
var actionsMenu = showingActionsMenu.get();
|
||||
if (actionsMenu != null && actionsMenu.isShowing()) {
|
||||
actionsMenu.hide();
|
||||
showingActionsMenu.set(null);
|
||||
m.show();
|
||||
return;
|
||||
}
|
||||
|
||||
m.hide();
|
||||
browserCm.show(m.getStyleableNode(), Side.RIGHT, 0, 0);
|
||||
showingActionsMenu.set(browserCm);
|
||||
event.consume();
|
||||
});
|
||||
return m;
|
||||
}
|
||||
|
||||
private List<MenuItem> updateMenuItems(
|
||||
ContextMenu contextMenu,
|
||||
Menu m,
|
||||
FileSystem.FileEntry fileEntry,
|
||||
boolean updateInstantly,
|
||||
AtomicReference<ContextMenu> showingActionsMenu)
|
||||
throws Exception {
|
||||
var newFiles = model.getFileSystem().listFiles(fileEntry.getPath());
|
||||
try (var s = newFiles) {
|
||||
var list = s.toList();
|
||||
// Wait until all files are listed, i.e. do not skip the stream elements
|
||||
list = list.subList(0, Math.min(list.size(), 150));
|
||||
|
||||
var newItems = new ArrayList<MenuItem>();
|
||||
if (list.isEmpty()) {
|
||||
newItems.add(new MenuItem("<empty>"));
|
||||
} else {
|
||||
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, showingActionsMenu),
|
||||
(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()),
|
||||
dirs.getFirst(),
|
||||
true,
|
||||
showingActionsMenu);
|
||||
}
|
||||
newItems.addAll(menus.values());
|
||||
}
|
||||
if (updateInstantly) {
|
||||
m.getItems().setAll(newItems);
|
||||
}
|
||||
return newItems;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,348 @@
|
|||
package io.xpipe.app.browser;
|
||||
|
||||
import io.xpipe.app.browser.icon.FileIconManager;
|
||||
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
|
||||
import io.xpipe.app.util.BooleanAnimationTimer;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
import io.xpipe.core.store.FileKind;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.event.EventTarget;
|
||||
import javafx.geometry.Side;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.Menu;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.Region;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class BrowserQuickAccessContextMenu extends ContextMenu {
|
||||
|
||||
static void onLeft(EventTarget target, boolean filter, Consumer<KeyEvent> r) {
|
||||
EventHandler<KeyEvent> keyEventEventHandler = event -> {
|
||||
if (event.getCode() == KeyCode.LEFT || event.getCode() == KeyCode.NUMPAD4) {
|
||||
r.accept(event);
|
||||
}
|
||||
};
|
||||
if (filter) {
|
||||
target.addEventFilter(KeyEvent.KEY_PRESSED, keyEventEventHandler);
|
||||
} else {
|
||||
target.addEventHandler(KeyEvent.KEY_PRESSED, keyEventEventHandler);
|
||||
}
|
||||
}
|
||||
|
||||
static void onRight(EventTarget target, boolean filter, Consumer<KeyEvent> r) {
|
||||
EventHandler<KeyEvent> keyEventEventHandler = event -> {
|
||||
if (event.getCode() == KeyCode.RIGHT || event.getCode() == KeyCode.NUMPAD6) {
|
||||
r.accept(event);
|
||||
}
|
||||
};
|
||||
if (filter) {
|
||||
target.addEventFilter(KeyEvent.KEY_PRESSED, keyEventEventHandler);
|
||||
} else {
|
||||
target.addEventHandler(KeyEvent.KEY_PRESSED, keyEventEventHandler);
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
class QuickAccessMenu {
|
||||
private final BrowserEntry browserEntry;
|
||||
private ContextMenu browserActionMenu;
|
||||
private final Menu menu;
|
||||
|
||||
public QuickAccessMenu(BrowserEntry browserEntry) {
|
||||
this.browserEntry = browserEntry;
|
||||
this.menu = new Menu(
|
||||
// Use original name, not the link target
|
||||
browserEntry.getRawFileEntry().getName(),
|
||||
PrettyImageHelper.ofFixedSizeSquare(FileIconManager.getFileIcon(browserEntry.getRawFileEntry(), false), 24)
|
||||
.createRegion());
|
||||
createMenu();
|
||||
addInputListeners();
|
||||
}
|
||||
|
||||
private void createMenu() {
|
||||
var fileEntry = browserEntry.getRawFileEntry();
|
||||
if (fileEntry.resolved().getKind() != FileKind.DIRECTORY) {
|
||||
createFileMenu();
|
||||
} else {
|
||||
createDirectoryMenu();
|
||||
}
|
||||
}
|
||||
|
||||
private void createFileMenu() {
|
||||
var fileEntry = browserEntry.getRawFileEntry();
|
||||
menu.setMnemonicParsing(false);
|
||||
menu.addEventFilter(Menu.ON_SHOWN, event -> {
|
||||
menu.hide();
|
||||
if (keyBasedNavigation && expandBrowserActionMenuKey) {
|
||||
if (!hideBrowserActionsMenu()) {
|
||||
showBrowserActionsMenu();
|
||||
}
|
||||
}
|
||||
});
|
||||
menu.setOnAction(event -> {
|
||||
if (event.getTarget() != menu) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hideBrowserActionsMenu()) {
|
||||
showBrowserActionsMenu();
|
||||
}
|
||||
});
|
||||
menu.getStyleClass().add("leaf");
|
||||
|
||||
var empty = new MenuItem("...");
|
||||
empty.setDisable(true);
|
||||
menu.getItems().add(empty);
|
||||
onRight(empty, true, keyEvent -> {
|
||||
keyEvent.consume();
|
||||
});
|
||||
}
|
||||
|
||||
private void createDirectoryMenu() {
|
||||
var fileEntry = browserEntry.getRawFileEntry().resolved();
|
||||
menu.setMnemonicParsing(false);
|
||||
var empty = new MenuItem("...");
|
||||
empty.setDisable(true);
|
||||
menu.getItems().add(empty);
|
||||
addHoverHandling(menu, empty);
|
||||
|
||||
menu.setOnAction(event -> {
|
||||
if (event.getTarget() != menu) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hideBrowserActionsMenu()) {
|
||||
menu.show();
|
||||
event.consume();
|
||||
return;
|
||||
}
|
||||
|
||||
showBrowserActionsMenu();
|
||||
event.consume();
|
||||
});
|
||||
|
||||
menu.addEventFilter(Menu.ON_SHOWING, event -> {
|
||||
hideBrowserActionsMenu();
|
||||
});
|
||||
|
||||
menu.addEventFilter(Menu.ON_SHOWN, event -> {
|
||||
if (keyBasedNavigation && expandBrowserActionMenuKey) {
|
||||
if (hideBrowserActionsMenu()) {
|
||||
menu.show();
|
||||
} else {
|
||||
showBrowserActionsMenu();
|
||||
}
|
||||
} else if (keyBasedNavigation) {
|
||||
expandDirectoryMenu(empty);
|
||||
}
|
||||
});
|
||||
|
||||
menu.addEventFilter(Menu.ON_HIDING, event -> {
|
||||
if (closeBrowserActionMenuKey) {
|
||||
menu.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void addHoverHandling(Menu m, MenuItem empty) {
|
||||
var hover = new SimpleBooleanProperty();
|
||||
menu.addEventFilter(Menu.ON_SHOWING, event -> {
|
||||
if (!keyBasedNavigation) {
|
||||
hover.set(true);
|
||||
}
|
||||
});
|
||||
menu.addEventFilter(Menu.ON_HIDING, event -> {
|
||||
if (!keyBasedNavigation) {
|
||||
hover.set(false);
|
||||
}
|
||||
});
|
||||
new BooleanAnimationTimer(hover, 100, () -> {
|
||||
expandDirectoryMenu(empty);
|
||||
}).start();
|
||||
}
|
||||
|
||||
private void addInputListeners() {
|
||||
menu.parentPopupProperty().subscribe(contextMenu -> {
|
||||
if (contextMenu != null) {
|
||||
contextMenu.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
|
||||
keyBasedNavigation = true;
|
||||
if (event.getCode().equals(KeyCode.ENTER)) {
|
||||
expandBrowserActionMenuKey = true;
|
||||
} else {
|
||||
expandBrowserActionMenuKey = false;
|
||||
}
|
||||
if (event.getCode().equals(KeyCode.LEFT) && browserActionMenu != null && browserActionMenu.isShowing()) {
|
||||
closeBrowserActionMenuKey = true;
|
||||
} else {
|
||||
closeBrowserActionMenuKey = false;
|
||||
}
|
||||
});
|
||||
contextMenu.addEventFilter(MouseEvent.ANY,event -> {
|
||||
keyBasedNavigation = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void expandDirectoryMenu(MenuItem empty) {
|
||||
if (menu.isShowing() && !menu.getItems().getFirst().equals(empty)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
var newItems = updateMenuItems(menu, browserEntry, false);
|
||||
Platform.runLater(() -> {
|
||||
var reshow = (browserActionMenu == null || !browserActionMenu.isShowing()) && menu.isShowing();
|
||||
if (reshow) {
|
||||
menu.hide();
|
||||
}
|
||||
menu.getItems().setAll(newItems);
|
||||
if (reshow) {
|
||||
menu.show();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private boolean hideBrowserActionsMenu() {
|
||||
if (shownBrowserActionsMenu != null && shownBrowserActionsMenu.isShowing()) {
|
||||
shownBrowserActionsMenu.hide();
|
||||
shownBrowserActionsMenu = null;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void showBrowserActionsMenu() {
|
||||
if (browserActionMenu == null) {
|
||||
this.browserActionMenu = new BrowserContextMenu(model, browserEntry);
|
||||
this.browserActionMenu.setOnAction(e -> {
|
||||
hide();
|
||||
});
|
||||
onLeft(this.browserActionMenu, true, keyEvent -> {
|
||||
this.browserActionMenu.hide();
|
||||
keyEvent.consume();
|
||||
});
|
||||
}
|
||||
|
||||
menu.hide();
|
||||
browserActionMenu.show(menu.getStyleableNode(), Side.RIGHT, 0, 0);
|
||||
shownBrowserActionsMenu = browserActionMenu;
|
||||
Platform.runLater(() -> {
|
||||
browserActionMenu.getItems().getFirst().getStyleableNode().requestFocus();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private final Supplier<BrowserEntry> base;
|
||||
private final OpenFileSystemModel model;
|
||||
private ContextMenu shownBrowserActionsMenu;
|
||||
|
||||
private boolean expandBrowserActionMenuKey;
|
||||
private boolean keyBasedNavigation;
|
||||
private boolean closeBrowserActionMenuKey;
|
||||
|
||||
public BrowserQuickAccessContextMenu(Supplier<BrowserEntry> base, OpenFileSystemModel model) {
|
||||
this.base = base;
|
||||
this.model = model;
|
||||
|
||||
addEventFilter(Menu.ON_SHOWING, e -> {
|
||||
Node content = getSkin().getNode();
|
||||
if (content instanceof Region r) {
|
||||
r.setMaxWidth(500);
|
||||
}
|
||||
});
|
||||
addEventFilter(Menu.ON_SHOWN, e -> {
|
||||
Platform.runLater(() -> {
|
||||
getItems().getFirst().getStyleableNode().requestFocus();
|
||||
});
|
||||
});
|
||||
onLeft(this, false, e -> {
|
||||
hide();
|
||||
e.consume();
|
||||
});
|
||||
setAutoHide(true);
|
||||
getStyleClass().add("condensed");
|
||||
}
|
||||
|
||||
public void showMenu(Node anchor) {
|
||||
getItems().clear();
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
var entry = base.get();
|
||||
if (entry.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY) {
|
||||
return;
|
||||
}
|
||||
|
||||
var actionsMenu = new AtomicReference<ContextMenu>();
|
||||
var r = new Menu();
|
||||
var newItems = updateMenuItems(r, entry, true);
|
||||
Platform.runLater(() -> {
|
||||
getItems().addAll(r.getItems());
|
||||
show(anchor, Side.RIGHT, 0, 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private MenuItem createItem(BrowserEntry browserEntry) {
|
||||
return new QuickAccessMenu(browserEntry).getMenu();
|
||||
}
|
||||
|
||||
private List<MenuItem> updateMenuItems(
|
||||
Menu m,
|
||||
BrowserEntry entry,
|
||||
boolean updateInstantly)
|
||||
throws Exception {
|
||||
var newFiles = model.getFileSystem().listFiles(entry.getRawFileEntry().resolved().getPath());
|
||||
try (var s = newFiles) {
|
||||
var list = s.map(fileEntry -> fileEntry.resolved()).toList();
|
||||
// Wait until all files are listed, i.e. do not skip the stream elements
|
||||
list = list.subList(0, Math.min(list.size(), 150));
|
||||
|
||||
var newItems = new ArrayList<MenuItem>();
|
||||
if (list.isEmpty()) {
|
||||
var empty = new Menu("<empty>");
|
||||
empty.getStyleClass().add("leaf");
|
||||
newItems.add(empty);
|
||||
} else {
|
||||
var browserEntries = list.stream()
|
||||
.map(fileEntry -> new BrowserEntry(fileEntry, model.getFileList(), false))
|
||||
.toList();
|
||||
var menus = browserEntries.stream()
|
||||
.sorted(model.getFileList().order())
|
||||
.collect(Collectors.toMap(
|
||||
e -> e,
|
||||
e -> createItem(e),
|
||||
(v1, v2) -> v2,
|
||||
LinkedHashMap::new));
|
||||
var dirs = browserEntries.stream()
|
||||
.filter(e -> e.getRawFileEntry().getKind() == FileKind.DIRECTORY)
|
||||
.toList();
|
||||
if (dirs.size() == 1) {
|
||||
updateMenuItems(
|
||||
(Menu) menus.get(dirs.getFirst()),
|
||||
dirs.getFirst(),
|
||||
true);
|
||||
}
|
||||
newItems.addAll(menus.values());
|
||||
}
|
||||
if (updateInstantly) {
|
||||
m.getItems().setAll(newItems);
|
||||
}
|
||||
return newItems;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,7 +9,6 @@ import io.xpipe.app.core.AppWindowHelper;
|
|||
import io.xpipe.app.fxcomps.Comp;
|
||||
import io.xpipe.app.fxcomps.SimpleComp;
|
||||
import io.xpipe.app.fxcomps.augment.GrowAugment;
|
||||
import io.xpipe.app.util.JfxHelper;
|
||||
import io.xpipe.app.util.LicenseRequiredException;
|
||||
import io.xpipe.app.util.PlatformState;
|
||||
import javafx.application.Platform;
|
||||
|
@ -151,8 +150,18 @@ public class ErrorHandlerComp extends SimpleComp {
|
|||
}
|
||||
}
|
||||
|
||||
private Region createActionButtonGraphic(String nameString, String descString) {
|
||||
var header = new Label(nameString);
|
||||
AppFont.header(header);
|
||||
var desc = new Label(descString);
|
||||
AppFont.small(desc);
|
||||
var text = new VBox(header, desc);
|
||||
text.setSpacing(2);
|
||||
return text;
|
||||
}
|
||||
|
||||
private Region createActionComp(ErrorAction a) {
|
||||
var r = JfxHelper.createNamedEntry(a.getName(), a.getDescription());
|
||||
var r = createActionButtonGraphic(a.getName(), a.getDescription());
|
||||
var b = new ButtonComp(null, r, () -> {
|
||||
takenAction.setValue(a);
|
||||
try {
|
||||
|
|
|
@ -3,55 +3,14 @@ package io.xpipe.app.util;
|
|||
import atlantafx.base.controls.Spacer;
|
||||
import io.xpipe.app.core.AppFont;
|
||||
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.kordamp.ikonli.javafx.FontIcon;
|
||||
|
||||
public class JfxHelper {
|
||||
|
||||
public static Region createNamedEntry(String nameString, String descString) {
|
||||
var header = new Label(nameString);
|
||||
AppFont.header(header);
|
||||
var desc = new Label(descString);
|
||||
AppFont.small(desc);
|
||||
var text = new VBox(header, desc);
|
||||
text.setSpacing(2);
|
||||
return text;
|
||||
}
|
||||
|
||||
public static Region createNamedEntry(String nameString, String descString, FontIcon graphic) {
|
||||
var header = new Label(nameString);
|
||||
var desc = new Label(descString);
|
||||
AppFont.small(desc);
|
||||
desc.setOpacity(0.65);
|
||||
var text = new VBox(header, desc);
|
||||
text.setSpacing(2);
|
||||
|
||||
var pane = new StackPane(graphic);
|
||||
var hbox = new HBox(pane, text);
|
||||
hbox.setSpacing(8);
|
||||
pane.prefWidthProperty()
|
||||
.bind(Bindings.createDoubleBinding(
|
||||
() -> (header.getHeight() + desc.getHeight()) * 0.6,
|
||||
header.heightProperty(),
|
||||
desc.heightProperty()));
|
||||
pane.prefHeightProperty()
|
||||
.bind(Bindings.createDoubleBinding(
|
||||
() -> header.getHeight() + desc.getHeight() + 2,
|
||||
header.heightProperty(),
|
||||
desc.heightProperty()));
|
||||
pane.prefHeightProperty().addListener((c, o, n) -> {
|
||||
var size = Math.min(n.intValue(), 100);
|
||||
graphic.setIconSize((int) (size * 0.55));
|
||||
});
|
||||
return hbox;
|
||||
}
|
||||
|
||||
public static Region createNamedEntry(String nameString, String descString, String image) {
|
||||
var header = new Label(nameString);
|
||||
AppFont.header(header);
|
||||
|
|
|
@ -208,8 +208,13 @@
|
|||
-fx-opacity: 1.0;
|
||||
}
|
||||
|
||||
.browser .quick-access-button {
|
||||
-fx-border-radius: 0;
|
||||
-fx-background-radius: 0;
|
||||
}
|
||||
|
||||
.browser .quick-access-button .context-menu .leaf .arrow {
|
||||
.browser .quick-access-button .context-menu .leaf > * > .arrow {
|
||||
-fx-pref-width: 0;
|
||||
-fx-opacity: 0;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue