58

I'm looking for a way to add autocomplete to a JavaFX ComboBox.

This AutoFillBox is known but not what I'm searching. What I want is a editable ComboBox, and while typing the list should filtered. But I want also to open the list without typing and seeing the whole items.

Any idea?

Dada
  • 6,313
  • 7
  • 24
  • 43
JulianG
  • 1,551
  • 1
  • 19
  • 29

11 Answers11

73

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.

Chavjoh
  • 466
  • 5
  • 13
Mateus Viccari
  • 7,389
  • 14
  • 65
  • 101
  • Great work here. I realized through using this that sometimes the number of items displayed in the combobox popup was inconsistent (too much room, too little room, etc.). I found the following bug occurs on Windows, and it has just been fixed in 8u60: https://javafx-jira.kenai.com/browse/RT-37622 . So if you are seeing this behavior, upgrade to 8u60 early access to fix (or wait for 8u60 official release). – brcolow May 24 '15 at 23:11
  • Great! I had a bad time trying to fix this problem here, thinking it was my fault! Now i won't have to blame MS Windows anymore when my clients call complaining. – Mateus Viccari May 25 '15 at 03:16
  • I've used the first one and its working perfectly except the warning below. _Discouraged access: The type 'ComboBoxListViewSkin' is not API_. One thing, When I enter a letter it gives me suggestion and after clicking on the arrow it shows all the results. I want that after entering a letter if I click on the arrow It will show only the Results started with that entered letter? To do that where should I modify the code. Thanks FYI: I am new to JAVA. – Shihab Jan 22 '16 at 23:00
  • The first code don't open the list of possible results. It only select the closest result. If I type 'A' I don't see all items starting with 'A' but is selected the first item and is not opened the combobox menu. – drenda Jun 23 '16 at 16:53
  • Yes, that was the catch using the first approach. If you can make it work please post an update here so i can complete the answer. – Mateus Viccari Jun 23 '16 at 20:27
  • @drenda, check the answer again, now everything seems to be working just fine in true auto-complete mode regardless java version. – Mateus Viccari Jun 28 '16 at 12:20
  • 2
    I'm surprised this isn't included in libraries like [ControlsFX](http://fxexperience.com/controlsfx/) – Stevoisiak Apr 09 '17 at 05:34
  • How can this handle copy paste of values? – krisbie Oct 02 '17 at 00:42
  • 3
    I think I made a little improvement here. What I did is modify this part of the code `} else if (event.getCode() == KeyCode.ENTER) { int selectedIndex = comboBox.getSelectionModel().getSelectedIndex(); if(selectedIndex > 0) comboBox.getSelectionModel().select(selectedIndex); else comboBox.getSelectionModel().selectFirst(); return; }` What it does is that if you click enter and there is nothing selected, it will select the first one. – JFValdes Nov 28 '17 at 10:40
  • 1
    There are many aspects of this implementation that are simply broken. Also, what is `getComboBoxValue()` for? It isn't called anywhere in this code. – Zephyr Feb 04 '22 at 13:57
  • @Zephyr Read the last paragraphs of the answer, there is an explanation for the `getComboBoxValue` method. – Mateus Viccari Feb 04 '22 at 14:56
  • 1
    Ah, I missed that somehow. Ignore that comment, then. Still can't get this solution to stop clearing the selection when the `ComboBox` loses focus, though. Doesn't seem to like the selection being changed from code outside of this. – Zephyr Feb 04 '22 at 15:01
  • @Zephr I looked at almost all the solutions provided here and this is the best one. https://stackoverflow.com/a/55709876/9278333 – trilogy May 03 '22 at 18:12
40

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.

Daniel Ziltener
  • 647
  • 1
  • 6
  • 21
JulianG
  • 1,551
  • 1
  • 19
  • 29
  • It's great to help! Can you post the line you removed, please? Maybe ist helps sombody too. – JulianG Feb 12 '14 at 08:00
  • 1
    @JulianG your answer has been [edited](http://stackoverflow.com/posts/20282301/revisions) - the part removed is `ListView lv = ((ComboBoxListViewSkin) comboBox.getSkin()).getListView();`. – assylias Feb 13 '14 at 23:42
  • Oh, I didn't see it. Thank you – JulianG Feb 14 '14 at 07:45
  • 2
    Why when i execute `campoOperacaoEstoque.getValue()` for a ComboBox it throws an exception `java.lang.ClassCastException: java.lang.String cannot be cast to model.Car` – Mateus Viccari Sep 29 '14 at 13:49
  • 1
    JulianG, i improved it a little bit, also to fit my needs, see my answer to the question. – Mateus Viccari Dec 09 '14 at 16:38
  • @mateus viccari set string convertor to combo box, it will fix your issue, this.comboBox.serConvertor(... ) – Dharmendrasinh Chudasama Dec 01 '21 at 09:30
  • I think this should be separated in two class one component an the other a listener. you have two a component and a listener in the same class it is not a solid principle solution. – Lucke Sep 25 '22 at 10:23
  • the property sb is not needed. – Lucke Sep 25 '22 at 10:32
20

With ControlsFX library you can do it with two lines of code:

comboBox.setEditable(true);
TextFields.bindAutoCompletion(comboBox.getEditor(), comboBox.getItems());
Korvin Gump
  • 383
  • 4
  • 7
  • This is a good answer, however if you have bind a model to your combobox instead of a simple `String` you will see object references instead of the String.
    To solve this you need to pass a [StringConverter](https://docs.oracle.com/javase/8/javafx/api/javafx/util/StringConverter.html)
    [example](http://paste.ofcode.org/uYePanGiV9xLGhVUEctxzP)
    – Ahmed Hasn. Jan 11 '17 at 11:40
  • 4
    This solution triggers the auto completion popup if the user uses the mouse to click the combobox arrow and selects a value. – Coren Apr 18 '17 at 21:36
  • @ConquerorsHaki ,so it is possible to add nodes to AutoComplete , I mean update items by adding graphic. – Menai Ala Eddine - Aladdin May 06 '18 at 01:09
  • @ConquerorsHaki ,so it is possible to add nodes to AutoComplete , I mean update items by adding graphic. – Menai Ala Eddine - Aladdin May 06 '18 at 01:14
  • @MenaiAlaEddine should be possible yes – Ahmed Hasn. May 07 '18 at 15:13
  • Update: ControlsFX now has `SearchableComboBox` built in: https://github.com/controlsfx/controlsfx/wiki/ControlsFX-Features#searchablecombobox – AvahW Jun 28 '20 at 16:30
  • @AhmedHasn. Can you please provie the example with the StringConverter? Your link does not work anymore. Thank you – Petr B Sep 18 '20 at 09:00
  • @sonrad10 Can you provide an example of the SearchableComboBox? It is not available on the controlsfx website. Thank you. – Petr B Sep 18 '20 at 09:01
  • @PetrB Sure, check this out: https://pastebin.com/1w0zEUiJ I've also included a fix to a (mildly annoying) [bug in the JavaFX library](https://bugs.openjdk.java.net/browse/JDK-8129123). – AvahW Sep 19 '20 at 10:30
  • 2
    ControlsFX is a compilation of bugs that updates twice a year. Self-made solution is way more reliable. – Evan Nov 10 '20 at 14:40
8

Based on Jonatan's answer, I was able to build the following solution:

enter image description here

enter image description here

enter image description here

enter image description here

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();
Eng.Fouad
  • 115,165
  • 71
  • 313
  • 417
  • Why you did not read JSON file or simple file of countries instead of many lines ! – Menai Ala Eddine - Aladdin May 06 '18 at 01:07
  • When you click shift, the editor clears. So use this check as well : `if (!comboBox.isShowing() || event.isShiftDown())` – trilogy Jan 08 '19 at 14:35
  • Also you need to fix the combobox closing when you press `SPACE` https://stackoverflow.com/questions/50013972/how-to-prevent-closing-of-autocompletecombobox-popupmenu-on-space-key-press-in-j – trilogy Jan 08 '19 at 15:57
  • Not really an autocomplete. It only become editable when user starts typing. – Evan Nov 10 '20 at 15:22
  • @Evan ... I think you're looking for an autocomplete text box.. not an autocomplete dropdown. This solution IS an autocomplete dropdown. – trilogy Nov 20 '20 at 14:23
  • @trilogy No, I'm looking exactly for autocomplete dropdown. But the control should somehow signalize to user that it supports typing. – Evan Nov 20 '20 at 17:11
  • @Evan the drop down does support editable text, but allowing the input of characters means it allows the input of something that may not be in the drop down. By definition that would be an autocomplete text field. This solution doubles down on the fact something in the list must be selected. There is a good autocomplete text field solution as well if you’re interested. – trilogy Nov 20 '20 at 18:06
  • @trilogy No, in terms of UX it's not good. How user supposed to know that it's editable or supports typing? It looks exactly like ordinary combobox until you type smth. That's the main problem with this solution. – Evan Nov 21 '20 at 05:56
  • @Evan drop downs always typically allow typing to jump to spots in the list. – trilogy Nov 21 '20 at 10:39
  • @Evan Anyway, don't lose the forest for the trees. I've put this into a real production environment and only got positive feedback. A lot of them realized it existed on their own. In any case, it's not mandatory to use it. I would see your point if it was mandatory. They are free to use it like a normal dropdown. – trilogy Nov 28 '20 at 01:39
7

I found Eng Fouad's answer to be the BEST overall (even for 10,000+ items) however, I had to fix 3 bugs:

  1. When you click SHIFT, the editor disappeared

  2. When you typed SPACE, the ComboBox would close

  3. When you "cleared the selection" and then opened the combobox and closed it without selecting anything, it would re-select the last item.

I also added a passing of a StringConverter, used Apache StringUtils for comparing as well as moved Streams to regular for-loops for performance purposes as per: https://blog.jooq.org/2015/12/08/3-reasons-why-you-shouldnt-replace-your-for-loops-by-stream-foreach/

Also as per Eng Fouad's answer,

In Java 9+, you can access the ListView like this:

ListView<ComboBoxItem> lv = (ListView<ComboBoxItem>) ((ComboBoxListViewSkin<?>) comboBox.getSkin()).getPopupContent();

Example Usage:

List<Locale> locales = Arrays.asList(Locale.getAvailableLocales());
ComboBox<HideableItem<Locale>> dropDown = AutoCompleteComboBox.createComboBoxWithAutoCompletionSupport(locales, new StringConverter<HideableItem<Locale>>()
{
                        
    @Override
    public String toString(HideableItem<Locale> object)
    {
        if(object!=null)
        {                    
            return object.getObject().getDisplayName();
        }else
        {
           return null;
        }
    }

    @Override
    public HideableItem<Locale> fromString(String string)
    {
        Locale foundLocale = locales.stream().filter((Locale i)
                        -> (i.getDisplayName()).equals(string)).findFirst().orElse(null);
        return new HideableItem(foundLocale, this);
    }
    
});

AutoCompleteComboBox.java

import com.sun.javafx.scene.control.skin.ComboBoxListViewSkin;
import java.util.List;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.event.Event;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Control;
import javafx.scene.control.ListView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.util.StringConverter;
import org.apache.commons.lang3.StringUtils;

public class AutoCompleteComboBox
{

    public static class HideableItem<T>
    {

        private final ObjectProperty<T> object = new SimpleObjectProperty<>();
        private final BooleanProperty hidden = new SimpleBooleanProperty();
        private StringConverter converter;

        public HideableItem(T object, StringConverter converter)
        {
            setConverter(converter);
            setObject(object);

        }

        private ObjectProperty<T> objectProperty()
        {
            return this.object;
        }

        public 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);
        }

        private void setConverter(StringConverter converter)
        {
            this.converter = converter;
        }

        @Override
        public String toString()
        {
            return getObject() == null ? null : converter.toString(this);
        }
    }

    public static <T> ComboBox<HideableItem<T>> createComboBoxWithAutoCompletionSupport(List<T> items, StringConverter converter)
    {
        ObservableList<HideableItem<T>> hideableHideableItems = FXCollections.observableArrayList(hideableItem -> new Observable[]
        {
            hideableItem.hiddenProperty()
        });

        for (T item : items)
        {
            HideableItem<T> hideableItem = new HideableItem<>(item, converter);
            hideableHideableItems.add(hideableItem);
        }

        FilteredList<HideableItem<T>> filteredHideableItems = new FilteredList<>(hideableHideableItems, t -> !t.isHidden());

        ComboBox<HideableItem<T>> comboBox = new ComboBox<>();
        comboBox.setItems(filteredHideableItems);
        comboBox.setConverter(converter);

        ComboBoxListViewSkin<HideableItem<T>> comboBoxListViewSkin = new ComboBoxListViewSkin<HideableItem<T>>(comboBox);
        comboBoxListViewSkin.getPopupContent().addEventFilter(KeyEvent.ANY, (KeyEvent event) ->
        {
            if (event.getCode() == KeyCode.SPACE)
            {
                event.consume();
            }
        });
        comboBox.setSkin(comboBoxListViewSkin);

        @SuppressWarnings("unchecked")
        HideableItem<T>[] selectedItem = (HideableItem<T>[]) new HideableItem[1];

        comboBox.addEventHandler(KeyEvent.KEY_PRESSED, event ->
        {
            if (!comboBox.isShowing() || event.isShiftDown() || event.isControlDown())
            {
                return;
            }
            comboBox.setEditable(true);
            comboBox.getEditor().clear();
        });

        comboBox.showingProperty().addListener((ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean 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);
                if (value != null)
                {
                    Platform.runLater(() ->
                    {
                        comboBox.getSelectionModel().select(selectedItem[0]);
                        comboBox.setValue(selectedItem[0]);
                    });
                }

            }
        });

        comboBox.setOnHidden((Event event) ->
        {
            for (HideableItem item : hideableHideableItems)
            {
                item.setHidden(false);
            }
        });
        comboBox.valueProperty().addListener((ObservableValue<? extends HideableItem<T>> obs, HideableItem<T> oldValue, HideableItem<T> newValue) ->
        {
            if (newValue == null)
            {
                for (HideableItem item : hideableHideableItems)
                {
                    item.setHidden(false);
                }
            }
        });

        comboBox.getEditor().textProperty().addListener((ObservableValue<? extends String> obs, String oldValue, String newValue) ->
        {
            if (!comboBox.isShowing())
            {
                return;
            }

            Platform.runLater(() ->
            {
                if (comboBox.getSelectionModel().getSelectedItem() == null)
                {
                    for (HideableItem item : hideableHideableItems)
                    {
                        item.setHidden(!StringUtils.containsIgnoreCase(item.toString(), newValue));
                    }
                } else
                {

                    boolean validText = false;

                    for (HideableItem hideableItem : hideableHideableItems)
                    {
                        if (hideableItem.toString().equals(newValue))
                        {
                            validText = true;
                            break;
                        }
                    }

                    if (!validText)
                    {
                        comboBox.getSelectionModel().select(null);
                    }
                }
            });
        });

        return comboBox;
    }
}
trilogy
  • 1,738
  • 15
  • 31
