Class ComboBox<T>

java.lang.Object
limn.scene.Widget<ComboBox<T>>
limn.components.ComboBox<T>

public final class ComboBox<T> extends Widget<ComboBox<T>>
Dropdown selector. The popup is a real native window: undecorated, floating, non-focus-stealing and with a transparent framebuffer, so its rounded corners (and the translucent panel behind them) composite over whatever is on screen, exactly like the rest of the theme. It can overflow the parent window like a native menu.

Keyboard: Space/Enter/Down opens; Up/Down move the highlight; Enter commits; Esc closes. The popup takes no focus when it opens; the combo keeps it, so clicking anywhere else in the parent window closes the popup via focus loss. A click in the list that chooses nothing, on its scroll bar or its padding, makes the list's window the desktop's key window; the keys it then receives are handed to the combo, and closing the list gives the parent window the keyboard back. Headless scenes (tests) keep the same open/highlight/commit state machine without a native window.

In-scene fallback. Where the window reports no WindowPeer.supportsAbsolutePositioning() (Wayland), a list in a window of its own would open somewhere other than under the field, and over a fullscreen window it would minimize an owner fullscreen at a named mode at the first click, while one at the mode the display is in already covers the screen and leaves a list window no edge to reach past, so in both the list is drawn as an overlay inside the owner window instead (see DisplayMode). Same rows, same keys, same type-ahead; the difference is that it is clamped to that window rather than to the display, so a combo near the bottom edge shows fewer rows and scrolls sooner. Chosen by the platform, not by the application, and popupWindow() is null throughout. Chosen when the list opens; a list already open in a window of its own when the owner enters or leaves fullscreen closes instead, as if dismissed, with the selection as it was: see DisplayMode.

Field and dropdown share one ControlSize step: the panel is the root of its own window-bound Scene, so it inherits through Widget.setInheritanceHost(limn.scene.Widget<?>) rather than through the tree. A dropdown at a different density from the field that opened it is simply a bug, so there is no setter on the panel. The in-scene presentation carries the same link on the overlay layer, which is the parentless widget there — the panel's own would be ignored, because a host link on a widget that has a parent loses to the tree.

