diff --git a/packages/flet/lib/src/controls/base_controls.dart b/packages/flet/lib/src/controls/base_controls.dart index ee1b74fc04..ba299393a4 100644 --- a/packages/flet/lib/src/controls/base_controls.dart +++ b/packages/flet/lib/src/controls/base_controls.dart @@ -282,11 +282,7 @@ Widget _positionedControl( top: top, right: right, bottom: bottom, - onEnd: control.getBool("on_animation_end", false)! - ? () { - control.triggerEvent("animation_end", "position"); - } - : null, + onEnd: () => control.triggerEvent("animation_end", "position"), child: widget, ); } else if (left != null || top != null || right != null || bottom != null) { @@ -305,25 +301,34 @@ Widget _positionedControl( } Widget _sizedControl(Widget widget, Control control) { - final skipProps = control.internals?["skip_properties"] as List?; - if (skipProps?.contains("width") == true || - skipProps?.contains("height") == true) { + final skipProps = control.internals?['skip_properties'] as List?; + if (skipProps != null && ['width', 'height'].any(skipProps.contains)) { return widget; } - var width = control.getDouble("width"); - var height = control.getDouble("height"); + final width = control.getDouble("width"); + final height = control.getDouble("height"); + final animationSize = control.getAnimation("animate_size"); - if ((width != null || height != null)) { - widget = ConstrainedBox( - constraints: BoxConstraints.tightFor(width: width, height: height), - child: widget, - ); - } - var animation = control.getAnimation("animate_size"); - if (animation != null) { - return AnimatedSize( - duration: animation.duration, curve: animation.curve, child: widget); + final hasFixedSize = width != null || height != null; + + if (animationSize != null) { + return hasFixedSize + ? AnimatedContainer( + duration: animationSize.duration, + curve: animationSize.curve, + width: width, + height: height, + child: widget, + ) + : AnimatedSize( + duration: animationSize.duration, + curve: animationSize.curve, + child: widget, + ); + } else { + return hasFixedSize + ? SizedBox(width: width, height: height, child: widget) + : widget; } - return widget; } diff --git a/packages/flet/lib/src/controls/cupertino_textfield.dart b/packages/flet/lib/src/controls/cupertino_textfield.dart index e49bd8d3a2..c3a718848b 100644 --- a/packages/flet/lib/src/controls/cupertino_textfield.dart +++ b/packages/flet/lib/src/controls/cupertino_textfield.dart @@ -41,11 +41,13 @@ class _CupertinoTextFieldControlState extends State { late final FocusNode _shiftEnterfocusNode; String? _lastFocusValue; String? _lastBlurValue; + TextSelection? _selection; @override void initState() { super.initState(); _controller = TextEditingController(); + _controller.addListener(_handleControllerChange); _shiftEnterfocusNode = FocusNode( onKeyEvent: (FocusNode node, KeyEvent evt) { if (!HardwareKeyboard.instance.isShiftPressed && @@ -67,6 +69,7 @@ class _CupertinoTextFieldControlState extends State { @override void dispose() { + _controller.removeListener(_handleControllerChange); _controller.dispose(); _shiftEnterfocusNode.removeListener(_onShiftEnterFocusChange); _shiftEnterfocusNode.dispose(); @@ -101,6 +104,25 @@ class _CupertinoTextFieldControlState extends State { widget.control.triggerEvent(_focusNode.hasFocus ? "focus" : "blur"); } + void _handleControllerChange() { + final selection = _controller.selection; + if (_selection == selection) return; + + _selection = selection; + + if (!selection.isValid || + !widget.control.getBool("on_selection_change", false)!) { + return; + } + + widget.control.updateProperties({"selection": selection.toMap()}); + widget.control.triggerEvent("selection_change", { + "selected_text": + _controller.text.substring(selection.start, selection.end), + "selection": selection.toMap() + }); + } + @override Widget build(BuildContext context) { debugPrint("CupertinoTextField build: ${widget.control.id}"); @@ -109,7 +131,12 @@ class _CupertinoTextFieldControlState extends State { var value = widget.control.getString("value", "")!; if (_value != value) { _value = value; - _controller.text = value; + _controller.value = TextEditingValue( + text: value, + // preserve cursor position at the end + selection: TextSelection.collapsed(offset: value.length), + ); + _selection = _controller.selection; } var shiftEnter = widget.control.getBool("shift_enter", false)!; @@ -165,6 +192,13 @@ class _CupertinoTextFieldControlState extends State { _focusNode.unfocus(); } + var selection = widget.control.getTextSelection("selection", + minOffset: 0, maxOffset: _controller.text.length); + if (selection != null && selection != _controller.selection) { + _controller.selection = selection; + _selection = selection; + } + var borderRadius = widget.control.getBorderRadius("border_radius"); BoxBorder? border; diff --git a/packages/flet/lib/src/controls/text.dart b/packages/flet/lib/src/controls/text.dart index 1945380a83..70d89ac514 100644 --- a/packages/flet/lib/src/controls/text.dart +++ b/packages/flet/lib/src/controls/text.dart @@ -82,13 +82,12 @@ class TextControl extends StatelessWidget { TextAlign textAlign = parseTextAlign(control.getString("text_align"), TextAlign.start)!; - TextOverflow overflow = parseTextOverflow(control.getString("overflow"), TextOverflow.clip)!; onSelectionChanged(TextSelection selection, SelectionChangedCause? cause) { control.triggerEvent("selection_change", { - "text": text, + "selected_text": text, "cause": cause?.name ?? "unknown", "selection": selection.toMap(), }); diff --git a/packages/flet/lib/src/controls/textfield.dart b/packages/flet/lib/src/controls/textfield.dart index 6e4d78acc1..0bdc707229 100644 --- a/packages/flet/lib/src/controls/textfield.dart +++ b/packages/flet/lib/src/controls/textfield.dart @@ -36,11 +36,13 @@ class _TextFieldControlState extends State { late final FocusNode _shiftEnterfocusNode; String? _lastFocusValue; String? _lastBlurValue; + TextSelection? _selection; @override void initState() { super.initState(); _controller = TextEditingController(); + _controller.addListener(_handleControllerChange); _shiftEnterfocusNode = FocusNode( onKeyEvent: (FocusNode node, KeyEvent evt) { if (!HardwareKeyboard.instance.isShiftPressed && @@ -62,6 +64,7 @@ class _TextFieldControlState extends State { @override void dispose() { + _controller.removeListener(_handleControllerChange); _controller.dispose(); _shiftEnterfocusNode.removeListener(_onShiftEnterFocusChange); _shiftEnterfocusNode.dispose(); @@ -92,6 +95,25 @@ class _TextFieldControlState extends State { widget.control.triggerEvent(_focusNode.hasFocus ? "focus" : "blur"); } + void _handleControllerChange() { + final selection = _controller.selection; + if (_selection == selection) return; + + _selection = selection; + + if (!selection.isValid || + !widget.control.getBool("on_selection_change", false)!) { + return; + } + + widget.control.updateProperties({"selection": selection.toMap()}); + widget.control.triggerEvent("selection_change", { + "selected_text": + _controller.text.substring(selection.start, selection.end), + "selection": selection.toMap() + }); + } + @override Widget build(BuildContext context) { debugPrint("TextField build: ${widget.control.id}"); @@ -103,9 +125,17 @@ class _TextFieldControlState extends State { _value = value; _controller.value = TextEditingValue( text: value, - selection: TextSelection.collapsed( - offset: value.length), // preserve cursor position at the end + // preserve cursor position at the end + selection: TextSelection.collapsed(offset: value.length), ); + _selection = _controller.selection; + } + + var selection = widget.control.getTextSelection("selection", + minOffset: 0, maxOffset: _controller.text.length); + if (selection != null && selection != _controller.selection) { + _controller.selection = selection; + _selection = selection; } var shiftEnter = widget.control.getBool("shift_enter", false)!; diff --git a/packages/flet/lib/src/utils/text.dart b/packages/flet/lib/src/utils/text.dart index 8ed752173c..a559691e0e 100644 --- a/packages/flet/lib/src/utils/text.dart +++ b/packages/flet/lib/src/utils/text.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:collection/collection.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; @@ -12,68 +14,42 @@ import 'material_state.dart'; TextStyle? getTextStyle(BuildContext context, String styleName) { var textTheme = Theme.of(context).textTheme; - switch (styleName.toLowerCase()) { - case "displaylarge": - return textTheme.displayLarge; - case "displaymedium": - return textTheme.displayMedium; - case "displaysmall": - return textTheme.displaySmall; - case "headlinelarge": - return textTheme.headlineLarge; - case "headlinemedium": - return textTheme.headlineMedium; - case "headlinesmall": - return textTheme.headlineSmall; - case "titlelarge": - return textTheme.titleLarge; - case "titlemedium": - return textTheme.titleMedium; - case "titlesmall": - return textTheme.titleSmall; - case "labellarge": - return textTheme.labelLarge; - case "labelmedium": - return textTheme.labelMedium; - case "labelsmall": - return textTheme.labelSmall; - case "bodylarge": - return textTheme.bodyLarge; - case "bodymedium": - return textTheme.bodyMedium; - case "bodysmall": - return textTheme.bodySmall; - } - return null; + final styles = { + "displaylarge": textTheme.displayLarge, + "displaymedium": textTheme.displayMedium, + "displaysmall": textTheme.displaySmall, + "headlinelarge": textTheme.headlineLarge, + "headlinemedium": textTheme.headlineMedium, + "headlinesmall": textTheme.headlineSmall, + "titlelarge": textTheme.titleLarge, + "titlemedium": textTheme.titleMedium, + "titlesmall": textTheme.titleSmall, + "labellarge": textTheme.labelLarge, + "labelmedium": textTheme.labelMedium, + "labelsmall": textTheme.labelSmall, + "bodylarge": textTheme.bodyLarge, + "bodymedium": textTheme.bodyMedium, + "bodysmall": textTheme.bodySmall, + }; + return styles[styleName.toLowerCase()]; } FontWeight? getFontWeight(String? weightName, [FontWeight? defaultWeight]) { - switch (weightName?.toLowerCase()) { - case "normal": - return FontWeight.normal; - case "bold": - return FontWeight.bold; - case "w100": - return FontWeight.w100; - case "w200": - return FontWeight.w200; - case "w300": - return FontWeight.w300; - case "w400": - return FontWeight.w400; - case "w500": - return FontWeight.w500; - case "w600": - return FontWeight.w600; - case "w700": - return FontWeight.w700; - case "w800": - return FontWeight.w800; - case "w900": - return FontWeight.w900; - default: - return defaultWeight; - } + if (weightName == null) return defaultWeight; + final weights = { + "normal": FontWeight.normal, + "bold": FontWeight.bold, + "w100": FontWeight.w100, + "w200": FontWeight.w200, + "w300": FontWeight.w300, + "w400": FontWeight.w400, + "w500": FontWeight.w500, + "w600": FontWeight.w600, + "w700": FontWeight.w700, + "w800": FontWeight.w800, + "w900": FontWeight.w900, + }; + return weights[weightName.toLowerCase()] ?? defaultWeight; } List parseTextSpans(List spans, ThemeData theme, @@ -157,6 +133,13 @@ TextBaseline? parseTextBaseline(String? value, [TextBaseline? defaultValue]) { defaultValue; } +TextAffinity? parseTextAffinity(String? value, [TextAffinity? defaultValue]) { + if (value == null) return defaultValue; + return TextAffinity.values.firstWhereOrNull( + (a) => a.name.toLowerCase() == value.toLowerCase()) ?? + defaultValue; +} + TextStyle? parseTextStyle(dynamic value, ThemeData theme, [TextStyle? defaultValue]) { if (value == null) return defaultValue; @@ -213,6 +196,35 @@ WidgetStateProperty? parseWidgetStateTextStyle( value, (jv) => parseTextStyle(jv, theme), defaultTextStyle); } +TextSelection? parseTextSelection( + dynamic value, { + int? minOffset, + int? maxOffset, + TextSelection? defaultValue, +}) { + if (value == null) return defaultValue; + + int baseOffset = parseInt(value['base_offset'], 0)!; + int extentOffset = parseInt(value['extent_offset'], 0)!; + + // Clamp values if limits are provided + if (minOffset != null) { + baseOffset = max(baseOffset, minOffset); + extentOffset = max(extentOffset, minOffset); + } + if (maxOffset != null) { + baseOffset = min(baseOffset, maxOffset); + extentOffset = min(extentOffset, maxOffset); + } + + return TextSelection( + baseOffset: baseOffset, + extentOffset: extentOffset, + affinity: parseTextAffinity(value['affinity'], TextAffinity.downstream)!, + isDirectional: parseBool(value['directional'], false)!, + ); +} + extension TextParsers on Control { TextStyle? getTextStyle(String propertyName, ThemeData theme, [TextStyle? defaultValue]) { @@ -243,6 +255,21 @@ extension TextParsers on Control { return parseTextBaseline(get(propertyName), defaultValue); } + TextAffinity? getTextAffinity(String propertyName, + [TextAffinity? defaultValue]) { + return parseTextAffinity(get(propertyName), defaultValue); + } + + TextSelection? getTextSelection( + String propertyName, { + int? minOffset, + int? maxOffset, + TextSelection? defaultValue, + }) { + return parseTextSelection(get(propertyName), + minOffset: minOffset, maxOffset: maxOffset, defaultValue: defaultValue); + } + WidgetStateProperty? getWidgetStateTextStyle( String propertyName, ThemeData theme, {TextStyle? defaultTextStyle, @@ -254,14 +281,9 @@ extension TextParsers on Control { extension TextSelectionExtension on TextSelection { Map toMap() => { - "start": start, - "end": end, "base_offset": baseOffset, "extent_offset": extentOffset, "affinity": affinity.name, "directional": isDirectional, - "collapsed": isCollapsed, - "valid": isValid, - "normalized": isNormalized, }; } diff --git a/sdk/python/examples/controls/cupertino_text_field/label_focus.py b/sdk/python/examples/controls/cupertino_text_field/label_focus.py deleted file mode 100644 index 7185848841..0000000000 --- a/sdk/python/examples/controls/cupertino_text_field/label_focus.py +++ /dev/null @@ -1,14 +0,0 @@ -import flet as ft - - -async def main(page: ft.Page): - page.theme_mode = ft.ThemeMode.LIGHT - page.add( - ctf := ft.CupertinoTextField( - label="Textfield Label", - ) - ) - await ctf.focus() - - -ft.run(main) diff --git a/sdk/python/examples/controls/cupertino_text_field/selection_change.py b/sdk/python/examples/controls/cupertino_text_field/selection_change.py new file mode 100644 index 0000000000..d44e9be939 --- /dev/null +++ b/sdk/python/examples/controls/cupertino_text_field/selection_change.py @@ -0,0 +1,52 @@ +import flet as ft + + +def main(page: ft.Page): + page.title = "Text selection" + + def handle_selection_change(e: ft.TextSelectionChangeEvent[ft.CupertinoTextField]): + selection.value = ( + f"Selection: '{e.selected_text}'" if e.selected_text else "No selection." + ) + selection_details.value = f"start={e.selection.start}, end={e.selection.end}" + caret.value = f"Caret position: {e.selection.end}" + + async def select_characters(e: ft.Event[ft.Button]): + await field.focus() + field.selection = ft.TextSelection( + base_offset=0, extent_offset=len(field.value) + ) + + async def move_caret(e: ft.Event[ft.Button]): + await field.focus() + field.selection = ft.TextSelection(base_offset=0, extent_offset=0) + + page.add( + ft.Column( + spacing=10, + controls=[ + field := ft.CupertinoTextField( + value="Lorem ipsum dolor sit amet, consectetur adipiscing elit.", + multiline=True, + min_lines=3, + autofocus=True, + on_selection_change=handle_selection_change, + ), + selection := ft.Text("Select some text from the field."), + selection_details := ft.Text(), + caret := ft.Text("Caret position: -"), + ft.Button( + content="Select all text", + on_click=select_characters, + ), + ft.Button( + content="Move caret to start", + on_click=move_caret, + ), + ], + ) + ) + + +if __name__ == "__main__": + ft.run(main) diff --git a/sdk/python/examples/controls/text_field/selection_change.py b/sdk/python/examples/controls/text_field/selection_change.py new file mode 100644 index 0000000000..43d6fa5abe --- /dev/null +++ b/sdk/python/examples/controls/text_field/selection_change.py @@ -0,0 +1,52 @@ +import flet as ft + + +def main(page: ft.Page): + page.title = "Text selection" + + def handle_selection_change(e: ft.TextSelectionChangeEvent[ft.TextField]): + selection.value = ( + f"Selection: '{e.selected_text}'" if e.selected_text else "No selection." + ) + selection_details.value = f"start={e.selection.start}, end={e.selection.end}" + caret.value = f"Caret position: {e.selection.end}" + + async def select_characters(e: ft.Event[ft.Button]): + await field.focus() + field.selection = ft.TextSelection( + base_offset=0, extent_offset=len(field.value) + ) + + async def move_caret(e: ft.Event[ft.Button]): + await field.focus() + field.selection = ft.TextSelection(base_offset=0, extent_offset=0) + + page.add( + ft.Column( + spacing=10, + controls=[ + field := ft.TextField( + value="Lorem ipsum dolor sit amet, consectetur adipiscing elit.", + multiline=True, + min_lines=3, + autofocus=True, + on_selection_change=handle_selection_change, + ), + selection := ft.Text("Select some text from the field."), + selection_details := ft.Text(), + caret := ft.Text("Caret position: -"), + ft.Button( + content="Select all text", + on_click=select_characters, + ), + ft.Button( + content="Move caret to start", + on_click=move_caret, + ), + ], + ) + ) + + +if __name__ == "__main__": + ft.run(main) diff --git a/sdk/python/packages/flet-map/src/flet_map/marker_layer.py b/sdk/python/packages/flet-map/src/flet_map/marker_layer.py index f16281af3f..34133c0070 100644 --- a/sdk/python/packages/flet-map/src/flet_map/marker_layer.py +++ b/sdk/python/packages/flet-map/src/flet_map/marker_layer.py @@ -28,7 +28,7 @@ class Marker(ft.Control): The coordinates of the marker. This will be the center of the marker, - if [`alignment`][(c).] is [`Alignment.CENTER`][flet.Alignment.]. + if [`alignment`][(c).] is [`Alignment.CENTER`][flet.]. """ rotate: Optional[bool] = None diff --git a/sdk/python/packages/flet/docs/controls/cupertinotextfield.md b/sdk/python/packages/flet/docs/controls/cupertinotextfield.md index c1d6b24199..47519e59ac 100644 --- a/sdk/python/packages/flet/docs/controls/cupertinotextfield.md +++ b/sdk/python/packages/flet/docs/controls/cupertinotextfield.md @@ -19,5 +19,16 @@ example_media: ../examples/controls/cupertino_text_field/media {{ image(example_media + "/cupertino_material_and_adaptive.png", alt="cupertino-material-and-adaptive", width="80%") }} +### Handling selection changes + +```python +--8<-- "{{ examples }}/selection_change.py" +``` + +### Background image + +```python +--8<-- "{{ examples }}/background_image.py" +``` {{ class_members(class_name) }} diff --git a/sdk/python/packages/flet/docs/controls/textfield.md b/sdk/python/packages/flet/docs/controls/textfield.md index 0dcdacaeed..20529fb418 100644 --- a/sdk/python/packages/flet/docs/controls/textfield.md +++ b/sdk/python/packages/flet/docs/controls/textfield.md @@ -28,6 +28,11 @@ example_media: ../examples/controls/text_field/media {{ image(example_media + "/handling_change_events.gif", alt="handling-change-events", width="80%") }} +### Handling selection changes + +```python +--8<-- "{{ examples }}/selection_change.py" +``` ### Password with reveal button diff --git a/sdk/python/packages/flet/mkdocs.yml b/sdk/python/packages/flet/mkdocs.yml index b235adc9e6..43fddb58eb 100644 --- a/sdk/python/packages/flet/mkdocs.yml +++ b/sdk/python/packages/flet/mkdocs.yml @@ -131,7 +131,7 @@ plugins: show_source: false group_by_category: true show_category_heading: true - show_labels: false + show_labels: true show_if_no_docstring: true docstring_section_style: list separate_signature: true diff --git a/sdk/python/packages/flet/src/flet/controls/core/text.py b/sdk/python/packages/flet/src/flet/controls/core/text.py index db7dc12c7c..b1ecabace8 100644 --- a/sdk/python/packages/flet/src/flet/controls/core/text.py +++ b/sdk/python/packages/flet/src/flet/controls/core/text.py @@ -51,56 +51,96 @@ class TextSelection: A range of text that represents a selection. """ - start: Optional[int] = None - """ - The index of the first character in the range. - """ - - end: Optional[int] = None - """ - The next index after the characters in this range. - """ - - selection: Optional[str] = None - """ - The text string that is selected. - """ - - base_offset: Optional[int] = None + base_offset: int """ The offset at which the selection originates. """ - extent_offset: Optional[int] = None + extent_offset: int """ The offset at which the selection terminates. """ - affinity: Optional["TextAffinity"] = None + affinity: "TextAffinity" = TextAffinity.DOWNSTREAM """ If the text range is collapsed and has more than one visual location (e.g., occurs at a line break), which of the two locations to use when painting the caret. """ - directional: Optional[bool] = None + directional: bool = False """ Whether this selection has disambiguated its base and extent. """ - collapsed: Optional[bool] = None - """ - Whether this range is empty (but still potentially placed inside the text). - """ - - valid: Optional[bool] = None - """ - Whether this range represents a valid position in the text. - """ - - normalized: Optional[bool] = None - """ - Whether the start of this range precedes the end. - """ + @property + def start(self) -> int: + """ + The index of the first character in the range. + + Note: + This property is read-only. + """ + if self.base_offset < self.extent_offset: + return self.base_offset + else: + return self.extent_offset + + @property + def end(self) -> int: + """ + The next index after the characters in this range. + + Note: + This property is read-only. + """ + if self.base_offset < self.extent_offset: + return self.extent_offset + else: + return self.base_offset + + @property + def is_valid(self) -> bool: + """ + Whether this range represents a valid position in the text. + + Note: + This property is read-only. + """ + return self.start >= 0 and self.end >= 0 + + @property + def is_collapsed(self) -> bool: + """ + Whether this range is empty (but still potentially placed inside the text). + + Note: + This property is read-only. + """ + return self.start == self.end + + @property + def is_normalized(self) -> bool: + """ + Whether the start of this range precedes the end. + + Note: + This property is read-only. + """ + return self.start <= self.end + + def get_selected_text(self, source_text: str) -> str: + """ + Returns the selected text from the given full text. + + Args: + source_text: The full text to get the selection from. + + Raises: + AssertionError: If the selection is not valid, + i.e. [`is_valid`][(c).] is `False`. + """ + assert self.is_valid + return source_text[self.start : self.end] class TextSelectionChangeCause(Enum): @@ -166,9 +206,16 @@ class TextSelectionChangeCause(Enum): @dataclass class TextSelectionChangeEvent(Event[EventControlType]): - text: str - cause: TextSelectionChangeCause + """An event emitted when the text selection changes.""" + + selected_text: str + """The selected text.""" + selection: TextSelection + """The new text selection.""" + + cause: Optional[TextSelectionChangeCause] = None + """The cause of the selection change.""" @control("Text") diff --git a/sdk/python/packages/flet/src/flet/controls/material/form_field_control.py b/sdk/python/packages/flet/src/flet/controls/material/form_field_control.py index c273271380..116649f7a5 100644 --- a/sdk/python/packages/flet/src/flet/controls/material/form_field_control.py +++ b/sdk/python/packages/flet/src/flet/controls/material/form_field_control.py @@ -324,4 +324,14 @@ class FormFieldControl(LayoutControl): """ async def focus(self): + """ + Request focus for this control. + + Example: + ```python + async def main(page: ft.Page): + page.add(ctf := ft.TextField()) + await ctf.focus() + ``` + """ await self._invoke_method("focus") diff --git a/sdk/python/packages/flet/src/flet/controls/material/textfield.py b/sdk/python/packages/flet/src/flet/controls/material/textfield.py index c4edf4908f..31326af67f 100644 --- a/sdk/python/packages/flet/src/flet/controls/material/textfield.py +++ b/sdk/python/packages/flet/src/flet/controls/material/textfield.py @@ -4,8 +4,9 @@ from flet.controls.adaptive_control import AdaptiveControl from flet.controls.base_control import BaseControl, control -from flet.controls.control_event import ControlEventHandler +from flet.controls.control_event import ControlEventHandler, EventHandler from flet.controls.core.autofill_group import AutofillHint +from flet.controls.core.text import TextSelection, TextSelectionChangeEvent from flet.controls.material.form_field_control import FormFieldControl from flet.controls.padding import PaddingValue from flet.controls.text_style import StrutStyle @@ -144,6 +145,19 @@ class TextField(FormFieldControl, AdaptiveControl): Current value of the TextField. """ + selection: Optional[TextSelection] = None + """ + Represents the current text selection or caret position in the field. + + When the user selects text, this property is updated to reflect the selected range. + If no text is selected, it contains an empty range indicating the caret position. + + Setting this property visually updates the field's selection to match the given + value, and hence leads to the [`on_selection_change`][(c).] event being triggered. + To ensure the selection is visible and the event is fired, the text field must + be focused. Call [`focus()`][(c).focus] on the field before setting this property. + """ + keyboard_type: KeyboardType = KeyboardType.TEXT """ The type of keyboard to use for editing the text. @@ -151,7 +165,7 @@ class TextField(FormFieldControl, AdaptiveControl): multiline: bool = False """ - `True` if TextField can contain multiple lines of text. + Whether this field can contain multiple lines of text. """ min_lines: Optional[int] = None @@ -400,7 +414,8 @@ class TextField(FormFieldControl, AdaptiveControl): """ Helps the autofill service identify the type of this text input. - More information [here](https://api.flutter.dev/flutter/material/TextField/autofillHints.html). + More information + [here](https://api.flutter.dev/flutter/material/TextField/autofillHints.html). """ on_change: Optional[ControlEventHandler["TextField"]] = None @@ -408,6 +423,16 @@ class TextField(FormFieldControl, AdaptiveControl): Called when the typed input for the TextField has changed. """ + on_selection_change: Optional[ + EventHandler[TextSelectionChangeEvent["TextField"]] + ] = None + """ + Called when the text selection or caret position changes. + + This can be triggered either by user interaction (selecting text or moving + the caret) or programmatically (through the [`selection`][(c).] property). + """ + on_click: Optional[ControlEventHandler["TextField"]] = None """ TBD diff --git a/sdk/python/packages/flet/src/flet/controls/services/file_picker.py b/sdk/python/packages/flet/src/flet/controls/services/file_picker.py index 64ad3d332d..5f4de3d141 100644 --- a/sdk/python/packages/flet/src/flet/controls/services/file_picker.py +++ b/sdk/python/packages/flet/src/flet/controls/services/file_picker.py @@ -29,8 +29,7 @@ class FilePickerFileType(Enum): MEDIA = "media" """ - A combination of [`VIDEO`][(c).] and - [`IMAGE`][(c).]. + A combination of [`VIDEO`][(c).] and [`IMAGE`][(c).]. """ IMAGE = "image" @@ -158,8 +157,11 @@ async def get_directory_path( dialog_title: The title of the dialog window. Defaults to [`FilePicker. initial_directory: The initial directory where the dialog should open. + Returns: + The selected directory path or `None` if the dialog was cancelled. + Raises: - NotImplementedError: If called in web app. + FletUnsupportedPlatformException: If called in web mode. """ if self.page.web: raise FletUnsupportedPlatformException( @@ -213,7 +215,7 @@ async def save_file( if (self.page.web or self.page.platform.is_mobile()) and not src_bytes: raise ValueError( '"src_bytes" is required when saving a file in web mode,' - "on Android and iOS." + "or on mobile (Android & iOS)." ) if self.page.web and not file_name: raise ValueError('"file_name" is required when saving a file in web mode.') @@ -248,8 +250,10 @@ async def pick_files( file_type: The file types allowed to be selected. allow_multiple: Allow the selection of multiple files at once. allowed_extensions: The allowed file extensions. Has effect only if - `file_type` is - [`FilePickerFileType.CUSTOM`][flet.]. + `file_type` is [`FilePickerFileType.CUSTOM`][flet.]. + + Returns: + A list of selected files. """ files = await self._invoke_method( "pick_files",