I am having a hard time consuming an onKeyPressed
event. I have a TextField
in my app that allows the user to press the [ENTER] key for a certain function; however, I also have a default button specified for the scene.
While I can successfully trigger the needed actions for the key pressed while in the TextField
, the default button's action is always executed first. I need to consume the event entirely for the keypress when the user is in the TextField
.
See the following 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();
}
}
The desired behavior is to be able to type in the textField
and press enter. The output should show only -> Enter
and the stage should remain.
However, what is currently happening is the stage closes with the following output:
-> Save
-> Enter
Do I have the event.consume()
call in the wrong place? I would like to keep the default button as is.
EDIT:
This only appears to be an issue in JDK 10. I tried again using JDK 1.8.161 and it behaves as desired. Possible bug in Java 10?
Bug report submitted: View Bug Report
As the documentation states :
Windows / Linux: A default Button receives ENTER key presses when it has focus. When the default button does not have focus, and focus is on another Button control, the ENTER key press will be received by the other, non-default Button. When focus is elsewhere in the user interface, and not on any Button, the ENTER key press will be received by the default button, if one is specified, and if no other node in the scene consumes it first.
So I believe it is a bug. As I say in the comments a workaround would be to check if the TextField has the focus inside the setOnAction of your befault button and consume the event there, until they fix it.
The question is answered (it's a bug which is reported by the OP, the fix is approved and will make it into openjfx14):
But why does that happen, exactly?
Below is an example to play with: for keys like F5 all is well, the dispatch happens exactly as specified: down the scenegraph until the textField, then back up until the accelerator. The output is:
-> 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
Plus any of the handlers in the chain can consume and stop further dispatch.
Now switch to ENTER, and see how the dispatch chain gets severely confused, such that the special pressed handler gets its turn as the very last, after the accelerator. The output:
-> 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
Consuming can be done (and works) in all handlers, except the special one on the field.
The source of the problem seems to be the manual forwarding of the keyEvent if no actionHandler had consumed it (I suspect that the forwarding code is from before the InputMap was introduced but ... didn't dig into that direction)
The example goes a bit (*cough - internal api, private fields ..) dirty and patches the textField's inputMap. The idea is to get rid off the manual forwarding and let the normal event dispatch do its job. The hook to control the normal dispatch is the event's consumed state. The patch code
Seems to work, as seen on the output of dispatch logging which now is the same as for normal keys like F5 - but beware: no formal testing done!
At last the example code:
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());
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With