From 116212076d3282fdfb8487761e6ebc2195388003 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Sun, 26 Nov 2023 17:30:33 +0100 Subject: [PATCH 1/4] new: `TileAffinity` and `ExpansionTile` --- package/lib/src/controls/create_control.dart | 8 + package/lib/src/controls/expansion_tile.dart | 137 +++++++ .../flet-core/src/flet_core/__init__.py | 3 +- .../flet-core/src/flet_core/expansion_tile.py | 384 ++++++++++++++++++ 4 files changed, 531 insertions(+), 1 deletion(-) create mode 100644 package/lib/src/controls/expansion_tile.dart create mode 100644 sdk/python/packages/flet-core/src/flet_core/expansion_tile.py diff --git a/package/lib/src/controls/create_control.dart b/package/lib/src/controls/create_control.dart index 59f555904..d9fbd4e48 100644 --- a/package/lib/src/controls/create_control.dart +++ b/package/lib/src/controls/create_control.dart @@ -36,6 +36,7 @@ import 'draggable.dart'; import 'dropdown.dart'; import 'elevated_button.dart'; import 'error.dart'; +import 'expansion_tile.dart'; import 'file_picker.dart'; import 'flet_app_control.dart'; import 'floating_action_button.dart'; @@ -420,6 +421,13 @@ Widget createWidget(Key? key, ControlViewModel controlView, Control? parent, control: controlView.control, children: controlView.children, parentDisabled: parentDisabled); + case "expansiontile": + return ExpansionTileControl( + key: key, + parent: parent, + control: controlView.control, + children: controlView.children, + parentDisabled: parentDisabled); case "listview": return ListViewControl( key: key, diff --git a/package/lib/src/controls/expansion_tile.dart b/package/lib/src/controls/expansion_tile.dart new file mode 100644 index 000000000..95d1a5b48 --- /dev/null +++ b/package/lib/src/controls/expansion_tile.dart @@ -0,0 +1,137 @@ +import 'package:flutter/material.dart'; + +import '../flet_app_services.dart'; +import '../models/control.dart'; +import '../utils/alignment.dart'; +import '../utils/borders.dart'; +import '../utils/colors.dart'; +import '../utils/edge_insets.dart'; +import 'create_control.dart'; +import 'error.dart'; + +class ExpansionTileControl extends StatelessWidget { + final Control? parent; + final Control control; + final List children; + final bool parentDisabled; + + const ExpansionTileControl( + {Key? key, + this.parent, + required this.control, + required this.children, + required this.parentDisabled}) + : super(key: key); + + @override + Widget build(BuildContext context) { + debugPrint("ExpansionTile build: ${control.id}"); + + final server = FletAppServices.of(context).server; + + var ctrls = children.where((c) => c.name == "controls" && c.isVisible); + var leadingCtrls = + children.where((c) => c.name == "leading" && c.isVisible); + var titleCtrls = children.where((c) => c.name == "title" && c.isVisible); + var subtitleCtrls = + children.where((c) => c.name == "subtitle" && c.isVisible); + var trailingCtrls = + children.where((c) => c.name == "trailing" && c.isVisible); + + if (titleCtrls.isEmpty) { + return const ErrorControl("ExpansionTile requires a title!"); + } + + bool disabled = control.isDisabled || parentDisabled; + bool onchange = control.attrBool("onchange", false)!; + bool maintainState = control.attrBool("maintainState", false)!; + bool initiallyExpanded = control.attrBool("maintainState", false)!; + + var iconColor = HexColor.fromString( + Theme.of(context), control.attrString("iconColor", "")!); + var textColor = HexColor.fromString( + Theme.of(context), control.attrString("textColor", "")!); + var bgColor = HexColor.fromString( + Theme.of(context), control.attrString("bgColor", "")!); + var collapsedBgColor = HexColor.fromString( + Theme.of(context), control.attrString("collapsedBgColor", "")!); + var collapsedIconColor = HexColor.fromString( + Theme.of(context), control.attrString("collapsedIconColor", "")!); + var collapsedTextColor = HexColor.fromString( + Theme.of(context), control.attrString("collapsedTextColor", "")!); + + var affinity = ListTileControlAffinity.values.firstWhere( + (e) => + e.name.toLowerCase() == + control.attrString("affinity", "")!.toLowerCase(), + orElse: () => ListTileControlAffinity.platform); + var clipBehavior = Clip.values.firstWhere( + (e) => + e.name.toLowerCase() == + control.attrString("clipBehavior", "")!.toLowerCase(), + orElse: () => Clip.none); + + var expandedCrossAxisAlignment = parseCrossAxisAlignment( + control, "crossAxisAlignment", CrossAxisAlignment.center); + + if (expandedCrossAxisAlignment == CrossAxisAlignment.baseline) { + return const ErrorControl( + 'CrossAxisAlignment.baseline is not supported since the expanded ' + 'controls are aligned in a column, not a row. ' + 'Try aligning the controls differently.'); + } + + Function(bool)? onChange = (onchange) && !disabled + ? (expanded) { + debugPrint( + "ExpansionTile ${control.id} was ${expanded ? "expanded" : "collapsed"}"); + server.sendPageEvent( + eventTarget: control.id, + eventName: "change", + eventData: "$expanded"); + } + : null; + + Widget tile = ExpansionTile( + controlAffinity: affinity, + childrenPadding: parseEdgeInsets(control, "controlsPadding"), + tilePadding: parseEdgeInsets(control, "tilePadding"), + expandedAlignment: parseAlignment(control, "expandedAlignment"), + expandedCrossAxisAlignment: parseCrossAxisAlignment( + control, "crossAxisAlignment", CrossAxisAlignment.center), + backgroundColor: bgColor, + iconColor: iconColor, + textColor: textColor, + collapsedBackgroundColor: collapsedBgColor, + collapsedIconColor: collapsedIconColor, + collapsedTextColor: collapsedTextColor, + maintainState: maintainState, + initiallyExpanded: initiallyExpanded, + clipBehavior: clipBehavior, + shape: parseOutlinedBorder(control, "shape"), + collapsedShape: parseOutlinedBorder(control, "collapsedShape"), + onExpansionChanged: onChange, + leading: leadingCtrls.isNotEmpty + ? createControl(control, leadingCtrls.first.id, disabled) + : null, + title: createControl(control, titleCtrls.first.id, disabled), + subtitle: subtitleCtrls.isNotEmpty + ? createControl(control, subtitleCtrls.first.id, disabled) + : null, + trailing: trailingCtrls.isNotEmpty + ? createControl(control, trailingCtrls.first.id, disabled) + : null, + children: ctrls.isNotEmpty + ? ctrls.map((c) => createControl(control, c.id, disabled)).toList() + : [], + ); + + return constrainedControl(context, tile, parent, control); + } +} + +class ExpansionTileClickNotifier extends ChangeNotifier { + void onClick() { + notifyListeners(); + } +} 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 7fcfadb6e..0768f061a 100644 --- a/sdk/python/packages/flet-core/src/flet_core/__init__.py +++ b/sdk/python/packages/flet-core/src/flet_core/__init__.py @@ -74,6 +74,7 @@ from flet_core.draggable import Draggable from flet_core.dropdown import Dropdown from flet_core.elevated_button import ElevatedButton +from flet_core.expansion_tile import ExpansionTile from flet_core.file_picker import ( FilePicker, FilePickerFileType, @@ -113,7 +114,7 @@ from flet_core.icon import Icon from flet_core.icon_button import IconButton from flet_core.image import Image -from flet_core.list_tile import ListTile +from flet_core.list_tile import ListTile, TileAffinity from flet_core.list_view import ListView from flet_core.margin import Margin from flet_core.markdown import Markdown, MarkdownExtensionSet diff --git a/sdk/python/packages/flet-core/src/flet_core/expansion_tile.py b/sdk/python/packages/flet-core/src/flet_core/expansion_tile.py new file mode 100644 index 000000000..40daa2f0a --- /dev/null +++ b/sdk/python/packages/flet-core/src/flet_core/expansion_tile.py @@ -0,0 +1,384 @@ +from enum import Enum +from typing import Any, Optional, Union, List + +from flet_core import OutlinedBorder, Alignment +from flet_core.constrained_control import ConstrainedControl +from flet_core.control import Control, OptionalNumber +from flet_core.ref import Ref +from flet_core.types import ( + AnimationValue, + OffsetValue, + PaddingValue, + ResponsiveNumber, + RotateValue, + ScaleValue, + ClipBehavior, + CrossAxisAlignment, +) + + +class TileAffinity(Enum): + LEADING = "leading" + TRAILING = "trailing" + PLATFORM = "platform" + + +class ExpansionTile(ConstrainedControl): + """ + A single-line ListTile with an expansion arrow icon that expands or collapses the tile to reveal or hide its controls. + + ----- + + Online docs: https://flet.dev/docs/controls/expansiontile + """ + + def __init__( + self, + controls: Optional[List[Control]] = None, + 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 + # + title: Optional[Control] = None, + subtitle: Optional[Control] = None, + leading: Optional[Control] = None, + trailing: Optional[Control] = None, + controls_padding: PaddingValue = None, + tile_padding: PaddingValue = None, + affinity: Optional[TileAffinity] = None, + expanded_alignment: Optional[Alignment] = None, + expanded_cross_axis_alignment: CrossAxisAlignment = CrossAxisAlignment.CENTER, + clip_behavior: Optional[ClipBehavior] = None, + initially_expanded: Optional[bool] = None, + maintain_state: Optional[bool] = None, + text_color: Optional[str] = None, + icon_color: Optional[str] = None, + shape: Optional[OutlinedBorder] = None, + bgcolor: Optional[str] = None, + collapsed_bgcolor: Optional[str] = None, + collapsed_icon_color: Optional[str] = None, + collapsed_text_color: Optional[str] = None, + collapsed_shape: Optional[OutlinedBorder] = None, + on_change=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.controls = controls + self.controls_padding = controls_padding + self.expanded_alignment = expanded_alignment + self.expanded_cross_axis_alignment = expanded_cross_axis_alignment + self.tile_padding = tile_padding + self.leading = leading + self.title = title + self.subtitle = subtitle + self.trailing = trailing + self.affinity = affinity + self.clip_behavior = clip_behavior + self.maintain_state = maintain_state + self.initially_expanded = initially_expanded + self.shape = shape + self.text_color = text_color + self.icon_color = icon_color + self.bgcolor = bgcolor + self.collapsed_bgcolor = collapsed_bgcolor + self.collapsed_icon_color = collapsed_icon_color + self.collapsed_text_color = collapsed_text_color + self.collapsed_shape = collapsed_shape + self.on_change = on_change + + def _get_control_name(self): + return "expansiontile" + + def _before_build_command(self): + super()._before_build_command() + self._set_attr_json("expandedAlignment", self.__expanded_alignment) + self._set_attr_json("controlsPadding", self.__controls_padding) + self._set_attr_json("tilePadding", self.__tile_padding) + self._set_attr_json("Shape", self.__shape) + self._set_attr_json("collapsedShape", self.__collapsed_shape) + + def _get_children(self): + children = [] + if self.__controls: + for c in self.__controls: + c._set_attr_internal("n", "controls") + children.append(c) + if self.__leading: + self.__leading._set_attr_internal("n", "leading") + children.append(self.__leading) + if self.__title: + self.__title._set_attr_internal("n", "title") + children.append(self.__title) + if self.__subtitle: + self.__subtitle._set_attr_internal("n", "subtitle") + children.append(self.__subtitle) + if self.__trailing: + self.__trailing._set_attr_internal("n", "trailing") + children.append(self.__trailing) + return children + + # 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 [] + + # controls_padding + @property + def controls_padding(self) -> PaddingValue: + return self.__controls_padding + + @controls_padding.setter + def controls_padding(self, value: PaddingValue): + self.__controls_padding = value + + # tile_padding + @property + def tile_padding(self) -> PaddingValue: + return self.__tile_padding + + @tile_padding.setter + def tile_padding(self, value: PaddingValue): + self.__tile_padding = value + + # expanded_alignment + @property + def expanded_alignment(self) -> Optional[Alignment]: + return self.__expanded_alignment + + @expanded_alignment.setter + def expanded_alignment(self, value: Optional[Alignment]): + self.__expanded_alignment = value + + # expanded_cross_axis_alignment + @property + def expanded_cross_axis_alignment(self) -> CrossAxisAlignment: + return self.__expanded_cross_axis_alignment + + @expanded_cross_axis_alignment.setter + def expanded_cross_axis_alignment(self, value: CrossAxisAlignment): + self.__expanded_cross_axis_alignment = value + self._set_attr( + "crossAxisAlignment", + value.value if isinstance(value, CrossAxisAlignment) else value, + ) + + # affinity + @property + def affinity(self) -> TileAffinity: + return self.__affinity + + @affinity.setter + def affinity(self, value: TileAffinity): + self.__affinity = value + self._set_attr( + "affinity", + value.value if isinstance(value, TileAffinity) else value, + ) + + # leading + @property + def leading(self) -> Optional[Control]: + return self.__leading + + @leading.setter + def leading(self, value: Optional[Control]): + self.__leading = value + + # title + @property + def title(self) -> Optional[Control]: + return self.__title + + @title.setter + def title(self, value: Optional[Control]): + self.__title = value + + # subtitle + @property + def subtitle(self) -> Optional[Control]: + return self.__subtitle + + @subtitle.setter + def subtitle(self, value: Optional[Control]): + self.__subtitle = value + + # trailing + @property + def trailing(self) -> Optional[Control]: + return self.__trailing + + @trailing.setter + def trailing(self, value: Optional[Control]): + self.__trailing = value + + # clip_behavior + @property + def clip_behavior(self) -> Optional[ClipBehavior]: + return self.__clip_behavior + + @clip_behavior.setter + def clip_behavior(self, value: Optional[ClipBehavior]): + self.__clip_behavior = value + self._set_attr( + "clipBehavior", value.value if isinstance(value, ClipBehavior) else value + ) + + # maintain_state + @property + def maintain_state(self) -> Optional[bool]: + return self._get_attr("maintainState", data_type="bool", def_value=False) + + @maintain_state.setter + def maintain_state(self, value: Optional[bool]): + self._set_attr("maintainState", value) + + # initially_expanded + @property + def initially_expanded(self) -> Optional[bool]: + return self._get_attr("initiallyExpanded", data_type="bool", def_value=False) + + @initially_expanded.setter + def initially_expanded(self, value: Optional[bool]): + self._set_attr("initiallyExpanded", value) + + # shape + @property + def shape(self) -> Optional[OutlinedBorder]: + return self.__shape + + @shape.setter + def shape(self, value: Optional[OutlinedBorder]): + self.__shape = value + + # text_color + @property + def text_color(self): + return self._get_attr("textColor") + + @text_color.setter + def text_color(self, value): + self._set_attr("textColor", value) + + # icon_color + @property + def icon_color(self): + return self._get_attr("iconColor") + + @icon_color.setter + def icon_color(self, value): + self._set_attr("iconColor", value) + + # bgcolor + @property + def bgcolor(self): + return self._get_attr("bgColor") + + @bgcolor.setter + def bgcolor(self, value): + self._set_attr("bgColor", value) + + # collapsed_bgcolor + @property + def collapsed_bgcolor(self): + return self._get_attr("collapsedBgColor") + + @collapsed_bgcolor.setter + def collapsed_bgcolor(self, value): + self._set_attr("collapsedBgColor", value) + + # collapsed_icon_color + @property + def collapsed_icon_color(self): + return self._get_attr("collapsedIconColor") + + @collapsed_icon_color.setter + def collapsed_icon_color(self, value): + self._set_attr("collapsedIconColor", value) + + # collapsed_text_color + @property + def collapsed_text_color(self): + return self._get_attr("collapsedTextColor") + + @collapsed_text_color.setter + def collapsed_text_color(self, value): + self._set_attr("collapsedTextColor", value) + + # collapsed_shape + @property + def collapsed_shape(self) -> Optional[OutlinedBorder]: + return self.__collapsed_shape + + @collapsed_shape.setter + def collapsed_shape(self, value: Optional[OutlinedBorder]): + self.__collapsed_shape = 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) From 17ebfef70c3690b35be58b7e934c90ff855dd63a Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Sun, 26 Nov 2023 17:31:23 +0100 Subject: [PATCH 2/4] remove unused import --- sdk/python/packages/flet-core/src/flet_core/column.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sdk/python/packages/flet-core/src/flet_core/column.py b/sdk/python/packages/flet-core/src/flet_core/column.py index 1dfb69f14..de750dd2a 100644 --- a/sdk/python/packages/flet-core/src/flet_core/column.py +++ b/sdk/python/packages/flet-core/src/flet_core/column.py @@ -15,7 +15,6 @@ RotateValue, ScaleValue, ScrollMode, - ScrollModeString, ) From bce4868a4c9c58a94b950e88b09a99c6adcd60eb Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Sun, 26 Nov 2023 17:39:23 +0100 Subject: [PATCH 3/4] fix `TileAffinity` import --- sdk/python/packages/flet-core/src/flet_core/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 0768f061a..61a92acdc 100644 --- a/sdk/python/packages/flet-core/src/flet_core/__init__.py +++ b/sdk/python/packages/flet-core/src/flet_core/__init__.py @@ -74,7 +74,7 @@ from flet_core.draggable import Draggable from flet_core.dropdown import Dropdown from flet_core.elevated_button import ElevatedButton -from flet_core.expansion_tile import ExpansionTile +from flet_core.expansion_tile import ExpansionTile, TileAffinity from flet_core.file_picker import ( FilePicker, FilePickerFileType, @@ -114,7 +114,7 @@ from flet_core.icon import Icon from flet_core.icon_button import IconButton from flet_core.image import Image -from flet_core.list_tile import ListTile, TileAffinity +from flet_core.list_tile import ListTile from flet_core.list_view import ListView from flet_core.margin import Margin from flet_core.markdown import Markdown, MarkdownExtensionSet From 6947b761df47c781a03fd4d2303fd9b6339abb47 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Mon, 27 Nov 2023 23:18:41 +0100 Subject: [PATCH 4/4] apply requested changes --- package/lib/src/controls/expansion_tile.dart | 6 ------ .../packages/flet-core/src/flet_core/expansion_tile.py | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/package/lib/src/controls/expansion_tile.dart b/package/lib/src/controls/expansion_tile.dart index 95d1a5b48..f586e6369 100644 --- a/package/lib/src/controls/expansion_tile.dart +++ b/package/lib/src/controls/expansion_tile.dart @@ -129,9 +129,3 @@ class ExpansionTileControl extends StatelessWidget { return constrainedControl(context, tile, parent, control); } } - -class ExpansionTileClickNotifier extends ChangeNotifier { - void onClick() { - notifyListeners(); - } -} diff --git a/sdk/python/packages/flet-core/src/flet_core/expansion_tile.py b/sdk/python/packages/flet-core/src/flet_core/expansion_tile.py index 40daa2f0a..2248b0c1e 100644 --- a/sdk/python/packages/flet-core/src/flet_core/expansion_tile.py +++ b/sdk/python/packages/flet-core/src/flet_core/expansion_tile.py @@ -147,7 +147,7 @@ def _before_build_command(self): self._set_attr_json("expandedAlignment", self.__expanded_alignment) self._set_attr_json("controlsPadding", self.__controls_padding) self._set_attr_json("tilePadding", self.__tile_padding) - self._set_attr_json("Shape", self.__shape) + self._set_attr_json("shape", self.__shape) self._set_attr_json("collapsedShape", self.__collapsed_shape) def _get_children(self):