14

I want a ComboBox, that filteres the list items as the user types. It should work as follow:

  • When typing, the textfield should show one possible selection, but the part of the word that the user has not yet typed should be highlighted.
  • When he opens the list, the dropdown menu should only show possible options?
  • Using the arrow keys, the user should select one of the remaining items after having narrow the possible items.
  • Filtering is not that important, jumpong to the first matching selection would be okay as well.

Is there anything like that available?

Rana Depto
  • 721
  • 3
  • 11
  • 31
user1406177
  • 1,328
  • 2
  • 22
  • 36
  • 1
    Does this answer your question? [AutoComplete ComboBox in JavaFX](https://stackoverflow.com/questions/19924852/autocomplete-combobox-in-javafx) – trilogy Mar 16 '21 at 15:12

5 Answers5

13

As far as the filtering of the drop down is concerned. Isn't wrapping the list of possible options in a FilteredList the best solution?

MCVE:

import javafx.application.Application;
import javafx.application.Platform;
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.TextField;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

public class MCVE extends Application {
    public void start(Stage stage) {
        HBox root = new HBox();

        ComboBox<String> cb = new ComboBox<String>();
        cb.setEditable(true);

        // Create a list with some dummy values.
        ObservableList<String> items = FXCollections.observableArrayList("One", "Two", "Three", "Four", "Five", "Six",
                "Seven", "Eight", "Nine", "Ten");

        // Create a FilteredList wrapping the ObservableList.
        FilteredList<String> filteredItems = new FilteredList<String>(items, p -> true);

        // Add a listener to the textProperty of the combobox editor. The
        // listener will simply filter the list every time the input is changed
        // as long as the user hasn't selected an item in the list.
        cb.getEditor().textProperty().addListener((obs, oldValue, newValue) -> {
            final TextField editor = cb.getEditor();
            final String selected = cb.getSelectionModel().getSelectedItem();

            // This needs run on the GUI thread to avoid the error described
            // here: https://bugs.openjdk.java.net/browse/JDK-8081700.
            Platform.runLater(() -> {
                // If the no item in the list is selected or the selected item
                // isn't equal to the current input, we refilter the list.
                if (selected == null || !selected.equals(editor.getText())) {
                    filteredItems.setPredicate(item -> {
                        // We return true for any items that starts with the
                        // same letters as the input. We use toUpperCase to
                        // avoid case sensitivity.
                        if (item.toUpperCase().startsWith(newValue.toUpperCase())) {
                            return true;
                        } else {
                            return false;
                        }
                    });
                }
            });
        });

        cb.setItems(filteredItems);

        root.getChildren().add(cb);

        Scene scene = new Scene(root);
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) {
        launch();
    }
}
Jonatan Stenbacka
  • 1,824
  • 2
  • 23
  • 48
  • Good answer. For future readers, see my implementation which is based on Jonatan's: https://stackoverflow.com/a/47933342/597657 – Eng.Fouad Dec 21 '17 at 22:10
3

Take a look:

import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.ComboBox;
import javafx.scene.control.TextField;

public class FilterComboBox extends ComboBox<String> {
    private ObservableList<String> initialList;
    private ObservableList<String> bufferList = FXCollections.observableArrayList();
    private String previousValue = "";

    public FilterComboBox(ObservableList<String> items) {
        super(items);
        super.setEditable(true);
        this.initialList = items;

        this.configAutoFilterListener();
    }

    private void configAutoFilterListener() {
        final FilterComboBox currentInstance = this;
        this.getEditor().textProperty().addListener(new ChangeListener<String>() {
            @Override
            public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
                previousValue = oldValue;
                final TextField editor = currentInstance.getEditor();
                final String selected = currentInstance.getSelectionModel().getSelectedItem();

                if (selected == null || !selected.equals(editor.getText())) {
                    filterItems(newValue, currentInstance);

                    currentInstance.show();
                    if (currentInstance.getItems().size() == 1) {
                        setUserInputToOnlyOption(currentInstance, editor);
                    }
                }
            }
        });
    }

    private void filterItems(String filter, ComboBox<String> comboBox) {
        if (filter.startsWith(previousValue) && !previousValue.isEmpty()) {
            ObservableList<String> filteredList = this.readFromList(filter, bufferList);
            bufferList.clear();
            bufferList = filteredList;
        } else {
            bufferList = this.readFromList(filter, initialList);
        }
        comboBox.setItems(bufferList);
    }

    private ObservableList<String> readFromList(String filter, ObservableList<String> originalList) {
        ObservableList<String> filteredList = FXCollections.observableArrayList();
        for (String item : originalList) {
            if (item.toLowerCase().startsWith(filter.toLowerCase())) {
                filteredList.add(item);
            }
        }

        return filteredList;
    }

    private void setUserInputToOnlyOption(ComboBox<String> currentInstance, final TextField editor) {
        final String onlyOption = currentInstance.getItems().get(0);
        final String currentText = editor.getText();
        if (onlyOption.length() > currentText.length()) {
            editor.setText(onlyOption);
            Platform.runLater(new Runnable() {
                @Override
                public void run() {
                    editor.selectRange(currentText.length(), onlyOption.length());
                }
            });
        }
    }
}

