Transparent JavaFX stage capture scrolling events if there is another window behind

geh :

Mouse events and scroll events behave in different ways

diagram

Mouse Events:

  1. The event is captured by mainStage

  2. The event is captured by mainStage

  3. The event is not captured

Scroll Events:

  1. The event is captured by mainStage

  2. The event is captured by secondStage

  3. The event is not captured

Is there any way that transparent secondStage does not capture scroll events?

My code:

Pane mainPane = new Pane(new Label("Main Stage"));
mainPane.setPrefSize(300, 300);
mainStage.setScene(new Scene(mainPane));

Stage secondStage = new Stage();
Pane secondPane = new Pane(new Label("Second Stage"));
secondPane.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, CornerRadii.EMPTY, Insets.EMPTY)));
secondPane.setBorder(new Border(
    new BorderStroke(Color.BLACK, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, new BorderWidths(2))));
secondPane.setPrefSize(300, 300);
secondStage.setScene(new Scene(secondPane, Color.TRANSPARENT));
secondStage.initStyle(StageStyle.TRANSPARENT);

mainStage.getScene().setOnScroll(event -> System.out.println("Scroll in main stage"));
secondStage.getScene().setOnScroll(event -> System.out.println("Scroll in second stage"));
mainStage.getScene().setOnMouseClicked(event -> System.out.println("Click in main stage"));
secondStage.getScene().setOnMouseClicked(event -> System.out.println("Click in second stage"));

mainStage.show();
secondStage.show();

Java version: 1.8.0_201 (64 bits), Windows 10

edit: The example is a simplification with only two windows. Fire the event programmatically implies discovering which stage is immediately lower and that is another problem in itself.

Sai Dandem :

It might be a great coincidence, that we also came with the same solution of transparent window because of not having the feature of managing z-index of stages. And We encountered the exact same issue as yours. ie, scroll events not propagating to underlying Stages. We used the below approach, not sure whether this can help you:

Firstly, We constructed a Singleton class that keeps a reference of Node that is currently hovered on.

Then, when we create any normal stage, we include the below handlers to the scene of that new stage. The key thing here is that, the mouse events are still able to pass through the transparent stage to the underlying window, keep track of node which sits under the mouse.

scene.addEventFilter(MouseEvent.MOUSE_EXITED_TARGET, e -> {
    hoverNode.set(null);
});
scene.addEventFilter(MouseEvent.MOUSE_MOVED, e -> {
    hoverNode.set(e.getTarget());
});

In the scene of the transparent window, we included the below handlers to delegate the scroll events to the underlying node.

scene.addEventFilter(ScrollEvent.SCROLL, e -> {
    if (hoverNode.get() != null) {
        Event.fireEvent(hoverNode.get(), e);
    }
});
scene.addEventHandler(ScrollEvent.SCROLL, e -> {
    if (hoverNode.get() != null) {
        Event.fireEvent(hoverNode.get(), e);
    }
});

I am pretty sure this is not the most desired way. But this addressed our issue. :)

Below is the quick demo code of what I mean.

import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.event.Event;
import javafx.event.EventTarget;
import javafx.geometry.Insets;
import javafx.geometry.Rectangle2D;
import javafx.scene.Cursor;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.StageStyle;

import java.util.stream.IntStream;

