mirror of
https://github.com/xpipe-io/xpipe.git
synced 2024-09-30 00:56:56 +13: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.SimpleCompStructure;
|
||||||
import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
|
import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
|
||||||
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
|
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.fxcomps.util.PlatformThread;
|
||||||
import io.xpipe.app.util.BooleanScope;
|
import io.xpipe.app.util.BooleanScope;
|
||||||
import io.xpipe.app.util.HumanReadableFormat;
|
import io.xpipe.app.util.HumanReadableFormat;
|
||||||
|
@ -503,20 +504,16 @@ final class BrowserFileListComp extends SimpleComp {
|
||||||
.get();
|
.get();
|
||||||
var quickAccess = new BrowserQuickAccessButtonComp(
|
var quickAccess = new BrowserQuickAccessButtonComp(
|
||||||
() -> getTableRow().getItem(), fileList.getFileSystemModel())
|
() -> getTableRow().getItem(), fileList.getFileSystemModel())
|
||||||
.hide(Bindings.createBooleanBinding(
|
.hide(BindingsHelper.persist(Bindings.createBooleanBinding(
|
||||||
() -> {
|
() -> {
|
||||||
var notDir = getTableRow()
|
var item = getTableRow().getItem();
|
||||||
.getItem()
|
var notDir = item.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY;
|
||||||
.getRawFileEntry()
|
var isParentLink = item
|
||||||
.getKind()
|
|
||||||
!= FileKind.DIRECTORY;
|
|
||||||
var isParentLink = getTableRow()
|
|
||||||
.getItem()
|
|
||||||
.getRawFileEntry()
|
.getRawFileEntry()
|
||||||
.equals(fileList.getFileSystemModel().getCurrentParentDirectory());
|
.equals(fileList.getFileSystemModel().getCurrentParentDirectory());
|
||||||
return notDir || isParentLink;
|
return notDir || isParentLink;
|
||||||
},
|
},
|
||||||
itemProperty()))
|
itemProperty())))
|
||||||
.createRegion();
|
.createRegion();
|
||||||
|
|
||||||
editing.addListener((observable, oldValue, newValue) -> {
|
editing.addListener((observable, oldValue, newValue) -> {
|
||||||
|
|
|
@ -87,20 +87,20 @@ public final class BrowserFileListModel {
|
||||||
: all.getValue();
|
: all.getValue();
|
||||||
|
|
||||||
var listCopy = new ArrayList<>(filtered);
|
var listCopy = new ArrayList<>(filtered);
|
||||||
sort(listCopy);
|
listCopy.sort(order());
|
||||||
shown.setValue(listCopy);
|
shown.setValue(listCopy);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sort(List<BrowserEntry> l) {
|
public Comparator<BrowserEntry> order() {
|
||||||
var syntheticFirst = Comparator.<BrowserEntry, Boolean>comparing(path -> !path.isSynthetic());
|
var syntheticFirst = Comparator.<BrowserEntry, Boolean>comparing(path -> !path.isSynthetic());
|
||||||
var dirsFirst = Comparator.<BrowserEntry, Boolean>comparing(
|
var dirsFirst = Comparator.<BrowserEntry, Boolean>comparing(
|
||||||
path -> path.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY);
|
path -> path.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY);
|
||||||
var comp = comparatorProperty.getValue();
|
var comp = comparatorProperty.getValue();
|
||||||
|
|
||||||
Comparator<? super BrowserEntry> us = comp != null
|
Comparator<BrowserEntry> us = comp != null
|
||||||
? syntheticFirst.thenComparing(dirsFirst).thenComparing(comp)
|
? syntheticFirst.thenComparing(dirsFirst).thenComparing(comp)
|
||||||
: syntheticFirst.thenComparing(dirsFirst);
|
: syntheticFirst.thenComparing(dirsFirst);
|
||||||
l.sort(us);
|
return us;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean rename(String filename, String newName) {
|
public boolean rename(String filename, String newName) {
|
||||||
|
|
|
@ -1,28 +1,12 @@
|
||||||
package io.xpipe.app.browser;
|
package io.xpipe.app.browser;
|
||||||
|
|
||||||
import io.xpipe.app.browser.icon.FileIconManager;
|
|
||||||
import io.xpipe.app.fxcomps.SimpleComp;
|
import io.xpipe.app.fxcomps.SimpleComp;
|
||||||
import io.xpipe.app.fxcomps.impl.IconButtonComp;
|
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.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.Menu;
|
||||||
import javafx.scene.control.MenuItem;
|
|
||||||
import javafx.scene.layout.Region;
|
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.function.Supplier;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
public class BrowserQuickAccessButtonComp extends SimpleComp {
|
public class BrowserQuickAccessButtonComp extends SimpleComp {
|
||||||
|
|
||||||
|
@ -36,188 +20,28 @@ public class BrowserQuickAccessButtonComp extends SimpleComp {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Region createSimple() {
|
protected Region createSimple() {
|
||||||
var cm = new ContextMenu();
|
var cm = new BrowserQuickAccessContextMenu(base, model);
|
||||||
var button = new IconButtonComp("mdi2c-chevron-double-right");
|
var button = new IconButtonComp("mdi2c-chevron-double-right");
|
||||||
button.apply(struc -> {
|
button.apply(struc -> {
|
||||||
struc.get().setOnAction(event -> {
|
struc.get().setOnAction(event -> {
|
||||||
if (!cm.isShowing()) {
|
if (!cm.isShowing()) {
|
||||||
showMenu(cm, struc.get());
|
cm.showMenu(struc.get());
|
||||||
} else {
|
} else {
|
||||||
cm.hide();
|
cm.hide();
|
||||||
}
|
}
|
||||||
event.consume();
|
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");
|
button.styleClass("quick-access-button");
|
||||||
return button.createRegion();
|
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.Comp;
|
||||||
import io.xpipe.app.fxcomps.SimpleComp;
|
import io.xpipe.app.fxcomps.SimpleComp;
|
||||||
import io.xpipe.app.fxcomps.augment.GrowAugment;
|
import io.xpipe.app.fxcomps.augment.GrowAugment;
|
||||||
import io.xpipe.app.util.JfxHelper;
|
|
||||||
import io.xpipe.app.util.LicenseRequiredException;
|
import io.xpipe.app.util.LicenseRequiredException;
|
||||||
import io.xpipe.app.util.PlatformState;
|
import io.xpipe.app.util.PlatformState;
|
||||||
import javafx.application.Platform;
|
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) {
|
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, () -> {
|
var b = new ButtonComp(null, r, () -> {
|
||||||
takenAction.setValue(a);
|
takenAction.setValue(a);
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -3,55 +3,14 @@ package io.xpipe.app.util;
|
||||||
import atlantafx.base.controls.Spacer;
|
import atlantafx.base.controls.Spacer;
|
||||||
import io.xpipe.app.core.AppFont;
|
import io.xpipe.app.core.AppFont;
|
||||||
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
|
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
|
||||||
import javafx.beans.binding.Bindings;
|
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.Region;
|
import javafx.scene.layout.Region;
|
||||||
import javafx.scene.layout.StackPane;
|
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import org.kordamp.ikonli.javafx.FontIcon;
|
|
||||||
|
|
||||||
public class JfxHelper {
|
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) {
|
public static Region createNamedEntry(String nameString, String descString, String image) {
|
||||||
var header = new Label(nameString);
|
var header = new Label(nameString);
|
||||||
AppFont.header(header);
|
AppFont.header(header);
|
||||||
|
|
|
@ -208,8 +208,13 @@
|
||||||
-fx-opacity: 1.0;
|
-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;
|
-fx-opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue