Skip to content

Commit 0044b8e

Browse files
feat: custom TimePicker hour formats (#5790)
* initial commit * docs * examples * `PageMediaData.always_use_24_hour_format` * tests * improve basic test * apply review suggestions * refactor: make `TimePickerEntryModeChangeEvent` a dataclass * Add finder index support to tester interactions Enhanced the tester API to allow specifying the index of a control when multiple matches are found. Updated Dart and Python tester interfaces, finder logic, and integration tests to support and utilize this capability, enabling more precise UI automation and testing. * Remove await from resize_page in test_hour_formats The call to resize_page in test_hour_formats is no longer awaited, likely because the function is now synchronous. This aligns the test with the updated API. * docs: add example image for time picker hour formats * Update time picker hour format tests and golden images Refreshed golden images for macOS time picker hour formats and added new images for additional formats. Modified test_time_picker.py to improve the _select_clock helper, ensuring more comprehensive screenshot coverage and interaction consistency. * Update golden images for macOS time picker tests Refreshed the golden PNG images for basic, 12-hour, and 24-hour time picker integration tests on macOS to reflect recent UI or rendering changes. * Update golden images for macOS time picker Refreshed basic.png and image_for_docs.png in the golden images for the macOS time picker integration tests to reflect recent UI or rendering changes. * improve consistency in tests * 12-hour system default * docs: clarify time format usage in tests for macOS CI --------- Co-authored-by: Feodor Fitsner <feodor@appveyor.com>
1 parent fc642d6 commit 0044b8e

40 files changed

+544
-166
lines changed

client/integration_test/flutter_tester.dart

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -71,20 +71,21 @@ class FlutterWidgetTester implements Tester {
7171
}
7272

7373
@override
74-
Future<void> tap(TestFinder finder) =>
75-
_tester.tap((finder as FlutterTestFinder).raw);
74+
Future<void> tap(TestFinder finder, int finderIndex) =>
75+
_tester.tap((finder as FlutterTestFinder).raw.at(finderIndex));
7676

7777
@override
78-
Future<void> longPress(TestFinder finder) =>
79-
_tester.longPress((finder as FlutterTestFinder).raw);
80-
78+
Future<void> longPress(TestFinder finder, int finderIndex) =>
79+
_tester.longPress((finder as FlutterTestFinder).raw.at(finderIndex));
8180
@override
82-
Future<void> enterText(TestFinder finder, String text) =>
83-
_tester.enterText((finder as FlutterTestFinder).raw, text);
81+
Future<void> enterText(TestFinder finder, int finderIndex, String text) =>
82+
_tester.enterText(
83+
(finder as FlutterTestFinder).raw.at(finderIndex), text);
8484

8585
@override
86-
Future<void> mouseHover(TestFinder finder) async {
87-
final center = _tester.getCenter((finder as FlutterTestFinder).raw);
86+
Future<void> mouseHover(TestFinder finder, int finderIndex) async {
87+
final center =
88+
_tester.getCenter((finder as FlutterTestFinder).raw.at(finderIndex));
8889
final gesture = await _tester.createGesture(kind: PointerDeviceKind.mouse);
8990
await gesture.addPointer();
9091
await gesture.moveTo(center);

packages/flet/lib/src/controls/time_picker.dart

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class _TimePickerControlState extends State<TimePickerControl> {
2525

2626
var open = widget.control.getBool("open", false)!;
2727
var value = widget.control.getTimeOfDay("value", TimeOfDay.now())!;
28+
var hourFormat = widget.control.getString("hour_format");
2829

2930
void onClosed(TimeOfDay? timeValue) {
3031
widget.control.updateProperties({"_open": false}, python: false);
@@ -44,15 +45,22 @@ class _TimePickerControlState extends State<TimePickerControl> {
4445
hourLabelText: widget.control.getString("hour_label_text"),
4546
minuteLabelText: widget.control.getString("minute_label_text"),
4647
errorInvalidText: widget.control.getString("error_invalid_text"),
47-
initialEntryMode: widget.control.getTimePickerEntryMode(
48-
"time_picker_entry_mode", TimePickerEntryMode.dial)!,
48+
initialEntryMode: widget.control
49+
.getTimePickerEntryMode("entry_mode", TimePickerEntryMode.dial)!,
4950
orientation: widget.control.getOrientation("orientation"),
5051
onEntryModeChanged: (TimePickerEntryMode mode) {
51-
widget.control.triggerEvent("entry_mode_change", mode.name);
52+
widget.control.updateProperties({"entry_mode": mode.name});
53+
widget.control
54+
.triggerEvent("entry_mode_change", {"entry_mode": mode.name});
5255
},
5356
);
5457

55-
return dialog;
58+
final hourFormatMap = {"h12": false, "h24": true, "system": null};
59+
return MediaQuery(
60+
data: MediaQuery.of(context)
61+
.copyWith(alwaysUse24HourFormat: hourFormatMap[hourFormat]),
62+
child: dialog,
63+
);
5664
}
5765

5866
if (open && (open != lastOpen)) {

packages/flet/lib/src/flet_backend.dart

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,13 @@ class FletBackend extends ChangeNotifier {
7777
};
7878
Brightness platformBrightness = Brightness.light;
7979
PageMediaData media = PageMediaData(
80-
padding: PaddingData(EdgeInsets.zero),
81-
viewPadding: PaddingData(EdgeInsets.zero),
82-
viewInsets: PaddingData(EdgeInsets.zero),
83-
devicePixelRatio: 0,
84-
orientation: Orientation.portrait);
80+
padding: PaddingData(EdgeInsets.zero),
81+
viewPadding: PaddingData(EdgeInsets.zero),
82+
viewInsets: PaddingData(EdgeInsets.zero),
83+
devicePixelRatio: 0,
84+
orientation: Orientation.portrait,
85+
alwaysUse24HourFormat: false,
86+
);
8587
TargetPlatform platform = defaultTargetPlatform;
8688

8789
late Control _page;

packages/flet/lib/src/protocol/page_media_data.dart

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ class PageMediaData extends Equatable {
77
final PaddingData viewInsets;
88
final double devicePixelRatio;
99
final Orientation orientation;
10+
final bool alwaysUse24HourFormat;
1011

1112
const PageMediaData({
1213
required this.padding,
1314
required this.viewPadding,
1415
required this.viewInsets,
1516
required this.devicePixelRatio,
1617
required this.orientation,
18+
required this.alwaysUse24HourFormat,
1719
});
1820

1921
Map<String, dynamic> toMap() => <String, dynamic>{
@@ -22,11 +24,18 @@ class PageMediaData extends Equatable {
2224
'view_insets': viewInsets.toMap(),
2325
'device_pixel_ratio': devicePixelRatio,
2426
'orientation': orientation.name,
27+
'always_use_24_hour_format': alwaysUse24HourFormat,
2528
};
2629

2730
@override
28-
List<Object?> get props =>
29-
[padding, viewPadding, viewInsets, devicePixelRatio, orientation];
31+
List<Object?> get props => [
32+
padding,
33+
viewPadding,
34+
viewInsets,
35+
devicePixelRatio,
36+
orientation,
37+
alwaysUse24HourFormat,
38+
];
3039
}
3140

3241
class PaddingData extends Equatable {

packages/flet/lib/src/services/tester.dart

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ class TesterService extends FletService {
4949
? control.backend.globalKeys[controlKey.toString()]
5050
: ValueKey(controlKey.value);
5151
if (key == null) {
52-
throw Exception("Scroll key not found: $key");
52+
throw Exception("Key not found: $key");
5353
}
5454
var finder = control.backend.tester!.findByKey(key);
5555
_finders[finder.id] = finder;
@@ -74,27 +74,29 @@ class TesterService extends FletService {
7474
return await control.backend.tester!.takeScreenshot(args["name"]);
7575

7676
case "tap":
77-
var finder = _finders[args["id"]];
77+
var finder = _finders[args["finder_id"]];
7878
if (finder != null) {
79-
await control.backend.tester!.tap(finder);
79+
await control.backend.tester!.tap(finder, args["finder_index"]);
8080
}
8181

8282
case "long_press":
83-
var finder = _finders[args["id"]];
83+
var finder = _finders[args["finder_id"]];
8484
if (finder != null) {
85-
await control.backend.tester!.longPress(finder);
85+
await control.backend.tester!.longPress(finder, args["finder_index"]);
8686
}
8787

8888
case "enter_text":
89-
var finder = _finders[args["id"]];
89+
var finder = _finders[args["finder_id"]];
9090
if (finder != null) {
91-
await control.backend.tester!.enterText(finder, args["text"]);
91+
await control.backend.tester!
92+
.enterText(finder, args["finder_index"], args["text"]);
9293
}
9394

9495
case "mouse_hover":
95-
var finder = _finders[args["id"]];
96+
var finder = _finders[args["finder_id"]];
9697
if (finder != null) {
97-
await control.backend.tester!.mouseHover(finder);
98+
await control.backend.tester!
99+
.mouseHover(finder, args["finder_index"]);
98100
}
99101

100102
case "teardown":

packages/flet/lib/src/testing/tester.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ abstract class Tester {
1313
TestFinder findByTooltip(String value);
1414
TestFinder findByIcon(IconData icon);
1515
Future<Uint8List> takeScreenshot(String name);
16-
Future<void> tap(TestFinder finder);
17-
Future<void> longPress(TestFinder finder);
18-
Future<void> enterText(TestFinder finder, String text);
19-
Future<void> mouseHover(TestFinder finder);
16+
Future<void> tap(TestFinder finder, int finderIndex);
17+
Future<void> longPress(TestFinder finder, int finderIndex);
18+
Future<void> enterText(TestFinder finder, int finderIndex, String text);
19+
Future<void> mouseHover(TestFinder finder, int finderIndex);
2020
void teardown();
2121
Future waitForTeardown();
2222
}

packages/flet/lib/src/widgets/page_media.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ class _PageMediaState extends State<PageMedia> {
6060
viewInsets: PaddingData(MediaQuery.viewInsetsOf(context)),
6161
devicePixelRatio: MediaQuery.devicePixelRatioOf(context),
6262
orientation: MediaQuery.orientationOf(context),
63+
alwaysUse24HourFormat: MediaQuery.alwaysUse24HourFormatOf(context),
6364
);
6465

6566
if (newMedia != backend.media || !pageSizeUpdated) {

sdk/python/examples/controls/time_picker/basic.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,27 @@
1-
import flet as ft
21
from datetime import time
32

3+
import flet as ft
4+
45

56
def main(page: ft.Page):
67
page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
78

89
def handle_change(e: ft.Event[ft.TimePicker]):
9-
page.add(ft.Text(f"TimePicker change: {time_picker.value}"))
10+
selection.value = f"Selection: {time_picker.value}"
11+
page.show_dialog(ft.SnackBar(f"TimePicker change: {time_picker.value}"))
1012

1113
def handle_dismissal(e: ft.Event[ft.TimePicker]):
12-
page.add(ft.Text(f"TimePicker dismissed: {time_picker.value}"))
14+
page.show_dialog(ft.SnackBar("TimePicker dismissed!"))
1315

1416
def handle_entry_mode_change(e: ft.TimePickerEntryModeChangeEvent):
15-
page.add(ft.Text(f"TimePicker Entry mode changed to {e.entry_mode}"))
17+
page.show_dialog(ft.SnackBar(f"Entry mode changed: {time_picker.entry_mode}"))
1618

1719
time_picker = ft.TimePicker(
18-
value=time(1, 2),
20+
value=time(hour=19, minute=30),
1921
confirm_text="Confirm",
2022
error_invalid_text="Time out of range",
2123
help_text="Pick your time slot",
24+
entry_mode=ft.TimePickerEntryMode.DIAL,
2225
on_change=handle_change,
2326
on_dismiss=handle_dismissal,
2427
on_entry_mode_change=handle_entry_mode_change,
@@ -28,8 +31,9 @@ def handle_entry_mode_change(e: ft.TimePickerEntryModeChangeEvent):
2831
ft.Button(
2932
content="Pick time",
3033
icon=ft.Icons.TIME_TO_LEAVE,
31-
on_click=lambda _: page.show_dialog(time_picker),
32-
)
34+
on_click=lambda: page.show_dialog(time_picker),
35+
),
36+
selection := ft.Text(weight=ft.FontWeight.BOLD),
3337
)
3438

3539

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
from datetime import time
2+
3+
import flet as ft
4+
5+
6+
def main(page: ft.Page):
7+
page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
8+
9+
def get_system_hour_format():
10+
"""Returns the current system's hour format."""
11+
return "24h" if page.media.always_use_24_hour_format else "12h"
12+
13+
def format_time(value: time) -> str:
14+
"""Returns a formatted time string based on TimePicker and system settings."""
15+
use_24h = time_picker.hour_format == ft.TimePickerHourFormat.H24 or (
16+
time_picker.hour_format == ft.TimePickerHourFormat.SYSTEM
17+
and page.media.always_use_24_hour_format
18+
)
19+
return value.strftime("%H:%M" if use_24h else "%I:%M %p")
20+
21+
def handle_change(e: ft.Event[ft.TimePicker]):
22+
selection.value = f"Selection: {format_time(time_picker.value)}"
23+
24+
time_picker = ft.TimePicker(
25+
value=time(hour=19, minute=30),
26+
help_text="Choose your meeting time",
27+
on_change=handle_change,
28+
)
29+
30+
def open_picker(e: ft.Event[ft.Button]):
31+
choice = format_dropdown.value
32+
hour_format_map = {
33+
"system": ft.TimePickerHourFormat.SYSTEM,
34+
"12h": ft.TimePickerHourFormat.H12,
35+
"24h": ft.TimePickerHourFormat.H24,
36+
}
37+
time_picker.hour_format = hour_format_map[choice]
38+
page.show_dialog(time_picker)
39+
40+
page.add(
41+
ft.Row(
42+
alignment=ft.MainAxisAlignment.CENTER,
43+
controls=[
44+
format_dropdown := ft.Dropdown(
45+
label="Hour format",
46+
value="system",
47+
width=260,
48+
key="dd",
49+
options=[
50+
ft.DropdownOption(
51+
key="system",
52+
text=f"System default ({get_system_hour_format()})",
53+
),
54+
ft.DropdownOption(key="12h", text="12-hour clock"),
55+
ft.DropdownOption(key="24h", text="24-hour clock"),
56+
],
57+
),
58+
ft.Button(
59+
"Open TimePicker",
60+
icon=ft.Icons.SCHEDULE,
61+
on_click=open_picker,
62+
),
63+
],
64+
),
65+
selection := ft.Text(weight=ft.FontWeight.BOLD),
66+
)
67+
68+
69+
if __name__ == "__main__":
70+
ft.run(main)

sdk/python/packages/flet/docs/controls/timepicker.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,15 @@ example_images: ../test-images/examples/material/golden/macos/time_picker
1616
--8<-- "{{ examples }}/basic.py"
1717
```
1818

19-
{{ image(example_images + "/basic.png", alt="basic", width="80%") }}
19+
{{ image(example_images + "/basic.png", width="80%") }}
20+
21+
### Hour Formats
22+
23+
```python
24+
--8<-- "{{ examples }}/hour_formats.py"
25+
```
26+
27+
{{ image(example_images + "/hour_formats.gif", width="80%") }}
2028

2129

2230
{{ class_members(class_name) }}

0 commit comments

Comments
 (0)