Я столкнулся со странным поведением приложения JavaFX, работающего в Linux. Приложение представляет собой простой графический редактор. Его основная область представлена ScrollPane с двумя полосами прокрутки и содержимым панели прокрутки AnchorPane, дочерними элементами которого являются два слоя - графический слой (где размещаются все объекты) и слой выбора (для реализации рамки выбора). Графический слой также имеет контекстное меню.
Выбор кадра реализуется путем обнаружения событий перетаскивания. Вы нажимаете кнопку мыши, и создается рамка выделения, затем вы перемещаете мышь и прямоугольник перерисовывается в соответствии с координатами X и Y мыши. Когда вы отпускаете кнопку, все объекты внутри прямоугольника выбираются, а затем прямоугольник удаляется. Ключевым моментом этого поведения является то, что всякий раз, когда я перетаскиваю мышь за пределы сцены, события перетаскивания мышью по-прежнему обнаруживаются, поэтому размер прямоугольника увеличивается, а полосы прокрутки соответственно уменьшаются, показывая, что общий размер графической области увеличивается. Это нормальное поведение.
Проблема в том, что после открытия контекстного меню, когда я пытаюсь выбрать объекты с помощью рамки выбора, рамка выбора не может выйти за пределы видимой области панели прокрутки, потому что события перетаскивания больше не обнаруживаются за пределами сцены. Нажатие мыши обнаруживается, перетаскивание мыши обнаруживается только внутри видимой области панели прокрутки. Но если я открываю контекстное меню, ЗАТЕМ нажимаю кнопку мыши на графическом слое и ПОСЛЕ этого использую рамку выделения, то все работает правильно. Другой способ вернуться к нормальному поведению – нажать клавишу ESC.
Данная проблема существует только в Linux, в Windows все работает идеально. Также эта проблема появилась после перехода с Java 8 со встроенным JavaFX на Java 17 с JavaFX 21.
Я придумал несколько обходных путей этой проблемы, например, используя Robot API для нажатия клавиши ESC, когда контекстное меню скрыто, но я хотел бы решить эту проблему более чистым и подходящим способом.
Буду признателен за любую помощь.
Минимальный воспроизводимый пример
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();
}
}
}
Действия по воспроизведению:
@SaiDandem Я обновил вопрос с примером кода
Я попробовал запустить это, но не смог, поскольку он опирался на стороннюю библиотеку (controlsfx), которой у меня не было. Не могли бы вы обновить свой пример, чтобы продемонстрировать проблему, не требуя сторонней библиотеки?
Я тоже попробовал запустить программу (у меня есть ControlsFX). Я потерялся на шаге 4. У меня это работает так, как ожидалось. Или, другими словами, это одно и то же поведение независимо от того, открываю я контекстное меню или нет.
И чего я действительно не понял, так это того, что вы делаете в NotificationPane. Действительно ли оно для этого предназначено? например, добавление событий, добавление их в качестве основного узла... и т. д.? Думаю, вам тоже стоит переосмыслить планировку. (если я правильно понимаю).
@jewelsea Я обновил код, чтобы исключить использование ControlsFX, проблема все еще сохраняется. Обратите внимание, что проблема может быть воспроизведена только в Linux.
@SaiDandem Обратите внимание, что проблема может быть воспроизведена только в Linux. Что касается верстки, использования графического слоя в качестве основного узла и обработки событий на нем - данный фрагмент кода является примером, в реальной жизни этот графический редактор является частью большого проекта, и я не думаю, что смогу изменить многое без обширного рефакторинга остальных модулей приложения
Перепробовав множество разных вариантов, я нашел приемлемый обходной путь. Механизм автоматического скрытия ContextMenu, похоже, оказывает некоторое влияние на нижележащие панели, поэтому:
contextMenu.setAutoHide(false);
решает проблему.
Конечно, теперь мне приходится вручную скрывать меню, но в любом случае я буду придерживаться этого решения.
Приведите Минимально воспроизводимый пример. Без кода очень сложно проанализировать вашу проблему.