我很难参加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
答案 0 :(得分:5)
文档说明:
Windows / Linux:默认按钮在获得焦点时会按ENTER键。当默认按钮没有焦点,并且焦点在另一个Button控件上时,另一个非默认Button将收到ENTER键。当焦点位于用户界面的其他位置而不是任何按钮上时,默认按钮将接收ENTER键,如果已指定,并且场景中没有其他节点首先使用它,则该按钮将被消耗。
所以我认为这是一个错误。正如我在评论中说的,一种解决方法是检查TextField是否在默认按钮的setOnAction内具有焦点,并在那里消费事件,直到他们修复它。
答案 1 :(得分:3)
问题得到了回答(这是OP is reported并接受的错误):
但是为什么会这样呢?
下面是一个可以使用的示例:对于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。这个想法是摆脱手动转发,让正常的事件分发工作。控制正常调度的钩子是事件的消费状态。补丁代码
似乎可以正常工作,如调度日志输出中所示,现在与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());
}