2

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.

screenshot 1

To reproduce in the MCVE below:

  1. Type "fbi" in the ComboBox and select "Federal Bureau of Investigations."
  2. Click on Select CIA button
  3. Return focus to the ComboBox by clicking in the field
  4. 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);
    }

}
Zephyr
  • 9,885
  • 4
  • 28
  • 63
  • hmm.. what's wrong with controlsfx autocomplete? – kleopatra Feb 04 '22 at 23:10
  • @kleopatra - I have tried ControlsFX's `TextFields.bindAutoComplete()` but that does not seem to provide a method of specifying my own matching criteria. It also does not display the dropdown (which has custom cells) but it's own text-only suggestion pane. – Zephyr Feb 05 '22 at 00:22
  • We are bound to Java 8 and ControlsFX 8.40.14 (so no `SearchableComboBox` control). – Zephyr Feb 05 '22 at 00:23
  • Testing with ControlsFX 8.40.18's `SearchableComboBox` shows it to be incredibly buggy (ie: value changes several times with each keystroke, clearing selection does **not** actually change the value of the `ComboBox`, etc). We generally avoid ControlsFX altogether. – Zephyr Feb 05 '22 at 00:28
  • then you are in for some hard work I think - it's not trivial and combo is quite buggy when it comes to input (was even more so in fx8, would consider it an utter waste of resources to invest in such an outdated version) - good luck! – kleopatra Feb 05 '22 at 06:25
  • _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_ did you try using a filteredList and update the predicate while typing? BTW: seing a choosing toString converter for the first time (and twice today :) - it prevents entering an uncontained item, is that intentional? And do you really need to know which keys are typed or is it enough to act on the result? If the latter, I would try playing with a TextFormatter. – kleopatra Feb 05 '22 at 11:22
  • https://stackoverflow.com/a/55709876/9278333 – trilogy Mar 14 '22 at 19:25

0 Answers0