AutoComplete ComboBox in JavaFX
With ControlsFX library you can do it with two lines of code:
comboBox.setEditable(true);
TextFields.bindAutoCompletion(comboBox.getEditor(), comboBox.getItems());
Based on Jonatan's answer, I was able to build the following solution:
import com.sun.javafx.scene.control.skin.ComboBoxListViewSkin;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Control;
import javafx.scene.control.ListView;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import java.util.ArrayList;
import java.util.List;
public class Main extends Application
{
public static class HideableItem<T>
{
private final ObjectProperty<T> object = new SimpleObjectProperty<>();
private final BooleanProperty hidden = new SimpleBooleanProperty();
private HideableItem(T object)
{
setObject(object);
}
private ObjectProperty<T> objectProperty(){return this.object;}
private T getObject(){return this.objectProperty().get();}
private void setObject(T object){this.objectProperty().set(object);}
private BooleanProperty hiddenProperty(){return this.hidden;}
private boolean isHidden(){return this.hiddenProperty().get();}
private void setHidden(boolean hidden){this.hiddenProperty().set(hidden);}
@Override
public String toString()
{
return getObject() == null ? null : getObject().toString();
}
}
public void start(Stage stage)
{
List<String> countries = new ArrayList<>();
countries.add("Afghanistan");
countries.add("Albania");
countries.add("Algeria");
countries.add("Andorra");
countries.add("Angola");
countries.add("Antigua and Barbuda");
countries.add("Argentina");
countries.add("Armenia");
countries.add("Australia");
countries.add("Austria");
countries.add("Azerbaijan");
countries.add("Bahamas");
countries.add("Bahrain");
countries.add("Bangladesh");
countries.add("Barbados");
countries.add("Belarus");
countries.add("Belgium");
countries.add("Belize");
countries.add("Benin");
countries.add("Bhutan");
countries.add("Bolivia");
countries.add("Bosnia and Herzegovina");
countries.add("Botswana");
countries.add("Brazil");
countries.add("Brunei");
countries.add("Bulgaria");
countries.add("Burkina Faso");
countries.add("Burundi");
countries.add("Cabo Verde");
countries.add("Cambodia");
countries.add("Cameroon");
countries.add("Canada");
countries.add("Central African Republic (CAR)");
countries.add("Chad");
countries.add("Chile");
countries.add("China");
countries.add("Colombia");
countries.add("Comoros");
countries.add("Democratic Republic of the Congo");
countries.add("Republic of the Congo");
countries.add("Costa Rica");
countries.add("Cote d'Ivoire");
countries.add("Croatia");
countries.add("Cuba");
countries.add("Cyprus");
countries.add("Czech Republic");
countries.add("Denmark");
countries.add("Djibouti");
countries.add("Dominica");
countries.add("Dominican Republic");
countries.add("Ecuador");
countries.add("Egypt");
countries.add("El Salvador");
countries.add("Equatorial Guinea");
countries.add("Eritrea");
countries.add("Estonia");
countries.add("Ethiopia");
countries.add("Fiji");
countries.add("Finland");
countries.add("France");
countries.add("Gabon");
countries.add("Gambia");
countries.add("Georgia");
countries.add("Germany");
countries.add("Ghana");
countries.add("Greece");
countries.add("Grenada");
countries.add("Guatemala");
countries.add("Guinea");
countries.add("Guinea-Bissau");
countries.add("Guyana");
countries.add("Haiti");
countries.add("Honduras");
countries.add("Hungary");
countries.add("Iceland");
countries.add("India");
countries.add("Indonesia");
countries.add("Iran");
countries.add("Iraq");
countries.add("Ireland");
countries.add("Israel");
countries.add("Italy");
countries.add("Jamaica");
countries.add("Japan");
countries.add("Jordan");
countries.add("Kazakhstan");
countries.add("Kenya");
countries.add("Kiribati");
countries.add("Kosovo");
countries.add("Kuwait");
countries.add("Kyrgyzstan");
countries.add("Laos");
countries.add("Latvia");
countries.add("Lebanon");
countries.add("Lesotho");
countries.add("Liberia");
countries.add("Libya");
countries.add("Liechtenstein");
countries.add("Lithuania");
countries.add("Luxembourg");
countries.add("Macedonia (FYROM)");
countries.add("Madagascar");
countries.add("Malawi");
countries.add("Malaysia");
countries.add("Maldives");
countries.add("Mali");
countries.add("Malta");
countries.add("Marshall Islands");
countries.add("Mauritania");
countries.add("Mauritius");
countries.add("Mexico");
countries.add("Micronesia");
countries.add("Moldova");
countries.add("Monaco");
countries.add("Mongolia");
countries.add("Montenegro");
countries.add("Morocco");
countries.add("Mozambique");
countries.add("Myanmar (Burma)");
countries.add("Namibia");
countries.add("Nauru");
countries.add("Nepal");
countries.add("Netherlands");
countries.add("New Zealand");
countries.add("Nicaragua");
countries.add("Niger");
countries.add("Nigeria");
countries.add("North Korea");
countries.add("Norway");
countries.add("Oman");
countries.add("Pakistan");
countries.add("Palau");
countries.add("Palestine");
countries.add("Panama");
countries.add("Papua New Guinea");
countries.add("Paraguay");
countries.add("Peru");
countries.add("Philippines");
countries.add("Poland");
countries.add("Portugal");
countries.add("Qatar");
countries.add("Romania");
countries.add("Russia");
countries.add("Rwanda");
countries.add("Saint Kitts and Nevis");
countries.add("Saint Lucia");
countries.add("Saint Vincent and the Grenadines");
countries.add("Samoa");
countries.add("San Marino");
countries.add("Sao Tome and Principe");
countries.add("Saudi Arabia");
countries.add("Senegal");
countries.add("Serbia");
countries.add("Seychelles");
countries.add("Sierra Leone");
countries.add("Singapore");
countries.add("Slovakia");
countries.add("Slovenia");
countries.add("Solomon Islands");
countries.add("Somalia");
countries.add("South Africa");
countries.add("South Korea");
countries.add("South Sudan");
countries.add("Spain");
countries.add("Sri Lanka");
countries.add("Sudan");
countries.add("Suriname");
countries.add("Swaziland");
countries.add("Sweden");
countries.add("Switzerland");
countries.add("Syria");
countries.add("Taiwan");
countries.add("Tajikistan");
countries.add("Tanzania");
countries.add("Thailand");
countries.add("Timor-Leste");
countries.add("Togo");
countries.add("Tonga");
countries.add("Trinidad and Tobago");
countries.add("Tunisia");
countries.add("Turkey");
countries.add("Turkmenistan");
countries.add("Tuvalu");
countries.add("Uganda");
countries.add("Ukraine");
countries.add("United Arab Emirates (UAE)");
countries.add("United Kingdom (UK)");
countries.add("United States of America (USA)");
countries.add("Uruguay");
countries.add("Uzbekistan");
countries.add("Vanuatu");
countries.add("Vatican City (Holy See)");
countries.add("Venezuela");
countries.add("Vietnam");
countries.add("Yemen");
countries.add("Zambia");
countries.add("Zimbabwe");
ComboBox<HideableItem<String>> comboBox = createComboBoxWithAutoCompletionSupport(countries);
comboBox.setMaxWidth(Double.MAX_VALUE);
HBox root = new HBox();
root.getChildren().add(comboBox);
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
comboBox.setMinWidth(comboBox.getWidth());
comboBox.setPrefWidth(comboBox.getWidth());
}
public static void main(String[] args)
{
launch();
}
private static <T> ComboBox<HideableItem<T>> createComboBoxWithAutoCompletionSupport(List<T> items)
{
ObservableList<HideableItem<T>> hideableHideableItems = FXCollections.observableArrayList(hideableItem -> new Observable[]{hideableItem.hiddenProperty()});
items.forEach(item ->
{
HideableItem<T> hideableItem = new HideableItem<>(item);
hideableHideableItems.add(hideableItem);
});
FilteredList<HideableItem<T>> filteredHideableItems = new FilteredList<>(hideableHideableItems, t -> !t.isHidden());
ComboBox<HideableItem<T>> comboBox = new ComboBox<>();
comboBox.setItems(filteredHideableItems);
@SuppressWarnings("unchecked")
HideableItem<T>[] selectedItem = (HideableItem<T>[]) new HideableItem[1];
comboBox.addEventHandler(KeyEvent.KEY_PRESSED, event ->
{
if(!comboBox.isShowing()) return;
comboBox.setEditable(true);
comboBox.getEditor().clear();
});
comboBox.showingProperty().addListener((observable, oldValue, newValue) ->
{
if(newValue)
{
@SuppressWarnings("unchecked")
ListView<HideableItem> lv = ((ComboBoxListViewSkin<HideableItem>) comboBox.getSkin()).getListView();
Platform.runLater(() ->
{
if(selectedItem[0] == null) // first use
{
double cellHeight = ((Control) lv.lookup(".list-cell")).getHeight();
lv.setFixedCellSize(cellHeight);
}
});
lv.scrollTo(comboBox.getValue());
}
else
{
HideableItem<T> value = comboBox.getValue();
if(value != null) selectedItem[0] = value;
comboBox.setEditable(false);
Platform.runLater(() ->
{
comboBox.getSelectionModel().select(selectedItem[0]);
comboBox.setValue(selectedItem[0]);
});
}
});
comboBox.setOnHidden(event -> hideableHideableItems.forEach(item -> item.setHidden(false)));
comboBox.getEditor().textProperty().addListener((obs, oldValue, newValue) ->
{
if(!comboBox.isShowing()) return;
Platform.runLater(() ->
{
if(comboBox.getSelectionModel().getSelectedItem() == null)
{
hideableHideableItems.forEach(item -> item.setHidden(!item.getObject().toString().toLowerCase().contains(newValue.toLowerCase())));
}
else
{
boolean validText = false;
for(HideableItem hideableItem : hideableHideableItems)
{
if(hideableItem.getObject().toString().equals(newValue))
{
validText = true;
break;
}
}
if(!validText) comboBox.getSelectionModel().select(null);
}
});
});
return comboBox;
}
}
UPDATE:
In Java 9+, you can access the ListView
like this:
ListView<ComboBoxItem> lv = (ListView<ComboBoxItem>) ((ComboBoxListViewSkin<?>) comboBox.getSkin()).getPopupContent();
I found a solution that's working for me:
public class AutoCompleteComboBoxListener<T> implements EventHandler<KeyEvent> {
private ComboBox comboBox;
private StringBuilder sb;
private ObservableList<T> data;
private boolean moveCaretToPos = false;
private int caretPos;
public AutoCompleteComboBoxListener(final ComboBox comboBox) {
this.comboBox = comboBox;
sb = new StringBuilder();
data = comboBox.getItems();
this.comboBox.setEditable(true);
this.comboBox.setOnKeyPressed(new EventHandler<KeyEvent>() {
@Override
public void handle(KeyEvent t) {
comboBox.hide();
}
});
this.comboBox.setOnKeyReleased(AutoCompleteComboBoxListener.this);
}
@Override
public void handle(KeyEvent event) {
if(event.getCode() == KeyCode.UP) {
caretPos = -1;
moveCaret(comboBox.getEditor().getText().length());
return;
} else if(event.getCode() == KeyCode.DOWN) {
if(!comboBox.isShowing()) {
comboBox.show();
}
caretPos = -1;
moveCaret(comboBox.getEditor().getText().length());
return;
} else if(event.getCode() == KeyCode.BACK_SPACE) {
moveCaretToPos = true;
caretPos = comboBox.getEditor().getCaretPosition();
} else if(event.getCode() == KeyCode.DELETE) {
moveCaretToPos = true;
caretPos = comboBox.getEditor().getCaretPosition();
}
if (event.getCode() == KeyCode.RIGHT || event.getCode() == KeyCode.LEFT
|| event.isControlDown() || event.getCode() == KeyCode.HOME
|| event.getCode() == KeyCode.END || event.getCode() == KeyCode.TAB) {
return;
}
ObservableList list = FXCollections.observableArrayList();
for (int i=0; i<data.size(); i++) {
if(data.get(i).toString().toLowerCase().startsWith(
AutoCompleteComboBoxListener.this.comboBox
.getEditor().getText().toLowerCase())) {
list.add(data.get(i));
}
}
String 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;
}
}
You can call it with
new AutoCompleteComboBoxListener<>(comboBox);
It's based on this and I customized it to fit my needs.
Feel free to use it and if anybody can improve it, tell me.
First, you'll have to create this class in your project:
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.scene.control.ComboBox;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
public class FxUtilTest {
public interface AutoCompleteComparator<T> {
boolean matches(String typedText, T objectToCompare);
}
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.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());
}
}
}
To make your ComboBox
autocomplete, use it like this:
FxUtilTest.autoCompleteComboBoxPlus(myComboBox, (typedText, itemToCompare) -> itemToCompare.getName().toLowerCase().contains(typedText.toLowerCase()) || itemToCompare.getAge().toString().equals(typedText));
Then, add a StringConverter
like the following example (because the ComboBox
value will return a String
and it has to be converted into your object):
myComboBox.setConverter(new StringConverter<>() {
@Override
public String toString(YourObject object) {
return object != null ? object.getName() : "";
}
@Override
public YourObject fromString(String string) {
return myComboBox.getItems().stream().filter(object ->
object.getName().equals(string)).findFirst().orElse(null);
}
});
Also be sure to use this method when you need to get the selected value from the combobox, otherwise you may face some exceptions like "class cast exception":
FxUtilTest.getComboBoxValue(myComboBox);
P.S.: There was some problems with this method in versions between JRE 8.51 and 8.65 which caused some weird behaviors, now the problems seem not to happen anymore. If you face some issue, you can see the edits made on this answer and get the older version which fixed the problem at the time. This method must work fine, if you face any problem, please, let me know.