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.
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:
ContextMenu
, not with the main menu (this was my use case)CustomMenuItem
s, or with MenuItem
s with a Graphic
, but it might!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.
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!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With