Class Card

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

public final class Card extends Widget<Card>
A card: a surface that holds one record, made of parts that are each optional. From the top, a picture (the media), a header of a title over a subtitle with a part before them and a part after them, the content, and a footer of actions at the trailing edge. A part that is not set, or is hidden, takes no room and leaves no gap.

 Card invoice = new Card(new Label("Three items, due on Friday.").setWrap(true))
         .setTitle("Harbor Lane Supplies")
         .setSubtitle("Invoice 1042")
         .setHeaderSuffix(new Label("$1,240.00").setStrong(true))
         .setFooter(new Button("Approve"));
 

Looks. Card.Style.ELEVATED, the default, lifts the card by the theme's Elevation.RAISED shadow, with a hairline round it on a dark palette, where a shadow on a dark ground does not show; Card.Style.OUTLINED draws a line round it and no shadow; Card.Style.FILLED sets it apart by tone alone; Card.Style.PLAIN draws nothing of its own, and its parts stand on the page. Card.Orientation.HORIZONTAL puts the media in a column at the leading side, beside the header and the content, with the footer across the whole card under both. The media is laid out as Card.MediaFit says, and may be an Icon, which the card draws itself, tinted with the accent when it is tintable.

Height. Given more height than it measures, as cards side by side in a row are, the card gives the extra to its content and keeps its footer at the bottom, so footers line up.

A card that is clicked. With onActivate(Runnable) the card is activated by a click anywhere outside its footer, and by Enter or Space while it holds the keyboard; the footer's actions are clicked as the buttons they are, and a press in the footer between them does nothing. A card with no footer is one click as a whole. The part of the card that is clicked takes the keyboard as one stop, shows the pointer's hover and press over itself alone, and is drawn apart from the footer by a line. It holds no control of its own: a widget that takes the keyboard (a button, a field, a check box) is refused in the media, the header and the content of a card that is activated, with an IllegalArgumentException from the setter or from onActivate; one that arrives later, inside a content the application changes, is logged once and answers nothing, and every press and key there stays the card's. Moved to the footer, taken out of the card, or left in a card that is no longer clicked, it answers again as it did, and the card keeps no hold on it. Actions go in the footer. A card that is not activated takes any widget in any part.

To a reader. A card with a title is a group named by its title, whose title is a heading; a card with none is transparent, as a Surface is, so a chart in a card is the chart. A card that is activated is a button named by its title and described by its subtitle, and what else is inside the part that is clicked is not read: a fact a reader needs from the content (an amount, a state) is said with Widget.setAccessibleDescription(String) on the card, which lands on that button, as Widget.setAccessibleName(String) does. With a footer, the button and the footer's actions are held in a group named by the title. An activated card with no title has no name unless the application gives it one.

