如何在DefaultButton操作之前使用KeyPressed事件?

时间:2018-07-17 18:50:09

标签: java javafx

我很难参加onKeyPressed事件。我的应用程序中有一个TextField,它允许用户按[ENTER]键来执行某些功能;但是,我也为场景指定了默认按钮。

虽然我可以在TextField中成功触发按下的键所需的操作,但始终会首先执行默认按钮的操作。当用户位于TextField中时,我需要为按键完全消耗事件。

请参阅以下MCVE:

import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonBar;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class Main extends Application {

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) {

        // Simple UI
        VBox root = new VBox(10);
        root.setPadding(new Insets(10));
        root.setAlignment(Pos.CENTER);

        // TextField
        TextField textField = new TextField();

        // Capture the [ENTER] key
        textField.setOnKeyPressed(event -> {
            if (event.getCode() == KeyCode.ENTER) {
                System.out.println("-> Enter");
                event.consume();
            }
        });

        // Buttons
        Button btnCancel = new Button("Cancel");
        btnCancel.setCancelButton(true);
        btnCancel.setOnAction(e -> {
            System.out.println("-> Cancel");
            primaryStage.close();
        });

        Button btnSave = new Button("Save");
        btnSave.setDefaultButton(true);
        btnSave.setOnAction(e -> {
            System.out.println("-> Save");
            primaryStage.close();
        });

        ButtonBar buttonBar = new ButtonBar();
        buttonBar.getButtons().addAll(btnCancel, btnSave);

        root.getChildren().addAll(textField, buttonBar);

        primaryStage.setScene(new Scene(root));
        primaryStage.setTitle("Consume Event");
        primaryStage.show();
    }
}

所需的行为是能够键入textField并按Enter。输出应仅显示-> Enter,并且应保留该阶段。

但是,当前正在发生的事情是,该阶段以以下输出关闭:

-> Save
-> Enter

我在错误的地方打了event.consume()电话吗?我想保留默认按钮。

编辑:

这似乎仅是JDK 10中的问题。我再次使用JDK 1.8.161尝试过,它的行为符合预期。 Java 10中可能存在错误?

已提交错误报告:View Bug Report

2 个答案:

答案 0 :(得分:5)

文档说明:

Windows / Linux:默认按钮在获得焦点时会按ENTER键。当默认按钮没有焦点,并且焦点在另一个Button控件上时,另一个非默认Button将收到ENTER键。当焦点位于用户界面的其他位置而不是任何按钮上时,默认按钮将接收ENTER键,如果已指定,并且场景中没有其他节点首先使用它,则该按钮将被消耗。

所以我认为这是一个错误。正如我在评论中说的,一种解决方法是检查TextField是否在默认按钮的setOnAction内具有焦点,并在那里消费事件,直到他们修复它。

答案 1 :(得分:3)

问题得到了回答(这是OP is reported并接受的错误):

  • 在“特殊”事件中使用事件(因为保证是最后一个在相同类型/阶段/事件中注册的事件处理程序)中,事件处理程序必须能够工作,即停止将事件分发给其他相关方
  • 在那个时间点,我们处于事件分发冒泡阶段的开始
  • 加速器由场景/阶段处理,即在冒泡阶段的结束中:如果所有加速器都正确运行,则在开始使用它们时不应使用它们。 (注意:找不到有关加速器何时使用的正式规范,仅是场景内部的KeyboardShortCutsHandler类型的EventDispatcher中的代码注释,因此请耐心等待。)

但是为什么会这样呢?

下面是一个可以使用的示例:对于F5之类的键来说一切都很好,调度完全按照指定的顺序进行:从场景图向下直到textField,然后向上直到加速器。输出为:

-> filter on parent:  source: VBox target: TextField
-> filter on field  source: TextField target: TextField
-> handler on field  source: TextField target: TextField
-> onKeyPressed on field  source: TextField target: TextField
-> handler on parent:  source: VBox target: TextField
-> onKeyPressed on parent  source: VBox target: TextField
in accelerator

此外,链中的任何处理程序都可以使用并停止进一步的调度。

现在切换到ENTER,查看调度链如何严重混乱,以使特殊按下的处理程序在加速器之后的最后一刻转向。输出:

-> filter on parent:  source: VBox target: TextField
-> filter on field  source: TextField target: TextField
-> handler on field  source: TextField target: TextField
action added: javafx.event.ActionEvent[source=TextField@53c9244[styleClass=text-input text-field]]
-> filter on parent:  source: VBox target: VBox
-> handler on parent:  source: VBox target: VBox
-> onKeyPressed on parent  source: VBox target: VBox
in accelerator
-> onKeyPressed on field  source: TextField target: TextField

可以在所有处理程序中完成(和工作)消费,除了现场的特殊处理程序之外。

问题的根源似乎是如果没有actionHandler消耗keyEvent,则手动转发keyEvent(我怀疑转发代码是在引入InputMap之前的,但是...并未深入探讨该方向)< / p>

