Class DateField

java.lang.Object
limn.scene.Widget<DateField>
limn.components.date.DateField

public final class DateField extends Widget<DateField>
A date, a time, or both, typed into segments the language orders.

Two starts and one knob decide the shape: a field starts at the year or at the hour, and setGranularity(limn.components.date.DateField.Granularity) says how fine it goes.


 new DateField()                              // 31/12/2026   -- a date
 new DateField().setGranularity(MONTH)        // 12/2026      -- a month
 new DateField().setGranularity(MINUTE)       // 31/12/2026 14:30
 DateField.ofTime()                           // 14:30        -- a time of day
 DateField.ofTime().setGranularity(SECOND)    // 14:30:45
 new DatePicker()                             // the first, with a calendar to pick from
 

The segments and the separators are the locale's. They come from the language's own short pattern, so a Brazilian field reads day, month, year, an American one month, day, year, a Japanese one year, month, day, and a Korean one keeps the full stop after the last of them. A two-digit year in a pattern is widened to four here, because the field is edited and a two-digit year in something a person types is an ambiguity the toolkit would be creating on purpose.

The keyboard. Up and Down adjust the focused segment and roll over its bounds; Left and Right move between segments; Home and End go to the first and last; Delete and Backspace clear the focused segment. Typing digits fills the focused segment and moves on to the next when it can hold no more, which is what makes a date typable as a run of digits: 31122026 in a day-month-year language commits the last day of 2026 without a separator being typed. A separator typed anyway moves on, so a person who types the slashes is not fighting the field either. Ctrl/Cmd+C copies the whole value as shown and Ctrl/Cmd+V parses what is on the clipboard, which is where 2026-12-31 pasted out of a spreadsheet is understood.

An incomplete field has no value. date() answers null until every segment of the date is filled, and the field publishes itself invalid meanwhile. It never guesses the missing part: guessing is what makes a form submit a date nobody typed.

Bounds are held, not snapped. A date outside setMinDate(java.time.LocalDate)/setMaxDate(java.time.LocalDate) or refused by setDateFilter(java.util.function.Predicate<java.time.LocalDate>) stays on screen, the field publishes VALIDITY with a message saying which of the three it broke, and date() still answers what was typed. Snapping a typed date to the nearest legal one throws away what the person wrote and tells them nothing.

To a screen reader it is a group of spin buttons, one per editable segment, which is the shape Spinner publishes for its two arrows and for the same reason: the caret is in one segment at a time, and a single text field publishing the whole date gives a reader no way to say which part that is.

Reading right to left the segments do not reorder. A date is a run of numbers, and a run of numbers keeps its own left-to-right order inside a right-to-left line: that is the bidirectional algorithm's rule and not a convention of this toolkit, and it is the same reading Spinner documents for its clock. What mirrors is which side of the box the run sits against.