4

I look around and try something. This look good:

public void handle( KeyEvent event ) {
        if( event.getCode() == KeyCode.BACK_SPACE)
            s = s.substring( 0, s.length() - 1 );
        else s += event.getText();
        for( String item: items ) {
            if( item.startsWith( s ) ) sm.select( item );
        }
    }

A Keyhandle for select the item with matching start charakter.

I hope this help you

KarlOi
  • 81
  • 5
4

A bit late for the party, but I came across this thread when I had exactly this problem and really liked the answers and the approach, so thanks to everyone who contributed.

But, I don't know what you guys are putting in your ComboBox, whenever I left the box without making a specific selection a ClassCastException was thrown. So I am guessing you all mainly use the ComboBox for picking Strings, so I had to come up with a StringConverter since I am using the ComboBox for Objects (FilterCriteria).

So here is the converter, hopefully helpful to someone.

private final StringConverter<FilterCriteria> filterCriteriaStringConverter = new StringConverter<FilterCriteria>()
{
    @Override public String toString(FilterCriteria filterCriteria)
    {
        if (filterCriteria == null)
        {
            return "";
        } else
        {
            return filterCriteria.getName();
        }
    }

    @Override public FilterCriteria fromString(String string)
    {
        Optional<FilterCriteria> optionalFilterCriteria = availableTypesComboBox.getItems().stream()
          .filter(filterCriteria -> filterCriteria.getName().contains(string))
          .findFirst();
        return optionalFilterCriteria.orElseGet(() -> availableTypesComboBox.getItems().get(0));
    }
};
Samarek
  • 444
  • 1
  • 8
  • 22
