I have been trying to create an auto-complete ComboBox
that behaves properly. The only semi-working example I could find online is the AutoComplete ComboBox in JavaFX answer, and it is the closest I've been able to find to a workable solution.
However, that implementation does not work when you need to programmatically select the item using comboBox.getSelectionModel().select()
.
When using that solution, there are several issues that come up. For instance, the text field gets cleared, the actual selected value doesn't update reliably either.
To reproduce in the MCVE below:
- Type "fbi" in the
ComboBox
and select "Federal Bureau of Investigations." - Click on
Select CIA
button - Return focus to the
ComboBox
by clicking in the field - Change the focus to the
TextField
At this point, you'll notice the ComboBox
text field is cleared, but the output shows that FBI has always been the value of the ComboBox
, and FBI is still the only selectable option in the ComboBox
.
So, because that solution does not work at all, how do we implement an AutoComplete (or auto filtering) ComboBox
? All I need is for the user to be able to type some letters and the ComboBox
provide a list of matching items from which to select, but also allow the value to be set from code.
MCVE to reproduce:
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.StringConverter;
public class AutoComboSample extends Application {
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) {
// Simple interface
VBox root = new VBox(10);
root.setAlignment(Pos.CENTER);
root.setPadding(new Insets(10));
final ComboBox<MyObject> comboBox = new ComboBox<>();
// Create sample data
final MyObject itemSvu = new MyObject(2, "Special Victims Unit", "SVU");
final MyObject itemFbi = new MyObject(3, "Federal Bureau of Investigations", "FBI");
final MyObject itemCia = new MyObject(4, "Central Intelligence Agency", "CIA");
final MyObject itemNsa = new MyObject(1, "National Security Administration", "NSA");
final ObservableList<MyObject> myObjects = FXCollections.observableArrayList();
myObjects.addAll(itemSvu, itemCia, itemFbi, itemNsa);
comboBox.setItems(myObjects);
// Button to programmatically select a value
final Button button = new Button("Select CIA");
button.setOnAction(event -> comboBox.getSelectionModel().select(itemCia));
// Create the String converter
comboBox.setConverter(new StringConverter<MyObject>() {
@Override
public String toString(MyObject object) {
return object != null ? object.getPrimary() : "";
}
@Override
public MyObject fromString(String string) {
return comboBox.getItems().stream()
.filter(object -> object.getPrimary().equals(string)).findFirst().orElse(null);
}
});
FxUtilTest.autoCompleteComboBoxPlus(comboBox, (typedText, itemToCompare) ->
itemToCompare.getPrimary().toLowerCase().contains(typedText.toLowerCase())
|| itemToCompare.getSecondary().toLowerCase().contains(typedText.toLowerCase()));
// Listener on ComboBox value to watch how it changes
comboBox.valueProperty().addListener((observable, oldValue, newValue) -> {
MyObject value = FxUtilTest.getComboBoxValue(comboBox);
System.out.println("New value: " + (value != null ? value.getPrimary() : ""));
});
root.getChildren().addAll(new Label("Select Item:"),
comboBox,
button,
new TextField());
// Show the stage
primaryStage.setScene(new Scene(root));
primaryStage.setTitle("Sample");
primaryStage.show();
}
}
class MyObject {
private final int id;
private final String primary;
private final String secondary;
public MyObject(int id, String primary, String secondary) {
this.id = id;
this.primary = primary;
this.secondary = secondary;
}
public String getPrimary() {
return primary;
}
public String getSecondary() {
return secondary;
}
}
class FxUtilTest {
public static <T> void autoCompleteComboBoxPlus(ComboBox<T> comboBox, AutoCompleteComparator<T> comparatorMethod) {
ObservableList<T> data = comboBox.getItems();
comboBox.setEditable(true);
comboBox.getEditor().focusedProperty().addListener(observable -> {
if (comboBox.getSelectionModel().getSelectedIndex() < 0) {
comboBox.getEditor().setText(null);
}
});
comboBox.valueProperty().addListener((observable, oldValue, newValue) -> {
comboBox.getEditor().setText("");
});
comboBox.addEventHandler(KeyEvent.KEY_PRESSED, t -> comboBox.hide());
comboBox.addEventHandler(KeyEvent.KEY_RELEASED, new EventHandler<KeyEvent>() {
private boolean moveCaretToPos = false;
private int caretPos;
@Override
public void handle(KeyEvent event) {
if (event.getCode() == KeyCode.UP) {
caretPos = -1;
if (comboBox.getEditor().getText() != null) {
moveCaret(comboBox.getEditor().getText().length());
}
return;
} else if (event.getCode() == KeyCode.DOWN) {
if (!comboBox.isShowing()) {
comboBox.show();
}
caretPos = -1;
if (comboBox.getEditor().getText() != null) {
moveCaret(comboBox.getEditor().getText().length());
}
return;
} else if (event.getCode() == KeyCode.BACK_SPACE) {
if (comboBox.getEditor().getText() != null) {
moveCaretToPos = true;
caretPos = comboBox.getEditor().getCaretPosition();
}
} else if (event.getCode() == KeyCode.DELETE) {
if (comboBox.getEditor().getText() != null) {
moveCaretToPos = true;
caretPos = comboBox.getEditor().getCaretPosition();
}
} else if (event.getCode() == KeyCode.ENTER) {
return;
}
if (event.getCode() == KeyCode.RIGHT || event.getCode() == KeyCode.LEFT || event.getCode().equals(KeyCode.SHIFT) || event.getCode().equals(KeyCode.CONTROL)
|| event.isControlDown() || event.getCode() == KeyCode.HOME
|| event.getCode() == KeyCode.END || event.getCode() == KeyCode.TAB) {
return;
}
ObservableList<T> list = FXCollections.observableArrayList();
for (T aData : data) {
if (aData != null && comboBox.getEditor().getText() != null && comparatorMethod.matches(comboBox.getEditor().getText(), aData)) {
list.add(aData);
}
}
String t = "";
if (comboBox.getEditor().getText() != null) {
t = comboBox.getEditor().getText();
}
comboBox.setItems(list);
comboBox.getEditor().setText(t);
if (!moveCaretToPos) {
caretPos = -1;
}
moveCaret(t.length());
if (!list.isEmpty()) {
comboBox.show();
}
}
private void moveCaret(int textLength) {
if (caretPos == -1) {
comboBox.getEditor().positionCaret(textLength);
} else {
comboBox.getEditor().positionCaret(caretPos);
}
moveCaretToPos = false;
}
});
}
public static <T> T getComboBoxValue(ComboBox<T> comboBox) {
if (comboBox.getSelectionModel().getSelectedIndex() < 0) {
return null;
} else {
return comboBox.getItems().get(comboBox.getSelectionModel().getSelectedIndex());
}
}
public interface AutoCompleteComparator<T> {
boolean matches(String typedText, T objectToCompare);
}
}