Class RadioButton

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

public final class RadioButton extends Widget<RadioButton>
A single-choice control: a ring that fills with a dot when selected. Radios in a ButtonGroup are mutually exclusive: selecting one deselects the rest. Unlike a Checkbox, clicking a selected radio does not turn it off. Selected by click, or Space/Enter when focused. Colours from Theme, extents from the SizeTokens row resolved on this widget, pens from Strokes.

The ring rides indicator, the same token as Checkbox's box, so a form mixing the two keeps every label on one optical column at every step.

The ring sits on the row's leading edge with the label running away from it, so both swap sides with the LayoutDirection resolved on this widget. The ring is a circle and the dot is concentric with it, so nothing inside the indicator has a side to reflect; only the column the pair occupies moves.

This row is under the 24 pt pointer target

The row is max(indicator, lineHeight) rather than the control-height ramp, and the indicator wins that max at every step (18 pt at MEDIUM), which is below the 24 pt target of WCAG 2.2 SC 2.5.8 (AA) on the axis that decides it: a label widens a target, it never heightens it. A radio's pointer target is exactly the ring-and-label box it paints.

The standard's own Spacing exception is what an application relies on: an undersized target conforms while a 24 pt circle centred on it clears every neighbour. A lone radio already satisfies that; a group (the normal case) does not until the pitch reaches 24, which is what Tokens.toggleColumnGap(limn.scene.Widget) gives. Stack a ButtonGroup's radios on that gap, not a tighter one.

  • Constructor Details

    • RadioButton

      public RadioButton(String text)
      A radio with a fixed label; see the I18nString constructor for localized text.
    • RadioButton

      public RadioButton(I18nString text)
      A radio button whose label follows the UI language; see I18nString.
  • Method Details

    • onChange

      public RadioButton onChange(@Nullable Consumer<Boolean> listener)
      Fires with true when this radio becomes selected, false when a sibling takes over.

      One handler at a time: null clears it, and setting a second while one is set throws IllegalStateException.

    • 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<RadioButton>
      Parameters:
      aspect - what changed; the origin is USER or this is not called
    • text

      public String text()
      Returns:
      the caption beside the indicator, resolved in this widget's own language. The accessible tree takes this as the control's name.
    • textSource

      public I18nString textSource()
      Returns:
      the localizable string behind text(). The "is it set here" reader, matching the pair Button and Label expose.
    • setText

      public RadioButton setText(String newText)
      Replaces the label with a fixed string. UI thread only.
    • setText

      public RadioButton setText(I18nString newText)
      Replaces the label with one that follows the UI language. UI thread only.
      Parameters:
      newText - the new caption
      Returns:
      this radio button
      Throws:
      NullPointerException - if newText is null
    • isSelected

      public boolean isSelected()
      Whether this radio is the selected one in its group.
    • select

      public void select()
      Selects this radio (idempotent). In a group, deselects the previously selected sibling and tells the group; standalone, just selects itself. A CODE write spelled as a verb: it announces VALUE/CODE on every member it moves and reaches no handler, exactly as ButtonGroup.setSelectedIndex(int) does.
    • onDetached

      protected void onDetached()
      A group does not hold a member that is out of the scene: a form rebuilt against the same group would otherwise leave it every radio of every form before, each still counted.
      Overrides:
      onDetached in class Widget<RadioButton>
    • onAttached

      protected void onAttached()
      Description copied from class: Widget
      Called when this widget enters a scene (attached to the tree). Widget.scene() is the scene it just joined. Fires top-down, so a parent runs before its children. Default no-op.
      Overrides:
      onAttached in class Widget<RadioButton>
    • 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<RadioButton>
    • baselineOffset

      protected float baselineOffset()
      Description copied from class: Widget
      Distance from this widget's top edge to its first text baseline, in logical points: the alignment reference for Flex.CrossAlignment.BASELINE. Default height(): 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 is textRuler().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. Flex guarantees that: it lays every child of a BASELINE line out at cross position 0 first, reads the baselines, then repositions with Widget.moveChild(limn.scene.Widget<?>, int), which moves without re-running layout.

      Overrides:
      baselineOffset in class Widget<RadioButton>
    • onPaint

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

      protected float paintOutset()
      The focus circle is the only thing that paints outside the box: radius ring/2 + Strokes.FOCUS_GAP_INDICATOR + pen/2 with a centred Strokes.FOCUS_RING_THIN pen puts its outer ink 3pt past the indicator, which is flush with the widget's leading edge — the left one reading left to right and the right one reading right to left — and (since the row is the indicator at every step) with its top and bottom. One outset covers every side, so the edge the indicator moves to is already inside the damage and the direction is not an input here. Scene assumes only 1pt of AA feather, so without this the fading ring left stale pixels under partial rendering. Deliberately the same expression as Checkbox's: the two are in declared lockstep and must damage the same rectangle, or a form column mixing them repaints unevenly.
      Overrides:
      paintOutset in class Widget<RadioButton>
    • onAccessibility

      protected void onAccessibility(Accessibility a)
      What this radio is to an assistive technology: one Accessible.Role.RADIO_BUTTON, named by the caption it holds, a member of its group's selection with its one-based position and the group's size, offering Accessible.Action.SELECT, and nothing else.

      The name is the held I18nString handed over by reference, never text(): the walk compares the reference, the locale and the translation epoch, so a hover or focus fade that damages this row on every frame concludes that nothing moved without allocating. It is handed over even when the caption is empty, because the walk names a nameless node from its tooltip and describes a named one with it; a caption-less radio with no tooltip publishes an empty name, which is the application's to fix and not this widget's to invent.

      The selection facet reads the selected field and never the eased dot, and its two numbers come from the group through readers that touch its list and allocate nothing; the public ButtonGroup.members() is a copy per call and would cost one per damaged frame. A standalone radio has no set and says so with zeros, which a bridge publishes as nothing. There is no group node for the numbers to hang under, because a ButtonGroup is not a widget and has no box, and there is no relation to it for the same reason: a relation's target has to be a published node. So the facet says the member has no container: a radio's selection change is its own selected-state event and is never laid on whatever layout node happens to be its published parent, and a bridge asked for its selection container answers none. The radio is deliberately not marked Accessible.State.ACTIVE: the publish step takes the first active node in a container's subtree as that container's active descendant, and a radio inside a list cell would hijack the list's.

      Only select is offered. A radio never toggles off, so there is no deselect, and the only route to no selection is ButtonGroup.clearSelection() from code. The box, the focus and scroll-into-view verbs on the group's one tab stop, and the enabled, visible, showing, focusable and focused states are the walk's; roving focus is already expressed through the focusable flag, whose setter invalidates the tree itself.

      Overrides:
      onAccessibility in class Widget<RadioButton>
      Parameters:
      a - the node being described
    • onAccessibilityAction

      protected boolean onAccessibilityAction(Accessible.Action action, Accessible.Argument arg)
      Performs a select an assistive technology asked for, exactly as a click or a Space press does: through the seam at USER, so a group deselects the previous member, both radios' onChange handlers and the group's own listener are notified the same way, and the tab stop moves with the selection. Selecting the member that is already selected is accepted and does nothing, because the state asked for holds. Any other verb is refused, including a press, which this node does not offer, and so is a select while this widget is disabled: the seam carries no such guard of its own, because it is also the path ButtonGroup.setSelectedIndex(int) takes to restore a disabled form's state, and the answer here has to be truthful.

      The state change that results is what a reader hears: the group's swap invalidates both members, the next frame republishes, and the difference raises the selected-state event on each. No invocation event is raised for a select, on any platform.

      Overrides:
      onAccessibilityAction in class Widget<RadioButton>
      Parameters:
      action - what was asked
      arg - ignored; a select carries none
      Returns:
      whether the select ran, or already held
    • onMouseEvent

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

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

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

      protected void onFocusLost()
      Description copied from class: Widget
      Called when this widget loses keyboard focus. Default: nothing.
      Overrides:
      onFocusLost in class Widget<RadioButton>