【问题标题】:How to consume KeyPressed event before DefaultButton action?如何在 DefaultButton 操作之前使用 KeyPressed 事件?
【发布时间】:2018-07-17 18:50:08
【问题描述】:

我很难处理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,并且该阶段应保留。

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

-> Save
-> Enter

我的event.consume() 电话打错了吗?我想保持默认按钮不变。

编辑:

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

已提交错误报告:View Bug Report

【问题讨论】:

  • 有趣...绝对不适合我。我正在使用 JDK 10.0.1;也许是一个错误?
  • 我更新了我的问题。该行为在 JDK 8 中是正确的,但在 JDK 10.0.1 中失败
  • @JKostikiadis - 我可以这样做,但我更想知道为什么 event.consume() 实际上并没有消费该事件。除非默认按钮行为使用在事件到达TextField 之前捕获事件的EventFilter
  • 似乎特定于 TextField 并且可能与我上次在 stackoverflow.com/a/51379832/203657 中挖掘的原因相同 - 补救措施至少是相同的:设置一个虚拟 onAction 处理程序或添加 no-forward 标记到 textField 的属性。仍然是一个 hack,但比调整默认按钮的操作更“本地化”问题

标签: java javafx


【解决方案1】:

如文档所述:

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

所以我相信这是一个错误。正如我在 cmets 中所说,一种解决方法是检查 TextField 是否在您的故障按钮的 setOnAction 内具有焦点并在那里使用该事件,直到他们修复它为止。

【讨论】:

  • 我提交了一份错误报告,并且正在使用您提到的解决方法。谢谢。
  • @Zephyr 和另一个:bugs.openjdk.java.net/browse/JDK-8207774 - 尽管按照建议修复它在这里没有帮助。并且不完全确定如何解决此问题:默认行为会创建一个 ActionEvent 并将 ENTER 转发给父级,如果该操作没有被任何 actionHandler 使用。所以它绕过了调度链,使字段的处理程序成为“最后”看到事件,在加速器和父处理程序采取行动之后。
【解决方案2】:

问题已得到解答(这是 OP is reported 的错误,修复已获批准并将进入 openjfx14):

  • 在“特殊”中使用事件(因为它保证是为相同类型/阶段/事件注册的最后一个处理程序)事件处理程序必须工作,即停止将事件分派给其他相关方李>
  • 那时我们正处于事件调度冒泡阶段的开始
  • 加速器由场景/阶段处理,即在冒泡阶段的结束:如果一切正常,则在开始时消耗它们时不应到达。 (注意:找不到关于何时处理加速器的正式规范,只是在场景内部的 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

消费可以在所有处理程序中完成(并且有效),除了场上的特殊处理程序。

问题的根源似乎是keyEvent的手动转发,如果没有actionHandler消耗它(我怀疑转发代码是在引入InputMap之前但是......没有深入那个方向)

这个例子有点脏(*cough - 内部 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());

}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-04-11
    • 2013-05-19
    • 2020-03-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-09-25
    相关资源
    最近更新 更多