Class ComboBox

java.lang.Object
limn.scene.Widget
limn.components.ComboBox

public class ComboBox extends Widget
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 never takes focus; the combo keeps it, so clicking anywhere else in the parent window closes the popup via focus loss. Headless scenes (tests) keep the same open/highlight/commit state machine without a native window.

In-scene fallback. Where the window reports no NativeWindow.supportsAbsolutePositioning() (Wayland), a list in a window of its own would open somewhere other than under the field, so the list is drawn as an overlay inside the owner window instead. 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.

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 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

  • Method Details

    • localized

      public static ComboBox 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 onSelect(Consumer<Integer> listener)
      Called with the chosen index whenever the selection changes: a pick from the popup and a setSelectedIndex(int) from code both arrive here.
    • selectedIndex

      public int selectedIndex()
      The selected item's index; never negative, since a combo refuses an empty item list and so always has exactly one selection. There is nothing to clear to.
    • selectedItem

      public String selectedItem()
      The selected item as it currently reads, in the UI language.
    • setSelectedIndex

      public ComboBox setSelectedIndex(int index)
      Selects an item and fires onSelect; code and a pick from the popup take the same path, so a listener sees every change either way. Selecting what is already selected changes nothing and fires nothing; that early return is what keeps two controls bound to each other from recursing, so do not remove it. 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 NativeWindow popupWindow()
      Returns:
      the popup's native window while open (screenshots/tests), 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).
    • setDisplayMode

      public ComboBox 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). 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 platform forcing IN_SCENE. While it is closed, what was last asked for.
    • close

      public void close()
      Dismisses the popup without changing the selection. No-op when closed.
    • onMeasure

      protected Size onMeasure(Constraints constraints)
      Description copied from class: Widget
      Reports the size this widget wants within constraints. Called once per layout pass, and the result is cached against the constraints and the resolved axes (size step, layout direction, locale), so it must be a pure function of them and of this widget's own state.

      Resolve the ControlSize and the LayoutDirection once each here and thread them down; never read either in a constructor. The locale needs no threading: it is in scope, and I18n.locale() answers it wherever text is resolved.

      Specified by:
      onMeasure in class Widget
    • baselineOffset

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

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

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

      protected void onKeyEvent(KeyEvent event)
      Description copied from class: Widget
      Key events (focused widget first, then ancestors).
      Overrides:
      onKeyEvent in class Widget
    • 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
    • onFocusGained

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

      protected void onFocusLost()
      Description copied from class: Widget
      Called when this widget loses keyboard focus. Default: nothing.
      Overrides:
      onFocusLost in class Widget
    • 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