Class TabbedPane

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

public final class TabbedPane extends Widget<TabbedPane>
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. Alt+Down opens the list of every tab while the strip overflows. 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 the list of every tab to jump directly. The list is anchored to that button, in a window of its own where the platform can place one and in the window's own scene where it cannot: a row per tab in the strip's order, the shown tab marked, each tab's icon, and on a closable tab a close button at the row's end. Up, Down, Home and End move its cursor, Enter or Space shows the tab under it, a typed letter goes to the next tab whose caption starts with it, Delete or Ctrl+W (Cmd+W on macOS) closes that tab through onTabClose and the list stays open without it, and Escape or Tab closes the list. Alt+Down on the focused tab opens it from the keyboard; the list button stays out of the tab order, so the strip is still one stop. The list goes with the last tab, and when the tabs fit again. 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.

Closing. A tab the application made closable (setTabClosable(int, boolean), or every tab added after setTabsClosable(boolean)) draws a close button at its end while it is selected or under the pointer, so a row of tabs does not read as a row of buttons. The button, a middle click, and Ctrl+W (Cmd+W on macOS) while the keyboard is in the pane close it, after asking onTabClose, which may refuse, now or later: a document with unsaved changes asks first. Closing the selected tab selects the one after it, or the one before when it was the last.

Moving. A tab is dragged along the bar, the others making room as it passes, and moved from the keyboard with Ctrl+Shift+Page Up and Page Down (Cmd on macOS), which a screen reader hears as the tab's new place. onTabMove hears what the user moved. A tab does not leave its bar.

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

    • tabTitle

      public String tabTitle(int index)
      Parameters:
      index - a tab in [0, tabCount)
      Returns:
      that tab's caption, resolved in this pane's own language
      Throws:
      IndexOutOfBoundsException - if index is not a tab
    • tabTitleSource

      public I18nString tabTitleSource(int index)
      Parameters:
      index - a tab in [0, tabCount)
      Returns:
      the localizable string behind tabTitle(int). A tab's caption was already an I18nString and was reachable only from inside this package; anything that has to announce a caption that changed needs it from outside.
      Throws:
      IndexOutOfBoundsException - if index is not a tab
    • setSelectedIndex

      public TabbedPane setSelectedIndex(int index)
      Selects a tab and scrolls it into view: a caller's write, so it announces SELECTION/CODE and reaches no handler. 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 announces nothing. 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(@Nullable IntConsumer listener)
      The application's response to the user choosing a tab: a click on a header, an arrow key across the strip, a pick from the list of every tab, an assistive technology's select. Never for setSelectedIndex(int), which is a caller's write; to hear every change whatever caused it, watch the pane instead.
      Parameters:
      listener - the handler, or null to clear the slot
      Returns:
      this pane
      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<TabbedPane>
      Parameters:
      aspect - what changed; the origin is USER or this is not called
    • setTabsClosable

      public TabbedPane setTabsClosable(boolean value)
      Whether tabs added from now on can be closed by the user. Tabs already in the pane keep what they have; setTabClosable(int, boolean) changes one.
      Parameters:
      value - whether they can
      Returns:
      this pane
    • isTabsClosable

      public boolean isTabsClosable()
      Returns:
      whether tabs added from now on can be closed by the user
    • setTabClosable

      public TabbedPane setTabClosable(int index, boolean value)
      Whether the user can close this tab: by its close button, a middle click, or Ctrl+W (Cmd+W on macOS). Code closes any tab with removeTab(int).
      Parameters:
      index - a tab in [0, tabCount)
      value - whether it can
      Returns:
      this pane
      Throws:
      IndexOutOfBoundsException - if index is not a tab
    • isTabClosable

      public boolean isTabClosable(int index)
      Parameters:
      index - a tab in [0, tabCount)
      Returns:
      whether the user can close it
      Throws:
      IndexOutOfBoundsException - if index is not a tab
    • tabContent

      public Widget<?> tabContent(int index)
      Parameters:
      index - a tab in [0, tabCount)
      Returns:
      the content the tab shows, as it was handed to addTab(java.lang.String, limn.scene.Widget<?>)
      Throws:
      IndexOutOfBoundsException - if index is not a tab
    • indexOfTab

      public int indexOfTab(Widget<?> content)
      Parameters:
      content - a widget
      Returns:
      the index of the tab that shows it, by identity, or -1 when none does
    • removeTab

      public TabbedPane removeTab(int index)
      Takes a tab out of the pane, asking no handler: a caller's write, announced as CHILDREN/CODE. Taking out the selected tab selects the one after it, or the one before when it was the last, and that is announced as SELECTION/ ADJUSTMENT. The keyboard, if it was in the pane, goes to the newly selected tab.
      Parameters:
      index - a tab in [0, tabCount)
      Returns:
      this pane
      Throws:
      IndexOutOfBoundsException - if index is not a tab
    • moveTab

      public TabbedPane moveTab(int from, int to)
      Moves a tab to another place in the order: a caller's write, announced as CHILDREN/CODE. The selected tab stays selected wherever it goes.
      Parameters:
      from - a tab in [0, tabCount)
      to - where it goes, in [0, tabCount)
      Returns:
      this pane
      Throws:
      IndexOutOfBoundsException - if either is not a tab
    • onTabClose

      public TabbedPane onTabClose(@Nullable Consumer<TabbedPane.TabCloseRequest> handler)
      Asks the application whether a tab the user is closing may close. The handler is given a request it answers now or later; without a handler the tab closes. Never asked for removeTab(int), which is a caller's own decision.
      Parameters:
      handler - the handler, or null to clear the slot
      Returns:
      this pane
      Throws:
      IllegalStateException - if a handler is already registered
    • onTabMove

      public TabbedPane onTabMove(@Nullable TabbedPane.TabMoveListener listener)
      The application's response to the user moving a tab, by dragging it along the bar or from the keyboard, called once the move is over with where it was and where it is. Never for moveTab(int, int); to hear every change, watch the pane for CHILDREN.
      Parameters:
      listener - the handler, or null to clear the slot
      Returns:
      this pane
      Throws:
      IllegalStateException - if a handler is already registered
    • onKeyEvent

      protected void onKeyEvent(KeyEvent event)
      Ctrl+W and Ctrl+Shift+Page Up and Page Down (Cmd on macOS), wherever the keyboard is in the pane: on a tab or inside a page, as every tabbed interface takes them.
      Overrides:
      onKeyEvent in class Widget<TabbedPane>
    • onDetached

      protected void onDetached()
      A pane taken out of its window takes its open list with it, as a combo box does.
      Overrides:
      onDetached in class Widget<TabbedPane>
    • 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<TabbedPane>
    • 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<TabbedPane>
    • onPaint

      protected void onPaint(Canvas canvas)
      Description copied from class: Widget
      Widget's own background/content, in local coordinates.
      Overrides:
      onPaint in class Widget<TabbedPane>
    • 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<TabbedPane>
    • paintChildren

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

      Indexed rather than an enhanced for, for the reason Scene's own overlay loop already carries: an iterator here is an allocation per widget per frame, and an idle window is the one place in this toolkit that is not allowed to allocate at all. A subtree of two hundred widgets was paying two hundred short-lived objects a frame to walk a list it can index.

      Overrides:
      paintChildren in class Widget<TabbedPane>
    • paintsDecoration

      protected boolean paintsDecoration()
      The line under the strip is a rule, and a rule is material rather than meaning.

      This pane is no node: it declares no role, no name, no verb and no state of its own, it is never focusable of its own accord, and it declares no children beyond the widgets it holds, so the accessible tree deletes it as scaffolding and hoists the strip, the three overflow controls and every panel into its place. That is the right shape, and it is what onAccessibilityChild(limn.scene.Widget<?>, limn.accessibility.Accessibility) is written against.

      What is not free is the log. The tree warns once per class about a widget that paints its own content and says nothing about itself, because the usual cause is a gauge nobody named; it can see that this class overrides onPaint(limn.graphics.Canvas) and it cannot see what the drawing is. The advice that warning gives is setAccessibleIgnored(true), which here would take the tab list, the chevrons and every panel out of the tree — so the warning would name a toolkit class in an application's log and recommend deleting the interface. The separator is a hairline between the strip and the panel below it, and there is nothing in it for a reader to lose, so this is the honest answer and the deletion happens in silence.

      Overrides:
      paintsDecoration in class Widget<TabbedPane>
      Returns:
      true; the only thing this widget draws is that line
    • onAccessibilityChild

      protected void onAccessibilityChild(Widget<?> child, Accessibility a)
      Says of a panel what only this pane knows: that it is the one a tab selects, which tab names it, and that the two are linked.

      A panel cannot say any of that itself. It is whatever an application handed addTab(java.lang.String, limn.scene.Widget<?>) — a column, a scroll view, a label — and it knows nothing about tabs; the pairing between a panel and a header exists only here. The header cannot say it either: its own parent is the strip. So this is the hook, and the pane being deleted does not stop it, because the slot open while it runs is the child's node and not this pane's. The pane declares nothing here, so it stays scaffolding.

      The role and the name are conditional, and that is the whole of the correctness. This hook runs after the child's own describe hook and before an application's overrides. A panel that already said what it is keeps it: one wrapped in a scroll view is a scroll pane, and eliding that role would take the scrolling with it. A panel that already has a name of its own keeps that too. Only a panel that said nothing takes the tab's caption, and an application's Widget.setAccessibleName(limn.i18n.I18nString) still wins over both, because the overrides run last.

      The link is unconditional, because it is true whatever the panel turned out to be. Its mirror is on the tab, which says which panel it controls.

      Only a panel is touched. The strip and the three overflow controls are children of this pane as well and arrive here too; none of them is a tab's panel, and this hook leaves them alone. Nothing is derived either: the caption is the string the header already holds, handed over by reference, so a frame damaged by the indicator's slide walks this hook without allocating.

      Geometry needs nothing. The selected panel is laid out under the strip at the pane's full width, and an unselected one is never laid out at all — but it is also not visible, so it and everything inside it publish without VISIBLE and without SHOWING, which is what keeps a hidden tab's controls from announcing as focusable and makes its stale box harmless.

      Overrides:
      onAccessibilityChild in class Widget<TabbedPane>
      Parameters:
      child - the child being described; a panel only when it is one of the pages
      a - the node being described, which is the child's and never this pane's