A cell contextMenu can't be activated by keyboard:它的根本原因是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<String> data = FXCollections.observableArrayList("one", "two", "three");
// ListView<String> listView = new ListView<>();
ListViewC<String> 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<T> extends ListViewSkin<T> 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<T> listView) {
super(listView);
}
}
/**
* ListView that hooks its skin into the event dispatch chain.
*/
private static class ListViewC<T> extends ListView<T> {
@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<T> extends ListCell<T> {
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);
}
}
答案 0 :(得分:1)
挖掘一些事实:
scene.processMenuEvent(...)
event.copyFor(...)
仅将它们映射到新的目标本地坐标所以任何一个自动化的希望都没有解决,我们必须重新计算位置。 (暂定)这样做的地方是自定义EventDispatcher。原始(读取:缺少所有健全性检查,未经过正式测试,可能会产生不必要的副作用!)下面的示例只是在委托给注入的EventDispatcher之前用新的键替换键盘触发的contextMenuEvent。客户端代码(如f.i.ListViewSkin)必须先传入targetCell,然后再添加到EventDispatchChain。
/**
* 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 (f.i. in ListViewSkin)
/**
* ListViewSkin that implements EventTarget and hooks the focused cell into
* the event dispatch chain
*/
private static class ListViewCSkin<T> extends ListViewSkin<T> 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<T> listView) {
super(listView);
}
}
修改强>
注意到一个轻微的(?)故障,如果单元格本身没有contextMenu,listView上的键盘激活的contextMenu将显示在单元格位置。如果单元格未使用,可能无法找到替换事件的方法,可能仍然在事件发送中遗漏了明显的事情(?)。