diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserSessionTabsComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserSessionTabsComp.java index d21c45001..a7eed34c4 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserSessionTabsComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserSessionTabsComp.java @@ -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); + } }); } }); diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/BrowseInNativeManagerActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/BrowseInNativeManagerActionProvider.java index 5775e47d6..0abd70110 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/impl/BrowseInNativeManagerActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/BrowseInNativeManagerActionProvider.java @@ -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()); + } } } } diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListCompEntry.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListCompEntry.java index af01f46b2..3eb275640 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListCompEntry.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListCompEntry.java @@ -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 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 tv, Node row, BrowserEntry item, BrowserFileListModel model) { + TableView 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; } diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemTabComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemTabComp.java index 981742099..0352ffc33 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemTabComp.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemTabComp.java @@ -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); diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserNavBarComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserNavBarComp.java index ba524d1a9..639529999 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserNavBarComp.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserNavBarComp.java @@ -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 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 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= 0; i--) { diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserStatusBarComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserStatusBarComp.java index 839d2bced..d898016d9 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserStatusBarComp.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserStatusBarComp.java @@ -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); } } diff --git a/app/src/main/java/io/xpipe/app/comp/base/ContextMenuAugment.java b/app/src/main/java/io/xpipe/app/comp/base/ContextMenuAugment.java deleted file mode 100644 index 1224dd432..000000000 --- a/app/src/main/java/io/xpipe/app/comp/base/ContextMenuAugment.java +++ /dev/null @@ -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 implements Consumer { - - private final Predicate mouseEventCheck; - private final Predicate keyEventCheck; - private final Supplier contextMenu; - - public ContextMenuAugment( - Predicate mouseEventCheck, - Predicate keyEventCheck, - Supplier contextMenu) { - this.mouseEventCheck = mouseEventCheck; - this.keyEventCheck = keyEventCheck; - this.contextMenu = contextMenu; - } - - @Override - public void accept(S struc) { - var currentContextMenu = new AtomicReference(); - - Supplier 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(); - }); - } - } -} diff --git a/app/src/main/java/io/xpipe/app/hub/category/StoreCategoryComp.java b/app/src/main/java/io/xpipe/app/hub/category/StoreCategoryComp.java index 3550d24d9..313f6daa7 100644 --- a/app/src/main/java/io/xpipe/app/hub/category/StoreCategoryComp.java +++ b/app/src/main/java/io/xpipe/app/hub/category/StoreCategoryComp.java @@ -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")); diff --git a/app/src/main/java/io/xpipe/app/hub/entry/DenseStoreEntryComp.java b/app/src/main/java/io/xpipe/app/hub/entry/DenseStoreEntryComp.java index 9a442ff5e..0fb41e279 100644 --- a/app/src/main/java/io/xpipe/app/hub/entry/DenseStoreEntryComp.java +++ b/app/src/main/java/io/xpipe/app/hub/entry/DenseStoreEntryComp.java @@ -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); diff --git a/app/src/main/java/io/xpipe/app/hub/entry/StandardStoreEntryComp.java b/app/src/main/java/io/xpipe/app/hub/entry/StandardStoreEntryComp.java index 95bbef09c..b9dc927e4 100644 --- a/app/src/main/java/io/xpipe/app/hub/entry/StandardStoreEntryComp.java +++ b/app/src/main/java/io/xpipe/app/hub/entry/StandardStoreEntryComp.java @@ -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); diff --git a/app/src/main/java/io/xpipe/app/hub/entry/StoreEntryBadge.java b/app/src/main/java/io/xpipe/app/hub/entry/StoreEntryBadge.java index f7e881867..e4d5b2e31 100644 --- a/app/src/main/java/io/xpipe/app/hub/entry/StoreEntryBadge.java +++ b/app/src/main/java/io/xpipe/app/hub/entry/StoreEntryBadge.java @@ -47,21 +47,30 @@ public interface StoreEntryBadge { } static Action providerMenu(String... ids) { - var cm = new AtomicReference(); 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(); 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); + } + } + }); } diff --git a/app/src/main/java/io/xpipe/app/hub/entry/StoreEntryComp.java b/app/src/main/java/io/xpipe/app/hub/entry/StoreEntryComp.java index d8bd4eb1f..f420fd29c 100644 --- a/app/src/main/java/io/xpipe/app/hub/entry/StoreEntryComp.java +++ b/app/src/main/java/io/xpipe/app/hub/entry/StoreEntryComp.java @@ -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(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) { diff --git a/app/src/main/java/io/xpipe/app/hub/entry/StoreEntryWrapper.java b/app/src/main/java/io/xpipe/app/hub/entry/StoreEntryWrapper.java index 4a34b4834..2f832fd79 100644 --- a/app/src/main/java/io/xpipe/app/hub/entry/StoreEntryWrapper.java +++ b/app/src/main/java/io/xpipe/app/hub/entry/StoreEntryWrapper.java @@ -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 shownDescription = new SimpleObjectProperty<>(); private final Property 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 lastInformationCategory = new SimpleObjectProperty<>(); private final ObservableList tags = FXCollections.observableArrayList(); private final Property nameIpAddress = new SimpleObjectProperty<>(); + private final Trigger 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() { diff --git a/app/src/main/java/io/xpipe/app/hub/entry/StoreQuickAccessButtonComp.java b/app/src/main/java/io/xpipe/app/hub/entry/StoreQuickAccessButtonComp.java index 3b7b3b55c..eb906e3fc 100644 --- a/app/src/main/java/io/xpipe/app/hub/entry/StoreQuickAccessButtonComp.java +++ b/app/src/main/java/io/xpipe/app/hub/entry/StoreQuickAccessButtonComp.java @@ -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