1

I've encountered strange behavior of JavaFX application running on linux. Application is a simple graphic editor. Its main area represented by ScrollPane with two scrollbars, and scroll pane's content AnchorPane, which children are two layers - graphic layer (where all objects are placed) and selection layer (for implementing selection frame). Graphic layer also has context menu.

Selection frame is implemented by detecting drag events. You press mouse button and selection frame is created, then you move mouse and rectangle repaints according to mouse X and Y coordinates. When you release button, all objects inside rectangle are selected, then rectangle is removed. Key moment of this behavior is whenever I drag mouse to outside of the stage, mouse drag events are still detected, so rectangle increases in size and scrollpane's scrollbars are decreasing accordingly, showing that overall graphic area size is increased. That's normal behavior.

The problem is, after opening context menu, when I try to select objects with selection frame, selection frame cannot increase itself to outside of visible area of scroll pane, cause drag events aren't detected outside of stage anymore. Mouse pressed is detected, mouse dragged detected only inside visible area of scroll pane. But if I open context menu, THEN press mouse button on graphic layer and AFTER this I use selection frame, then everything works properly. Another way to return to normal behavior - is pressing ESC key.

This problem exists only on Linux, on Windows everything works perfectly. Also this problem appeared after migrating from Java 8 with built-in JavaFX to Java 17 with JavaFX 21.

I came up with several workarounds for this problem, for example using Robot API to press ESC key when context menu is hidden, but I'd like to solve this problem in cleaner and more appropriate way.

I would appreciate any help.

Minimal, Reproducible Example

package org.example;

import javafx.application.Application;
import javafx.geometry.Point2D;
import javafx.scene.Scene;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.ScrollPane;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.BorderPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;

public class Main extends Application {
    public static void main(String[] args) {
        launch(args);
    }

    public void start(Stage primaryStage) {
        AnchorPane mainPane = new AnchorPane();

        //Creating scroll pane
        ScrollPane scroll = new ScrollPane();
        scroll.setContent(mainPane);
        scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.ALWAYS);
        scroll.setVbarPolicy(ScrollPane.ScrollBarPolicy.ALWAYS);
        scroll.setPrefWidth(1024);
        scroll.setPrefHeight(768);

        //Creating selection layer to place selection frame
        AnchorPane selectionLayer = new AnchorPane();
        selectionLayer.setMouseTransparent(true);

        //Creating graphic layer
        BorderPane graphicLayer = new BorderPane();
        graphicLayer.setMinHeight(768);
        graphicLayer.setMinWidth(1024);
        graphicLayer.setFocusTraversable(false);

        //Creating context menu
        ContextMenu contextMenu = new ContextMenu();
        contextMenu.getItems().add(new MenuItem("test1"));
        contextMenu.getItems().add(new MenuItem("test2"));

        graphicLayer.setOnContextMenuRequested(e -> contextMenu.show(graphicLayer, e.getScreenX(), e.getScreenY()));

        SelectionController controller = new SelectionController(selectionLayer);

        graphicLayer.setOnMousePressed(event -> {
            System.err.println("PRESSED");
            if (contextMenu.isShowing()) {
                contextMenu.hide();
            }
            controller.startSelection(event);
        });

        graphicLayer.setOnMouseDragged(event -> {
            System.err.println("DRAGGED");
            controller.setPosition(event);
        });

        graphicLayer.setOnMouseReleased(event -> {
            System.err.println("RELEASED");
            controller.finishSelection(event);
        });

        mainPane.getChildren().addAll(graphicLayer, selectionLayer);

        Scene scene = new Scene(scroll);
        primaryStage.setScene(scene);

        primaryStage.show();
    }

    class SelectionController {
        AnchorPane selectionLayer;
        Point2D startingPoint;
        Rectangle selectionFrame;

        SelectionController(AnchorPane selectionLayer) {
            this.selectionLayer = selectionLayer;
        }

        void startSelection(MouseEvent event) {
            startingPoint = new Point2D(event.getX(), event.getY());

            selectionFrame = new Rectangle();
            selectionFrame.setId("selection_frame");
            selectionFrame.setFill(Color.TRANSPARENT);
            selectionFrame.getStrokeDashArray().add(10.0);
            selectionFrame.setStrokeWidth(2.0);
            selectionFrame.setStroke(Color.LIGHTSKYBLUE);
            selectionFrame.xProperty().set(startingPoint.getX());
            selectionFrame.yProperty().set(startingPoint.getY());

            selectionLayer.getChildren().add(selectionFrame);
        }

        void setPosition(MouseEvent event) {
            double x = event.getX();
            double y = event.getY();

            if (x < startingPoint.getX()) {
                selectionFrame.xProperty().set(x);
                selectionFrame.widthProperty().set(startingPoint.getX() - x);
            } else {
                selectionFrame.widthProperty().set(x - startingPoint.getX());
            }

            if (y < startingPoint.getY()) {
                selectionFrame.yProperty().set(y);
                selectionFrame.heightProperty().set(startingPoint.getY() - y);
            } else {
                selectionFrame.heightProperty().set(y - startingPoint.getY());
            }
        }

        void finishSelection(MouseEvent event) {
            // To some logic to filter objects on graphic layer inside selection frame

            selectionLayer.getChildren().clear();
        }
    }
}

Actions to reproduce:

  1. Launch application
  2. Try to use selection frame, top left corner inside stage, then drag bottom right corner outside visible boundaries of stage, you can see that dragged events are still triggered
  3. Press right button to invoke context menu
  4. When menu is still showing try to repeat step 2, you can see that selection frame doesn't grow beyond stage boundaries, despite mouse pointer is outside stage. Also dragged events are not triggered outside stage
  5. Repeat step 3. Now if you click inside stage or press ESC button before creating selection frame, selection frame behaves as it should
7
  • 2
    Please provide Minimal Reproducible example. Without code it is very hard to analyze your issue.
    – Sai Dandem
    Commented Jun 27 at 8:08
  • @SaiDandem I've updated question with example code
    – vit
    Commented Jun 27 at 11:12
  • I tried running this, but could not as it relied on a third-party library (controlsfx), which I did not have. Could you update your example to demonstrate the issue without requiring a third-party library?
    – jewelsea
    Commented Jun 27 at 21:06
  • I too tried running the program (I do have ControlsFX). I am lost at step 4. For me it works as expected. Or in other way, it is the same behaviour irrespective if I open context menu or not.
    – Sai Dandem
    Commented Jun 27 at 22:13
  • And what i really did'nt get is the stuff you are doing on NotificationPane. Is it really meant for that? like adding events, adding that as the main node.. etc? I think you need to rethink about your layout as well. (If I understand correctly).
    – Sai Dandem
    Commented Jun 27 at 22:37

1 Answer 1

1

After trying a lot of different options, I've found an acceptable workround. ContextMenu's auto-hiding mechanism seems to have some effect on underlying panes so:

contextMenu.setAutoHide(false);

solves the problem.

Of course now I have to handle menu hiding manually, but anyways I'll stick with this solution.

Not the answer you're looking for? Browse other questions tagged or ask your own question.