public class ScrollThroughTransparentStage_Demo extends Application {
    @Override
    public void start(Stage stage) throws Exception {
        stage.setTitle("Main Window");
        VBox root = new VBox(buildScrollPane());
        root.setStyle("-fx-background-color:#888888;");
        root.setSpacing(10);
        root.setPadding(new Insets(10));

        Button normalStageBtn = new Button("Normal Stage");
        normalStageBtn.setOnAction(e -> {
            Stage normalStage = new Stage();
            normalStage.initOwner(stage);
            Scene normalScene = new Scene(buildScrollPane(), 300, 300);
            addHandlers(normalScene);
            normalStage.setScene(normalScene);
            normalStage.show();
        });

        CheckBox allowScrollThrough = new CheckBox("Allow scroll through transparency");
        allowScrollThrough.setSelected(true);

        HBox buttons = new HBox(normalStageBtn);
        buttons.setSpacing(20);
        root.getChildren().addAll(allowScrollThrough,buttons);
        Scene scene = new Scene(root, 600, 600);
        addHandlers(scene);
        stage.setScene(scene);
        stage.show();

        /* Transparent Stage */
        Stage transparentStage = new Stage();
        transparentStage.initOwner(stage);
        transparentStage.initStyle(StageStyle.TRANSPARENT);
        Pane mainRoot = new Pane();
        Pane transparentRoot = new Pane(mainRoot);
        transparentRoot.setStyle("-fx-background-color:transparent;");
        Scene transparentScene = new Scene(transparentRoot, Color.TRANSPARENT);
        transparentStage.setScene(transparentScene);
        transparentScene.addEventFilter(ScrollEvent.SCROLL, e -> {
            if (allowScrollThrough.isSelected() && HoverNodeSingleton.getInstance().getHoverNode() != null) {
                Event.fireEvent(HoverNodeSingleton.getInstance().getHoverNode(), e);
            }
        });
        transparentScene.addEventHandler(ScrollEvent.SCROLL, e -> {
            if (allowScrollThrough.isSelected() && HoverNodeSingleton.getInstance().getHoverNode() != null) {
                Event.fireEvent(HoverNodeSingleton.getInstance().getHoverNode(), e);
            }
        });
        determineStageSize(transparentStage, mainRoot);
        transparentStage.show();

        Button transparentStageBtn = new Button("Transparent Stage");
        transparentStageBtn.setOnAction(e -> {
            MiniStage miniStage = new MiniStage(mainRoot);
            ScrollPane scrollPane = buildScrollPane();
            scrollPane.setPrefSize(300, 300);
            miniStage.setContent(scrollPane);
            miniStage.show();
        });
        buttons.getChildren().add(transparentStageBtn);
    }

    private static void determineStageSize(Stage stage, Node root) {
        DoubleProperty width = new SimpleDoubleProperty();
        DoubleProperty height = new SimpleDoubleProperty();
        DoubleProperty shift = new SimpleDoubleProperty();
        Screen.getScreens().forEach(screen -> {
            Rectangle2D bounds = screen.getVisualBounds();
            width.set(width.get() + bounds.getWidth());

            if (bounds.getHeight() > height.get()) {
                height.set(bounds.getHeight());
            }
            if (bounds.getMinX() < shift.get()) {
                shift.set(bounds.getMinX());
            }
        });
        stage.setX(shift.get());
        stage.setY(0);
        stage.setWidth(width.get());
        stage.setHeight(height.get());
        root.setTranslateX(-1 * shift.get());
    }

    private void addHandlers(Scene scene) {
        scene.addEventFilter(MouseEvent.MOUSE_EXITED_TARGET, e -> {
            HoverNodeSingleton.getInstance().setHoverNode(null);
        });
        scene.addEventFilter(MouseEvent.MOUSE_MOVED, e -> {
            HoverNodeSingleton.getInstance().setHoverNode(e.getTarget());
        });
    }

    private ScrollPane buildScrollPane() {
        VBox vb = new VBox();
        vb.setSpacing(10);
        vb.setPadding(new Insets(15));
        IntStream.rangeClosed(1, 100).forEach(i -> vb.getChildren().add(new Label(i + "")));
        ScrollPane scrollPane = new ScrollPane(vb);
        return scrollPane;
    }

    class MiniStage extends Group {
        private Pane parent;
        double sceneX, sceneY, layoutX, layoutY;
        protected BorderPane windowPane;
        private BorderPane windowTitleBar;
        private Label labelTitle;
        private Button buttonClose;

        public MiniStage(Pane parent) {
            this.parent = parent;
            buildRootNode();
            getChildren().add(windowPane);
            addEventHandler(MouseEvent.MOUSE_PRESSED, e -> toFront());
        }

