Search code examples
javafx-2mouseeventvisibilityinvisiblescenegraph

JavaFX 2.2 Mouseevent for invisible node


I'm trying to receive MouseEvents for an invisible node in JavaFX 2.2. Think of it as an interactive but invisible Zone that should trigger an action for example when the mouse hovers it. The problem is, that this is not a statically defined zone, but there are multiple zones (a lot of it) that can be moved and resized by the application. So for my use-case it would be a lot of overhead to globally listen to mouse-movement and perform a manual detection of - say - MouseMove-Events.

Currently, I'm experimenting with a transparent Rectangle (new Rectangle(200, 100, Color.TRANSPARENT)), but the actual / final Application will use some sort of a Pane for it, because it's actually a draggable container for other components (when not full of components, it has transparent areas and MouseMoves must be detected on these transparent areas as well).

I additionally would appreciate answers that help me to get a better understanding of how JavaFX 2.2 generally handles MouseEvents depending on the visibility of nodes.

My experiments have shown the following general insights so far:

  • Given a transparent Scene: Mouse Events will only be passed to foreign Applications (visually below the Scene) when the user clicks on a transparent area. There's no way to pass a mouse-event "to the OS" when the user clicks on a visible pixel of the Scene. Right?

  • A Pane on top of other nodes will per default swallow any MouseEvent unless it is MouseTransparent or the MouseClick appears on a non-visible (transparent) area.

  • pickOnBounds(true|false) is there to enable (true) bounds-based (rectangular) detection of MouseEvents or disable it (false). Latter effectively handles mouse-events only for visible pixels / areas. pickOnBounds(true) seems not to work for completely invisible nodes. Right?

  • My experiments have shown, that a node needs at least a fill of - new Color(1,1,1,0.004) to be considered visible. Lower alpha-values are considered invisible, which causes MouseEvents not to be handled, even if pickOnBounds(true) has been called.

Did I get this right? Then there would be no way for an invisible Node to receive MouseEvents.

Or is there a special requirement for pickOnBounds to work? Do I need to call it only after the node has been shown or something similar? Any other suggestions?


Solution

  • In a nutshell: use Node.setOpacity(0.0)

    The opacity property controls a Node's "visual transparency" without affecting it's ability to receive Events, see APIdocs. Setting this property to zero achieves the effect you (and I) were looking for: An invisible but mouse-sensitive "hot zone"-Node.

    This is in contrast to Node.setVisible(false) which I tried first. That approach also disables Event handling. From Node.setVisible() APIdocs:

    Invisible nodes never receive mouse events or keyboard focus and never maintain keyboard focus when they become invisible.

    "Invisible" there really means "after calling setVisible(false)" and should not be confused with opacity or fully transparent pixels in an image.

    Due to lack of reputation I can't post a screenshot directly, so: link to screenshot that shows the hot zone layout of the example code below (the Node's opacity is not set to 0 in the screenshot for obvious reasons).

    The example uses a Group as hot zone which contains a rectangle and a circle to define the areas in which mouse events are captured. The opacity property and mouse handler only need to be set on the Group, not it's children.

    That way you can construct arbitrarily shaped hot zones. If you want to use an image with transparent areas as a hot zone, its pickOnBounds property needs to be set to false so the actual image content is considered, not only the bounding box.

    Hope it helps!

    public class HotZoneTest extends Application {
    
        @Override
        public void start(Stage primaryStage) {
            StackPane root = new StackPane();
            Scene scene = new Scene(root, 300, 250);
            primaryStage.setScene(scene);
            primaryStage.show();
    
            Group hotZone = new Group();
            root.getChildren().add(hotZone);
    
            hotZone.getChildren().add(new Rectangle(10, 20, 100, 50));
            hotZone.getChildren().add(new Circle(50, 120, 20));
    
            hotZone.setOpacity(0.4); //set to 0.0 to make invisible
    
            EventHandler handler = new EventHandler() {
                @Override
                public void handle(Event e) {
                    System.out.println("hotZone mouse event: " + e);
                }
            };
            hotZone.addEventHandler(MouseEvent.MOUSE_ENTERED, handler);
            hotZone.addEventHandler(MouseEvent.MOUSE_CLICKED, handler);
            hotZone.addEventHandler(MouseEvent.MOUSE_EXITED, handler);
    
        }
    

    edit: regarding your specific sub-qestions (all as far as I know, I'm not an FX guru :) )

    There's no way to pass a mouse-event "to the OS" when the user clicks on a visible pixel of the Scene. Right?

    Interesting, never tried that. A pure speculation on what might work: Get screen coordinates of mouse event, move your window out of the way, use java.awt.Robot to move the OS cursor to coordinates of mouse event, fire a click there if needed, and move your window back. Beware: Sounds like a total hack!

    A Pane on top of other nodes will per default swallow any MouseEvent unless it is MouseTransparent or the MouseClick appears on a non-visible (transparent) area.

    That's how I understand it, too; not sure about mouse enter/exit though. For those you can listen on MOUSE_ENTERED_TARGET/MOUSE_EXITED_TARGET at least in a parent to determine which child was entered/exited. Register an event filter on the parent and consume the event there if you want to prevent the child from receiving the event.

    pickOnBounds(true|false) is there to enable (true) bounds-based (rectangular) detection of MouseEvents or disable it (false). Latter effectively handles mouse-events only for visible pixels / areas.

    Yes.

    pickOnBounds(true) seems not to work for completely invisible nodes.

    True for Nodes made invisible by calling setInvisible(true).

    My experiments have shown, that a node needs at least a fill of - new Color(1,1,1,0.004) to be considered visible.

    Can't comment, but your experiment result seems sound.

    Then there would be no way for an invisible Node to receive MouseEvents.

    Using .setOpacity(0.0) makes a Node "visually invisible", but still receive events and honor setbickOnBounds