Class ColorPickerButton

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

public final class ColorPickerButton extends Widget<ColorPickerButton>
A colour, shown as a chip, that opens a ColorPicker in a dialog when clicked: the control an application reaches for when it needs a colour from the user but cannot give up a panel to a whole picker.

 ColorPickerButton accent = new ColorPickerButton(Color.rgb(0xF59E0B));
 accent.onChange(colour -> { shape.setFill(colour); shape.invalidate(); });
 

Cancel is a change, not a silence. The picker updates the chip and calls onChange on every move, so an application sees the colour live; dismissing the dialog puts the previous colour back and reports that as a change too. The button's own color() is therefore always the answer, and a caller that simply applies what it is handed is correct with no bookkeeping of its own, which is the whole reason the listener fires on the way back. Keep the listener cheap: it runs on every frame of a drag.

The caption defaults to the colour's hex and follows it. setText(java.lang.String) replaces it with anything else, including nothing: a button with an empty caption is chrome and a chip, which is the form a dense inspector column wants. It is then square, as a Button holding only an icon is: as wide as it is tall, with the chip in the middle, and Widget.setMinWidth(float) or Widget.setMaxWidth(float) make it wider or narrower. In a wider box the chip keeps the leading edge, as far from it as from the top of the square; in a narrower one it is in the middle; in a taller one, which a row stretching its children makes, it is as far from the leading edge as in the square and in the middle top to bottom.

Not a Button subclass: what it draws is a value, not a label, and the two disagree about almost every line of paint. It behaves like one (hover, focus ring, Enter and Space) because a control that opens a dialog should.

