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);
}
}