From 059fbc6d08b6a327d1ec803a066279b42e6dd134 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Mon, 18 May 2026 05:33:53 +0300 Subject: [PATCH 1/3] Picker: support custom default date via setDefaultDate (#4973) Adds Picker.DateGetter plus setDefaultDate(DateGetter)/setDefaultDate(Date) so PICKER_TYPE_DATE, PICKER_TYPE_DATE_AND_TIME, and PICKER_TYPE_CALENDAR pickers can show a caller-supplied initial value (resolved lazily on each open) instead of always defaulting to `new Date()`. After a successful selection the picked value pins the picker and the getter is no longer consulted; clearing with setDate(null) (or switching off a date type) re-enables it. Custom lightweight popup buttons that call setDate during edit are snapshotted/rolled back on Cancel so default-getter behavior survives a cancelled action. getDate() returns the resolved default when no explicit value has been set so the UI and programmatic read agree. Behavior with no setDefaultDate call is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/com/codename1/ui/spinner/Picker.java | 172 +++++++++++++++++- 1 file changed, 171 insertions(+), 1 deletion(-) diff --git a/CodenameOne/src/com/codename1/ui/spinner/Picker.java b/CodenameOne/src/com/codename1/ui/spinner/Picker.java index ef9337a6c1..01ae305653 100644 --- a/CodenameOne/src/com/codename1/ui/spinner/Picker.java +++ b/CodenameOne/src/com/codename1/ui/spinner/Picker.java @@ -109,6 +109,19 @@ public class Picker extends Button { private Date startDate; private Date endDate; private VirtualInputDevice currentInput; + /// Supplies the initial date shown by the picker when no explicit value has + /// been set via `setDate(Date)`. Resolved lazily each time the picker is + /// opened (or `getDate()` is called against an unset picker) so it can + /// depend on state that changes at runtime - e.g. a reminder picker whose + /// default tracks "one hour before the current due date" can keep returning + /// fresh values as the due date moves. Null means fall back to `new Date()`. + private DateGetter defaultDateGetter; + /// True once the picker's date has been pinned by an explicit + /// `setDate(non-null)` call or a successful user selection. While false the + /// picker treats `value` as a placeholder and prefers `defaultDateGetter` + /// (when set) as the displayed value, so callers can install a default + /// after construction and still see it take effect. + private boolean dateValueExplicitlySet; // Variables to store the form's previous margins before showing // the popup dialog so that we can restore them when the popup is disposed. @@ -131,6 +144,27 @@ public class Picker extends Button { /// `getDate()` returns what the picker held before editing began. Non-null only /// while the popup is on screen. private Object preEditValue; + /// Companion snapshot of `dateValueExplicitlySet` paired with `preEditValue`. + /// A custom popup button that calls `setDate(...)` flips the flag to true; + /// if the user then cancels we restore the original flag value so the next + /// open re-resolves the configured default getter instead of being stuck on + /// the staged date. + private boolean preEditDateValueExplicitlySet; + + /// Functional interface that supplies the default `Date` to display when a + /// date-type picker is opened without an explicit value. Evaluated lazily + /// each time the default is needed so it can return a value that depends + /// on state that changes after the picker is constructed (e.g. "one hour + /// before the current due date"). + /// + /// @see Picker#setDefaultDate(DateGetter) + /// @see Picker#setDefaultDate(Date) + public interface DateGetter { + /// Returns the default date the picker should show when no explicit + /// value has been set. Returning `null` falls back to the framework + /// default (`new Date()`). + Date get(); + } /// Placement options for custom lightweight popup buttons. public static final class LightweightPopupButtonPlacement { @@ -211,6 +245,14 @@ public void actionPerformed(ActionEvent evt) { evt.consume(); return; } + // For date-type pickers that haven't been pinned with setDate, fold the + // resolved default into `value` before any show path reads it. Both + // showInteractionDialog() and the native/heavyweight branches below + // consume `value` directly, so a single priming step here is enough. + // The flag stays false so a Cancel that doesn't commit a new value + // still lets the next open re-resolve the default (useful when the + // getter returns a moving target like "due date - 1 hour"). + applyDefaultDateIfNeeded(); if ((useLightweightPopup || !Display.getInstance().isNativePickerTypeSupported(type)) && isLightweightModeSupportedForType(type)) { showInteractionDialog(); evt.consume(); @@ -234,6 +276,7 @@ public void actionPerformed(ActionEvent evt) { Object val = Display.getInstance().showNativePicker(type, Picker.this, value, metaData); if (val != null) { value = val; + markDateExplicitlySetIfDateType(); updateValue(); } else { // cancel pressed. Don't send the rest of the events. @@ -291,6 +334,7 @@ public void actionPerformed(ActionEvent evt) { cld.set(Calendar.MONTH, ds.getCurrentMonth() - 1); cld.set(Calendar.YEAR, ds.getCurrentYear()); value = cld.getTime(); + dateValueExplicitlySet = true; } else { evt.consume(); } @@ -353,6 +397,7 @@ public void actionPerformed(ActionEvent evt) { } cld.set(Calendar.MINUTE, dts.getCurrentMinute()); value = cld.getTime(); + dateValueExplicitlySet = true; } else { evt.consume(); } @@ -505,11 +550,18 @@ private void endEditing(int command, InteractionDialog dlg, InternalPickerWidget // (e.g. a custom "+7 days" button) so getDate() returns the // value the picker held before editing began. value = preEditValue; + // Pair the rollback with the flag snapshot so a Cancel after a + // setDate-firing custom button doesn't permanently pin the + // date and silence future default-getter resolutions. + dateValueExplicitlySet = preEditDateValueExplicitlySet; preEditValue = null; + preEditDateValueExplicitlySet = false; updateValue(); } else { preEditValue = null; + preEditDateValueExplicitlySet = false; value = spinner.getValue(); + markDateExplicitlySetIfDateType(); updateValue(); // (x, y) = (-99, -99) signals the built-in action listner // to ignore this event and just propagage it to external @@ -567,6 +619,7 @@ private void showInteractionDialog() { // value; without this snapshot a Cancel after such a button would // leak the staged value into the picker (#4897 follow-up). preEditValue = value; + preEditDateValueExplicitlySet = dateValueExplicitlySet; final InteractionDialog dlg = new InteractionDialog() { ActionListener keyListener; @@ -1202,7 +1255,11 @@ public void setType(int type) { case Display.PICKER_TYPE_DATE: case Display.PICKER_TYPE_DATE_AND_TIME: if (!(value instanceof Date)) { + // Switching from a non-date type drops whatever date the + // user pinned earlier, so clear the "explicit" flag too - + // a subsequent open should honor `defaultDateGetter` again. value = new Date(); + dateValueExplicitlySet = false; } break; case Display.PICKER_TYPE_STRINGS: @@ -1251,7 +1308,60 @@ private Object currentValue() { /// /// the date object public Date getDate() { - return (Date) currentValue(); + // If the popup is showing, the live wheel position wins regardless of + // whether a default is configured - the user is in the middle of + // editing and should see the in-flight value. + if (currentSpinner != null) { + return (Date) currentSpinner.getValue(); + } + // No explicit setDate yet -> let the default getter take over so the + // value returned here matches what the picker would show if opened now. + if (!dateValueExplicitlySet && defaultDateGetter != null) { + return resolveDefaultDate(); + } + return (Date) value; + } + + /// Primes `value` with the resolved default before a date-type picker is + /// shown, so the existing show paths (which read `value` directly) display + /// the configured default. Does nothing for non-date types, when the + /// caller has already pinned a date with `setDate(Date)`, or when no + /// default getter has been configured - in that last case the picker + /// keeps the legacy behavior of showing whatever was in `value` (typically + /// the construction-time `new Date()`). + private void applyDefaultDateIfNeeded() { + if (dateValueExplicitlySet || defaultDateGetter == null) { + return; + } + switch (type) { + case Display.PICKER_TYPE_DATE: + case Display.PICKER_TYPE_DATE_AND_TIME: + case Display.PICKER_TYPE_CALENDAR: + Date d = defaultDateGetter.get(); + if (d != null) { + value = d; + } + break; + default: + break; + } + } + + /// Marks the picker's date as explicitly chosen by the user once they have + /// successfully committed a value through any of the picker UIs. Pins the + /// default getter out so it doesn't keep overwriting the user's selection + /// on subsequent opens. Guarded on type so it doesn't pollute the flag for + /// non-date pickers (where the flag is unused anyway). + private void markDateExplicitlySetIfDateType() { + switch (type) { + case Display.PICKER_TYPE_DATE: + case Display.PICKER_TYPE_DATE_AND_TIME: + case Display.PICKER_TYPE_CALENDAR: + dateValueExplicitlySet = true; + break; + default: + break; + } } /// Sets the date, this value is used both for type date/date and time. Notice that this @@ -1272,12 +1382,72 @@ public Date getDate() { /// - `d`: the new date public void setDate(Date d) { value = d; + // A null clears the explicit value, restoring the default-date behavior + // (so a subsequent setDefaultDate / new Date() fallback takes effect again). + dateValueExplicitlySet = d != null; if (currentSpinner != null) { currentSpinner.setValue(d); } updateValue(); } + /// Installs a dynamic default for `PICKER_TYPE_DATE`, `PICKER_TYPE_DATE_AND_TIME`, + /// and `PICKER_TYPE_CALENDAR` pickers. The getter is consulted every time the + /// picker is opened or `getDate()` is called against a picker whose date has + /// not been pinned by `setDate(Date)`, so dependent defaults (e.g. a reminder + /// that should fire one hour before a separately-tracked due date) stay in + /// sync with the value they depend on. Passing `null` removes the default and + /// reverts to `new Date()`. + /// + /// #### Parameters + /// + /// - `getter`: the supplier of the default date, or `null` to restore the + /// framework default of `new Date()` + public void setDefaultDate(DateGetter getter) { + this.defaultDateGetter = getter; + updateValue(); + } + + /// Convenience overload that pins the picker's default to a fixed date. + /// Equivalent to passing a `DateGetter` that always returns `d`. Passing + /// `null` clears the default. + /// + /// #### Parameters + /// + /// - `d`: the fixed default date, or `null` to restore the framework default + public void setDefaultDate(final Date d) { + if (d == null) { + setDefaultDate((DateGetter) null); + return; + } + setDefaultDate(new DateGetter() { + @Override + public Date get() { + return d; + } + }); + } + + /// Returns the `DateGetter` previously installed via + /// `setDefaultDate(DateGetter)` (or the wrapper produced by the `Date` + /// overload), or `null` if no default has been configured. + public DateGetter getDefaultDate() { + return defaultDateGetter; + } + + /// Resolves the date to display when the picker is opened without an + /// explicit value: the configured default getter's result if non-null, + /// otherwise a fresh `new Date()`. + private Date resolveDefaultDate() { + if (defaultDateGetter != null) { + Date d = defaultDateGetter.get(); + if (d != null) { + return d; + } + } + return new Date(); + } + private String twoDigits(int i) { if (i < 10) { return "0" + i; From c6d0ff2351480d9ea3e729527bf3e99162807bf6 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Mon, 18 May 2026 06:30:36 +0300 Subject: [PATCH 2/3] Picker: replace anonymous DateGetter with named static class SpotBugs (SIC_INNER_SHOULD_BE_STATIC_ANON) flagged the anonymous DateGetter created by setDefaultDate(Date) - it captures no Picker state, so promoting it to a named static inner class avoids the hidden enclosing-Picker reference and clears the CI gate. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/com/codename1/ui/spinner/Picker.java | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/CodenameOne/src/com/codename1/ui/spinner/Picker.java b/CodenameOne/src/com/codename1/ui/spinner/Picker.java index 01ae305653..5c2abe6b54 100644 --- a/CodenameOne/src/com/codename1/ui/spinner/Picker.java +++ b/CodenameOne/src/com/codename1/ui/spinner/Picker.java @@ -1415,17 +1415,24 @@ public void setDefaultDate(DateGetter getter) { /// #### Parameters /// /// - `d`: the fixed default date, or `null` to restore the framework default - public void setDefaultDate(final Date d) { - if (d == null) { - setDefaultDate((DateGetter) null); - return; + public void setDefaultDate(Date d) { + setDefaultDate(d == null ? null : new FixedDateGetter(d)); + } + + /// Named static wrapper for the `setDefaultDate(Date)` overload. Static + /// (rather than an anonymous inner class) so it does not retain a hidden + /// reference to the enclosing `Picker`. + private static final class FixedDateGetter implements DateGetter { + private final Date date; + + FixedDateGetter(Date date) { + this.date = date; + } + + @Override + public Date get() { + return date; } - setDefaultDate(new DateGetter() { - @Override - public Date get() { - return d; - } - }); } /// Returns the `DateGetter` previously installed via From 5399c45d4e982dd7972d4d666645390ee62046df Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Mon, 18 May 2026 07:40:16 +0300 Subject: [PATCH 3/3] Picker: add unit tests for setDefaultDate (#4973) Covers: fixed default, dynamic getter re-evaluation, explicit setDate override, setDate(null) re-enables default, clearing the getter, getDefaultDate accessor, setType-off-and-back reset, DATE_AND_TIME support, null-returning getter falls back to new Date(), and the custom-popup-button cancel rollback path. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../ui/spinner/PickerDefaultDateTest.java | 240 ++++++++++++++++++ 1 file changed, 240 insertions(+) create mode 100644 maven/core-unittests/src/test/java/com/codename1/ui/spinner/PickerDefaultDateTest.java diff --git a/maven/core-unittests/src/test/java/com/codename1/ui/spinner/PickerDefaultDateTest.java b/maven/core-unittests/src/test/java/com/codename1/ui/spinner/PickerDefaultDateTest.java new file mode 100644 index 0000000000..16218ad921 --- /dev/null +++ b/maven/core-unittests/src/test/java/com/codename1/ui/spinner/PickerDefaultDateTest.java @@ -0,0 +1,240 @@ +package com.codename1.ui.spinner; + +import com.codename1.components.InteractionDialog; +import com.codename1.junit.FormTest; +import com.codename1.junit.UITestBase; +import com.codename1.testing.TestCodenameOneImplementation; +import com.codename1.ui.Button; +import com.codename1.ui.Component; +import com.codename1.ui.Container; +import com.codename1.ui.Display; +import com.codename1.ui.DisplayTest; +import com.codename1.ui.Form; +import com.codename1.ui.layouts.BoxLayout; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.Calendar; +import java.util.Date; + +/// Covers Picker.setDefaultDate / DateGetter wiring (RFE #4973). +public class PickerDefaultDateTest extends UITestBase { + + private static Date date(int year, int month, int day) { + Calendar c = Calendar.getInstance(); + c.clear(); + c.set(year, month, day, 0, 0, 0); + return c.getTime(); + } + + @Test + public void fixedDefaultDateAppliesWhenNoSetDateCalled() { + Picker p = new Picker(); + p.setType(Display.PICKER_TYPE_DATE); + Date fixed = date(2030, Calendar.JUNE, 15); + p.setDefaultDate(fixed); + Assertions.assertEquals(fixed, p.getDate(), + "Default date should be returned when no explicit setDate has been made"); + } + + @Test + public void dynamicDefaultDateReEvaluatesEachRead() { + Picker p = new Picker(); + p.setType(Display.PICKER_TYPE_DATE); + final Date[] slot = new Date[] { date(2030, Calendar.JANUARY, 1) }; + p.setDefaultDate(new Picker.DateGetter() { + @Override + public Date get() { + return slot[0]; + } + }); + Assertions.assertEquals(slot[0], p.getDate(), "First read should reflect initial getter value"); + slot[0] = date(2031, Calendar.FEBRUARY, 2); + Assertions.assertEquals(slot[0], p.getDate(), + "Second read should reflect the getter's updated value (lazy evaluation)"); + } + + @Test + public void explicitSetDateOverridesDefault() { + Picker p = new Picker(); + p.setType(Display.PICKER_TYPE_DATE); + p.setDefaultDate(date(2030, Calendar.JUNE, 15)); + Date pinned = date(2028, Calendar.MARCH, 20); + p.setDate(pinned); + Assertions.assertEquals(pinned, p.getDate(), + "Once a date is explicitly pinned, the default getter must not be consulted"); + } + + @Test + public void setDateNullReEnablesDefault() { + Picker p = new Picker(); + p.setType(Display.PICKER_TYPE_DATE); + Date defaultDate = date(2030, Calendar.JUNE, 15); + p.setDefaultDate(defaultDate); + p.setDate(date(2028, Calendar.MARCH, 20)); + p.setDate(null); + Assertions.assertEquals(defaultDate, p.getDate(), + "setDate(null) should clear the explicit pin and restore the default"); + } + + @Test + public void clearingDefaultGetterFallsBackToValueField() { + Picker p = new Picker(); + p.setType(Display.PICKER_TYPE_DATE); + p.setDefaultDate(date(2030, Calendar.JUNE, 15)); + p.setDefaultDate((Picker.DateGetter) null); + Assertions.assertNull(p.getDefaultDate(), + "getDefaultDate should return null after clearing"); + Assertions.assertNotNull(p.getDate(), + "Clearing the default should leave the picker with a usable (non-null) date"); + } + + @Test + public void getDefaultDateReturnsInstalledGetter() { + Picker p = new Picker(); + p.setType(Display.PICKER_TYPE_DATE); + Picker.DateGetter getter = new Picker.DateGetter() { + @Override + public Date get() { + return date(2030, Calendar.JUNE, 15); + } + }; + p.setDefaultDate(getter); + Assertions.assertSame(getter, p.getDefaultDate(), + "getDefaultDate should return the exact getter installed via setDefaultDate(DateGetter)"); + } + + @Test + public void setTypeOffDateTypeAndBackRestoresDefault() { + Picker p = new Picker(); + p.setType(Display.PICKER_TYPE_DATE); + Date defaultDate = date(2030, Calendar.JUNE, 15); + p.setDefaultDate(defaultDate); + p.setDate(date(2028, Calendar.MARCH, 20)); + p.setType(Display.PICKER_TYPE_TIME); + p.setType(Display.PICKER_TYPE_DATE); + Assertions.assertEquals(defaultDate, p.getDate(), + "Switching off a date type drops the pinned date, so the default should apply again"); + } + + @Test + public void defaultAppliesToDateAndTimeType() { + Picker p = new Picker(); + p.setType(Display.PICKER_TYPE_DATE_AND_TIME); + Date fixed = date(2030, Calendar.JUNE, 15); + p.setDefaultDate(fixed); + Assertions.assertEquals(fixed, p.getDate(), + "Default should apply to PICKER_TYPE_DATE_AND_TIME as well"); + } + + @Test + public void getterReturningNullFallsBackToNewDate() { + Picker p = new Picker(); + p.setType(Display.PICKER_TYPE_DATE); + p.setDefaultDate(new Picker.DateGetter() { + @Override + public Date get() { + return null; + } + }); + Assertions.assertNotNull(p.getDate(), + "A getter that returns null must fall back to a fresh Date, never null"); + } + + @FormTest + public void cancelAfterCustomButtonRollsBackExplicitFlag() { + TestCodenameOneImplementation impl = TestCodenameOneImplementation.getInstance(); + if (impl != null) { + impl.setTablet(false); + } + final Picker picker = new Picker(); + picker.setType(Display.PICKER_TYPE_DATE); + picker.setUseLightweightPopup(true); + final Date defaultDate = date(2030, Calendar.JUNE, 15); + picker.setDefaultDate(defaultDate); + // Custom button simulates the "+7 days" case from the RFE - calls setDate + // mid-edit, which would normally pin the date. A subsequent Cancel must + // roll the picker back so the default getter is consulted again. + picker.addLightweightPopupButton("Shift", new Runnable() { + @Override + public void run() { + picker.setDate(date(2031, Calendar.FEBRUARY, 2)); + } + }); + Form f = new Form(new BoxLayout(BoxLayout.Y_AXIS)); + f.add(picker); + f.show(); + + picker.pressed(); + picker.released(); + runAnimations(f); + + InteractionDialog dlg = findInteractionDialog(f); + Assertions.assertNotNull(dlg, "Lightweight popup should be open"); + Button shift = findButtonWithText(dlg, "Shift"); + Assertions.assertNotNull(shift, "Custom button should be present"); + shift.pressed(); + shift.released(); + DisplayTest.flushEdt(); + runAnimations(f); + + Button cancel = findButtonWithText(dlg, "Cancel"); + Assertions.assertNotNull(cancel, "Cancel button should be present"); + cancel.pressed(); + cancel.released(); + DisplayTest.flushEdt(); + runAnimations(f); + + Assertions.assertEquals(defaultDate, picker.getDate(), + "Cancel after a custom-button setDate must restore the default-getter behavior"); + } + + private void runAnimations(Form f) { + long start = System.currentTimeMillis(); + while (System.currentTimeMillis() - start < 400) { + f.animate(); + f.layoutContainer(); + DisplayTest.flushEdt(); + f.revalidate(); + try { + Thread.sleep(10); + } catch (InterruptedException ignored) { + // ignored + } + } + } + + private InteractionDialog findInteractionDialog(Form f) { + InteractionDialog dlg = findInteractionDialog(f.getLayeredPane()); + if (dlg != null) return dlg; + return findInteractionDialog((Container) f); + } + + private InteractionDialog findInteractionDialog(Container c) { + for (int i = 0; i < c.getComponentCount(); i++) { + Component child = c.getComponentAt(i); + if (child instanceof InteractionDialog) { + return (InteractionDialog) child; + } + if (child instanceof Container) { + InteractionDialog found = findInteractionDialog((Container) child); + if (found != null) return found; + } + } + return null; + } + + private Button findButtonWithText(Container container, String text) { + for (int i = 0; i < container.getComponentCount(); i++) { + Component c = container.getComponentAt(i); + if (c instanceof Button && text.equals(((Button) c).getText())) { + return (Button) c; + } + if (c instanceof Container) { + Button b = findButtonWithText((Container) c, text); + if (b != null) return b; + } + } + return null; + } +}