How to make the MenuItems in a JavaFX context menu support an onMouseOver event


Question

I'm looking for some guidance on how to proceed with a problem I'm having. I hava a JavaFX scene and within it some nodes (shapes) that connect to each other with one or more lines. I can right-click on a shape to bring up a context menu. Let's say this particular shape that was just right-clicked has 3 lines coming out of it (call them line1, line2, line3) and you want to use the context menu to delete one. You can select "line2" for example, and it will fire the onAction event to remove that line. That all works fine.

The trouble is, you don't know which of the 3 lines on the screen is line1 or line2 or line3 (unless of course they are labeled) and so you don't know which one you are about to remove until you remove it. What I would really like to do, for example, is to place my mouse over "line2" in the context menu and have line2 in the scene change color or something to indicate that it is the one about to be deleted (before I click the mouse). However, the only event I see supported by MenuItem is the onAction event for when it is clicked. Is there some way to give it onMouseOver functionality? if not, how could this feature be implemented?

Thanks!

1
3
10/9/2013 11:12:45 PM

Accepted Answer

Try this SSCCE:

import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.effect.DropShadow;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.scene.shape.Line;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;

public class ContextMenuDemo extends Application {

    private DropShadow ds = new DropShadow();

    @Override
    public void start(Stage primaryStage) {

        final Line line1 = new Line(60, 10, 150, 10);
        final Line line2 = new Line(60, 30, 150, 50);
        final Line line3 = new Line(60, 60, 150, 90);

        final ContextMenu cm = new ContextMenu();
        cm.getItems().add(getMenuItemForLine("line 1", line1));
        cm.getItems().add(getMenuItemForLine("line 2", line2));
        cm.getItems().add(getMenuItemForLine("line 3", line3));

        final Rectangle rectangle = new Rectangle(70, 70, Color.TAN);
        rectangle.addEventHandler(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() {
            @Override
            public void handle(MouseEvent e) {
                if (e.getButton() == MouseButton.SECONDARY) {
                    cm.show(rectangle, e.getScreenX(), e.getScreenY());
                }
            }
        });

        Group root = new Group();
        root.getChildren().addAll(rectangle, line1, line2, line3);
        Scene scene = new Scene(root, 300, 250);
        // load style of modified paddings for menuitems
        scene.getStylesheets().add(getClass().getResource("style.css").toExternalForm());
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private MenuItem getMenuItemForLine(String menuName, final Line line) {

        Label menuLabel = new Label(menuName);
        // apply style to occupy larger space for label
        menuLabel.setStyle("-fx-padding: 5 10 5 10");
        MenuItem mi = new MenuItem();
        mi.setGraphic(menuLabel);
        line.setStroke(Color.BLUE);

        menuLabel.addEventHandler(MouseEvent.MOUSE_ENTERED, new EventHandler<MouseEvent>() {
            @Override
            public void handle(MouseEvent event) {
                line.setStroke(Color.RED);
                line.setEffect(ds);
            }
        });

        menuLabel.addEventHandler(MouseEvent.MOUSE_EXITED, new EventHandler<MouseEvent>() {
            @Override
            public void handle(MouseEvent event) {
                line.setStroke(Color.BLUE);
                line.setEffect(null);
            }
        });

        return mi;
    }

    public static void main(String[] args) {
        launch(args);
    }
}

with style.css

.menu-item {
/*    -fx-skin: "com.sun.javafx.scene.control.skin.MenuItemSkin";*/
    -fx-background-color: transparent;
    -fx-padding: 0em; /* do not pad for item. we want to ccupy all spaces for graphics only */
}

.menu-item:focused {
     -fx-background: -fx-accent;
     -fx-background-color: -fx-selection-bar;
     -fx-text-fill: -fx-selection-bar-text;
}

.menu-item .graphic-container {
    -fx-padding: 0em; /* do not pad for graphics, label graphic pads itself */
}

.menu-item .label {
    -fx-padding: 0em;  /* do not pad for label, since there is no label text set */
    -fx-text-fill: -fx-text-base-color;
}

Screenshot:

Screenshot

Description:
This is somewhat a bug that MenuItem does not work for MenuItem.addEventHandler(MouseEvent.MOUSE_ENTERED, ...) I think. As a workaround, we define new Label, register event handlers to it and set it as a graphic of menu item while the text(label) of menuitem intentionally left an empty. But the graphic of menu item does not (by default) occupy all space of menu item, so mouse events are not handled properly at the edges of menu item. To overcome this problem we reset all paddings of menuitem, menuitem's label and graphic through css. You can observe this by commenting out the style loading in the above code.

2
10/10/2013 6:10:10 PM

Here is a sample App I just created on an aproach to identify the lines:

import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Tooltip;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Line;
import javafx.stage.Stage;

public class MainTest extends Application {
    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage stage) throws Exception {
        // TODO Auto-generated method stub
        AnchorPane anchorPane = new AnchorPane();
        Scene scene = new Scene(anchorPane);
        stage.setScene(scene);

        Line linea = new Line(0, 0, 50, 50);
        linea.setFill(Color.BLACK);
        final Tooltip t = new Tooltip("Line 1");
        linea.setOnMouseEntered(new EventHandler<MouseEvent>() {

            @Override
            public void handle(MouseEvent event) {
                Line line = (Line) event.getSource();
                line.setStroke(Color.RED);

                t.show((Line) event.getSource(), event.getScreenX(),
                        event.getScreenY());

            }
        });
        linea.setOnMouseExited(new EventHandler<MouseEvent>() {

            @Override
            public void handle(MouseEvent event) {
                Line line = (Line) event.getSource();
                line.setStroke(Color.BLACK);
                t.hide();
            }
        });
        anchorPane.getChildren().add(linea);
        stage.show();
    }
}

Hope it helps!


Licensed under: CC-BY-SA with attribution
Not affiliated with: Stack Overflow
Icon