diff --git a/package/lib/src/controls/create_control.dart b/package/lib/src/controls/create_control.dart index 06561784d..fca1d238e 100644 --- a/package/lib/src/controls/create_control.dart +++ b/package/lib/src/controls/create_control.dart @@ -86,6 +86,7 @@ import 'vertical_divider.dart'; import 'webview.dart'; import 'window_drag_area.dart'; import 'cupertino_checkbox.dart'; +import 'cupertino_switch.dart'; Widget createControl(Control? parent, String id, bool parentDisabled, {Widget? nextChild}) { @@ -334,8 +335,7 @@ Widget createWidget(Key? key, ControlViewModel controlView, Control? parent, control: controlView.control, children: controlView.children, parentDisabled: parentDisabled, - dispatch: controlView.dispatch - ); + dispatch: controlView.dispatch); case "stack": return StackControl( key: key, @@ -502,6 +502,13 @@ Widget createWidget(Key? key, ControlViewModel controlView, Control? parent, control: controlView.control, parentDisabled: parentDisabled, dispatch: controlView.dispatch); + case "cupertinoswitch": + return CupertinoSwitchControl( + key: key, + parent: parent, + control: controlView.control, + parentDisabled: parentDisabled, + dispatch: controlView.dispatch); case "slider": return SliderControl( key: key, diff --git a/package/lib/src/controls/cupertino_switch.dart b/package/lib/src/controls/cupertino_switch.dart new file mode 100644 index 000000000..cccf9c6c0 --- /dev/null +++ b/package/lib/src/controls/cupertino_switch.dart @@ -0,0 +1,150 @@ +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 '../protocol/update_control_props_payload.dart'; +import '../utils/buttons.dart'; +import '../utils/colors.dart'; +import 'create_control.dart'; +import 'list_tile.dart'; + +enum LabelPosition { right, left } + +class CupertinoSwitchControl extends StatefulWidget { + final Control? parent; + final Control control; + final bool parentDisabled; + final dynamic dispatch; + + const CupertinoSwitchControl( + {super.key, + this.parent, + required this.control, + required this.parentDisabled, + required this.dispatch}); + + @override + State createState() => _CupertinoSwitchControlState(); +} + +class _CupertinoSwitchControlState extends State { + bool _value = false; + late final FocusNode _focusNode; + + @override + void initState() { + super.initState(); + _focusNode = FocusNode(); + _focusNode.addListener(_onFocusChange); + } + + @override + void dispose() { + _focusNode.removeListener(_onFocusChange); + _focusNode.dispose(); + super.dispose(); + } + + void _onChange(bool value) { + var svalue = value.toString(); + debugPrint(svalue); + setState(() { + _value = value; + }); + List> props = [ + {"i": widget.control.id, "value": svalue} + ]; + widget.dispatch( + UpdateControlPropsAction(UpdateControlPropsPayload(props: props))); + final server = FletAppServices.of(context).server; + server.updateControlProps(props: props); + server.sendPageEvent( + eventTarget: widget.control.id, eventName: "change", eventData: svalue); + } + + void _onFocusChange() { + FletAppServices.of(context).server.sendPageEvent( + eventTarget: widget.control.id, + eventName: _focusNode.hasFocus ? "focus" : "blur", + eventData: ""); + } + + @override + Widget build(BuildContext context) { + debugPrint("CupertinoSwitchControl build: ${widget.control.id}"); + + String label = widget.control.attrString("label", "")!; + 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, + converter: (store) => store.dispatch, + builder: (context, dispatch) { + debugPrint( + "CupertinoSwitch StoreConnector build: ${widget.control.id}"); + + bool value = widget.control.attrBool("value", false)!; + if (_value != value) { + _value = value; + } + + var materialThumbColor = parseMaterialStateColor( + Theme.of(context), widget.control, "thumbColor"); + + var materialTrackColor = parseMaterialStateColor( + Theme.of(context), widget.control, "trackColor"); + + var swtch = CupertinoSwitch( + autofocus: autofocus, + focusNode: _focusNode, + activeColor: HexColor.fromString(Theme.of(context), + widget.control.attrString("activeColor", "")!), + thumbColor: materialThumbColor?.resolve({}), + trackColor: materialTrackColor?.resolve({}), + focusColor: HexColor.fromString(Theme.of(context), + widget.control.attrString("focusColor", "")!), + value: _value, + onChanged: !disabled + ? (bool value) { + _onChange(value); + } + : null); + + ListTileClicks.of(context)?.notifier.addListener(() { + _onChange(!_value); + }); + + Widget result = swtch; + 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(!_value); + } + : null, + child: labelPosition == LabelPosition.right + ? Row(children: [swtch, labelWidget]) + : Row(children: [labelWidget, swtch]))); + } + + return constrainedControl( + context, result, widget.parent, widget.control); + }); + } +} diff --git a/package/lib/src/controls/switch.dart b/package/lib/src/controls/switch.dart index 225f7b366..6fee0f126 100644 --- a/package/lib/src/controls/switch.dart +++ b/package/lib/src/controls/switch.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; @@ -11,6 +12,7 @@ import '../utils/colors.dart'; import '../utils/icons.dart'; import 'create_control.dart'; import 'list_tile.dart'; +import 'cupertino_switch.dart'; enum LabelPosition { right, left } @@ -78,6 +80,16 @@ class _SwitchControlState extends State { Widget build(BuildContext context) { debugPrint("SwitchControl build: ${widget.control.id}"); + bool adaptive = widget.control.attrBool("adaptive", false)!; + if (adaptive && + (defaultTargetPlatform == TargetPlatform.iOS || + defaultTargetPlatform == TargetPlatform.macOS)) { + return CupertinoSwitchControl( + control: widget.control, + parentDisabled: widget.parentDisabled, + dispatch: widget.dispatch); + } + String label = widget.control.attrString("label", "")!; LabelPosition labelPosition = LabelPosition.values.firstWhere( (p) => @@ -91,7 +103,7 @@ class _SwitchControlState extends State { distinct: true, converter: (store) => store.dispatch, builder: (context, dispatch) { - debugPrint("Checkbox StoreConnector build: ${widget.control.id}"); + debugPrint("Switch StoreConnector build: ${widget.control.id}"); bool value = widget.control.attrBool("value", false)!; if (_value != value) { @@ -115,6 +127,8 @@ class _SwitchControlState extends State { Theme.of(context), widget.control, "thumbIcon"), trackColor: parseMaterialStateColor( Theme.of(context), widget.control, "trackColor"), + focusColor: HexColor.fromString(Theme.of(context), + widget.control.attrString("focusColor", "")!), value: _value, onChanged: !disabled ? (bool value) { 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 7f03f73d2..5e2d1e921 100644 --- a/sdk/python/packages/flet-core/src/flet_core/__init__.py +++ b/sdk/python/packages/flet-core/src/flet_core/__init__.py @@ -227,3 +227,4 @@ from flet_core.navigation_drawer import NavigationDrawer, NavigationDrawerDestination from flet_core.selection_area import SelectionArea from flet_core.cupertino_checkbox import CupertinoCheckbox +from flet_core.cupertino_switch import CupertinoSwitch diff --git a/sdk/python/packages/flet-core/src/flet_core/cupertino_switch.py b/sdk/python/packages/flet-core/src/flet_core/cupertino_switch.py new file mode 100644 index 000000000..2a39f631c --- /dev/null +++ b/sdk/python/packages/flet-core/src/flet_core/cupertino_switch.py @@ -0,0 +1,239 @@ +from typing import Any, 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, + OffsetValue, + ResponsiveNumber, + RotateValue, + ScaleValue, +) + + +class CupertinoSwitch(ConstrainedControl): + """ + An iOS-style switch. Used to toggle the on/off state of a single setting. + + Example: + ``` + import flet as ft + + def main(page: ft.Page): + page.add( + ft.CupertinoSwitch(label="Cupertino Switch", value=True), + ft.Switch(label="Material Checkbox", value=True), + ft.Container(height=20), + ft.Text( + "Adaptive Switch shows as CupertinoSwitch on macOS and iOS and as Switch on other platforms:" + ), + ft.Switch(adaptive=True, label="Adaptive Switch", value=True), + ) + + ft.app(target=main) + ``` + ----- + + Online docs: https://flet.dev/docs/controls/cupertinoswitch + """ + + 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[bool] = None, + autofocus: Optional[bool] = None, + active_color: Optional[str] = None, + focus_color: Optional[str] = None, + thumb_color: Optional[str] = None, + track_color: Optional[str] = None, + on_change=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.active_color = active_color + self.focus_color = focus_color + self.thumb_color = thumb_color + self.track_color = track_color + self.on_change = on_change + self.on_focus = on_focus + self.on_blur = on_blur + + def _get_control_name(self): + return "cupertinoswitch" + + def _before_build_command(self): + super()._before_build_command() + self._set_attr_json("thumbColor", self.__thumb_color) + self._set_attr_json("trackColor", self.__track_color) + + # value + @property + def value(self) -> Optional[bool]: + return self._get_attr("value", data_type="bool", def_value=False) + + @value.setter + def value(self, value: Optional[bool]): + 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) + + # 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) + + # active_color + @property + def active_color(self): + return self._get_attr("activeColor") + + @active_color.setter + def active_color(self, value): + self._set_attr("activeColor", value) + + # focus_color + @property + def focus_color(self): + return self._get_attr("focusColor") + + @focus_color.setter + def focus_color(self, value): + self._set_attr("focusColor", value) + + # thumb_color + @property + def thumb_color(self) -> Optional[str]: + return self.__thumb_color + + @thumb_color.setter + def thumb_color(self, value: Optional[str]): + self.__thumb_color = value + + # track_color + @property + def track_color(self) -> Optional[str]: + return self.__track_color + + @track_color.setter + def track_color(self, value: Optional[str]): + self.__track_color = value + + # 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_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) diff --git a/sdk/python/packages/flet-core/src/flet_core/switch.py b/sdk/python/packages/flet-core/src/flet_core/switch.py index 88e29020f..1d0abc8ee 100644 --- a/sdk/python/packages/flet-core/src/flet_core/switch.py +++ b/sdk/python/packages/flet-core/src/flet_core/switch.py @@ -14,11 +14,6 @@ ScaleValue, ) -try: - from typing import Literal -except ImportError: - from typing_extensions import Literal - class Switch(ConstrainedControl): """ @@ -91,11 +86,13 @@ def __init__( autofocus: Optional[bool] = None, active_color: Optional[str] = None, active_track_color: Optional[str] = None, + focus_color: Optional[str] = None, inactive_thumb_color: Optional[str] = None, inactive_track_color: Optional[str] = None, thumb_color: Union[None, str, Dict[MaterialState, str]] = None, thumb_icon: Union[None, str, Dict[MaterialState, str]] = None, track_color: Union[None, str, Dict[MaterialState, str]] = None, + adaptive: Optional[bool] = None, on_change=None, on_focus=None, on_blur=None, @@ -135,11 +132,13 @@ def __init__( self.autofocus = autofocus self.active_color = active_color self.active_track_color = active_track_color + self.focus_color = focus_color self.inactive_thumb_color = inactive_thumb_color self.inactive_track_color = inactive_track_color self.thumb_color = thumb_color self.thumb_icon = thumb_icon self.track_color = track_color + self.adaptive = adaptive self.on_change = on_change self.on_focus = on_focus self.on_blur = on_blur @@ -187,6 +186,15 @@ def label_position(self, value: LabelPosition): def __set_label_position(self, value: LabelPositionString): self._set_attr("labelPosition", 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) + # autofocus @property def autofocus(self) -> Optional[bool]: @@ -214,6 +222,15 @@ def active_track_color(self): def active_track_color(self, value): self._set_attr("activeTrackColor", value) + # focus_color + @property + def focus_color(self): + return self._get_attr("focusColor") + + @focus_color.setter + def focus_color(self, value): + self._set_attr("focusColor", value) + # inactive_thumb_color @property def inactive_thumb_color(self):