1

If you use ControlsFx then SearchableComboBox is a simple extension of a ComboBox which shows a search field while the popup is showing. The user can type any text into this search field to filter the popup list.

enter image description here

0

To add to Mateus' code, the following will create prompt text for auto-completion. For example, if you typed "s", an item beginning with "s" from the ObservableArray (which populated the ComboBox) will serve as the prompt text. Obviously, it would not make much sense to use it with "CONTAINING" parameter.

public class ACComboBox1 {
static String some;
static String typedText;
static StringBuilder sb = new StringBuilder();
public enum AutoCompleteMode {
    STARTS_WITH,CONTAINING,;
}


public static<T> void autoCompleteComboBox(ComboBox<T> comboBox, AutoCompleteMode mode) {

    ObservableList<T> data = comboBox.getItems();

    comboBox.addEventHandler(KeyEvent.KEY_PRESSED, new EventHandler<KeyEvent>() {
        public void handle(KeyEvent event){

        comboBox.hide();
        }
            });
    comboBox.addEventHandler(KeyEvent.KEY_RELEASED, new EventHandler<KeyEvent>() {
        private boolean moveCaretToPos = false;
        private int caretPos;
        public void handle(KeyEvent event) {

            String keyPressed = event.getCode().toString().toLowerCase();



            if ("space".equals(keyPressed) ){
                typedText= " ";
            } else if ("shift".equals(keyPressed ) || "command".equals(keyPressed)
                   || "alt".equals(keyPressed) ) {

                return;
            } else  {
                typedText = event.getCode().toString().toLowerCase();
            }


            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();
                typedText=null;
                sb.delete(0, sb.length());
                comboBox.getEditor().setText(null);
                return;

            } else if (event.getCode() == KeyCode.DELETE) {
                moveCaretToPos = true;
                caretPos = comboBox.getEditor().getCaretPosition();
                return;
            } else if (event.getCode().equals(KeyCode.TAB)) {

                some = null;
                typedText = null;
                sb.delete(0, sb.length());
                return;
            } else if (event.getCode() == KeyCode.LEFT
                    || event.isControlDown() || event.getCode() == KeyCode.HOME
                    || event.getCode() == KeyCode.END || event.getCode() == KeyCode.RIGHT) {

                return;
            }


            if (typedText==null){
            typedText = comboBox.getEditor().getText().toLowerCase();
            sb.append(typedText);

            }else{
                System.out.println("sb:"+sb);
                System.out.println("tt:"+typedText);

                sb.append(typedText);


            } 

            ObservableList<T> list = FXCollections.observableArrayList();
            for (T aData : data) {

                  if (mode.equals(AutoCompleteMode.STARTS_WITH) && aData.toString().toLowerCase().startsWith(sb.toString())) {
                    list.add(aData);
                    some = aData.toString();
                } else if (mode.equals(AutoCompleteMode.CONTAINING) && aData.toString().toLowerCase().contains(comboBox.getEditor().getText().toLowerCase())) {
                    list.add(aData);
                }
            }


            comboBox.setItems(list);

            comboBox.getEditor().setText(some);



            comboBox.getEditor().positionCaret(sb.length());
            comboBox.getEditor().selectEnd();
            if (!moveCaretToPos) {
                caretPos = -1;
            }

            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());
    }
}

}

