diff --git a/client/pubspec.lock b/client/pubspec.lock index 266b530e1..60ec9c1d5 100644 --- a/client/pubspec.lock +++ b/client/pubspec.lock @@ -434,10 +434,10 @@ packages: dependency: transitive description: name: plugin_platform_interface - sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d + sha256: f4f88d4a900933e7267e2b353594774fc0d07fb072b47eedcd5b54e1ea3269f8 url: "https://pub.dev" source: hosted - version: "2.1.6" + version: "2.1.7" pointycastle: dependency: transitive description: @@ -546,10 +546,10 @@ packages: dependency: transitive description: name: shared_preferences_web - sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf + sha256: "7b15ffb9387ea3e237bb7a66b8a23d2147663d391cafc5c8f37b2e7b4bde5d21" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.2" shared_preferences_windows: dependency: transitive description: @@ -663,10 +663,10 @@ packages: dependency: transitive description: name: url_launcher_ios - sha256: "4ac97281cf60e2e8c5cc703b2b28528f9b50c8f7cebc71df6bdf0845f647268a" + sha256: bba3373219b7abb6b5e0d071b0fe66dfbe005d07517a68e38d4fc3638f35c6d3 url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "6.2.1" url_launcher_linux: dependency: transitive description: @@ -807,10 +807,10 @@ packages: dependency: transitive description: name: webview_flutter_platform_interface - sha256: "6d9213c65f1060116757a7c473247c60f3f7f332cac33dc417c9e362a9a13e4f" + sha256: adb8c03c2be231bea5a8ed0e9039e9d18dbb049603376beaefa15393ede468a5 url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.7.0" webview_flutter_wkwebview: dependency: transitive description: @@ -868,5 +868,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.2.0-194.0.dev <4.0.0" - flutter: ">=3.13.0" + dart: ">=3.2.0 <4.0.0" + flutter: ">=3.16.0" diff --git a/package/lib/src/controls/create_control.dart b/package/lib/src/controls/create_control.dart index f8a52bcf5..974dbee40 100644 --- a/package/lib/src/controls/create_control.dart +++ b/package/lib/src/controls/create_control.dart @@ -29,6 +29,7 @@ import 'column.dart'; import 'container.dart'; import 'datatable.dart'; import 'date_picker.dart'; +import 'time_picker.dart'; import 'divider.dart'; import 'drag_target.dart'; import 'draggable.dart'; @@ -326,6 +327,14 @@ Widget createWidget(Key? key, ControlViewModel controlView, Control? parent, parentDisabled: parentDisabled, dispatch: controlView.dispatch, ); + case "timepicker": + return TimePickerControl( + parent: parent, + control: controlView.control, + children: controlView.children, + parentDisabled: parentDisabled, + dispatch: controlView.dispatch, + ); case "draggable": return DraggableControl( key: key, diff --git a/package/lib/src/controls/time_picker.dart b/package/lib/src/controls/time_picker.dart new file mode 100644 index 000000000..5b0a5c443 --- /dev/null +++ b/package/lib/src/controls/time_picker.dart @@ -0,0 +1,109 @@ +import 'package:flutter/material.dart'; + +import '../actions.dart'; +import '../flet_app_services.dart'; +import '../models/control.dart'; +import '../protocol/update_control_props_payload.dart'; + +class TimePickerControl extends StatefulWidget { + final Control? parent; + final Control control; + final List children; + final bool parentDisabled; + final dynamic dispatch; + + const TimePickerControl({ + Key? key, + this.parent, + required this.control, + required this.children, + required this.parentDisabled, + required this.dispatch, + }) : super(key: key); + + @override + State createState() => _TimePickerControlState(); +} + +class _TimePickerControlState extends State { + @override + Widget build(BuildContext context) { + debugPrint("TimePicker build: ${widget.control.id}"); + + bool lastOpen = widget.control.state["open"] ?? false; + + var open = widget.control.attrBool("open", false)!; + TimeOfDay value = widget.control.attrTime("value") ?? TimeOfDay.now(); + String? helpText = widget.control.attrString("helpText"); + String? cancelText = widget.control.attrString("cancelText"); + String? confirmText = widget.control.attrString("confirmText"); + String? hourLabelText = widget.control.attrString("hourLabelText"); + String? minuteLabelText = widget.control.attrString("minuteLabelText"); + String? errorInvalidText = widget.control.attrString("errorInvalidText"); + TimePickerEntryMode timePickerEntryMode = TimePickerEntryMode.values + .firstWhere( + (a) => + a.name.toLowerCase() == + widget.control + .attrString("timePickerEntryMode", "")! + .toLowerCase(), + orElse: () => TimePickerEntryMode.dial); + + void onClosed(TimeOfDay? timeValue) { + String stringValue; + String eventName; + if (timeValue == null) { + String hourString = value.hour.toString(); + String minuteString = value.minute.toString(); + stringValue = '$hourString:$minuteString'; + eventName = "dismiss"; + } else { + String hourString = timeValue.hour.toString(); + String minuteString = timeValue.minute.toString(); + stringValue = '$hourString:$minuteString'; + eventName = "change"; + } + widget.control.state["open"] = false; + List> props = [ + {"i": widget.control.id, "value": stringValue, "open": "false"} + ]; + widget.dispatch( + UpdateControlPropsAction(UpdateControlPropsPayload(props: props))); + FletAppServices.of(context).server.updateControlProps(props: props); + + FletAppServices.of(context).server.sendPageEvent( + eventTarget: widget.control.id, + eventName: eventName, + eventData: stringValue); + } + + Widget createSelectTimeDialog() { + Widget dialog = TimePickerDialog( + initialTime: value, + helpText: helpText, + cancelText: cancelText, + confirmText: confirmText, + hourLabelText: hourLabelText, + minuteLabelText: minuteLabelText, + errorInvalidText: errorInvalidText, + initialEntryMode: timePickerEntryMode, + ); + + return dialog; + } + + if (open && (open != lastOpen)) { + widget.control.state["open"] = open; + + WidgetsBinding.instance.addPostFrameCallback((_) { + showDialog( + context: context, + builder: (context) => createSelectTimeDialog()).then((result) { + debugPrint("pickTime() completed"); + onClosed(result); + }); + }); + } + return const SizedBox.shrink(); + } +} diff --git a/package/lib/src/models/control.dart b/package/lib/src/models/control.dart index 9de0569ec..a4f3a15e2 100644 --- a/package/lib/src/models/control.dart +++ b/package/lib/src/models/control.dart @@ -1,4 +1,5 @@ import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; class Control extends Equatable { static const reservedProps = ['i', 'p', 't', 'c', 'n']; @@ -96,6 +97,16 @@ class Control extends Equatable { return DateTime.parse(value); } + TimeOfDay? attrTime(String name, [TimeOfDay? defValue]) { + var value = attrs[name.toLowerCase()]; + if (value == null) { + return defValue; + } + List splitted = value.split(':'); + return TimeOfDay( + hour: int.parse(splitted[0]), minute: int.parse(splitted[1])); + } + Control copyWith( {String? id, String? pid, diff --git a/sdk/python/packages/flet-core/src/flet_core/__init__.py b/sdk/python/packages/flet-core/src/flet_core/__init__.py index be5790700..81ad3b07a 100644 --- a/sdk/python/packages/flet-core/src/flet_core/__init__.py +++ b/sdk/python/packages/flet-core/src/flet_core/__init__.py @@ -177,6 +177,7 @@ TextField, TextOnlyInputFilter, ) +from flet_core.time_picker import TimePicker, TimePickerEntryMode from flet_core.theme import ( ColorScheme, PageTransitionsTheme, diff --git a/sdk/python/packages/flet-core/src/flet_core/date_picker.py b/sdk/python/packages/flet-core/src/flet_core/date_picker.py index 032a433d4..df50cffc9 100644 --- a/sdk/python/packages/flet-core/src/flet_core/date_picker.py +++ b/sdk/python/packages/flet-core/src/flet_core/date_picker.py @@ -32,7 +32,7 @@ class DatePicker(Control): It is added to [`page.overlay`](page#overlay) and called using its `pick_date()` method. - Depending on the `date_picker_mode`, it will show either a Calendar or an Input (TextField) for picking a date. + Depending on the `date_picker_entry_mode`, it will show either a Calendar or an Input (TextField) for picking a date. Example: ``` diff --git a/sdk/python/packages/flet-core/src/flet_core/time_picker.py b/sdk/python/packages/flet-core/src/flet_core/time_picker.py new file mode 100644 index 000000000..158f066ea --- /dev/null +++ b/sdk/python/packages/flet-core/src/flet_core/time_picker.py @@ -0,0 +1,231 @@ +from datetime import time +from enum import Enum +from typing import Any, Optional, Union + +from flet_core.control import Control, OptionalNumber +from flet_core.ref import Ref + +from flet_core.types import ResponsiveNumber + + +class TimePickerEntryMode(Enum): + DIAL = "dial" + INPUT = "input" + DIAL_ONLY = "dialOnly" + INPUT_ONLY = "inputOnly" + + +class TimePicker(Control): + """ + A Material-style time picker dialog. + + It is added to [`page.overlay`](page#overlay) and called using its `pick_time()` method. + + Depending on the `time_picker_mode`, it will show either a Dial or an Input (hour and minute text fields) for picking a time. + + Example: + ``` + import datetime + import flet as ft + + def main(page: ft.Page): + def change_time(e): + print(f"Time picker changed, value (minute) is {time_picker.value.minute}") + + def dismissed(e): + print(f"Time picker dismissed, value is {time_picker.value}") + + time_picker = ft.TimePicker( + confirm_text="Confirm", + error_invalid_text="Time out of range", + help_text="Pick your time slot", + on_change=change_time, + on_dismiss=dismissed, + ) + + page.overlay.append(time_picker) + + date_button = ft.ElevatedButton( + "Pick time", + icon=ft.icons.TIME_TO_LEAVE, + on_click=lambda _: time_picker.pick_time(), + ) + + page.add(date_button) + + + ft.app(target=main) + ``` + + ----- + + Online docs: https://flet.dev/docs/controls/time_picker + """ + + def __init__( + self, + ref: Optional[Ref] = None, + expand: Optional[Union[bool, int]] = None, + col: Optional[ResponsiveNumber] = None, + opacity: OptionalNumber = None, + tooltip: Optional[str] = None, + visible: Optional[bool] = None, + disabled: Optional[bool] = None, + data: Any = None, + open: bool = False, + value: Optional[time] = None, + time_picker_entry_mode: Optional[TimePickerEntryMode] = None, + hour_label_text: Optional[str] = None, + minute_label_text: Optional[str] = None, + help_text: Optional[str] = None, + cancel_text: Optional[str] = None, + confirm_text: Optional[str] = None, + error_invalid_text: Optional[str] = None, + on_change=None, + on_dismiss=None, + ): + Control.__init__( + self, + ref=ref, + expand=expand, + col=col, + opacity=opacity, + tooltip=tooltip, + visible=visible, + disabled=disabled, + data=data, + ) + self.value = value + self.help_text = help_text + self.cancel_text = cancel_text + self.confirm_text = confirm_text + self.error_invalid_text = error_invalid_text + self.hour_label_text = hour_label_text + self.minute_label_text = minute_label_text + self.time_picker_entry_mode = time_picker_entry_mode + self.on_change = on_change + self.on_dismiss = on_dismiss + self.open = open + + def _get_control_name(self): + return "timepicker" + + def pick_time(self): + self.open = True + self.update() + + async def pick_time_async(self): + self.open = True + await self.update_async() + + # open + @property + def open(self) -> Optional[bool]: + return self._get_attr("open", data_type="bool", def_value=False) + + @open.setter + def open(self, value: Optional[bool]): + self._set_attr("open", value) + + # value + @property + def value(self) -> Optional[time]: + value_string = self._get_attr( + "value", def_value=None + ) # value_string in comes in format 'HH:MM' + splitted = value_string.split(":") + return ( + time(hour=int(splitted[0]), minute=int(splitted[1])) + if value_string + else None + ) + + @value.setter + def value(self, value: Optional[Union[time, str]]): + if isinstance(value, (time)): + value = value.strftime("%H:%M") + self._set_attr("value", value) + + # hour_label_text + @property + def hour_label_text(self) -> Optional[str]: + return self._get_attr("hourLabelText", def_value=None) + + @hour_label_text.setter + def hour_label_text(self, value: Optional[str]): + self._set_attr("hourLabelText", value) + + # minute_label_text + @property + def minute_label_text(self) -> Optional[str]: + return self._get_attr("minuteLabelText", def_value=None) + + @minute_label_text.setter + def minute_label_text(self, value: Optional[str]): + self._set_attr("minuteLabelText", value) + + # help_text + @property + def help_text(self) -> Optional[str]: + return self._get_attr("helpText", def_value=None) + + @help_text.setter + def help_text(self, value: Optional[str]): + self._set_attr("helpText", value) + + # cancel_text + @property + def cancel_text(self) -> Optional[str]: + return self._get_attr("cancelText", def_value=None) + + @cancel_text.setter + def cancel_text(self, value: Optional[str]): + self._set_attr("cancelText", value) + + # confirm_text + @property + def confirm_text(self) -> Optional[str]: + return self._get_attr("confirmText", def_value=None) + + @confirm_text.setter + def confirm_text(self, value: Optional[str]): + self._set_attr("confirmText", value) + + # error_invalid_text + @property + def error_invalid_text(self) -> Optional[str]: + return self._get_attr("errorInvalidText", def_value=None) + + @error_invalid_text.setter + def error_invalid_text(self, value: Optional[str]): + self._set_attr("errorInvalidText", value) + + # time_picker_entry_mode + @property + def time_picker_entry_mode(self) -> Optional[TimePickerEntryMode]: + return self.__time_picker_entry_mode + + @time_picker_entry_mode.setter + def time_picker_entry_mode(self, value: Optional[TimePickerEntryMode]): + self.__time_picker_entry_mode = value + self._set_attr( + "timePickerEntryMode", value.value if value is not None else None + ) + + # on_change + @property + def on_change(self): + return self._get_event_handler("change") + + @on_change.setter + def on_change(self, handler): + self._add_event_handler("change", handler) + + # on_dismiss + @property + def on_dismiss(self): + return self._get_event_handler("dismiss") + + @on_dismiss.setter + def on_dismiss(self, handler): + self._add_event_handler("dismiss", handler)