How to consume KeyPressed event before DefaultButton action?
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):
- consuming the event in the "special" (in that it is guaranteed to be the last in handlers registered for the same type/phase/event) event handler must work, that is stop dispatching the events to other interested parties
- at that point in time we are at the start of the bubbling phase of event dispatch
- accelerators are processed by the scene/stage, that is at the end of the bubbling phase: if all went correctly, they shouldn't be reached when consumed at its start. (Note: could not find a formal specification of when accelerators are handled, just a code comment in the scene's internal EventDispatcher of type KeyboardShortCutsHandler, so take it with a grain of salt).
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
- replaces the ENTER keyMapping with a custom implementation
- disables the autoConsume flag of the mapping, this moves the control entirely into the custom handler
- creates and fires a ActionEvent (with both source and target set to the field, this is fixing JDK-8207774) via the field
- sets the consumed state of the ENTER event if the action was handled, let it bubble up otherwise
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());
}