JavaFX TextField Auto-suggestions

Here is my solution based on This.

public class AutocompletionlTextField extends TextFieldWithLengthLimit {
    //Local variables
    //entries to autocomplete
    private final SortedSet<String> entries;      
    //popup GUI
    private ContextMenu entriesPopup;


    public AutocompletionlTextField() {
        super();
        this.entries = new TreeSet<>();
        this.entriesPopup = new ContextMenu();

        setListner();
    }  


    /**
     * wrapper for default constructor with setting of "TextFieldWithLengthLimit" LengthLimit
     * 
     * @param lengthLimit 
     */
    public AutocompletionlTextField(int lengthLimit) {        
        this();
        super.setLengthLimit(lengthLimit);                
    }


    /**
     * "Suggestion" specific listners
     */
    private void setListner() {     
        //Add "suggestions" by changing text
        textProperty().addListener((observable, oldValue, newValue) -> {
            String enteredText = getText();
            //always hide suggestion if nothing has been entered (only "spacebars" are dissalowed in TextFieldWithLengthLimit)
            if (enteredText == null || enteredText.isEmpty()) {
                entriesPopup.hide();
            } else {
                //filter all possible suggestions depends on "Text", case insensitive
                List<String> filteredEntries = entries.stream()
                        .filter(e -> e.toLowerCase().contains(enteredText.toLowerCase()))
                        .collect(Collectors.toList());
                //some suggestions are found
                if (!filteredEntries.isEmpty()) {
                    //build popup - list of "CustomMenuItem"
                    populatePopup(filteredEntries, enteredText);
                    if (!entriesPopup.isShowing()) { //optional
                        entriesPopup.show(AutocompletionlTextField.this, Side.BOTTOM, 0, 0); //position of popup
                    }
                //no suggestions -> hide
                } else {
                    entriesPopup.hide();
                }
            }
        });

        //Hide always by focus-in (optional) and out
        focusedProperty().addListener((observableValue, oldValue, newValue) -> {
            entriesPopup.hide();
        });
    }             


    /**
    * Populate the entry set with the given search results. Display is limited to 10 entries, for performance.
    * 
    * @param searchResult The set of matching strings.
    */
    private void populatePopup(List<String> searchResult, String searchReauest) {
        //List of "suggestions"
        List<CustomMenuItem> menuItems = new LinkedList<>();
        //List size - 10 or founded suggestions count
        int maxEntries = 10;
        int count = Math.min(searchResult.size(), maxEntries);
        //Build list as set of labels
        for (int i = 0; i < count; i++) {
          final String result = searchResult.get(i);
          //label with graphic (text flow) to highlight founded subtext in suggestions
          Label entryLabel = new Label();
          entryLabel.setGraphic(Styles.buildTextFlow(result, searchReauest));  
          entryLabel.setPrefHeight(10);  //don't sure why it's changed with "graphic"
          CustomMenuItem item = new CustomMenuItem(entryLabel, true);
          menuItems.add(item);

          //if any suggestion is select set it into text and close popup
          item.setOnAction(actionEvent -> {
              setText(result);
              positionCaret(result.length());
              entriesPopup.hide();
          });
        }

        //"Refresh" context menu
        entriesPopup.getItems().clear();
        entriesPopup.getItems().addAll(menuItems);
    }


    /**
    * Get the existing set of autocomplete entries.
    * 
    * @return The existing autocomplete entries.
    */
    public SortedSet<String> getEntries() { return entries; }
}

You must extends from “TextField” instead of “TextFieldWithLengthLimit” and delete constructor with “Length limit”.

I use static methods to work with Styles. It’s used here to “highlight” entered text inside suggestion results. Here is the code of methos from this class:

/**
 * Build TextFlow with selected text. Return "case" dependent.
 * 
 * @param text - string with text
 * @param filter - string to select in text
 * @return - TextFlow
 */
public static TextFlow buildTextFlow(String text, String filter) {        
    int filterIndex = text.toLowerCase().indexOf(filter.toLowerCase());
    Text textBefore = new Text(text.substring(0, filterIndex));
    Text textAfter = new Text(text.substring(filterIndex + filter.length()));
    Text textFilter = new Text(text.substring(filterIndex,  filterIndex + filter.length())); //instead of "filter" to keep all "case sensitive"
    textFilter.setFill(Color.ORANGE);
    textFilter.setFont(Font.font("Helvetica", FontWeight.BOLD, 12));  
    return new TextFlow(textBefore, textFilter, textAfter);
}    

You may add this “AutocompletionlTextField” in FXML (dont forget about “imports”) or inside constructor. To set “suggestions” list on use “entries” getter:

AutocompletionlTextField field = new AutocompletionlTextField();
field.getEntries().addAll(YOUR_ARRAY_OF_STRINGS);

It seems like that in my application:
enter image description here

Hope it helps.

Leave a Comment