Issue
I've set a callback to the Scene onKeyPressed, but it gets called 2 times. In particular:
- if the focus is on a TextField, the method set onKeyPressed is called 2 times;
- if the focus is on a non-text-editable component (e.g. a Button), it gets called just once.
Debugging info
Breakpoint placed at line 181, the one that calls the selectBack()
method.
From the debugger it appears that it's getting called always by that same event from Scene:
Minimal Reproducible Example
I'm using Java11 (jdk-11.0.11) + JavaFX11 (javafx-sdk-11.0.2)
Project structure:
Test
|
+-src
|
+-application
| |
| +--Controller.java
| |
| +--Main.java
| |
| +--Test.fxml
|
module-info.java
Main.java class:
package application;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.layout.AnchorPane;
public class Main extends Application {
@Override
public void start(Stage stage){
try {
FXMLLoader loader = new FXMLLoader(Main.class.getResource("Test.fxml"));
AnchorPane root = (AnchorPane) loader.load();
Scene scene = new Scene(root);
stage.setTitle("Test");
stage.setScene(scene);
stage.show();
}
catch(Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
launch(args);
}
}
Controller.java class:
package application;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.AnchorPane;
public class Controller {
@FXML
private AnchorPane base;
private int counter = 0;
public void initialize()
{
Platform.runLater(() -> {
this.base.getScene().setOnKeyPressed(e -> {
if(e.getCode() == KeyCode.ESCAPE)
test(new ActionEvent());
});
});
}
@FXML private void move(ActionEvent event)
{
}
@FXML private void test(ActionEvent event)
{
System.out.println("Counter: " + counter + " (" + System.currentTimeMillis() + ")");
Alert alert = new Alert(AlertType.INFORMATION, "Test");
alert.setContentText("Counter: " + counter);
alert.showAndWait();
counter++;
}
}
Test.fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.AnchorPane?>
<AnchorPane id="base" fx:id="base" prefHeight="400.0" prefWidth="400.0" xmlns="http://javafx.com/javafx/18" xmlns:fx="http://javafx.com/fxml/1" fx:controller="application.Controller">
<children>
<TextField layoutX="125.0" layoutY="187.0" />
<Button layoutX="174.0" layoutY="273.0" mnemonicParsing="false" text="Button" />
</children>
</AnchorPane>
module-info.java:
module test {
requires javafx.controls;
requires javafx.fxml;
requires transitive javafx.base;
requires transitive javafx.graphics;
opens application;
}
Run configuration with VM arguments: --module-path "bin;C:\Program Files\Java\javafx-sdk-11.0.2\lib" -m test/application.Main
To reproduce the issue: run the application and press 'ESC':
- if the focus is on the TextField, it will show the Alert 2 times;
- if the focus is on the Button, the Alert will show just once.
Solution
It seems like this is a misbehavior that has been fixed in OpenJFX 14.
I can reproduce the behavior with OpenJFX 11.0.1 ... 13.0.2, but not with OpenJFX 14 and later!
The relevant issue in the OpenJDK bug tracker seems to be https://bugs.openjdk.org/browse/JDK-8092352 (RT-23952) , which caused the implementation of e.g. com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent
to be changed.
If you can, switching to OpenJFX 14+ is probably the best option.
If you absolutely cannot update the JFX version:
- Consuming the event(s) does not help, as the two calls receive two different event instances.
- The only way I can think of to avoid triggering the dialog twice is to discard the second event if it comes too soon after the first one, which is a bit tricky because the second event is dispatched after the Alert is closed (because of the blocking
showAndWait
call), so we have to take that into account. There is the risk that we might discard "real" events, but because of the blocking Alert, it probably isn't much of a problem here.
Something like this:
public class Controller {
@FXML
private AnchorPane base;
private int counter = 0;
public void initialize() {
Platform.runLater(() -> {
this.base.getScene().setOnKeyPressed(new EventHandler<KeyEvent>() {
// might want to tweak this
private final int DISCARD_EVENT_DISTANCE_MILLIS = 10;
private Instant lastInteractionTimestamp = null;
@Override
public void handle(KeyEvent e) {
if (e.getCode() == KeyCode.ESCAPE) {
final Instant now = Instant.now();
System.out.println(now);
if (shouldDiscardEvent(now)) {
System.out.println("Discard event that comes too soon after previous interaction: " + now);
lastInteractionTimestamp = now;
} else {
lastInteractionTimestamp = test(new ActionEvent());
}
}
}
private boolean shouldDiscardEvent(final Instant now) {
return lastInteractionTimestamp != null
&& lastInteractionTimestamp.plusMillis(DISCARD_EVENT_DISTANCE_MILLIS).isAfter(now);
}
});
});
}
@FXML
private void move(ActionEvent event) {
}
@FXML
private Instant test(ActionEvent event) {
System.out.println("Counter: " + counter + " (" + Instant.now() + ")");
Alert alert = new Alert(AlertType.INFORMATION, "Test");
alert.setContentText("Counter: " + counter);
alert.showAndWait();
counter++;
return Instant.now();
}
}
Answered By - Sonnenkind
Answer Checked By - Gilberto Lyons (JavaFixing Admin)