Rounak
  • 613
  • 3
  • 8
  • 22
  • Hi, I tried your code and if i move the focus out of the component and focus it again to search another item, it doesn't clear the typed text... And i can't erase typed keys with backspace... – Mateus Viccari Sep 16 '15 at 17:40
0

here is a simple one

public class AutoShowComboBoxHelper {
    public AutoShowComboBoxHelper(final ComboBox<String> comboBox, final Callback<String, String> textBuilder) {
        final ObservableList<String> items = FXCollections.observableArrayList(comboBox.getItems());

        comboBox.getEditor().textProperty().addListener((ov, o, n) -> {
            if (n.equals(comboBox.getSelectionModel().getSelectedItem())) {
                return;
            }

            comboBox.hide();
            final FilteredList<String> filtered = items.filtered(s -> textBuilder.call(s).toLowerCase().contains(n.toLowerCase()));
            if (filtered.isEmpty()) {
                comboBox.getItems().setAll(items);
            } else {
                comboBox.getItems().setAll(filtered);
                comboBox.show();
            }
        });
    }
}

and a way to use it:

new AutoShowComboBoxHelper(combo, item -> buildTextToCompare(item));

For the sake of simplicity I used String::contains in this code, for better performance use org.apache.commons.lang3.StringUtils::containsIgnoreCase

Fred
  • 79
  • 3
  • I have difficulties to understand your code. What is buildTextToCompare? Where is this method defined / explained? – Thorsten Sep 23 '20 at 15:33
0

I suggest to try solution from small utility library jalvafx

List<String> items = Arrays.asList("Mercury", 
                                   "Venus", 
                                   "Earth", 
                                   "Mars", 
                                   "Jupiter", 
                                   "Saturn", 
                                   "Neptune");

ComboBoxCustomizer.create(comboBox)
                  .autocompleted(items)
                  .customize();

By default, double click to clear value. There are some other usefull features. You can add extra columns or glyphs, single out specific items, change items default toString representation ...

ComboBoxCustomizer.create(comboBox)
                  .autocompleted(items)
                  .overrideToString(o -> "planet: " + o)
                  .multyColumn(o -> Arrays.asList("column 2", "column 3"))
                  .emphasized(o -> o.endsWith("s"))
                  .customize();
Oleksii Valuiskyi
  • 2,691
  • 1
  • 8
  • 22