Cell:如何通过键盘激活contextMenu?

单元格contextMenu不能被键盘激活 :它的根本原因是contextMenuEvent被调度到聚焦节点 – 这是包含表而不是单元格。 Jonathan的错误评估概述了如何解决它:

执行此操作的“正确”方法是覆盖TableView中的buildEventDispatchChain并包含TableViewSkin(如果它实现EventDispatcher),并将其转发到表行中的单元格。

试图遵循该路径(下面是ListView的一个示例,因为只有一个级别的皮肤要实现而不是两个用于TableView)。 它的工作方式是:单元格contextMenu由键盘弹出触发器激活,但相对于表格相对于单元格而言是相对的。

问题:如何挂钩到调度链,使其相对于单元格?

可运行代码示例:

package de.swingempire.fx.scene.control.et; import javafx.application.Application; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.event.EventDispatchChain; import javafx.event.EventTarget; import javafx.scene.Node; import javafx.scene.Parent; import javafx.scene.Scene; import javafx.scene.control.Cell; import javafx.scene.control.ContextMenu; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; import javafx.scene.control.MenuItem; import javafx.scene.control.Skin; import javafx.stage.Stage; import com.sun.javafx.event.EventHandlerManager; import com.sun.javafx.scene.control.skin.ListViewSkin; /** * Activate cell contextMenu by keyboard, quick shot on ListView * @author Jeanette Winzenburg, Berlin */ public class ListViewETContextMenu extends Application { private Parent getContent() { ObservableList data = FXCollections.observableArrayList("one", "two", "three"); // ListView listView = new ListView(); ListViewC listView = new ListViewC(); listView.setItems(data); listView.setCellFactory(p -> new ListCellC(new ContextMenu(new MenuItem("item")))); return listView; } /** * ListViewSkin that implements EventTarget and * hooks the focused cell into the event dispatch chain */ private static class ListViewCSkin extends ListViewSkin implements EventTarget { private EventHandlerManager eventHandlerManager = new EventHandlerManager(this); @Override public EventDispatchChain buildEventDispatchChain( EventDispatchChain tail) { int focused = getSkinnable().getFocusModel().getFocusedIndex(); if (focused > - 1) { Cell cell = flow.getCell(focused); tail = cell.buildEventDispatchChain(tail); } // returning the chain as is or prepend our // eventhandlermanager doesn't make a difference // return tail; return tail.prepend(eventHandlerManager); } // boiler-plate constructor public ListViewCSkin(ListView listView) { super(listView); } } /** * ListView that hooks its skin into the event dispatch chain. */ private static class ListViewC extends ListView { @Override public EventDispatchChain buildEventDispatchChain( EventDispatchChain tail) { if (getSkin() instanceof EventTarget) { tail = ((EventTarget) getSkin()).buildEventDispatchChain(tail); } return super.buildEventDispatchChain(tail); } @Override protected Skin createDefaultSkin() { return new ListViewCSkin(this); } } private static class ListCellC extends ListCell { public ListCellC(ContextMenu menu) { setContextMenu(menu); } // boiler-plate: copy of default implementation @Override public void updateItem(T item, boolean empty) { super.updateItem(item, empty); if (empty) { setText(null); setGraphic(null); } else if (item instanceof Node) { setText(null); Node currentNode = getGraphic(); Node newNode = (Node) item; if (currentNode == null || ! currentNode.equals(newNode)) { setGraphic(newNode); } } else { /** * This label is used if the item associated with this cell is to be * represented as a String. While we will lazily instantiate it * we never clear it, being more afraid of object churn than a minor * "leak" (which will not become a "major" leak). */ setText(item == null ? "null" : item.toString()); setGraphic(null); } } } @Override public void start(Stage primaryStage) throws Exception { Scene scene = new Scene(getContent()); primaryStage.setScene(scene); primaryStage.show(); } public static void main(String[] args) { launch(args); } } 

挖掘一些事实:

  • scene.processMenuEvent(...)创建并触发contextMenuEvent
  • 对于键盘触发事件,该方法计算相对于目标节点中间某处的场景/屏幕坐标(这是当前焦点所有者)
  • 这些(场景/屏幕)绝对坐标无法更改: event.copyFor(...)仅将它们映射到新的目标本地坐标

所以任何一个自动化的希望都没有成功,我们必须重新计算位置。 一个(暂定的)地方是一个自定义的EventDispatcher。 原始(读取:缺少所有健全性检查,未经过正式测试,可能会产生不必要的副作用!)下面的示例只是在委托给注入的EventDispatcher之前,用新的键盘触发了contextMenuEvent。 客户端代码(如ListViewSkin)必须在预先添加到EventDispatchChain之前传入targetCell。

 /** * EventDispatcher that replaces a keyboard-triggered ContextMenuEvent by a * newly created event that has screen coordinates relativ to the target cell. * */ private static class ContextMenuEventDispatcher implements EventDispatcher { private EventDispatcher delegate; private Cell targetCell; public ContextMenuEventDispatcher(EventDispatcher delegate) { this.delegate = delegate; } /** * Sets the target cell for the context menu. * @param cell */ public void setTargetCell(Cell cell) { this.targetCell = cell; } /** * Implemented to replace a keyboard-triggered contextMenuEvent before * letting the delegate dispatch it. * */ @Override public Event dispatchEvent(Event event, EventDispatchChain tail) { event = handleContextMenuEvent(event); return delegate.dispatchEvent(event, tail); } private Event handleContextMenuEvent(Event event) { if (!(event instanceof ContextMenuEvent) || targetCell == null) return event; ContextMenuEvent cme = (ContextMenuEvent) event; if (!cme.isKeyboardTrigger()) return event; final Bounds bounds = targetCell.localToScreen( targetCell.getBoundsInLocal()); // calculate screen coordinates of contextMenu double x2 = bounds.getMinX() + bounds.getWidth() / 4; double y2 = bounds.getMinY() + bounds.getHeight() / 2; // instantiate a contextMenuEvent with the cell-related coordinates ContextMenuEvent toCell = new ContextMenuEvent(ContextMenuEvent.CONTEXT_MENU_REQUESTED, 0, 0, x2, y2, true, null); return toCell; } } // usage (fi in ListViewSkin) /** * ListViewSkin that implements EventTarget and hooks the focused cell into * the event dispatch chain */ private static class ListViewCSkin extends ListViewSkin implements EventTarget { private ContextMenuEventDispatcher contextHandler = new ContextMenuEventDispatcher(new EventHandlerManager(this)); @Override public EventDispatchChain buildEventDispatchChain( EventDispatchChain tail) { int focused = getSkinnable().getFocusModel().getFocusedIndex(); Cell cell = null; if (focused > -1) { cell = flow.getCell(focused); tail = cell.buildEventDispatchChain(tail); } contextHandler.setTargetCell(cell); // the handlerManager doesn't make a difference return tail.prepend(contextHandler); } // boiler-plate constructor public ListViewCSkin(ListView listView) { super(listView); } } 

编辑

刚注意到一个轻微的(?)故障,如果单元格本身没有contextMenu,则listView上的键盘激活的contextMenu会显示在单元格位置。 如果单元格未使用,则无法找到替换事件的方法,可能仍然在事件调度中遗漏了明显的事情(?)。