Like every widget, it is touched from the UI thread only: each method that changes it checks, and throws on any other thread.

  • Field Details

    • REFUSE_TWO_DIGIT_YEARS

      public static final int REFUSE_TWO_DIGIT_YEARS
      The value for setTwoDigitYearWindow(int) that turns the guess off: a two-digit year, pasted into the field or typed and left, is left blank and the field stays incomplete.
      See Also:
  • Constructor Details

    • DateField

      public DateField()
      A field that edits a date, down to the day.
  • Method Details

    • ofTime

      public static DateField ofTime()
      A field that edits a time of day, on the clock the language keeps: twenty-four hours in Portuguese and German, twelve and a day period in English and Korean. Down to the minute until setGranularity(limn.components.date.DateField.Granularity) says otherwise.
      Returns:
      the field
    • hasDate

      public boolean hasDate()
      Returns:
      whether this field carries date segments: it starts at the year
    • hasTime

      public boolean hasTime()
      Returns:
      whether this field carries time segments: it goes down to at least the hour
    • granularity

      public DateField.Granularity granularity()
      Returns:
      how fine this field goes; DateField.Granularity.DAY for a date, DateField.Granularity.MINUTE for a time
    • setGranularity

      public DateField setGranularity(DateField.Granularity level)
      How fine this field goes: which segment is its last.

      A field that starts at the year takes any level: DateField.Granularity.MONTH makes it a month picker with no day, DateField.Granularity.MINUTE adds a clock after the date. A field that starts at the hour takes DateField.Granularity.HOUR, DateField.Granularity.MINUTE or DateField.Granularity.SECOND and refuses the three date levels: it has no date to edit down to, and silently accepting the call would leave a field that shows nothing.

      Segments the new level no longer holds are emptied; the value follows (a date field made a month field answers the first of its month), and the change is announced as VALUE from code when it moved.

      Parameters:
      level - how fine to go
      Returns:
      this
      Throws:
      IllegalArgumentException - for a date level on a field built by ofTime()
    • date

      public @Nullable LocalDate date()
      Returns:
      the date, or null if this field has no date part or its segments are not all filled. A date outside the bounds is still answered here: the field holds what was typed and says separately that it is not acceptable (see isValid()). At a DateField.Granularity coarser than a day this is the first day of the period named — the last day for the end field of a DatePicker.ofRange()
    • time

      public @Nullable LocalTime time()
      Returns:
      the time of day, or null if this field has no time part or it is incomplete. At DateField.Granularity.HOUR the minute is zero, and for the end field of a period the last one of the hour, with the seconds to match
    • dateTime

      public @Nullable LocalDateTime dateTime()
      Returns:
      both halves as one value, or null unless this field carries both and both are complete. Never invented: a date-only field answers null here rather than midnight
    • setDate

      public DateField setDate(@Nullable LocalDate date)
      Sets the date, or clears it.
      Parameters:
      date - the date, or null to empty the date segments
      Returns:
      this
    • setTime

      public DateField setTime(@Nullable LocalTime time)
      Sets the time of day, or clears it.
      Parameters:
      time - the time, or null to empty the time segments
      Returns:
      this
    • setDateTime

      public DateField setDateTime(@Nullable LocalDateTime value)
      Sets both halves at once, which is one announcement rather than two: a caller writing a timestamp is making one change, and a watcher that saw two would see a moment that never existed between them.
      Parameters:
      value - the moment, or null to empty every segment
      Returns:
      this
    • clear

      public DateField clear()
      Clears every segment.
    • minDate

      public @Nullable LocalDate minDate()
      Returns:
      the earliest acceptable date, or null for no lower bound
    • setMinDate

      public DateField setMinDate(@Nullable LocalDate date)
      The earliest acceptable date. A typed date before it is held and published invalid, never snapped. A field coarser than a day names a month or a year, which is acceptable when any day of it is: a month field bounded at 15 September accepts September.
      Parameters:
      date - the bound, or null for none
      Returns:
      this
    • maxDate

      public @Nullable LocalDate maxDate()
      Returns:
      the latest acceptable date, or null for no upper bound
    • setMaxDate

      public DateField setMaxDate(@Nullable LocalDate date)
      The latest acceptable date; the mirror of setMinDate(java.time.LocalDate).
      Parameters:
      date - the bound, or null for none
      Returns:
      this
    • dateFilter

      public @Nullable Predicate<LocalDate> dateFilter()
      Returns:
      the filter, or null if every date inside the bounds is acceptable
    • setDateFilter

      public DateField setDateFilter(@Nullable Predicate<LocalDate> filter)
      Which dates are acceptable beyond the two bounds. Unlike the calendar's, this one is called when the value changes rather than once per painted cell, so it may be as expensive as an application's own validation is.
      Parameters:
      filter - answers whether a date is acceptable, or null to accept every date
      Returns:
      this
    • isValid

      public boolean isValid()
      Returns:
      whether what the field holds is complete and acceptable. An empty field is valid: a form decides whether a date is required, and a field that called itself invalid the moment it was created would put an error on every blank form
    • validationMessage

      public @Nullable String validationMessage()
      Returns:
      why the field is not valid, as it reads in the field's own language, or null when it is. This is what a screen reader is told after "invalid", and what an application shows beside the field if it does not have a message of its own; see validationMessageSource() for the value behind it
    • validationMessageSource

      public @Nullable I18nString validationMessageSource()
      Returns:
      the localizable value behind validationMessage(), or null when the field is valid
    • isEmpty

      public boolean isEmpty()
      Returns:
      whether every segment is empty
    • chronology

      public Chronology chronology()
      Returns:
      the calendar system the segments are shown in
    • setChronology

      public DateField setChronology(@Nullable Chronology chronology)
      Shows the segments in a calendar system of the application's choosing rather than the language's; the value exchanged stays ISO either way.
      Parameters:
      chronology - the calendar, or null to follow the language
      Returns:
      this
    • onChange

      public DateField onChange(@Nullable Runnable listener)
      The application's response to the user moving the value: the date, or the time of day in a field that has one. The handler reads what it needs, date(), time() or dateTime(); the date is null while the segments are incomplete, which is the state a person is in for most of the time they are typing.

      The user alone: setDate(java.time.LocalDate) from code never reaches it.

      Parameters:
      listener - what to run, or null to clear the slot
      Returns:
      this
      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<DateField>
      Parameters:
      aspect - what changed; the origin is USER or this is not called
    • text

      public String text()
      The whole value as it is drawn: what Ctrl+C copies. A reader is not handed this string; it is told the segments one at a time, each with its own value.
      Returns:
      the field's text, with its separators and its dashes
    • setClock

      public DateField setClock(@Nullable Clock newClock)
      Where this field reads today and now from, for the one thing it asks them: what an empty segment becomes on its first step. Default null, the system clock in the default zone read at each call, as LocalDate.now() and LocalTime.now() are.
      Parameters:
      newClock - the clock, or null for the system's
      Returns:
      this
    • twoDigitYearWindow

      public int twoDigitYearWindow()
      Returns:
      how many years back the window for a two-digit year starts; 80 unless changed
    • setTwoDigitYearWindow

      public DateField setTwoDigitYearWindow(int yearsBack)
      Which century a two-digit year means. A year written with two digits — pasted as 31/12/26, or typed as 26 and left with a Right or a Home — resolves into the hundred years that start yearsBack years before today by the field's clock: by default 80 back and 19 ahead, so in 2026 26 is 2026 and 85 is 1985. A calendar whose years carry their era (Reiwa 8) is not windowed: there a two-digit year is the whole year.

      REFUSE_TWO_DIGIT_YEARS turns the guess off: a two-digit year, pasted or typed and left, is left blank and the field stays incomplete. A typed one stayed what was typed until 2026-09-14, which called "31/12/26" the valid year 26.

      Parameters:
      yearsBack - how far back the window starts, 0 to 99, or REFUSE_TWO_DIGIT_YEARS
      Returns:
      this
    • focusedSegment

      public int focusedSegment()
      Returns:
      which segment the caret is in, counted over the editable segments in reading order
    • 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<DateField>
    • 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<DateField>
    • 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<DateField>
    • onPaint

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

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

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

      protected boolean acceptsTextInput()
      Description copied from class: Widget
      Whether this widget edits text and should drive the platform input method (IME): the scene enables the IME while such a widget holds focus and disables it otherwise, so composition never intercepts keys meant for non-text UI. Text widgets override this to true. Default: false.
      Overrides:
      acceptsTextInput in class Widget<DateField>
    • onPreedit

      protected void onPreedit(PreeditEvent event)
      Description copied from class: Widget
      In-progress IME composition ("preedit") for this widget while it is focused: the still-composing text, shown inline but not yet committed (the commit later arrives as Widget.onCharTyped(limn.scene.event.CharEvent)). Only widgets that accept text input receive it. Default: ignored.
      Overrides:
      onPreedit in class Widget<DateField>
    • caretRect

      protected @Nullable Rect caretRect()
      The box of the segment the caret is in, in scene coordinates: the band the paint fills behind it, which is this field's caret. The platform puts an input method's candidate window under this rectangle, so a digit composed through a Japanese or Chinese input method has its list of candidates under the segment it will land in, and follows the caret to the next segment when a full one moves it on. Without it the list stayed wherever the platform last had it: the window's corner, or the caret of whichever text field had the focus before.

      The field draws no composition, so there is no caret inside the segment to point at while one is up: the segment is where the committed digits go, and it is the whole answer.

      Walks the same composed run the paint, a click and the accessible tree walk, so reading right to left it moves with the run to the trailing edge, and the segment a reader is told the caret is in is the one the candidate window sits under.

      Overrides:
      caretRect in class Widget<DateField>
      Returns:
      the box, or null before the first layout and while no caret is shown
    • onMouseEvent

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

      protected void onCharTyped(CharEvent event)
      Description copied from class: Widget
      Committed text input (focused widget first, then ancestors).
      Overrides:
      onCharTyped in class Widget<DateField>
    • onCanEdit

      protected boolean onCanEdit(EditCommand command)
      Description copied from class: Widget
      Whether this widget can do an editing command now: copy with something selected, paste with something to paste, undo with something to take back. Scene.canEdit(limn.scene.EditCommand) asks the widget that has the keyboard and then each of its ancestors, and a menu's item of an editing role is enabled by the answer.

      A widget answers for the commands it takes from the keyboard, and does for the command what it does for the key.

      Overrides:
      onCanEdit in class Widget<DateField>
      Returns:
      false here: a widget edits nothing until it says so
    • onEdit

      protected boolean onEdit(EditCommand command)
      Description copied from class: Widget
      Does an editing command, as the user's own act. Called on the first widget, from the one with the keyboard upward, whose Widget.onCanEdit(limn.scene.EditCommand) answered true.
      Overrides:
      onEdit in class Widget<DateField>
      Returns:
      whether it was done
    • onKeyEvent

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

      protected void onAccessibility(Accessibility a)
      A group of spin buttons, one per editable segment.

      The group carries no value (the comment below says why) and the validity message when it has one; each segment carries its own name, its own bounds, its own value against its own range — or the empty word when nothing is typed there — and the two step verbs, which reach the same private path Up and Down reach.

      The group names itself with nothing: what a date field is for is the question the form asks, which only the application has, and this is a public widget an application names with setAccessibleName, a bound caption or a tooltip.

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

      protected boolean onAccessibilityAction(Accessible.Action action, Accessible.Argument arg)
      The calendar's open and close, for a field inside a picker: exactly the two verbs the tree publishes, one at a time by state, so a reader's Expand on an open field is refused rather than reported done while nothing happened.
      Overrides:
      onAccessibilityAction in class Widget<DateField>
      Parameters:
      action - what was asked; one of the seventeen, including the four that carry an argument
      arg - the argument, or Accessible.Argument.NONE
      Returns:
      whether this widget did it. false is the honest answer for a verb it does not offer.
    • onSyntheticAction

      protected boolean onSyntheticAction(long key, Accessible.Action action, Accessible.Argument arg)
      A segment's step or set, through the same path the keyboard takes: the verbs are the two keys they stand for, so an assistive technology rolls over the segment's bounds exactly as Up and Down do, and reaches the application's handler through the same USER seam.
      Overrides:
      onSyntheticAction in class Widget<DateField>
      Parameters:
      key - the key this widget gave that child when it declared it
      action - what was asked
      arg - the argument, or Accessible.Argument.NONE
      Returns:
      whether this widget did it