Skip to content

Auto-Completion — Suggesting Values While the User Types

Overview and Motivation

Auto-completion turns a plain text field into a field that proposes matching values as the user types. A popup below the component lists the candidates, the matching parts of each candidate are highlighted, and picking one writes it into the field.

The feature is built around two ideas that shape the whole API:

  • A suggestion is a set of matched segments, not just a string. The generator returns where it matched, not only what matched. That is what allows the popup to embolden exactly the characters the user typed, and it is what makes non-obvious matching strategies — camel case, separated numbers, regular expressions — presentable rather than mysterious.
  • Matching runs off the FX thread. Choices may be large, and a matching strategy may be expensive. The controller runs the generator in the background and coalesces input, so the field keeps up with typing instead of stuttering or timing out.

Auto-completion is off by default. It is enabled per component by setting a function; there is no global switch.

Key Concepts

Enabling it: setAutoCompletion

Every FxTextComponent carries the entry point:

void setAutoCompletion(Function<String, List<List<SubString>>> autoCompletion);   // null to clear
Function<String, List<List<SubString>>> getAutoCompletion();                      // null = disabled

Setting a non-null function creates an AutoCompletionController for the component; setting null closes it again, removing the listeners and hiding the popup. The controller is created through FxUtilities.createAutoCompletion(...), so applications that replace FxUtilities can substitute their own controller.

These components support it:

Component Notes
FxTextField, FxTextArea Directly on the control.
FxComboBox, FxDatePicker Attached to the control's editor.
FxCustomTextField, FxMaskTextField tentackle-fx-atlanta.

Two components refuse it and throw FxRuntimeException if a non-null function is set: FxPasswordField (auto-completing a password would disclose it) and FxHTMLEditor (unsupported).

The function type

The generator is a plain Function<String, List<List<SubString>>>:

  • The input is the current text of the component.
  • The result is one entry per suggestion.
  • Each suggestion is a List<SubString> — the segments of that candidate that matched the input.

All SubStrings of a suggestion refer to the same underlying string, so suggestion.getFirst().getString() is the candidate's text; the segments only mark ranges within it. That is the string written into the field when the user picks the suggestion.

SubString (org.tentackle.misc, tentackle-core) describes a substring by its begin and end index and builds the segment lazily, so a generator can compute matches without allocating strings for candidates it ends up rejecting.

An empty result list means "no suggestions" and hides the popup. You can implement the function from scratch — a lambda hitting a database is perfectly legitimate — but the built-in generators cover the usual strategies.

Suggestion generators

AbstractSuggestionGenerator<T> is the base class. It holds the choices (objects of any type T) and converts each to the string compared against the input:

SimpleSuggestionGenerator<Customer> generator = new SimpleSuggestionGenerator<>() {
  @Override
  protected String convert(Customer customer) {
    return customer.getName();     // defaults to toString()
  }
};
generator.setChoices(customerList);
nameField.setAutoCompletion(generator);

setChoices converts the list once, up front, into the select list — the strings actually matched against. Subclasses may override setSelectList to bypass or filter that conversion.

The four built-in strategies:

Generator Matches Good for
SimpleSuggestionGenerator The input as a plain substring, anywhere in the candidate. Ordinary text.
CamelCaseSuggestionGenerator Input segments against the candidate's camel-case segments. Identifiers, CustomerInvoiceReport.
SeparatorSuggestionGenerator Input segments against segments split by a separator string. Structured codes like XXX-YYY-Z-AAA.
RegexSuggestionGenerator Input segments against segments found by a regular expression. Anything with a lexical structure of its own.

SimpleSuggestionGenerator and SeparatorSuggestionGenerator are case-insensitive by default (setCaseSensitive(true) to change that).

Segmented matching

The latter three extend AbstractSegmentedSuggestionGenerator, which implements one shared rule: split both the input and the candidate into segments, then walk the input segments in order, consuming candidate segments as they match. A candidate is suggested only if every input segment found a match — so the input's order is respected, but gaps are allowed.

