javafxmenujfxpanel

Click on open JavaFX Menu embedded in JFXPanel does not close it


When embedding a Menu in a Swing window through a JFXPanel, I cannot close the menu by clicking on it. Sometimes it blinks, as if it closed and immediately reopened.

package testjavafx;

public class TestMenuJavaFX extends Application {

    @Override
    public void start(Stage primaryStage) {
        MenuBar menuBar = new MenuBar(
                new Menu("Menu 1", null,
                        new MenuItem("Menu item 1-1"),
                        new MenuItem("Menu item 1-2")),
                new Menu("Menu 2", null,
                        new MenuItem("Menu item 2-1"),
                        new MenuItem("Menu item 2-2")),
                new Menu("Menu 3", null,
                        new MenuItem("Menu item 3-1"),
                        new MenuItem("Menu item 3-1")));
        menuBar.setPrefWidth(300);
        Region root = new Pane(menuBar);
        root.setPrefSize(300, 185);

        useJFXPanel(root);
        //usePrimaryStage(primaryStage, root);
    }

    private static void useJFXPanel(Region root) {
        JFXPanel jfxPanel = new JFXPanel();
        jfxPanel.setScene(new Scene(root));
        JFrame jFrame = new JFrame("test menu JavaFX");
        jFrame.setSize((int) root.getWidth(), (int) root.getHeight());
        jFrame.add(jfxPanel);
        jFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        jFrame.setLocationRelativeTo(null);
        jFrame.setVisible(true);
    }

    private static void usePrimaryStage(Stage primaryStage, Parent root) {
        primaryStage.setScene(new Scene(root));
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(TestMenuJavaFX.class, args);
    }
}

By using the method usePrimaryStage I get the expected behavior (click on the menu to open it, click again to close it), but with useJFXPanel the problem appears.

This is an event handling issue, the mouse click is first dispatched to the JFXPanel as a Swing mouse event then the JFXPanel internally dispatches a JavaFX mouse event to its embedded Scene.
It appears that, during the Swing part, the menu loses focus and closes, and when the event reaches the Menu instance it finds it closed and therefore opens it.

I tried to inherit the Menu class to add a mouse click event handler to it, however it does not handle mouse clicks, and using the showing/shown and hiding/hidden events provided did not help it (because the problem happens earlier).
I also tried to subclass MenuBar to add a mouse click event handler, but the handler is only called when clicking on the bar outside of a menu, so no luck here, and to subclass JFXPanel to override processMouseEvent and retrieve the MenuBarButton instance through reflection black magic but i couldn't make it work.

This is a bug, right? And is there a (easy and clean, ideally) workaround to this issue?

I'm using OpenJDK 11.0.10.9 and JavaFX 17.0.0.1.


Solution

  • On my system, this wouldn't run, but hung on startup because the JFrame is created and shown on the wrong thread. Correcting that did display the behavior you describe, which does appear to be a bug.

    I found one workaround, which is to capture a ON_HIDING event and schedule a call to hide the menu further down the event queue, using Platform.runLater(...). The resulting code looks like;

    public class TestMenuJavaFX extends Application {
    
        @Override
        public void start(Stage primaryStage) {
            MenuBar menuBar = new MenuBar(
                    new Menu("Menu 1", null,
                            new MenuItem("Menu item 1-1"),
                            new MenuItem("Menu item 1-2")),
                    new Menu("Menu 2", null,
                            new MenuItem("Menu item 2-1"),
                            new MenuItem("Menu item 2-2")),
                    new Menu("Menu 3", null,
                            new MenuItem("Menu item 3-1"),
                            new MenuItem("Menu item 3-1")));
            menuBar.setPrefWidth(300);
            
            menuBar.getMenus().forEach(menu -> {
                menu.addEventHandler(Menu.ON_HIDING, e -> {
                    Platform.runLater(menu::hide);
                });
            });
            
            Region root = new Pane(menuBar);
            root.setPrefSize(300, 185);
    
            useJFXPanel(root);
            //usePrimaryStage(primaryStage, root);
        }
    
        private static void useJFXPanel(Region root) {
            JFXPanel jfxPanel = new JFXPanel();
            jfxPanel.setScene(new Scene(root));
            SwingUtilities.invokeLater(() -> {
                JFrame jFrame = new JFrame("test menu JavaFX");
                jFrame.setSize((int) root.getWidth(), (int) root.getHeight());
                jFrame.add(jfxPanel);
                jFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                jFrame.setLocationRelativeTo(null);
                jFrame.setVisible(true);
            });
        }
    
        private static void usePrimaryStage(Stage primaryStage, Parent root) {
            primaryStage.setScene(new Scene(root));
            primaryStage.show();
        }
    
        public static void main(String[] args) {
            launch(TestMenuJavaFX.class, args);
        }
    }