diff --git a/CodenameOne/src/com/codename1/ui/spinner/Picker.java b/CodenameOne/src/com/codename1/ui/spinner/Picker.java index cc50d54501..542226b878 100644 --- a/CodenameOne/src/com/codename1/ui/spinner/Picker.java +++ b/CodenameOne/src/com/codename1/ui/spinner/Picker.java @@ -45,6 +45,7 @@ import com.codename1.ui.events.ActionListener; import com.codename1.ui.layouts.BorderLayout; import com.codename1.ui.layouts.BoxLayout; +import com.codename1.ui.layouts.FlowLayout; import com.codename1.ui.layouts.GridLayout; import com.codename1.ui.list.DefaultListModel; import com.codename1.ui.plaf.Border; @@ -781,7 +782,11 @@ public void actionPerformed(ActionEvent evt) { switch (entry.alignment) { case Component.CENTER: if (center == null) { - center = new Container(BoxLayout.x()); + // FlowLayout(CENTER) so the buttons actually center inside + // the BorderLayout.CENTER slot below; BoxLayout.x() would + // pack them at the slot's left edge, which is the bug from + // https://github.com/codenameone/CodenameOne/issues/4819. + center = new Container(new FlowLayout(Component.CENTER, Component.CENTER)); $(center).selectAllStyles().setMargin(0).setPadding(0).setBorder(Border.createEmpty()).setBgTransparency(0); } center.add(button); diff --git a/scripts/android/screenshots/LightweightPickerButtons_above_center.png b/scripts/android/screenshots/LightweightPickerButtons_above_center.png new file mode 100644 index 0000000000..77525ff127 Binary files /dev/null and b/scripts/android/screenshots/LightweightPickerButtons_above_center.png differ diff --git a/scripts/android/screenshots/LightweightPickerButtons_below_right.png b/scripts/android/screenshots/LightweightPickerButtons_below_right.png new file mode 100644 index 0000000000..478e7795ac Binary files /dev/null and b/scripts/android/screenshots/LightweightPickerButtons_below_right.png differ diff --git a/scripts/android/screenshots/LightweightPickerButtons_between_mixed.png b/scripts/android/screenshots/LightweightPickerButtons_between_mixed.png new file mode 100644 index 0000000000..40daec275b Binary files /dev/null and b/scripts/android/screenshots/LightweightPickerButtons_between_mixed.png differ diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/LightweightPickerButtonsScreenshotTest.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/LightweightPickerButtonsScreenshotTest.java index b6644caa39..4646fd3a19 100644 --- a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/LightweightPickerButtonsScreenshotTest.java +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/LightweightPickerButtonsScreenshotTest.java @@ -1,54 +1,166 @@ package com.codenameone.examples.hellocodenameone.tests; +import com.codename1.ui.Component; import com.codename1.ui.Display; import com.codename1.ui.Form; import com.codename1.ui.layouts.BoxLayout; import com.codename1.ui.spinner.Picker; +import com.codename1.ui.spinner.Picker.LightweightPopupButtonPlacement; import com.codename1.ui.util.UITimer; import java.time.LocalDate; import java.time.ZoneId; import java.util.Date; +/** + * Captures the lightweight Picker popup with custom buttons across the + * placement and alignment combinations that matter. Originally a single + * baseline shot ({@code LightweightPickerButtons}); extended after + * issue #4819 where {@code Component.CENTER} alignment silently + * left-aligned the buttons. + * + * The variant count is intentionally trimmed to four: each variant + * cycle (popup show, throttled chunk emission, popup dismiss) costs + * roughly 5-6s on Android, and {@code Cn1ssDeviceRunner}'s per-test + * deadline is 30s on native platforms. The {@code _between_mixed} + * shot subsumes isolated LEFT / CENTER / RIGHT BETWEEN_CANCEL_AND_DONE + * captures: it lays out all three alignments in the same row with + * explicit L / C / R labels, so a regression that re-broke any of + * them would visibly collapse one column toward another. + */ public class LightweightPickerButtonsScreenshotTest extends BaseTest { + private Form form; private Picker picker; private Date fixedDate; private Date fixedDatePlus7; + private Variant[] variants; @Override public boolean runTest() { - Form form = createForm("Picker Quick Buttons", BoxLayout.y(), "LightweightPickerButtons"); fixedDate = toDate(LocalDate.of(2026, 4, 11)); fixedDatePlus7 = toDate(LocalDate.of(2026, 4, 18)); + variants = buildVariants(); + + form = new Form("Picker Quick Buttons", BoxLayout.y()) { + @Override + protected void onShowCompleted() { + runVariantsFrom(0); + } + }; picker = new Picker(); picker.setType(Display.PICKER_TYPE_DATE); picker.setUseLightweightPopup(true); picker.setDate(fixedDate); + form.add(picker); + form.show(); + return true; + } + + private Variant[] buildVariants() { + // Order matters for telemetry but not for correctness. The + // first entry's image name is "LightweightPickerButtons" to + // preserve the pre-existing golden (Today / +7 Days, both LEFT + // aligned) checked in under scripts//screenshots/. + return new Variant[] { + new Variant("LightweightPickerButtons", new Runnable() { + @Override + public void run() { + addToday(LightweightPopupButtonPlacement.BETWEEN_CANCEL_AND_DONE, Component.LEFT); + addPlus7(LightweightPopupButtonPlacement.BELOW_SPINNER, Component.LEFT); + } + }), + new Variant("LightweightPickerButtons_between_mixed", new Runnable() { + @Override + public void run() { + // Same placement, three alignments: covers the bug + // from #4819 (CENTER had been left-aligning) and + // the LEFT / RIGHT siblings in one shot. + picker.addLightweightPopupButton("L", null, + LightweightPopupButtonPlacement.BETWEEN_CANCEL_AND_DONE, Component.LEFT); + picker.addLightweightPopupButton("C", null, + LightweightPopupButtonPlacement.BETWEEN_CANCEL_AND_DONE, Component.CENTER); + picker.addLightweightPopupButton("R", null, + LightweightPopupButtonPlacement.BETWEEN_CANCEL_AND_DONE, Component.RIGHT); + } + }), + new Variant("LightweightPickerButtons_above_center", new Runnable() { + @Override + public void run() { + addToday(LightweightPopupButtonPlacement.ABOVE_SPINNER, Component.CENTER); + } + }), + new Variant("LightweightPickerButtons_below_right", new Runnable() { + @Override + public void run() { + addPlus7(LightweightPopupButtonPlacement.BELOW_SPINNER, Component.RIGHT); + } + }), + }; + } + + private void addToday(int placement, int alignment) { picker.addLightweightPopupButton("Today", new Runnable() { @Override public void run() { picker.setDate(fixedDate); } - }); + }, placement, alignment); + } + + private void addPlus7(int placement, int alignment) { picker.addLightweightPopupButton("+7 Days", new Runnable() { @Override public void run() { picker.setDate(fixedDatePlus7); } - }, Picker.LightweightPopupButtonPlacement.BELOW_SPINNER); - form.add(picker); - form.show(); - return true; + }, placement, alignment); } - @Override - protected void registerReadyCallback(Form parent, Runnable run) { + private void runVariantsFrom(final int index) { + if (index >= variants.length) { + done(); + return; + } + final Variant variant = variants[index]; + picker.clearLightweightPopupButtons(); picker.setDate(fixedDate); + variant.configure.run(); picker.startEditingAsync(); - UITimer.timer(1000, false, parent, run); + // Wait for the popup to slide up before screenshotting. Each + // variant cycle (wait + chunk-throttled emit + popup dismiss) + // costs ~5s on Android, and the per-test deadline in + // Cn1ssDeviceRunner is 30s, so this budget can't be padded. + // 600ms is generous: the InteractionDialog transition is + // <300ms and setDate is applied synchronously above. + UITimer.timer(600, false, form, new Runnable() { + @Override + public void run() { + Cn1ssDeviceRunnerHelper.emitCurrentFormScreenshot(variant.imageName, new Runnable() { + @Override + public void run() { + picker.stopEditing(new Runnable() { + @Override + public void run() { + runVariantsFrom(index + 1); + } + }); + } + }); + } + }); } private static Date toDate(LocalDate date) { return new Date(date.atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli()); } + + private static final class Variant { + final String imageName; + final Runnable configure; + + Variant(String imageName, Runnable configure) { + this.imageName = imageName; + this.configure = configure; + } + } } diff --git a/scripts/ios/screenshots/LightweightPickerButtons_above_center.png b/scripts/ios/screenshots/LightweightPickerButtons_above_center.png new file mode 100644 index 0000000000..53c54d3607 Binary files /dev/null and b/scripts/ios/screenshots/LightweightPickerButtons_above_center.png differ diff --git a/scripts/ios/screenshots/LightweightPickerButtons_below_right.png b/scripts/ios/screenshots/LightweightPickerButtons_below_right.png new file mode 100644 index 0000000000..33763e0663 Binary files /dev/null and b/scripts/ios/screenshots/LightweightPickerButtons_below_right.png differ diff --git a/scripts/ios/screenshots/LightweightPickerButtons_between_mixed.png b/scripts/ios/screenshots/LightweightPickerButtons_between_mixed.png new file mode 100644 index 0000000000..bb4604f752 Binary files /dev/null and b/scripts/ios/screenshots/LightweightPickerButtons_between_mixed.png differ