From 65e96a0d97eb406f6cd454a40e7b6f9ccbde5b64 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Mon, 11 Dec 2023 21:15:13 +0100 Subject: [PATCH 1/4] CupertinoRadio control --- package/lib/src/controls/create_control.dart | 8 + package/lib/src/controls/cupertino_radio.dart | 154 ++++++++++++++++++ .../flet-core/src/flet_core/__init__.py | 1 + 3 files changed, 163 insertions(+) create mode 100644 package/lib/src/controls/cupertino_radio.dart diff --git a/package/lib/src/controls/create_control.dart b/package/lib/src/controls/create_control.dart index 06561784d..141f3595e 100644 --- a/package/lib/src/controls/create_control.dart +++ b/package/lib/src/controls/create_control.dart @@ -63,6 +63,7 @@ import 'popup_menu_button.dart'; import 'progress_bar.dart'; import 'progress_ring.dart'; import 'radio.dart'; +import 'cupertino_radio.dart'; import 'radio_group.dart'; import 'range_slider.dart'; import 'responsive_row.dart'; @@ -532,6 +533,13 @@ Widget createWidget(Key? key, ControlViewModel controlView, Control? parent, control: controlView.control, parentDisabled: parentDisabled, dispatch: controlView.dispatch); + case "cupertinoradio": + return CupertinoRadioControl( + key: key, + parent: parent, + control: controlView.control, + parentDisabled: parentDisabled, + dispatch: controlView.dispatch); case "dropdown": return DropdownControl( key: key, diff --git a/package/lib/src/controls/cupertino_radio.dart b/package/lib/src/controls/cupertino_radio.dart new file mode 100644 index 000000000..bd60e4133 --- /dev/null +++ b/package/lib/src/controls/cupertino_radio.dart @@ -0,0 +1,154 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_redux/flutter_redux.dart'; + +import '../actions.dart'; +import '../flet_app_services.dart'; +import '../models/app_state.dart'; +import '../models/control.dart'; +import '../models/control_ancestor_view_model.dart'; +import '../protocol/update_control_props_payload.dart'; +import '../utils/colors.dart'; +import 'create_control.dart'; +import 'error.dart'; +import 'list_tile.dart'; + +enum LabelPosition { right, left } + +class CupertinoRadioControl extends StatefulWidget { + final Control? parent; + final Control control; + final bool parentDisabled; + final dynamic dispatch; + + const CupertinoRadioControl( + {Key? key, + this.parent, + required this.control, + required this.parentDisabled, + required this.dispatch}) + : super(key: key); + + @override + State createState() => _CupertinoRadioControlState(); +} + +class _CupertinoRadioControlState extends State { + late final FocusNode _focusNode; + + @override + void initState() { + super.initState(); + _focusNode = FocusNode(); + _focusNode.addListener(_onFocusChange); + } + + void _onFocusChange() { + FletAppServices.of(context).server.sendPageEvent( + eventTarget: widget.control.id, + eventName: _focusNode.hasFocus ? "focus" : "blur", + eventData: ""); + } + + @override + void dispose() { + _focusNode.removeListener(_onFocusChange); + _focusNode.dispose(); + super.dispose(); + } + + void _onChange(String ancestorId, String? value) { + var svalue = value ?? ""; + debugPrint(svalue); + List> props = [ + {"i": ancestorId, "value": svalue} + ]; + widget.dispatch( + UpdateControlPropsAction(UpdateControlPropsPayload(props: props))); + + final server = FletAppServices.of(context).server; + server.updateControlProps(props: props); + server.sendPageEvent( + eventTarget: ancestorId, eventName: "change", eventData: svalue); + } + + @override + Widget build(BuildContext context) { + debugPrint("CupertinoRadio build: ${widget.control.id}"); + + String label = widget.control.attrString("label", "")!; + String value = widget.control.attrString("value", "")!; + LabelPosition labelPosition = LabelPosition.values.firstWhere( + (p) => + p.name.toLowerCase() == + widget.control.attrString("labelPosition", "")!.toLowerCase(), + orElse: () => LabelPosition.right); + bool autofocus = widget.control.attrBool("autofocus", false)!; + bool disabled = widget.control.isDisabled || widget.parentDisabled; + + return StoreConnector( + distinct: true, + ignoreChange: (state) { + return state.controls[widget.control.id] == null; + }, + converter: (store) => ControlAncestorViewModel.fromStore( + store, widget.control.id, "radiogroup"), + builder: (context, viewModel) { + debugPrint( + "CupertinoRadio StoreConnector build: ${widget.control.id}"); + + if (viewModel.ancestor == null) { + return const ErrorControl( + "CupertinoRadio control must be enclosed with RadioGroup."); + } + + String groupValue = viewModel.ancestor!.attrString("value", "")!; + String ancestorId = viewModel.ancestor!.id; + + var cupertinoRadio = CupertinoRadio( + autofocus: autofocus, + focusNode: _focusNode, + groupValue: groupValue, + value: value, + useCheckmarkStyle: + widget.control.attrBool("useCheckmarkStyle", false)!, + fillColor: HexColor.fromString(Theme.of(context), + widget.control.attrString("fillColor", "")!), + activeColor: HexColor.fromString(Theme.of(context), + widget.control.attrString("activeColor", "")!), + inactiveColor: HexColor.fromString(Theme.of(context), + widget.control.attrString("inactiveColor", "")!), + onChanged: !disabled + ? (String? value) { + _onChange(ancestorId, value); + } + : null); + + ListTileClicks.of(context)?.notifier.addListener(() { + _onChange(ancestorId, value); + }); + + Widget result = cupertinoRadio; + if (label != "") { + var labelWidget = disabled + ? Text(label, + style: TextStyle(color: Theme.of(context).disabledColor)) + : MouseRegion( + cursor: SystemMouseCursors.click, child: Text(label)); + result = MergeSemantics( + child: GestureDetector( + onTap: !disabled + ? () { + _onChange(ancestorId, value); + } + : null, + child: labelPosition == LabelPosition.right + ? Row(children: [cupertinoRadio, labelWidget]) + : Row(children: [labelWidget, cupertinoRadio]))); + } + + return constrainedControl( + context, result, widget.parent, widget.control); + }); + } +} 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 c8c76fd31..06a4d5dd0 100644 --- a/sdk/python/packages/flet-core/src/flet_core/__init__.py +++ b/sdk/python/packages/flet-core/src/flet_core/__init__.py @@ -227,4 +227,5 @@ from flet_core.badge import Badge from flet_core.navigation_drawer import NavigationDrawer, NavigationDrawerDestination from flet_core.selection_area import SelectionArea +from flet_core.cupertino_radio import CupertinoRadio from flet_core.cupertino_checkbox import CupertinoCheckbox From f4d3bf9ea19d0f9b8d91c86e518a82bd4d389815 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Mon, 11 Dec 2023 21:15:51 +0100 Subject: [PATCH 2/4] Radio.adaptive --- package/lib/src/controls/radio.dart | 10 ++++++++++ sdk/python/packages/flet-core/src/flet_core/radio.py | 11 +++++++++++ 2 files changed, 21 insertions(+) diff --git a/package/lib/src/controls/radio.dart b/package/lib/src/controls/radio.dart index 3f5a9e91b..0f4e2e424 100644 --- a/package/lib/src/controls/radio.dart +++ b/package/lib/src/controls/radio.dart @@ -75,6 +75,16 @@ class _RadioControlState extends State { Widget build(BuildContext context) { debugPrint("Radio build: ${widget.control.id}"); + bool adaptive = widget.control.attrBool("adaptive", false)!; + if (adaptive && + (defaultTargetPlatform == TargetPlatform.iOS || + defaultTargetPlatform == TargetPlatform.macOS)) { + return CupertinoRadioControl( + control: widget.control, + parentDisabled: widget.parentDisabled, + dispatch: widget.dispatch); + } + String label = widget.control.attrString("label", "")!; String value = widget.control.attrString("value", "")!; LabelPosition labelPosition = LabelPosition.values.firstWhere( diff --git a/sdk/python/packages/flet-core/src/flet_core/radio.py b/sdk/python/packages/flet-core/src/flet_core/radio.py index aa61ba446..bcd8c9968 100644 --- a/sdk/python/packages/flet-core/src/flet_core/radio.py +++ b/sdk/python/packages/flet-core/src/flet_core/radio.py @@ -85,6 +85,7 @@ def __init__( label_position: LabelPosition = LabelPosition.NONE, value: Optional[str] = None, autofocus: Optional[bool] = None, + adaptive: Optional[bool] = None, fill_color: Union[None, str, Dict[MaterialState, str]] = None, on_focus=None, on_blur=None, @@ -122,6 +123,7 @@ def __init__( self.label = label self.label_position = label_position self.autofocus = autofocus + self.adaptive = adaptive self.fill_color = fill_color self.on_focus = on_focus self.on_blur = on_blur @@ -202,3 +204,12 @@ def autofocus(self) -> Optional[bool]: @autofocus.setter def autofocus(self, value: Optional[bool]): self._set_attr("autofocus", value) + + # adaptive + @property + def adaptive(self) -> Optional[bool]: + return self._get_attr("adaptive", data_type="bool", def_value=False) + + @adaptive.setter + def adaptive(self, value: Optional[bool]): + self._set_attr("adaptive", value) From a022027c358871687b0328ab38b4a657f765ed1a Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Mon, 11 Dec 2023 21:16:14 +0100 Subject: [PATCH 3/4] Radio.active_color --- package/lib/src/controls/radio.dart | 5 +++++ sdk/python/packages/flet-core/src/flet_core/radio.py | 11 +++++++++++ 2 files changed, 16 insertions(+) diff --git a/package/lib/src/controls/radio.dart b/package/lib/src/controls/radio.dart index 0f4e2e424..f9bc3c1b1 100644 --- a/package/lib/src/controls/radio.dart +++ b/package/lib/src/controls/radio.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; @@ -8,7 +9,9 @@ import '../models/control.dart'; import '../models/control_ancestor_view_model.dart'; import '../protocol/update_control_props_payload.dart'; import '../utils/buttons.dart'; +import '../utils/colors.dart'; import 'create_control.dart'; +import 'cupertino_radio.dart'; import 'error.dart'; import 'list_tile.dart'; @@ -118,6 +121,8 @@ class _RadioControlState extends State { focusNode: _focusNode, groupValue: groupValue, value: value, + activeColor: HexColor.fromString(Theme.of(context), + widget.control.attrString("activeColor", "")!), fillColor: parseMaterialStateColor( Theme.of(context), widget.control, "fillColor"), onChanged: !disabled diff --git a/sdk/python/packages/flet-core/src/flet_core/radio.py b/sdk/python/packages/flet-core/src/flet_core/radio.py index bcd8c9968..db70fa213 100644 --- a/sdk/python/packages/flet-core/src/flet_core/radio.py +++ b/sdk/python/packages/flet-core/src/flet_core/radio.py @@ -87,6 +87,7 @@ def __init__( autofocus: Optional[bool] = None, adaptive: Optional[bool] = None, fill_color: Union[None, str, Dict[MaterialState, str]] = None, + active_color: Optional[str] = None, on_focus=None, on_blur=None, ): @@ -125,6 +126,7 @@ def __init__( self.autofocus = autofocus self.adaptive = adaptive self.fill_color = fill_color + self.active_color = active_color self.on_focus = on_focus self.on_blur = on_blur @@ -144,6 +146,15 @@ def value(self) -> Optional[str]: def value(self, value: Optional[str]): self._set_attr("value", value) + # active_color + @property + def active_color(self) -> Optional[str]: + return self._get_attr("activeColor") + + @active_color.setter + def active_color(self, value: Optional[str]): + self._set_attr("activeColor", value) + # label @property def label(self): From c902508cc2ce882bc2989ea299e2c14ee6e4be51 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Mon, 11 Dec 2023 21:17:57 +0100 Subject: [PATCH 4/4] create cupertino_radio.py --- client/macos/Podfile.lock | 2 +- .../src/flet_core/cupertino_radio.py | 215 ++++++++++++++++++ 2 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 sdk/python/packages/flet-core/src/flet_core/cupertino_radio.py diff --git a/client/macos/Podfile.lock b/client/macos/Podfile.lock index d72c000db..9890db6a4 100644 --- a/client/macos/Podfile.lock +++ b/client/macos/Podfile.lock @@ -57,4 +57,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7 -COCOAPODS: 1.12.1 +COCOAPODS: 1.14.2 diff --git a/sdk/python/packages/flet-core/src/flet_core/cupertino_radio.py b/sdk/python/packages/flet-core/src/flet_core/cupertino_radio.py new file mode 100644 index 000000000..77a08219c --- /dev/null +++ b/sdk/python/packages/flet-core/src/flet_core/cupertino_radio.py @@ -0,0 +1,215 @@ +from typing import Any, Dict, Optional, Union + +from flet_core.constrained_control import ConstrainedControl +from flet_core.control import OptionalNumber +from flet_core.ref import Ref +from flet_core.types import ( + AnimationValue, + LabelPosition, + LabelPositionString, + MaterialState, + OffsetValue, + ResponsiveNumber, + RotateValue, + ScaleValue, +) + +try: + from typing import Literal +except ImportError: + from typing_extensions import Literal + + +class CupertinoRadio(ConstrainedControl): + """ + Radio buttons let people select a single option from two or more choices. + + ----- + + Online docs: https://flet.dev/docs/controls/cupertinoradio + """ + + def __init__( + self, + ref: Optional[Ref] = None, + key: Optional[str] = None, + width: OptionalNumber = None, + height: OptionalNumber = None, + left: OptionalNumber = None, + top: OptionalNumber = None, + right: OptionalNumber = None, + bottom: OptionalNumber = None, + expand: Union[None, bool, int] = None, + col: Optional[ResponsiveNumber] = None, + opacity: OptionalNumber = None, + rotate: RotateValue = None, + scale: ScaleValue = None, + offset: OffsetValue = None, + aspect_ratio: OptionalNumber = None, + animate_opacity: AnimationValue = None, + animate_size: AnimationValue = None, + animate_position: AnimationValue = None, + animate_rotation: AnimationValue = None, + animate_scale: AnimationValue = None, + animate_offset: AnimationValue = None, + on_animation_end=None, + tooltip: Optional[str] = None, + visible: Optional[bool] = None, + disabled: Optional[bool] = None, + data: Any = None, + # + # Specific + # + label: Optional[str] = None, + label_position: LabelPosition = LabelPosition.NONE, + value: Optional[str] = None, + autofocus: Optional[bool] = None, + use_checkmark_style: Optional[bool] = None, + fill_color: Optional[str] = None, + active_color: Optional[str] = None, + inactive_color: Optional[str] = None, + on_focus=None, + on_blur=None, + ): + ConstrainedControl.__init__( + self, + ref=ref, + key=key, + width=width, + height=height, + left=left, + top=top, + right=right, + bottom=bottom, + expand=expand, + col=col, + opacity=opacity, + rotate=rotate, + scale=scale, + offset=offset, + aspect_ratio=aspect_ratio, + animate_opacity=animate_opacity, + animate_size=animate_size, + animate_position=animate_position, + animate_rotation=animate_rotation, + animate_scale=animate_scale, + animate_offset=animate_offset, + on_animation_end=on_animation_end, + tooltip=tooltip, + visible=visible, + disabled=disabled, + data=data, + ) + self.value = value + self.label = label + self.label_position = label_position + self.autofocus = autofocus + self.use_checkmark_style = use_checkmark_style + self.fill_color = fill_color + self.active_color = active_color + self.inactive_color = inactive_color + self.on_focus = on_focus + self.on_blur = on_blur + + def _get_control_name(self): + return "cupertinoradio" + + def _before_build_command(self): + super()._before_build_command() + + # value + @property + def value(self) -> Optional[str]: + return self._get_attr("value", def_value="") + + @value.setter + def value(self, value: Optional[str]): + self._set_attr("value", value) + + # label + @property + def label(self): + return self._get_attr("label") + + @label.setter + def label(self, value): + self._set_attr("label", value) + + # label_position + @property + def label_position(self) -> LabelPosition: + return self.__label_position + + @label_position.setter + def label_position(self, value: LabelPosition): + self.__label_position = value + if isinstance(value, LabelPosition): + self._set_attr("labelPosition", value.value) + else: + self.__set_label_position(value) + + def __set_label_position(self, value: LabelPositionString): + self._set_attr("labelPosition", value) + + # fill_color + @property + def fill_color(self) -> Optional[str]: + return self._get_attr("fillColor") + + @fill_color.setter + def fill_color(self, value: Optional[str]): + self._set_attr("fillColor", value) + + # on_focus + @property + def on_focus(self): + return self._get_event_handler("focus") + + @on_focus.setter + def on_focus(self, handler): + self._add_event_handler("focus", handler) + + # on_blur + @property + def on_blur(self): + return self._get_event_handler("blur") + + @on_blur.setter + def on_blur(self, handler): + self._add_event_handler("blur", handler) + + # autofocus + @property + def autofocus(self) -> Optional[bool]: + return self._get_attr("autofocus", data_type="bool", def_value=False) + + @autofocus.setter + def autofocus(self, value: Optional[bool]): + self._set_attr("autofocus", value) + + # use_checkmark_style + @property + def use_checkmark_style(self) -> Optional[bool]: + return self._get_attr("useCheckmarkStyle", data_type="bool", def_value=False) + + @use_checkmark_style.setter + def use_checkmark_style(self, value: Optional[bool]): + self._set_attr("useCheckmarkStyle", value) + + # active_color + @property + def active_color(self) -> Optional[str]: + return self._get_attr("activeColor") + + @active_color.setter + def active_color(self, value: Optional[str]): + self._set_attr("activeColor", value) + + # inactive_color + @property + def inactive_color(self) -> Optional[str]: + return self._get_attr("inactiveColor") + + @inactive_color.setter + def inactive_color(self, value: Optional[str]): + self._set_attr("inactiveColor", value)