【问题标题】:Cell: how to activate a contextMenu by keyboard?单元格:如何通过键盘激活上下文菜单?
【发布时间】:2015-02-23 12:23:13
【问题描述】:

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

【问题讨论】:

  • @brian 是的,已经看到了 - 无论如何感谢您的参考 :-) 但是一个 hack(另一个是跟踪 contextMenu 的显示属性并手动定位它)不是我的之后 - 我想挂接到事件调度中,这样默认的显示/定位就可以正常工作,而无需对部分客户端代码进行任何进一步的努力

标签: java contextmenu javafx-8 event-dispatching


【解决方案1】:

挖掘一些事实:

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

所以任何自动魔法的希望都没有实现,我们必须重新计算位置。执行此操作的(暂定)位置是自定义 EventDispatcher。下面的原始示例(阅读:缺少所有健全性检查,未经正式测试,可能有不需要的副作用!)在委托给注入的 EventDispatcher 之前,只需将键盘触发的 contextMenuEvent 替换为新的。客户端代码(如 f.i. 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 (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 会显示在单元格位置。如果单元未使用该事件,则找不到不替换该事件的方法,可能仍然在事件调度中遗漏了一些明显的 (?) 内容。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-08-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-09-17
    • 2015-08-24
    相关资源
    最近更新 更多