JavaFX KeyEvent propagation order


Question

I want to listen to some KeyEvent in my scene, say KeyCode.ESCAPE(close the scene when pressed).

scene.addEventHandler(KeyEvent.ANY, event -> {
            if (event.isConsumed())
                return;
            switch (event.getCode()) {
            case ESCAPE:
                stage.hide();
                event.consume();
                break;
            default:
                break;
            }
        });

Now, the nodes inside the scene could have listened to ESCAPE too.

// ....
someOtherNode.addEventHandler(KeyEvent.ANY, e -> {
        if (e.getCode() == KeyCode.ESCAPE) {
            // do stuff
            e.consume();
        }
});
// ....

How do I make sure that the KeyEvent will be consumed from the node and not the scene?

Based on the diagram from Oracle, A workaround would be adding a dummy Node at the end of the Node hierarchy that listens to KeyCodes

enter image description here

But is there a better solution, like inverting the propagation route?

EDIT:

The use case:

A popup-like node that blocks other nodes would need to listens to the ESC key or focusProperty() so that it can close itself.

1
4
2/10/2014 8:52:09 AM

Accepted Answer

There's two ways you can affect events:

  1. Use the Node.addEventFilter(...) method to register a filter. A filter will execute on the capturing phase of the event (as the window is getting more specific, determining which Nodes should get the event).

  2. Use the Node.addEventHandler(...) method to register a handler. The handler will execute starting at the most specific node found in the capturing phase, heading down until it is consumed.

So in the capturing phase, a stack is created. Starting with the window (topmost parent), each node that this event could potentially execute on is added to the stack (ending with the bottom most child). A filter can interrupt this process, or just execute an event during this process.

In the bubbling phase, the event handlers will start firing from the top of the stack (created in the capturing phase) until the stack is empty or the event is consumed.

In your case, you really shouldn't have anything to worry about. If any node cares about processing the "ESC" event, they will do so in the bubbling phase (and they should consume the event to prevent further processing). You can see this behavior in the ComboBox. If they don't care, it will bubble up to your Scene and that handler will execute. Just make sure any custom code you create that processes an "ESC" press also consumes that event.

For more information, there is a explanation and tutorial here: http://docs.oracle.com/javafx/2/events/jfxpub-events.htm

And here is some sample code demonstrating the Escape functionality. Pressing ESC while focused on the ComboBox will not cause the application to close, while it will close with the other controls.

import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.TableColumn.CellDataFeatures;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Callback;
import javafx.util.converter.DefaultStringConverter;


public class FXEventFiltering extends Application {

    public static void main(String[] args) { launch(args); }

    @Override
    public void start(final Stage stage) throws Exception {
        //All the controls are added here
        VBox box = new VBox();
        ComboBox<String> dropdown = new ComboBox<>();
        TextField field = new TextField();
        CheckBox check = new CheckBox("Check");
        RadioButton radio = new RadioButton("Radio!");
        TextArea area = new TextArea();
        TableView<String> table = new TableView<String>(FXCollections.observableArrayList(new String[]{"one","two"}));
        TableColumn<String, String> tc = new TableColumn<String, String>("Column1");
        tc.setEditable(true);
        tc.setCellFactory(TextFieldTableCell.<String,String>forTableColumn(new DefaultStringConverter()));
        tc.setCellValueFactory(new Callback<CellDataFeatures<String,String>, ObservableValue<String>>(){
            @Override
            public ObservableValue<String> call(CellDataFeatures<String, String> arg0) {
                return new SimpleStringProperty(arg0.getValue());
            }});
        table.getColumns().add(tc);

        box.getChildren().addAll(dropdown, field, check, radio, area, table);

        //Setting up your scene
        Scene scene = new Scene(box);
        stage.setScene(scene);
        scene.addEventHandler(KeyEvent.ANY, new EventHandler<KeyEvent>() {
            @Override
            public void handle(KeyEvent event) {
                System.out.println("KEYS!" + event.getEventType().getName());
                switch (event.getCode()) {
                case ESCAPE:
                    System.out.println("Escape!");
                    stage.hide();
                    event.consume();
                    break;
                default:
                    break;
                }
            }
        });

        box.requestFocus(); // Removing default focus

        stage.show();
    }

}
6
2/12/2014 10:18:39 PM

Maybe you could loop over all nodes after catching the event in the scene to find out which node has actual focus? Then you could call node method to close?


Licensed under: CC-BY-SA with attribution
Not affiliated with: Stack Overflow
Icon