Rework context menus

This commit is contained in:
crschnick
2026-09-18 17:58:09 +00:00
parent 2f941e1129
commit f8e3fc22f1
19 changed files with 422 additions and 371 deletions
@@ -15,6 +15,7 @@ import io.xpipe.app.platform.PlatformThread;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.ContextMenuWrapper;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.DoubleProperty;
@@ -65,14 +66,19 @@ public class BrowserSessionTabsComp extends SimpleRegionBuilder {
}
if (new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN).match(keyEvent)) {
tabs.getTabs().remove(current);
if (current.isClosable()) {
tabs.getTabs().remove(current);
}
keyEvent.consume();
return;
}
if (new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN, KeyCombination.SHIFT_DOWN)
.match(keyEvent)) {
tabs.getTabs().clear();
tabs.getTabs()
.removeAll(tabs.getTabs().stream()
.filter(t -> t.isClosable())
.toList());
keyEvent.consume();
}
@@ -298,7 +304,7 @@ public class BrowserSessionTabsComp extends SimpleRegionBuilder {
}
private ContextMenu createContextMenu(TabPane tabs, Tab tab, BrowserSessionTab tabModel) {
var cm = MenuHelper.createContextMenu();
var cm = new ContextMenu();
if (tabModel.isCloseable()) {
var unpin = MenuHelper.createMenuItem(LabelGraphic.none(), "unpinTab");
@@ -408,9 +414,6 @@ public class BrowserSessionTabsComp extends SimpleRegionBuilder {
private Tab createTab(TabPane tabs, BrowserSessionTab tabModel) {
var tab = new Tab();
if (tabModel.isCloseable()) {
tab.setContextMenu(createContextMenu(tabs, tab, tabModel));
}
tab.setClosable(tabModel.isCloseable());
// Prevent closing while busy
@@ -514,6 +517,7 @@ public class BrowserSessionTabsComp extends SimpleRegionBuilder {
if (color != null) {
c.getStyleClass().add(color.getId());
}
c.addEventHandler(DragEvent.DRAG_ENTERED, de -> {
// Prevent switch when dragging local files into app
if (tabModel.isCloseable() && !de.getDragboard().hasContent(DataFormat.FILES)) {
@@ -521,6 +525,11 @@ public class BrowserSessionTabsComp extends SimpleRegionBuilder {
de.consume();
}
});
if (tabModel.isCloseable()) {
var cm = new ContextMenuWrapper(() -> createContextMenu(tabs, tab, tabModel));
cm.installOnMouseClick(c, mouseEvent -> mouseEvent.getButton() == MouseButton.SECONDARY, false);
}
});
}
});
@@ -4,6 +4,7 @@ import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.browser.action.BrowserActionProvider;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.fs.FileKind;
import io.xpipe.app.process.ShellControl;
import io.xpipe.app.util.DesktopHelper;
@@ -42,7 +43,11 @@ public class BrowseInNativeManagerActionProvider implements BrowserActionProvide
for (BrowserEntry entry : getEntries()) {
var e = entry.getRawFileEntry().getPath();
var localFile = sc.getLocalSystemAccess().translateToLocalSystemPath(e);
DesktopHelper.browseFileInDirectory(localFile.asLocalPath());
if (entry.getRawFileEntry().getKind() == FileKind.DIRECTORY) {
DesktopHelper.browseFile(localFile.asLocalPath());
} else {
DesktopHelper.browseFileInDirectory(localFile.asLocalPath());
}
}
}
}
@@ -4,10 +4,7 @@ import io.xpipe.app.browser.BrowserFullSessionModel;
import io.xpipe.app.core.AppSystemInfo;
import io.xpipe.app.fs.FileKind;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.GlobalTimer;
import io.xpipe.app.util.OsType;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.app.util.*;
import javafx.scene.Node;
import javafx.scene.control.ContextMenu;
@@ -15,6 +12,7 @@ import javafx.scene.control.TableView;
import javafx.scene.image.Image;
import javafx.scene.input.*;
import javafx.scene.layout.Region;
import lombok.Getter;
import java.io.IOException;
@@ -28,31 +26,25 @@ import java.util.Objects;
public class BrowserFileListCompEntry {
private final TableView<BrowserEntry> tv;
private final Node row;
private final Region row;
private final BrowserEntry item;
private final BrowserFileListModel model;
private final ContextMenuWrapper contextMenu;
private Instant lastHoverUpdate;
private ContextMenu lastContextMenu;
public BrowserFileListCompEntry(
TableView<BrowserEntry> tv, Node row, BrowserEntry item, BrowserFileListModel model) {
TableView<BrowserEntry> tv, Region row, BrowserEntry item, BrowserFileListModel model) {
this.tv = tv;
this.row = row;
this.item = item;
this.model = model;
this.contextMenu = new ContextMenuWrapper(() -> new BrowserContextMenu(model.getFileSystemModel(), item, false));
}
public void onMouseClick(MouseEvent t) {
if (lastContextMenu != null) {
lastContextMenu.hide();
lastContextMenu = null;
}
if (showContextMenu(t)) {
var cm = new BrowserContextMenu(model.getFileSystemModel(), item, false);
cm.show(row, t.getScreenX(), t.getScreenY());
lastContextMenu = cm;
contextMenu.show(row, t.getScreenX(), t.getScreenY());
t.consume();
return;
}
@@ -9,6 +9,7 @@ import io.xpipe.app.core.AppSizeBreakpoints;
import io.xpipe.app.platform.InputHelper;
import io.xpipe.app.platform.MenuHelper;
import io.xpipe.app.platform.PlatformThread;
import io.xpipe.app.util.ContextMenuWrapper;
import io.xpipe.app.util.FilePath;
import io.xpipe.app.util.GlobalTimer;
@@ -75,11 +76,8 @@ public class BrowserFileSystemTabComp extends SimpleRegionBuilder {
var menuButton = MenuHelper.createMenuButton();
menuButton.setGraphic(new FontIcon("mdral-folder_open"));
new ContextMenuAugment<>(
event -> event.getButton() == MouseButton.PRIMARY,
null,
() -> new BrowserContextMenu(model, null, false))
.accept(menuButton);
var cm = new ContextMenuWrapper(() -> new BrowserContextMenu(model, null, false));
cm.installOnMouseClick(menuButton, mouseEvent -> mouseEvent.getButton() == MouseButton.PRIMARY, false);
menuButton.disableProperty().bind(PlatformThread.sync(model.getInOverview()));
RegionDescriptor.builder().nameKey("directoryOptions").build().apply(menuButton);
@@ -5,7 +5,6 @@ import io.xpipe.app.comp.RegionBuilder;
import io.xpipe.app.comp.RegionStructure;
import io.xpipe.app.comp.RegionStructureBuilder;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.comp.base.ContextMenuAugment;
import io.xpipe.app.comp.base.PrettyImageHelper;
import io.xpipe.app.comp.base.TextFieldComp;
import io.xpipe.app.core.AppFontSizes;
@@ -14,6 +13,7 @@ import io.xpipe.app.platform.LabelGraphic;
import io.xpipe.app.platform.MenuHelper;
import io.xpipe.app.platform.PlatformThread;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.ContextMenuWrapper;
import io.xpipe.app.util.ThreadHelper;
import javafx.application.Platform;
@@ -63,9 +63,11 @@ public class BrowserNavBarComp extends RegionStructureBuilder<HBox, BrowserNavBa
var homeButton = new ButtonComp(null, breadcrumbsGraphic, null)
.describe(d -> d.nameKey("directoryOptions"))
.apply(new ContextMenuAugment<>(event -> event.getButton() == MouseButton.PRIMARY, null, () -> {
return model.getInOverview().get() ? null : new BrowserContextMenu(model, null, false);
}))
.apply(struc -> {
var cm = new ContextMenuWrapper(
() -> model.getInOverview().get() ? null : new BrowserContextMenu(model, null, false));
cm.installOnButton(struc);
})
.build();
homeButton.getStyleClass().add(Styles.LEFT_PILL);
homeButton.getStyleClass().add("path-graphic-button");
@@ -75,8 +77,10 @@ public class BrowserNavBarComp extends RegionStructureBuilder<HBox, BrowserNavBa
.describe(
d -> d.nameKey("history").shortcut(new KeyCodeCombination(KeyCode.H, KeyCombination.ALT_DOWN)))
.style(Styles.RIGHT_PILL)
.apply(new ContextMenuAugment<>(
event -> event.getButton() == MouseButton.PRIMARY, null, this::createContextMenu))
.apply(struc -> {
var cm = new ContextMenuWrapper(() -> createContextMenu());
cm.installOnMouseClick(struc, event -> event.getButton() == MouseButton.PRIMARY, false);
})
.build();
AppFontSizes.xs(historyButton);
@@ -212,7 +216,7 @@ public class BrowserNavBarComp extends RegionStructureBuilder<HBox, BrowserNavBa
}
private ContextMenu createContextMenu() {
var cm = MenuHelper.createContextMenu();
var cm = new ContextMenu();
var f = model.getHistory().getForwardHistory(8).stream().toList();
for (int i = f.size() - 1; i >= 0; i--) {
@@ -3,13 +3,13 @@ package io.xpipe.app.browser.file;
import io.xpipe.app.comp.BaseRegionBuilder;
import io.xpipe.app.comp.RegionBuilder;
import io.xpipe.app.comp.SimpleRegionBuilder;
import io.xpipe.app.comp.base.ContextMenuAugment;
import io.xpipe.app.comp.base.HorizontalComp;
import io.xpipe.app.comp.base.IconButtonComp;
import io.xpipe.app.comp.base.LabelComp;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.platform.BindingsHelper;
import io.xpipe.app.platform.PlatformThread;
import io.xpipe.app.util.ContextMenuWrapper;
import io.xpipe.app.util.HumanReadableFormat;
import io.xpipe.app.util.ThreadHelper;
@@ -236,12 +236,5 @@ public class BrowserStatusBarComp extends SimpleRegionBuilder {
r.setOnDragDone(event -> {
emptyEntry.onDragDone(event);
});
// Use status bar as an extension of file list
new ContextMenuAugment<>(
mouseEvent -> mouseEvent.getButton() == MouseButton.SECONDARY,
null,
() -> new BrowserContextMenu(model, null, false))
.accept(r);
}
}
@@ -1,103 +0,0 @@
package io.xpipe.app.comp.base;
import javafx.event.ActionEvent;
import javafx.geometry.Side;
import javafx.scene.control.ButtonBase;
import javafx.scene.control.ContextMenu;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Region;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier;
public class ContextMenuAugment<S extends Region> implements Consumer<S> {
private final Predicate<MouseEvent> mouseEventCheck;
private final Predicate<KeyEvent> keyEventCheck;
private final Supplier<ContextMenu> contextMenu;
public ContextMenuAugment(
Predicate<MouseEvent> mouseEventCheck,
Predicate<KeyEvent> keyEventCheck,
Supplier<ContextMenu> contextMenu) {
this.mouseEventCheck = mouseEventCheck;
this.keyEventCheck = keyEventCheck;
this.contextMenu = contextMenu;
}
@Override
public void accept(S struc) {
var currentContextMenu = new AtomicReference<ContextMenu>();
Supplier<Boolean> hide = () -> {
if (currentContextMenu.get() != null && currentContextMenu.get().isShowing()) {
currentContextMenu.get().hide();
currentContextMenu.set(null);
return true;
}
return false;
};
var r = struc;
r.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
var hidden = hide.get();
if (mouseEventCheck != null && mouseEventCheck.test(event)) {
if (!hidden) {
var cm = contextMenu.get();
if (cm != null) {
cm.show(r, event.getScreenX(), event.getScreenY());
currentContextMenu.set(cm);
}
}
event.consume();
}
});
r.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
if (mouseEventCheck != null && mouseEventCheck.test(event)) {
event.consume();
}
});
r.addEventHandler(KeyEvent.KEY_RELEASED, event -> {
if (keyEventCheck != null && keyEventCheck.test(event)) {
event.consume();
}
});
r.addEventHandler(KeyEvent.KEY_PRESSED, event -> {
if (keyEventCheck != null) {
var hidden = hide.get();
if (keyEventCheck.test(event)) {
if (!hidden) {
var cm = contextMenu.get();
if (cm != null) {
cm.show(r, Side.BOTTOM, 0, 0);
currentContextMenu.set(cm);
}
}
event.consume();
}
}
});
if (r instanceof ButtonBase buttonBase && keyEventCheck == null) {
buttonBase.addEventHandler(ActionEvent.ACTION, event -> {
if (buttonBase.getOnAction() != null) {
return;
}
if (!hide.get()) {
var cm = contextMenu.get();
if (cm != null) {
cm.show(r, Side.TOP, 0, 0);
currentContextMenu.set(cm);
}
}
event.consume();
});
}
}
}
@@ -12,6 +12,7 @@ import io.xpipe.app.platform.*;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreColor;
import io.xpipe.app.util.ContextMenuWrapper;
import io.xpipe.app.util.DesktopHelper;
import io.xpipe.app.util.OsType;
@@ -68,7 +69,9 @@ public class StoreCategoryComp extends SimpleRegionBuilder {
});
})
.build();
var showing = new SimpleBooleanProperty();
var contextMenu = new ContextMenuWrapper(() -> createContextMenu());
var contextMenuShowing = new SimpleBooleanProperty();
var expandIcon = Bindings.createObjectBinding(
() -> {
@@ -123,12 +126,9 @@ public class StoreCategoryComp extends SimpleRegionBuilder {
struc.setAlignment(Pos.CENTER);
struc.setPadding(new Insets(0, 0, 0, 0));
})
.apply(new ContextMenuAugment<>(
mouseEvent -> mouseEvent.getButton() == MouseButton.PRIMARY, null, () -> {
var cm = createContextMenu();
showing.bind(cm.showingProperty());
return cm;
}))
.apply(struc -> {
contextMenu.installOnButton(struc);
})
.describe(d -> d.nameKey("configuration"))
.style("status-button");
@@ -156,7 +156,7 @@ public class StoreCategoryComp extends SimpleRegionBuilder {
var dragIntoIndicator = createDragIntoIndicator(dragOver);
var showStatus = hover.or(new SimpleBooleanProperty(DataStorage.get().syncEnabled()))
.or(showing)
.or(contextMenuShowing)
.or(focus);
var indentSpacer = RegionBuilder.hspacer(Bindings.createDoubleBinding(() -> {
return category.getDepth().getValue() * 8.0;
@@ -200,12 +200,11 @@ public class StoreCategoryComp extends SimpleRegionBuilder {
});
});
h.apply(new ContextMenuAugment<>(
mouseEvent -> mouseEvent.getButton() == MouseButton.SECONDARY,
keyEvent -> keyEvent.getCode() == KeyCode.SPACE,
() -> createContextMenu()));
h.apply(struc -> {
struc.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
contextMenu.installOnMouseClick(struc, mouseEvent -> mouseEvent.getButton() == MouseButton.SECONDARY, true);
});
h.apply(struc -> {
struc.addEventHandler(KeyEvent.KEY_PRESSED, event -> {
if (event.getCode() == KeyCode.SPACE) {
category.toggleExpanded();
event.consume();
@@ -452,7 +451,7 @@ public class StoreCategoryComp extends SimpleRegionBuilder {
}
private ContextMenu createContextMenu() {
var contextMenu = MenuHelper.createContextMenu();
var contextMenu = new ContextMenu();
if (AppPrefs.get().enableHttpApi().get()) {
var copyId = new MenuItem(AppI18n.get("copyId"), new FontIcon("mdi2c-content-copy"));
@@ -82,7 +82,7 @@ public class DenseStoreEntryComp extends StoreEntryComp {
? contentRegion
: RegionBuilder.empty().hide(new ReadOnlyBooleanWrapper(true)).build();
cr.getStyleClass().add("custom-content");
var bb = createButtonBar(name);
var bb = createButtonBar();
var controls = new HBox(cr, bb);
controls.setFillHeight(true);
HBox.setHgrow(cr, Priority.ALWAYS);
@@ -99,7 +99,7 @@ public class StandardStoreEntryComp extends StoreEntryComp {
? contentRegion
: RegionBuilder.empty().hide(new ReadOnlyBooleanWrapper(true)).build();
cr.getStyleClass().add("custom-content");
var bb = createButtonBar(name);
var bb = createButtonBar();
var controls = new HBox(cr, bb);
controls.setFillHeight(true);
HBox.setHgrow(cr, Priority.ALWAYS);
@@ -47,21 +47,30 @@ public interface StoreEntryBadge {
}
static Action providerMenu(String... ids) {
var cm = new AtomicReference<ContextMenu>();
return new Action() {
@Override
public void run(StoreEntryWrapper wrapper, Button button) {
if (cm.get() == null) {
cm.set(MenuHelper.createContextMenu());
private ContextMenu contextMenu;
private void buildContextMenu(StoreEntryWrapper wrapper) {
if (contextMenu == null) {
contextMenu = new ContextMenu();
}
contextMenu.getItems().clear();
var provs = getProviders(wrapper);
cm.get().getItems().clear();
for (var p : provs) {
var item = StoreEntryComp.buildMenuItemForAction(wrapper, p);
cm.get().getItems().add(item);
contextMenu.getItems().add(item);
}
MenuHelper.show(cm.get(), button, Side.BOTTOM);
}
@Override
public void run(StoreEntryWrapper wrapper, Button button) {
var cm = new ContextMenuWrapper(() -> {
buildContextMenu(wrapper);
return contextMenu;
});
cm.show(button, Side.BOTTOM);
}
@Override
@@ -189,58 +198,71 @@ public interface StoreEntryBadge {
return null;
}
var cm = new AtomicReference<ContextMenu>();
var busy = new SimpleBooleanProperty();
return of("mdi2s-server-network-outline", effective).withAction((wrapper, b) -> {
b.opacityProperty()
.bind(PlatformThread.sync(Bindings.createDoubleBinding(
() -> {
return busy.get() ? 0.5 : 1.0;
},
busy)));
return of("mdi2s-server-network-outline", effective).withAction(new Action() {
if (wrapper.getEntry().getStore() instanceof HostAddressStore has) {
if (busy.get()) {
return;
private ContextMenu contextMenu;
private void buildContextMenu(HostAddress has) {
if (contextMenu == null) {
contextMenu = new ContextMenu();
}
ThreadHelper.runFailableAsync(() -> {
BooleanScope.executeExclusive(busy, () -> {
has.refreshHostAddressOrThrow();
contextMenu.getItems().clear();
for (var a : has.getAvailable()) {
var i = new MenuItem();
i.setText(a);
i.setGraphic(new FontIcon("mdi2c-clipboard-multiple-outline"));
i.setOnAction(event -> {
ClipboardHelper.copyText(a);
event.consume();
});
var refreshed = has.getHostAddress();
if (refreshed == null || refreshed.isEmpty()) {
return;
}
if (refreshed.isSingle()) {
ClipboardHelper.copyText(refreshed.get());
return;
}
Platform.runLater(() -> {
if (cm.get() == null) {
cm.set(MenuHelper.createContextMenu());
}
cm.get().getItems().clear();
for (var a : refreshed.getAvailable()) {
var i = new MenuItem();
i.setText(a);
i.setGraphic(new FontIcon("mdi2c-clipboard-multiple-outline"));
i.setOnAction(event -> {
ClipboardHelper.copyText(a);
event.consume();
});
cm.get().getItems().add(i);
}
MenuHelper.show(cm.get(), b, Side.BOTTOM);
});
});
} else {
ClipboardHelper.copyText(effective);
contextMenu.getItems().add(i);
}
}
@Override
public void run(StoreEntryWrapper wrapper, Button b) {
b.opacityProperty()
.bind(PlatformThread.sync(Bindings.createDoubleBinding(
() -> {
return busy.get() ? 0.5 : 1.0;
},
busy)));
if (wrapper.getEntry().getStore() instanceof HostAddressStore has) {
if (busy.get()) {
return;
}
ThreadHelper.runFailableAsync(() -> {
BooleanScope.executeExclusive(busy, () -> {
has.refreshHostAddressOrThrow();
});
var refreshed = has.getHostAddress();
if (refreshed == null || refreshed.isEmpty()) {
return;
}
if (refreshed.isSingle()) {
ClipboardHelper.copyText(refreshed.get());
return;
}
Platform.runLater(() -> {
var cm = new ContextMenuWrapper(() -> {
buildContextMenu(addr);
return contextMenu;
});
cm.show(b, Side.BOTTOM);
});
});
} else {
ClipboardHelper.copyText(effective);
}
}
});
}
@@ -39,7 +39,6 @@ import atlantafx.base.layout.InputGroup;
import atlantafx.base.theme.Styles;
import org.kordamp.ikonli.javafx.FontIcon;
import java.lang.ref.WeakReference;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
@@ -63,10 +62,11 @@ public abstract class StoreEntryComp extends SimpleRegionBuilder {
}
},
App.getApp().getStage().widthProperty());
private static String DEFAULT_NOTES = null;
protected final StoreSection section;
protected final BaseRegionBuilder<?, ?> content;
protected final IntegerProperty contextMenuCount = new SimpleIntegerProperty();
protected final ContextMenuWrapper contextMenu = new ContextMenuWrapper(() -> createContextMenu());
public StoreEntryComp(StoreSection section, BaseRegionBuilder<?, ?> content) {
this.section = section;
@@ -137,10 +137,6 @@ public abstract class StoreEntryComp extends SimpleRegionBuilder {
return;
}
if (getWrapper().getRenaming().get()) {
return;
}
var count = AppPrefs.get().requireDoubleClickForConnections().get() ? 2 : 1;
if (mouseEvent.getClickCount() != count) {
return;
@@ -157,11 +153,7 @@ public abstract class StoreEntryComp extends SimpleRegionBuilder {
mouseEvent.consume();
});
new ContextMenuAugment<>(
mouseEvent -> mouseEvent.getButton() == MouseButton.SECONDARY,
null,
() -> this.createContextMenu(name))
.accept(r);
contextMenu.installOnMouseClick(r, mouseEvent -> mouseEvent.getButton() == MouseButton.SECONDARY, true);
var loading =
new LoadingOverlayComp(RegionBuilder.of(() -> r), getWrapper().getEffectiveBusy(), false);
@@ -224,7 +216,10 @@ public abstract class StoreEntryComp extends SimpleRegionBuilder {
var name = new LazyTextFieldComp(prop);
name.style("name");
name.applyStructure(struc -> {
getWrapper().getRenaming().bind(struc.getTextField().focusedProperty());
getWrapper().getRenameTrigger().onFire(() -> {
struc.get().requestFocus();
struc.getTextField().selectAll();
});
});
return name;
}
@@ -337,7 +332,7 @@ public abstract class StoreEntryComp extends SimpleRegionBuilder {
return stack;
}
protected Region createButtonBar(Region name) {
protected Region createButtonBar() {
var list = DerivedObservableList.wrap(getWrapper().getMajorActionProviders(), false);
var buttons = list.mapped(actionProvider -> {
var button = buildButton(actionProvider);
@@ -349,7 +344,7 @@ public abstract class StoreEntryComp extends SimpleRegionBuilder {
var ig = new InputGroup();
Runnable update = () -> {
var l = new ArrayList<Node>(buttons);
var settingsButton = createSettingsButton(name).build();
var settingsButton = createSettingsButton().build();
l.add(settingsButton);
l.forEach(o -> o.getStyleClass().remove(Styles.FLAT));
ig.getChildren().setAll(l);
@@ -373,56 +368,55 @@ public abstract class StoreEntryComp extends SimpleRegionBuilder {
}
: null);
if (branch != null) {
button.apply(new ContextMenuAugment<>(
mouseEvent -> mouseEvent.getButton() == MouseButton.PRIMARY, keyEvent -> false, () -> {
var cm = MenuHelper.createContextMenu();
var children = branch
.getChildren(getWrapper().getEntry().ref())
.stream()
.filter(hubMenuItemProvider -> {
return hubMenuItemProvider.isApplicable(
getWrapper().getEntry().ref());
})
.toList();
var cats = Arrays.stream(StoreActionCategory.values())
.collect(Collectors.toCollection(ArrayList::new));
cats.addFirst(null);
for (var cat : cats) {
var catChildren = children.stream()
.filter(actionProvider -> actionProvider.getCategory() == cat)
.toList();
if (catChildren.isEmpty()) {
continue;
}
var buttonCm = new ContextMenuWrapper(() -> {
var cm = new ContextMenu();
var children = branch
.getChildren(getWrapper().getEntry().ref())
.stream()
.filter(hubMenuItemProvider -> {
return hubMenuItemProvider.isApplicable(
getWrapper().getEntry().ref());
})
.toList();
var cats = Arrays.stream(StoreActionCategory.values())
.collect(Collectors.toCollection(ArrayList::new));
cats.addFirst(null);
for (var cat : cats) {
var catChildren = children.stream()
.filter(actionProvider -> actionProvider.getCategory() == cat)
.toList();
if (catChildren.isEmpty()) {
continue;
}
catChildren.forEach(childProvider -> {
var menu = buildMenuItemForAction(getWrapper(), childProvider);
if (menu != null) {
cm.getItems().add(menu);
}
});
cm.getItems().add(new SeparatorMenuItem());
catChildren.forEach(childProvider -> {
var menu = buildMenuItemForAction(getWrapper(), childProvider);
if (menu != null) {
cm.getItems().add(menu);
}
});
cm.getItems().add(new SeparatorMenuItem());
}
if (cm.getItems().getLast() instanceof SeparatorMenuItem) {
cm.getItems().removeLast();
}
if (cm.getItems().getLast() instanceof SeparatorMenuItem) {
cm.getItems().removeLast();
}
return cm;
}));
return cm;
});
button.apply(struc -> buttonCm.installOnButton(struc));
}
button.describe(d -> d.name(p.getName(getWrapper().getEntry().ref())));
return button;
}
protected BaseRegionBuilder<?, ?> createSettingsButton(Region name) {
protected BaseRegionBuilder<?, ?> createSettingsButton() {
var settingsButton = new IconButtonComp("mdi2d-dots-horizontal-circle-outline", null);
settingsButton.style("settings");
settingsButton.describe(d -> d.nameKey("more"));
settingsButton.apply(new ContextMenuAugment<>(
event -> event.getButton() == MouseButton.PRIMARY,
null,
() -> StoreEntryComp.this.createContextMenu(name)));
settingsButton.apply(struc -> {
contextMenu.installOnButton(struc);
});
return settingsButton;
}
@@ -432,20 +426,8 @@ public abstract class StoreEntryComp extends SimpleRegionBuilder {
return c;
}
private void handleContextMenuCount(ContextMenu contextMenu) {
var ref = new WeakReference<>(contextMenu);
contextMenuCount.set(contextMenuCount.get() + 1);
contextMenuCount.addListener((observable, oldValue, newValue) -> {
var cm = ref.get();
if (cm != null) {
cm.hide();
}
});
}
protected ContextMenu createContextMenu(Region name) {
var contextMenu = MenuHelper.createContextMenu();
handleContextMenuCount(contextMenu);
protected ContextMenu createContextMenu() {
var contextMenu = new ContextMenu();
var cats = Arrays.stream(StoreActionCategory.values()).collect(Collectors.toCollection(ArrayList::new));
cats.addFirst(null);
@@ -470,7 +452,7 @@ public abstract class StoreEntryComp extends SimpleRegionBuilder {
case OsType.Windows ignored -> new KeyCodeCombination(KeyCode.F2);
});
rename.setOnAction(event -> {
name.requestFocus();
getWrapper().getRenameTrigger().fire(null);
event.consume();
});
items.add(items.size(), rename);
@@ -589,7 +571,7 @@ public abstract class StoreEntryComp extends SimpleRegionBuilder {
items.add(tags);
}
if (getWrapper().canBreakOutCategory()) {
if (getWrapper().canToggleBreakOutCategory()) {
var breakOut = new MenuItem();
var is = getWrapper().getBreakoutCategory().isPresent();
if (is) {
@@ -36,6 +36,7 @@ import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import lombok.Getter;
import org.int4.fx.values.util.Trigger;
import java.net.Inet4Address;
import java.time.Instant;
@@ -74,13 +75,13 @@ public class StoreEntryWrapper {
private final Property<String> shownDescription = new SimpleObjectProperty<>();
private final Property<StoreEntryInformation> shownInformation = new SimpleObjectProperty<>();
private final BooleanProperty template = new SimpleBooleanProperty();
private final BooleanProperty renaming = new SimpleBooleanProperty();
private final BooleanProperty pinToTop = new SimpleBooleanProperty();
private final DoubleProperty orderIndex = new SimpleDoubleProperty();
private final BooleanProperty effectiveBusy = new SimpleBooleanProperty();
private final Property<StoreCategoryWrapper> lastInformationCategory = new SimpleObjectProperty<>();
private final ObservableList<String> tags = FXCollections.observableArrayList();
private final Property<Inet4Address> nameIpAddress = new SimpleObjectProperty<>();
private final Trigger<Void> renameTrigger = Trigger.of();
private boolean effectiveBusyProviderBound = false;
public StoreEntryWrapper(DataStoreEntry entry) {
@@ -387,10 +388,17 @@ public class StoreEntryWrapper {
return false;
}
public boolean canBreakOutCategory() {
public boolean canToggleBreakOutCategory() {
if (entry.getBreakOutCategory() != null) {
return true;
}
var section = StoreViewState.get().getSectionForWrapper(this);
var parentSection = StoreViewState.get().getParentSectionForWrapper(this);
return (getStore().getValue() instanceof FixedHierarchyStore
|| getStore().getValue() instanceof GroupStore<?>)
&& StoreViewState.get().getParentSectionForWrapper(this).isPresent();
&& parentSection.isPresent()
&& section.isPresent() && !section.get().getAllChildren().getList().isEmpty();
}
public void breakOutCategory() {
@@ -8,6 +8,7 @@ import io.xpipe.app.platform.LabelGraphic;
import io.xpipe.app.platform.MenuHelper;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.ContextMenuWrapper;
import javafx.geometry.Side;
import javafx.scene.control.Button;
import javafx.scene.control.ContextMenu;
@@ -28,12 +29,12 @@ public class StoreQuickAccessButtonComp extends RegionBuilder<Button> {
this.action = action;
}
private ContextMenu createMenu() {
private ContextMenu createContextMenu() {
if (section.getShownChildren().getList().isEmpty()) {
return null;
}
var cm = MenuHelper.createContextMenu();
var cm = new ContextMenu();
cm.getStyleClass().add("condensed");
Menu menu = (Menu) recurse(cm, section);
cm.getItems().addAll(menu.getItems());
@@ -85,21 +86,10 @@ public class StoreQuickAccessButtonComp extends RegionBuilder<Button> {
@Override
public Button createSimple() {
var cm = new ContextMenuWrapper(() -> createContextMenu());
var button = new IconButtonComp("mdi2c-chevron-double-right");
button.apply(struc -> {
AtomicReference<ContextMenu> menu = new AtomicReference<>();
struc.setOnAction(event -> {
if (menu.get() == null) {
menu.set(createMenu());
}
if (menu.get() == null) {
return;
}
MenuHelper.toggleMenuShow(menu.get(), struc, Side.RIGHT);
event.consume();
});
cm.installOnButton(struc);
});
return button.build();
}
@@ -15,12 +15,14 @@ import io.xpipe.app.platform.MenuHelper;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.store.DataStore;
import io.xpipe.app.util.ContextMenuWrapper;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.ListChangeListener;
import javafx.geometry.Pos;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuItem;
import javafx.scene.input.MouseButton;
@@ -203,12 +205,14 @@ public class StoreEntryListBatchBarComp extends SimpleRegionBuilder {
},
expanded,
s.getName());
var button = new ButtonComp(name, new SimpleObjectProperty<>(s.getIcon()), () -> {
if (batchActions.size() > 0) {
return;
}
runActions(s);
var button = new ButtonComp(name, s.getIcon(), null);
button.apply(struc -> {
struc.setOnAction(e -> {
if (batchActions.size() == 0) {
runActions(s);
e.consume();
}
});
});
button.describe(d -> d.name(s.getName()));
@@ -219,15 +223,17 @@ public class StoreEntryListBatchBarComp extends SimpleRegionBuilder {
childrenRefs.getList()));
if (batchActions.size() > 0) {
button.apply(new ContextMenuAugment<>(
mouseEvent -> mouseEvent.getButton() == MouseButton.PRIMARY, keyEvent -> false, () -> {
var cm = MenuHelper.createContextMenu();
s.getChildren(childrenRefs.getList()).forEach(childProvider -> {
var menu = buildMenuItemForAction(childrenRefs.getList(), childProvider);
cm.getItems().add(menu);
});
return cm;
}));
var cm = new ContextMenuWrapper(() -> {
var c = new ContextMenu();
s.getChildren(childrenRefs.getList()).forEach(childProvider -> {
var menu = buildMenuItemForAction(childrenRefs.getList(), childProvider);
c.getItems().add(menu);
});
return c;
});
button.apply(struc -> {
cm.installOnButton(struc);
});
}
return button;
}
@@ -2,7 +2,6 @@ package io.xpipe.app.hub.list;
import io.xpipe.app.comp.BaseRegionBuilder;
import io.xpipe.app.comp.SimpleRegionBuilder;
import io.xpipe.app.comp.base.ContextMenuAugment;
import io.xpipe.app.comp.base.ListBoxViewComp;
import io.xpipe.app.comp.base.MultiContentComp;
import io.xpipe.app.comp.base.VerticalComp;
@@ -14,6 +13,7 @@ import io.xpipe.app.hub.section.StoreSectionComp;
import io.xpipe.app.platform.MenuHelper;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.ContextMenuWrapper;
import javafx.animation.AnimationTimer;
import javafx.beans.binding.Bindings;
import javafx.beans.property.DoubleProperty;
@@ -22,6 +22,7 @@ import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.value.ObservableValue;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.ScrollBar;
import javafx.scene.control.ScrollPane;
import javafx.scene.input.DragEvent;
@@ -81,14 +82,12 @@ public class StoreEntryListComp extends SimpleRegionBuilder {
content.style("store-list-comp");
content.vgrow();
content.apply(s -> {
var cm = new ContextMenuAugment<>(me -> me.getButton() == MouseButton.SECONDARY, null, () -> {
var menu = MenuHelper.createContextMenu();
StoreCreationMenu.addButtons(menu.getItems(), false);
return menu;
});
cm.accept(s);
var cm = new ContextMenuWrapper(() -> {
var menu = new ContextMenu();
StoreCreationMenu.addButtons(menu.getItems(), false);
return menu;
});
content.apply(struc -> cm.installOnMouseClick(struc, mouseEvent -> mouseEvent.getButton() == MouseButton.SECONDARY, true));
content.apply(s -> {
setupBorderScroll(s);
@@ -270,12 +269,12 @@ public class StoreEntryListComp extends SimpleRegionBuilder {
var map = new LinkedHashMap<BaseRegionBuilder<?, ?>, ObservableValue<Boolean>>();
map.put(
new StoreNotFoundComp().apply(s -> {
var cm = new ContextMenuAugment<>(me -> me.getButton() == MouseButton.SECONDARY, null, () -> {
var menu = MenuHelper.createContextMenu();
var cm = new ContextMenuWrapper(() -> {
var menu = new ContextMenu();
StoreCreationMenu.addButtons(menu.getItems(), false);
return menu;
});
cm.accept(s);
cm.installOnMouseClick(s, mouseEvent -> mouseEvent.getButton() == MouseButton.SECONDARY, true);
}),
Bindings.and(
Bindings.not(Bindings.isEmpty(
@@ -148,10 +148,6 @@ public class StoreSectionComp extends StoreSectionBaseComp {
entryButton.hgrow();
entryButton.apply(struc -> {
struc.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
if (section.getWrapper().getRenaming().get()) {
return;
}
if (event.getCode() == KeyCode.SPACE) {
section.getWrapper().toggleExpanded();
event.consume();
@@ -48,34 +48,6 @@ public class MenuHelper {
return cb;
}
public static ContextMenu createContextMenu() {
ContextMenu contextMenu = new ContextMenu();
contextMenu.setAutoHide(!AppPrefs.get().limitedTouchscreenMode().get());
InputHelper.onLeft(contextMenu, false, e -> {
contextMenu.hide();
e.consume();
});
contextMenu.addEventFilter(Menu.ON_SHOWING, e -> {
Node content = contextMenu.getSkin().getNode();
if (content instanceof Region r) {
r.setMaxWidth(500);
}
});
contextMenu.addEventFilter(Menu.ON_SHOWN, e -> {
Platform.runLater(() -> {
var first = contextMenu.getItems().getFirst();
if (first != null) {
var s = first.getStyleableNode();
if (s != null) {
s.requestFocus();
}
}
});
});
AppFontSizes.lg(contextMenu.getStyleableNode());
return contextMenu;
}
public static MenuItem createMenuItem(LabelGraphic graphic, String nameKey) {
var i = new MenuItem();
i.textProperty().bind(AppI18n.observable(nameKey));
@@ -92,16 +64,4 @@ public class MenuHelper {
contextMenu.show(ref, side, 0, 0);
}
}
public static void toggleMenuShow(ContextMenu contextMenu, Node ref, Side side) {
if (!contextMenu.isShowing()) {
// Prevent NPE in show()
if (contextMenu.getScene() == null || ref == null || ref.getScene() == null) {
return;
}
contextMenu.show(ref, side, 0, 0);
} else {
contextMenu.hide();
}
}
}
@@ -0,0 +1,191 @@
package io.xpipe.app.util;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.platform.InputHelper;
import io.xpipe.app.prefs.AppPrefs;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.geometry.Side;
import javafx.scene.Node;
import javafx.scene.control.ButtonBase;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuItem;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Region;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Predicate;
import java.util.function.Supplier;
public class ContextMenuWrapper {
private static final Set<ContextMenu> allContextMenus = new HashSet<>();
private final Supplier<ContextMenu> contextMenuSupplier;
private ContextMenu contextMenu;
public ContextMenuWrapper(Supplier<ContextMenu> contextMenuSupplier) {this.contextMenuSupplier = contextMenuSupplier;}
private void applyFixes() {
contextMenu.setAutoHide(!AppPrefs.get().limitedTouchscreenMode().get());
InputHelper.onLeft(contextMenu, false, e -> {
contextMenu.hide();
e.consume();
});
contextMenu.addEventFilter(Menu.ON_SHOWING, e -> {
Node content = contextMenu.getSkin().getNode();
if (content instanceof Region r) {
r.setMaxWidth(500);
}
});
contextMenu.addEventFilter(Menu.ON_SHOWN, e -> {
Platform.runLater(() -> {
var first = contextMenu.getItems().getFirst();
if (first != null) {
var s = first.getStyleableNode();
if (s != null) {
s.requestFocus();
}
}
});
});
AppFontSizes.lg(contextMenu.getStyleableNode());
contextMenu.addEventFilter(KeyEvent.KEY_PRESSED, e -> {
for (MenuItem item : contextMenu.getItems()) {
if (item.getAccelerator() != null && item.getAccelerator().match(e)) {
contextMenu.hide();
item.fire();
e.consume();
}
}
});
}
private ContextMenu getOrCreate() {
if (contextMenu == null) {
contextMenu = contextMenuSupplier.get();
if (contextMenu != null) {
applyFixes();
}
}
return contextMenu;
}
private boolean isHidden() {
return contextMenu == null || !contextMenu.isShowing();
}
private boolean isShowing() {
return contextMenu != null && contextMenu.isShowing();
}
private void hideOthers(boolean includeSelf) {
for (ContextMenu cm : allContextMenus) {
if (!includeSelf && cm.equals(contextMenu)) {
continue;
}
cm.hide();
}
}
public boolean show(Region r, Side side) {
if (isShowing()) {
return true;
}
var cm = getOrCreate();
if (cm != null) {
// Prevent NPE in show()
if (contextMenu.getScene() == null || r == null || r.getScene() == null) {
return false;
}
cm.show(r, side, 0, 0);
allContextMenus.add(cm);
return true;
} else {
return false;
}
}
public boolean show(Region r, double x, double y) {
if (isShowing()) {
return true;
}
var cm = getOrCreate();
if (cm != null) {
// Prevent NPE in show()
if (contextMenu.getScene() == null || r == null || r.getScene() == null) {
return false;
}
cm.show(r, x, y);
allContextMenus.add(cm);
return true;
} else {
return false;
}
}
public void installOnButton(ButtonBase r) {
installOnButton(r, Side.BOTTOM);
}
public void installOnButton(ButtonBase r, Side side) {
r.addEventFilter(ActionEvent.ACTION, event -> {
if (r.getOnAction() != null) {
return;
}
var showing = isShowing();
hideOthers(showing);
if (!showing && show(r, side)) {
event.consume();
}
});
}
public void installOnMouseClick(Region r, Predicate<MouseEvent> mouseEventCheck, boolean showAtLocation) {
r.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
if (mouseEventCheck != null && mouseEventCheck.test(event)) {
hideOthers(showAtLocation);
var shown = (showAtLocation && show(r, event.getScreenX(), event.getScreenY())) || (!showAtLocation && show(r, Side.BOTTOM));
if (shown) {
event.consume();
}
}
});
r.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
if (mouseEventCheck != null && mouseEventCheck.test(event)) {
event.consume();
}
});
}
public void installOnKeyPress(Region r, Predicate<KeyEvent> keyEventCheck) {
r.addEventHandler(KeyEvent.KEY_RELEASED, event -> {
if (keyEventCheck != null && keyEventCheck.test(event)) {
event.consume();
}
});
r.addEventHandler(KeyEvent.KEY_PRESSED, event -> {
if (keyEventCheck != null) {
if (keyEventCheck.test(event)) {
var showing = isShowing();
hideOthers(showing);
if (!showing && show(r, Side.BOTTOM)) {
event.consume();
}
}
}
});
}
}