        @Override
        public void toFront() {
            parent.getChildren().remove(this);
            parent.getChildren().add(this);
        }

        public void setContent(Node content) {
            // Computing the bounds of the content before rendering
            Group grp = new Group(content);
            new Scene(grp);
            grp.applyCss();
            grp.requestLayout();
            double width = grp.getLayoutBounds().getWidth();
            double height = grp.getLayoutBounds().getHeight() + 30; // 30 title bar height
            grp.getChildren().clear();

            windowPane.setCenter(content);
            // Centering the stage
            Rectangle2D screenBounds = Screen.getPrimary().getBounds();
            setX(screenBounds.getWidth() / 2 - width / 2);
            setY(screenBounds.getHeight() / 2 - height / 2);
        }

        public Node getContent() {
            return windowPane.getCenter();
        }

        public void setX(double x) {
            setLayoutX(x);
        }

        public void setY(double y) {
            setLayoutY(y);
        }

        public void show() {
            if (!parent.getChildren().contains(this)) {
                parent.getChildren().add(this);
            }
        }

        public void hide() {
            parent.getChildren().remove(this);
        }

        private void buildRootNode() {
            windowPane = new BorderPane();
            windowPane.setStyle("-fx-border-width:2px;-fx-border-color:#444444;");
            labelTitle = new Label("Mini Stage");
            labelTitle.setStyle("-fx-font-weight:bold;");
            labelTitle.setMaxHeight(Double.MAX_VALUE);
            buttonClose = new Button("X");
            buttonClose.setFocusTraversable(false);
            buttonClose.setStyle("-fx-background-color:red;-fx-background-radius:0;-fx-background-insets:0;");
            buttonClose.setOnMouseClicked(evt -> hide());

            windowTitleBar = new BorderPane();
            windowTitleBar.setStyle("-fx-border-width: 0 0 2px 0;-fx-border-color:#444444;-fx-background-color:#BBBBBB");
            windowTitleBar.setLeft(labelTitle);
            windowTitleBar.setRight(buttonClose);
            windowTitleBar.setPadding(new Insets(0, 0, 0, 10));
            windowTitleBar.getStyleClass().add("nonfocus-title-bar");
            windowPane.setTop(windowTitleBar);
            assignTitleBarEvents();
        }

        private void assignTitleBarEvents() {
            windowTitleBar.setOnMousePressed(this::recordWindowLocation);
            windowTitleBar.setOnMouseDragged(this::moveWindow);
            windowTitleBar.setOnMouseReleased(this::resetMousePointer);
        }

        private final void recordWindowLocation(final MouseEvent event) {
            sceneX = event.getSceneX();
            sceneY = event.getSceneY();
            layoutX = getLayoutX();
            layoutY = getLayoutY();
            getScene().setCursor(Cursor.MOVE);
        }

        private final void resetMousePointer(final MouseEvent event) {
            // Updating the new layout positions
            setLayoutX(layoutX + getTranslateX());
            setLayoutY(layoutY + getTranslateY());

            // Resetting the translate positions
            setTranslateX(0);
            setTranslateY(0);
            getScene().setCursor(Cursor.DEFAULT);
        }

        private final void moveWindow(final MouseEvent event) {
            double offsetX = event.getSceneX() - sceneX;
            double offsetY = event.getSceneY() - sceneY;
            setTranslateX(offsetX);
            setTranslateY(offsetY);
            event.consume();
        }
    }
}

/**
 * Singleton class.
 */
class HoverNodeSingleton {
    private static HoverNodeSingleton INSTANCE = new HoverNodeSingleton();
    private EventTarget hoverNode;

    private HoverNodeSingleton() {
    }

    public static HoverNodeSingleton getInstance() {
        return INSTANCE;
    }

    public EventTarget getHoverNode() {
        return hoverNode;
    }

    public void setHoverNode(EventTarget hoverNode) {
        this.hoverNode = hoverNode;
    }
}

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=304722&siteId=1