Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

First option in the context menu is highlighted without hovering the mouse from javafx 9

Tags:

java-9

javafx

when we right click for context menu, the first option in the list is being highlighted without hovering the mouse. This happens only for the first time right click after the application is opened. This behavior is observed from javafx-9. Till javafx-8 its working fine.

Tried with the sample code:

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.layout.TilePane;
import javafx.stage.Stage;

public class SampleContextMenu extends Application {
    // labels
    Label l;

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

    // launch the application
    public void start(Stage stage) {
        // set title for the stage
        stage.setTitle("creating contextMenu ");

        // create a label
        Label label1 = new Label("This is a ContextMenu example ");

        // create a menu
        ContextMenu contextMenu = new ContextMenu();

        // create menuitems
        MenuItem menuItem1 = new MenuItem("menu item 1");
        MenuItem menuItem2 = new MenuItem("menu item 2");
        MenuItem menuItem3 = new MenuItem("menu item 3");

        // add menu items to menu
        contextMenu.getItems().add(menuItem1);
        contextMenu.getItems().add(menuItem2);
        contextMenu.getItems().add(menuItem3);

        // create a tilepane
        TilePane tilePane = new TilePane(label1);

        // setContextMenu to label
        label1.setContextMenu(contextMenu);

        // create a scene
        Scene sc = new Scene(tilePane, 200, 200);

        // set the scene
        stage.setScene(sc);

        stage.show();
    }
}
like image 350
Vazid Avatar asked Jul 15 '19 05:07

Vazid


1 Answers

After a bit of digging, turns out that the culprit (so to say) is the default focus traversal on initially showing of a scene - which is to focus the first focusable node, which in the case of a contextMenu is the first item.

First try of a hack-around: request the focus back onto the scene's root when the item is focused. The steps:

  • register a onShown handler on the contextMenu
  • in the handler, grab the scene that contains the contextMenu: at this time, its focusOwner is still null, so we need to register a changeListener on its focusOwner property
  • in the listener, on first change of the focusOwner (the old value is null), request focus on the root and cleanup the listeners

Beware: this is not good enough, turned out to be a cosmetic hack only, there are several glitches as noted in the comments

  • requesting the focus to the scene root disables keyboard navigation
  • the first item is still active: pressing enter activates its action

Next try (now going really dirty, requiring access to hidden implementation details of non-public classes!): replace the last step of the first try by

  • grab the containing ContextMenuContent (internal class in com.sun.xx), it's the grandparent of the focused item
  • request focus on that content to make the highlight disappear
  • update that content to be aware of no-item-focused (reflective access to private field)

In code:

contextMenu.setOnShown(e -> {
    Scene scene = contextMenu.getScene();
    scene.focusOwnerProperty().addListener((src, ov, nv) -> {
        // focusOwner set after first showing
        if (ov == null) {
            // transfer focus to root
            // old hack (see the beware section) on why it doesn't work
            //  scene.getRoot().requestFocus();

            // next try: 
            // grab the containing ContextMenuContainer and force the internal
            // book-keeping into no-item-focused state
            Parent parent = nv.getParent().getParent();
            parent.requestFocus();
            // reflective setting of private field, this is my utility method, use your own ;)
            invokeSetFieldValue(ContextMenuContent.class, parent, "currentFocusedIndex", -1);
            // cleanup
            contextMenu.setOnShown(null);

         }
    });
});

For convenience, here's the utility method for reflective access of internal fields (no rocket sciene, just plain java ;)

public static void invokeSetFieldValue(Class<?> declaringClass, Object target, String name, Object value) {
    try {
        Field field = declaringClass.getDeclaredField(name);
        field.setAccessible(true);
        field.set(target, value);
    } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
        Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e);
    }
}
like image 153
kleopatra Avatar answered Nov 20 '22 10:11

kleopatra