Class TabbedPane

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

public class TabbedPane extends Widget
Tabbed panel: a horizontal strip of tab headers over a content area that shows the selected tab. The strip's horizontal alignment is configurable: TabbedPane.TabAlignment.LEFT, TabbedPane.TabAlignment.CENTER or TabbedPane.TabAlignment.RIGHT. Headers select on click; only the selected content is laid out and painted. All visuals come from the Theme.

This or a SegmentedControl? Ask who owns the content. A tabbed pane owns it (addTab(java.lang.String, limn.scene.Widget) hands it the pages and it decides which is laid out), which is why it carries chevrons, a popup and a scrollable strip. A segmented control owns nothing and hands back an index, so it fits wherever the caller keeps the content and switches it itself.

Keyboard: the strip is a single tab stop (roving focus): only the selected header is focusable, so Tab/Shift+Tab land on it; Left/Right arrows move between tabs (Home/End jump to the ends) and Enter/Space dive into the panel. Focus follows a programmatic selection while the strip holds it.

Overflow: when the headers don't fit, the strip becomes a clipped, scrollable viewport (alignment is moot then): chevron buttons appear at the edges (the dead side disabled, not hidden; hiding would resize the viewport and oscillate at the ends), the mouse wheel scrolls the strip, and a list button opens a PopupMenu of all tabs (check on the current one) to jump directly. Selecting a tab (by click, keyboard or setSelectedIndex(int)) always scrolls it into view. The three square controls shrink with the pane rather than being dropped, each capped at a sixth of its width, so the viewport never falls below half the pane, never reaches zero, and never changes non-monotonically as the pane is resized.

Sizes follow the ControlSize resolved on this pane: type, tab paddings, icon and reveal margin come from the SizeTokens row, while the strip separator, the selected-tab indicator, the focus ring and the chevron pen are weights and stay identical at every step. The strip height composes the independent type and padding ramps, so it is deliberately fractional, which is why the whole layout must derive from one resolve per pass.

Reading right to left the strip is still one run of tabs in reading order, and the run reflects with the interface: the first tab lands on the edge reading starts from and the run advances away from it, the three overflow controls swap ends keeping the order they had, and the two scroll chevrons turn around. Left and Right select the tab that is visually to the left and to the right, so the keyboard and the pointer agree; Home and End name the ends of the tab order and stay first and last in both directions.

  • Constructor Details

  • Method Details

    • addTab

      public TabbedPane addTab(String title, Widget content)
      Appends a tab. The first one added is selected. UI thread only.
    • addTab

      public TabbedPane addTab(I18nString title, Widget content)
      A tab whose caption follows the UI language; see I18nString.
    • addTab

      public TabbedPane addTab(String title, Icon icon, Widget content)
      Appends a tab with a leading icon, tinted to the tab's text colour and drawn as authored whichever way the pane reads. Leading is the side reading starts on: the left of the caption left to right, its right in a right-to-left pane.
    • addTab

      public TabbedPane addTab(I18nString title, Icon icon, Widget content)
      Appends a tab with an icon and a caption that follows the UI language.
    • addTab

      public TabbedPane addTab(String title, Icon icon, Icon.Mirroring mirroring, Widget content)
      Appends a tab with a leading icon and says whether the glyph inside it turns around when the interface does. The icon's position is leading either way and is this pane's decision; whether the drawing is a back arrow (which turns around) or a logo, a download arrow or a photograph (which must not) is knowable only at the call that placed it.
      Parameters:
      mirroring - Icon.Mirroring.NEVER unless this glyph is directional; a wrong NEVER is one arrow pointing the wrong way, and a wrong IN_RTL is a flipped brand mark
    • addTab

      public TabbedPane addTab(I18nString title, Icon icon, Icon.Mirroring mirroring, Widget content)
      The icon-mirroring form for a caption that follows the UI language.
    • setAlignment

      public TabbedPane setAlignment(TabbedPane.TabAlignment newAlignment)
      Where the strip sits when the headers are narrower than the pane. Ignored while overflowing.
    • alignment

      public TabbedPane.TabAlignment alignment()
      The strip's horizontal alignment.
    • selectedIndex

      public int selectedIndex()
      Index of the selected tab, or -1 when there are none. A pane that holds tabs always has one selected, so there is no clearing operation and no way back to -1.
    • tabCount

      public int tabCount()
      How many tabs the pane holds; and, read before an addTab, the index that call will land on.

      That is what it is for. addTab returns the pane so a builder can chain, which leaves a caller that needs to select a tab later with a hand-written index, and a hand-written index is wrong the moment a tab is inserted above it, silently, because every index below merely shifts to another real tab.

    • setSelectedIndex

      public TabbedPane setSelectedIndex(int index)
      Selects a tab, scrolls it into view and fires onSelect(java.util.function.Consumer<java.lang.Integer>); code and a click take the same path, so a listener sees every change either way. Re-selecting the current tab still scrolls it back into view (a caller asking for a tab is asking to be shown it) but 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 - a tab in [0, tabCount)
      Throws:
      IndexOutOfBoundsException - if index is not a tab; an empty pane has none, so every index throws there. Arrowing past an end is not this: keyboard traversal stops at the end rather than raising.
    • onSelect

      public TabbedPane onSelect(Consumer<Integer> listener)
      Called with the new index whenever the selection changes, by click, keyboard or code.
    • 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
    • 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
    • onPaint

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

      protected boolean clipsChildren()
      Keeps a panel inside the pane.

      onLayout() hands the selected panel a box, and a box is all it is: nothing stops a child from measuring taller than it and painting past the bottom edge. A panel that does (anything not wrapped in a ScrollView, which is most content once a window is made short enough) then draws over whatever the pane is sitting on, and it looks like the container has no bounds at all rather than like the content is too big.

      Clipping is the honest answer, and it is not a substitute for scrolling: it makes overflow read as content that continues past an edge, which is what it is. Content that should be reachable still needs a scroll view around it, and the demo's panels have one.

      Overrides:
      clipsChildren in class Widget
    • paintChildren

      protected void paintChildren(Canvas canvas)
      Description copied from class: Widget
      Children pass; override to clip (e.g. scroll views).
      Overrides:
      paintChildren in class Widget