JavaFX ContextMenu how do I get the clicked Object?
Asked Answered
S

5

6

I am learning javafx.scene.control.ContextMenu, and right now I am facing a problem:

how do I get the clicked Object from EventHandler? both event.source() and event.target() return the MenuItem.

let me explain with an example: what should I write inside the function handle?

    TextField text = new TextField();
    Label label1 = new Label("hello");
    Label label2 = new Label("world");
    Label label3 = new Label("java");

    ContextMenu menu = new ContextMenu();
    MenuItem item = new MenuItem("copy to text field");
    menu.getItems().add(item);
    item.setOnAction(new EventHandler(){
        public void handle(Event event) {
            //I want to copy the text of the Label I clicked to TextField
            event.consume();
        }
    });

    label1.setContextMenu(menu);
    label2.setContextMenu(menu);
    label3.setContextMenu(menu);

EDIT: I was hoping there was some simple solution (one liner), but if there isn't then there are lot's of complex way to do it.

Shallop answered 19/3, 2015 at 15:46 Comment(0)
J
2

You could create your own instance of ContextMenu and add the action parent to it for further reference:

public class Main extends Application {

    TextField text = new TextField();

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

    @Override
    public void start(Stage primaryStage) {


        Label label1 = new Label("hello");
        Label label2 = new Label("world");
        Label label3 = new Label("java");

        label1.setContextMenu(new MyContextMenu(label1));
        label2.setContextMenu(new MyContextMenu(label2));
        label3.setContextMenu(new MyContextMenu(label3));

        HBox root = new HBox();

        root.getChildren().addAll(text, label1, label2, label3);

        Scene scene = new Scene(root, 300, 100);

        primaryStage.setScene(scene);
        primaryStage.show();

    }

    private class MyContextMenu extends ContextMenu {

        public MyContextMenu(Label label) {

            MenuItem item = new MenuItem("copy to text field");
            item.setOnAction(event -> {

                // I want to copy the text of the Label I clicked to TextField
                text.setText(label.getText());

                event.consume();
            });

            getItems().add(item);

        }

    }
}
Juba answered 19/3, 2015 at 16:46 Comment(2)
that's a good idea, but I was hoping such a functionality already existed.Shallop
possible - not my preference: extending is a rather "heavy" measure, if we do then the extension should do some "heavy" work, not just custom configuration. My personal cents :)Magnetite
B
2

I think the easiest way is to save the Node as UserData of context menu.

EventHandler<? super ContextMenuEvent> eventHandle = e->menu.setUseData(e.getSource());
label1.setOnContextMenuRequested(eventHandle );
label2.setOnContextMenuRequested(eventHandle );
label3.setOnContextMenuRequested(eventHandle );

and in action:

EventHandler<ActionEvent> menuItemEvent = e->{
    Node node = (Node) ((MenuItem)e.getSource()).getParentPopup().getUserData();
   ...
};
Brandi answered 16/3, 2019 at 15:45 Comment(0)
M
2

To sum up the basic requirement: get hold of the node that a contextMenu was opened for. According to the api doc of PopupWindow (the grandparent of ContextMenu), that should be easy to achieve

show(Node node, ...)

... The popup is associated with the specified owner node...

Node getOwnerNode()

The node which is the owner of this popup.

So the general approach in the action of a MenuItem is to

  • get hold of the item's parentPopup (that's the contextMenu), might have to work up the ladder if there are nested menus
  • grab its ownerNode
  • access whatever property is needed

The example at the end does just that in copyText and verifies, that it is working as expected ... iff we are not using a control's contextMenuProperty. The reason for the not-working in controls is a method contract violation (probably introduced by a bug fix around auto-hide behavior in textInputControls) of ContextMenu: it always uses the show(Window w, ..) after it has been set as contextMenu to any control (implementation detail: Control.contextMenuProperty sets a flag setShowRelativeToWindow(true) which triggers the mis-behavior)

Now what can we do to get hold of the ownerNode? There are several options, none of which is nice:

  • as done in the other answers, somehow keep track of the ownerNode: by using factory method, by storing in the user properties or any other ad-hoc means
  • extend ContextMenu, override show(Node owner, ... ) and keep the given owner in a custom property
  • extend ContextMenu, override show(Node owner, ...) go dirty and reflectively set super ownerNode to the given
  • go dirty and reflectively reset the offending showRelativeToWindow flag back to false after setting the menu to any control

The first two introduce additional coupling, the latter (besides the dirty reflective access) might re-introduce problems with auto-hide (the "fixed" behavior is dirty in itself .. violating the "keep-open-if-owner-clicked" guarantee)

At the end, an example to play with:

public class ContextMenuOwnerSO extends Application {

