Issue
I am trying to add an action on the click event on a menu in the menubar using javafx. Thing is, I saw a lot of posts about it but no answers worked for me. I manage to do it using the "On Showing" on the menu ,which is fine, but this event is only trigger (as the others) if the menu has at least one menu item. This is not something I want but I have no choice for now.
Here is the fxml :
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Menu?>
<?import javafx.scene.control.MenuBar?>
<?import javafx.scene.control.MenuItem?>
<?import javafx.scene.layout.BorderPane?>
<BorderPane maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" prefHeight="200.0" prefWidth="500.0" xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.HangmanGameFXViews.view.MenuesActionsControlleur">
<top>
<MenuBar fx:id="menusBar" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" onMouseReleased="#switchToAbout" prefWidth="400.0" BorderPane.alignment="CENTER">
<menus>
<Menu mnemonicParsing="false" text="Fichiers">
<items>
<MenuItem mnemonicParsing="false" text="Nouveau" />
<MenuItem mnemonicParsing="false" onAction="#switchToScore" text="Scores" />
<MenuItem mnemonicParsing="false" onAction="#switchToRules" text="Règles" />
<MenuItem mnemonicParsing="false" onAction="#exit" text="Quitter" />
</items>
</Menu>
<Menu fx:id="about" mnemonicParsing="false" onShowing="#switchToAbout" text="À propos">
<items>
<!-- THIS THE DUMMY MENU I USE TO BE ABLE TO TRIGGER THE EVENT ON THE PARENT -->
<MenuItem fx:id="dummyMenuItem" mnemonicParsing="false" />
</items></Menu>
</menus>
</MenuBar>
</top>
</BorderPane>
The code of the view controller :
package org.HangmanGameFXViews.view;
import org.HangmanGameFXViews.Main;
import javafx.event.ActionEvent;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuBar;
import javafx.scene.control.MenuItem;
import javafx.stage.Stage;
public class MenuesActionsControlleur {
private Stage stageDialogue;
private Main main;
@FXML
private Menu about;
@FXML
private MenuBar menusBar;
@FXML
private MenuItem dummyMenuItem;
@FXML
private void initialize() {
about.addEventHandler(Event.ANY, new EventHandler<Event>() {
@Override
public void handle(Event event) {
System.out.println("Showing Menu 1");
System.out.println(event.getTarget().toString());
System.out.println(event.getEventType().toString());
}
});
}
@FXML
public void switchToRules() {
main.switchToRules();
}
@FXML
public void switchToAbout() {
dummyMenuItem.setDisable(true);
main.switchToAbout();
dummyMenuItem.setDisable(false);
}
@FXML
public void clickableMenu(ActionEvent e){
System.out.println("Menu clicked");
}
@FXML
public void switchToNew() {
main.switchToNew();
}
@FXML
public void switchToScore() {
main.switchToScore();
}
public Stage getStageDialogue() {
return stageDialogue;
}
public void setStageDialogue(Stage stageDialogue) {
this.stageDialogue = stageDialogue;
}
@FXML
public void exit() {
stageDialogue.close();
}
public void setMainClass(Main m) {
main = m;
stageDialogue = main.getStagePrincipal();
}
}
Thank you for any help.
Solution
As already noted in the comments: a Menu is not meant to act like a Button (or MenuItem) - its "action" is to open a ContextMenu showing its items. Implementing it to fire in that context is possible (beware: it might confuse users and it requires a bit of dirt in using implementation details and internal api).
That said: the basic idea for installing custom handlers is to do so on the styleableNode that represents the Menu. By default, access to that node is implemented only if the Menu is an item in a ContextMenu and not when represented by a MenuButton in a MenuBar (which I would consider a bug, but that's another story).
So we have to do it ourselves
- extend Menu
- add api to set a containing MenuBar
- implement getStyleableNode to access the MenuButton
- add some wiring to redirect a mouseReleased (or whatever) into firing the Menu's action
A custom menu might be someting like (obviously not production quality :):
public class MyMenu extends Menu {
private MenuBar parentMenuBar;
private Parent menuBarContainer;
private MenuButton menuButton;
private EventHandler<MouseEvent> redirector = this::redirect;
public MyMenu(String string) {
super(string);
}
public void setParentMenuBar(MenuBar menuBar) {
this.parentMenuBar = menuBar;
// tbd: cleanup if menuBar and/or its skin disposed/changed
if (menuBar != null) {
menuBar.skinProperty().addListener((src, ov, nv) -> {
if (nv instanceof MenuBarSkin && menuBar.getChildrenUnmodifiable().size() == 1) {
menuBarContainer = (Parent) menuBar.getChildrenUnmodifiable().get(0);
menuBarContainer.getChildrenUnmodifiable().addListener((ListChangeListener)c -> {
updateEventRedirector();
});
updateEventRedirector();
}
});
}
}
protected void redirect(MouseEvent e) {
// fire only if there are no items
if (getItems().size() == 0) fire();
}
/**
* Rewire eventHandler when our styleable node is changed
*/
private void updateEventRedirector() {
if (menuButton != null) {
menuButton.removeEventHandler(MouseEvent.MOUSE_RELEASED, redirector);
}
menuButton = getParentMenuButton();
if (menuButton != null) {
menuButton.addEventHandler(MouseEvent.MOUSE_RELEASED, redirector);
}
}
@Override
public Node getStyleableNode() {
if (parentMenuBar != null && parentMenuBar.getChildrenUnmodifiable().size() == 1) {
return menuButton;
}
return super.getStyleableNode();
}
private MenuButton getParentMenuButton() {
if (parentMenuBar == null || parentMenuBar.getChildrenUnmodifiable().size() != 1) return null;
// beware: implementation detail of menuBarSkin!
Parent parent = (Parent) parentMenuBar.getChildrenUnmodifiable().get(0);
for (Node child : parent.getChildrenUnmodifiable()) {
// beware: internal api!
if (child instanceof MenuBarButton) {
MenuBarButton menuButton = (MenuBarButton) child;
if (menuButton.menu == this) {
return menuButton;
}
}
}
return null;
}
}
To use:
bar = new MenuBar();
first = new MyMenu("dummy");
first.setParentMenuBar(bar);
first.setOnAction(e -> System.out.println("menu handler"));
bar.getMenus().addAll(first, new Menu("other"));
Answered By - kleopatra
Answer Checked By - Candace Johnson (JavaFixing Volunteer)