Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JavaFX MenuItem can't be canceled by moving the mouse away

Tags:

java

javafx

enter image description here

If you hold the mouse click on one of the MenuItems, and then drag the mouse away, the button will remain selected. Even if you drag the mouse to New or Save, the Open button will remain selected. If you release the mouse anywhere outside Open, the command will still execute. In order words, the moment you held the mouse down, it is imminent that Open's onAction() will be called.

This is different from the normal behavior in Mac OSX and, I guess, Windows native applications. In them, even if you were holding down the click on a MenuItem, if you move the mouse away, the button will not trigger. But it does happen with JavaFX.

What can I do to fix this? JavaFX 8.

like image 517
Voldemort Avatar asked Mar 26 '15 16:03

Voldemort


2 Answers

DISCLAIMER: This is not a proper solution, it is ugly, not well tested, and has many limitations (see below). Also it will probably break with the next Java release, BUT it solved the problem in my pretty standard plain vanilla case so I decided to share it just in case someone might find it useful. Use it your own risk! :)

I also have a tiny hope that someone with a better knowledge of JavaFX skinning API might improve it, I would appreciate!

The limitations:

  • Only works for ContextMenu, not with the main menu (this was my use case)
  • Only works for one-level menus (couldn't figure out how to extend for submenus)
  • Not sure if it works with CustomMenuItems, or with MenuItems with a Graphic, but it might!
  • It relies on the standard ContextMenuContent as a skin for the ContextMenu. If you have your own skin then it will not work.

Here is the helper class:

public class ContextMenuFixer {

public static void fix(ContextMenu contextMenu) {
    if (contextMenu.getSkin() != null) {
        fix(contextMenu, (ContextMenuContent) contextMenu.getSkin().getNode());
    } else {
        contextMenu.skinProperty().addListener((observable, oldValue, newValue) -> {
            if(newValue != null) {
                fix(contextMenu, (ContextMenuContent) contextMenu.getSkin().getNode());
            }
        });
    }
}

private static void fix(ContextMenu menu, ContextMenuContent content) {

    content.getItemsContainer().getChildren().forEach(node -> {

        EventHandler<? super MouseEvent> releaseEventFilter = event -> {
            if (!((Node) event.getTarget()).isFocused()) {
                event.consume();
                menu.hide();
            }
        };
        node.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
            node.removeEventFilter(MouseEvent.MOUSE_RELEASED, releaseEventFilter);

        });

        node.addEventHandler(MouseEvent.DRAG_DETECTED, event -> {
            node.startFullDrag();
            node.addEventFilter(MouseEvent.MOUSE_RELEASED, releaseEventFilter);
        });

        node.addEventHandler(MouseDragEvent.MOUSE_DRAG_ENTERED, event -> {
            MouseEvent e = event.copyFor(event.getSource(), event.getTarget(), MouseEvent.MOUSE_ENTERED);
            node.fireEvent(e);
        });

        node.addEventHandler(MouseDragEvent.MOUSE_DRAG_RELEASED, event -> {
            Event e = event.copyFor(event.getSource(), event.getTarget(), MouseEvent.MOUSE_RELEASED);
            node.fireEvent(e);
        });

        node.addEventHandler(MouseDragEvent.MOUSE_DRAG_EXITED, event -> {
            node.getParent().requestFocus();
        });

        node.addEventHandler(MouseEvent.MOUSE_RELEASED, event -> {
            menu.hide();
        });

    });
}

}

Usage is very straightforward:

    ContextMenuFixer.fix(myContextMenu);

P.S. A cleaner solution would be to write your own proper skin class (i.e., to replace ContextMenuContent) but I deliberately didn't want to go that way because of the maintenance cost.

like image 156
Denis Avatar answered Oct 17 '22 17:10

Denis


Finally, I find a workaround for JavaFX MenuBar. Great thanks to Danis for his answer on how to fix it for ContextMenu. What I did is hack through the MenuBarSkin until I find the ContextMenu in it, and apply Danis's code. Therefore, My solution only removes the MenuBar limitation but leave all other limitations list by Danis, and it is even more ugly.

Here is it:

public static void fix(MenuBar menubar) throws Exception {
    // Hack through MenuBarSkin until we get the ContextMenus
    Field container = MenuBarSkin.class.getDeclaredField("container");
    container.setAccessible(true);
    Field openMenu = MenuBarSkin.class.getDeclaredField("openMenu");
    openMenu.setAccessible(true);
    Field popup = MenuButtonSkinBase.class.getDeclaredField("popup");
    popup.setAccessible(true);

    MenuBarSkin mBarSkin = new MenuBarSkin(menubar);
    menubar.setSkin(mBarSkin);

    // Modified code from Danis
    HBox hBox = (HBox) container.get(mBarSkin);
    hBox.getChildren().forEach(child -> {
        MenuButton mButton = (MenuButton) child;
        MenuButtonSkin mButtonSkin = new MenuButtonSkin(mButton);
        mButton.setSkin(mButtonSkin);

        ContextMenu contextMenu;
        try {
            contextMenu = (ContextMenu) popup.get(mButtonSkin);
        } catch (IllegalArgumentException | IllegalAccessException e1) {
            e1.printStackTrace();
            return;
        }

        ContextMenuSkin cmSkin = new ContextMenuSkin(contextMenu);
        contextMenu.setSkin(cmSkin);
        ContextMenuContent content = (ContextMenuContent) cmSkin.getNode();

        contextMenu.setOnHiding(event -> {
            try {
                ((Menu) openMenu.get(mBarSkin)).hide();
                openMenu.set(mBarSkin, null);
            } catch (IllegalArgumentException | IllegalAccessException e) {
                e.printStackTrace();
                return;
            }
        });

        content.getItemsContainer().getChildren().forEach(node -> {
            EventHandler<? super MouseEvent> releaseEventFilter = event -> {
                if (!((Node) event.getTarget()).isFocused()) {
                    event.consume();
                    contextMenu.hide();
                    mButton.hide();
                }
            };
            node.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
                node.removeEventFilter(MouseEvent.MOUSE_RELEASED, releaseEventFilter);

            });

            node.addEventHandler(MouseEvent.DRAG_DETECTED, event -> {
                node.startFullDrag();
                node.addEventFilter(MouseEvent.MOUSE_RELEASED, releaseEventFilter);
            });

            node.addEventHandler(MouseDragEvent.MOUSE_DRAG_ENTERED, event -> {
                MouseEvent e = event.copyFor(event.getSource(), event.getTarget(), MouseEvent.MOUSE_ENTERED);
                node.fireEvent(e);
            });

            node.addEventHandler(MouseDragEvent.MOUSE_DRAG_RELEASED, event -> {
                Event e = event.copyFor(event.getSource(), event.getTarget(), MouseEvent.MOUSE_RELEASED);
                node.fireEvent(e);
            });

            node.addEventHandler(MouseDragEvent.MOUSE_DRAG_EXITED, event -> {
                node.getParent().requestFocus();
            });

            node.addEventHandler(MouseEvent.MOUSE_RELEASED, event -> {
                contextMenu.hide();
            });
        });
    });
}

And use it with:

fix(urMenuBar);

Hope this will be helpful!

like image 29
Guo Ferrell Avatar answered Oct 17 '22 18:10

Guo Ferrell