0

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:

  1. first call
  2. second call

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.
mikyll98
  • 1,195
  • 3
  • 8
  • 29
  • 3
    There is no need for `Platform.runLater()` here, but that is not the cause of the issue. Create and post a [mre] that reproduces the problem. – James_D Aug 12 '22 at 00:58
  • @James_D I've tried removing Platform.runLater() and it throws NullPointerException... however I cannot seem to reproduce that problem in a simplier project, I'll try to provide further details or to actually make that minimal reproducible example. – mikyll98 Aug 12 '22 at 01:32
  • You would appear to have at least two handlers that will call `selectBack`, the one added to the scene and the one added via FXML. Could that be the problem? But as noted, I think a [mre] is necessary here. – Slaw Aug 12 '22 at 06:56
  • What is null when you remove `Platform.runLater()`? Are you loading the FXML from a background thread? (Though that should actually be safe anyway.) – James_D Aug 12 '22 at 11:16
  • @James_D I'm loading it the same way I've found everywhere (from Main class, inside the start method got from Application class). However I had found [this answer](https://stackoverflow.com/a/26061123/19544859), which explains why you shouldn't get the Scene from initialize() method – mikyll98 Aug 12 '22 at 11:35
  • I think @Slaw is correct that you have multiple handlers. It looks like you have `selectBack` as callable from your FXML. Did you perhaps also set up a keybinding there somewhere? It would be useful if you show us the FXML (at least the parts that reference `selectBack`). – Ruckus T-Boom Aug 12 '22 at 14:56
  • Ah, yes, I didn't notice the `getScene()`. That would cause a NPE, and `Platform.runLater()` doesn't necessarily *guarantee* to avoid it. It's better to register that handler at the point in the code where you create the `Scene` (perhaps in your application subclass). You can still call a method in the controller. But none of that is the cause of the problem; as suggested, post a [mre]. – James_D Aug 12 '22 at 14:58
  • Do you have a button in the FXML with `cancelButton="true"` and `onAction="#selectBack"`? – James_D Aug 12 '22 at 15:01
  • @RuckusT-Boom I've updated the question with some links to the GitHub repository (with the interested classes). – mikyll98 Aug 12 '22 at 16:22
  • Anyways, responding to @James_D , I'm still not able to reproduce the issue on another project, it seems not to happen there so I guess I'm missing something. However, in case I won't get to a fix, I'll just remove that key binding feature, since it's not strictly necessary. Regarding the `cancelButton="true"` unfortunately I don't have anything like that – mikyll98 Aug 12 '22 at 16:22
  • 2
    Put a breakpoint in the handler, run in debug mode, and examine the stack trace when you hit the breakpoint to see if you can determine where it's being called from. – James_D Aug 12 '22 at 16:27
  • @James_D I've edited the question again, adding the debug info, but I discovered something very weird: the problem occurs just when a TextField has the focus. I'm going to write a small minimal reproducible example and link it. – mikyll98 Aug 12 '22 at 17:05
  • 1
    You have `onKeyTyped` handlers for your text fields. Does it still occur if you remove them? These are not a good idea anyway; to respond to changes in the text in a text field, you should register a listener with the text field's `textProperty`. (Because, for example, if the user pastes text in using the mouse, your key handler won't be invoked and your UI will potentially be in an inconsistent state.) – James_D Aug 12 '22 at 17:13
  • 1
    Edit the question to include the [mre], don't link it. – James_D Aug 12 '22 at 17:14
  • Should I delete some part of the question to make it more clear or I can just append the Example at the end? – mikyll98 Aug 12 '22 at 17:24
  • Assuming the [mre] reproduces the problem, I would delete large parts of the question. The [edit history](https://stackoverflow.com/posts/73328208/revisions) is available anyway. – James_D Aug 12 '22 at 17:25
  • This doesn't reproduce the issue for me. I only see the alert once. JavaFX 18.0.1 on JDK 18.0.2. – James_D Aug 12 '22 at 17:36
  • @James_D is it possible that it was fixed between Java 11 and Java 18? – mikyll98 Aug 12 '22 at 17:43

1 Answers1

1

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();
    }
}
Sonnenkind
  • 74
  • 2