Class CalendarView

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

public final class CalendarView extends Widget<CalendarView>
A month at a time: six weeks of seven days, a header that names the month and pages it, and optionally a leading column of week numbers.

A widget in its own right and not a part of DatePicker, because a scheduling screen wants a calendar sitting in a panel rather than hidden in a popup. The picker uses this one; so can an application, with the same three lines.


 CalendarView calendar = new CalendarView()
         .setMinDate(LocalDate.now())
         .setDateFilter(day -> day.getDayOfWeek() != DayOfWeek.SATURDAY
                            && day.getDayOfWeek() != DayOfWeek.SUNDAY)
         .onSelect(() -> booking.setStart(calendar.selectedDate()));
 

The value is ISO and the calendar drawn is the reader's. Everything this class hands out and takes in is a LocalDate; the month names, the year number and the length of a month come from the chronology resolved for the widget's effective locale, which is ISO for every language until one carries a u-ca extension or an application calls setChronology(java.time.chrono.Chronology). A Thai user driving a Buddhist calendar picks a day out of a grid headed with a year 543 greater, and the application stores the ISO date it asked for.

Always six rows. A month needs four to six depending on where it starts, and a grid that changed height as it paged would resize the popup under the pointer and move the button the user was about to press. The leading and trailing cells are filled with the neighbouring months' days, drawn muted; they are selectable, and picking one pages the grid to that month.

The keyboard cursor is not the selection. Arrows move a cursor, Enter and Space commit it. A grid where arrowing selected would fire a form's handler seven times crossing a week, and would make a range impossible to build with the keyboard at all. Left and Right move by a day and mirror with the layout; Up and Down move by a week and never mirror; Home and End are the first and last day of the week; PageUp and PageDown page the month, and with Shift the year.

Bounds are enforced, not snapped. A day outside setMinDate(java.time.LocalDate)/setMaxDate(java.time.LocalDate), or refused by setDateFilter(java.util.function.Predicate<java.time.LocalDate>), is drawn disabled, refuses a click and Enter, and is published disabled without a select verb. The cursor stops on it rather than skipping it: a reader arrowing across the month then hears that the day is unavailable, where a skip would have left a hole nobody was told about.

