Class ColorPicker

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

public final class ColorPicker extends Widget<ColorPicker>
Picks a colour the way a graphics application does: a saturation/value field for the current hue, a hue ramp beside it, a before/after swatch, a hex field, and numeric channels in whichever model the user thinks in: RGB, HSV or CMYK. Every channel is a line of its own: the letter, a rail showing what that channel does, and the number. Alpha, when it is offered, is one more such line.

Hue and saturation live on the widget rather than being re-derived from the colour. Grey has no hue and black has no saturation, so a colour alone cannot say where the cursor was: dragging the value down to black and back up would otherwise return red. What was last chosen survives the trip.

Alpha is a mode. setAlphaEnabled(boolean) removes the whole alpha line, rail and number together, and color() then always returns an opaque colour. Alpha is the last line, so losing it shortens the picker rather than rearranging it.

Two opposite answers about direction live in this one widget, and unifying them would be wrong. The rails are value axes laid along the reading axis, so their sweep, their thumb, their pointer and their horizontal arrows all turn round together when the picker reads right to left. The saturation/value field and the hue ramp do not turn round: the first is a colour space, whose white corner is a convention every artist has met and not a side of the page, and the second is vertical, which no direction touches. The ramp still changes sides, because the row holding it is what places it.

The answer is a display-range Color. An application authoring light rather than pixels needs a multiplier beside this widget, not inside it: the swatch cannot show what such a multiplier does, since normalising an over-range colour for display divides it straight back out.

Not a dialog; put it in one with Dialog.setContent(limn.scene.Widget<?>), or inline it in a panel. final, unlike most components here: it is a composite of four painted parts wired to one model, and a subclass would be overriding paint and layout it does not own. Compose one instead.

  • Constructor Details

    • ColorPicker

      public ColorPicker()
      A picker starting on opaque white, with the alpha line offered.
  • Method Details

    • color

      public Color color()
      The chosen colour. Opaque unless setAlphaEnabled(boolean) says otherwise.
    • setInitialColor

      public ColorPicker setInitialColor(Color color)
      Sets the colour and the "before" swatch: this is what opening the picker on an existing colour means. Use setColor(limn.graphics.Color) to move the selection without moving the comparison.
    • setColor

      public ColorPicker setColor(Color color)
      Moves the selection. Hue and saturation are taken from color only when it has them: a grey has no hue to read, and overwriting the current one with zero would swing the field to red for no reason the artist can see.
    • setAlphaEnabled

      public ColorPicker setAlphaEnabled(boolean enabled)
      Whether the picker offers alpha at all (default true). Turning it off hides the ramp and the field and makes color() opaque, for the many things being coloured that cannot be translucent.

      Announces RANGE/CODE for the flag, and before it, when turning alpha off moved the colour, VALUE/ADJUSTMENT: the colour moved as a consequence of the call, which named alpha and not the colour.

    • isAlphaEnabled

      public boolean isAlphaEnabled()
      Whether the alpha line is offered; when off, color() is always opaque.
    • setFormat

      public ColorPicker setFormat(ColorPicker.Format format)
      Which numeric model the channel rows show. The visual field is always HSV.

      The one entry point, whichever end it came from: a tab the user clicked arrives here through the picker's watcher on the pane, and this pushes the selection back the other way for a caller that set it. No guard is needed for the round trip: the format is written first, so the pane's announcement finds the picker already there and the watcher returns.

    • format

      public ColorPicker.Format format()
      The notation the channel rows are currently showing.
    • onChange

      public ColorPicker onChange(@Nullable Consumer<Color> listener)
      Fires on every move: a picker shows its answer live, it does not wait for OK. That makes a delivery a preview: see onCommit for the one that is a decision. Never for setColor(limn.graphics.Color), setInitialColor(limn.graphics.Color) or setAlphaEnabled(boolean), which are a caller's writes; to hear every move whatever caused it, watch the picker instead.
      Parameters:
      listener - the handler, or null to clear the slot
      Returns:
      this picker
      Throws:
      IllegalStateException - if a handler is already registered
    • onCommit

      public ColorPicker onCommit(@Nullable Consumer<Color> listener)
      Fires once with the settled colour when a drag ends, the difference between a preview and a decision, and the same split Slider.onChange/Slider.onCommit makes. This is where an undo entry is closed, or a value written to a document. A typed hex, a typed or stepped channel and a typed alpha commit too, because a key press is a whole gesture.
      Parameters:
      listener - the handler, or null to clear the slot
      Returns:
      this picker
      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<ColorPicker>
      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<ColorPicker>
    • 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<ColorPicker>
    • paintOutset

      protected float paintOutset()
      The 8 points the saturation/value cursor's ring can hang past the picker's box. The field's top edge is the picker's top and its leading edge the picker's leading edge, and the ring is centred on the exact colour, so parked on white, on the pure hue or anywhere along those two edges, half of it lies outside the picker. Declared so that a repaint pass landing only in that band, where a neighbour above or beside the picker repaints, still paints the picker rather than skipping it and wiping the ring's arc there.
      Overrides:
      paintOutset in class Widget<ColorPicker>
    • onKeyEvent

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

      protected void onAccessibility(Accessibility a)
      What the picker is to an assistive technology: one Accessible.Role.COLOR_CHOOSER node, and nothing else on it. Everything a reader can read or operate is a real widget under it with a node of its own — the hex field, the notation tabs, each channel's rail and number, the alpha line — so the chooser is the container those are read inside and not a second copy of what they say.

      The role is not optional here, and that is the whole reason this hook exists. The picker is focusable from its constructor, because the arrow keys walk the saturation/value plane, and a focusable widget survives the transparency predicate whatever it declares. One that declares no role is published as an unknown control and names this class in an application's log, which is a defect a toolkit class may not ship.

      No name is declared. The widget holds no title, no placeholder and no tooltip, so it has nothing to hand over: an application names it with setAccessibleName, and a picker put in a dialog is named by the dialog around it.

      And no value text holding the hex, which is the obvious thing to publish and is wrong twice over. A text with no number beside it is dropped by the builder, and a value change is raised only when a number moves, so such a text would cost a formatted string on every drag step and reach nobody; and a colour has no honest scalar to pair it with. The colour is readable where it is shown, in the hex field's own text and in the channel numbers, each of which is a node in its own right.

      No step verbs either. This widget's arrows move two axes — saturation across, value up — and one pair of verbs cannot say which; the plane they walk is the saturation/value field's box rather than this one, so a verb for it belongs on the node carrying that box.

      The box, the language, the children in tree order, the enabled, visible, showing, focusable and focused bits, and the focus and scroll-into-view verbs are all the walk's, and are correct without help: the root column is laid out over the whole of this widget, so every descendant's rectangle is inside the chooser's.

      Overrides:
      onAccessibility in class Widget<ColorPicker>
      Parameters:
      a - the node being described