How to create a working AutoComplete ComboBox?

Viewed 74

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

}
0 Answers
Related