From c41c0d37c3ce663bb441864236959a063a28b3c1 Mon Sep 17 00:00:00 2001 From: TheEthicalBoy <98978078+ndonkoHenri@users.noreply.github.com> Date: Sun, 17 Dec 2023 17:50:25 +0100 Subject: [PATCH] `SearchBar` control (#2212) * initial commit * attempt to add dismiss method * Create SearchController on init * SearchAnchor.bar -> SearchAnchor + SearchBar * open_view and close_view: sync and async * add value prop * add value default --------- Co-authored-by: Feodor Fitsner --- client/ios/Podfile.lock | 4 +- package/lib/src/controls/create_control.dart | 9 + package/lib/src/controls/search_anchor.dart | 227 ++++++++++ .../flet-core/src/flet_core/__init__.py | 1 + .../flet-core/src/flet_core/search_anchor.py | 415 ++++++++++++++++++ 5 files changed, 654 insertions(+), 2 deletions(-) create mode 100644 package/lib/src/controls/search_anchor.dart create mode 100644 sdk/python/packages/flet-core/src/flet_core/search_anchor.py diff --git a/client/ios/Podfile.lock b/client/ios/Podfile.lock index 26fa685f9..50abe6cf7 100644 --- a/client/ios/Podfile.lock +++ b/client/ios/Podfile.lock @@ -97,7 +97,7 @@ SPEC CHECKSUMS: audioplayers_darwin: 877d9a4d06331c5c374595e46e16453ac7eafa40 DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 - file_picker: ce3938a0df3cc1ef404671531facef740d03f920 + file_picker: 15fd9539e4eb735dc54bae8c0534a7a9511a03de Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 integration_test: 13825b8a9334a850581300559b8839134b124670 path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 @@ -105,7 +105,7 @@ SPEC CHECKSUMS: sensors_plus: 5717760720f7e6acd96fdbd75b7428f5ad755ec2 shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780 - url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 + url_launcher_ios: bf5ce03e0e2088bad9cc378ea97fa0ed5b49673b webview_flutter_wkwebview: 2e2d318f21a5e036e2c3f26171342e95908bd60a PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3 diff --git a/package/lib/src/controls/create_control.dart b/package/lib/src/controls/create_control.dart index caf329e66..58929b080 100644 --- a/package/lib/src/controls/create_control.dart +++ b/package/lib/src/controls/create_control.dart @@ -1,6 +1,7 @@ import 'dart:math'; import 'package:collection/collection.dart'; +import 'package:flet/src/controls/search_anchor.dart'; import 'package:flet/src/controls/segmented_button.dart'; import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; @@ -483,6 +484,14 @@ Widget createWidget(Key? key, ControlViewModel controlView, Control? parent, control: controlView.control, children: controlView.children, parentDisabled: parentDisabled); + case "searchbar": + return SearchAnchorControl( + key: key, + parent: parent, + control: controlView.control, + children: controlView.children, + parentDisabled: parentDisabled, + dispatch: controlView.dispatch); case "checkbox": return CheckboxControl( key: key, diff --git a/package/lib/src/controls/search_anchor.dart b/package/lib/src/controls/search_anchor.dart new file mode 100644 index 000000000..12b86364c --- /dev/null +++ b/package/lib/src/controls/search_anchor.dart @@ -0,0 +1,227 @@ +import 'dart:convert'; + +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/borders.dart'; +import '../utils/buttons.dart'; +import '../utils/colors.dart'; +import '../utils/text.dart'; +import 'create_control.dart'; + +class SearchAnchorControl extends StatefulWidget { + final Control? parent; + final Control control; + final List children; + final bool parentDisabled; + final dynamic dispatch; + + const SearchAnchorControl( + {super.key, + this.parent, + required this.control, + required this.children, + required this.parentDisabled, + required this.dispatch}); + + @override + State createState() => _SearchAnchorControlState(); +} + +class _SearchAnchorControlState extends State { + late final SearchController _controller; + String _value = ""; + + @override + void initState() { + super.initState(); + _controller = SearchController(); + _controller.addListener(_searchTextChanged); + } + + @override + void dispose() { + _controller.removeListener(_searchTextChanged); + _controller.dispose(); + super.dispose(); + } + + void _searchTextChanged() { + debugPrint("_searchTextChanged: ${_controller.text}"); + List> props = [ + {"i": widget.control.id, "value": _controller.text} + ]; + widget.dispatch( + UpdateControlPropsAction(UpdateControlPropsPayload(props: props))); + FletAppServices.of(context).server.updateControlProps(props: props); + } + + @override + Widget build(BuildContext context) { + debugPrint("SearchAnchor build: ${widget.control.id}"); + bool disabled = widget.control.isDisabled || widget.parentDisabled; + + debugPrint(widget.control.attrs.toString()); + + return StoreConnector( + distinct: true, + converter: (store) => store.dispatch, + builder: (context, dispatch) { + debugPrint("SearchAnchor StoreConnector build: ${widget.control.id}"); + + var value = widget.control.attrString("value"); + if (value != null) { + _controller.text = value; + } + + bool onChange = widget.control.attrBool("onChange", false)!; + bool onTap = widget.control.attrBool("onTap", false)!; + bool onSubmit = widget.control.attrBool("onSubmit", false)!; + + var suggestionCtrls = + widget.children.where((c) => c.name == "controls" && c.isVisible); + var barLeadingCtrls = widget.children + .where((c) => c.name == "barLeading" && c.isVisible); + var barTrailingCtrls = widget.children + .where((c) => c.name == "barTrailing" && c.isVisible); + var viewLeadingCtrls = widget.children + .where((c) => c.name == "viewLeading" && c.isVisible); + var viewTrailingCtrls = widget.children + .where((c) => c.name == "viewTrailing" && c.isVisible); + + var viewBgcolor = HexColor.fromString( + Theme.of(context), widget.control.attrString("viewBgcolor", "")!); + var dividerColor = HexColor.fromString(Theme.of(context), + widget.control.attrString("dividerColor", "")!); + + TextStyle? viewHeaderTextStyle = parseTextStyle( + Theme.of(context), widget.control, "viewHeaderTextStyle"); + TextStyle? viewHintTextStyle = parseTextStyle( + Theme.of(context), widget.control, "viewHintTextStyle"); + + var method = widget.control.attrString("method"); + + if (method != null) { + debugPrint("SearchAnchor JSON method: $method"); + + var mj = json.decode(method); + var name = mj["n"] as String; + var params = Map.from(mj["p"] as Map); + + if (name == "closeView") { + WidgetsBinding.instance.addPostFrameCallback((_) { + List> props = [ + {"i": widget.control.id, "method": ""} + ]; + widget.dispatch(UpdateControlPropsAction( + UpdateControlPropsPayload(props: props))); + FletAppServices.of(context) + .server + .updateControlProps(props: props); + if (_controller.isOpen) { + var text = params["text"].toString(); + setState(() { + _controller.closeView(text); + }); + } + }); + } else if (name == "openView") { + WidgetsBinding.instance.addPostFrameCallback((_) { + List> props = [ + {"i": widget.control.id, "method": ""} + ]; + widget.dispatch(UpdateControlPropsAction( + UpdateControlPropsPayload(props: props))); + FletAppServices.of(context) + .server + .updateControlProps(props: props); + if (!_controller.isOpen) { + _controller.openView(); + } + }); + } + } + + Widget anchor = SearchAnchor( + searchController: _controller, + headerHintStyle: viewHintTextStyle, + headerTextStyle: viewHeaderTextStyle, + viewSide: parseBorderSide( + Theme.of(context), widget.control, "viewSide"), + isFullScreen: widget.control.attrBool("fullScreen", false), + viewBackgroundColor: viewBgcolor, + dividerColor: dividerColor, + viewHintText: widget.control.attrString("viewHintText"), + viewElevation: widget.control.attrDouble("viewElevation"), + viewShape: parseOutlinedBorder(widget.control, "viewShape"), + viewTrailing: viewTrailingCtrls.isNotEmpty + ? viewTrailingCtrls.map((ctrl) { + return createControl(widget.parent, ctrl.id, disabled); + }) + : null, + viewLeading: viewLeadingCtrls.isNotEmpty + ? createControl( + widget.parent, viewLeadingCtrls.first.id, disabled) + : null, + builder: (BuildContext context, SearchController controller) { + return SearchBar( + controller: controller, + hintText: widget.control.attrString("barHintText"), + backgroundColor: parseMaterialStateColor( + Theme.of(context), widget.control, "barBgcolor"), + overlayColor: parseMaterialStateColor( + Theme.of(context), widget.control, "barOverlayColor"), + leading: barLeadingCtrls.isNotEmpty + ? createControl( + widget.parent, barLeadingCtrls.first.id, disabled) + : null, + trailing: barTrailingCtrls.isNotEmpty + ? barTrailingCtrls.map((ctrl) { + return createControl( + widget.parent, ctrl.id, disabled); + }) + : null, + onTap: () { + if (onTap) { + FletAppServices.of(context).server.sendPageEvent( + eventTarget: widget.control.id, + eventName: "tap", + eventData: ""); + } + controller.openView(); + }, + onSubmitted: onSubmit + ? (String value) { + FletAppServices.of(context).server.sendPageEvent( + eventTarget: widget.control.id, + eventName: "submit", + eventData: value); + } + : null, + onChanged: onChange + ? (String value) { + FletAppServices.of(context).server.sendPageEvent( + eventTarget: widget.control.id, + eventName: "change", + eventData: value); + } + : null, + ); + }, + suggestionsBuilder: + (BuildContext context, SearchController controller) { + return suggestionCtrls.map((ctrl) { + return createControl(widget.parent, ctrl.id, disabled); + }); + }); + + return constrainedControl( + context, anchor, 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 389c74d02..325e50d41 100644 --- a/sdk/python/packages/flet-core/src/flet_core/__init__.py +++ b/sdk/python/packages/flet-core/src/flet_core/__init__.py @@ -162,6 +162,7 @@ from flet_core.row import Row from flet_core.safe_area import SafeArea from flet_core.scrollable_control import OnScrollEvent +from flet_core.search_anchor import SearchBar from flet_core.segmented_button import Segment, SegmentedButton from flet_core.semantics import Semantics from flet_core.shader_mask import ShaderMask diff --git a/sdk/python/packages/flet-core/src/flet_core/search_anchor.py b/sdk/python/packages/flet-core/src/flet_core/search_anchor.py new file mode 100644 index 000000000..0deaa8345 --- /dev/null +++ b/sdk/python/packages/flet-core/src/flet_core/search_anchor.py @@ -0,0 +1,415 @@ +import time +from typing import Any, Optional, Union, List, Dict + +from flet_core import BorderSide, OutlinedBorder +from flet_core.textfield import TextCapitalization +from flet_core.constrained_control import ConstrainedControl +from flet_core.control import Control, OptionalNumber +from flet_core.ref import Ref +from flet_core.text_style import TextStyle +from flet_core.types import ( + AnimationValue, + OffsetValue, + ResponsiveNumber, + RotateValue, + ScaleValue, + MaterialState, +) + +try: + from typing import Literal +except ImportError: + from typing_extensions import Literal + + +class SearchBar(ConstrainedControl): + """ + Manages a "search view" route that allows the user to select one of the suggested completions for a search query. + + ----- + + Online docs: https://flet.dev/docs/controls/searchbar + """ + + def __init__( + self, + controls: Optional[List[Control]] = None, + ref: Optional[Ref] = None, + key: Optional[str] = None, + width: OptionalNumber = None, + height: 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, + # + # SearchBar Specific + # + value: Optional[str] = None, + bar_leading: Optional[Control] = None, + bar_trailing: Optional[List[Control]] = None, + bar_hint_text: Optional[str] = None, + bar_bgcolor: Union[None, str, Dict[MaterialState, str]] = None, + bar_overlay_color: Union[None, str, Dict[MaterialState, str]] = None, + view_leading: Optional[Control] = None, + view_trailing: Optional[List[Control]] = None, + view_elevation: OptionalNumber = None, + view_bgcolor: Optional[str] = None, + view_hint_text: Optional[str] = None, + view_side: Optional[BorderSide] = None, + view_shape: Optional[OutlinedBorder] = None, + view_header_text_style: Optional[TextStyle] = None, + view_hint_text_style: Optional[TextStyle] = None, + divider_color: Optional[str] = None, + full_screen: Optional[bool] = None, + capitalization: TextCapitalization = TextCapitalization.NONE, + on_tap=None, + on_submit=None, + on_change=None, + ): + ConstrainedControl.__init__( + self, + ref=ref, + key=key, + width=width, + height=height, + 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.controls = controls + self.bar_leading = bar_leading + self.bar_trailing = bar_trailing + self.bar_hint_text = bar_hint_text + self.bar_bgcolor = bar_bgcolor + self.bar_overlay_color = bar_overlay_color + self.view_leading = view_leading + self.view_trailing = view_trailing + self.view_elevation = view_elevation + self.view_bgcolor = view_bgcolor + self.view_hint_text = view_hint_text + self.view_side = view_side + self.view_shape = view_shape + self.view_header_text_style = view_header_text_style + self.view_hint_text_style = view_hint_text_style + self.divider_color = divider_color + self.full_screen = full_screen + self.capitalization = capitalization + self.on_tap = on_tap + self.on_submit = on_submit + self.on_change = on_change + + def _get_control_name(self): + return "searchbar" + + def _before_build_command(self): + super()._before_build_command() + self._set_attr_json("barBgcolor", self.__bar_bgcolor) + self._set_attr_json("barOverlayColor", self.__bar_overlay_color) + self._set_attr_json("viewShape", self.__view_shape) + self._set_attr_json("viewHeaderTextStyle", self.__view_header_text_style) + self._set_attr_json("viewHintTextStyle", self.__view_hint_text_style) + self._set_attr_json("viewSide", self.__view_side) + + def _get_children(self): + children = [] + if self.__bar_leading: + self.__bar_leading._set_attr_internal("n", "barLeading") + children.append(self.__bar_leading) + if self.__view_leading: + self.__view_leading._set_attr_internal("n", "viewLeading") + children.append(self.__view_leading) + if self.__bar_trailing: + for i in self.__bar_trailing: + i._set_attr_internal("n", "barTrailing") + children.append(i) + if self.__view_trailing: + for i in self.__view_trailing: + i._set_attr_internal("n", "viewTrailing") + children.append(i) + if self.__controls: + for i in self.__controls: + i._set_attr_internal("n", "controls") + children.append(i) + return children + + def open_view(self): + m = { + "n": "openView", + "i": str(time.time()), + "p": {}, + } + self._set_attr_json("method", m) + self.update() + + async def open_view_async(self): + m = { + "n": "openView", + "i": str(time.time()), + "p": {}, + } + self._set_attr_json("method", m) + await self.update() + + def close_view(self, text: str = ""): + m = { + "n": "closeView", + "i": str(time.time()), + "p": {"text": text}, + } + self._set_attr_json("method", m) + self.update() + + async def close_view_async(self, text: str = ""): + m = { + "n": "closeView", + "i": str(time.time()), + "p": {"text": text}, + } + self._set_attr_json("method", m) + await self.update() + + # bar_leading + @property + def bar_leading(self) -> Optional[Control]: + return self.__bar_leading + + @bar_leading.setter + def bar_leading(self, value: Optional[Control]): + self.__bar_leading = value + + # bar_trailing + @property + def bar_trailing(self) -> Optional[List[Control]]: + return self.__bar_trailing + + @bar_trailing.setter + def bar_trailing(self, value: Optional[List[Control]]): + self.__bar_trailing = value + + # bar_bgcolor + @property + def bar_bgcolor(self) -> Union[None, str, Dict[MaterialState, str]]: + return self.__bar_bgcolor + + @bar_bgcolor.setter + def bar_bgcolor(self, value: Union[None, str, Dict[MaterialState, str]]): + self.__bar_bgcolor = value + + # bar_overlay_color + @property + def bar_overlay_color(self) -> Union[None, str, Dict[MaterialState, str]]: + return self.__bar_overlay_color + + @bar_overlay_color.setter + def bar_overlay_color(self, value: Union[None, str, Dict[MaterialState, str]]): + self.__bar_overlay_color = value + + # view_leading + @property + def view_leading(self) -> Optional[Control]: + return self.__view_leading + + @view_leading.setter + def view_leading(self, value: Optional[Control]): + self.__view_leading = value + + # view_trailing + @property + def view_trailing(self) -> Optional[List[Control]]: + return self.__view_trailing + + @view_trailing.setter + def view_trailing(self, value: Optional[List[Control]]): + self.__view_trailing = value + + # view_elevation + @property + def view_elevation(self) -> OptionalNumber: + return self._get_attr("viewElevation") + + @view_elevation.setter + def view_elevation(self, value: OptionalNumber): + self._set_attr("viewElevation", value) + + # view_bgcolor + @property + def view_bgcolor(self): + return self._get_attr("viewBgcolor") + + @view_bgcolor.setter + def view_bgcolor(self, value): + self._set_attr("viewBgcolor", value) + + # divider_color + @property + def divider_color(self): + return self._get_attr("dividerColor") + + @divider_color.setter + def divider_color(self, value): + self._set_attr("dividerColor", value) + + # bar_hint_text + @property + def bar_hint_text(self): + return self._get_attr("barHintText") + + @bar_hint_text.setter + def bar_hint_text(self, value): + self._set_attr("barHintText", value) + + # view_hint_text + @property + def view_hint_text(self): + return self._get_attr("viewHintText") + + @view_hint_text.setter + def view_hint_text(self, value): + self._set_attr("viewHintText", value) + + # view_shape + @property + def view_shape(self) -> Optional[OutlinedBorder]: + return self.__view_shape + + @view_shape.setter + def view_shape(self, value: Optional[OutlinedBorder]): + self.__view_shape = value + + # view_side + @property + def view_side(self) -> Optional[BorderSide]: + return self.__view_side + + @view_side.setter + def view_side(self, value: Optional[BorderSide]): + self.__view_side = value + + # full_screen + @property + def full_screen(self) -> Optional[bool]: + return self._get_attr("fullScreen", data_type="bool", def_value=False) + + @full_screen.setter + def full_screen(self, value: Optional[bool]): + self._set_attr("fullScreen", value) + + # capitalization + @property + def capitalization(self) -> TextCapitalization: + return self.__capitalization + + @capitalization.setter + def capitalization(self, value: TextCapitalization): + self.__capitalization = value + self._set_attr( + "capitalization", + value.value if isinstance(value, TextCapitalization) else value, + ) + + # view_header_text_style + @property + def view_header_text_style(self): + return self.__view_header_text_style + + @view_header_text_style.setter + def view_header_text_style(self, value: Optional[TextStyle]): + self.__view_header_text_style = value + + # view_hint_text_style + @property + def view_hint_text_style(self): + return self.__view_hint_text_style + + @view_hint_text_style.setter + def view_hint_text_style(self, value: Optional[TextStyle]): + self.__view_hint_text_style = value + + # controls + @property + def controls(self): + return self.__controls + + @controls.setter + def controls(self, value: Optional[List[Control]]): + self.__controls = value if value is not None else [] + + # 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) + + # 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) + if handler is not None: + self._set_attr("onchange", True) + else: + self._set_attr("onchange", None) + + # on_tap + @property + def on_tap(self): + return self._get_event_handler("tap") + + @on_tap.setter + def on_tap(self, handler): + self._add_event_handler("tap", handler) + if handler is not None: + self._set_attr("ontap", True) + else: + self._set_attr("ontap", None) + + # on_submit + @property + def on_submit(self): + return self._get_event_handler("submit") + + @on_submit.setter + def on_submit(self, handler): + self._add_event_handler("submit", handler) + if handler is not None: + self._set_attr("onsubmit", True) + else: + self._set_attr("onsubmit", None)