Skip to content

Commit 7b44a67

Browse files
committed
Add lightweight picker quick-action buttons
1 parent 22eb74f commit 7b44a67

5 files changed

Lines changed: 244 additions & 3 deletions

File tree

CodenameOne/src/com/codename1/ui/spinner/Picker.java

Lines changed: 102 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@
5454

5555
import java.util.Calendar;
5656
import java.util.Date;
57+
import java.util.ArrayList;
58+
import java.util.Collections;
59+
import java.util.List;
5760
import java.util.ListIterator;
5861

5962
import static com.codename1.ui.ComponentSelector.$;
@@ -115,6 +118,29 @@ public class Picker extends Button {
115118
private boolean useLightweightPopup;
116119
private Runnable stopEditingCallback;
117120
private boolean suppressPaint;
121+
private final ArrayList<LightweightPopupButton> lightweightPopupButtons = new ArrayList<LightweightPopupButton>();
122+
123+
/// Placement options for custom lightweight popup buttons.
124+
public static final class LightweightPopupButtonPlacement {
125+
/// Place the custom button in the top button row between the `Cancel` and `Done` groups.
126+
public static final int BETWEEN_CANCEL_AND_DONE = 0;
127+
/// Place the custom button row directly above the spinner wheels.
128+
public static final int ABOVE_SPINNER = 1;
129+
/// Place the custom button row directly below the spinner wheels.
130+
public static final int BELOW_SPINNER = 2;
131+
}
132+
133+
private static final class LightweightPopupButton {
134+
private final String text;
135+
private final Runnable action;
136+
private final int placement;
137+
138+
private LightweightPopupButton(String text, Runnable action, int placement) {
139+
this.text = text;
140+
this.action = action;
141+
this.placement = placement;
142+
}
143+
}
118144

119145
/// Default constructor
120146
public Picker() {
@@ -559,8 +585,17 @@ protected void deinitialize() {
559585
.setBgTransparency(0)
560586
.setMargin(0)
561587
.setPaddingMillimeters(3f, 0);
562-
//wrapper.add(BorderLayout.CENTER, spinnerC);
563-
dlg.getContentPane().add(BorderLayout.CENTER, wrapper);
588+
Container spinnerSection = new Container(new BorderLayout());
589+
spinnerSection.add(BorderLayout.CENTER, wrapper);
590+
Container topCustomButtons = createLightweightPopupButtonRow(spinner, LightweightPopupButtonPlacement.ABOVE_SPINNER, isTablet);
591+
if (topCustomButtons != null) {
592+
spinnerSection.add(BorderLayout.NORTH, topCustomButtons);
593+
}
594+
Container bottomCustomButtons = createLightweightPopupButtonRow(spinner, LightweightPopupButtonPlacement.BELOW_SPINNER, isTablet);
595+
if (bottomCustomButtons != null) {
596+
spinnerSection.add(BorderLayout.SOUTH, bottomCustomButtons);
597+
}
598+
dlg.getContentPane().add(BorderLayout.CENTER, spinnerSection);
564599

565600

566601
Button doneButton = new Button("Done", isTablet ? "PickerButtonTablet" : "PickerButton");
@@ -643,7 +678,8 @@ public void actionPerformed(ActionEvent evt) {
643678
west.add(nextButton);
644679
}
645680

646-
Container buttonBar = BorderLayout.centerEastWest(null, doneButton, west);
681+
Container centerButtons = createLightweightPopupButtonRow(spinner, LightweightPopupButtonPlacement.BETWEEN_CANCEL_AND_DONE, isTablet);
682+
Container buttonBar = BorderLayout.centerEastWest(centerButtons, doneButton, west);
647683
buttonBar.setUIID(isTablet ? "PickerButtonBarTablet" : "PickerButtonBar");
648684
dlg.getContentPane().add(BorderLayout.NORTH, buttonBar);
649685

@@ -716,6 +752,69 @@ public void actionPerformed(ActionEvent evt) {
716752
updateValue();
717753
}
718754

755+
private Container createLightweightPopupButtonRow(final InternalPickerWidget spinner, int placement, boolean isTablet) {
756+
Container row = null;
757+
for (LightweightPopupButton entry : lightweightPopupButtons) {
758+
if (entry.placement != placement) {
759+
continue;
760+
}
761+
if (row == null) {
762+
row = new Container(BoxLayout.x());
763+
row.setUIID(isTablet ? "PickerButtonBarTablet" : "PickerButtonBar");
764+
$(row).selectAllStyles().setMargin(0).setPadding(0).setBorder(Border.createEmpty()).setBgTransparency(0);
765+
}
766+
Button button = new Button(entry.text, isTablet ? "PickerButtonTablet" : "PickerButton");
767+
button.addActionListener(e -> {
768+
if (entry.action != null) {
769+
entry.action.run();
770+
}
771+
spinner.setValue(value);
772+
updateValue();
773+
});
774+
row.add(button);
775+
}
776+
return row;
777+
}
778+
779+
/// Adds a custom button to the lightweight picker popup in the default placement
780+
/// between the `Cancel` and `Done` areas.
781+
///
782+
/// #### Parameters
783+
///
784+
/// - `text`: Button label.
785+
/// - `action`: Action to run when the button is pressed.
786+
public void addLightweightPopupButton(String text, Runnable action) {
787+
addLightweightPopupButton(text, action, LightweightPopupButtonPlacement.BETWEEN_CANCEL_AND_DONE);
788+
}
789+
790+
/// Adds a custom button to the lightweight picker popup.
791+
///
792+
/// #### Parameters
793+
///
794+
/// - `text`: Button label.
795+
/// - `action`: Action to run when the button is pressed.
796+
/// - `placement`: One of `LightweightPopupButtonPlacement#BETWEEN_CANCEL_AND_DONE`,
797+
/// `LightweightPopupButtonPlacement#ABOVE_SPINNER`, or `LightweightPopupButtonPlacement#BELOW_SPINNER`.
798+
public void addLightweightPopupButton(String text, Runnable action, int placement) {
799+
lightweightPopupButtons.add(new LightweightPopupButton(text, action, placement));
800+
}
801+
802+
/// Removes all custom lightweight popup buttons that were previously added with
803+
/// `#addLightweightPopupButton`.
804+
public void clearLightweightPopupButtons() {
805+
lightweightPopupButtons.clear();
806+
}
807+
808+
/// Returns an immutable list of custom button labels currently configured for
809+
/// the lightweight popup.
810+
public List<String> getLightweightPopupButtonLabels() {
811+
ArrayList<String> out = new ArrayList<String>();
812+
for (LightweightPopupButton b : lightweightPopupButtons) {
813+
out.add(b.text);
814+
}
815+
return Collections.unmodifiableList(out);
816+
}
817+
719818
/// Whether useLightweightPopup should default to true, this can be set via
720819
/// the theme constant `lightweightPickerBool`
721820
///

docs/developer-guide/The-Components-Of-Codename-One.asciidoc

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3525,6 +3525,30 @@ The text displayed by the picker on selection is generated automatically by the
35253525

35263526
A common use case is to format date values based on a specific appearance and `Picker` has builtin support for a custom display formatter. Just use the `setFormatter(SimpleDateFormat)` method and set the appearance for the field.
35273527

3528+
When using lightweight picker mode (`setUseLightweightPopup(true)`), you can add custom quick-action buttons to the popup. This is useful for actions like setting the date to "Today" or "+7 Days" without scrolling the wheels manually.
3529+
3530+
[source,java]
3531+
----
3532+
Picker picker = new Picker();
3533+
picker.setType(Display.PICKER_TYPE_DATE);
3534+
picker.setUseLightweightPopup(true);
3535+
picker.setDate(new Date());
3536+
3537+
picker.addLightweightPopupButton("Today", () -> picker.setDate(new Date()));
3538+
3539+
picker.addLightweightPopupButton("+7 Days", () -> {
3540+
Calendar cal = Calendar.getInstance();
3541+
cal.add(Calendar.DAY_OF_MONTH, 7);
3542+
picker.setDate(cal.getTime());
3543+
}, Picker.LightweightPopupButtonPlacement.BELOW_SPINNER);
3544+
----
3545+
3546+
Button placement options are:
3547+
3548+
- `Picker.LightweightPopupButtonPlacement.BETWEEN_CANCEL_AND_DONE` (default)
3549+
- `Picker.LightweightPopupButtonPlacement.ABOVE_SPINNER`
3550+
- `Picker.LightweightPopupButtonPlacement.BELOW_SPINNER`
3551+
35283552
=== SwipeableContainer
35293553

35303554
The https://www.codenameone.com/javadoc/com/codename1/ui/SwipeableContainer.html[SwipeableContainer] allows us to place a component such as a https://www.codenameone.com/javadoc/com/codename1/components/MultiButton.html[MultiButton] on top of additional "options"

maven/core-unittests/src/test/java/com/codename1/ui/spinner/PickerCoverageTest.java

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
import org.junit.jupiter.api.Assertions;
1313

1414
import java.util.concurrent.atomic.AtomicBoolean;
15+
import java.util.Calendar;
16+
import java.util.Date;
1517

1618
public class PickerCoverageTest extends UITestBase {
1719

@@ -424,4 +426,73 @@ public void testSizeChangedListenerExplicit() {
424426
DisplayTest.flushEdt();
425427
f.animate();
426428
}
429+
430+
@FormTest
431+
public void testLightweightPopupCustomButtonsInButtonBar() {
432+
cleanup();
433+
Form form = new Form("Custom Buttons", new BoxLayout(BoxLayout.Y_AXIS));
434+
Picker picker = new Picker();
435+
picker.setType(Display.PICKER_TYPE_DATE);
436+
picker.setUseLightweightPopup(true);
437+
picker.setDate(new Date(126, Calendar.JANUARY, 10));
438+
picker.addLightweightPopupButton("Today", new Runnable() {
439+
@Override
440+
public void run() {
441+
picker.setDate(new Date(126, Calendar.JANUARY, 1));
442+
}
443+
});
444+
form.add(picker);
445+
form.show();
446+
waitForForm(form);
447+
448+
picker.pressed();
449+
picker.released();
450+
DisplayTest.flushEdt();
451+
runAnimations(form);
452+
453+
InteractionDialog dlg = findInteractionDialog(form);
454+
Assertions.assertNotNull(dlg, "Dialog should be open");
455+
Button today = findButtonWithText(dlg, "Today");
456+
Assertions.assertNotNull(today, "Custom button should be present in picker popup");
457+
458+
today.pressed();
459+
today.released();
460+
DisplayTest.flushEdt();
461+
runAnimations(form);
462+
463+
Date selected = picker.getDate();
464+
Calendar cal = Calendar.getInstance();
465+
cal.setTime(selected);
466+
Assertions.assertEquals(1, cal.get(Calendar.DAY_OF_MONTH), "Custom action should update picker date");
467+
}
468+
469+
@FormTest
470+
public void testLightweightPopupCustomButtonPlacements() {
471+
cleanup();
472+
Form form = new Form("Custom Placement", new BoxLayout(BoxLayout.Y_AXIS));
473+
Picker picker = new Picker();
474+
picker.setType(Display.PICKER_TYPE_DATE);
475+
picker.setUseLightweightPopup(true);
476+
picker.addLightweightPopupButton("Top", new Runnable() {
477+
@Override
478+
public void run() {}
479+
}, Picker.LightweightPopupButtonPlacement.ABOVE_SPINNER);
480+
picker.addLightweightPopupButton("Bottom", new Runnable() {
481+
@Override
482+
public void run() {}
483+
}, Picker.LightweightPopupButtonPlacement.BELOW_SPINNER);
484+
form.add(picker);
485+
form.show();
486+
waitForForm(form);
487+
488+
picker.pressed();
489+
picker.released();
490+
DisplayTest.flushEdt();
491+
runAnimations(form);
492+
493+
InteractionDialog dlg = findInteractionDialog(form);
494+
Assertions.assertNotNull(dlg, "Dialog should be open");
495+
Assertions.assertNotNull(findButtonWithText(dlg, "Top"), "Top custom button should be rendered");
496+
Assertions.assertNotNull(findButtonWithText(dlg, "Bottom"), "Bottom custom button should be rendered");
497+
}
427498
}

scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ public final class Cn1ssDeviceRunner extends DeviceRunner {
7474
new TabsScreenshotTest(),
7575
new TextAreaAlignmentScreenshotTest(),
7676
new ValidatorLightweightPickerScreenshotTest(),
77+
new LightweightPickerButtonsScreenshotTest(),
7778
new ToastBarTopPositionScreenshotTest(),
7879
// Keep this as the last screenshot test; orientation changes can leak into subsequent screenshots.
7980
new OrientationLockScreenshotTest(),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.codenameone.examples.hellocodenameone.tests;
2+
3+
import com.codename1.ui.Display;
4+
import com.codename1.ui.Form;
5+
import com.codename1.ui.layouts.BoxLayout;
6+
import com.codename1.ui.spinner.Picker;
7+
import com.codename1.ui.util.UITimer;
8+
9+
import java.util.Calendar;
10+
import java.util.Date;
11+
12+
public class LightweightPickerButtonsScreenshotTest extends BaseTest {
13+
private Picker picker;
14+
15+
@Override
16+
public boolean runTest() {
17+
Form form = createForm("Picker Quick Buttons", BoxLayout.y(), "LightweightPickerButtons");
18+
picker = new Picker();
19+
picker.setType(Display.PICKER_TYPE_DATE);
20+
picker.setUseLightweightPopup(true);
21+
picker.setDate(new Date());
22+
picker.addLightweightPopupButton("Today", new Runnable() {
23+
@Override
24+
public void run() {
25+
picker.setDate(new Date());
26+
}
27+
});
28+
picker.addLightweightPopupButton("+7 Days", new Runnable() {
29+
@Override
30+
public void run() {
31+
Calendar cal = Calendar.getInstance();
32+
cal.add(Calendar.DAY_OF_MONTH, 7);
33+
picker.setDate(cal.getTime());
34+
}
35+
}, Picker.LightweightPopupButtonPlacement.BELOW_SPINNER);
36+
form.add(picker);
37+
form.show();
38+
return true;
39+
}
40+
41+
@Override
42+
protected void registerReadyCallback(Form parent, Runnable run) {
43+
picker.startEditingAsync();
44+
UITimer.timer(1000, false, parent, run);
45+
}
46+
}

0 commit comments

Comments
 (0)