该示例有点脏(*咳嗽-内部api,私有字段..),并修补了textField的inputMap。这个想法是摆脱手动转发,让正常的事件分发工作。控制正常调度的钩子是事件的消费状态。补丁代码

  • 用自定义实现替换ENTER键映射
  • 禁用映射的autoConsume标志,这会将控件完全移到自定义处理程序中
  • 通过该字段创建并触发一个ActionEvent(将源和目标均设置为该字段,这将修复JDK-8207774
  • 如果操作已被执行,则设置ENTER事件的消耗状态,否则将其冒泡

似乎可以正常工作,如调度日志输出中所示,现在与F5之类的普通键相同-但要注意:未进行正式测试!

最后是示例代码:

public class TextFieldActionHandler extends Application {

    private TextField textField;

    private KeyCode actor = KeyCode.ENTER;
//    private KeyCode actor = KeyCode.F5;
    private Parent createContent() {
        textField = new TextField("just some text");
        textField.skinProperty().addListener((src, ov, nv) -> {
            replaceEnter(textField);

        });
        // only this here is in the bug report, with consume
        // https://bugs.openjdk.java.net/browse/JDK-8207774
        textField.addEventHandler(ActionEvent.ACTION, e -> {
            System.out.println("action added: " + e);
//            e.consume();
        });

        //everything else is digging around
        textField.setOnKeyPressed(event -> {
            logEvent("-> onKeyPressed on field ",  event);
        });

        textField.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
            logEvent("-> filter on field ", event);
        });

        textField.addEventHandler(KeyEvent.KEY_PRESSED, event -> {
            logEvent("-> handler on field ", event);
        });

        VBox pane = new VBox(10, textField);

        pane.addEventHandler(KeyEvent.KEY_PRESSED, e -> {
            logEvent("-> handler on parent: ", e);
        });

        pane.addEventFilter(KeyEvent.KEY_PRESSED, e -> {
            logEvent("-> filter on parent: ", e);
        });

        //everything else is digging around
        pane.setOnKeyPressed(event -> {
            logEvent("-> onKeyPressed on parent ",  event);
        });

        return pane;
    }

    private void logEvent(String message, KeyEvent event) {
        logEvent(message, event, false);
    }

    private void logEvent(String message, KeyEvent event, boolean consume) {
        if (event.getCode() == actor) {
            System.out.println(message + " source: " + event.getSource().getClass().getSimpleName() 
                    + " target: " + event.getTarget().getClass().getSimpleName());
            if (consume)
                event.consume();    
        }

    }
    @Override
    public void start(Stage stage) throws Exception {
        Scene scene = new Scene(createContent());
        scene.getAccelerators().put(KeyCombination.keyCombination(actor.getName()),
                () -> System.out.println("in accelerator"));
        stage.setScene(scene);
        stage.setTitle(FXUtils.version());
        stage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }

    /** 
     * fishy code snippet from TextFieldBehaviour: 
     * 
     * https://bugs.openjdk.java.net/browse/JDK-8207774
     * during fire, the actionEvent without target is copied - such that
     * the check for being consumed of the original has no effect
     */
//    @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);
//
//        if (onAction == null && !actionEvent.isConsumed()) {
//            forwardToParent(event);
//        }
//    }


    // dirty patching
    protected void replaceEnter(TextField field) {
        TextFieldBehavior behavior = (TextFieldBehavior) FXUtils.invokeGetFieldValue(
                TextFieldSkin.class, field.getSkin(), "behavior");
        InputMap<TextField> inputMap = behavior.getInputMap();
        KeyBinding binding = new KeyBinding(KeyCode.ENTER);

        KeyMapping keyMapping = new KeyMapping(binding, this::fire);
        keyMapping.setAutoConsume(false);
        // note: this fails prior to 9-ea-108
        // due to https://bugs.openjdk.java.net/browse/JDK-8150636
        inputMap.getMappings().remove(keyMapping); 
        inputMap.getMappings().add(keyMapping);
    }

    /**
     * Copy from TextFieldBehaviour, changed to set the field as
     * both source and target of the created ActionEvent.
     * 
     * @param event
     */
    protected void fire(KeyEvent event) {
        EventHandler<ActionEvent> onAction = textField.getOnAction();
        ActionEvent actionEvent = new ActionEvent(textField, textField);

        textField.commitValue();
        textField.fireEvent(actionEvent);
        // remove the manual forwarding, instead consume the keyEvent if
        // the action handler has consumed the actionEvent
        // this way, the normal event dispatch can jump in with the normal
        // sequence
        if (onAction != null || actionEvent.isConsumed()) {
            event.consume();
        }
        // original code
//        if (onAction == null && !actionEvent.isConsumed()) {
////            forwardToParent(event);
//        }
        logEvent("in fire: " + event.isConsumed(), event);
    }

    protected void forwardToParent(KeyEvent event) {
        if (textField.getParent() !=  null) {
            textField.getParent().fireEvent(event);
        }
    }

    @SuppressWarnings("unused")
    private static final Logger LOG = Logger
            .getLogger(TextFieldActionHandler.class.getName());

}