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:
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.