JavaFX CustomMenuItem与TextField的奇怪行为

时间:2018-07-12 13:58:05

标签: javafx javafx-8

也许有人可以解释以下行为-希望有可能的解决方法...谢谢。

使用包含TextField的CustomMenuItem时,通过在文本字段内按Enter触发MenuItem 上方的操作...除非文本字段设置了动作侦听器(未添加)...需要使用addEventHandler而不是setOnAction ...:-/

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.CustomMenuItem;
import javafx.scene.control.MenuButton;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TextField;
import javafx.stage.Stage;

public class CustomMenuTest extends Application {
    @Override
    public void start(Stage primaryStage) throws Exception {

        MenuButton menu = new MenuButton("Fancy Menu...");

        MenuItem hello = new MenuItem("Hello");
        hello.setOnAction(event -> System.out.println("Hello | " + event.getSource()));

        MenuItem world = new MenuItem("World!");
        world.setOnAction(event -> System.out.println("World! | " + event.getSource()));

        /*
        Set the cursor into the TextField, maybe type something, and hit enter.
        --> Expected: "ADD: <Text you typed> ..."
        --> Actual: "ADD: <Text you typed> ..." AND "World! ..." - so the button above gets triggered as well.
        If I activate listener (II) or (III), everything works fine - even the empty action in (III) does is job, but this is ugly...
        (And I can't use (II), because I need (I).
         */
        TextField textField = new TextField();
        /*   I */ textField.addEventHandler(ActionEvent.ACTION,
                                  event -> System.out.println("ADD: " + textField.getText() + " | " + event.getSource()));
        /*  II */ // textField.setOnAction(event -> System.out.println("SET: " + textField.getText() + " | " + event.getSource()));
        /* III */ // textField.setOnAction(__ -> {/* do nothing */});

        CustomMenuItem custom = new CustomMenuItem(textField, false);

        menu.getItems().addAll(hello, world, custom);

        primaryStage.setScene(new Scene(menu));
        primaryStage.show();
    }
}

我正在使用Java 8。

有什么想法吗?

1 个答案:

答案 0 :(得分:3)

(至少是部分原因)是ContextMenuContent的内部结构(充当menuItems列表的外观)被弄糊涂了:

  • 它在ENTER上注册一个keyHandler,最后触发内部跟踪的当前关注项
  • 点击进入自定义内容后,内部跟踪无法正常工作

一种破解方法是强制更新内部状态(请注意:需要访问隐藏的api!),f.i。在textField的mouseEntered处理程序中。

某些代码:

    TextField textField = new TextField();
    CustomMenuItem custom = new CustomMenuItem(textField, false);
    // menuItemContainer registers a mouseClicked handler that fires
    // need to consume before it reaches the container
    textField.addEventFilter(MouseEvent.MOUSE_CLICKED, e -> e.consume());
    // hack to update internal state of ContextMenuItem
    textField.addEventHandler(MouseEvent.MOUSE_ENTERED, e -> {
        ContextMenuContent cmContent = null;
        Node node = textField;
        while (node != null) {
            node = node.getParent();
            if (node instanceof ContextMenuContent) {
                cmContent = (ContextMenuContent) node;
                break;
            }
        }
        if (cmContent != null) {
            Parent menuItemContainer = textField.getParent();
            Parent menuBox = menuItemContainer.getParent();
            int index = menuBox.getChildrenUnmodifiable().indexOf(menuItemContainer);
            // hack internal state
            cmContent.requestFocusOnIndex(index);
        }
    });
    /* I */
    textField.addEventHandler(ActionEvent.ACTION, event -> {
        System.out.println("ADD: " + textField.getText() + " | "
                + event.getSource()
        );
        // consume to prevent item to fire twice
        event.consume();

    });

    custom.setOnAction(e -> {
        // someone needs to hide the popup
        // could be done in textField actionHandler as well
        if (custom.getParentPopup() != null) {
            custom.getParentPopup().hide();
        }
        System.out.println("action custom menu " + e.getSource());
    });

报告了bug

在进一步挖掘中:真正的罪魁祸首似乎是MenuItemContainer(即单个项目的容器)

  • 对于customMenuItems,它注册一个mouseEntered处理程序,该处理程序要求关注自身
  • 对于其他类型的menuItem,它会在其focusProperty上注册一个侦听器,该侦听器会更新ContextMenuContent的currentFocusedIndex

一个干净的解决方法可能是为所有项目(分隔符除外)注册焦点侦听器


进一步挖掘,打开另一个选项/错误;)setOnAction与addHandler(ActionEvent ...)行为不同的原因是TextFieldBehaviour中的一些可疑代码:

@Override protected void fire(KeyEvent event) {
    TextField textField = getNode();
    EventHandler<ActionEvent> onAction = textField.getOnAction();
    ActionEvent actionEvent = new ActionEvent(textField, null);

    textField.commitValue();
    textField.fireEvent(actionEvent);
    // ---> this condition smells 
    if (onAction == null && !actionEvent.isConsumed()) {
        forwardToParent(event);
    }
}

我认为实现的意图是仅在有特殊的onAction处理程序或没有普通处理程序使用它的情况下转发。即使事件处理程序消耗了事件,检查事件是否已消耗也总是返回false-因为该事件是在分派过程中复制的……也就是说,事件处理程序会更改事件的消耗,而不是原始事件。

转发给父母会触发不正确的menuItem(由于内部记账方面的错误,请参见上文),因此正在寻找另一种方法来防止该问题:

protected void forwardToParent(KeyEvent event) {
    // fix for JDK-8145515
    if (getNode().getProperties().containsKey(DISABLE_FORWARD_TO_PARENT)) {
        return;
    }

    if (getNode().getParent() != null) {
        getNode().getParent().fireEvent(event);
    }
}

实际上,在属性中添加该标记可防止触发错误的项目-无需访问内部类(尽管仍然高度依赖于实现..):

textField.getProperties().put(
   "TextInputControlBehavior.disableForwardToParent", true);
textField.addEventHandler(ActionEvent.ACTION, event -> {