Adam
Adam

Reputation: 36703

Delegate mouse events to all children in a JavaFX StackPane

I'm trying to come up with a solution to allow multiple Pane nodes handle mouse events independently when assembled into a StackPane

StackPane
   Pane 1
   Pane 2
   Pane 3

I'd like to be able to handle mouse events in each child, and the first child calling consume() stops the event going to the next child.

I'm also aware of setPickOnBounds(false), but this does not solve all cases as some of the overlays will be pixel based with Canvas, i.e. not involving the scene graph.

I've tried various experiments with Node.fireEvent(). However these always lead to recursion ending in stack overflow. This is because the event is propagated from the root scene and triggers the same handler again.

What I'm looking for is some method to trigger the event handlers on the child panes individually without the event travelling through its normal path.

My best workaround so far is to capture the event with a filter and manually invoke the handler. I'd need to repeat this for MouseMoved etc

parent.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {
    for (Node each : parent.getChildren()) {
        if (!event.isConsumed()) {
            each.getOnMouseClicked().handle(event);
        }
    }
    event.consume();
});

However this only triggers listeners added with setOnMouseClicked, not addEventHandler, and only on that node, not child nodes.

Another sort of solution is just to accept JavaFX doesn't work like this, and restructure the panes like this, this will allow normal event propagation to take place.

Pane 1
   Pane 2
      Pane 3

Example

import javafx.application.Application;
import javafx.event.Event;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

public class EventsInStackPane extends Application {

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

    private static class DebugPane extends Pane {
        public DebugPane(Color color, String name) {
            setBackground(new Background(new BackgroundFill(color, CornerRadii.EMPTY, Insets.EMPTY)));
            setOnMouseClicked(event -> {
                System.out.println("setOnMouseClicked " + name + " " + event);
            });
            addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
                System.out.println("addEventHandler " + name + " " + event);
            });
            addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {
                System.out.println("addEventFilter " + name + " " + event);
            });
        }

    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        DebugPane red = new DebugPane(Color.RED, "red");
        DebugPane green = new DebugPane(Color.GREEN, "green");
        DebugPane blue = new DebugPane(Color.BLUE, "blue");
        setBounds(red, 0, 0, 400, 400);
        setBounds(green, 25, 25, 350, 350);
        setBounds(blue, 50, 50, 300, 300);

        StackPane parent = new StackPane(red, green, blue);
        eventHandling(parent);

        primaryStage.setScene(new Scene(parent));
        primaryStage.show();
    }

    private void eventHandling(StackPane parent) {
        parent.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
            if (!event.isConsumed()) {
                for (Node each : parent.getChildren()) {
                    Event copy = event.copyFor(event.getSource(), each);
                    parent.fireEvent(copy);
                    if (copy.isConsumed()) {
                        break;
                    }
                }
            }
            event.consume();
        });
    }
    private void setBounds(DebugPane panel, int x, int y, int width, int height) {
        panel.setLayoutX(x);
        panel.setLayoutY(y);
        panel.setPrefWidth(width);
        panel.setPrefHeight(height);
    }
}

Upvotes: 4

Views: 425

Answers (1)

Adam
Adam

Reputation: 36703

Using the hint from @jewelsea I was able to use a custom chain. I've done this from a "catcher" Pane which is added to the front of the StackPane. This then builds a chain using all the children, in reverse order, excluding itself.

private void eventHandling(StackPane parent) {
    Pane catcher = new Pane() {
        @Override
        public EventDispatchChain buildEventDispatchChain(EventDispatchChain tail) {
            EventDispatchChain chain = super.buildEventDispatchChain(tail);
            for (int i = parent.getChildren().size() - 1; i >= 0; i--) {
                Node child = parent.getChildren().get(i);
                if (child != this) {
                    chain = chain.prepend(child.getEventDispatcher());
                }
            }
            return chain;
        }
    };
    parent.getChildren().add(catcher);
}

Upvotes: 3

Related Questions