Проблемы с обнаружением событий после использования контекстного меню в JavaFX (Linux)

Я столкнулся со странным поведением приложения 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();
        }
    }
}

Действия по воспроизведению:

  1. Запустить приложение
  2. Попробуйте использовать рамку выделения, верхний левый угол внутри сцены, затем перетащите нижний правый угол за пределы видимых границ сцены. Вы увидите, что перетаскиваемые события по-прежнему срабатывают.
  3. Нажмите правую кнопку, чтобы вызвать контекстное меню.
  4. Когда меню все еще отображается, попробуйте повторить шаг 2, вы увидите, что рамка выделения не выходит за границы сцены, несмотря на то, что указатель мыши находится за пределами сцены. Также перетаскиваемые события не запускаются за пределами сцены.
  5. Повторите шаг 3. Теперь, если вы щелкнете внутри сцены или нажмете кнопку ESC перед созданием рамки выделения, рамка выделения будет вести себя так, как должна.

Приведите Минимально воспроизводимый пример. Без кода очень сложно проанализировать вашу проблему.

Sai Dandem 27.06.2024 10:08

@SaiDandem Я обновил вопрос с примером кода

vit 27.06.2024 13:12

Я попробовал запустить это, но не смог, поскольку он опирался на стороннюю библиотеку (controlsfx), которой у меня не было. Не могли бы вы обновить свой пример, чтобы продемонстрировать проблему, не требуя сторонней библиотеки?

jewelsea 27.06.2024 23:06

Я тоже попробовал запустить программу (у меня есть ControlsFX). Я потерялся на шаге 4. У меня это работает так, как ожидалось. Или, другими словами, это одно и то же поведение независимо от того, открываю я контекстное меню или нет.

Sai Dandem 28.06.2024 00:13

И чего я действительно не понял, так это того, что вы делаете в NotificationPane. Действительно ли оно для этого предназначено? например, добавление событий, добавление их в качестве основного узла... и т. д.? Думаю, вам тоже стоит переосмыслить планировку. (если я правильно понимаю).

Sai Dandem 28.06.2024 00:37

@jewelsea Я обновил код, чтобы исключить использование ControlsFX, проблема все еще сохраняется. Обратите внимание, что проблема может быть воспроизведена только в Linux.

vit 28.06.2024 06:57

@SaiDandem Обратите внимание, что проблема может быть воспроизведена только в Linux. Что касается верстки, использования графического слоя в качестве основного узла и обработки событий на нем - данный фрагмент кода является примером, в реальной жизни этот графический редактор является частью большого проекта, и я не думаю, что смогу изменить многое без обширного рефакторинга остальных модулей приложения

vit 28.06.2024 06:57
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
1
7
77
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Перепробовав множество разных вариантов, я нашел приемлемый обходной путь. Механизм автоматического скрытия ContextMenu, похоже, оказывает некоторое влияние на нижележащие панели, поэтому:

contextMenu.setAutoHide(false);

решает проблему.

Конечно, теперь мне приходится вручную скрывать меню, но в любом случае я буду придерживаться этого решения.

Другие вопросы по теме