    private Parent createContent() {

        TextField text = new TextField();
        // the general approach to grab a property from the Node
        // that the ContextMenu was opened on
        EventHandler<ActionEvent> copyText = e -> {
            MenuItem source = (MenuItem) e.getTarget();
            ContextMenu popup = source.getParentPopup();
            String ownerText = "<not available>";
            if (popup != null) {
                Node ownerNode = popup.getOwnerNode();
                if (ownerNode instanceof Labeled) {
                    ownerText = ((Label) ownerNode).getText();
                } else if (ownerNode instanceof Text) {
                    ownerText = ((Text) ownerNode).getText();
                }
            }
            text.setText(ownerText);
        };

        MenuItem printOwner = new MenuItem("copy to text field");
        printOwner.setOnAction(copyText);

        // verify with manual managing of contextMenu
        Text textNode = new Text("I DON'T HAVE a contextMenu property");
        Label textNode2 = new Label("I'm NOT USING the contextMenu property");
        ContextMenu nodeMenu = new ContextMenu();
        nodeMenu.getItems().addAll(printOwner);
        EventHandler<ContextMenuEvent> openRequest = e -> {
            nodeMenu.show((Node) e.getSource(), Side.BOTTOM, 0, 0);
            e.consume();
        };

        textNode.setOnContextMenuRequested(openRequest);
        textNode2.setOnContextMenuRequested(openRequest);

        Label label1 = new Label("I'm USING the contextMenu property");

        ContextMenu menu = new ContextMenu() {

            // force menu to have an owner node: this being the case, it is not hidden 
            // on mouse events inside its owner
            //@Override
            //public void show(Node anchor, double screenX, double screenY) {
            //    ReadOnlyObjectWrapper<Node> owner = 
            //            (ReadOnlyObjectWrapper<Node>) 
            //            FXUtils.invokeGetFieldValue(PopupWindow.class, this, "ownerNode");
            //    owner.set(anchor);
            //    super.show(anchor, screenX, screenY);
            //}

        };
        MenuItem item = new MenuItem("copy to text field");
        menu.getItems().add(item);
        item.setOnAction(copyText);

        label1.setContextMenu(menu);
        // same effect as forcing the owner node 
        // has to be done after the last setting of contextMenuProperty 
        // setting to true was introduced as fix for
        // https://bugs.openjdk.java.net/browse/JDK-8114638
        //FXUtils.invokeGetMethodValue(ContextMenu.class, menu, "setShowRelativeToWindow", Boolean.TYPE, false);

        VBox content = new VBox(10, textNode, textNode2, text, label1);
        return content;

    }

    @Override
    public void start(Stage stage) throws Exception {
        stage.setScene(new Scene(createContent(), 400, 200));
        stage.setTitle(FXUtils.version());
        stage.show();
    }

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

    @SuppressWarnings("unused")
    private static final Logger LOG = Logger
            .getLogger(ContextMenuOwnerSO.class.getName());

}
Magnetite answered 18/3, 2019 at 12:21 Comment(2)
This is the correct answer. There is no reason to create a contextmenu for each node.Homophony
What is the FXUtils class that you are using? It does not exist in the official openjFX docs.Dockyard
E
1

Just create a different ContextMenu instance for each label:

TextField text = new TextField();
Label label1 = new Label("hello");
Label label2 = new Label("world");
Label label3 = new Label("java");

label1.setContextMenu(createContextMenu(label1, text));       
label2.setContextMenu(createContextMenu(label2, text));            
label3.setContextMenu(createContextMenu(label3, text));

// ...

private ContextMenu createContextMenu(Label label, TextField text) {
    ContextMenu menu = new ContextMenu();
    MenuItem item = new MenuItem("copy to text field");
    menu.getItems().add(item);
    item.setOnAction(new EventHandler(){
        public void handle(Event event) {
            text.setText(label.getText());
        }
    });
    return menu ;
}
Ebon answered 19/3, 2015 at 17:13 Comment(0)
E
0

I'm aware it's been some time since it has been asked but as I was looking to solve my similiar problem with JavaFX context menu I ran into this thread and Oleksandr Potomkin answer gave me an idea on how to solve it.

What I wanted to achieve is a functioning ContextMenu (one for many fields) that would give me access to the control that opened context menu (or have been called by Accelerator) when I click MenuItem.

Also I had a problem with setting the Accelerator - it would work if I'm focused on form but wouldn't work if I'm focused on the desired control. It should be the other way around...

What I did is I created a class that would initialize a ContextMenu (in it's constructor) and share a method to link that context menu to desired controls:

public class FieldContextMenu {
    ContextMenu menu;
    MenuItem menuCopy;

    public FieldContextMenu() {
        menu = new ContextMenu();

        menuCopy = new MenuItem("Copy");
        menuCopy.setAccelerator(KeyCombination.keyCombination("Ctrl+C"));
        menuCopy.setOnAction(event -> System.out.println(((TextField) menu.getUserData()).getText()));

        menu.getItems().addAll(menuCopy);
    }

    public void link(Control ctrl) {
        ctrl.setContextMenu(menu);
        // onKeyPressed so KeyCombination work while focused on this control
        ctrl.setOnKeyPressed(event -> {
            if(event.isControlDown() && event.getCode() == KeyCode.C) {
                menu.setUserData(ctrl);
                menuCopy.fire();
            }
        });
        // setting this control in menus UserData when ContextMenu is activated in this control
        ctrl.setOnContextMenuRequested(e -> menu.setUserData(ctrl));
    }
}

And here's how I use it in FXML Controller:

public class ExampleController {
    @FXML private AnchorPane rootPane;
    @FXML private TextField textField1;
    @FXML private TextField textField2;

    @FXML protected void initialize() {
        // consume roots keyPressed event so the accelerator wouldn't "run" when outside of the control
        rootPane.setOnKeyPressed(event -> {
            if(event.isControlDown()) event.consume();
        });

        FieldContextMenu contextMenu = new FieldContextMenu();
        contextMenu.link(textField1);
        contextMenu.link(textField2);
    }
}

The way I'm doing it the ContextMenu gets initialized just once = less memory usage (if I'm thinking correctly).

Excrement answered 23/10, 2019 at 7:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.