Reading right to left the header's leading part, the horizontal media column and the rounded corners of a covering picture are on the right, and the footer's actions sit at the left.

  • Constructor Details

    • Card

      public Card()
      An empty card; every part is set afterwards.
    • Card

      public Card(Widget<?> content)
      A card holding content, every other part unset.
      Parameters:
      content - what the card holds
  • Method Details

    • setMedia

      public Card setMedia(@Nullable Widget<?> newMedia)
      Sets the media: a picture, a video, an avatar, any widget. It is laid out in the box Card.MediaFit gives it and told nothing else, so an ImageView that should crop is given ImageView.Fit.COVER by the caller. null takes it away, and so does an icon set in its place. UI thread only.
      Parameters:
      newMedia - the media, or null
      Returns:
      this card
      Throws:
      IllegalArgumentException - when newMedia is already in another container or holds this card, or when the card is activated and it takes the keyboard or holds a widget that does; the card is then left as it was
    • setMedia

      public Card setMedia(@Nullable Icon icon)
      Sets an icon as the media, drawn by the card as authored. A tintable icon is drawn in the accent. The icon is decoration to a reader, as a button's is. UI thread only.
      Parameters:
      icon - the icon, or null to take the media away
      Returns:
      this card
    • setMedia

      public Card setMedia(@Nullable Icon icon, Icon.Mirroring mirroring)
      Sets an icon as the media, saying whether its drawing turns around when the card reads right to left. UI thread only.
      Parameters:
      icon - the icon, or null to take the media away
      mirroring - Icon.Mirroring.NEVER unless the glyph means a direction
      Returns:
      this card
    • clearMedia

      public Card clearMedia()
      Takes the media away, as setMedia(null) would: that call does not compile, because null fits more than one of the overloads.
      Returns:
      this card
    • media

      public @Nullable Widget<?> media()
      Returns:
      the media widget, or null when there is none or the media is an icon
    • mediaIcon

      public @Nullable Icon mediaIcon()
      Returns:
      the icon drawn as the media, or null
    • setHeaderPrefix

      public Card setHeaderPrefix(@Nullable Widget<?> newPrefix)
      Sets the part before the title, spanning its two lines: an avatar, an icon. UI thread only.
      Parameters:
      newPrefix - the part, or null to take it away
      Returns:
      this card
      Throws:
      IllegalArgumentException - when the part is already in another container or holds this card, or when the card is activated and it takes the keyboard or holds a widget that does; the card is then left as it was
    • headerPrefix

      public @Nullable Widget<?> headerPrefix()
      Returns:
      the part before the title, or null
    • setTitle

      public Card setTitle(@Nullable String newTitle)
      Sets a fixed title: one line, or as many as setTitleLines(int) allows, strong, ending with an ellipsis where it does not fit.
      Parameters:
      newTitle - the title, or null to take it away
      Returns:
      this card
    • setTitle

      public Card setTitle(@Nullable I18nString newTitle)
      Sets a title that follows the UI language. It names the card to a reader. UI thread only.
      Parameters:
      newTitle - the title, or null to take it away
      Returns:
      this card
    • clearTitle

      public Card clearTitle()
      Takes the title away, as setTitle(null) would: that call does not compile, because null fits more than one of the overloads.
      Returns:
      this card
    • title

      public @Nullable String title()
      Returns:
      the title, resolved in this card's own language, or null
    • setSubtitle

      public Card setSubtitle(@Nullable String newSubtitle)
      Sets a fixed subtitle: one line, or as many as setSubtitleLines(int) allows, muted, under the title.
      Parameters:
      newSubtitle - the subtitle, or null to take it away
      Returns:
      this card
    • setSubtitle

      public Card setSubtitle(@Nullable I18nString newSubtitle)
      Sets a subtitle that follows the UI language. Without a title it is drawn where the title would be, muted, and names nothing. An activated card's subtitle is its description to a reader. UI thread only.
      Parameters:
      newSubtitle - the subtitle, or null to take it away
      Returns:
      this card
    • clearSubtitle

      public Card clearSubtitle()
      Takes the subtitle away, as setSubtitle(null) would: that call does not compile, because null fits more than one of the overloads.
      Returns:
      this card
    • subtitle

      public @Nullable String subtitle()
      Returns:
      the subtitle, resolved in this card's own language, or null
    • setTitleLines

      public Card setTitleLines(int lines)
      Lets the title break across up to lines lines, the last ending with an ellipsis when more of it remains; 1, the default, keeps it on one line. A long name then reads whole on a narrow card instead of being cut after a few letters. The header grows to the lines the title takes, and a reader still hears the whole title. UI thread only.
      Parameters:
      lines - the most lines, 1 or more
      Returns:
      this card
      Throws:
      IllegalArgumentException - when lines is below 1
    • titleLines

      public int titleLines()
      Returns:
      the most lines the title breaks across; see setTitleLines(int)
    • setSubtitleLines

      public Card setSubtitleLines(int lines)
      Lets the subtitle break across up to lines lines, as setTitleLines(int) does the title; 1, the default, keeps it on one line. UI thread only.
      Parameters:
      lines - the most lines, 1 or more
      Returns:
      this card
      Throws:
      IllegalArgumentException - when lines is below 1
    • subtitleLines

      public int subtitleLines()
      Returns:
      the most lines the subtitle breaks across; see setSubtitleLines(int)
    • setHeaderSuffix

      public Card setHeaderSuffix(@Nullable Widget<?> newSuffix)
      Sets the part after the title, spanning its two lines: a badge, an amount. UI thread only.
      Parameters:
      newSuffix - the part, or null to take it away
      Returns:
      this card
      Throws:
      IllegalArgumentException - when the part is already in another container or holds this card, or when the card is activated and it takes the keyboard or holds a widget that does; the card is then left as it was
    • headerSuffix

      public @Nullable Widget<?> headerSuffix()
      Returns:
      the part after the title, or null
    • setContent

      public Card setContent(@Nullable Widget<?> newContent)
      Sets what the card holds under its header: one widget, a column for several. It takes the height the card is given beyond what it measures. UI thread only.
      Parameters:
      newContent - the content, or null to take it away
      Returns:
      this card
      Throws:
      IllegalArgumentException - when the content is already in another container or holds this card, or when the card is activated and it takes the keyboard or holds a widget that does; the card is then left as it was
    • content

      public @Nullable Widget<?> content()
      Returns:
      the content, or null
    • setFooter

      public Card setFooter(Widget<?>... actions)
      Sets the footer's actions, which sit at the card's trailing edge and wrap to another line on a narrow card; the ones it had go back to the application. With no actions, it takes the footer away. Of an activated card, the footer is the part a click does not activate. UI thread only.
      Parameters:
      actions - the actions, in reading order: each in no container, or in this footer already
      Returns:
      this card
      Throws:
      NullPointerException - when actions or one of them is null
      IllegalArgumentException - when an action is already in another container, holds this card, or is passed twice; the footer is then left as it was
    • footer

      public List<Widget<?>> footer()
      Returns:
      the footer's actions, in reading order; read only
    • setStyle

      public Card setStyle(Card.Style newStyle)
      Sets how the card is set apart (default Card.Style.ELEVATED). UI thread only.
      Parameters:
      newStyle - the style
      Returns:
      this card
    • style

      public Card.Style style()
      Returns:
      how the card is set apart
    • setOrientation

      public Card setOrientation(Card.Orientation newOrientation)
      Sets where the media goes (default Card.Orientation.VERTICAL). UI thread only.
      Parameters:
      newOrientation - the orientation
      Returns:
      this card
    • orientation

      public Card.Orientation orientation()
      Returns:
      where the media goes
    • setMediaFit

      public Card setMediaFit(Card.MediaFit fit)
      Sets how the media is laid out (default Card.MediaFit.NATURAL). UI thread only.
      Parameters:
      fit - the fit
      Returns:
      this card
    • mediaFit

      public Card.MediaFit mediaFit()
      Returns:
      how the media is laid out
    • setMediaAspectRatio

      public Card setMediaAspectRatio(float widthOverHeight)
      Sets the media's width over its height where it stretches across a vertical card (Card.MediaFit.STRETCH and Card.MediaFit.COVER); 16 to 9 by default. UI thread only.
      Parameters:
      widthOverHeight - the ratio, above zero
      Returns:
      this card
      Throws:
      IllegalArgumentException - when not above zero or not finite
    • mediaAspectRatio

      public float mediaAspectRatio()
      Returns:
      the media's width over its height where it stretches across a vertical card
    • onActivate

      public Card onActivate(@Nullable Runnable handler)
      Makes the card activated by a click outside its footer, by Enter or Space while it holds the keyboard, and by a reader's press; null makes it a plain card again. A press is announced to the watchers as INVOKED first. Not called while the card is disabled. UI thread only.
      Parameters:
      handler - the handler, or null to clear the slot
      Returns:
      this card
      Throws:
      IllegalStateException - if a handler is already registered
      IllegalArgumentException - when the media, the header or the content holds a widget that takes the keyboard
    • isActivatable

      public boolean isActivatable()
      Returns:
      whether the card is activated by a click: whether onActivate(java.lang.Runnable) holds a handler
    • 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<Card>
      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<Card>
    • 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<Card>
    • 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<Card>
    • paintsDecoration

      protected boolean paintsDecoration()
      The fill and the shadow are background to the parts, and say nothing of themselves: a card with no title and no handler is not in the accessible tree.
      Overrides:
      paintsDecoration in class Widget<Card>
      Returns:
      true, always
    • onPaint

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

      protected void onPaintOverlay(Canvas canvas)
      The edge, over a picture that covers the card, and the line above an activated card's footer.
      Overrides:
      onPaintOverlay in class Widget<Card>
    • onAccessibility

      protected void onAccessibility(Accessibility a)
      A group named by the title, when there is a title and the card is not activated, or when it is activated and has a footer, whose actions the group holds beside the area's button; and nothing otherwise, the card's node being its area's button or none.
      Overrides:
      onAccessibility in class Widget<Card>
      Parameters:
      a - the node being described; write into it, and never keep it
    • accessibleLabelTarget

      protected Widget<?> accessibleLabelTarget()
      The area carries a caption bound to an activated card, and its name and description.
      Overrides:
      accessibleLabelTarget in class Widget<Card>
      Returns:
      the widget that carries a label bound to this one; this by default
      See Also: