Class TabbedPane
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.
-
Nested Class Summary
Nested ClassesModifier and TypeClassDescriptionstatic enumWhere the strip sits when the headers are narrower than the pane, named for where it lands in an interface that reads left to right.static interfaceOne question toonTabClose: may this tab close? Answered once, withTabbedPane.TabCloseRequest.allow()orTabbedPane.TabCloseRequest.refuse(), now or later: a handler that shows a dialog keeps the request and answers from its result.static interfaceWhat the user moved: a tab from one place in the order to another. -
Constructor Summary
ConstructorsConstructorDescriptionAn empty pane; add pages withaddTab(java.lang.String, limn.scene.Widget<?>). -
Method Summary
Modifier and TypeMethodDescriptionaddTab(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.Appends a tab with a leading icon, tinted to the tab's text colour and drawn as authored whichever way the pane reads.Appends a tab.addTab(I18nString title, Icon icon, Icon.Mirroring mirroring, Widget<?> content) The icon-mirroring form for a caption that follows the UI language.addTab(I18nString title, Icon icon, Widget<?> content) Appends a tab with an icon and a caption that follows the UI language.addTab(I18nString title, Widget<?> content) A tab whose caption follows the UI language; seeI18nString.The strip's horizontal alignment.protected booleanKeeps a panel inside the pane.protected voidhandleUserChange(Change.Aspect aspect) Runs this widget's application handler for a change it has just announced.intindexOfTab(Widget<?> content) booleanisTabClosable(int index) booleanmoveTab(int from, int to) Moves a tab to another place in the order: a caller's write, announced asCHILDREN/CODE.protected voidonAccessibilityChild(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.protected voidA pane taken out of its window takes its open list with it, as a combo box does.protected voidonKeyEvent(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.protected voidonLayout()Containers position children here (measure +Widget.layoutBox(float, float, float, float)per child).protected SizeonMeasure(Constraints constraints) Reports the size this widget wants withinconstraints.protected voidWidget's own background/content, in local coordinates.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.onTabClose(@Nullable Consumer<TabbedPane.TabCloseRequest> handler) Asks the application whether a tab the user is closing may close.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.protected voidpaintChildren(Canvas canvas) Children pass; override to clip (e.g.protected booleanThe line under the strip is a rule, and a rule is material rather than meaning.removeTab(int index) Takes a tab out of the pane, asking no handler: a caller's write, announced asCHILDREN/CODE.intIndex of the selected tab, or-1when there are none.setAlignment(TabbedPane.TabAlignment newAlignment) Where the strip sits when the headers are narrower than the pane.setSelectedIndex(int index) Selects a tab and scrolls it into view: a caller's write, so it announcesSELECTION/CODEand reaches no handler.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).setTabsClosable(boolean value) Whether tabs added from now on can be closed by the user.Widget<?> tabContent(int index) inttabCount()How many tabs the pane holds; and, read before anaddTab, the index that call will land on.tabTitle(int index) tabTitleSource(int index) Methods inherited from class limn.scene.Widget
acceptsTextInput, accessibleLabelAccessKey, accessibleLabelTarget, accessibleLabelText, add, add, announceChange, backdropReach, baselineOffset, baselineOffsetOf, caretRect, children, clipboard, clipHeight, clipWidth, clipX, clipY, controlSize, cursor, declaredControlSize, declaredLayoutDirection, declaredLocale, focusArrivedBackward, focusArrivedByTraversal, height, hitTest, hitTestChild, id, imageCursor, inheritanceHost, invalidate, invalidate, invalidateAccessible, isEnabled, isFocusable, isFocused, isRightToLeft, isSelfEnabled, isShowing, isVisible, isVisibleByItsOwnFlag, layoutBox, layoutDirection, locale, localToScene, localToSceneBounds, markNeedsContainedLayout, markNeedsLayout, markNeedsLayoutInPlace, maxHeight, maxWidth, measure, minHeight, minWidth, moveChild, moveChild, moveChild, neutralBase, notifyChange, notifyTextEdit, observeChanges, onAccessibility, onAccessibilityAction, onAccessibilityChildAction, onAccessibilityChildIdentity, onAttached, onCanEdit, onCharTyped, onDragExit, onDragOver, onDragStart, onDrop, onEdit, onFocusGained, onFocusLost, onHitTest, onMouseEvent, onPaintOverlay, onPreedit, onSyntheticAction, overlayPassesPointer, paintOutset, paintsFromBackdrop, paintWidget, parent, remove, remove, requestFocus, requestFocus, revealInView, revealInView, runHandler, scene, sceneNanos, sceneToLocal, self, setAccessibleDescribedBy, setAccessibleDescription, setAccessibleDescription, setAccessibleIgnored, setAccessibleLabelledBy, setAccessibleName, setAccessibleName, setAccessibleRole, setControlSize, setCursor, setEnabled, setFocusable, setId, setImageCursor, setInheritanceHost, setLayoutDirection, setLocale, setMaxHeight, setMaxWidth, setMinHeight, setMinWidth, setPresentedElsewhere, setTooltip, setTooltip, setVisible, shapeText, textRuler, tooltip, tooltipSource, width, x, y
-
Constructor Details
-
TabbedPane
public TabbedPane()An empty pane; add pages withaddTab(java.lang.String, limn.scene.Widget<?>).
-
-
Method Details
-
addTab
Appends a tab. The first one added is selected. UI thread only. -
addTab
A tab whose caption follows the UI language; seeI18nString. -
addTab
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
Appends a tab with an icon and a caption that follows the UI language. -
addTab
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.NEVERunless this glyph is directional; a wrongNEVERis one arrow pointing the wrong way, and a wrongIN_RTLis a flipped brand mark
-
addTab
The icon-mirroring form for a caption that follows the UI language. -
setAlignment
Where the strip sits when the headers are narrower than the pane. Ignored while overflowing. -
alignment
The strip's horizontal alignment. -
selectedIndex
public int selectedIndex()Index of the selected tab, or-1when 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 anaddTab, the index that call will land on.That is what it is for.
addTabreturns 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
- Parameters:
index- a tab in[0, tabCount)- Returns:
- that tab's caption, resolved in this pane's own language
- Throws:
IndexOutOfBoundsException- ifindexis not a tab
-
tabTitleSource
- Parameters:
index- a tab in[0, tabCount)- Returns:
- the localizable string behind
tabTitle(int). A tab's caption was already anI18nStringand was reachable only from inside this package; anything that has to announce a caption that changed needs it from outside. - Throws:
IndexOutOfBoundsException- ifindexis not a tab
-
setSelectedIndex
Selects a tab and scrolls it into view: a caller's write, so it announcesSELECTION/CODEand 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- ifindexis 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
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 forsetSelectedIndex(int), which is a caller's write; to hear every change whatever caused it, watch the pane instead.- Parameters:
listener- the handler, ornullto clear the slot- Returns:
- this pane
- Throws:
IllegalStateException- if a handler is already registered
-
handleUserChange
Description copied from class:WidgetRuns this widget's application handler for a change it has just announced. Called byWidget.runHandler(limn.scene.Change), and so byWidget.notifyChange(limn.scene.Change), only for aUSERorigin 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 boxselectedIndex(), a text fieldtext(). 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: aSearchFieldthat overrode this for its own aspect and did not chain would silently deleteTextField'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:
handleUserChangein classWidget<TabbedPane>- Parameters:
aspect- what changed; the origin isUSERor this is not called
-
setTabsClosable
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
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 withremoveTab(int).- Parameters:
index- a tab in[0, tabCount)value- whether it can- Returns:
- this pane
- Throws:
IndexOutOfBoundsException- ifindexis 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- ifindexis not a tab
-
tabContent
- 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- ifindexis not a tab
-
indexOfTab
- Parameters:
content- a widget- Returns:
- the index of the tab that shows it, by identity, or -1 when none does
-
removeTab
Takes a tab out of the pane, asking no handler: a caller's write, announced asCHILDREN/CODE. Taking out the selected tab selects the one after it, or the one before when it was the last, and that is announced asSELECTION/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- ifindexis not a tab
-
moveTab
Moves a tab to another place in the order: a caller's write, announced asCHILDREN/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
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 forremoveTab(int), which is a caller's own decision.- Parameters:
handler- the handler, ornullto clear the slot- Returns:
- this pane
- Throws:
IllegalStateException- if a handler is already registered
-
onTabMove
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 formoveTab(int, int); to hear every change, watch the pane forCHILDREN.- Parameters:
listener- the handler, ornullto clear the slot- Returns:
- this pane
- Throws:
IllegalStateException- if a handler is already registered
-
onKeyEvent
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:
onKeyEventin classWidget<TabbedPane>
-
onDetached
protected void onDetached()A pane taken out of its window takes its open list with it, as a combo box does.- Overrides:
onDetachedin classWidget<TabbedPane>
-
onMeasure
Description copied from class:WidgetReports the size this widget wants withinconstraints. 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
ControlSizeand theLayoutDirectiononce each here and thread them down; never read either in a constructor. The locale needs no threading: it is in scope, andI18n.locale()answers it wherever text is resolved.- Specified by:
onMeasurein classWidget<TabbedPane>
-
onLayout
protected void onLayout()Description copied from class:WidgetContainers position children here (measure +Widget.layoutBox(float, float, float, float)per child).- Overrides:
onLayoutin classWidget<TabbedPane>
-
onPaint
Description copied from class:WidgetWidget's own background/content, in local coordinates.- Overrides:
onPaintin classWidget<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 aScrollView, 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:
clipsChildrenin classWidget<TabbedPane>
-
paintChildren
Description copied from class:WidgetChildren pass; override to clip (e.g. scroll views).Indexed rather than an enhanced
for, for the reasonScene'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:
paintChildrenin classWidget<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 issetAccessibleIgnored(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:
paintsDecorationin classWidget<TabbedPane>- Returns:
true; the only thing this widget draws is that line
-
onAccessibilityChild
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:
onAccessibilityChildin classWidget<TabbedPane>- Parameters:
child- the child being described; a panel only when it is one of the pagesa- the node being described, which is the child's and never this pane's
-