Class RangeSlider

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

public final class RangeSlider extends Widget<RangeSlider>
A horizontal slider of two thumbs that selects a range in [min, max]: a lower value and an upper one over the same track, the span between them filled. Built as Slider is: the same track, knobs, steps, hover and focus ring, and the same direction rules reading right to left.

 RangeSlider price = new RangeSlider(0, 1000).setStep(10).setMinimumGap(50).setValues(200, 600);
 price.onChange((low, high) -> filter.setPrice(low, high));
 

A thumb stops at the other, or at the minimum gap the application sets: the lower can never pass above upper - gap, nor the upper below lower + gap. A press on the track moves the nearer thumb there and drags it; with the two on one spot, the one the pointer goes towards.

Each thumb is a control of its own for the keyboard and for an assistive technology: two Tab stops, the lower first, each moved by the arrows, Page Up and Page Down, Home and End within what the other leaves it, and each a slider node named "Minimum" or "Maximum" in the interface's language, whose bounds are the range it may reach now.

onChange and onCommit are the application's response to the user; setValues(float, float) reaches neither. Every change, whatever moved it, reaches a watcher as VALUE, and the end of a drag or a key press as COMMITTED.

  • Constructor Details

    • RangeSlider

      public RangeSlider(float min, float max)
      A slider over [min, max], its range the whole of it.
      Throws:
      IllegalArgumentException - if max is not above min
  • Method Details

    • setValues

      public RangeSlider setValues(float newLower, float newUpper)
      Sets both values, clamped to the bounds, snapped to the step and held the minimum gap apart. Values closer than the gap move apart, the upper up first and the lower down when the upper meets the maximum, each to the next step past the gap, so a value is always one the steps reach. Announces VALUE/CODE when either moved and reaches no handler.
      Throws:
      IllegalArgumentException - if newLower is above newUpper, or either is not a number
    • lower

      public float lower()
      Returns:
      the lower value
    • upper

      public float upper()
      Returns:
      the upper value
    • min

      public float min()
      Returns:
      the lower bound, inclusive
    • max

      public float max()
      Returns:
      the upper bound, inclusive
    • setStep

      public RangeSlider setStep(float newStep)
      Sets the discrete increment (0 = continuous) and snaps both values onto it.
      Throws:
      IllegalArgumentException - if it is negative or not a number
    • step

      public float step()
      Returns:
      the grid the values are snapped onto, or 0 for a continuous slider
    • setMinimumGap

      public RangeSlider setMinimumGap(float newGap)
      Sets the least distance between the two values, 0 unless set: a range of at least a day, a price band of at least ten. The values are moved apart if they are closer.
      Throws:
      IllegalArgumentException - if it is negative, not a number, or larger than the bounds
    • minimumGap

      public float minimumGap()
      Returns:
      the least distance between the two values
    • onChange

      public RangeSlider onChange(@Nullable RangeSlider.RangeListener listener)
      The application's response to the user moving either thumb, with both values: on a drag, a key or an assistive technology's step, and never for setValues(float, float).
      Parameters:
      listener - the handler, or null to clear the slot
      Returns:
      this slider
      Throws:
      IllegalStateException - if a handler is already registered
    • onCommit

      public RangeSlider onCommit(@Nullable RangeSlider.RangeListener listener)
      Called with the range the user settled on: once when a drag ends, and after each key press.
      Parameters:
      listener - the handler, or null to clear the slot
      Returns:
      this slider
      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<RangeSlider>
      Parameters:
      aspect - what changed; the origin is USER or this is not called
    • 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<RangeSlider>
    • 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<RangeSlider>
    • paintOutset

      protected float paintOutset()
      Description copied from class: Widget
      How far beyond this widget's bounds its painting may reach, in logical points: its own painting and any descendant no ancestor clips (a drop shadow, a glow, a child positioned outside the parent's box). Partial rendering uses it both ways: Widget.invalidate() damage grows by it, and a repaint pass skips this subtree only when bounds + outset miss the pass region; painting farther than declared leaves stale pixels.

      The work is divided by who knows the number. A widget declares what it paints itself outside its box: Label's icon overhang, the focus ring Button, Checkbox and RadioButton draw outside theirs. A widget that lays out children and does not clip them declares how far they may paint past it, because a ring on a child at its edge hangs past it too, and a pass landing only in that band would otherwise skip it and never reach the child. Every layout the toolkit ships does that by default with the reach the theme gives a focus ring, and takes another number through its public paintOverflow, which sets what this hook answers; a layout an application writes inherits both by extending limn.scene.layout.Container, and one that extends this class directly overrides this hook itself. A widget that clips its children declares only its own painting, since the clip bounds theirs.

      Asked of every widget a repaint pass considers, and on every invalidate(), so an override answers in constant time without allocating: a number held or looked up, never a walk over the children. Default 0: everything stays inside the bounds, which is true for most leaves.

      Overrides:
      paintOutset in class Widget<RangeSlider>
    • onPaint

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

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

      protected void onAccessibility(Accessibility a)
      Description copied from class: Widget
      Says what this widget is, for an assistive technology.

      Called on the UI thread inside the publish step, with this widget's own locale in scope, and never from a paint. A widget that overrides nothing still gets a node with its bounds, its enabled, focusable, visible and showing states, its language, its children in tree order, the focus and scroll-into-view verbs when it is focusable, and its tooltip as a name when nothing else supplied one. What it does not get is a role: a widget that declares no role, no name, no description, no state, no verb and no facet is scaffolding, and the tree removes it and hoists its children in its place, so that a screen reader hears the controls rather than the boxes.

      Nothing here may allocate. The builder's setters take a facet's fields rather than a facet, and a name is handed over as the localizable string this widget holds rather than as a resolved one, so that a frame which damaged this widget and changed nothing about it costs a comparison and no memory at all. Accessibility states the two rules and what breaks when they are ignored.

      Overrides:
      onAccessibility in class Widget<RangeSlider>
      Parameters:
      a - the node being described; write into it, and never keep it
    • paintsDecoration

      protected boolean paintsDecoration()
      Description copied from class: Widget
      Whether what Widget.onPaint(limn.graphics.Canvas) draws is material rather than meaning: a wash, a blur, a shadow, a rule. Default false.

      A widget that draws its own content and declares nothing is removed from the accessible tree by the transparency rule, and the toolkit says so once per class — because the usual cause is a gauge or a sparkline nobody named, and the interface it drew is then simply absent with nothing said. That guard asks "does this class draw?", which reflection can answer; it cannot ask whether the drawing means anything, and this widget is the only thing that can. Overriding this to true is that answer, and the removal then happens in silence.

      It is not Widget.setAccessibleIgnored(boolean), and the difference is the subtree. Ignored takes this widget's children out of the tree with it, which on a panel whose whole purpose is to sit behind controls hides every control it wraps. Saying the painting is decoration changes nothing about the tree at all: the node is removed exactly as it would have been, the children hoist into its place, and only the warning goes away.

      Answer it true only for a class whose drawing a reader loses nothing by never hearing about. A widget that paints information is named or given a role instead; there is no third answer that leaves the picture out and keeps the user informed.

      Overrides:
      paintsDecoration in class Widget<RangeSlider>
      Returns:
      whether this widget's own drawing carries no information