问题
A cell contextMenu can't be activated by keyboard: it's underlying reason being that the contextMenuEvent is dispatched to the focused node - which is the containing table, not the cell. The bug evaluation by Jonathan has an outline of how solve it:
The 'proper' way to do this is to probably override the buildEventDispatchChain in TableView and include the TableViewSkin (if it implements EventDispatcher), and to keep forwarding this down to the cells in the table row.
Tried to follow that path (below is an example for ListView, simply because there's only one level of skins to implement vs. two for a TableView). It's working, kind of: the cell contextMenu is activated by the keyboard popup trigger, but positioned relative to the table vs. relative to the cell.
Question: how to hook into the dispatch chain such that it's located relative to the cell?
Runnable code example:
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);
}
}
回答1:
Digging up some facts:
- the contextMenuEvent is created and fired off in
scene.processMenuEvent(...)
- for keyboard triggered events, the method calculates scene/screen coordinates relative to somewhere in the middle of the target node (which is the current focus owner)
- these (scene/screen) absolute coordinates can't be changed:
event.copyFor(...)
only maps them to the new target local coordinates
So any hope for some automagic didn't work out, we have to re-calculate the location. A (tentative) place to do this is a custom EventDispatcher. The raw (read: missing all sanity checks, not formally tested, might have unwanted side-effects!) example below simply replaces a keyboard-triggered contextMenuEvent by a new one before delegating to an injected EventDispatcher. Client code (like f.i. the ListViewSkin) must pass-in the targetCell before prepending to the 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);
}
}
Edit
Just noticed a slight (?) glitch in that a keyboard-activated contextMenu on the listView is shown at the cell location if the cell doesn't have a contextMenu on its own. Couldn't find a way to not replace the event if unused by the cell, probably still missing something obvious (?) in the event dispatch.
来源:https://stackoverflow.com/questions/28673753/cell-how-to-activate-a-contextmenu-by-keyboard