Class SegmentedControl
RadioButton group or tabs. The selected segment is marked by an
accent indicator that slides between segments (animated like the
TabbedPane indicator). Click a segment, or move with Left/Right when
focused. Colours come from Theme; the box, the type and the gutters come
from the resolved ControlSize row (SizeTokens), while the
track border stays one weight at every step (Strokes).
Width: the track is exactly as wide as its segments need and no wider, centred in
whatever box the parent assigns. That matters because the common parent (a column with
STRETCH) hands every child the full row, and a pill track drawn edge to edge with
four short segments huddled at its left end reads as broken. The margins either side belong
to what is behind this control: a click landing on one selects nothing.
Overflow: when the segments do not fit, the strip becomes a clipped, scrollable
viewport with a chevron at each edge, the dead side drawn disabled rather than dropped,
since dropping it would resize the viewport and make the strip jump at the ends. The mouse
wheel scrolls it, a chevron click scrolls by most of a viewport, and selecting a segment by
any route (click, Left/Right, or setSelectedIndex(int)) scrolls it into view. Nothing
is ever painted outside the track. There is no all-segments popup, which is the one part of
the TabbedPane overflow story this control does not carry: its labels are handed in
by the caller and are few and short by construction, and a control that fits in a toolbar
row has nowhere to put a third piece of chrome.
Reading direction: the strip is a row of items in reading order, so it mirrors. The
segments are placed from the edge reading starts at, the back chevron sits in that same gutter
and points back towards the first segment, and Left selects the segment that is visually on the
left: a keyboard that walked the other way from the pointer would be arguing with the screen.
Home and End do not mirror, because they name the first and last segment rather
than a side. Only the placement moves; the track, the viewport and each label inside its own
segment are symmetric and stay where they are, and there is no mirror transform anywhere here.
This or a TabbedPane? Ask who owns the content. A segmented
control owns none: it takes labels and hands back an index, so the caller keeps
the content and switches it, which is what makes it the one that belongs in a
toolbar row, a form, or inside another widget that already owns what changes. A
tabbed pane is the opposite: you hand it the pages and it owns them, laying out
and painting only the selected one, and carrying the overflow story that owning
an unknown number of pages requires. Looking alike is not the test; a row of
labels is what both are, and what they do is where they split.
-
Constructor Summary
ConstructorsConstructorDescriptionSegmentedControl(List<String> segments) A control over the given fixed segment labels; the first is selected.SegmentedControl(I18nString... segments) A control whose segment captions follow the UI language; the first is selected. -
Method Summary
Modifier and TypeMethodDescriptionprotected floatDistance from this widget's top edge to its first text baseline, in logical points: the alignment reference forFlex.CrossAlignment.BASELINE.protected booleanYes, and vacuously: this control paints its segments itself and has no child widgets, so there is nothing a clip could fail to contain.protected voidhandleUserChange(Change.Aspect aspect) Runs this widget's application handler for a change it has just announced.protected voidWhat this strip is to an assistive technology: one horizontalAccessible.Role.RADIO_GROUPholding oneAccessible.Role.RADIO_BUTTONper segment, plus the two scroll arrows while it overflows.protected voidCalled when this widget takes keyboard focus.protected voidCalled when this widget loses keyboard focus.protected voidonKeyEvent(KeyEvent event) Key events (focused widget first, then ancestors).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 voidonMouseEvent(MouseEvent event) Mouse events (bubbling); callevent.consume()when handled.protected voidWidget's own background/content, in local coordinates.onSelect(@Nullable IntConsumer listener) The application's response to the user choosing a segment: a click, an arrow key, an assistive technology's select.protected booleanonSyntheticAction(long key, Accessible.Action action, Accessible.Argument arg) Selects a segment or scrolls the strip, through the same private paths a click takes.protected floatThe focus ring reachesStrokes.FOCUS_RING_OUTSETbeyond the box, and the scene inflates a widget's damage by1 + paintOutset().segment(int index) intsegmentSource(int index) intThe selected segment's index; never negative, since the control refuses an empty segment list and so always has exactly one selection.setSelectedIndex(int index) Selects a segment and scrolls it into view.Methods inherited from class limn.scene.Widget
acceptsTextInput, accessibleLabelAccessKey, accessibleLabelTarget, accessibleLabelText, add, add, announceChange, backdropReach, 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, onAccessibilityAction, onAccessibilityChild, onAccessibilityChildAction, onAccessibilityChildIdentity, onAttached, onCanEdit, onCharTyped, onDetached, onDragExit, onDragOver, onDragStart, onDrop, onEdit, onHitTest, onPaintOverlay, onPreedit, overlayPassesPointer, paintChildren, paintsDecoration, 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
-
SegmentedControl
A control over the given fixed segment labels; the first is selected. Needs at least one.Each label is wrapped as a literal, exactly as the
Stringconstructors ofButton,LabelandCheckboxdo. A literal cannot follow the interface's language, so a control whose captions should is built from the localizable constructor instead.- Parameters:
segments- the captions; at least one- Throws:
IllegalArgumentException- if the list is empty
-
SegmentedControl
A control whose segment captions follow the UI language; the first is selected.Every other name in the accessible tree is resolved under the subtree's own language, and a caption held as a bare string is the one that cannot be: it is fixed at construction, so a screen reader reads it in whatever language the caller happened to build it in.
- Parameters:
segments- the captions; at least one- Throws:
IllegalArgumentException- if none is givenNullPointerException- if any of them isnull
-
-
Method Details
-
segmentCount
public int segmentCount()- Returns:
- how many segments this control has; always at least one
-
segment
- Parameters:
index- a segment in[0, segmentCount)- Returns:
- that segment's caption, resolved in this widget's own language
- Throws:
IndexOutOfBoundsException- ifindexis not a segment
-
segmentSource
- Parameters:
index- a segment in[0, segmentCount)- Returns:
- the localizable string behind
segment(int) - Throws:
IndexOutOfBoundsException- ifindexis not a segment
-
onSelect
The application's response to the user choosing a segment: a click, an arrow key, an assistive technology's select. Called with the chosen index. Never forsetSelectedIndex(int), which is a caller's write; to hear every change whatever caused it, watch the control instead.One handler at a time:
nullclears it, and setting a second while one is set throwsIllegalStateException. -
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<SegmentedControl>- Parameters:
aspect- what changed; the origin isUSERor this is not called
-
selectedIndex
public int selectedIndex()The selected segment's index; never negative, since the control refuses an empty segment list and so always has exactly one selection. There is nothing to clear to. -
setSelectedIndex
Selects a segment and scrolls it into view. The change is announced to the watchers as the code's and does not reachonSelect, which answers the user. Re-selecting the current segment still reveals it (a caller asking for a segment is asking to be shown it) but changes nothing and announces 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 segment in[0, segmentCount)- Throws:
IndexOutOfBoundsException- ifindexis not a segment. Arrowing past an end is not this: keyboard traversal stops at the end rather than raising.
-
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<SegmentedControl>
-
baselineOffset
protected float baselineOffset()Description copied from class:WidgetDistance from this widget's top edge to its first text baseline, in logical points: the alignment reference forFlex.CrossAlignment.BASELINE. Defaultheight(): align on the bottom edge, the correct fallback for a widget with no text. Text-bearing components override with the expression they already paint with; a single-line control's istextRuler().centredBaseline(font, height()), which stands the line's capitals in the middle of the box.Valid only once this widget has been given a box.
Flexguarantees that: it lays every child of a BASELINE line out at cross position 0 first, reads the baselines, then repositions withWidget.moveChild(limn.scene.Widget<?>, int), which moves without re-running layout.- Overrides:
baselineOffsetin classWidget<SegmentedControl>
-
clipsChildren
protected boolean clipsChildren()Yes, and vacuously: this control paints its segments itself and has no child widgets, so there is nothing a clip could fail to contain.Answered because it is what
Widget.markNeedsContainedLayout()requires, and that is the difference between a selection repainting this control and repainting the window. It clips no paint of this control's own — the focus ring still reaches out throughpaintOutset()— since the hook concernspaintChildrenalone.- Overrides:
clipsChildrenin classWidget<SegmentedControl>
-
onLayout
protected void onLayout()Description copied from class:WidgetContainers position children here (measure +Widget.layoutBox(float, float, float, float)per child).- Overrides:
onLayoutin classWidget<SegmentedControl>
-
onPaint
Description copied from class:WidgetWidget's own background/content, in local coordinates.- Overrides:
onPaintin classWidget<SegmentedControl>
-
paintOutset
protected float paintOutset()The focus ring reachesStrokes.FOCUS_RING_OUTSETbeyond the box, and the scene inflates a widget's damage by1 + paintOutset(). Without this the ring's fade-out sheds stale pixels under partial rendering.Declares reach only; it moves no ink.
- Overrides:
paintOutsetin classWidget<SegmentedControl>
-
onFocusGained
protected void onFocusGained()Description copied from class:WidgetCalled when this widget takes keyboard focus. Default: nothing.- Overrides:
onFocusGainedin classWidget<SegmentedControl>
-
onFocusLost
protected void onFocusLost()Description copied from class:WidgetCalled when this widget loses keyboard focus. Default: nothing.- Overrides:
onFocusLostin classWidget<SegmentedControl>
-
onAccessibility
What this strip is to an assistive technology: one horizontalAccessible.Role.RADIO_GROUPholding oneAccessible.Role.RADIO_BUTTONper segment, plus the two scroll arrows while it overflows.A radio group and not a tab list, for the reason this class's own documentation gives: it owns no content, it takes labels and hands back an index, so there is no page for a tab to select and "tab, 1 of 4" would offer navigation that leads nowhere. The selection facet is single and always satisfied, which is this control's documented invariant on both halves — the constructor refuses an empty segment list, and the selected index is never negative because there is nothing to clear to. The active descendant is not declared here and could not be: the publish step resolves it from the first node in this subtree that says it is active, which is the selected segment below.
The scroll facet is published on every frame rather than only while the strip overflows, so that resizing past the fitting point moves two numbers instead of making a whole facet appear and disappear. Every value is read from the fields the layout settled and from this widget's own maximum, so the facet cannot drift from the clamp that produced it, and it is derived from
overflowingrather than from the arithmetic alone: that predicate carries the half-point slop below which every path here declines to scroll at all. The offset is a distance from the leading edge in both directions, so the percentage is published unflipped; the vertical pair is the axis's nothing-to-scroll answer.Where the segment boxes come from, which is the whole difficulty of this widget. A segment's box is neither the widget's box divided by the count nor a raw edge out of
bounds: the track takes only what the segments need and centres itself in whatever a stretching parent handed over, the viewport is inset by the two chevron gutters, the scroll offset is subtracted, and reading right to left a span is placed by its end.cellLeft(boolean, float, float)folds all four together and is called here rather than re-derived, so the box a reader is given is the box a click lands in. Tree order stays the reading order in both directions; only the coordinates mirror.The geometry is guarded and the children are not, because
boundsis null until the first layout and that state reaches a published tree: a strip inside an invisible subtree is skipped by the layout and still walked, so an unguarded index would throw inside the publish step. What a reader is told there — how many segments there are and which one is selected — does not depend on a layout having run, and gaining the children on the first one would be a structure change for nothing.The hover index is deliberately not published. It is a pointer affordance, and a fact derived from it would republish the whole tree on every mouse move; so are the indicator's slide, the focus fade and the segment it is resting on. Nothing here formats a string: every name is an
I18nStringthis widget holds, handed over by reference, so a frame damaged by any of those compares equal and allocates nothing.The group names itself with nothing. A radio group's name is the question it asks, which only the application has, and this is a public class an application reaches with
setAccessibleName, a bound caption or a tooltip. Every segment carries a real name, so an unnamed group still announces its selection.- Overrides:
onAccessibilityin classWidget<SegmentedControl>- Parameters:
a- the node being described
-
onSyntheticAction
Selects a segment or scrolls the strip, through the same private paths a click takes.A select reaches
choose(int), which is literally what the click branch calls, so an assistive technology's select clamps the same way, reveals the same way and firesonSelectthrough the sameUSERseam a click does — including the early return that changes nothing and fires nothing when the segment was already selected. Re-selecting the current segment is still accepted, because it still reveals, which is the answer the pointer gets there.A press on an arrow scrolls by the same expression the click branch uses, where the sign is the arrow's logical identity and the offset is logical too, so it needs no mirroring. The answer is whether the offset moved: a dead arrow is published disabled and without the verb above, and its scroll clamps to nothing here, so a press that arrived on it anyway is refused rather than reported done.
A segment answers
SELECTalone and neverFOCUS: the selection is the cursor here, so a focus that did not select would be a lie and one that did would beSELECTunder another name. The walk grantsFOCUSonly to a focusable widget node, and a segment is not one, so the verb is neither published nor performed.No enabled guard of its own, and none is owed. The scene's dispatcher already walks this widget and every ancestor for the enabled flag, refuses an owner that is not showing, refuses a modal-blocked window and refuses anything outside the layer that owns input, all before this runs. It runs on the UI thread inside the posted task, which is what
setSelectedIndex(int)'s own thread check asks for.- Overrides:
onSyntheticActionin classWidget<SegmentedControl>- Parameters:
key- a segment's index, or one of the two chevron keysaction- what is being askedarg- unused; every verb here is parameterless- Returns:
- whether this control did it
-
onMouseEvent
Description copied from class:WidgetMouse events (bubbling); callevent.consume()when handled.- Overrides:
onMouseEventin classWidget<SegmentedControl>
-
onKeyEvent
Description copied from class:WidgetKey events (focused widget first, then ancestors).- Overrides:
onKeyEventin classWidget<SegmentedControl>
-