The LayoutDirection arrives by that same link and for that same reason: a list whose rows read the other way from the field that opened them is a bug rather than a configuration. Reading right to left the label sits against the right edge, the chevron takes the gutter on the left, the list hangs from the field's right edge and the marker column, the row labels and the scrollbar all move to the other side. The chevron itself does not turn over: it points up and down.

  • Constructor Details

    • ComboBox

      public ComboBox(List<? extends T> items)
      A combo over items that read as themselves: a String as it is, an I18nString in the UI language (a theme or unit picker, whose words follow a language change), anything else by its toString(). For items that read some other way, give the text: ComboBox(List, Function).
      Parameters:
      items - the items, in the order the list shows them; at least one
      Throws:
      IllegalArgumentException - for an empty list
    • ComboBox

      public ComboBox(List<? extends T> items, Function<? super T,?> text)
      A combo over items that read as text says: new ComboBox<>(customers, Customer::name). The text may be a String or an I18nString, which follows the UI language; anything else reads by its toString(). Asked once per item, here.
      Parameters:
      items - the items, in the order the list shows them; at least one
      text - how an item reads in the field and the list
      Throws:
      IllegalArgumentException - for an empty list
    • ComboBox

      public ComboBox(Function<? super T,?> text)
      A combo whose items come from a search the application runs as the user types, and start empty: new ComboBox<>(Customer::name).setSearch(text -> Ui.async(() -> find(text))). It is editable from the start, since there is nothing to choose before something is typed.
      Parameters:
      text - how an item reads in the field and the list
  • Method Details

    • localized

      public static ComboBox<I18nString> localized(List<I18nString> items)
      A combo whose items follow the UI language: a theme or unit picker, as opposed to a list of file names. The items are held as values, so a language change re-measures the popup instead of leaving yesterday's words in it.
    • onSelect

      public ComboBox<T> onSelect(@Nullable Runnable handler)
      The application's response to the user changing the selection: a click on a row, Enter on the highlight, an assistive technology's select, a typed text the field accepted. Never for setSelectedIndex(int) or the other setters, which are a caller's writes; to hear every change whatever caused it, watch the combo instead. The handler reads what it needs, selectedItem() or, holding many, selectedItems(), as a ListView's does.
      Parameters:
      handler - the handler, or null to clear the slot
      Returns:
      this combo
      Throws:
      IllegalStateException - if a handler is already registered
    • handleUserChange

      protected void handleUserChange(Change.Aspect aspect)
      Description copied from class: Widget
      Runs this widget's application handler for a change it has just announced. Called by Widget.runHandler(limn.scene.Change), and so by Widget.notifyChange(limn.scene.Change), only for a USER origin and after every watcher has run.

      A component overrides it once and switches on the aspect, reading its own accessors for the payload: a slider passes value(), a combo box selectedIndex(), a text field text(). A component with no handler does not override it.

      An override that does not recognise the aspect must call super, because a subclass of a component that has a handler inherits that component's dispatch through this method: a SearchField that overrode this for its own aspect and did not chain would silently delete TextField's. The ordering lives in the base class, this is the one link in it a subclass can break, and the contract test drives every subclass through its parent's gestures to catch one that does.

      Overrides:
      handleUserChange in class Widget<ComboBox<T>>
      Parameters:
      aspect - what changed; the origin is USER or this is not called
    • selectedIndex

      public int selectedIndex()
      The selected item's index. Never negative for a combo that chooses: it refuses an empty item list and so always has exactly one selection. -1 only for an editable combo with free text (setFreeText(java.util.function.Function<java.lang.String, ? extends T>)), while its value is a text the list does not hold, or none.
    • selectedItem

      public @Nullable T selectedItem()
      The selected item itself: one of the items, or what a free text became, or null when an editable combo with free text was emptied. Holding many, the first of them in the list's order, or null for none.
    • selectedText

      public String selectedText()
      The selected item as it currently reads, in the UI language: empty when nothing is selected. Holding many, their texts in the list's order, joined as the language joins a list.
    • setSelectionMode

      public ComboBox<T> setSelectionMode(SelectionMode mode)
      How many items the combo holds: SelectionMode.SINGLE, the default, or SelectionMode.MULTI, where the list stays open while items are toggled and the field shows them as chips. NONE is refused: a combo box that chooses nothing is a label. Switching to MULTI starts with nothing selected, since the item a combo holding one must hold is not one the user chose, and a filter that opened on it would already be filtering; switching back keeps the first selected, or the first item when none was. Each is announced as SELECTION/ADJUSTMENT when it moves anything. Not with setSearch(java.util.function.Function<java.lang.String, ? extends java.util.concurrent.CompletionStage<? extends java.util.List<? extends T>>>) or setFreeText(java.util.function.Function<java.lang.String, ? extends T>). UI thread only.
      Parameters:
      mode - SINGLE or MULTI
      Returns:
      this combo
      Throws:
      IllegalArgumentException - for NONE
      IllegalStateException - for MULTI on a combo that searches or takes free text
    • selectionMode

      public SelectionMode selectionMode()
      Returns:
      how many items the combo holds; see setSelectionMode(limn.components.SelectionMode)
    • selectedItems

      public List<T> selectedItems()
      Returns:
      the selected items, in the list's order; empty when nothing is selected
    • selectedIndices

      public int[] selectedIndices()
      Returns:
      every selected item's index, ascending; empty when nothing is selected
    • isSelected

      public boolean isSelected(int index)
      Parameters:
      index - an item
      Returns:
      whether it is selected
    • setSelectedIndices

      public ComboBox<T> setSelectedIndices(int... indices)
      Selects exactly these items: a caller's write, announced as SELECTION/CODE when it moved anything, and reaching no handler. Holding one, exactly one index. UI thread only.
      Parameters:
      indices - items in [0, itemCount)
      Returns:
      this combo
      Throws:
      IndexOutOfBoundsException - for an index that is not an item
      IllegalArgumentException - for other than one index on a combo holding one item
    • setSelectedItems

      public ComboBox<T> setSelectedItems(Collection<? extends T> selection)
      Selects exactly these items, compared with equals: a caller's write, as setSelectedIndices(int...) is.
      Parameters:
      selection - some of the items
      Returns:
      this combo
      Throws:
      IllegalArgumentException - for an item that is not one of them, or for other than one item on a combo holding one
    • selectAll

      public ComboBox<T> selectAll()
      Selects every item, holding many; nothing holding one. A caller's write, announced as SELECTION/CODE when it moved anything. UI thread only.
      Returns:
      this combo
    • clearSelection

      public ComboBox<T> clearSelection()
      Drops the selection, holding many, or with free text on; a combo that chooses one item always holds one, and refuses. A caller's write, announced as SELECTION/CODE when it moved anything. UI thread only.
      Returns:
      this combo
      Throws:
      IllegalStateException - on a combo holding one item without free text
    • setEmptySelectionAllowed

      public ComboBox<T> setEmptySelectionAllowed(boolean allowed)
      Lets a combo holding one item hold none: the list gains a first option that stands for "nothing", whose value is null, and choosing it empties the combo. It reads as the item text function answers for null, and shows the icon the icon function answers for it, so the application words it ("None", "Any status") as it words its items; a function that cannot take null leaves it blank. Turned off while nothing is held, the combo takes the first item. Holding many, a selection may always be empty and the list shows no such option. UI thread only.
      Parameters:
      allowed - whether the combo may hold nothing
      Returns:
      this combo
    • isEmptySelectionAllowed

      public boolean isEmptySelectionAllowed()
      Returns:
      whether a combo holding one item may hold none; see setEmptySelectionAllowed(boolean)
    • setFreeText

      public ComboBox<T> setFreeText(@Nullable Function<String,? extends T> fromText)
      Lets an editable combo take a text that names no item, as a "City" that may be new or a tag does: leaving the field with such a text makes it the value, through fromText, and leaving it empty clears the value. null, the default, accepts items only: such a text is refused, put back to the selected item's, and a screen reader is told so. UI thread only.
      Parameters:
      fromText - how a text becomes an item, or null to accept items only
      Returns:
      this combo
    • setSelectedItem

      public ComboBox<T> setSelectedItem(T item)
      Selects item: a caller's write, as setSelectedIndex(int) is.
      Parameters:
      item - one of the items, compared with equals
      Returns:
      this combo
      Throws:
      IllegalArgumentException - if item is not one of them
    • setEditable

      public ComboBox<T> setEditable(boolean value)
      Makes the field text the user types into, or a button again: typing filters the list, the arrows walk it, Enter picks the highlighted item and leaving the field picks the item the text names, or puts the selected item's text back. The field keeps the caret, the selection, the clipboard and the input method while the list is up, as a text field does. UI thread only.
      Parameters:
      value - whether the field is editable
      Returns:
      this combo
    • isEditable

      public boolean isEditable()
      Returns:
      whether the field is text the user types into; see setEditable(boolean)
    • setFilter

      public ComboBox<T> setFilter(@Nullable BiPredicate<? super T,String> rule)
      Replaces the rule an editable combo filters its list by: whether an item matches what is typed. The default finds the text anywhere in an item's text, ignoring case and accents, and lists the items that start with it first; a predicate lists the items it accepts in their order. UI thread only.
      Parameters:
      rule - the rule, or null for the default
      Returns:
      this combo
    • setSearch

      public ComboBox<T> setSearch(@Nullable Function<String,? extends CompletionStage<? extends List<? extends T>>> finder)
      Makes an editable combo take its items from a search the application runs in the background as the user types, instead of filtering a list: text -> Ui.async(() -> find(text)). The search is asked 200L ms after the last keystroke, the list says it is loading meanwhile, an answer for a text the user has typed past is dropped, and the number found is told to a screen reader. While a search supplies the items the value is held by itself and selectedIndex() answers -1. UI thread only.
      Parameters:
      finder - the search, or null to go back to the list in memory
      Returns:
      this combo
    • setItemIcon

      public ComboBox<T> setItemIcon(@Nullable Function<? super T,? extends @Nullable Icon> icon)
      Gives each item an icon, shown before its text in the list, in a chip and in the field of a combo holding one item. Tinted with the text's ink as a Label's is, so a picture that must keep its colours is an icon that is not tintable. Asked once per item, here and when a search answers. UI thread only.
      Parameters:
      icon - an item's icon, answering null for an item without one; or null to show none
      Returns:
      this combo
    • setItemIcon

      public ComboBox<T> setItemIcon(@Nullable Function<? super T,? extends @Nullable Icon> icon, Function<? super T,Icon.Mirroring> mirroring)
      Gives each item an icon, as setItemIcon(Function) does, and says of each whether its icon means a direction: one that answers Icon.Mirroring.IN_RTL, a back arrow or a paper plane, is drawn flipped while the combo reads right to left. Asked with the icon, and handed null for the empty option as the icon function is. UI thread only.
      Parameters:
      icon - an item's icon, answering null for an item without one; or null to show none
      mirroring - whether an item's icon is flipped right to left; Icon.Mirroring.NEVER unless the glyph is directional
      Returns:
      this combo
    • setPlaceholder

      public ComboBox<T> setPlaceholder(String hint)
      A fixed hint the field shows, muted, while it reads nothing: a combo holding many with none selected, an empty option with no text of its own, an editable combo's emptied text. It is never the value: an empty option that has words shows them, and the hint only fills a field that would otherwise be blank. It names the combo for a screen reader when nothing else does, as a text field's placeholder does.
      Parameters:
      hint - the hint; empty for none
      Returns:
      this combo
    • setPlaceholder

      public ComboBox<T> setPlaceholder(I18nString hint)
      setPlaceholder(String), in words that follow the UI language. UI thread only.
      Parameters:
      hint - the hint; I18nString.EMPTY for none
      Returns:
      this combo
    • placeholder

      public String placeholder()
      Returns:
      the hint as it currently reads; see setPlaceholder(String)
    • setChipRenderer

      public ComboBox<T> setChipRenderer(@Nullable Function<? super T,? extends Widget<?>> renderer)
      Replaces how a chip is drawn in the field of a combo holding many: the widget standing for an item, asked once per item when it is first selected. It is drawn and never operated, taking no pointer and no keyboard, and it is not in the accessible tree, since a reader hears the field's value, which names every item. UI thread only.
      Parameters:
      renderer - the widget for an item, or null for the default chip, a pill with the item's icon and text
      Returns:
      this combo
      Throws:
      IllegalArgumentException - later, when a chip it answers can take the keyboard
    • text

      public String text()
      Returns:
      what the field shows: the text typed into an editable combo, else the selected item's
    • setSelectedIndex

      public ComboBox<T> setSelectedIndex(int index)
      Selects an item: a caller's write, so it announces SELECTION/CODE and reaches no handler. Selecting what is already selected changes nothing and announces nothing. UI thread only.
      Parameters:
      index - an item in [0, itemCount)
      Throws:
      IndexOutOfBoundsException - if index is not an item; an index computed from a lookup that missed is a caller's bug here, not a request for the nearest item
    • isOpen

      public boolean isOpen()
      Whether the popup is showing.
    • highlightedIndex

      public int highlightedIndex()
      Returns:
      the highlighted popup row (keyboard navigation state)
    • popupWindow

      public @Nullable NativeWindow popupWindow()
      The window the open list is drawn in, for an application that captures it or a test that drives it: the list is a window of its own, so a capture of the owner window does not contain it. Supported API, not a test hook.
      Returns:
      the popup's native window while open, else null, which is also the answer while an open popup is drawn in the scene, where there is no window to return
    • open

      public void open()
      Opens the popup (native window when bound; pure state when headless). Announces EXPANDED/CODE; the click, the keys and an assistive technology's expand announce it as USER through the same seam. In a scene, nothing happens, and nothing is announced, while the combo box is hidden, drawn as nothing by a transform above it, or on its way out of its window.
    • setDisplayMode

      public ComboBox<T> setDisplayMode(DisplayMode mode)
      Asks for a presentation of the drop-down list; see DisplayMode. Default DisplayMode.NATIVE_WINDOW, so a long list can extend past the window's edge the way a platform combo's does.

      A preference, not a guarantee. A list asked for NATIVE_WINDOW is still drawn in scene on a window that cannot be positioned at an anchor (Wayland), and over a fullscreen window, for the reasons DisplayMode gives. IN_SCENE is always honoured, and it is the choice to make when the window has to contain everything it shows: a screenshot, a recording or a screen share of the owner window captures an in-scene list and never a native one.

      Takes effect on the next open.

    • displayMode

      public DisplayMode displayMode()
      Returns:
      how the list is presented: while it is open, what it actually chose, including a fullscreen owner or a platform forcing IN_SCENE over a request for a window. While it is closed, what was last asked for, since the next open decides afresh over the window as it is then.
    • close

      public void close()
      Dismisses the popup without changing the selection. No-op when closed. Announces EXPANDED/CODE; a dismissal by the user is USER, and one the widget makes for itself -- the window losing focus, the field being detached -- is an adjustment.
    • onMeasure

      protected Size onMeasure(Constraints constraints)
      What a label with no strong character of its own falls back to, given the direction the caller already resolved for its pass: an item that is all digits or punctuation ("--", "1/2", "1.0") reads the way the form around it does.

      The resolved direction is a parameter rather than a second read, so that one pass cannot shape a label against one direction and place it against the other. Every strong character still decides for itself, which is why a Latin item in a right-to-left combo still reads left to right.

      Specified by:
      onMeasure in class Widget<ComboBox<T>>
    • baselineOffset

      protected float baselineOffset()
      Mixed-step rows align on this, not on the box; see Flex.CrossAlignment.BASELINE.
      Overrides:
      baselineOffset in class Widget<ComboBox<T>>
    • onPaint

      protected void onPaint(Canvas canvas)
      Description copied from class: Widget
      Widget's own background/content, in local coordinates.
      Overrides:
      onPaint in class Widget<ComboBox<T>>
    • onMouseEvent

      protected void onMouseEvent(MouseEvent event)
      Description copied from class: Widget
      Mouse events (bubbling); call event.consume() when handled.
      Overrides:
      onMouseEvent in class Widget<ComboBox<T>>
    • onCanEdit

      protected boolean onCanEdit(EditCommand command)
      Description copied from class: Widget
      Whether this widget can do an editing command now: copy with something selected, paste with something to paste, undo with something to take back. Scene.canEdit(limn.scene.EditCommand) asks the widget that has the keyboard and then each of its ancestors, and a menu's item of an editing role is enabled by the answer.

      A widget answers for the commands it takes from the keyboard, and does for the command what it does for the key.

      Overrides:
      onCanEdit in class Widget<ComboBox<T>>
      Returns:
      false here: a widget edits nothing until it says so
    • onEdit

      protected boolean onEdit(EditCommand command)
      Description copied from class: Widget
      Does an editing command, as the user's own act. Called on the first widget, from the one with the keyboard upward, whose Widget.onCanEdit(limn.scene.EditCommand) answered true.
      Overrides:
      onEdit in class Widget<ComboBox<T>>
      Returns:
      whether it was done
    • onKeyEvent

      protected void onKeyEvent(KeyEvent event)
      Description copied from class: Widget
      Key events (focused widget first, then ancestors).
      Overrides:
      onKeyEvent in class Widget<ComboBox<T>>
    • onCharTyped

      protected void onCharTyped(CharEvent event)
      Type-ahead: letters typed into an open list jump to what they spell, the way a combo does on Windows and a pop-up button does on GTK and macOS. Without it the only way through a long clamped list is one arrow press per item.

      Two behaviours a user expects and neither of which falls out of plain prefix matching. The buffer expires after TYPE_AHEAD_RESET_NANOS, so "so" typed slowly is two separate jumps rather than a search for "so": a pause means a new word. And repeating one letter cycles through the items starting with it instead of re-finding the first, which is how a list of six things beginning with S is navigated at all.

      The search wraps and starts after the current highlight, so a match already under the cursor is not what a fresh keystroke finds.

      Overrides:
      onCharTyped in class Widget<ComboBox<T>>
    • onFocusGained

      protected void onFocusGained()
      Description copied from class: Widget
      Called when this widget takes keyboard focus. Default: nothing.
      Overrides:
      onFocusGained in class Widget<ComboBox<T>>
    • accessibleLabelTarget

      protected Widget<?> accessibleLabelTarget()
      An editable combo's caption names its editor, where the caret and the reader are.
      Overrides:
      accessibleLabelTarget in class Widget<ComboBox<T>>
      Returns:
      the widget that carries a label bound to this one; this by default
      See Also:
    • onLayout

      protected void onLayout()
      Description copied from class: Widget
      Containers position children here (measure + Widget.layoutBox(float, float, float, float) per child).
      Overrides:
      onLayout in class Widget<ComboBox<T>>
    • onFocusLost

      protected void onFocusLost()
      Description copied from class: Widget
      Called when this widget loses keyboard focus. Default: nothing.
      Overrides:
      onFocusLost in class Widget<ComboBox<T>>
    • onDetached

      protected void onDetached()
      A combo removed from the tree must never strand its dropdown on screen: a programmatically-opened popup has no focus to lose, so focus-lost alone does not cover this.
      Overrides:
      onDetached in class Widget<ComboBox<T>>
    • onAccessibility

      protected void onAccessibility(Accessibility a)
      The field, which is a combo box that is open or shut and is showing one of its items. The options are not here: they are drawn by the panel and described by it, in whichever scene that panel is mounted in, which is what keeps their rectangles right in both presentations.

      The selected item is the value and never the name. A combo has no caption of its own — its items are values and not a label — so the name is left for an application's setAccessibleName or for the tooltip default to supply, and a combo with neither is published unnamed. Naming it from the item it shows would be worse than that: the name would change on every pick, and on the one platform that maps a name and a value to different attributes a reader would hear the same word twice for every combo in the interface.

      The number is what carries the text. The publish step builds a value facet only for a node that declared a number, so the selected item's text alone would be dropped in silence; and the difference raises its value event off the number and never off the text, so a constant standing in for one would make every pick silent on the channel a reader listens to. The index is the honest number — nothing else about the selection is ordered — and declaring it is also what advertises SET_VALUE, which is how an assistive technology picks an item without opening the list at all.

      Nothing here is formatted. The item's text comes from the I18nString the combo already holds, through that string's own per-language memo, so a hover fade or a focus fade — each of which damages this widget on every frame it lasts — walks this hook without allocating anything to conclude that nothing moved.

      Overrides:
      onAccessibility in class Widget<ComboBox<T>>
      Parameters:
      a - the builder for this node
    • onAccessibilityAction

      protected boolean onAccessibilityAction(Accessible.Action action, Accessible.Argument arg)
      Opens the list, closes it, or picks an item, each through the path the user's own gesture takes.

      No enabled check of its own, unlike the panel's: the node acted on here is this widget, so the scene's own gate has already walked this field and every ancestor for isEnabled(), checked that it is showing, that the window is not modal-blocked and that it is inside the layer that owns input. That last test is why a Accessible.Action.COLLAPSE sent to an open in-scene list does nothing, and why the walk does not publish one there: the list's own overlay is that layer, the field is behind it, and the dismissal that lands there is the overlay's CANCEL — the same asymmetry the keyboard has, where Esc is delivered to the overlay and handed back here.

      Overrides:
      onAccessibilityAction in class Widget<ComboBox<T>>
      Parameters:
      action - what is being asked
      arg - the item to select for SET_VALUE, by number or by text
      Returns:
      whether this widget did it