Masy
Masy

Reputation: 67

Controlling a JavaFX ContextMenu connected to a TextField with the arrow keys

I am trying to make an autocomplete style TextField that uses a ContextMenu to display the suggestions below the TextField. I would like for the ContextMenu to be displayed when a user presses the down key while focus is on the TextField. This is my current solution:

setOnKeyPressed(event -> {
        System.out.println("pressed " + event.getCode());
        switch (event.getCode()) {
            case DOWN:
                if(getText().length()>0) {
                    if (!suggestionMenu.isShowing()) {
                        suggestionMenu.show(AutoCompleteTextField.this, Side.BOTTOM, 0, 0);
                    }
                    suggestionMenu.getSkin().getNode().lookup(".menu-item").requestFocus();
                }
                break;
        }
    });

Source: ContextMenu and programmatically selecting an item

Using this code, the down arrow always "selects" (colours blue) the first item in the list. The problem is that sometimes (seems random to me), the second arrow key-press will not yield any response in the ContextMenu - the first item will stay selected. After that press, it will always work fine.

I would also prefer that pressing up while having the first element selected would hide the ContextMenu, and that space would not fire the onAction method of the MenuItems, though I don't really understand how the focus/event listening for this menu works. It seems like the keyboard has two focuses at once - up/down, spacebar and enter on the ContextMenu, while everything else goes to the TextField.

Edit: Here is a complete example. When showing the ContextMenu using the arrow down key, sometimes it will cause the problematic behaviour, other times not.

Main.java

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

public class Main extends Application {
    @Override
    public void start(Stage stage) throws Exception {
        BorderPane pane = new BorderPane();
        AutoCompleteTextField actf = new AutoCompleteTextField();
        pane.setTop(actf);
        stage.setScene(new Scene(pane));
        stage.show();
    }
}

AutoCompleteTextField.java

import javafx.geometry.Side;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.CustomMenuItem;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;

public class AutoCompleteTextField extends TextField {
    private ContextMenu suggestionMenu;

    public AutoCompleteTextField(){
        super();
        suggestionMenu = new ContextMenu();
        for(int i = 0; i<5; i++) {
            CustomMenuItem item = new CustomMenuItem(new Label("Item "+i), true);
            item.setOnAction(event -> {
                setText("selected");
                positionCaret(getText().length());
                suggestionMenu.hide();
            });
            suggestionMenu.getItems().add(item);
        }

        textProperty().addListener((observable, oldValue, newValue) -> {
            if(getText().length()>0){
                if (!suggestionMenu.isShowing())
                    suggestionMenu.show(AutoCompleteTextField.this, Side.BOTTOM, 0, 0);
            } else {
                suggestionMenu.hide();
            }
        });

        setOnKeyPressed(event -> {
            System.out.println("pressed " + event.getCode());
            switch (event.getCode()) {
                case DOWN:
                    if(getText().length()>0) {
                        if (!suggestionMenu.isShowing()) {
                            suggestionMenu.show(AutoCompleteTextField.this, Side.BOTTOM, 0, 0);
                        }
                        suggestionMenu.getSkin().getNode().lookup(".menu-item").requestFocus();
                    }
                    break;
            }
        });


    }
}

Upvotes: 2

Views: 761

Answers (2)

Jo&#227;o Schmidt
Jo&#227;o Schmidt

Reputation: 81

I created a solution based on this and the ContextMenuContent.class.

ContextMenuContent, as kleopatra suggested, does most of the key bindings. Inside, there is a method called requestFocusOnIndex() that allowed me to get rid that weird behavior.

So your code could be:

textField.addEventFilter(KeyEvent.KEY_PRESSED, event->{
    if(event.getCode() == KeyCode.DOWN) {
        if(!suggestionMenu.isShowing())
            suggestionMenu.show(textField, Side.BOTTOM, 0, 0);
        suggestionMenuKeyBindings = (ContextMenuContent) suggestionMenu.getSkin().getNode();
        suggestionMenuKeyBindings.requestFocusOnIndex(0);
    }
});

Also, you will need to put

--add-exports javafx.controls/com.sun.javafx.scene.control=ALL-UNNAMED

When compiling AND running, otherwise you may get an IllegalAccessError

Upvotes: 1

Caelis
Caelis

Reputation: 311

I workaround the problem in Kotlin by opening a "fake" ContextMenu in the AutoCompleteTextField init-block and closing it after one millisecond in other coroutine.

You can do that in Java as well with a new thread. Hope it helps!

GlobalScope.launch {
    Platform.runLater {
        suggestionMenu.items.add(MenuItem("fake"))
        suggestionMenu.show(this@AutoCompleteTextField, Side.BOTTOM, 0.0, 0.0)
    }
    delay(1)
    Platform.runLater {
        suggestionMenu.hide()
    }
}

Upvotes: 0

Related Questions