It is based on the answer that is found in this forum. Hope this helps.

Dale
  • 1,903
  • 1
  • 16
  • 24
1

I searched similar for a while and found this. Take a look:

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) {
        ListView lv = ((ComboBoxListViewSkin) comboBox.getSkin()).getListView();

        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.

JulianG
  • 1,551
  • 1
  • 19
  • 29
0

Here is a simple solution that also fix problems with SPACE key:

public static <T> void makeComboBoxSearchable(ComboBox<T> comboBox, Function<T, String> toString) {
    comboBox.setConverter(new StringConverter<>() {
        @Override
        public String toString(T t) {
            return t == null ? "" : toString.apply(t);
        }

        @Override
        public T fromString(String s) {
            return comboBox.getItems().stream().filter(item -> toString.apply(item).equals(s)).findFirst().orElse(null);
        }
    });
    comboBox.setEditable(true);
    final FilteredList<T> filteredItems = comboBox.getItems().filtered(item -> true);

    SortedList<T> sorted = filteredItems.sorted((o1, o2) -> toString.apply(o1).compareToIgnoreCase(toString.apply(o2)));
    comboBox.getEditor().textProperty().addListener((observableValue, oldValue, newValue) -> {
        final T selected = comboBox.getSelectionModel().getSelectedItem();
        final TextField editor = comboBox.getEditor();

        Platform.runLater(() -> {
            if (selected == null || !toString.apply(selected).equals(editor.getText())) {
                filteredItems.setPredicate(item -> toString.apply(item).toLowerCase().contains(newValue.toLowerCase()));
                comboBox.setItems(sorted);
                // To avoid focus on first item in list when space is pressed
                ((ListView<?>) ((ComboBoxListViewSkin<?>) comboBox.getSkin()).getPopupContent()).getFocusModel().focus(-1);
                comboBox.hide();
                comboBox.show();
            }
        });
    });
}

You can call it with

makeComboBoxSearchable(comboBox, toStringFunction);
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community May 12 '23 at 15:39
0

I understand this question was answered 9 years ago, however I ran into a similar problem of making a combo box that auto suggests the closest match to what a user enters. Having referenced Mateus Viccari's solution for an auto fill combo box at https://stackoverflow.com/a/27384068/22098160 and Perneel's quesiton at combobox jump to typed char, I was able to form a solution that met my needs quite nicely.

When the user enters any sequence of characters that match with the beginning or all of any of those in the list, only those results that match what the user has entered are displayed in the dropdown with the dropdown menu resized accordingly.

public static void autoFilter(List<String> optionSet) {

    methodCalled++;

    if (newString.matches("^[A-Za-z0-9_.,'/ ]+$")) {

        for (String str : optionSet) {

            if (filter != newString.length())
                filter = 0;

            System.out.println(str);

            for (int i = 0; i < newString.length(); i++) {

                if (i >= str.length()) {
                    filter = 0;
                    break;
                }

                Character first = (newString.charAt(i));
                Character second = (str.charAt(i));

                String second1 = second.toString();
                System.out.println("[" + second1 + "]");
                String first1 = first.toString();
                System.out.println("[" + first1 + "]");

                if (first1.equalsIgnoreCase(second1)) {

                    filter++;
                    System.out.println("Value of count: " + filter);

                    if (filter == newString.length() && methodCalled == 1) { 

                        cmbBx.hide();
                        newList.add(str);

                        cmbBx.setValue(newString);
                        filter = 0;

                    }

                }

                else if (!first1.equalsIgnoreCase(second1)) {
                    newList.remove(str);
                }

            }

        }

        cmbBx.setItems(FXCollections.observableArrayList(newList));

        if (caratOffset < 1)
            cmbBx.getEditor().positionCaret(newString.length() + 1);

        cmbBx.show();

    }

}

It should be noted that the matches method's contents contain the space character. This was a problem I ran into as the program did not interpret that the space character was part of the entered string of characters, so this fixes that. Any characters that need to be interpreted can be added inside the square brackets.

A new list is created in the above method which replaces the original observable list passed to the combo box which contains the filtered results.

An issue that seemed to persist was that the carat would move to the incorrect position in the combo box when new text was entered, so this line of code

if (caratOffset < 1)
            cmbBx.getEditor().positionCaret(newString.length() + 1);

allowed for correct repositioning. Additionally to allow for proper repositioning, within the key released event handler, the left and right arrow keys now properly position the carat as the user intends.

The method is called by passing it a list, for example

autoFilter(options);

as shown in the start method below:

public void start(Stage primaryStage) throws Exception {

    options.add("Hank");
    options.add("Hule");
    options.add("Holiday");
    options.add("Holly");
    options.add("Saul");
    options.add("Skyler");
    options.add("Mike");
    options.add("Gus");
    options.add("Tuco");
    options.add("Nacho");
    options.add("Jesse");
    options.add("Walter");
    options.add("Walter Jr.");

    

    cmbBx.setItems(FXCollections.observableList(options));
    cmbBx.setEditable(true);
    

    cmbBx.setOnKeyReleased(new EventHandler<KeyEvent>() {
        @Override
        public void handle(KeyEvent event) {

            newString = newString + event.getText();

            if (newString.length() > 0) {

                cmbBx.addEventFilter(KeyEvent.KEY_RELEASED, event1 -> {

                    if (event1.getCode() == KeyCode.DOWN) {
                        cmbBx.show();
                        event1.consume();
                    }

                    else if (event1.getCode() == KeyCode.UP) {
                        cmbBx.hide();
                        event1.consume();
                    }

                    else if (event1.getCode() == KeyCode.LEFT && caratOffset <= 1) {
                        cmbBx.getEditor().positionCaret(newString.length() - caratOffset);
                        caratOffset++;
                    }

                    else if (event1.getCode() == KeyCode.BACK_SPACE || event1.getCode() == KeyCode.DELETE) {

                        // full deletion when text is highlighted
                        if (cmbBx.getEditor().getCaretPosition() == 0
                                && (event1.getCode() == KeyCode.BACK_SPACE || event1.getCode() == KeyCode.DELETE)) {
                            newString = "";
                            cmbBx.setValue(newString);
                            methodCalled = 0;
                        }

                        if (methodCalled > 0 && newString.length() != 0) {
                            newString = newString.substring(0, newString.length() - 1);
                        }

                        newList.clear();

                        filter = 0;
                        methodCalled = 0;

                    }

                    else if (event1.getCode() == KeyCode.ENTER) {
                        event1.consume();

                    }

                    else if (event1.getCode() == KeyCode.SPACE) {
                        newList.clear();

                        filter = 0;
                        methodCalled = 0;
                    }

                });

                cmbBx.hide();
                autoFilter(options);
                
            }

            else if (newString.length() == 0) {

                cmbBx.hide();
                cmbBx.setItems(FXCollections.observableArrayList(options));
                cmbBx.show();

                newList.clear();
                filter = 0;
                methodCalled = 0;

            }

            cmbBx.setValue(newString);

            if (caratOffset < 1)
                cmbBx.getEditor().positionCaret(newString.length() + 1);

        }

    });
    

}

For anyone wanting to test this, the full code is given below with a sample list:

public class AutoSuggest extends Application {

public static ComboBox cmbBx = new ComboBox();
public static String newString = "";
public static ArrayList<String> options = new ArrayList<>();
public static ArrayList<String> newList = new ArrayList<>();
public static int methodCalled = 0;
public static int filter = 0;
public static int caratOffset = 0;

public static void main(String[] args) {

    launch(args);

}



@Override
public void start(Stage primaryStage) throws Exception {

    options.add("Hank");
    options.add("Hule");
    options.add("Holiday");
    options.add("Holly");
    options.add("Saul");
    options.add("Skyler");
    options.add("Mike");
    options.add("Gus");
    options.add("Tuco");
    options.add("Nacho");
    options.add("Jesse");
    options.add("Walter");
    options.add("Walter Jr.");

    

    cmbBx.setItems(FXCollections.observableList(options));
    cmbBx.setEditable(true);
    

    cmbBx.setOnKeyReleased(new EventHandler<KeyEvent>() {
        @Override
        public void handle(KeyEvent event) {

            newString = newString + event.getText();

            if (newString.length() > 0) {

                cmbBx.addEventFilter(KeyEvent.KEY_RELEASED, event1 -> {

                    if (event1.getCode() == KeyCode.DOWN) {
                        cmbBx.show();
                        event1.consume();
                    }

                    else if (event1.getCode() == KeyCode.UP) {
                        cmbBx.hide();
                        event1.consume();
                    }

                    else if (event1.getCode() == KeyCode.LEFT && caratOffset <= 1) {
                        cmbBx.getEditor().positionCaret(newString.length() - caratOffset);
                        caratOffset++;
                    }

                    else if (event1.getCode() == KeyCode.BACK_SPACE || event1.getCode() == KeyCode.DELETE) {

                        // full deletion when text is highlighted
                        if (cmbBx.getEditor().getCaretPosition() == 0
                                && (event1.getCode() == KeyCode.BACK_SPACE || event1.getCode() == KeyCode.DELETE)) {
                            newString = "";
                            cmbBx.setValue(newString);
                            methodCalled = 0;
                        }

                        if (methodCalled > 0 && newString.length() != 0) {
                            newString = newString.substring(0, newString.length() - 1);
                        }

                        newList.clear();

                        filter = 0;
                        methodCalled = 0;

                    }

                    else if (event1.getCode() == KeyCode.ENTER) {
                        event1.consume();

                    }

                    else if (event1.getCode() == KeyCode.SPACE) {
                        newList.clear();

                        filter = 0;
                        methodCalled = 0;
                    }

                });

                cmbBx.hide();
                autoFilter(options);
                
            }

            else if (newString.length() == 0) {

                cmbBx.hide();
                cmbBx.setItems(FXCollections.observableArrayList(options));
                cmbBx.show();

                newList.clear();
                filter = 0;
                methodCalled = 0;

            }

            cmbBx.setValue(newString);

            if (caratOffset < 1)
                cmbBx.getEditor().positionCaret(newString.length() + 1);

        }

    });
    

    BorderPane root = new BorderPane();
    root.setCenter(cmbBx);
    
    Scene scene = new Scene(root, 250, 250);
    scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
    
    primaryStage.setTitle("Auto Suggest ComboBox");
    primaryStage.setScene(scene);
    primaryStage.setResizable(false);
    primaryStage.show();

}


public static void autoFilter(List<String> optionSet) {

    methodCalled++;

    if (newString.matches("^[A-Za-z0-9_.,'/ ]+$")) {

        for (String str : optionSet) {

            if (filter != newString.length())
                filter = 0;

            System.out.println(str);

            for (int i = 0; i < newString.length(); i++) {

                if (i >= str.length()) {
                    filter = 0;
                    break;
                }

                Character first = (newString.charAt(i));
                Character second = (str.charAt(i));

                String second1 = second.toString();
                System.out.println("[" + second1 + "]");
                String first1 = first.toString();
                System.out.println("[" + first1 + "]");
                
                


                if (first1.equalsIgnoreCase(second1)) {

                    filter++;
                    System.out.println("Value of count: " + filter);

                    if (filter == newString.length() && methodCalled == 1) { 

                        cmbBx.hide();
                        newList.add(str);

                        cmbBx.setValue(newString);
                        filter = 0;

                    }

                }

                else if (!first1.equalsIgnoreCase(second1)) {
                    newList.remove(str);
                }

            }

        }

        cmbBx.setItems(FXCollections.observableArrayList(newList));

        if (caratOffset < 1)
            cmbBx.getEditor().positionCaret(newString.length() + 1);

        cmbBx.show();

    }

}
}

Please feel free to use this solution or any portion of this code and do not hesitate to contact me if there are any questions or concerns.

kmj15_23
  • 1
  • 1