问题已得到解答(这是 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());
}