Cell: how to activate contextMenu using the keyboard?

A context of a Menu cell cannot be activated by the keyboard : the main reason is that contextMenuEvent is sent to the focused node - which is a table, not a cell. Jonathan's error assessment has a plan on how to solve this problem:

The “right” way to do this is probably to override buildEventDispatchChain in the TableView and enable the TableViewSkin (if it implements EventDispatcher) and continue to forward this down to the cells in the table row.

Tried to follow this path (below is an example for ListView, simply because there is only one level of skins to implement compared to two for TableView). This works, sort of: the contextMenu cell is activated using a pop-up keyboard trigger, but is positioned relative to the table compared to the cell.

Question: how to connect to the sending chain so that it is located relative to the cell?

Example startup code:

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);
    }
}
+4
source share
1 answer

Digging out some facts:

  • contextMenuEvent is created and run in scene.processMenuEvent(...)
  • for events triggered by the keyboard, the method calculates the coordinates of the scene / screen relative to somewhere in the middle of the node target (which is the current focus owner)
  • (/) : event.copyFor(...) .

, , . A () - EventDispatcher. (: , , !). contextMenuEvent , EventDispatcher. (, f.i. ListViewSkin) 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

(?) , contextMenu listView , Menu . , , , - (?) .

+1

All Articles