sillyfly sillyfly - 5 months ago 23
Java Question

Pass Context menu shortcuts up from editing control

I have a JavaFX pane (TabPane, if it matters) with a context menu associated with it. The context menu has a few shortcut keys (F2, F3) defined in it, and indeed when they are pressed the correct action is performed.
However, when inside a TextField or ComboBox the shortcut keys are completely ignored.

Why is this happening, and how can I overcome this?
If possible, I would like to avoid setting "onKeyPressed" for every single control.

(Edit: Apparently it is the TabPane that has the ContextMenu. For some reason which I can't fathom normal Panes can't have context menus, but TabPane is a Control. Don't know if it changes anything)

Edit: Provided is a minimal example:

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCombination;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class Example extends Application {

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

@Override
public void start(Stage stage) throws Exception {

VBox vbox = new VBox();
vbox.getChildren().add(new TextField());

TabPane tp = new TabPane();
Tab t = new Tab("foo");
t.setContent(vbox);

tp.getTabs().add(t);

ContextMenu cm = new ContextMenu();
MenuItem mi = new MenuItem("Action");
mi.setAccelerator(KeyCombination.keyCombination("F2"));
mi.setOnAction(ev->{ System.out.println("Action!"); });
cm.getItems().add(mi);

tp.setContextMenu(cm);
Scene sc = new Scene(tp);

stage.setScene(sc);

stage.show();
}
}


Note that pressing
F2
while inside the TextField does nothing, while pressing it while not inside it prints out "Action!"

Answer

I have found this roundabout solution to the problem: Add an EventFilter on the TabPane - this fires even when an editing control has focus:

tabPane.addEventFilter(KeyEvent.KEY_PRESSED, this::keyPressed);

Use this as your keyPressed function:

private void keyPressed(KeyEvent event) {
  for (MenuItem mi : tabPane.getContextMenu().getItems())
     {
        if (mi.getAccelerator()!=null && mi.getAccelerator().match(event))
        {
            mi.getOnAction().handle(null);
            event.consume();
            return;
        }
    }
}

It is important to consume the event, so in case no editing control is selected it won't get fired twice. This obviously won't work if you have nested menus, but in my case it is a flat context menu.

If there is anything horribly wrong with this solution, or a more straightforward way to solve this problem - please let me know!

Edit: It may be desirable to add !mi.isDisable() to the firing condition, to avoid firing events for disabled menu items.