Mutant Bob
Mutant Bob

Reputation: 3549

How do I dispatch key events to the correct JavaFX shape that was under the mouse?

I have a JavaFX Scene with a couple of different Groups. Each group has several filled PolyLines.

I would like to trigger some computation when the user presses the n key while they are inside one of the filled PolyLines.

I used scene.setOnKeyPressed to install a KeyEvent handler and printed it out. I'm not sure how to figure out which PolyLine the event occurred over. The target of the event happens to be a Button left over from an early tutorial I was following. The event printout does not display any coordinates, and even if it had some, I'm not sure how best to traverse the Node tree to hunt down which PolyLine is of interest.

What is the proper idiom for responding to key events that happen while the mouse is over a JavaFX Shape?

Upvotes: 1

Views: 337

Answers (1)

Slaw
Slaw

Reputation: 45776

Key events are only delivered to the Node that has the focus. You can request a Node obtain focus by calling Node#requestFocus(). Using that, one solution is you can add mouse event handlers to each Shape that request focus when the mouse enters the area. Then you add key event handlers to each Shape rather than the Scene. The key event will be delivered to, and handled by, the Node with the focus, which will be the Shape which has the mouse over it. To stop key events from being delivered to the Shape once the mouse exits the area you can add a handler that requests focus somewhere else (e.g. a common parent or the root of the scene).

import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.HBox;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Polygon;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.Shape;
import javafx.stage.Stage;

public final class App extends Application {

    @Override
    public void start(Stage primaryStage) {
        var root = new HBox(15, createCircle(), createTriangle(), createSquare());
        root.setAlignment(Pos.CENTER);
        root.setPadding(new Insets(30));

        primaryStage.setScene(new Scene(root));
        primaryStage.setTitle("Shapes");
        primaryStage.show();
    }

    private Shape createCircle() {
        var circle = new Circle(100);
        addHandlers(circle, "Circle");
        return circle;
    }

    private Shape createTriangle() {
        var polygon = new Polygon(100, 0, 200, 200, 0, 200, 100, 0);
        addHandlers(polygon, "Triangle");
        return polygon;
    }

    private Shape createSquare() {
        var rectangle = new Rectangle(200, 200);
        addHandlers(rectangle, "Square");
        return rectangle;
    }

    private void addHandlers(Shape shape, String name) {
        // Notice you have access to the specific shape in the 
        // event handlers. The source of the event will also
        // be the shape.
        shape.setOnMouseEntered(event -> {
            if (!shape.isFocused()) {
                event.consume();
                shape.requestFocus();
            }
        });
        shape.setOnMouseExited(event -> {
            if (shape.isFocused()) {
                event.consume();
                shape.getScene().getRoot().requestFocus();
            }
        });
        shape.setOnKeyPressed(event -> {
            if (event.getCode() == KeyCode.N) {
                event.consume();
                System.out.println("KEY_PRESSED['N']: " + name);
            }
        });
    }

}

Another option to adding MOUSE_ENTERED and MOUSE_EXITED event handlers is to listen to the Node#hover property of the Shape. When it changes to true, request the focus; when it changes to false, remove the focus.


Requesting the Shape have the focus will pull the focus away from the Node that currently has the focus. For instance, if you have a TextField with the focus then it will lose the focus whenever the mouse hovers over one of the Shapes. This will prevent the user from being able to type into the TextField (until they go and click on it).

If this behavior is undesirable then you can, instead of requesting focus, maintain some field/property/model that tells you which Shape is currently hovered over. Then you'd add an event filter to a common parent of the TextField and Shapes. If a Shape has the mouse hovering over it you do what is necessary and then consume the event (which will stop it from reaching whichever Node has the focus).

Upvotes: 4

Related Questions