Reputation: 67
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;
}
});
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
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
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