Skip to content

Commit 0b7d3b3

Browse files
authored
Add lightweight picker quick-action buttons (#4734)
Fixed #4733
1 parent 22eb74f commit 0b7d3b3

8 files changed

Lines changed: 252 additions & 3 deletions

File tree

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

Lines changed: 110 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,21 @@ 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 topCustomButtons = createLightweightPopupButtonRow(spinner, LightweightPopupButtonPlacement.ABOVE_SPINNER, isTablet);
589+
Container bottomCustomButtons = createLightweightPopupButtonRow(spinner, LightweightPopupButtonPlacement.BELOW_SPINNER, isTablet);
590+
if (topCustomButtons != null || bottomCustomButtons != null) {
591+
Container spinnerSection = new Container(new BorderLayout());
592+
spinnerSection.add(BorderLayout.CENTER, wrapper);
593+
if (topCustomButtons != null) {
594+
spinnerSection.add(BorderLayout.NORTH, topCustomButtons);
595+
}
596+
if (bottomCustomButtons != null) {
597+
spinnerSection.add(BorderLayout.SOUTH, bottomCustomButtons);
598+
}
599+
dlg.getContentPane().add(BorderLayout.CENTER, spinnerSection);
600+
} else {
601+
dlg.getContentPane().add(BorderLayout.CENTER, wrapper);
602+
}
564603

565604

566605
Button doneButton = new Button("Done", isTablet ? "PickerButtonTablet" : "PickerButton");
@@ -643,7 +682,8 @@ public void actionPerformed(ActionEvent evt) {
643682
west.add(nextButton);
644683
}
645684

646-
Container buttonBar = BorderLayout.centerEastWest(null, doneButton, west);
685+
Container centerButtons = createLightweightPopupButtonRow(spinner, LightweightPopupButtonPlacement.BETWEEN_CANCEL_AND_DONE, isTablet);
686+
Container buttonBar = BorderLayout.centerEastWest(centerButtons, doneButton, west);
647687
buttonBar.setUIID(isTablet ? "PickerButtonBarTablet" : "PickerButtonBar");
648688
dlg.getContentPane().add(BorderLayout.NORTH, buttonBar);
649689

@@ -716,6 +756,73 @@ public void actionPerformed(ActionEvent evt) {
716756
updateValue();
717757
}
718758

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

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
}
22.5 KB
Loading
-387 Bytes
Loading

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+
}
117 KB
Loading

0 commit comments

Comments
 (0)