Reading right to left, the chip and its caption swap sides together: the chip is the leading item of the pair and the caption follows it inwards. Nothing else moves. The box is the same size in both directions, the chip is the same square, and the colour inside it has no reading axis of its own.

  • Constructor Details

    • ColorPickerButton

      public ColorPickerButton()
      A button on opaque white.
    • ColorPickerButton

      public ColorPickerButton(Color initial)
      A button on initial.
  • Method Details

    • color

      public Color color()
      The colour on the chip. Opaque unless setAlphaEnabled(boolean) says otherwise.
    • setColor

      public ColorPickerButton setColor(Color value)
      Sets the colour: a caller's write, announced as VALUE/CODE and reaching no handler, so a listener that writes back does not loop. The picker's moves and a cancelled dialog's restore are the user's and take the other origin through the same seam.
    • setAlphaEnabled

      public ColorPickerButton setAlphaEnabled(boolean enabled)
      Whether the picker offers alpha at all (default true). Turning it off makes color() opaque immediately, and drops the alpha line from the dialog, for the many things being coloured that cannot be translucent.
    • isAlphaEnabled

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

      public ColorPickerButton onChange(@Nullable Consumer<Color> listener)
      The application's response to the user moving the colour: on every move of the picker, and once more with the previous colour if the dialog is dismissed. Never for setColor(limn.graphics.Color); to hear every change whatever caused it, watch the button instead.
      Parameters:
      listener - the handler, or null to clear the slot
      Returns:
      this button
      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<ColorPickerButton>
      Parameters:
      aspect - what changed; the origin is USER or this is not called
    • text

      public String text()
      The caption as it currently reads: the colour's hex unless one was set.
    • setText

      public ColorPickerButton setText(String value)
      Replaces the caption with a fixed string; "" leaves chrome and the chip.
    • setText

      public ColorPickerButton setText(I18nString value)
      Replaces the caption with a value that follows the UI language.
    • setTextFromColor

      public ColorPickerButton setTextFromColor()
      Puts the caption back to the colour's hex, which is where it starts.
    • setDialogTitle

      public ColorPickerButton setDialogTitle(I18nString value)
      The dialog's title; "Colour" in the UI language unless set.
    • isPickerOpen

      public boolean isPickerOpen()
      Whether the picker dialog is up; a second click while it is does nothing.
    • openPicker

      public void openPicker()
      Raises the picker, as a click would. Public so a menu item or a keyboard shortcut elsewhere can open the same dialog this button opens.
      Throws:
      IllegalStateException - if this button is not in a scene
    • setPickerDisplayMode

      public ColorPickerButton setPickerDisplayMode(DisplayMode mode)
      Asks for the picker to be raised in a window of its own or as an overlay inside this button's window; see DisplayMode. Default DisplayMode.NATIVE_WINDOW.

      The reason to choose IN_SCENE here is usually not the platform. A colour picker is the one dialog whose whole job is to change what is behind it, so an application that shows the result live (a theme editor, a drawing tool) wants the picker and the thing it is recolouring in one window and one screenshot. A native window is also invisible to anything that captures the owner window, which is why the toolkit's own gallery raises this one in scene.

      A preference, not a guarantee, and the dialog's own: over a fullscreen window the picker is drawn in scene whatever was asked, for the reasons DisplayMode gives: an owner fullscreen at a named mode is minimized when the picker's window takes the focus, one at the mode the display is in already covers the screen, and on a Mac without a GPU the picker held the UI for 1.4 to 3.7 seconds as a window, against 0.6 to 0.7 in scene. A picker already up in a window moves into the scene when the owner goes fullscreen, the colour it holds unchanged. pickerDisplayMode() answers what happened.

      Takes effect on the next openPicker(); a picker already up is not moved by it.

      See Also:
    • pickerDisplayMode

      public DisplayMode pickerDisplayMode()
      Returns:
      how the picker is presented: while it is up, what its dialog actually chose, including a fullscreen owner forcing IN_SCENE over a request for a window, at the open or later. While it is not, what the next one will be asked for, since the next openPicker() decides afresh over the window as it is then.
    • onMeasure

      protected Size onMeasure(Constraints constraints)
      What the shaper should fall back to for a caption with nothing strong in it, given the direction this pass already resolved.

      A fallback and not an imposition: the first-strong rule still decides everything a strong character can decide, so the default caption — a hex, which is Latin and digits — still reads left to right inside a right-to-left form, and only where it is placed changes. What this settles is the caption that says nothing either way, whose right answer is the direction of the interface around it.

      Takes the resolved direction rather than reading it again, so that one paint cannot place the caption for one direction and shape it for the other.

      Specified by:
      onMeasure in class Widget<ColorPickerButton>
    • baselineOffset

      protected float baselineOffset()
      The baseline BASELINE rows align on, the expression onPaint(limn.graphics.Canvas) draws with.
      Overrides:
      baselineOffset in class Widget<ColorPickerButton>
    • paintOutset

      protected float paintOutset()
      The focus ring is drawn outside the box; damage has to know. See Button.
      Overrides:
      paintOutset in class Widget<ColorPickerButton>
    • onPaint

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

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

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

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

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

      protected void onAccessibility(Accessibility a)
      What this button is to an assistive technology: one Accessible.Role.BUTTON carrying a popup, offering a press, and named by whichever of its two captions it is painting.

      A button and not a colour chooser. The chooser is the ColorPicker the press raises, which has a node of its own in whichever tree it is mounted in. What this widget does is open a dialog, which is what a button with a popup does.

      The name is the caption, and the caption is two different things. A caption set through setText(java.lang.String) is an I18nString this widget holds, so it is handed over by reference and the walk compares the reference, the language and the translation epoch. The default caption is the colour's hex, which has no source to compare because it is formatted, so it is handed over from hex() with the counter that memo was filled against. Both are Accessible.NameFrom.CONTENT: on the default path that is exact, since the hex is literally the painted caption, and with an empty caption it is the closest of the five, the same divergence the picker's own swatch and the split pane's divider already record. One provenance for both branches also keeps a setText("") and a setTextFromColor() from churning the attribute one platform writes the name into.

      An empty caption is still named, by the colour. setText("") is a supported configuration — chrome and a chip, which is what a dense inspector column wants — and the widget still paints the chip there, so the colour is its whole content. A focusable node with no name at all is the one thing the gallery check refuses, and this is the answer that costs no application a line.

      The hex moves to the description when a caption displaced it, and is published exactly once either way. What that costs, said plainly: a captioned button with a tooltip loses the tooltip, because the walk gives a tooltip to the description only when nothing else took it. The trade is deliberate — a caption already identifies the control, and the colour is published nowhere else in this widget's tree, so without it a reader of a captioned colour well can never learn the colour. An application that disagrees calls setAccessibleDescription, which is applied after this hook and wins.

      Accessible.State.HAS_POPUP always, and not only while the picker is up. It is the bit that tells a reader this is not a plain button before anything has happened, and a bit that moved with the dialog would republish this node twice per picker.

      No facet of any kind, and the survey's row is wrong to want one. "Value text is the hex" cannot be implemented: Accessibility.valueText(java.lang.String, long) writes the text and leaves the node without a number, the publish step builds a value facet only where a number is present, and the difference raises a value event only when a number moves — so such a text is dropped in silence on all three platforms. And there is no honest number to pair with it, because a colour has no scalar. The hex belongs in the name, which is where it is.

      No relation. Presented in scene, the dialog's overlay is a parentless root whose inheritance host is this button, so the walk publishes the popup relation there and its mirror here; declaring it again would put two of the same relation on this node, since relations are not de-duplicated. Presented in a window of its own — the default — the picker is in another scene and another tree, and the Dialog the survey's row names is not a widget at all, so a relation naming it could only ever be dropped.

      Declared here is all a reader is told. The box, the language, the enabled, visible, showing, focusable and focused bits and the focus and scroll-into-view verbs are the walk's; paintOutset() is how far the focus ring's ink reaches for damage and is never the operable rectangle. The armed visual is not published, for Button's reasons: no platform maps it, it would republish the tree twice per click, and a press from an assistive technology never arms. The chip is not a node either — the whole box is one hit target, a node for it would say what the name already says, and leaving it out keeps this hook free of Widget.layoutDirection() entirely while the box mirrors for free.

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

      protected boolean onAccessibilityAction(Accessible.Action verb, Accessible.Argument arg)
      Raises the picker an assistive technology asked for, through openPicker() — the same call a left click and an Enter or Space release make, so the dialog, its Cancel and OK, and the revert that reports the previous colour through onChange are all exactly what the pointer would have got.

      No enabled check of its own, unlike Button's. The node acted on here is this widget, so the scene's own gate has already walked this button and every ancestor for isEnabled(), checked that it is showing, that the window is not modal-blocked and that it is inside the layer that owns input. This widget's click and key paths rely on exactly that gate and carry no guard either, and a reader's press that was stricter than the pointer's would be a second rule to keep in step.

      The refusal while a picker is already up is about truthfulness rather than safety: openPicker() returns silently in that case, and a true here is published as an invocation, so acknowledging a press that did nothing would tell a reader something happened. The scene's own reachability test refuses first in both presentations — in scene the dialog's overlay owns input, natively the owner window is modal-blocked — so this is the second line and not the first.

      Overrides:
      onAccessibilityAction in class Widget<ColorPickerButton>
      Parameters:
      verb - what was asked
      arg - ignored; a press carries none
      Returns:
      whether this widget did it