To a screen reader this is a table: TABLE over ROWs of CELLs under a row of COLUMN_HEADERs, which are the four roles already mapped on all three platforms for the Table widget. A day cell is named with the whole date and not the bare number, because a cell heard on its own has to say what it is.

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

  • Constructor Details

    • CalendarView

      public CalendarView()
      A calendar showing this month, with nothing selected.

      "This month" is read once, from the system clock, at construction. A calendar is not a clock and does not repaint at midnight; an application that leaves one on screen across a day boundary and cares calls setVisibleMonth(java.time.LocalDate) when it notices.

  • Method Details

    • selectedDate

      public @Nullable LocalDate selectedDate()
      Returns:
      the selected day, or null if none is; always null in RANGE mode
    • setSelectedDate

      public CalendarView setSelectedDate(@Nullable LocalDate day)
      Selects a day, or clears the selection.

      Pages the grid to the month holding it, because a selection the user cannot see is a selection they have to go looking for. Selecting the day already selected changes nothing and announces nothing, which is the early return that keeps two calendars bound to each other from recursing.

      Parameters:
      day - the day, or null to clear
      Returns:
      this
    • selectedRange

      public @Nullable DateRange selectedRange()
      Returns:
      the selected period, or null if none is complete. A period being built — one end chosen and the other not — is not a period and is not answered here
    • setSelectedRange

      public CalendarView setSelectedRange(@Nullable DateRange range)
      Selects a period, or clears it. Pages the grid to the month holding its start.
      Parameters:
      range - the period, or null to clear
      Returns:
      this
    • focusedDate

      public @Nullable LocalDate focusedDate()
      Where the keyboard is, which is not the selection: arrows move this and Enter commits it, so a grid can be walked without firing a form's handler at every step.

      Public because it is announced: the ACTIVE aspect says the cursor moved, and a widget may not announce a state no accessor answers.

      Returns:
      the day the cursor is on, or null before the grid has had focus
    • selectionMode

      public CalendarView.SelectionMode selectionMode()
      Returns:
      what a click does; CalendarView.SelectionMode.SINGLE unless it was changed
    • setSelectionMode

      public CalendarView setSelectionMode(CalendarView.SelectionMode mode)
      Whether this calendar picks a day, a period, or nothing.

      Changing the mode drops whatever the other mode had selected, because a day is not a period and carrying one across as the other would be inventing a selection the user never made.

      Parameters:
      mode - the mode
      Returns:
      this
    • view

      public CalendarView.View view()
      Returns:
      whether the grid is showing days, months or years
    • setView

      public CalendarView setView(CalendarView.View wanted)
      Shows days, months or years. An application opens a birth-date picker on CalendarView.View.YEARS and saves its user four hundred presses.
      Parameters:
      wanted - what to show
      Returns:
      this
      Throws:
      IllegalArgumentException - for a view finer than the granularity(): a month picker has no days to show
    • granularity

      public CalendarView.View granularity()
      Returns:
      the finest view this calendar picks in; CalendarView.View.DAYS unless it was changed
    • setGranularity

      public CalendarView setGranularity(CalendarView.View level)
      What this calendar picks: a day, a month or a year.

      A CalendarView.View.MONTHS calendar is a month picker: it opens on the twelve months, a pick there is the selection (the first day of the month, as an ISO date) rather than a step down to days it cannot pick, and the title climbs only to the years and back. A CalendarView.View.YEARS calendar is a year picker the same way. A period at either level runs from the first day of its first month or year to the last day of its last, so a range of March to June answers 1 March to 30 June.

      Takes a CalendarView.View rather than the field's finer list on purpose: an hour is nothing a grid can show, so an hour granularity here does not compile. DatePicker converts.

      Changing the level drops the selection, for setSelectionMode(limn.components.date.CalendarView.SelectionMode)'s reason: a day is not a month, and carrying one across as the other would be inventing a choice. A view finer than the new level moves up to it, announced as VALUE with the origin ADJUSTMENT: the calendar moved it, because the level moved.

      Parameters:
      level - the finest view
      Returns:
      this
    • visibleMonth

      public LocalDate visibleMonth()
      Returns:
      the ISO date of the first day of the month currently drawn, in the calendar being drawn: for a Hijri grid that is the first of the Hijri month, which is most often the middle of an ISO one
    • setVisibleMonth

      public CalendarView setVisibleMonth(LocalDate day)
      Pages the grid to the month holding a day.
      Parameters:
      day - any day in the wanted month
      Returns:
      this
    • setClock

      public CalendarView setClock(@Nullable Clock newClock)
      Where this calendar reads today from: the ring on today's cell, the "today" a reader hears on it, and the month it opens on while nobody has chosen one.

      Default null: the system clock in the default zone, read at each call, exactly as LocalDate.now(). A fixed clock is for whatever has to look the same tomorrow as it does today -- a capture, a test, a screenshot in a document -- and for an application that keeps its own idea of the date, a server's or a simulated one.

      A month already chosen, by setVisibleMonth(java.time.LocalDate), setSelectedDate(java.time.LocalDate) or the person paging, stays where it is; only the month construction guessed from the wall clock follows the new one.

      Parameters:
      newClock - the clock, or null for the system's
      Returns:
      this
    • minDate

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

      public CalendarView setMinDate(@Nullable LocalDate day)
      The earliest selectable day. A day before it is drawn disabled, refuses a click and carries no select verb; it is not hidden, because a month with half its days missing reads as a broken grid rather than as a bounded one.
      Parameters:
      day - the bound, or null for none
      Returns:
      this
    • maxDate

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

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

      public @Nullable Predicate<LocalDate> dateFilter()
      Returns:
      the filter, or null if every day inside the bounds may be picked
    • setDateFilter

      public CalendarView setDateFilter(@Nullable Predicate<LocalDate> filter)
      Which days may be picked, beyond the two bounds: weekends out, blackout dates out, only the days a resource is free.

      Called during paint, once per visible cell. It must be cheap and it must be pure: a filter that queries a database is a filter that stalls a frame, and one that answers differently for the same day makes the grid flicker between two pictures of itself.

      Announced as RANGE/CODE when it is a different filter; handing the same one again is no change. Two different filters that happen to answer alike are still announced: the calendar cannot know that without asking every day there is.

      Parameters:
      filter - answers whether a day may be picked, or null to allow every day
      Returns:
      this
    • setDayMarks

      public CalendarView setDayMarks(@Nullable Function<LocalDate,@Nullable DayMark> marks)
      What to draw under a day beyond its number: a holiday, a deadline, a day with appointments.

      Called during paint, once per visible cell, under the same two obligations setDateFilter(java.util.function.Predicate<java.time.LocalDate>) carries.

      Parameters:
      marks - answers a DayMark for a day, or null for none; may itself return null for a day with nothing to say
      Returns:
      this
    • showsWeekNumbers

      public boolean showsWeekNumbers()
      Returns:
      whether the week-number column is shown
    • setShowWeekNumbers

      public CalendarView setShowWeekNumbers(boolean show)
      Shows a leading column of week numbers, in the numbering this language uses — which is not ISO's everywhere, and deliberately so: Germany counts a week as belonging to the year holding four of its days and the United States to the year holding its first, so the same week at the turn of a year has two right answers and this shows each reader theirs. A language named without a region ("de") is read for the region it most likely means, and a first day set with setFirstDayOfWeek(java.time.DayOfWeek) is where the numbered weeks start too.
      Parameters:
      show - whether to show the column
      Returns:
      this
    • chronology

      public Chronology chronology()
      Returns:
      the calendar system being drawn, resolved from the locale unless one was declared
    • setChronology

      public CalendarView setChronology(@Nullable Chronology chronology)
      Draws the grid in a calendar system of the application's choosing rather than the language's.

      Rarely wanted, and worth having: an application whose domain is a calendar — an Islamic finance ledger, a Japanese government form — needs the calendar its documents are written in whatever language the interface is set to. The value exchanged is ISO either way.

      Parameters:
      chronology - the calendar to draw, or null to follow the language again
      Returns:
      this
    • firstDayOfWeek

      public DayOfWeek firstDayOfWeek()
      Returns:
      the day the drawn week starts on
    • setFirstDayOfWeek

      public CalendarView setFirstDayOfWeek(@Nullable DayOfWeek day)
      Starts the week on a day of the application's choosing rather than the language's: Monday in Germany, Sunday in Brazil and Saturday in Egypt are what the locale answers, and an application whose own week is fixed by regulation says so here.
      Parameters:
      day - the first day of a week, or null to follow the language
      Returns:
      this
    • onSelect

      public CalendarView onSelect(@Nullable Runnable listener)
      The application's response to the user picking: a day in CalendarView.SelectionMode.SINGLE, a closed period in CalendarView.SelectionMode.RANGE. The first click of a period reaches nothing: a range with one end is not a range. The handler reads what it needs, selectedDate() or selectedRange(), as every list-shaped widget's does.

      The user alone: a setSelectedDate(java.time.LocalDate) from code never reaches it. Code that wants every change, whoever made it, watches the widget with Widget.observeChanges(limn.scene.ChangeObserver).

      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)
      The handler, reached for a day in SINGLE and a closed period in RANGE.

      Chains to super for every other aspect: that call is the link a subclass must not break.

      Overrides:
      handleUserChange in class Widget<CalendarView>
      Parameters:
      aspect - what changed; the origin is USER or this is not called
    • isSelectable

      public boolean isSelectable(@Nullable LocalDate day)
      Whether a day may be picked: inside both bounds and not refused by the filter.
      Parameters:
      day - the day
      Returns:
      whether a click on it would do anything
    • onMeasure

      protected Size onMeasure(Constraints constraints)
      A square day cell at the step's control height, so a calendar and a text field at the same density agree about how tall a row of anything is, and seven of them plus the week column is the natural width. The title is measured too: a long month name at a large step is wider than seven cells in several languages, and a header clipped to the grid would cut it.
      Specified by:
      onMeasure in class Widget<CalendarView>
    • 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<CalendarView>
    • 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<CalendarView>
    • onPaint

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

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

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

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

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

      protected void onAccessibility(Accessibility a)
      The grid as a table, which is what it is: TABLE carrying six rows and seven columns (eight with week numbers), a header row of COLUMN_HEADERs, one ROW per week and one CELL per day, plus the two paging buttons and the title, a BUTTON that climbs to the month and year choosers — the same TABLE, three rows of four months or six of four years, each cell carrying SELECT and FOCUS where it leads somewhere (describeChooser(limn.accessibility.Accessibility, boolean)).

      Every role here was already mapped for the Table widget, three weeks before this widget existed, and that is the whole accessibility cost of a calendar: no role is added to the model, no facet, and no bridge code in limn-backend-lwjgl, where the three bridges live.

      A cell is named with the whole date. "9 September 2026" and not "9": a reader arrowing into a cell hears the cell, and the column head says only which weekday it is under. The strings come from the memo the layout filled, so a frame damaged by a hover allocates nothing here — except the full dates, which are built in this hook because they are needed nowhere else and building 42 of them per layout would be worse.

      Today carries a word of its own, because today is drawn as a ring and nothing else says so, which makes it exactly the kind of fact that reaches everyone who looks and nobody who listens.

      The pointer's hover is deliberately not published: a fact derived from it would republish the whole tree on every mouse move.

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

      protected boolean onSyntheticAction(long key, Accessible.Action action, Accessible.Argument arg)
      Picking a day and paging the month, through the same private paths a click takes: an assistive technology's select reaches pick(java.time.LocalDate, limn.scene.Change.Origin) exactly as a click does, so it refuses an unselectable day the same way, pages the same way and reaches the handler through the same USER seam. FOCUS on a day or a chooser cell takes the path an arrow takes instead, and moves only the cursor.

      No enabled guard of its own beyond the two the paths already carry: the scene's dispatcher has walked this widget and every ancestor for the enabled flag, refused an owner that is not showing and refused a modal-blocked window before this runs.

      Overrides:
      onSyntheticAction in class Widget<CalendarView>
      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