如何让JavaFX菜单项响应TAB键按下?

3
一个JavaFX MenuItem可以通过设置ActionEvent EventHandler响应大多数KeyPress事件。然而,虽然事件处理程序确实捕获了KeyCode.ENTER的KeyPress事件,但它没有捕获KeyCode.TAB的KeyPress事件。显然,一些按键事件如TAB在更深层次上被处理。例如,箭头键允许遍历菜单。
我的ContextMenu是一个列表,列出了用户在TextField中开始输入的电子邮件地址字符串的完成项。用户想要按箭头键选择所需的项目,并使用TAB键执行完成操作。
我可以将事件处理程序附加到ContextMenu本身并捕获TAB keypress。但是,事件的源是ContextMenu,我找不到ContextMenu中任何变量指示在按下TAB键时突出显示哪个MenuItem。MenuItem允许css样式控制菜单项焦点的外观,但它没有任何属性告诉它是否处于焦点状态。
我尝试通过MenuItem buildEventDispatchChain() futzing with the EventDispatchChain,但无济于事。似乎没有办法拦截TAB KeyPress或以其他方式确定在按下TAB键时哪个菜单项处于焦点状态。
有什么建议吗?

如果你已经成功拦截了菜单级别的TAB键按下事件,那么谁会阻止你存储最新聚焦的MenuItem并为其触发操作(如果不为空)呢?例如,为每个MenuItem附加一个焦点监听器,如果该项被聚焦,那么您就知道以后要触发什么操作了。 - eckig
我无法在菜单级别上拦截TAB键按下事件。我看不到任何方法可以将焦点侦听器附加到菜单项。菜单项不是节点。 - Eric Saund
2个回答

1
如果我没理解错的话,您想要覆盖默认的按键监听器以添加自己的响应,因此我们必须找到它被应用的地方。
为了使其工作,我们必须使用私有API... ContextMenu 皮肤(ContextMenuSkin) 使用一个 ContextMenuContent 对象,作为包含所有项目的容器。这些项目中的每个项目也在一个 ContextMenuContent.MenuItemContainer 容器中。
我们可以在父容器上覆盖 keypressed 监听器,同时可以在项目容器上添加 focusedProperty 监听器。
使用这个私有API。
import com.sun.javafx.scene.control.skin.ContextMenuContent;

这对我很有效:

这在编程方面起作用:

private ContextMenuContent.MenuItemContainer itemSelected=null;

@Override
public void start(Stage primaryStage) {

    MenuItem cmItem1 = new MenuItem("Item 1");
    cmItem1.setOnAction(e->System.out.println("Item 1"));
    MenuItem cmItem2 = new MenuItem("Item 2");
    cmItem2.setOnAction(e->System.out.println("Item 2"));

    final ContextMenu cm = new ContextMenu(cmItem1,cmItem2);

    Scene scene = new Scene(new StackPane(), 300, 250);
    scene.setOnMouseClicked(t -> {
        if(t.getButton()==MouseButton.SECONDARY || t.isControlDown()){
            cm.show(scene.getWindow(),t.getScreenX(),t.getScreenY());

            ContextMenuContent cmc= (ContextMenuContent)cm.getSkin().getNode();

            cmc.setOnKeyPressed(ke->{
                switch (ke.getCode()) {
                    case UP:    break;
                    case DOWN:  break;
                    case TAB:   ke.consume();
                                if(itemSelected!=null){
                                    itemSelected.getItem().fire();
                                }
                                cm.hide();
                                break;
                    default: break;
                }
            });
            VBox itemsContainer = cmc.getItemsContainer();
            itemsContainer.getChildren().forEach(n->{
                ContextMenuContent.MenuItemContainer item=(ContextMenuContent.MenuItemContainer)n;
                item.focusedProperty().addListener((obs,b,b1)->{
                    if(b1){
                        itemSelected=item;
                    }
                });
            });
        }
    });

    primaryStage.setScene(scene);
    primaryStage.show();
}

这里重要的一点是,ContextMenu 必须曾经可见过,才不会为 null,或者手动分配一个 skin。这让我有点困惑。 - Marv

1
很好!谢谢@jose!我最终编写了略有不同的代码,但关键是使用com.sun.javafx.scene.control.skin.ContextMenuContent,它提供访问包含菜单项的ContextMenuContent.MenuItemContainer对象的方法。
为了不破坏现有的上/下箭头键行为,我向ContextMenuContent对象添加了一个新的处理程序;这个处理程序只消耗TAB键按下事件,其他所有事件都通过到其正常处理程序。
查看ContextMenuContent类时,我借用了它们用于查找焦点项目的现有方法,因此不必添加focusedProperty侦听器。
此外,我使用Java 1.7,并且没有lambda表达式,我使用非常基本的编程风格。
public class MenuItemHandler_CMC <T extends Event> implements EventHandler {
    public ContextMenuContent m_cmc;

    public AddressCompletionMenuItemHandler_CMC(ContextMenuContent cmc){
        m_cmc = cmc;
    }

    @Override
    public void handle(Event event){
        KeyEvent ke = (KeyEvent)event;
        switch(ke.getCode()){
        case TAB: 
            ke.consume();
            MenuItem focused_menu_item = findFocusedMenuItem();
            if(focused_menu_item != null){
                focused_menu_item.fire();
            }
            break;
        default: break;
        }
    }

    public MenuItem findFocusedMenuItem() {
        VBox items_container = m_cmc.getItemsContainer();
        for (int i = 0; i < items_container.getChildren().size(); i++) {
            Node n = items_container.getChildren().get(i);
            if (n.isFocused()) {
                ContextMenuContent.MenuItemContainer menu_item_container = (ContextMenuContent.MenuItemContainer)n;
                MenuItem menu_item = menu_item_container.getItem();
                return menu_item;
            }
        }
        return null;
    }
}

附加另一个处理程序。
    if(m_context_menu.getSkin() != null){
        ContextMenuContent cmc = (ContextMenuContent)m_context_menu.getSkin().getNode();
        MenuItemHandler_CMC menu_item_handler_cmc = new MenuItemHandler_CMC(cmc);
        cmc.addEventHandler(KeyEvent.KEY_PRESSED, menu_item_handler_cmc);
    }

谢谢 @EricSaund 提供解决方案。我没有使用上/下箭头按键来消耗事件,所以它没有被覆盖。我在 switch 语句中加入了它,以防你想要实现一些自定义行为。 - José Pereda

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接