Subclasses supply just two things: extractSegments(String) (how to split) and match(inputSegment, itemSegment) (when two segments match, and which part matched). Candidate segments are cached per select list, so splitting happens once per candidate rather than once per keystroke.

setMaxSuggestions(int) caps the result — matching stops as soon as the cap is reached. The default 0 means unlimited, which is worth revisiting for large choice lists.

Camel case deserves a note, because it carries two refinements. Its input handling starts case-insensitive and switches to case-sensitive as soon as the user types an uppercase letter: cir matches CustomerInvoiceReport, and once you type CIr you have committed to that casing. And because the natural alphabetical sort is useless here, it sorts by visual quality of the match — earliest match position first, then most segments matched, then the remainder alphabetically.

The popup

AutoCompletionPopup is a PopupControl shown directly below the component, with its list width bound to the component's. The first suggestion is preselected, ENTER or a primary mouse click accepts the selection, ESCAPE hides the popup, and it auto-hides when the component loses focus.

AutoCompletionCellFactory renders each row as a TextFlow, drawing the matched segments in bold and the rest in the theme's default font. Override AutoCompletionPopup.createCellFactory() to render rows differently.

Styling hooks — the framework itself ships no rules for these, they exist for themes and applications:

Hook Kind Applies to
.autocompletion style class the popup
.autocompletion-pane style class the popup's StackPane
.autocompletion-list style class the ListView inside
-tt-autocomplete-visible-row-count CSS property rows shown before scrolling (default 16)
-tt-autocomplete-fixed-cell-size CSS property row height (default 0 = derive from the text component's height)

Threading and input coalescing

The controller never runs the generator on the FX application thread. It runs it in a JavaFX Service, one task at a time: while a task is running, further keystrokes only overwrite the pending input, and the newest input is picked up when the running task finishes. Intermediate inputs are simply skipped. Typing therefore costs at most one queued match, no matter how slow the generator, and there are no timeouts to tune.

Two consequences worth knowing:

  • A generator instance belongs to one component. The built-ins are stateful (choices, the cached segments, and RegexSuggestionGenerator's reused Matcher) and are called from a background thread. Give each component its own instance.
  • A custom generator must be safe to call off the FX thread. It must not touch the scene graph. If it queries a database, that is fine — that is the point of running it in the background — but it should be a PDO/persistence call, not UI work.

If the generator throws, the controller logs a warning and shows nothing; the field stays usable.

The popup stays hidden in the cases where it would only be noise: no suggestions at all, or exactly one suggestion that already equals the field's content.

How It Fits Together

A controller offering completion over a list of customers:

  1. The FXML declares an FxTextField (or an FxComboBox, or the RDC's GuiProvider creates one).
  2. The controller creates a suggestion generator, overriding convert() to map each PDO to the string the user should see and match against.
  3. setChoices(...) loads the candidates — eagerly here, or lazily, or not at all if the function queries on each call instead.
  4. field.setAutoCompletion(generator) installs it. The delegate lazily creates the AutoCompletionController, which hooks the component's text and focus properties.
  5. The user types. Each keystroke schedules a background match (coalescing anything still pending), and the popup appears with the matched segments in bold.
  6. The user hits ENTER. The candidate's full text is written into the field, which fires the component's normal binding and validation machinery — auto-completion sets text, it does not bypass anything downstream.

Package Layout

Package Content Module
org.tentackle.fx.component.auto AutoCompletionController, the suggestion generators, AutoCompletionCellFactory tentackle-fx
org.tentackle.fx.component AutoCompletionPopup tentackle-fx
org.tentackle.fx.component.skin AutoCompletionPopupSkin tentackle-fx
org.tentackle.misc SubString tentackle-core
  • tentackle-fx — the extended JavaFX layer this builds on.
  • Binding — what happens to the value once it lands in the field.
  • Validation — completed values are validated like any other input.
  • tentackle-fx-atlantaFxCustomTextField and FxMaskTextField.
  • Tables and Trees — the other configuration-driven part of the FX layer.