diff --git a/lib/common/widgets/flutter/text_field/adaptive_text_selection_toolbar.dart b/lib/common/widgets/flutter/text_field/adaptive_text_selection_toolbar.dart index b94765544..215e42d3b 100644 --- a/lib/common/widgets/flutter/text_field/adaptive_text_selection_toolbar.dart +++ b/lib/common/widgets/flutter/text_field/adaptive_text_selection_toolbar.dart @@ -311,8 +311,7 @@ class AdaptiveTextSelectionToolbar extends StatelessWidget { @override Widget build(BuildContext context) { // If there aren't any buttons to build, build an empty toolbar. - if ((children != null && children!.isEmpty) || - (buttonItems != null && buttonItems!.isEmpty)) { + if ((children ?? buttonItems)?.isEmpty ?? true) { return const SizedBox.shrink(); } diff --git a/lib/common/widgets/flutter/text_field/cupertino/cupertino_adaptive_text_selection_toolbar.dart b/lib/common/widgets/flutter/text_field/cupertino/adaptive_text_selection_toolbar.dart similarity index 100% rename from lib/common/widgets/flutter/text_field/cupertino/cupertino_adaptive_text_selection_toolbar.dart rename to lib/common/widgets/flutter/text_field/cupertino/adaptive_text_selection_toolbar.dart diff --git a/lib/common/widgets/flutter/text_field/cupertino/cupertino_spell_check_suggestions_toolbar.dart b/lib/common/widgets/flutter/text_field/cupertino/spell_check_suggestions_toolbar.dart similarity index 100% rename from lib/common/widgets/flutter/text_field/cupertino/cupertino_spell_check_suggestions_toolbar.dart rename to lib/common/widgets/flutter/text_field/cupertino/spell_check_suggestions_toolbar.dart diff --git a/lib/common/widgets/flutter/text_field/cupertino/cupertino_text_field.dart b/lib/common/widgets/flutter/text_field/cupertino/text_field.dart similarity index 78% rename from lib/common/widgets/flutter/text_field/cupertino/cupertino_text_field.dart rename to lib/common/widgets/flutter/text_field/cupertino/text_field.dart index 626adf280..7e4de43aa 100644 --- a/lib/common/widgets/flutter/text_field/cupertino/cupertino_text_field.dart +++ b/lib/common/widgets/flutter/text_field/cupertino/text_field.dart @@ -5,24 +5,25 @@ /// @docImport 'package:flutter/material.dart'; library; +import 'dart:math' as math; import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle; import 'package:PiliPlus/common/widgets/flutter/text_field/controller.dart'; -import 'package:PiliPlus/common/widgets/flutter/text_field/cupertino/cupertino_adaptive_text_selection_toolbar.dart'; -import 'package:PiliPlus/common/widgets/flutter/text_field/cupertino/cupertino_spell_check_suggestions_toolbar.dart'; +import 'package:PiliPlus/common/widgets/flutter/text_field/cupertino/adaptive_text_selection_toolbar.dart'; +import 'package:PiliPlus/common/widgets/flutter/text_field/cupertino/spell_check_suggestions_toolbar.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/spell_check.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/system_context_menu.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/text_selection.dart'; import 'package:flutter/cupertino.dart' hide - SpellCheckConfiguration, - EditableTextContextMenuBuilder, EditableText, EditableTextState, - SystemContextMenu, CupertinoSpellCheckSuggestionsToolbar, + EditableTextContextMenuBuilder, + SystemContextMenu, CupertinoAdaptiveTextSelectionToolbar, + SpellCheckConfiguration, TextSelectionGestureDetectorBuilderDelegate, TextSelectionGestureDetectorBuilder, TextSelectionOverlay; @@ -31,14 +32,6 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; -export 'package:flutter/services.dart' - show - SmartDashesType, - SmartQuotesType, - TextCapitalization, - TextInputAction, - TextInputType; - const TextStyle _kDefaultPlaceholderStyle = TextStyle( fontWeight: FontWeight.w400, color: CupertinoColors.placeholderText, @@ -89,30 +82,6 @@ const CupertinoDynamicColor _kClearButtonColor = // throughout the codebase. const int _iOSHorizontalCursorOffsetPixels = -2; -/// Visibility of text field overlays based on the state of the current text entry. -/// -/// Used to toggle the visibility behavior of the optional decorating widgets -/// surrounding the [EditableText] such as the clear text button. -enum OverlayVisibilityMode { - /// Overlay will never appear regardless of the text entry state. - never, - - /// Overlay will only appear when the current text entry is not empty. - /// - /// This includes prefilled text that the user did not type in manually. But - /// does not include text in placeholders. - editing, - - /// Overlay will only appear when the current text entry is empty. - /// - /// This also includes not having prefilled text that the user did not type - /// in manually. Texts in placeholders are ignored. - notEditing, - - /// Always show the overlay regardless of the text entry state. - always, -} - class _CupertinoTextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDetectorBuilder { _CupertinoTextFieldSelectionGestureDetectorBuilder({ @@ -226,7 +195,8 @@ class CupertinoRichTextField extends StatefulWidget { /// /// The [selectionHeightStyle] and [selectionWidthStyle] properties allow /// changing the shape of the selection highlighting. These properties default - /// to [ui.BoxHeightStyle.tight] and [ui.BoxWidthStyle.tight], respectively. + /// to [EditableText.defaultSelectionHeightStyle] and + /// [EditableText.defaultSelectionWidthStyle], respectively. /// /// The [autocorrect], [autofocus], [clearButtonMode], [dragStartBehavior], /// [expands], [obscureText], [prefixMode], [readOnly], [scrollPadding], @@ -248,7 +218,6 @@ class CupertinoRichTextField extends StatefulWidget { this.groupId = EditableText, required this.controller, this.focusNode, - this.undoController, this.decoration = _kDefaultRoundedBorderDecoration, this.padding = const EdgeInsets.all(7.0), this.placeholder, @@ -302,12 +271,13 @@ class CupertinoRichTextField extends StatefulWidget { this.cursorRadius = const Radius.circular(2.0), this.cursorOpacityAnimates = true, this.cursorColor, - this.selectionHeightStyle = ui.BoxHeightStyle.tight, - this.selectionWidthStyle = ui.BoxWidthStyle.tight, + this.selectionHeightStyle, + this.selectionWidthStyle, this.keyboardAppearance, this.scrollPadding = const EdgeInsets.all(20.0), this.dragStartBehavior = DragStartBehavior.start, bool? enableInteractiveSelection, + this.selectAllOnFocus, this.selectionControls, this.onTap, this.scrollController, @@ -397,7 +367,6 @@ class CupertinoRichTextField extends StatefulWidget { this.groupId = EditableText, required this.controller, this.focusNode, - this.undoController, this.decoration, this.padding = const EdgeInsets.all(7.0), this.placeholder, @@ -427,7 +396,7 @@ class CupertinoRichTextField extends StatefulWidget { this.autofocus = false, this.obscuringCharacter = '•', this.obscureText = false, - this.autocorrect = true, + this.autocorrect, SmartDashesType? smartDashesType, SmartQuotesType? smartQuotesType, this.enableSuggestions = true, @@ -448,12 +417,13 @@ class CupertinoRichTextField extends StatefulWidget { this.cursorRadius = const Radius.circular(2.0), this.cursorOpacityAnimates = true, this.cursorColor, - this.selectionHeightStyle = ui.BoxHeightStyle.tight, - this.selectionWidthStyle = ui.BoxWidthStyle.tight, + this.selectionHeightStyle, + this.selectionWidthStyle, this.keyboardAppearance, this.scrollPadding = const EdgeInsets.all(20.0), this.dragStartBehavior = DragStartBehavior.start, bool? enableInteractiveSelection, + this.selectAllOnFocus, this.selectionControls, this.onTap, this.scrollController, @@ -653,7 +623,7 @@ class CupertinoRichTextField extends StatefulWidget { final bool obscureText; /// {@macro flutter.widgets.editableText.autocorrect} - final bool autocorrect; + final bool? autocorrect; /// {@macro flutter.services.TextInputConfiguration.smartDashesType} final SmartDashesType smartDashesType; @@ -763,12 +733,12 @@ class CupertinoRichTextField extends StatefulWidget { /// Controls how tall the selection highlight boxes are computed to be. /// /// See [ui.BoxHeightStyle] for details on available styles. - final ui.BoxHeightStyle selectionHeightStyle; + final ui.BoxHeightStyle? selectionHeightStyle; /// Controls how wide the selection highlight boxes are computed to be. /// /// See [ui.BoxWidthStyle] for details on available styles. - final ui.BoxWidthStyle selectionWidthStyle; + final ui.BoxWidthStyle? selectionWidthStyle; /// The appearance of the keyboard. /// @@ -783,6 +753,9 @@ class CupertinoRichTextField extends StatefulWidget { /// {@macro flutter.widgets.editableText.enableInteractiveSelection} final bool enableInteractiveSelection; + /// {@macro flutter.widgets.editableText.selectAllOnFocus} + final bool? selectAllOnFocus; + /// {@macro flutter.widgets.editableText.selectionControls} final TextSelectionControls? selectionControls; @@ -842,8 +815,7 @@ class CupertinoRichTextField extends StatefulWidget { BuildContext context, EditableTextState editableTextState, ) { - if (defaultTargetPlatform == TargetPlatform.iOS && - SystemContextMenu.isSupported(context)) { + if (SystemContextMenu.isSupportedByField(editableTextState)) { return SystemContextMenu.editableText( editableTextState: editableTextState, ); @@ -914,257 +886,252 @@ class CupertinoRichTextField extends StatefulWidget { ); } - /// {@macro flutter.widgets.undoHistory.controller} - final UndoHistoryController? undoController; - @override State createState() => _CupertinoRichTextFieldState(); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); - properties.add( - DiagnosticsProperty( - 'controller', - controller, - defaultValue: null, - ), - ); - properties.add( - DiagnosticsProperty( - 'focusNode', - focusNode, - defaultValue: null, - ), - ); - properties.add( - DiagnosticsProperty( - 'undoController', - undoController, - defaultValue: null, - ), - ); - properties.add( - DiagnosticsProperty('decoration', decoration), - ); - properties.add(DiagnosticsProperty('padding', padding)); - properties.add(StringProperty('placeholder', placeholder)); - properties.add( - DiagnosticsProperty('placeholderStyle', placeholderStyle), - ); - properties.add( - DiagnosticsProperty( - 'prefix', - prefix == null ? null : prefixMode, - ), - ); - properties.add( - DiagnosticsProperty( - 'suffix', - suffix == null ? null : suffixMode, - ), - ); - properties.add( - DiagnosticsProperty( - 'clearButtonMode', - clearButtonMode, - ), - ); - properties.add( - DiagnosticsProperty( - 'clearButtonSemanticLabel', - clearButtonSemanticLabel, - ), - ); - properties.add( - DiagnosticsProperty( - 'keyboardType', - keyboardType, - defaultValue: TextInputType.text, - ), - ); - properties.add( - DiagnosticsProperty('style', style, defaultValue: null), - ); - properties.add( - DiagnosticsProperty('autofocus', autofocus, defaultValue: false), - ); - properties.add( - DiagnosticsProperty( - 'obscuringCharacter', - obscuringCharacter, - defaultValue: '•', - ), - ); - properties.add( - DiagnosticsProperty( - 'obscureText', - obscureText, - defaultValue: false, - ), - ); - properties.add( - DiagnosticsProperty('autocorrect', autocorrect, defaultValue: true), - ); - properties.add( - EnumProperty( - 'smartDashesType', - smartDashesType, - defaultValue: obscureText - ? SmartDashesType.disabled - : SmartDashesType.enabled, - ), - ); - properties.add( - EnumProperty( - 'smartQuotesType', - smartQuotesType, - defaultValue: obscureText - ? SmartQuotesType.disabled - : SmartQuotesType.enabled, - ), - ); - properties.add( - DiagnosticsProperty( - 'enableSuggestions', - enableSuggestions, - defaultValue: true, - ), - ); - properties.add(IntProperty('maxLines', maxLines, defaultValue: 1)); - properties.add(IntProperty('minLines', minLines, defaultValue: null)); - properties.add( - DiagnosticsProperty('expands', expands, defaultValue: false), - ); - properties.add(IntProperty('maxLength', maxLength, defaultValue: null)); - properties.add( - EnumProperty( - 'maxLengthEnforcement', - maxLengthEnforcement, - defaultValue: null, - ), - ); - properties.add( - DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0), - ); - properties.add( - DoubleProperty('cursorHeight', cursorHeight, defaultValue: null), - ); - properties.add( - DiagnosticsProperty( - 'cursorRadius', - cursorRadius, - defaultValue: null, - ), - ); - properties.add( - DiagnosticsProperty( - 'cursorOpacityAnimates', - cursorOpacityAnimates, - defaultValue: true, - ), - ); - properties.add( - createCupertinoColorProperty( - 'cursorColor', - cursorColor, - defaultValue: null, - ), - ); - properties.add( - FlagProperty( - 'selectionEnabled', - value: selectionEnabled, - defaultValue: true, - ifFalse: 'selection disabled', - ), - ); - properties.add( - DiagnosticsProperty( - 'selectionControls', - selectionControls, - defaultValue: null, - ), - ); - properties.add( - DiagnosticsProperty( - 'scrollController', - scrollController, - defaultValue: null, - ), - ); - properties.add( - DiagnosticsProperty( - 'scrollPhysics', - scrollPhysics, - defaultValue: null, - ), - ); - properties.add( - EnumProperty( - 'textAlign', - textAlign, - defaultValue: TextAlign.start, - ), - ); - properties.add( - DiagnosticsProperty( - 'textAlignVertical', - textAlignVertical, - defaultValue: null, - ), - ); - properties.add( - EnumProperty( - 'textDirection', - textDirection, - defaultValue: null, - ), - ); - properties.add( - DiagnosticsProperty( - 'clipBehavior', - clipBehavior, - defaultValue: Clip.hardEdge, - ), - ); - properties.add( - DiagnosticsProperty( - 'scribbleEnabled', - scribbleEnabled, - defaultValue: true, - ), - ); - properties.add( - DiagnosticsProperty( - 'stylusHandwritingEnabled', - stylusHandwritingEnabled, - defaultValue: EditableText.defaultStylusHandwritingEnabled, - ), - ); - properties.add( - DiagnosticsProperty( - 'enableIMEPersonalizedLearning', - enableIMEPersonalizedLearning, - defaultValue: true, - ), - ); - properties.add( - DiagnosticsProperty( - 'spellCheckConfiguration', - spellCheckConfiguration, - defaultValue: null, - ), - ); - properties.add( - DiagnosticsProperty>( - 'contentCommitMimeTypes', - contentInsertionConfiguration?.allowedMimeTypes ?? const [], - defaultValue: contentInsertionConfiguration == null - ? const [] - : kDefaultContentInsertionMimeTypes, - ), - ); + properties + ..add( + DiagnosticsProperty( + 'controller', + controller, + defaultValue: null, + ), + ) + ..add( + DiagnosticsProperty( + 'focusNode', + focusNode, + defaultValue: null, + ), + ) + ..add( + DiagnosticsProperty('decoration', decoration), + ) + ..add(DiagnosticsProperty('padding', padding)) + ..add(StringProperty('placeholder', placeholder)) + ..add( + DiagnosticsProperty('placeholderStyle', placeholderStyle), + ) + ..add( + DiagnosticsProperty( + 'prefix', + prefix == null ? null : prefixMode, + ), + ) + ..add( + DiagnosticsProperty( + 'suffix', + suffix == null ? null : suffixMode, + ), + ) + ..add( + DiagnosticsProperty( + 'clearButtonMode', + clearButtonMode, + ), + ) + ..add( + DiagnosticsProperty( + 'clearButtonSemanticLabel', + clearButtonSemanticLabel, + ), + ) + ..add( + DiagnosticsProperty( + 'keyboardType', + keyboardType, + defaultValue: TextInputType.text, + ), + ) + ..add( + DiagnosticsProperty('style', style, defaultValue: null), + ) + ..add( + DiagnosticsProperty('autofocus', autofocus, defaultValue: false), + ) + ..add( + DiagnosticsProperty( + 'obscuringCharacter', + obscuringCharacter, + defaultValue: '•', + ), + ) + ..add( + DiagnosticsProperty( + 'obscureText', + obscureText, + defaultValue: false, + ), + ) + ..add( + DiagnosticsProperty( + 'autocorrect', + autocorrect, + defaultValue: null, + ), + ) + ..add( + EnumProperty( + 'smartDashesType', + smartDashesType, + defaultValue: obscureText + ? SmartDashesType.disabled + : SmartDashesType.enabled, + ), + ) + ..add( + EnumProperty( + 'smartQuotesType', + smartQuotesType, + defaultValue: obscureText + ? SmartQuotesType.disabled + : SmartQuotesType.enabled, + ), + ) + ..add( + DiagnosticsProperty( + 'enableSuggestions', + enableSuggestions, + defaultValue: true, + ), + ) + ..add(IntProperty('maxLines', maxLines, defaultValue: 1)) + ..add(IntProperty('minLines', minLines, defaultValue: null)) + ..add( + DiagnosticsProperty('expands', expands, defaultValue: false), + ) + ..add(IntProperty('maxLength', maxLength, defaultValue: null)) + ..add( + EnumProperty( + 'maxLengthEnforcement', + maxLengthEnforcement, + defaultValue: null, + ), + ) + ..add( + DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0), + ) + ..add( + DoubleProperty('cursorHeight', cursorHeight, defaultValue: null), + ) + ..add( + DiagnosticsProperty( + 'cursorRadius', + cursorRadius, + defaultValue: null, + ), + ) + ..add( + DiagnosticsProperty( + 'cursorOpacityAnimates', + cursorOpacityAnimates, + defaultValue: true, + ), + ) + ..add( + createCupertinoColorProperty( + 'cursorColor', + cursorColor, + defaultValue: null, + ), + ) + ..add( + FlagProperty( + 'selectionEnabled', + value: selectionEnabled, + defaultValue: true, + ifFalse: 'selection disabled', + ), + ) + ..add( + DiagnosticsProperty( + 'selectionControls', + selectionControls, + defaultValue: null, + ), + ) + ..add( + DiagnosticsProperty( + 'scrollController', + scrollController, + defaultValue: null, + ), + ) + ..add( + DiagnosticsProperty( + 'scrollPhysics', + scrollPhysics, + defaultValue: null, + ), + ) + ..add( + EnumProperty( + 'textAlign', + textAlign, + defaultValue: TextAlign.start, + ), + ) + ..add( + DiagnosticsProperty( + 'textAlignVertical', + textAlignVertical, + defaultValue: null, + ), + ) + ..add( + EnumProperty( + 'textDirection', + textDirection, + defaultValue: null, + ), + ) + ..add( + DiagnosticsProperty( + 'clipBehavior', + clipBehavior, + defaultValue: Clip.hardEdge, + ), + ) + ..add( + DiagnosticsProperty( + 'scribbleEnabled', + scribbleEnabled, + defaultValue: true, + ), + ) + ..add( + DiagnosticsProperty( + 'stylusHandwritingEnabled', + stylusHandwritingEnabled, + defaultValue: EditableText.defaultStylusHandwritingEnabled, + ), + ) + ..add( + DiagnosticsProperty( + 'enableIMEPersonalizedLearning', + enableIMEPersonalizedLearning, + defaultValue: true, + ), + ) + ..add( + DiagnosticsProperty( + 'spellCheckConfiguration', + spellCheckConfiguration, + defaultValue: null, + ), + ) + ..add( + DiagnosticsProperty>( + 'contentCommitMimeTypes', + contentInsertionConfiguration?.allowedMimeTypes ?? const [], + defaultValue: contentInsertionConfiguration == null + ? const [] + : kDefaultContentInsertionMimeTypes, + ), + ); } static final TextMagnifierConfiguration _iosMagnifierConfiguration = @@ -1220,9 +1187,7 @@ class _CupertinoRichTextFieldState extends State implements TextSelectionGestureDetectorBuilderDelegate, AutofillClient { final GlobalKey _clearGlobalKey = GlobalKey(); - // RestorableRichTextEditingController? _controller; RichTextEditingController get _effectiveController => widget.controller; - // widget.controller ?? _controller!.value; FocusNode? _focusNode; FocusNode get _effectiveFocusNode => @@ -1257,9 +1222,6 @@ class _CupertinoRichTextFieldState extends State state: this, controller: widget.controller, ); - // if (widget.controller == null) { - // _createLocalController(); - // } _effectiveFocusNode.canRequestFocus = widget.enabled; _effectiveFocusNode.addListener(_handleFocusChanged); } @@ -1267,13 +1229,6 @@ class _CupertinoRichTextFieldState extends State @override void didUpdateWidget(CupertinoRichTextField oldWidget) { super.didUpdateWidget(oldWidget); - // if (widget.controller == null && oldWidget.controller != null) { - // _createLocalController(oldWidget.controller!.value); - // } else if (widget.controller != null && oldWidget.controller == null) { - // unregisterFromRestoration(_controller!); - // _controller!.dispose(); - // _controller = null; - // } if (widget.focusNode != oldWidget.focusNode) { (oldWidget.focusNode ?? _focusNode)?.removeListener(_handleFocusChanged); @@ -1283,27 +1238,7 @@ class _CupertinoRichTextFieldState extends State } @override - void restoreState(RestorationBucket? oldBucket, bool initialRestore) { - // if (_controller != null) { - // _registerController(); - // } - } - - // void _registerController() { - // assert(_controller != null); - // registerForRestoration(_controller!, 'controller'); - // _controller!.value.addListener(updateKeepAlive); - // } - - // void _createLocalController([TextEditingValue? value]) { - // assert(_controller == null); - // _controller = value == null - // ? RestorableRichTextEditingController() - // : RestorableRichTextEditingController.fromValue(value); - // if (!restorePending) { - // _registerController(); - // } - // } + void restoreState(RestorationBucket? oldBucket, bool initialRestore) {} @override String? get restorationId => widget.restorationId; @@ -1312,7 +1247,6 @@ class _CupertinoRichTextFieldState extends State void dispose() { _effectiveFocusNode.removeListener(_handleFocusChanged); _focusNode?.dispose(); - // _controller?.dispose(); super.dispose(); } @@ -1331,8 +1265,9 @@ class _CupertinoRichTextFieldState extends State bool _shouldShowSelectionHandles(SelectionChangedCause? cause) { // When the text field is activated by something that doesn't trigger the - // selection overlay, we shouldn't show the handles either. - if (!_selectionGestureDetectorBuilder.shouldShowSelectionToolbar) { + // selection toolbar, we shouldn't show the handles either. + if (!_selectionGestureDetectorBuilder.shouldShowSelectionToolbar || + !_selectionGestureDetectorBuilder.shouldShowSelectionHandles) { return false; } @@ -1541,14 +1476,17 @@ class _CupertinoRichTextFieldState extends State // In the middle part, stack the placeholder on top of the main EditableText // if needed. Expanded( - child: Stack( - // Ideally this should be baseline aligned. However that comes at - // the cost of the ability to compute the intrinsic dimensions of - // this widget. - // See also https://github.com/flutter/flutter/issues/13715. - alignment: AlignmentDirectional.center, - textDirection: widget.textDirection, - children: [?placeholder, editableText], + child: Directionality( + textDirection: + widget.textDirection ?? Directionality.of(context), + child: _BaselineAlignedStack( + placeholder: placeholder, + editableText: editableText, + editableTextBaseline: + textStyle.textBaseline ?? TextBaseline.alphabetic, + placeholderBaseline: + placeholderStyle.textBaseline ?? TextBaseline.alphabetic, + ), ), ), ?suffixWidget, @@ -1714,7 +1652,7 @@ class _CupertinoRichTextFieldState extends State DefaultSelectionStyle.of(context).selectionColor, context, ) ?? - CupertinoTheme.of(context).primaryColor.withOpacity(0.2); + CupertinoTheme.of(context).primaryColor.withValues(alpha: 0.2); // Set configuration as disabled if not otherwise specified. If specified, // ensure that configuration uses Cupertino text style for misspelled words @@ -1792,6 +1730,7 @@ class _CupertinoRichTextFieldState extends State scrollController: widget.scrollController, scrollPhysics: widget.scrollPhysics, enableInteractiveSelection: widget.enableInteractiveSelection, + selectAllOnFocus: widget.selectAllOnFocus, autofillClient: this, clipBehavior: widget.clipBehavior, restorationId: 'editable', @@ -1877,3 +1816,262 @@ class _CupertinoRichTextFieldState extends State ); } } + +enum _BaselineAlignedStackSlot { placeholder, editableText } + +class _BaselineAlignedStack + extends + SlottedMultiChildRenderObjectWidget< + _BaselineAlignedStackSlot, + RenderBox + > { + const _BaselineAlignedStack({ + required this.editableTextBaseline, + required this.placeholderBaseline, + required this.editableText, + this.placeholder, + }); + + final TextBaseline editableTextBaseline; + final TextBaseline placeholderBaseline; + final Widget? placeholder; + final Widget editableText; + + @override + Iterable<_BaselineAlignedStackSlot> get slots => + _BaselineAlignedStackSlot.values; + + @override + Widget? childForSlot(_BaselineAlignedStackSlot slot) { + return switch (slot) { + _BaselineAlignedStackSlot.placeholder => placeholder, + _BaselineAlignedStackSlot.editableText => editableText, + }; + } + + @override + _RenderBaselineAlignedStack createRenderObject(BuildContext context) { + return _RenderBaselineAlignedStack( + editableTextBaseline: editableTextBaseline, + placeholderBaseline: placeholderBaseline, + ); + } + + @override + void updateRenderObject( + BuildContext context, + _RenderBaselineAlignedStack renderObject, + ) { + renderObject + ..editableTextBaseline = editableTextBaseline + ..placeholderBaseline = placeholderBaseline; + } +} + +class _BaselineAlignedStackParentData + extends ContainerBoxParentData {} + +class _RenderBaselineAlignedStack extends RenderBox + with + SlottedContainerRenderObjectMixin< + _BaselineAlignedStackSlot, + RenderBox + > { + _RenderBaselineAlignedStack({ + required TextBaseline editableTextBaseline, + required TextBaseline placeholderBaseline, + }) : _editableTextBaseline = editableTextBaseline, + _placeholderBaseline = placeholderBaseline; + + TextBaseline get editableTextBaseline => _editableTextBaseline; + TextBaseline _editableTextBaseline; + set editableTextBaseline(TextBaseline value) { + if (_editableTextBaseline == value) { + return; + } + _editableTextBaseline = value; + markNeedsLayout(); + } + + TextBaseline get placeholderBaseline => _placeholderBaseline; + TextBaseline _placeholderBaseline; + set placeholderBaseline(TextBaseline value) { + if (_placeholderBaseline == value) { + return; + } + _placeholderBaseline = value; + markNeedsLayout(); + } + + @override + void setupParentData(RenderBox child) { + if (child.parentData is! _BaselineAlignedStackParentData) { + child.parentData = _BaselineAlignedStackParentData(); + } + } + + RenderBox? get _placeholderChild { + return childForSlot(_BaselineAlignedStackSlot.placeholder); + } + + RenderBox get _editableTextChild { + final RenderBox? child = childForSlot( + _BaselineAlignedStackSlot.editableText, + ); + assert(child != null); + return child!; + } + + @override + double computeMinIntrinsicHeight(double width) { + return math.max( + _placeholderChild?.getMinIntrinsicHeight(width) ?? 0.0, + _editableTextChild.getMinIntrinsicHeight(width), + ); + } + + @override + double computeMaxIntrinsicHeight(double width) { + return math.max( + _placeholderChild?.getMaxIntrinsicHeight(width) ?? 0.0, + _editableTextChild.getMaxIntrinsicHeight(width), + ); + } + + @override + double computeMinIntrinsicWidth(double height) { + return math.max( + _placeholderChild?.getMinIntrinsicWidth(height) ?? 0.0, + _editableTextChild.getMinIntrinsicWidth(height), + ); + } + + @override + double computeMaxIntrinsicWidth(double height) { + return math.max( + _placeholderChild?.getMaxIntrinsicWidth(height) ?? 0.0, + _editableTextChild.getMaxIntrinsicWidth(height), + ); + } + + @override + void performLayout() { + assert(constraints.hasTightWidth); + final RenderBox? placeholder = _placeholderChild; + final RenderBox editableText = _editableTextChild; + + final _BaselineAlignedStackParentData editableTextParentData = + editableText.parentData! as _BaselineAlignedStackParentData; + final _BaselineAlignedStackParentData? placeholderParentData = + placeholder?.parentData as _BaselineAlignedStackParentData?; + + size = _computeSize( + constraints: constraints, + layoutChild: ChildLayoutHelper.layoutChild, + getBaseline: ChildLayoutHelper.getBaseline, + ); + + final double editableTextBaselineValue = editableText.getDistanceToBaseline( + editableTextBaseline, + )!; + final double? placeholderBaselineValue = placeholder?.getDistanceToBaseline( + placeholderBaseline, + ); + + assert(placeholder != null || placeholderBaselineValue == null); + final double placeholderY = placeholderBaselineValue != null + ? editableTextBaselineValue - placeholderBaselineValue + : 0.0; + + final double offsetYAdjustment = math.max(0, placeholderY); + editableTextParentData.offset = Offset(0, offsetYAdjustment); + placeholderParentData?.offset = Offset(0, placeholderY + offsetYAdjustment); + } + + @override + void paint(PaintingContext context, Offset offset) { + final RenderBox? placeholder = _placeholderChild; + final RenderBox editableText = _editableTextChild; + + if (placeholder != null) { + final _BaselineAlignedStackParentData placeholderParentData = + placeholder.parentData! as _BaselineAlignedStackParentData; + context.paintChild(placeholder, offset + placeholderParentData.offset); + } + + final _BaselineAlignedStackParentData editableTextParentData = + editableText.parentData! as _BaselineAlignedStackParentData; + context.paintChild(editableText, offset + editableTextParentData.offset); + } + + @override + Size computeDryLayout(covariant BoxConstraints constraints) { + return _computeSize( + constraints: constraints, + layoutChild: ChildLayoutHelper.dryLayoutChild, + getBaseline: ChildLayoutHelper.getDryBaseline, + ); + } + + Size _computeSize({ + required BoxConstraints constraints, + required ChildLayouter layoutChild, + required ChildBaselineGetter getBaseline, + }) { + double width = constraints.minWidth; + double height = constraints.minHeight; + + final RenderBox editableText = _editableTextChild; + final Size editableTextSize = layoutChild(editableText, constraints); + final double editableTextBaselineValue = getBaseline( + editableText, + constraints, + editableTextBaseline, + )!; + final double editableTextDescent = + editableTextSize.height - editableTextBaselineValue; + + Size? placeholderSize; + double? placeholderBaselineValue; + final RenderBox? placeholder = _placeholderChild; + if (placeholder != null) { + placeholderSize = layoutChild(placeholder, constraints); + width = math.max(width, placeholderSize.width); + placeholderBaselineValue = getBaseline( + placeholder, + constraints, + placeholderBaseline, + ); + final double placeholderDescent = + placeholderSize.height - placeholderBaselineValue!; + // The size is the sum of the placeholder's max ascent and descent and the + // editable text's max ascent and descent. + final double maxExtentBaseline = + math.max(editableTextBaselineValue, placeholderBaselineValue) + + math.max(editableTextDescent, placeholderDescent); + height = math.max(height, maxExtentBaseline); + } + + height = math.max(height, editableTextSize.height); + width = math.max(width, editableTextSize.width); + final Size size = Size(width, height); + assert(size.isFinite); + return constraints.constrain(size); + } + + @override + bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { + final RenderBox editableText = _editableTextChild; + final _BaselineAlignedStackParentData editableTextParentData = + editableText.parentData! as _BaselineAlignedStackParentData; + + return result.addWithPaintOffset( + offset: editableTextParentData.offset, + position: position, + hitTest: (BoxHitTestResult result, Offset transformed) { + assert(transformed == position - editableTextParentData.offset); + return editableText.hitTest(result, position: transformed); + }, + ); + } +} diff --git a/lib/common/widgets/flutter/text_field/editable.dart b/lib/common/widgets/flutter/text_field/editable.dart index b4ee1dfe5..b5c3618ae 100644 --- a/lib/common/widgets/flutter/text_field/editable.dart +++ b/lib/common/widgets/flutter/text_field/editable.dart @@ -5,7 +5,6 @@ /// @docImport 'package:flutter/cupertino.dart'; library; -import 'dart:collection'; import 'dart:math' as math; import 'dart:ui' as ui @@ -17,9 +16,9 @@ import 'dart:ui' TextBox; import 'package:PiliPlus/common/widgets/flutter/text_field/controller.dart'; +import 'package:characters/characters.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; @@ -153,7 +152,10 @@ class VerticalCaretMovementRun implements Iterator { final TextPosition closestPosition = _editable._textPainter .getPositionForOffset(newOffset); final MapEntry position = - MapEntry(newOffset, closestPosition); + MapEntry( + newOffset, + closestPosition, + ); _positionCache[lineNumber] = position; return position; } @@ -294,8 +296,8 @@ class RenderEditable extends RenderBox bool paintCursorAboveText = false, Offset cursorOffset = Offset.zero, double devicePixelRatio = 1.0, - ui.BoxHeightStyle selectionHeightStyle = ui.BoxHeightStyle.tight, - ui.BoxWidthStyle selectionWidthStyle = ui.BoxWidthStyle.tight, + ui.BoxHeightStyle selectionHeightStyle = ui.BoxHeightStyle.max, + ui.BoxWidthStyle selectionWidthStyle = ui.BoxWidthStyle.max, bool? enableInteractiveSelection, this.floatingCursorAddedMargin = const EdgeInsets.fromLTRB(4, 4, 4, 5), TextRange? promptRectRange, @@ -1301,7 +1303,7 @@ class RenderEditable extends RenderBox // can be re-used when [assembleSemanticsNode] is called again. This ensures // stable ids for the [SemanticsNode]s of [TextSpan]s across // [assembleSemanticsNode] invocations. - LinkedHashMap? _cachedChildNodes; + Map? _cachedChildNodes; /// Returns a list of rects that bound the given selection, and the text /// direction. The text direction is used by the engine to calculate @@ -1311,7 +1313,11 @@ class RenderEditable extends RenderBox List getBoxesForSelection(TextSelection selection) { _computeTextMetricsIfNeeded(); return _textPainter - .getBoxesForSelection(selection) + .getBoxesForSelection( + selection, + boxHeightStyle: selectionHeightStyle, + boxWidthStyle: selectionWidthStyle, + ) .map( (TextBox textBox) => TextBox.fromLTRBD( textBox.left + _paintOffset.dx, @@ -1381,6 +1387,7 @@ class RenderEditable extends RenderBox ..isMultiline = _isMultiline ..textDirection = textDirection ..isFocused = hasFocus + ..isFocusable = true ..isTextField = true ..isReadOnly = readOnly // This is the default for customer that uses RenderEditable directly. @@ -1437,8 +1444,7 @@ class RenderEditable extends RenderBox int placeholderIndex = 0; int childIndex = 0; RenderBox? child = firstChild; - final LinkedHashMap newChildCache = - LinkedHashMap(); + final Map newChildCache = {}; _cachedCombinedSemanticsInfos ??= combineSemanticsInfo(_semanticsInfo!); for (final InlineSpanSemanticsInformation info in _cachedCombinedSemanticsInfos!) { @@ -1507,8 +1513,9 @@ class RenderEditable extends RenderBox onDoubleTap: final VoidCallback? handler, ): if (handler != null) { - configuration.onTap = handler; - configuration.isLink = true; + configuration + ..onTap = handler + ..isLink = true; } case LongPressGestureRecognizer( onLongPress: final GestureLongPressCallback? onLongPress, @@ -2203,17 +2210,14 @@ class RenderEditable extends RenderBox Offset? to, required SelectionChangedCause cause, }) { - final localFrom = globalToLocal(from); _computeTextMetricsIfNeeded(); + final localFrom = globalToLocal(from); final TextPosition fromPosition = _textPainter.getPositionForOffset( localFrom - _paintOffset, ); - final TextPosition? toPosition = to == null ? null - : _textPainter.getPositionForOffset( - globalToLocal(to) - _paintOffset, - ); + : _textPainter.getPositionForOffset(globalToLocal(to) - _paintOffset); int baseOffset = fromPosition.offset; int extentOffset = toPosition?.offset ?? fromPosition.offset; @@ -2265,7 +2269,6 @@ class RenderEditable extends RenderBox /// beginning and end of a word respectively. /// /// {@macro flutter.rendering.RenderEditable.selectPosition} - void selectWordsInRange({ required Offset from, Offset? to, @@ -2278,9 +2281,7 @@ class RenderEditable extends RenderBox final TextSelection fromWord = getWordAtOffset(fromPosition); final TextPosition toPosition = to == null ? fromPosition - : _textPainter.getPositionForOffset( - globalToLocal(to) - _paintOffset, - ); + : _textPainter.getPositionForOffset(globalToLocal(to) - _paintOffset); final TextSelection toWord = toPosition == fromPosition ? fromWord : getWordAtOffset(toPosition); @@ -2526,9 +2527,7 @@ class RenderEditable extends RenderBox ..layout(minWidth: minWidth, maxWidth: maxWidth); final double width = forceLine ? constraints.maxWidth - : constraints.constrainWidth( - _textIntrinsics.size.width + _caretMargin, - ); + : constraints.constrainWidth(_textIntrinsics.size.width + _caretMargin); return Size( width, constraints.constrainHeight(_preferredHeight(constraints.maxWidth)), @@ -2603,8 +2602,9 @@ class RenderEditable extends RenderBox _backgroundRenderObject?.layout(painterConstraints); _maxScrollExtent = _getMaxScrollExtent(contentSize); - offset.applyViewportDimension(_viewportExtent); - offset.applyContentDimensions(0.0, _maxScrollExtent); + offset + ..applyViewportDimension(_viewportExtent) + ..applyContentDimensions(0.0, _maxScrollExtent); } // The relative origin in relation to the distance the user has theoretically @@ -2942,28 +2942,29 @@ class RenderEditable extends RenderBox @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); - properties.add(ColorProperty('cursorColor', cursorColor)); - properties.add( - DiagnosticsProperty>('showCursor', showCursor), - ); - properties.add(IntProperty('maxLines', maxLines)); - properties.add(IntProperty('minLines', minLines)); - properties.add( - DiagnosticsProperty('expands', expands, defaultValue: false), - ); - properties.add(ColorProperty('selectionColor', selectionColor)); - properties.add( - DiagnosticsProperty( - 'textScaler', - textScaler, - defaultValue: TextScaler.noScaling, - ), - ); - properties.add( - DiagnosticsProperty('locale', locale, defaultValue: null), - ); - properties.add(DiagnosticsProperty('selection', selection)); - properties.add(DiagnosticsProperty('offset', offset)); + properties + ..add(ColorProperty('cursorColor', cursorColor)) + ..add( + DiagnosticsProperty>('showCursor', showCursor), + ) + ..add(IntProperty('maxLines', maxLines)) + ..add(IntProperty('minLines', minLines)) + ..add( + DiagnosticsProperty('expands', expands, defaultValue: false), + ) + ..add(ColorProperty('selectionColor', selectionColor)) + ..add( + DiagnosticsProperty( + 'textScaler', + textScaler, + defaultValue: TextScaler.noScaling, + ), + ) + ..add( + DiagnosticsProperty('locale', locale, defaultValue: null), + ) + ..add(DiagnosticsProperty('selection', selection)) + ..add(DiagnosticsProperty('offset', offset)); } @override @@ -3154,11 +3155,13 @@ class _TextHighlightPainter extends RenderEditablePainter { highlightPaint.color = color; final TextPainter textPainter = renderEditable._textPainter; - final List boxes = textPainter.getBoxesForSelection( - TextSelection(baseOffset: range.start, extentOffset: range.end), - boxHeightStyle: selectionHeightStyle, - boxWidthStyle: selectionWidthStyle, - ); + final Set boxes = textPainter + .getBoxesForSelection( + TextSelection(baseOffset: range.start, extentOffset: range.end), + boxHeightStyle: selectionHeightStyle, + boxWidthStyle: selectionWidthStyle, + ) + .toSet(); for (final TextBox box in boxes) { canvas.drawRect( @@ -3215,7 +3218,7 @@ class _CaretPainter extends RenderEditablePainter { Color? get caretColor => _caretColor; Color? _caretColor; set caretColor(Color? value) { - if (caretColor?.value == value?.value) { + if (caretColor?.toARGB32() == value?.toARGB32()) { return; } @@ -3246,7 +3249,7 @@ class _CaretPainter extends RenderEditablePainter { Color? get backgroundCursorColor => _backgroundCursorColor; Color? _backgroundCursorColor; set backgroundCursorColor(Color? value) { - if (backgroundCursorColor?.value == value?.value) { + if (backgroundCursorColor?.toARGB32() == value?.toARGB32()) { return; } @@ -3318,7 +3321,7 @@ class _CaretPainter extends RenderEditablePainter { paintRegularCursor(canvas, renderEditable, caretColor, caretTextPosition); } - final Color? floatingCursorColor = this.caretColor?.withOpacity(0.75); + final Color? floatingCursorColor = this.caretColor?.withValues(alpha: 0.75); // Floating Cursor. if (floatingCursorRect == null || floatingCursorColor == null || diff --git a/lib/common/widgets/flutter/text_field/editable_text.dart b/lib/common/widgets/flutter/text_field/editable_text.dart index ba71cd1b0..dbc2f8340 100644 --- a/lib/common/widgets/flutter/text_field/editable_text.dart +++ b/lib/common/widgets/flutter/text_field/editable_text.dart @@ -20,7 +20,6 @@ import 'dart:async'; import 'dart:io' show Platform; import 'dart:math' as math; import 'dart:ui' as ui hide TextStyle; -import 'dart:ui'; import 'package:PiliPlus/common/widgets/flutter/text_field/controller.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/editable.dart'; @@ -30,34 +29,16 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' hide + EditableText, + EditableTextState, SpellCheckConfiguration, - buildTextSpanWithSpellCheckSuggestions, - TextSelectionOverlay, - TextSelectionGestureDetectorBuilder; + TextSelectionGestureDetectorBuilder, + TextSelectionOverlay; import 'package:flutter/rendering.dart' hide RenderEditable, VerticalCaretMovementRun; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; -export 'package:flutter/services.dart' - show - KeyboardInsertedContent, - SelectionChangedCause, - SmartDashesType, - SmartQuotesType, - TextEditingValue, - TextInputType, - TextSelection; - -// Examples can assume: -// late BuildContext context; -// late WidgetTester tester; - -/// Signature for the callback that reports when the user changes the selection -/// (including the cursor location). -typedef SelectionChangedCallback = - void Function(TextSelection selection, SelectionChangedCause? cause); - /// Signature for a widget builder that builds a context menu for the given /// [EditableTextState]. /// @@ -135,54 +116,6 @@ class _RenderCompositionCallback extends RenderProxyBox { } } -/// A controller for an editable text field. -/// -/// Whenever the user modifies a text field with an associated -/// [RichTextEditingController], the text field updates [value] and the controller -/// notifies its listeners. Listeners can then read the [text] and [selection] -/// properties to learn what the user has typed or how the selection has been -/// updated. -/// -/// Similarly, if you modify the [text] or [selection] properties, the text -/// field will be notified and will update itself appropriately. -/// -/// A [RichTextEditingController] can also be used to provide an initial value for a -/// text field. If you build a text field with a controller that already has -/// [text], the text field will use that text as its initial value. -/// -/// The [value] (as well as [text] and [selection]) of this controller can be -/// updated from within a listener added to this controller. Be aware of -/// infinite loops since the listener will also be notified of the changes made -/// from within itself. Modifying the composing region from within a listener -/// can also have a bad interaction with some input methods. Gboard, for -/// example, will try to restore the composing region of the text if it was -/// modified programmatically, creating an infinite loop of communications -/// between the framework and the input method. Consider using -/// [TextInputFormatter]s instead for as-you-type text modification. -/// -/// If both the [text] and [selection] properties need to be changed, set the -/// controller's [value] instead. Setting [text] will clear the selection -/// and composing range. -/// -/// Remember to [dispose] of the [RichTextEditingController] when it is no longer -/// needed. This will ensure we discard any resources used by the object. -/// -/// {@tool dartpad} -/// This example creates a [TextField] with a [RichTextEditingController] whose -/// change listener forces the entered text to be lower case and keeps the -/// cursor at the end of the input. -/// -/// ** See code in examples/api/lib/widgets/editable_text/text_editing_controller.0.dart ** -/// {@end-tool} -/// -/// See also: -/// -/// * [TextField], which is a Material Design text field that can be controlled -/// with a [RichTextEditingController]. -/// * [EditableText], which is a raw region of editable text that can be -/// controlled with a [RichTextEditingController]. -/// * Learn how to use a [RichTextEditingController] in one of our [cookbook recipes](https://docs.flutter.dev/cookbook/forms/text-field-changes#2-use-a-texteditingcontroller). - // A time-value pair that represents a key frame in an animation. class _KeyFrame { const _KeyFrame(this.time, this.value); @@ -506,7 +439,7 @@ class EditableText extends StatefulWidget { this.readOnly = false, this.obscuringCharacter = '•', this.obscureText = false, - this.autocorrect = true, + bool? autocorrect, SmartDashesType? smartDashesType, SmartQuotesType? smartQuotesType, this.enableSuggestions = true, @@ -556,12 +489,13 @@ class EditableText extends StatefulWidget { this.cursorOpacityAnimates = false, this.cursorOffset, this.paintCursorAboveText = false, - this.selectionHeightStyle = ui.BoxHeightStyle.tight, - this.selectionWidthStyle = ui.BoxWidthStyle.tight, + ui.BoxHeightStyle? selectionHeightStyle, + ui.BoxWidthStyle? selectionWidthStyle, this.scrollPadding = const EdgeInsets.all(20.0), this.keyboardAppearance = Brightness.light, this.dragStartBehavior = DragStartBehavior.start, bool? enableInteractiveSelection, + bool? selectAllOnFocus, this.scrollController, this.scrollPhysics, this.autocorrectionTextRectColor, @@ -586,7 +520,10 @@ class EditableText extends StatefulWidget { this.contextMenuBuilder, this.spellCheckConfiguration, this.magnifierConfiguration = TextMagnifierConfiguration.disabled, + this.hintLocales, }) : assert(obscuringCharacter.length == 1), + autocorrect = + autocorrect ?? _inferAutocorrect(autofillHints: autofillHints), smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled), @@ -608,6 +545,7 @@ class EditableText extends StatefulWidget { ), enableInteractiveSelection = enableInteractiveSelection ?? (!readOnly || !obscureText), + selectAllOnFocus = selectAllOnFocus ?? _defaultSelectAllOnFocus, toolbarOptions = selectionControls is TextSelectionHandleControls && toolbarOptions == null @@ -647,7 +585,10 @@ class EditableText extends StatefulWidget { ...inputFormatters ?? const Iterable.empty(), ] : inputFormatters, - showCursor = showCursor ?? !readOnly; + showCursor = showCursor ?? !readOnly, + selectionHeightStyle = + selectionHeightStyle ?? defaultSelectionHeightStyle, + selectionWidthStyle = selectionWidthStyle ?? defaultSelectionWidthStyle; /// Controls the text being edited. final RichTextEditingController controller; @@ -737,7 +678,7 @@ class EditableText extends StatefulWidget { /// {@template flutter.widgets.editableText.autocorrect} /// Whether to enable autocorrection. /// - /// Defaults to true. + /// False on iOS if [autofillHints] contains password-related hints, otherwise true. /// {@endtemplate} final bool autocorrect; @@ -1302,7 +1243,7 @@ class EditableText extends StatefulWidget { /// [TextSelectionGestureDetectorBuilder] to wrap the [EditableText], and set /// [rendererIgnoresPointer] to true. /// - /// When [rendererIgnoresPointer] is true true, the [RenderEditable] created + /// When [rendererIgnoresPointer] is true, the [RenderEditable] created /// by this widget will not handle pointer events. /// /// This property is false by default. @@ -1396,8 +1337,9 @@ class EditableText extends StatefulWidget { /// cut/copy/paste menu, and tapping to move the text caret. /// /// When this is false, the text selection cannot be adjusted by - /// the user, text cannot be copied, and the user cannot paste into - /// the text field from the clipboard. + /// the user, the cut/copy/paste menu is hidden, and the shortcuts to + /// cut/copy/paste text do nothing but stop propagation of the key event + /// to other key event handlers in the focus chain. /// /// Defaults to true. /// {@endtemplate} @@ -1480,6 +1422,16 @@ class EditableText extends StatefulWidget { /// {@endtemplate} bool get selectionEnabled => enableInteractiveSelection; + /// {@template flutter.widgets.editableText.selectAllOnFocus} + /// Whether this field should select all text when gaining focus. + /// + /// When false, focusing this text field will leave its + /// existing text selection unchanged. + /// + /// Defaults to true on web and desktop platforms, and false on mobile platforms. + /// {@endtemplate} + final bool selectAllOnFocus; + /// {@template flutter.widgets.editableText.autofillHints} /// A list of strings that helps the autofill service identify the type of this /// text input. @@ -1714,12 +1666,62 @@ class EditableText extends StatefulWidget { /// {@macro flutter.widgets.magnifier.intro} final TextMagnifierConfiguration magnifierConfiguration; + /// {@macro flutter.services.TextInputConfiguration.hintLocales} + final List? hintLocales; + + /// The default value for [selectionHeightStyle]. + /// + /// On web platforms, this defaults to [ui.BoxHeightStyle.max]. + /// + /// On native platforms, this defaults to [ui.BoxHeightStyle.includeLineSpacingMiddle] for all + /// platforms. + static ui.BoxHeightStyle get defaultSelectionHeightStyle { + if (kIsWeb) { + return ui.BoxHeightStyle.max; + } + return ui.BoxHeightStyle.includeLineSpacingMiddle; + } + + /// The default value for [selectionWidthStyle]. + /// + /// On web platforms, this defaults to [ui.BoxWidthStyle.max] for Apple platforms running + /// Safari (webkit) based browsers and [ui.BoxWidthStyle.tight] for all others. + /// + /// On non-web platforms, this defaults to [ui.BoxWidthStyle.max]. + static ui.BoxWidthStyle get defaultSelectionWidthStyle { + // if (kIsWeb) { + // if (defaultTargetPlatform == TargetPlatform.iOS || + // WebBrowserDetection.isSafari) { + // // On macOS web, the selection width behavior differs when running on + // // Chrom(e|ium) (blink) or Safari (webkit). + // return ui.BoxWidthStyle.max; + // } + // return ui.BoxWidthStyle.tight; + // } + return ui.BoxWidthStyle.max; + } + /// The default value for [stylusHandwritingEnabled]. static const bool defaultStylusHandwritingEnabled = true; bool get _userSelectionEnabled => enableInteractiveSelection && (!readOnly || !obscureText); + /// The default value for [selectAllOnFocus]. + static bool get _defaultSelectAllOnFocus { + if (kIsWeb) { + return true; + } + return switch (defaultTargetPlatform) { + TargetPlatform.android => false, + TargetPlatform.iOS => false, + TargetPlatform.fuchsia => false, + TargetPlatform.linux => true, + TargetPlatform.macOS => true, + TargetPlatform.windows => true, + }; + } + /// Returns the [ContextMenuButtonItem]s representing the buttons in this /// platform's default selection menu for an editable field. /// @@ -1818,6 +1820,38 @@ class EditableText extends StatefulWidget { return resultButtonItem; } + // Infer the value of autocorrect from autofillHints. + static bool _inferAutocorrect({required Iterable? autofillHints}) { + if (autofillHints == null || autofillHints.isEmpty || kIsWeb) { + return true; + } + + switch (defaultTargetPlatform) { + case TargetPlatform.iOS: + // username, password and newPassword are password related hint. + // newUsername is not supported on iOS. + final bool passwordRelatedHint = autofillHints.any( + (String hint) => + hint == AutofillHints.username || + hint == AutofillHints.password || + hint == AutofillHints.newPassword, + ); + if (passwordRelatedHint) { + // https://github.com/flutter/flutter/issues/134723 + // Set autocorrect to false to prevent password bar from flashing. + return false; + } + case TargetPlatform.macOS: + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + break; + } + + return true; + } + // Infer the keyboard type of an `EditableText` if it's not specified. static TextInputType _inferKeyboardType({ required Iterable? autofillHints, @@ -2000,7 +2034,7 @@ class EditableText extends StatefulWidget { DiagnosticsProperty( 'autocorrect', autocorrect, - defaultValue: true, + defaultValue: null, ), ) ..add( @@ -2136,6 +2170,13 @@ class EditableText extends StatefulWidget { ? const [] : kDefaultContentInsertionMimeTypes, ), + ) + ..add( + DiagnosticsProperty?>( + 'hintLocales', + hintLocales, + defaultValue: null, + ), ); } } @@ -2438,6 +2479,7 @@ class EditableTextState extends State if (selection.isCollapsed || widget.obscureText) { return; } + // bggRGjQaUbCoE copySelection final String text = widget.controller.getSelectionText(selection) ?? selection.textInside(textEditingValue.text); @@ -2455,7 +2497,6 @@ class EditableTextState extends State case TargetPlatform.android: case TargetPlatform.fuchsia: // Collapse the selection and hide the toolbar and handles. - userUpdateTextEditingValue( TextEditingValue( text: textEditingValue.text, @@ -2480,6 +2521,7 @@ class EditableTextState extends State if (selection.isCollapsed) { return; } + // bggRGjQaUbCoE cutSelection final String text = widget.controller.getSelectionText(selection) ?? selection.textInside(textEditingValue.text); @@ -2528,12 +2570,7 @@ class EditableTextState extends State selection.baseOffset, selection.extentOffset, ); - // final TextEditingValue collapsedTextEditingValue = - // textEditingValue.copyWith( - // selection: TextSelection.collapsed(offset: lastSelectionIndex), - // ); - // final newValue = collapsedTextEditingValue.replaced(selection, text); - + // bggRGjQaUbCoE _pasteText widget.controller.syncRichText( selection.isCollapsed ? TextEditingDeltaInsertion( @@ -2551,15 +2588,12 @@ class EditableTextState extends State composing: TextRange.empty, ), ); - final newValue = _value.copyWith( text: widget.controller.plainText, selection: widget.controller.newSelection, composing: TextRange.empty, ); - userUpdateTextEditingValue(newValue, cause); - if (cause == SelectionChangedCause.toolbar) { // Schedule a call to bringIntoView() after renderEditable updates. SchedulerBinding.instance.addPostFrameCallback((_) { @@ -2579,7 +2613,6 @@ class EditableTextState extends State // selecting it. return; } - userUpdateTextEditingValue( textEditingValue.copyWith( selection: TextSelection( @@ -3520,7 +3553,9 @@ class EditableTextState extends State ); case FloatingCursorDragState.End: // Resume cursor blinking. - _startCursorBlink(); + if (_hasFocus) { + _startCursorBlink(); + } // We skip animation if no update has happened. if (_lastTextPosition != null && _lastBoundedOffset != null) { _floatingCursorResetController!.value = 0.0; @@ -4417,15 +4452,6 @@ class EditableTextState extends State final bool textCommitted = !oldValue.composing.isCollapsed && value.composing.isCollapsed; final bool selectionChanged = oldValue.selection != value.selection; - // if (!textChanged && selectionChanged) { - // value = value.copyWith( - // selection: widget.controller.updateSelection( - // oldSelection: _value.selection, - // newSelection: value.selection, - // cause: cause, - // ), - // ); - // } if (textChanged || textCommitted) { // Only apply input formatters if the text has changed (including uncommitted @@ -4689,17 +4715,9 @@ class EditableTextState extends State TextSelection? _adjustedSelectionWhenFocused() { TextSelection? selection; - final bool isDesktop = switch (defaultTargetPlatform) { - TargetPlatform.android || - TargetPlatform.iOS || - TargetPlatform.fuchsia => false, - TargetPlatform.macOS || - TargetPlatform.linux || - TargetPlatform.windows => true, - }; final bool shouldSelectAll = + widget.selectAllOnFocus && widget.selectionEnabled && - (kIsWeb || isDesktop) && !_isMultiline && !_nextFocusChangeIsInternal && !_justResumed; @@ -5043,10 +5061,10 @@ class EditableTextState extends State } /// Shows the magnifier at the position given by `positionToShow`, - /// if there is no magnifier visible. + /// if no magnifier exists. /// /// Updates the magnifier to the position given by `positionToShow`, - /// if there is a magnifier visible. + /// if a magnifier exits. /// /// Does nothing if a magnifier couldn't be shown, such as when the selection /// overlay does not currently exist. @@ -5055,22 +5073,20 @@ class EditableTextState extends State return; } - if (_selectionOverlay!.magnifierIsVisible) { + if (_selectionOverlay!.magnifierExists) { _selectionOverlay!.updateMagnifier(positionToShow); } else { _selectionOverlay!.showMagnifier(positionToShow); } } - /// Hides the magnifier if it is visible. + /// Hides the magnifier. void hideMagnifier() { if (_selectionOverlay == null) { return; } - if (_selectionOverlay!.magnifierIsVisible) { - _selectionOverlay!.hideMagnifier(); - } + _selectionOverlay!.hideMagnifier(); } // Tracks the location a [_ScribblePlaceholder] should be rendered in the @@ -5161,6 +5177,7 @@ class EditableTextState extends State allowedMimeTypes: widget.contentInsertionConfiguration == null ? const [] : widget.contentInsertionConfiguration!.allowedMimeTypes, + hintLocales: widget.hintLocales, ); } @@ -5357,10 +5374,7 @@ class EditableTextState extends State void _replaceText(ReplaceTextIntent intent) { final TextEditingValue oldValue = _value; - // final TextEditingValue newValue = intent.currentTextEditingValue.replaced( - // intent.replacementRange, - // intent.replacementText, - // ); + // bggRGjQaUbCoE _replaceText widget.controller.syncRichText( intent.replacementText.isEmpty ? TextEditingDeltaDeletion( @@ -5387,7 +5401,6 @@ class EditableTextState extends State selection: widget.controller.newSelection, composing: TextRange.empty, ); - userUpdateTextEditingValue(newValue, intent.cause); // If there's no change in text and selection (e.g. when selecting and @@ -5509,7 +5522,6 @@ class EditableTextState extends State } bringIntoView(nextSelection.extent); - userUpdateTextEditingValue( _value.copyWith(selection: nextSelection), SelectionChangedCause.keyboard, @@ -5718,7 +5730,8 @@ class EditableTextState extends State ), ), ScrollToDocumentBoundaryIntent: _makeOverridable( - CallbackAction( + _WebComposingDisablingCallbackAction( + this, onInvoke: _scrollToDocumentBoundary, ), ), @@ -5748,11 +5761,7 @@ class EditableTextState extends State // Copy Paste SelectAllTextIntent: _makeOverridable(_SelectAllAction(this)), CopySelectionTextIntent: _makeOverridable(_CopySelectionAction(this)), - PasteTextIntent: _makeOverridable( - CallbackAction( - onInvoke: (PasteTextIntent intent) => pasteText(intent.cause), - ), - ), + PasteTextIntent: _makeOverridable(_PasteSelectionAction(this)), TransposeCharactersIntent: _makeOverridable(_transposeCharactersAction), EditableTextTapOutsideIntent: _makeOverridable( @@ -5826,7 +5835,13 @@ class EditableTextState extends State ? AxisDirection.down : AxisDirection.right, controller: _scrollController, - physics: widget.scrollPhysics, + // On iOS a single-line TextField should not scroll. + physics: + widget.scrollPhysics ?? + (!_isMultiline && + defaultTargetPlatform == TargetPlatform.iOS + ? const _NeverUserScrollableScrollPhysics() + : null), dragStartBehavior: widget.dragStartBehavior, restorationId: widget.restorationId, // If a ScrollBehavior is not provided, only apply scrollbars when @@ -5970,15 +5985,19 @@ class EditableTextState extends State final int placeholderLocation = _value.text.length - _placeholderLocation; if (_isMultiline) { // The zero size placeholder here allows the line to break and keep the caret on the first line. - placeholders.add( - const _ScribblePlaceholder(child: SizedBox.shrink(), size: Size.zero), - ); - placeholders.add( - _ScribblePlaceholder( - child: const SizedBox.shrink(), - size: Size(renderEditable.size.width, 0.0), - ), - ); + placeholders + ..add( + const _ScribblePlaceholder( + child: SizedBox.shrink(), + size: Size.zero, + ), + ) + ..add( + _ScribblePlaceholder( + child: const SizedBox.shrink(), + size: Size(renderEditable.size.width, 0.0), + ), + ); } else { placeholders.add( const _ScribblePlaceholder( @@ -6061,8 +6080,8 @@ class _Editable extends MultiChildRenderObjectWidget { this.cursorRadius, required this.cursorOffset, required this.paintCursorAboveText, - this.selectionHeightStyle = ui.BoxHeightStyle.tight, - this.selectionWidthStyle = ui.BoxWidthStyle.tight, + ui.BoxHeightStyle? selectionHeightStyle, + ui.BoxWidthStyle? selectionWidthStyle, this.enableInteractiveSelection = true, required this.textSelectionDelegate, required this.devicePixelRatio, @@ -6070,7 +6089,11 @@ class _Editable extends MultiChildRenderObjectWidget { this.promptRectColor, required this.clipBehavior, required this.controller, - }) : super( + }) : selectionHeightStyle = + selectionHeightStyle ?? EditableText.defaultSelectionHeightStyle, + selectionWidthStyle = + selectionWidthStyle ?? EditableText.defaultSelectionWidthStyle, + super( children: WidgetSpan.extractFromInlineSpan(inlineSpan, textScaler), ); @@ -6203,6 +6226,20 @@ class _Editable extends MultiChildRenderObjectWidget { } } +class _NeverUserScrollableScrollPhysics extends ScrollPhysics { + /// Creates a scroll physics that prevents scrolling with user input, for example + /// by dragging, but still allows for programmatic scrolling. + const _NeverUserScrollableScrollPhysics({super.parent}); + + @override + _NeverUserScrollableScrollPhysics applyTo(ScrollPhysics? ancestor) { + return _NeverUserScrollableScrollPhysics(parent: buildParent(ancestor)); + } + + @override + bool get allowUserScrolling => false; +} + @immutable class _ScribbleCacheKey { const _ScribbleCacheKey({ @@ -6454,11 +6491,7 @@ class _CodePointBoundary extends TextBoundary { // ------------------------------- Text Actions ------------------------------- class _DeleteTextAction extends ContextAction { - _DeleteTextAction( - this.state, - this.getTextBoundary, - this._applyTextBoundary, - ); + _DeleteTextAction(this.state, this.getTextBoundary, this._applyTextBoundary); final EditableTextState state; final TextBoundary Function() getTextBoundary; @@ -6658,7 +6691,15 @@ class _UpdateTextSelectionAction } @override - bool get isActionEnabled => state._value.selection.isValid; + bool get isActionEnabled { + if (kIsWeb && + state.widget.selectionEnabled && + state._value.composing.isValid) { + return false; + } + + return state._value.selection.isValid; + } } class _UpdateTextSelectionVerticallyAction< @@ -6745,7 +6786,33 @@ class _UpdateTextSelectionVerticallyAction< } @override - bool get isActionEnabled => state._value.selection.isValid; + bool get isActionEnabled { + if (kIsWeb && + state.widget.selectionEnabled && + state._value.composing.isValid) { + return false; + } + + return state._value.selection.isValid; + } +} + +class _WebComposingDisablingCallbackAction + extends CallbackAction { + _WebComposingDisablingCallbackAction(this.state, {required super.onInvoke}); + + final EditableTextState state; + + @override + bool get isActionEnabled { + if (kIsWeb && + state.widget.selectionEnabled && + state._value.composing.isValid) { + return false; + } + + return super.isActionEnabled; + } } class _SelectAllAction extends ContextAction { @@ -6755,6 +6822,10 @@ class _SelectAllAction extends ContextAction { @override Object? invoke(SelectAllTextIntent intent, [BuildContext? context]) { + if (!state.widget.selectionEnabled) { + return null; + } + return Actions.invoke( context!, UpdateSelectionIntent( @@ -6764,9 +6835,6 @@ class _SelectAllAction extends ContextAction { ), ); } - - @override - bool get isActionEnabled => state.widget.selectionEnabled; } class _CopySelectionAction extends ContextAction { @@ -6776,16 +6844,35 @@ class _CopySelectionAction extends ContextAction { @override void invoke(CopySelectionTextIntent intent, [BuildContext? context]) { + if (!state._value.selection.isValid || state._value.selection.isCollapsed) { + return; + } + + if (!state.widget.selectionEnabled) { + return; + } + if (intent.collapseSelection) { state.cutSelection(intent.cause); } else { state.copySelection(intent.cause); } } +} + +class _PasteSelectionAction extends ContextAction { + _PasteSelectionAction(this.state); + + final EditableTextState state; @override - bool get isActionEnabled => - state._value.selection.isValid && !state._value.selection.isCollapsed; + void invoke(PasteTextIntent intent, [BuildContext? context]) { + if (!state.widget.selectionEnabled) { + return; + } + + state.pasteText(intent.cause); + } } /// A [ClipboardStatusNotifier] whose [value] is hardcoded to diff --git a/lib/common/widgets/flutter/text_field/spell_check.dart b/lib/common/widgets/flutter/text_field/spell_check.dart index e067b5526..649a81151 100644 --- a/lib/common/widgets/flutter/text_field/spell_check.dart +++ b/lib/common/widgets/flutter/text_field/spell_check.dart @@ -5,15 +5,11 @@ /// @docImport 'editable_text.dart'; library; -import 'dart:math' as math; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/painting.dart'; -import 'package:flutter/services.dart' - show SpellCheckResults, SpellCheckService, SuggestionSpan, TextEditingValue; - import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart' show EditableTextContextMenuBuilder; +import 'package:flutter/foundation.dart'; +import 'package:flutter/painting.dart'; +import 'package:flutter/services.dart' show SpellCheckService; /// Controls how spell check is performed for text input. /// @@ -121,351 +117,3 @@ class SpellCheckConfiguration { _spellCheckEnabled, ); } - -// Methods for displaying spell check results: - -/// Adjusts spell check results to correspond to [newText] if the only results -/// that the handler has access to are the [results] corresponding to -/// [resultsText]. -/// -/// Used in the case where the request for the spell check results of the -/// [newText] is lagging in order to avoid display of incorrect results. -List _correctSpellCheckResults( - String newText, - String resultsText, - List results, -) { - final List correctedSpellCheckResults = []; - int spanPointer = 0; - int offset = 0; - - // Assumes that the order of spans has not been jumbled for optimization - // purposes, and will only search since the previously found span. - int searchStart = 0; - - while (spanPointer < results.length) { - final SuggestionSpan currentSpan = results[spanPointer]; - final String currentSpanText = resultsText.substring( - currentSpan.range.start, - currentSpan.range.end, - ); - final int spanLength = currentSpan.range.end - currentSpan.range.start; - - // Try finding SuggestionSpan from resultsText in new text. - final String escapedText = RegExp.escape(currentSpanText); - final RegExp currentSpanTextRegexp = RegExp('\\b$escapedText\\b'); - final int foundIndex = newText - .substring(searchStart) - .indexOf(currentSpanTextRegexp); - - // Check whether word was found exactly where expected or elsewhere in the newText. - final bool currentSpanFoundExactly = - currentSpan.range.start == foundIndex + searchStart; - final bool currentSpanFoundExactlyWithOffset = - currentSpan.range.start + offset == foundIndex + searchStart; - final bool currentSpanFoundElsewhere = foundIndex >= 0; - - if (currentSpanFoundExactly || currentSpanFoundExactlyWithOffset) { - // currentSpan was found at the same index in newText and resultsText - // or at the same index with the previously calculated adjustment by - // the offset value, so apply it to new text by adding it to the list of - // corrected results. - final SuggestionSpan adjustedSpan = SuggestionSpan( - TextRange( - start: currentSpan.range.start + offset, - end: currentSpan.range.end + offset, - ), - currentSpan.suggestions, - ); - - // Start search for the next misspelled word at the end of currentSpan. - searchStart = math.min( - currentSpan.range.end + 1 + offset, - newText.length, - ); - correctedSpellCheckResults.add(adjustedSpan); - } else if (currentSpanFoundElsewhere) { - // Word was pushed forward but not modified. - final int adjustedSpanStart = searchStart + foundIndex; - final int adjustedSpanEnd = adjustedSpanStart + spanLength; - final SuggestionSpan adjustedSpan = SuggestionSpan( - TextRange(start: adjustedSpanStart, end: adjustedSpanEnd), - currentSpan.suggestions, - ); - - // Start search for the next misspelled word at the end of the - // adjusted currentSpan. - searchStart = math.min(adjustedSpanEnd + 1, newText.length); - // Adjust offset to reflect the difference between where currentSpan - // was positioned in resultsText versus in newText. - offset = adjustedSpanStart - currentSpan.range.start; - correctedSpellCheckResults.add(adjustedSpan); - } - spanPointer++; - } - return correctedSpellCheckResults; -} - -/// Builds the [TextSpan] tree given the current state of the text input and -/// spell check results. -/// -/// The [value] is the current [TextEditingValue] requested to be rendered -/// by a text input widget. The [composingWithinCurrentTextRange] value -/// represents whether or not there is a valid composing region in the -/// [value]. The [style] is the [TextStyle] to render the [value]'s text with, -/// and the [misspelledTextStyle] is the [TextStyle] to render misspelled -/// words within the [value]'s text with. The [spellCheckResults] are the -/// results of spell checking the [value]'s text. -TextSpan buildTextSpanWithSpellCheckSuggestions( - TextEditingValue value, - bool composingWithinCurrentTextRange, - TextStyle? style, - TextStyle misspelledTextStyle, - SpellCheckResults spellCheckResults, -) { - List spellCheckResultsSpans = - spellCheckResults.suggestionSpans; - final String spellCheckResultsText = spellCheckResults.spellCheckedText; - - if (spellCheckResultsText != value.text) { - spellCheckResultsSpans = _correctSpellCheckResults( - value.text, - spellCheckResultsText, - spellCheckResultsSpans, - ); - } - - // We will draw the TextSpan tree based on the composing region, if it is - // available. - // TODO(camsim99): The two separate strategies for building TextSpan trees - // based on the availability of a composing region should be merged: - // https://github.com/flutter/flutter/issues/124142. - final bool shouldConsiderComposingRegion = - defaultTargetPlatform == TargetPlatform.android; - if (shouldConsiderComposingRegion) { - return TextSpan( - style: style, - children: _buildSubtreesWithComposingRegion( - spellCheckResultsSpans, - value, - style, - misspelledTextStyle, - composingWithinCurrentTextRange, - ), - ); - } - - return TextSpan( - style: style, - children: _buildSubtreesWithoutComposingRegion( - spellCheckResultsSpans, - value, - style, - misspelledTextStyle, - value.selection.baseOffset, - ), - ); -} - -/// Builds the [TextSpan] tree for spell check without considering the composing -/// region. Instead, uses the cursor to identify the word that's actively being -/// edited and shouldn't be spell checked. This is useful for platforms and IMEs -/// that don't use the composing region for the active word. -List _buildSubtreesWithoutComposingRegion( - List? spellCheckSuggestions, - TextEditingValue value, - TextStyle? style, - TextStyle misspelledStyle, - int cursorIndex, -) { - final List textSpanTreeChildren = []; - - int textPointer = 0; - int currentSpanPointer = 0; - int endIndex; - final String text = value.text; - final TextStyle misspelledJointStyle = - style?.merge(misspelledStyle) ?? misspelledStyle; - bool cursorInCurrentSpan = false; - - // Add text interwoven with any misspelled words to the tree. - if (spellCheckSuggestions != null) { - while (textPointer < text.length && - currentSpanPointer < spellCheckSuggestions.length) { - final SuggestionSpan currentSpan = - spellCheckSuggestions[currentSpanPointer]; - - if (currentSpan.range.start > textPointer) { - endIndex = currentSpan.range.start < text.length - ? currentSpan.range.start - : text.length; - textSpanTreeChildren.add( - TextSpan(style: style, text: text.substring(textPointer, endIndex)), - ); - textPointer = endIndex; - } else { - endIndex = currentSpan.range.end < text.length - ? currentSpan.range.end - : text.length; - cursorInCurrentSpan = - currentSpan.range.start <= cursorIndex && - currentSpan.range.end >= cursorIndex; - textSpanTreeChildren.add( - TextSpan( - style: cursorInCurrentSpan ? style : misspelledJointStyle, - text: text.substring(currentSpan.range.start, endIndex), - ), - ); - - textPointer = endIndex; - currentSpanPointer++; - } - } - } - - // Add any remaining text to the tree if applicable. - if (textPointer < text.length) { - textSpanTreeChildren.add( - TextSpan(style: style, text: text.substring(textPointer, text.length)), - ); - } - - return textSpanTreeChildren; -} - -/// Builds [TextSpan] subtree for text with misspelled words with logic based on -/// a valid composing region. -List _buildSubtreesWithComposingRegion( - List? spellCheckSuggestions, - TextEditingValue value, - TextStyle? style, - TextStyle misspelledStyle, - bool composingWithinCurrentTextRange, -) { - final List textSpanTreeChildren = []; - - int textPointer = 0; - int currentSpanPointer = 0; - int endIndex; - SuggestionSpan currentSpan; - final String text = value.text; - final TextRange composingRegion = value.composing; - final TextStyle composingTextStyle = - style?.merge(const TextStyle(decoration: TextDecoration.underline)) ?? - const TextStyle(decoration: TextDecoration.underline); - final TextStyle misspelledJointStyle = - style?.merge(misspelledStyle) ?? misspelledStyle; - bool textPointerWithinComposingRegion = false; - bool currentSpanIsComposingRegion = false; - - // Add text interwoven with any misspelled words to the tree. - if (spellCheckSuggestions != null) { - while (textPointer < text.length && - currentSpanPointer < spellCheckSuggestions.length) { - currentSpan = spellCheckSuggestions[currentSpanPointer]; - - if (currentSpan.range.start > textPointer) { - endIndex = currentSpan.range.start < text.length - ? currentSpan.range.start - : text.length; - textPointerWithinComposingRegion = - composingRegion.start >= textPointer && - composingRegion.end <= endIndex && - !composingWithinCurrentTextRange; - - if (textPointerWithinComposingRegion) { - _addComposingRegionTextSpans( - textSpanTreeChildren, - text, - textPointer, - composingRegion, - style, - composingTextStyle, - ); - textSpanTreeChildren.add( - TextSpan( - style: style, - text: text.substring(composingRegion.end, endIndex), - ), - ); - } else { - textSpanTreeChildren.add( - TextSpan(style: style, text: text.substring(textPointer, endIndex)), - ); - } - - textPointer = endIndex; - } else { - endIndex = currentSpan.range.end < text.length - ? currentSpan.range.end - : text.length; - currentSpanIsComposingRegion = - textPointer >= composingRegion.start && - endIndex <= composingRegion.end && - !composingWithinCurrentTextRange; - textSpanTreeChildren.add( - TextSpan( - style: currentSpanIsComposingRegion - ? composingTextStyle - : misspelledJointStyle, - text: text.substring(currentSpan.range.start, endIndex), - ), - ); - - textPointer = endIndex; - currentSpanPointer++; - } - } - } - - // Add any remaining text to the tree if applicable. - if (textPointer < text.length) { - if (textPointer < composingRegion.start && - !composingWithinCurrentTextRange) { - _addComposingRegionTextSpans( - textSpanTreeChildren, - text, - textPointer, - composingRegion, - style, - composingTextStyle, - ); - - if (composingRegion.end != text.length) { - textSpanTreeChildren.add( - TextSpan( - style: style, - text: text.substring(composingRegion.end, text.length), - ), - ); - } - } else { - textSpanTreeChildren.add( - TextSpan(style: style, text: text.substring(textPointer, text.length)), - ); - } - } - - return textSpanTreeChildren; -} - -/// Helper method to create [TextSpan] tree children for specified range of -/// text up to and including the composing region. -void _addComposingRegionTextSpans( - List treeChildren, - String text, - int start, - TextRange composingRegion, - TextStyle? style, - TextStyle composingTextStyle, -) { - treeChildren.add( - TextSpan(style: style, text: text.substring(start, composingRegion.start)), - ); - treeChildren.add( - TextSpan( - style: composingTextStyle, - text: text.substring(composingRegion.start, composingRegion.end), - ), - ); -} diff --git a/lib/common/widgets/flutter/text_field/spell_check_suggestions_toolbar.dart b/lib/common/widgets/flutter/text_field/spell_check_suggestions_toolbar.dart index 0659619bf..8fabf49d3 100644 --- a/lib/common/widgets/flutter/text_field/spell_check_suggestions_toolbar.dart +++ b/lib/common/widgets/flutter/text_field/spell_check_suggestions_toolbar.dart @@ -2,9 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:PiliPlus/common/widgets/flutter/text_field/adaptive_text_selection_toolbar.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart'; import 'package:flutter/cupertino.dart' hide EditableText, EditableTextState; -import 'package:flutter/material.dart' hide EditableText, EditableTextState; +import 'package:flutter/material.dart' + hide EditableText, EditableTextState, AdaptiveTextSelectionToolbar; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart' show SelectionChangedCause, SuggestionSpan; diff --git a/lib/common/widgets/flutter/text_field/system_context_menu.dart b/lib/common/widgets/flutter/text_field/system_context_menu.dart index 479603ecf..c5965d3a1 100644 --- a/lib/common/widgets/flutter/text_field/system_context_menu.dart +++ b/lib/common/widgets/flutter/text_field/system_context_menu.dart @@ -6,6 +6,7 @@ library; import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart' hide EditableText, EditableTextState; import 'package:flutter/services.dart'; @@ -80,7 +81,7 @@ class SystemContextMenu extends StatefulWidget { ), ), items: items ?? getDefaultItems(editableTextState), - onSystemHide: editableTextState.hideToolbar, + onSystemHide: () => editableTextState.hideToolbar(false), ); } @@ -96,6 +97,13 @@ class SystemContextMenu extends StatefulWidget { /// of the input. /// /// Defaults to the result of [getDefaultItems]. + /// + /// To add custom menu items, pass [IOSSystemContextMenuItemCustom] instances + /// in the [items] list. Each custom item requires a title and an onPressed callback. + /// + /// See also: + /// + /// * [IOSSystemContextMenuItemCustom], which creates custom menu items. final List items; /// Called when the system hides this context menu. @@ -110,8 +118,30 @@ class SystemContextMenu extends StatefulWidget { /// Whether the current device supports showing the system context menu. /// /// Currently, this is only supported on newer versions of iOS. + /// + /// See also: + /// + /// * [isSupportedByField], which uses this method and determines whether an + /// individual [EditableTextState] supports the system context menu. static bool isSupported(BuildContext context) { - return MediaQuery.maybeSupportsShowingSystemContextMenu(context) ?? false; + return defaultTargetPlatform == TargetPlatform.iOS && + (MediaQuery.maybeSupportsShowingSystemContextMenu(context) ?? false); + } + + /// Whether the given field supports showing the system context menu. + /// + /// Currently [SystemContextMenu] is only supported with an active + /// [TextInputConnection]. In cases where this isn't possible, such as in a + /// read-only field, fall back to using a Flutter-rendered context menu like + /// [AdaptiveTextSelectionToolbar]. + /// + /// See also: + /// + /// * [isSupported], which is used by this method and determines whether the + /// platform in general supports showing the system context menu. + static bool isSupportedByField(EditableTextState editableTextState) { + return !editableTextState.widget.readOnly && + isSupported(editableTextState.context); } /// The default [items] for the given [EditableTextState]. @@ -136,6 +166,8 @@ class SystemContextMenu extends StatefulWidget { const IOSSystemContextMenuItemLookUp(), if (editableTextState.searchWebEnabled) const IOSSystemContextMenuItemSearchWeb(), + if (editableTextState.liveTextInputEnabled) + const IOSSystemContextMenuItemLiveText(), ]; } @@ -177,259 +209,3 @@ class _SystemContextMenuState extends State { return const SizedBox.shrink(); } } - -/// Describes a context menu button that will be rendered in the iOS system -/// context menu and not by Flutter itself. -/// -/// See also: -/// -/// * [SystemContextMenu], a widget that can be used to display the system -/// context menu. -/// * [IOSSystemContextMenuItemData], which performs a similar role but at the -/// method channel level and mirrors the requirements of the method channel -/// API. -/// * [ContextMenuButtonItem], which performs a similar role for Flutter-drawn -/// context menus. -@immutable -sealed class IOSSystemContextMenuItem { - const IOSSystemContextMenuItem(); - - /// The text to display to the user. - /// - /// Not exposed for some built-in menu items whose title is always set by the - /// platform. - String? get title => null; - - /// Returns the representation of this class used by method channels. - IOSSystemContextMenuItemData getData(WidgetsLocalizations localizations); - - @override - int get hashCode => title.hashCode; - - @override - bool operator ==(Object other) { - if (identical(this, other)) { - return true; - } - if (other.runtimeType != runtimeType) { - return false; - } - return other is IOSSystemContextMenuItem && other.title == title; - } -} - -/// Creates an instance of [IOSSystemContextMenuItem] for the system's built-in -/// copy button. -/// -/// Should only appear when there is a selection that can be copied. -/// -/// The title and action are both handled by the platform. -/// -/// See also: -/// -/// * [SystemContextMenu], a widget that can be used to display the system -/// context menu. -/// * [IOSSystemContextMenuItemDataCopy], which specifies the data to be sent to -/// the platform for this same button. -final class IOSSystemContextMenuItemCopy extends IOSSystemContextMenuItem { - /// Creates an instance of [IOSSystemContextMenuItemCopy]. - const IOSSystemContextMenuItemCopy(); - - @override - IOSSystemContextMenuItemDataCopy getData(WidgetsLocalizations localizations) { - return const IOSSystemContextMenuItemDataCopy(); - } -} - -/// Creates an instance of [IOSSystemContextMenuItem] for the system's built-in -/// cut button. -/// -/// Should only appear when there is a selection that can be cut. -/// -/// The title and action are both handled by the platform. -/// -/// See also: -/// -/// * [SystemContextMenu], a widget that can be used to display the system -/// context menu. -/// * [IOSSystemContextMenuItemDataCut], which specifies the data to be sent to -/// the platform for this same button. -final class IOSSystemContextMenuItemCut extends IOSSystemContextMenuItem { - /// Creates an instance of [IOSSystemContextMenuItemCut]. - const IOSSystemContextMenuItemCut(); - - @override - IOSSystemContextMenuItemDataCut getData(WidgetsLocalizations localizations) { - return const IOSSystemContextMenuItemDataCut(); - } -} - -/// Creates an instance of [IOSSystemContextMenuItem] for the system's built-in -/// paste button. -/// -/// Should only appear when the field can receive pasted content. -/// -/// The title and action are both handled by the platform. -/// -/// See also: -/// -/// * [SystemContextMenu], a widget that can be used to display the system -/// context menu. -/// * [IOSSystemContextMenuItemDataPaste], which specifies the data to be sent -/// to the platform for this same button. -final class IOSSystemContextMenuItemPaste extends IOSSystemContextMenuItem { - /// Creates an instance of [IOSSystemContextMenuItemPaste]. - const IOSSystemContextMenuItemPaste(); - - @override - IOSSystemContextMenuItemDataPaste getData( - WidgetsLocalizations localizations, - ) { - return const IOSSystemContextMenuItemDataPaste(); - } -} - -/// Creates an instance of [IOSSystemContextMenuItem] for the system's built-in -/// select all button. -/// -/// Should only appear when the field can have its selection changed. -/// -/// The title and action are both handled by the platform. -/// -/// See also: -/// -/// * [SystemContextMenu], a widget that can be used to display the system -/// context menu. -/// * [IOSSystemContextMenuItemDataSelectAll], which specifies the data to be -/// sent to the platform for this same button. -final class IOSSystemContextMenuItemSelectAll extends IOSSystemContextMenuItem { - /// Creates an instance of [IOSSystemContextMenuItemSelectAll]. - const IOSSystemContextMenuItemSelectAll(); - - @override - IOSSystemContextMenuItemDataSelectAll getData( - WidgetsLocalizations localizations, - ) { - return const IOSSystemContextMenuItemDataSelectAll(); - } -} - -/// Creates an instance of [IOSSystemContextMenuItem] for the -/// system's built-in look up button. -/// -/// Should only appear when content is selected. -/// -/// The [title] is optional, but it must be specified before being sent to the -/// platform. Typically it should be set to -/// [WidgetsLocalizations.lookUpButtonLabel]. -/// -/// The action is handled by the platform. -/// -/// See also: -/// -/// * [SystemContextMenu], a widget that can be used to display the system -/// context menu. -/// * [IOSSystemContextMenuItemDataLookUp], which specifies the data to be sent -/// to the platform for this same button. -final class IOSSystemContextMenuItemLookUp extends IOSSystemContextMenuItem { - /// Creates an instance of [IOSSystemContextMenuItemLookUp]. - const IOSSystemContextMenuItemLookUp({this.title}); - - @override - final String? title; - - @override - IOSSystemContextMenuItemDataLookUp getData( - WidgetsLocalizations localizations, - ) { - return IOSSystemContextMenuItemDataLookUp( - title: title ?? localizations.lookUpButtonLabel, - ); - } - - @override - String toString() { - return 'IOSSystemContextMenuItemLookUp(title: $title)'; - } -} - -/// Creates an instance of [IOSSystemContextMenuItem] for the -/// system's built-in search web button. -/// -/// Should only appear when content is selected. -/// -/// The [title] is optional, but it must be specified before being sent to the -/// platform. Typically it should be set to -/// [WidgetsLocalizations.searchWebButtonLabel]. -/// -/// The action is handled by the platform. -/// -/// See also: -/// -/// * [SystemContextMenu], a widget that can be used to display the system -/// context menu. -/// * [IOSSystemContextMenuItemDataSearchWeb], which specifies the data to be -/// sent to the platform for this same button. -final class IOSSystemContextMenuItemSearchWeb extends IOSSystemContextMenuItem { - /// Creates an instance of [IOSSystemContextMenuItemSearchWeb]. - const IOSSystemContextMenuItemSearchWeb({this.title}); - - @override - final String? title; - - @override - IOSSystemContextMenuItemDataSearchWeb getData( - WidgetsLocalizations localizations, - ) { - return IOSSystemContextMenuItemDataSearchWeb( - title: title ?? localizations.searchWebButtonLabel, - ); - } - - @override - String toString() { - return 'IOSSystemContextMenuItemSearchWeb(title: $title)'; - } -} - -/// Creates an instance of [IOSSystemContextMenuItem] for the -/// system's built-in share button. -/// -/// Opens the system share dialog. -/// -/// Should only appear when shareable content is selected. -/// -/// The [title] is optional, but it must be specified before being sent to the -/// platform. Typically it should be set to -/// [WidgetsLocalizations.shareButtonLabel]. -/// -/// See also: -/// -/// * [SystemContextMenu], a widget that can be used to display the system -/// context menu. -/// * [IOSSystemContextMenuItemDataShare], which specifies the data to be sent -/// to the platform for this same button. -final class IOSSystemContextMenuItemShare extends IOSSystemContextMenuItem { - /// Creates an instance of [IOSSystemContextMenuItemShare]. - const IOSSystemContextMenuItemShare({this.title}); - - @override - final String? title; - - @override - IOSSystemContextMenuItemDataShare getData( - WidgetsLocalizations localizations, - ) { - return IOSSystemContextMenuItemDataShare( - title: title ?? localizations.shareButtonLabel, - ); - } - - @override - String toString() { - return 'IOSSystemContextMenuItemShare(title: $title)'; - } -} - -// TODO(justinmc): Support the "custom" type. -// https://github.com/flutter/flutter/issues/103163 diff --git a/lib/common/widgets/flutter/text_field/text_field.dart b/lib/common/widgets/flutter/text_field/text_field.dart index 86e8cfe80..61e797550 100644 --- a/lib/common/widgets/flutter/text_field/text_field.dart +++ b/lib/common/widgets/flutter/text_field/text_field.dart @@ -15,8 +15,8 @@ import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle; import 'package:PiliPlus/common/widgets/flutter/text_field/adaptive_text_selection_toolbar.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/controller.dart'; -import 'package:PiliPlus/common/widgets/flutter/text_field/cupertino/cupertino_spell_check_suggestions_toolbar.dart'; -import 'package:PiliPlus/common/widgets/flutter/text_field/cupertino/cupertino_text_field.dart'; +import 'package:PiliPlus/common/widgets/flutter/text_field/cupertino/spell_check_suggestions_toolbar.dart'; +import 'package:PiliPlus/common/widgets/flutter/text_field/cupertino/text_field.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/spell_check.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/spell_check_suggestions_toolbar.dart'; @@ -26,62 +26,31 @@ import 'package:flutter/cupertino.dart' hide EditableText, EditableTextState, - CupertinoSpellCheckSuggestionsToolbar, - SystemContextMenu, - SpellCheckConfiguration, EditableTextContextMenuBuilder, - buildTextSpanWithSpellCheckSuggestions, + SystemContextMenu, + CupertinoSpellCheckSuggestionsToolbar, + SpellCheckConfiguration, CupertinoTextField, - TextSelectionGestureDetectorBuilderDelegate, TextSelectionGestureDetectorBuilder, - TextSelectionOverlay; + TextSelectionOverlay, + TextSelectionGestureDetectorBuilderDelegate; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' hide EditableText, EditableTextState, - SpellCheckSuggestionsToolbar, + EditableTextContextMenuBuilder, AdaptiveTextSelectionToolbar, SystemContextMenu, + SpellCheckSuggestionsToolbar, SpellCheckConfiguration, - EditableTextContextMenuBuilder, - buildTextSpanWithSpellCheckSuggestions, - TextSelectionGestureDetectorBuilderDelegate, TextSelectionGestureDetectorBuilder, - TextSelectionOverlay; + TextSelectionOverlay, + TextSelectionGestureDetectorBuilderDelegate; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; -export 'package:flutter/services.dart' - show - SmartDashesType, - SmartQuotesType, - TextCapitalization, - TextInputAction, - TextInputType; - -// Examples can assume: -// late BuildContext context; -// late FocusNode myFocusNode; - -/// Signature for the [RichTextField.buildCounter] callback. -typedef InputCounterWidgetBuilder = - Widget? Function( - /// The build context for the TextField. - BuildContext context, { - - /// The length of the string currently in the input. - required int currentLength, - - /// The maximum string length that can be entered into the TextField. - required int? maxLength, - - /// Whether or not the TextField is currently focused. Mainly provided for - /// the [liveRegion] parameter in the [Semantics] widget for accessibility. - required bool isFocused, - }); - class _TextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDetectorBuilder { _TextFieldSelectionGestureDetectorBuilder({ @@ -209,6 +178,14 @@ class _TextFieldSelectionGestureDetectorBuilder /// [RichTextField] to ensure proper scroll coordination for [RichTextField] and its /// components like [TextSelectionOverlay]. /// +/// {@tool dartpad} +/// This sample demonstrates how to use the [Shortcuts] and [Actions] widgets +/// to create a custom `Shift+Enter` keyboard shortcut for inserting a new line +/// in a [RichTextField]. +/// +/// ** See code in examples/api/lib/material/text_field/text_field.3.dart ** +/// {@end-tool} +/// /// See also: /// /// * [TextFormField], which integrates with the [Form] widget. @@ -262,7 +239,8 @@ class RichTextField extends StatefulWidget { /// /// The [selectionHeightStyle] and [selectionWidthStyle] properties allow /// changing the shape of the selection highlighting. These properties default - /// to [ui.BoxHeightStyle.tight] and [ui.BoxWidthStyle.tight], respectively. + /// to [EditableText.defaultSelectionHeightStyle] and + /// [EditableText.defaultSelectionHeightStyle], respectively. /// /// See also: /// @@ -293,7 +271,7 @@ class RichTextField extends StatefulWidget { this.statesController, this.obscuringCharacter = '•', this.obscureText = false, - this.autocorrect = true, + this.autocorrect, SmartDashesType? smartDashesType, SmartQuotesType? smartQuotesType, this.enableSuggestions = true, @@ -315,12 +293,13 @@ class RichTextField extends StatefulWidget { this.cursorOpacityAnimates, this.cursorColor, this.cursorErrorColor, - this.selectionHeightStyle = ui.BoxHeightStyle.tight, - this.selectionWidthStyle = ui.BoxWidthStyle.tight, + this.selectionHeightStyle, + this.selectionWidthStyle, this.keyboardAppearance, this.scrollPadding = const EdgeInsets.all(20.0), this.dragStartBehavior = DragStartBehavior.start, bool? enableInteractiveSelection, + this.selectAllOnFocus, this.selectionControls, this.onTap, this.onTapAlwaysCalled = false, @@ -346,6 +325,7 @@ class RichTextField extends StatefulWidget { this.canRequestFocus = true, this.spellCheckConfiguration, this.magnifierConfiguration, + this.hintLocales, }) : assert(obscuringCharacter.length == 1), smartDashesType = smartDashesType ?? @@ -526,7 +506,7 @@ class RichTextField extends StatefulWidget { final bool obscureText; /// {@macro flutter.widgets.editableText.autocorrect} - final bool autocorrect; + final bool? autocorrect; /// {@macro flutter.services.TextInputConfiguration.smartDashesType} final SmartDashesType smartDashesType; @@ -578,6 +558,8 @@ class RichTextField extends StatefulWidget { /// field showing how many characters have been entered. If set to a number /// greater than 0, it will also display the maximum number allowed. If set /// to [RichTextField.noMaxLength] then only the current character count is displayed. + /// To remove the counter, set [InputDecoration.counterText] to an empty string or + /// return null from [RichTextField.buildCounter] callback. /// /// After [maxLength] characters have been input, additional input /// is ignored, unless [maxLengthEnforcement] is set to @@ -642,6 +624,27 @@ class RichTextField extends StatefulWidget { /// /// If non-null this property overrides the [decoration]'s /// [InputDecoration.enabled] property. + /// + /// When a text field is disabled, all of its children widgets are also + /// disabled, including the [InputDecoration.suffixIcon]. If you need to keep + /// the suffix icon interactive while disabling the text field, consider using + /// [readOnly] and [enableInteractiveSelection] instead: + /// + /// ```dart + /// TextField( + /// enabled: true, + /// readOnly: true, + /// enableInteractiveSelection: false, + /// decoration: InputDecoration( + /// suffixIcon: IconButton( + /// onPressed: () { + /// // This will work because the TextField is enabled + /// }, + /// icon: const Icon(Icons.edit_outlined), + /// ), + /// ), + /// ) + /// ``` final bool? enabled; /// Determines whether this widget ignores pointer events. @@ -683,12 +686,12 @@ class RichTextField extends StatefulWidget { /// Controls how tall the selection highlight boxes are computed to be. /// /// See [ui.BoxHeightStyle] for details on available styles. - final ui.BoxHeightStyle selectionHeightStyle; + final ui.BoxHeightStyle? selectionHeightStyle; /// Controls how wide the selection highlight boxes are computed to be. /// /// See [ui.BoxWidthStyle] for details on available styles. - final ui.BoxWidthStyle selectionWidthStyle; + final ui.BoxWidthStyle? selectionWidthStyle; /// The appearance of the keyboard. /// @@ -703,6 +706,9 @@ class RichTextField extends StatefulWidget { /// {@macro flutter.widgets.editableText.enableInteractiveSelection} final bool enableInteractiveSelection; + /// {@macro flutter.widgets.editableText.selectAllOnFocus} + final bool? selectAllOnFocus; + /// {@macro flutter.widgets.editableText.selectionControls} final TextSelectionControls? selectionControls; @@ -837,7 +843,7 @@ class RichTextField extends StatefulWidget { /// offset and - if no [controller] has been provided - the content of the /// text field. If a [controller] has been provided, it is the responsibility /// of the owner of that controller to persist and restore it, e.g. by using - /// a [RestorableTextEditingController]. + /// a [RestorableRichTextEditingController]. /// /// The state of this widget is persisted in a [RestorationBucket] claimed /// from the surrounding [RestorationScope] using the provided restoration ID. @@ -883,12 +889,14 @@ class RichTextField extends StatefulWidget { /// be possible to move the focus to the text field with tab key. final bool canRequestFocus; + /// {@macro flutter.services.TextInputConfiguration.hintLocales} + final List? hintLocales; + static Widget _defaultContextMenuBuilder( BuildContext context, EditableTextState editableTextState, ) { - if (defaultTargetPlatform == TargetPlatform.iOS && - SystemContextMenu.isSupported(context)) { + if (SystemContextMenu.isSupportedByField(editableTextState)) { return SystemContextMenu.editableText( editableTextState: editableTextState, ); @@ -991,7 +999,9 @@ class RichTextField extends StatefulWidget { defaultValue: null, ), ) - ..add(DiagnosticsProperty('enabled', enabled, defaultValue: null)) + ..add( + DiagnosticsProperty('enabled', enabled, defaultValue: null), + ) ..add( DiagnosticsProperty( 'decoration', @@ -1006,7 +1016,9 @@ class RichTextField extends StatefulWidget { defaultValue: TextInputType.text, ), ) - ..add(DiagnosticsProperty('style', style, defaultValue: null)) + ..add( + DiagnosticsProperty('style', style, defaultValue: null), + ) ..add( DiagnosticsProperty('autofocus', autofocus, defaultValue: false), ) @@ -1028,7 +1040,7 @@ class RichTextField extends StatefulWidget { DiagnosticsProperty( 'autocorrect', autocorrect, - defaultValue: true, + defaultValue: null, ), ) ..add( @@ -1058,7 +1070,9 @@ class RichTextField extends StatefulWidget { ) ..add(IntProperty('maxLines', maxLines, defaultValue: 1)) ..add(IntProperty('minLines', minLines, defaultValue: null)) - ..add(DiagnosticsProperty('expands', expands, defaultValue: false)) + ..add( + DiagnosticsProperty('expands', expands, defaultValue: false), + ) ..add(IntProperty('maxLength', maxLength, defaultValue: null)) ..add( EnumProperty( @@ -1102,8 +1116,12 @@ class RichTextField extends StatefulWidget { defaultValue: null, ), ) - ..add(DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0)) - ..add(DoubleProperty('cursorHeight', cursorHeight, defaultValue: null)) + ..add( + DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0), + ) + ..add( + DoubleProperty('cursorHeight', cursorHeight, defaultValue: null), + ) ..add( DiagnosticsProperty( 'cursorRadius', @@ -1118,7 +1136,9 @@ class RichTextField extends StatefulWidget { defaultValue: null, ), ) - ..add(ColorProperty('cursorColor', cursorColor, defaultValue: null)) + ..add( + ColorProperty('cursorColor', cursorColor, defaultValue: null), + ) ..add( ColorProperty('cursorErrorColor', cursorErrorColor, defaultValue: null), ) @@ -1208,6 +1228,13 @@ class RichTextField extends StatefulWidget { ? const [] : kDefaultContentInsertionMimeTypes, ), + ) + ..add( + DiagnosticsProperty?>( + 'hintLocales', + hintLocales, + defaultValue: null, + ), ); } } @@ -1215,9 +1242,7 @@ class RichTextField extends StatefulWidget { class RichTextFieldState extends State with RestorationMixin implements TextSelectionGestureDetectorBuilderDelegate, AutofillClient { - // RestorableRichTextEditingController? _controller; RichTextEditingController get _effectiveController => widget.controller; - // widget.controller ?? _controller!.value; FocusNode? _focusNode; FocusNode get _effectiveFocusNode => @@ -1260,13 +1285,7 @@ class RichTextFieldState extends State bool get _hasIntrinsicError => widget.maxLength != null && widget.maxLength! > 0 && - ( - // widget.controller == null - // ? !restorePending && - // _effectiveController.value.text.characters.length > - // widget.maxLength! - // : - _effectiveController.value.text.characters.length > widget.maxLength!); + _effectiveController.value.text.characters.length > widget.maxLength!; bool get _hasError => widget.decoration?.errorText != null || @@ -1288,7 +1307,10 @@ class RichTextFieldState extends State .applyDefaults(themeData.inputDecorationTheme) .copyWith( enabled: _isEnabled, - hintMaxLines: widget.decoration?.hintMaxLines ?? widget.maxLines, + hintMaxLines: + widget.decoration?.hintMaxLines ?? + themeData.inputDecorationTheme.hintMaxLines ?? + widget.maxLines, ); // No need to build anything if counter or counterText were given directly. @@ -1368,9 +1390,6 @@ class RichTextFieldState extends State state: this, controller: widget.controller, ); - // if (widget.controller == null) { - // _createLocalController(); - // } _effectiveFocusNode.canRequestFocus = widget.canRequestFocus && _isEnabled; _effectiveFocusNode.addListener(_handleFocusChanged); _initStatesController(); @@ -1394,13 +1413,6 @@ class RichTextFieldState extends State @override void didUpdateWidget(RichTextField oldWidget) { super.didUpdateWidget(oldWidget); - // if (widget.controller == null && oldWidget.controller != null) { - // _createLocalController(oldWidget.controller!.value); - // } else if (widget.controller != null && oldWidget.controller == null) { - // unregisterFromRestoration(_controller!); - // _controller!.dispose(); - // _controller = null; - // } if (widget.focusNode != oldWidget.focusNode) { (oldWidget.focusNode ?? _focusNode)?.removeListener(_handleFocusChanged); @@ -1434,26 +1446,7 @@ class RichTextFieldState extends State } @override - void restoreState(RestorationBucket? oldBucket, bool initialRestore) { - // if (_controller != null) { - // _registerController(); - // } - } - - // void _registerController() { - // assert(_controller != null); - // registerForRestoration(_controller!, 'controller'); - // } - - // void _createLocalController([TextEditingValue? value]) { - // assert(_controller == null); - // _controller = value == null - // ? RestorableRichTextEditingController() - // : RestorableRichTextEditingController.fromValue(value); - // if (!restorePending) { - // _registerController(); - // } - // } + void restoreState(RestorationBucket? oldBucket, bool initialRestore) {} @override String? get restorationId => widget.restorationId; @@ -1462,7 +1455,6 @@ class RichTextFieldState extends State void dispose() { _effectiveFocusNode.removeListener(_handleFocusChanged); _focusNode?.dispose(); - // _controller?.dispose(); _statesController.removeListener(_handleStatesControllerChange); _internalStatesController?.dispose(); super.dispose(); @@ -1476,8 +1468,9 @@ class RichTextFieldState extends State bool _shouldShowSelectionHandles(SelectionChangedCause? cause) { // When the text field is activated by something that doesn't trigger the - // selection overlay, we shouldn't show the handles either. - if (!_selectionGestureDetectorBuilder.shouldShowSelectionToolbar) { + // selection toolbar, we shouldn't show the handles either. + if (!_selectionGestureDetectorBuilder.shouldShowSelectionToolbar || + !_selectionGestureDetectorBuilder.shouldShowSelectionHandles) { return false; } @@ -1570,7 +1563,7 @@ class RichTextFieldState extends State WidgetStatesController? _internalStatesController; void _handleStatesControllerChange() { - // Force a rebuild to resolve MaterialStateProperty properties. + // Force a rebuild to resolve WidgetStateProperty properties. setState(() {}); } @@ -1581,11 +1574,12 @@ class RichTextFieldState extends State if (widget.statesController == null) { _internalStatesController = WidgetStatesController(); } - _statesController.update(WidgetState.disabled, !_isEnabled); - _statesController.update(WidgetState.hovered, _isHovering); - _statesController.update(WidgetState.focused, _effectiveFocusNode.hasFocus); - _statesController.update(WidgetState.error, _hasError); - _statesController.addListener(_handleStatesControllerChange); + _statesController + ..update(WidgetState.disabled, !_isEnabled) + ..update(WidgetState.hovered, _isHovering) + ..update(WidgetState.focused, _effectiveFocusNode.hasFocus) + ..update(WidgetState.error, _hasError) + ..addListener(_handleStatesControllerChange); } // AutofillClient implementation start. @@ -1716,7 +1710,7 @@ class RichTextFieldState extends State cupertinoTheme.primaryColor; selectionColor = selectionStyle.selectionColor ?? - cupertinoTheme.primaryColor.withOpacity(0.40); + cupertinoTheme.primaryColor.withValues(alpha: 0.40); cursorRadius ??= const Radius.circular(2.0); cursorOffset = Offset( iOSHorizontalOffset / MediaQuery.devicePixelRatioOf(context), @@ -1737,7 +1731,7 @@ class RichTextFieldState extends State cupertinoTheme.primaryColor; selectionColor = selectionStyle.selectionColor ?? - cupertinoTheme.primaryColor.withOpacity(0.40); + cupertinoTheme.primaryColor.withValues(alpha: 0.40); cursorRadius ??= const Radius.circular(2.0); cursorOffset = Offset( iOSHorizontalOffset / MediaQuery.devicePixelRatioOf(context), @@ -1767,7 +1761,7 @@ class RichTextFieldState extends State theme.colorScheme.primary; selectionColor = selectionStyle.selectionColor ?? - theme.colorScheme.primary.withOpacity(0.40); + theme.colorScheme.primary.withValues(alpha: 0.40); case TargetPlatform.linux: forcePressEnabled = false; @@ -1781,7 +1775,7 @@ class RichTextFieldState extends State theme.colorScheme.primary; selectionColor = selectionStyle.selectionColor ?? - theme.colorScheme.primary.withOpacity(0.40); + theme.colorScheme.primary.withValues(alpha: 0.40); handleDidGainAccessibilityFocus = () { // Automatically activate the TextField when it receives accessibility focus. if (!_effectiveFocusNode.hasFocus && @@ -1805,7 +1799,7 @@ class RichTextFieldState extends State theme.colorScheme.primary; selectionColor = selectionStyle.selectionColor ?? - theme.colorScheme.primary.withOpacity(0.40); + theme.colorScheme.primary.withValues(alpha: 0.40); handleDidGainAccessibilityFocus = () { // Automatically activate the TextField when it receives accessibility focus. if (!_effectiveFocusNode.hasFocus && @@ -1876,9 +1870,11 @@ class RichTextFieldState extends State scrollPadding: widget.scrollPadding, keyboardAppearance: keyboardAppearance, enableInteractiveSelection: widget.enableInteractiveSelection, + selectAllOnFocus: widget.selectAllOnFocus, dragStartBehavior: widget.dragStartBehavior, scrollController: widget.scrollController, scrollPhysics: widget.scrollPhysics, + autofillHints: widget.autofillHints, autofillClient: this, autocorrectionTextRectColor: autocorrectionTextRectColor, clipBehavior: widget.clipBehavior, @@ -1892,6 +1888,7 @@ class RichTextFieldState extends State magnifierConfiguration: widget.magnifierConfiguration ?? TextMagnifier.adaptiveMagnifierConfiguration, + hintLocales: widget.hintLocales, ), ), ); @@ -2026,26 +2023,17 @@ TextStyle _m2CounterErrorStyle(BuildContext context) => Theme.of( // dev/tools/gen_defaults/bin/gen_defaults.dart. // dart format off -TextStyle? _m3StateInputStyle(BuildContext context) => - WidgetStateTextStyle.resolveWith((Set states) { - if (states.contains(WidgetState.disabled)) { - return TextStyle( - color: Theme.of(context) - .textTheme - .bodyLarge! - .color - ?.withOpacity(0.38)); - } - return TextStyle(color: Theme.of(context).textTheme.bodyLarge!.color); - }); +TextStyle? _m3StateInputStyle(BuildContext context) => WidgetStateTextStyle.resolveWith((Set states) { + if (states.contains(WidgetState.disabled)) { + return TextStyle(color: Theme.of(context).textTheme.bodyLarge!.color?.withValues(alpha:0.38)); + } + return TextStyle(color: Theme.of(context).textTheme.bodyLarge!.color); +}); -TextStyle _m3InputStyle(BuildContext context) => - Theme.of(context).textTheme.bodyLarge!; +TextStyle _m3InputStyle(BuildContext context) => Theme.of(context).textTheme.bodyLarge!; -TextStyle _m3CounterErrorStyle(BuildContext context) => Theme.of(context) - .textTheme - .bodySmall! - .copyWith(color: Theme.of(context).colorScheme.error); +TextStyle _m3CounterErrorStyle(BuildContext context) => + Theme.of(context).textTheme.bodySmall!.copyWith(color: Theme.of(context).colorScheme.error); // dart format on // END GENERATED TOKEN PROPERTIES - TextField diff --git a/lib/common/widgets/flutter/text_field/text_selection.dart b/lib/common/widgets/flutter/text_field/text_selection.dart index 84850eecd..6bdb85498 100644 --- a/lib/common/widgets/flutter/text_field/text_selection.dart +++ b/lib/common/widgets/flutter/text_field/text_selection.dart @@ -1,16 +1,37 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// @docImport 'package:flutter/cupertino.dart'; +/// @docImport 'package:flutter/material.dart'; +library; + import 'dart:math' as math; -import 'dart:ui'; import 'package:PiliPlus/common/widgets/flutter/text_field/controller.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/editable.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart' show kMinInteractiveDimension; +import 'package:flutter/material.dart' hide EditableText, EditableTextState; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart' hide EditableText, EditableTextState; +/// Delegate interface for the [TextSelectionGestureDetectorBuilder]. +/// +/// The interface is usually implemented by the [State] of text field +/// implementations wrapping [EditableText], so that they can use a +/// [TextSelectionGestureDetectorBuilder] to build a +/// [TextSelectionGestureDetector] for their [EditableText]. The delegate +/// provides the builder with information about the current state of the text +/// field. Based on that information, the builder adds the correct gesture +/// handlers to the gesture detector. +/// +/// See also: +/// +/// * [TextField], which implements this delegate for the Material text field. +/// * [CupertinoTextField], which implements this delegate for the Cupertino +/// text field. abstract class TextSelectionGestureDetectorBuilderDelegate { /// [GlobalKey] to the [EditableText] for which the /// [TextSelectionGestureDetectorBuilder] will build a [TextSelectionGestureDetector]. @@ -83,6 +104,10 @@ class TextSelectionGestureDetectorBuilder { // Hides the magnifier on supported platforms, currently only Android and iOS. void _hideMagnifierIfSupportedByPlatform() { + if (!_isEditableTextMounted) { + return; + } + switch (defaultTargetPlatform) { case TargetPlatform.android: case TargetPlatform.iOS: @@ -181,6 +206,7 @@ class TextSelectionGestureDetectorBuilder { offset, ); final TextSelection selection = renderEditable.selection!; + // bggRGjQaUbCoE on select final TextSelection nextSelection = selection.copyWith( extentOffset: controller.tapOffsetSimple(tappedPosition.offset), ); @@ -193,12 +219,23 @@ class TextSelectionGestureDetectorBuilder { /// Whether to show the selection toolbar. /// - /// It is based on the signal source when a [onTapDown] is called. This getter - /// will return true if current [onTapDown] event is triggered by a touch or - /// a stylus. + /// It is based on the signal source when [onTapDown], [onSecondaryTapDown], + /// [onDragSelectionStart], or [onForcePressStart] is called. This getter + /// will return true if the current [onTapDown], or [onDragSelectionStart] event + /// is triggered by a touch or a stylus. It will always return true for the + /// current [onSecondaryTapDown] or [onForcePressStart] event. bool get shouldShowSelectionToolbar => _shouldShowSelectionToolbar; bool _shouldShowSelectionToolbar = true; + /// Whether to show the selection handles. + /// + /// It is based on the signal source when [onTapDown], [onSecondaryTapDown], + /// [onDragSelectionStart], is called. This getter will return true if the + /// current [onTapDown], [onSecondaryTapDown], or [onDragSelectionStart] event + /// is triggered by a touch or a stylus. + bool get shouldShowSelectionHandles => _shouldShowSelectionHandles; + bool _shouldShowSelectionHandles = true; + /// The [State] of the [EditableText] for which the builder will provide a /// [TextSelectionGestureDetector]. @protected @@ -209,6 +246,13 @@ class TextSelectionGestureDetectorBuilder { @protected RenderEditable get renderEditable => editableText.renderEditable; + /// Returns `true` if a widget with the global key [delegate.editableTextKey] + /// is in the tree and the widget is mounted. + /// + /// Otherwise returns `false`. + bool get _isEditableTextMounted => + delegate.editableTextKey.currentContext?.mounted ?? false; + /// Whether the Shift key was pressed when the most recent [PointerDownEvent] /// was tracked by the [BaseTapAndDragGestureRecognizer]. bool _isShiftPressed = false; @@ -311,6 +355,7 @@ class TextSelectionGestureDetectorBuilder { kind == null || kind == PointerDeviceKind.touch || kind == PointerDeviceKind.stylus; + _shouldShowSelectionHandles = _shouldShowSelectionToolbar; // It is impossible to extend the selection when the shift key is pressed, if the // renderEditable.selection is invalid. @@ -504,6 +549,7 @@ class TextSelectionGestureDetectorBuilder { // Precise devices should place the cursor at a precise position if the // word at the text position is not misspelled. renderEditable.selectPosition(cause: SelectionChangedCause.tap); + editableText.hideToolbar(); case PointerDeviceKind.touch: case PointerDeviceKind.unknown: // If the word that was tapped is misspelled, select the word and show the spell check suggestions @@ -719,22 +765,23 @@ class TextSelectionGestureDetectorBuilder { /// callback. @protected void onSingleLongTapEnd(LongPressEndDetails details) { - _hideMagnifierIfSupportedByPlatform(); + _onSingleLongTapEndOrCancel(); if (shouldShowSelectionToolbar) { editableText.showToolbar(); } - _longPressStartedWithoutFocus = false; - _dragStartViewportOffset = 0.0; - _dragStartScrollOffset = 0.0; - if (defaultTargetPlatform == TargetPlatform.iOS && - delegate.selectionEnabled && - editableText.textEditingValue.selection.isCollapsed) { - // Update the floating cursor. - final RawFloatingCursorPoint cursorPoint = RawFloatingCursorPoint( - state: FloatingCursorDragState.End, - ); - editableText.updateFloatingCursor(cursorPoint); - } + } + + /// Handler for [TextSelectionGestureDetector.onSingleLongTapCancel]. + /// + /// By default, it hides the magnifier and the floating cursor if necessary. + /// + /// See also: + /// + /// * [TextSelectionGestureDetector.onSingleLongTapCancel], which triggers + /// this callback. + @protected + void onSingleLongTapCancel() { + _onSingleLongTapEndOrCancel(); } /// Handler for [TextSelectionGestureDetector.onSecondaryTap]. @@ -785,6 +832,10 @@ class TextSelectionGestureDetectorBuilder { TapDownDetails(globalPosition: details.globalPosition), ); _shouldShowSelectionToolbar = true; + _shouldShowSelectionHandles = + details.kind == null || + details.kind == PointerDeviceKind.touch || + details.kind == PointerDeviceKind.stylus; } /// Handler for [TextSelectionGestureDetector.onDoubleTapDown]. @@ -806,6 +857,23 @@ class TextSelectionGestureDetectorBuilder { } } + void _onSingleLongTapEndOrCancel() { + _hideMagnifierIfSupportedByPlatform(); + _longPressStartedWithoutFocus = false; + _dragStartViewportOffset = 0.0; + _dragStartScrollOffset = 0.0; + if (_isEditableTextMounted && + defaultTargetPlatform == TargetPlatform.iOS && + delegate.selectionEnabled && + editableText.textEditingValue.selection.isCollapsed) { + // Update the floating cursor. + final RawFloatingCursorPoint cursorPoint = RawFloatingCursorPoint( + state: FloatingCursorDragState.End, + ); + editableText.updateFloatingCursor(cursorPoint); + } + } + // Selects the set of paragraphs in a document that intersect a given range of // global positions. void _selectParagraphsInRange({ @@ -954,6 +1022,7 @@ class TextSelectionGestureDetectorBuilder { kind == null || kind == PointerDeviceKind.touch || kind == PointerDeviceKind.stylus; + _shouldShowSelectionHandles = _shouldShowSelectionToolbar; _dragStartSelection = renderEditable.selection; _dragStartScrollOffset = _scrollPosition; @@ -1300,6 +1369,7 @@ class TextSelectionGestureDetectorBuilder { onSingleLongTapStart: onSingleLongTapStart, onSingleLongTapMoveUpdate: onSingleLongTapMoveUpdate, onSingleLongTapEnd: onSingleLongTapEnd, + onSingleLongTapCancel: onSingleLongTapCancel, onDoubleTapDown: onDoubleTapDown, onTripleTapDown: onTripleTapDown, onDragSelectionStart: onDragSelectionStart, @@ -1312,6 +1382,149 @@ class TextSelectionGestureDetectorBuilder { } } +/// A gesture detector to respond to non-exclusive event chains for a text field. +/// +/// An ordinary [GestureDetector] configured to handle events like tap and +/// double tap will only recognize one or the other. This widget detects both: +/// the first tap and then any subsequent taps that occurs within a time limit +/// after the first. +/// +/// See also: +/// +/// * [TextField], a Material text field which uses this gesture detector. +/// * [CupertinoTextField], a Cupertino text field which uses this gesture +/// detector. +class TextSelectionGestureDetector extends StatefulWidget { + /// Create a [TextSelectionGestureDetector]. + /// + /// Multiple callbacks can be called for one sequence of input gesture. + const TextSelectionGestureDetector({ + super.key, + this.onTapTrackStart, + this.onTapTrackReset, + this.onTapDown, + this.onForcePressStart, + this.onForcePressEnd, + this.onSecondaryTap, + this.onSecondaryTapDown, + this.onSingleTapUp, + this.onSingleTapCancel, + this.onUserTap, + this.onSingleLongTapStart, + this.onSingleLongTapMoveUpdate, + this.onSingleLongTapEnd, + this.onSingleLongTapCancel, + this.onDoubleTapDown, + this.onTripleTapDown, + this.onDragSelectionStart, + this.onDragSelectionUpdate, + this.onDragSelectionEnd, + this.onUserTapAlwaysCalled = false, + this.behavior, + required this.child, + }); + + /// {@template flutter.gestures.selectionrecognizers.TextSelectionGestureDetector.onTapTrackStart} + /// Callback used to indicate that a tap tracking has started upon + /// a [PointerDownEvent]. + /// {@endtemplate} + final VoidCallback? onTapTrackStart; + + /// {@template flutter.gestures.selectionrecognizers.TextSelectionGestureDetector.onTapTrackReset} + /// Callback used to indicate that a tap tracking has been reset which + /// happens on the next [PointerDownEvent] after the timer between two taps + /// elapses, the recognizer loses the arena, the gesture is cancelled or + /// the recognizer is disposed of. + /// {@endtemplate} + final VoidCallback? onTapTrackReset; + + /// Called for every tap down including every tap down that's part of a + /// double click or a long press, except touches that include enough movement + /// to not qualify as taps (e.g. pans and flings). + final GestureTapDragDownCallback? onTapDown; + + /// Called when a pointer has tapped down and the force of the pointer has + /// just become greater than [ForcePressGestureRecognizer.startPressure]. + final GestureForcePressStartCallback? onForcePressStart; + + /// Called when a pointer that had previously triggered [onForcePressStart] is + /// lifted off the screen. + final GestureForcePressEndCallback? onForcePressEnd; + + /// Called for a tap event with the secondary mouse button. + final GestureTapCallback? onSecondaryTap; + + /// Called for a tap down event with the secondary mouse button. + final GestureTapDownCallback? onSecondaryTapDown; + + /// Called for the first tap in a series of taps, consecutive taps do not call + /// this method. + /// + /// For example, if the detector was configured with [onTapDown] and + /// [onDoubleTapDown], three quick taps would be recognized as a single tap + /// down, followed by a tap up, then a double tap down, followed by a single tap down. + final GestureTapDragUpCallback? onSingleTapUp; + + /// Called for each touch that becomes recognized as a gesture that is not a + /// short tap, such as a long tap or drag. It is called at the moment when + /// another gesture from the touch is recognized. + final GestureCancelCallback? onSingleTapCancel; + + /// Called for the first tap in a series of taps when [onUserTapAlwaysCalled] is + /// disabled, which is the default behavior. + /// + /// When [onUserTapAlwaysCalled] is enabled, this is called for every tap, + /// including consecutive taps. + final GestureTapCallback? onUserTap; + + /// Called for a single long tap that's sustained for longer than + /// [kLongPressTimeout] but not necessarily lifted. Not called for a + /// double-tap-hold, which calls [onDoubleTapDown] instead. + final GestureLongPressStartCallback? onSingleLongTapStart; + + /// Called after [onSingleLongTapStart] when the pointer is dragged. + final GestureLongPressMoveUpdateCallback? onSingleLongTapMoveUpdate; + + /// Called after [onSingleLongTapStart] when the pointer is lifted. + final GestureLongPressEndCallback? onSingleLongTapEnd; + + /// Called after [onSingleLongTapStart] when the pointer is canceled. + final GestureLongPressCancelCallback? onSingleLongTapCancel; + + /// Called after a momentary hold or a short tap that is close in space and + /// time (within [kDoubleTapTimeout]) to a previous short tap. + final GestureTapDragDownCallback? onDoubleTapDown; + + /// Called after a momentary hold or a short tap that is close in space and + /// time (within [kDoubleTapTimeout]) to a previous double-tap. + final GestureTapDragDownCallback? onTripleTapDown; + + /// Called when a mouse starts dragging to select text. + final GestureTapDragStartCallback? onDragSelectionStart; + + /// Called repeatedly as a mouse moves while dragging. + final GestureTapDragUpdateCallback? onDragSelectionUpdate; + + /// Called when a mouse that was previously dragging is released. + final GestureTapDragEndCallback? onDragSelectionEnd; + + /// Whether [onUserTap] will be called for all taps including consecutive taps. + /// + /// Defaults to false, so [onUserTap] is only called for each distinct tap. + final bool onUserTapAlwaysCalled; + + /// How this gesture detector should behave during hit testing. + /// + /// This defaults to [HitTestBehavior.deferToChild]. + final HitTestBehavior? behavior; + + /// Child below this widget. + final Widget child; + + @override + State createState() => _TextSelectionGestureDetectorState(); +} + class _TextSelectionGestureDetectorState extends State { // Converts the details.consecutiveTapCount from a TapAndDrag*Details object, @@ -1411,21 +1624,19 @@ class _TextSelectionGestureDetectorState } void _handleLongPressStart(LongPressStartDetails details) { - if (widget.onSingleLongTapStart != null) { - widget.onSingleLongTapStart!(details); - } + widget.onSingleLongTapStart?.call(details); } void _handleLongPressMoveUpdate(LongPressMoveUpdateDetails details) { - if (widget.onSingleLongTapMoveUpdate != null) { - widget.onSingleLongTapMoveUpdate!(details); - } + widget.onSingleLongTapMoveUpdate?.call(details); } void _handleLongPressEnd(LongPressEndDetails details) { - if (widget.onSingleLongTapEnd != null) { - widget.onSingleLongTapEnd!(details); - } + widget.onSingleLongTapEnd?.call(details); + } + + void _handleLongPressCancel() { + widget.onSingleLongTapCancel?.call(); } @override @@ -1445,7 +1656,8 @@ class _TextSelectionGestureDetectorState if (widget.onSingleLongTapStart != null || widget.onSingleLongTapMoveUpdate != null || - widget.onSingleLongTapEnd != null) { + widget.onSingleLongTapEnd != null || + widget.onSingleLongTapCancel != null) { gestures[LongPressGestureRecognizer] = GestureRecognizerFactoryWithHandlers( () => LongPressGestureRecognizer( @@ -1456,7 +1668,8 @@ class _TextSelectionGestureDetectorState instance ..onLongPressStart = _handleLongPressStart ..onLongPressMoveUpdate = _handleLongPressMoveUpdate - ..onLongPressEnd = _handleLongPressEnd; + ..onLongPressEnd = _handleLongPressEnd + ..onLongPressCancel = _handleLongPressCancel; }, ); } @@ -1824,8 +2037,11 @@ class TextSelectionOverlay { /// specifically is visible. bool get toolbarIsVisible => _selectionOverlay.toolbarIsVisible; - /// Whether the magnifier is currently visible. - bool get magnifierIsVisible => _selectionOverlay._magnifierController.shown; + /// {@macro flutter.widgets.SelectionOverlay.magnifierIsVisible} + bool get magnifierIsVisible => _selectionOverlay.magnifierIsVisible; + + /// {@macro flutter.widgets.SelectionOverlay.magnifierExists} + bool get magnifierExists => _selectionOverlay.magnifierExists; /// Whether the spell check menu is currently visible. /// @@ -1965,6 +2181,16 @@ class TextSelectionOverlay { late double _endHandleDragTarget; // The initial selection when a selection handle drag has started. + // + // This is used on Apple platforms to: + // + // 1. Preserve a collapsed selection: if the selection was collapsed when the drag + // began, then it should remain collapsed throughout the entire drag. + // 2. Anchor the non-dragged end of a non-collapsed selection: On Apple platforms, + // the dragged handle always defines the selection's new extent. The drag start + // selection provides the original position for the selection's new base. This + // allows the selection handles to correctly swap their logical order (invert) + // during the drag. TextSelection? _dragStartSelection; void _handleSelectionEndHandleDragStart(DragStartDetails details) { @@ -1990,7 +2216,12 @@ class TextSelectionOverlay { final TextPosition position = renderObject.getPositionForPoint( Offset(details.globalPosition.dx, centerOfLineGlobal), ); - _dragStartSelection ??= _selection; + + // The drag start selection is only utilized on Apple platforms. + if (defaultTargetPlatform == TargetPlatform.iOS || + defaultTargetPlatform == TargetPlatform.macOS) { + _dragStartSelection ??= _selection; + } _selectionOverlay.showMagnifier( _buildMagnifier( @@ -2031,7 +2262,6 @@ class TextSelectionOverlay { if (!renderObject.attached) { return; } - assert(_dragStartSelection != null); // This is NOT the same as details.localPosition. That is relative to the // selection handle, whereas this is relative to the RenderEditable. @@ -2059,27 +2289,27 @@ class TextSelectionOverlay { // bggRGjQaUbCoE right drag position = controller.dragOffset(position); - if (_dragStartSelection!.isCollapsed) { - _selectionOverlay.updateMagnifier( - _buildMagnifier( - currentTextPosition: position, - globalGesturePosition: details.globalPosition, - renderEditable: renderObject, - ), - ); - - final TextSelection currentSelection = TextSelection.fromPosition( - position, - ); - _handleSelectionHandleChanged(currentSelection); - return; - } - final TextSelection newSelection; switch (defaultTargetPlatform) { // On Apple platforms, dragging the base handle makes it the extent. case TargetPlatform.iOS: case TargetPlatform.macOS: + assert(_dragStartSelection != null); + if (_dragStartSelection!.isCollapsed) { + _selectionOverlay.updateMagnifier( + _buildMagnifier( + currentTextPosition: position, + globalGesturePosition: details.globalPosition, + renderEditable: renderObject, + ), + ); + + final TextSelection currentSelection = TextSelection.fromPosition( + position, + ); + _handleSelectionHandleChanged(currentSelection); + return; + } // Use this instead of _dragStartSelection.isNormalized because TextRange.isNormalized // always returns true for a TextSelection. final bool dragStartSelectionNormalized = @@ -2095,6 +2325,21 @@ class TextSelectionOverlay { case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.windows: + if (_selection.isCollapsed) { + _selectionOverlay.updateMagnifier( + _buildMagnifier( + currentTextPosition: position, + globalGesturePosition: details.globalPosition, + renderEditable: renderObject, + ), + ); + + final TextSelection currentSelection = TextSelection.fromPosition( + position, + ); + _handleSelectionHandleChanged(currentSelection); + return; + } newSelection = TextSelection( baseOffset: _selection.baseOffset, extentOffset: position.offset, @@ -2146,7 +2391,12 @@ class TextSelectionOverlay { final TextPosition position = renderObject.getPositionForPoint( Offset(details.globalPosition.dx, centerOfLineGlobal), ); - _dragStartSelection ??= _selection; + + // The drag start selection is only utilized on Apple platforms. + if (defaultTargetPlatform == TargetPlatform.iOS || + defaultTargetPlatform == TargetPlatform.macOS) { + _dragStartSelection ??= _selection; + } _selectionOverlay.showMagnifier( _buildMagnifier( @@ -2161,7 +2411,6 @@ class TextSelectionOverlay { if (!renderObject.attached) { return; } - assert(_dragStartSelection != null); // This is NOT the same as details.localPosition. That is relative to the // selection handle, whereas this is relative to the RenderEditable. @@ -2186,27 +2435,27 @@ class TextSelectionOverlay { // bggRGjQaUbCoE single drag, left drag position = controller.dragOffset(position); - if (_dragStartSelection!.isCollapsed) { - _selectionOverlay.updateMagnifier( - _buildMagnifier( - currentTextPosition: position, - globalGesturePosition: details.globalPosition, - renderEditable: renderObject, - ), - ); - - final TextSelection currentSelection = TextSelection.fromPosition( - position, - ); - _handleSelectionHandleChanged(currentSelection); - return; - } - final TextSelection newSelection; switch (defaultTargetPlatform) { // On Apple platforms, dragging the base handle makes it the extent. case TargetPlatform.iOS: case TargetPlatform.macOS: + assert(_dragStartSelection != null); + if (_dragStartSelection!.isCollapsed) { + _selectionOverlay.updateMagnifier( + _buildMagnifier( + currentTextPosition: position, + globalGesturePosition: details.globalPosition, + renderEditable: renderObject, + ), + ); + + final TextSelection currentSelection = TextSelection.fromPosition( + position, + ); + _handleSelectionHandleChanged(currentSelection); + return; + } // Use this instead of _dragStartSelection.isNormalized because TextRange.isNormalized // always returns true for a TextSelection. final bool dragStartSelectionNormalized = @@ -2222,6 +2471,21 @@ class TextSelectionOverlay { case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.windows: + if (_selection.isCollapsed) { + _selectionOverlay.updateMagnifier( + _buildMagnifier( + currentTextPosition: position, + globalGesturePosition: details.globalPosition, + renderEditable: renderObject, + ), + ); + + final TextSelection currentSelection = TextSelection.fromPosition( + position, + ); + _handleSelectionHandleChanged(currentSelection); + return; + } newSelection = TextSelection( baseOffset: position.offset, extentOffset: _selection.extentOffset, @@ -2250,19 +2514,26 @@ class TextSelectionOverlay { return; } _dragStartSelection = null; + final bool draggingHandles = + _selectionOverlay.isDraggingStartHandle || + _selectionOverlay.isDraggingEndHandle; if (selectionControls is! TextSelectionHandleControls) { - _selectionOverlay.hideMagnifier(); - if (!_selection.isCollapsed) { - _selectionOverlay.showToolbar(); + if (!draggingHandles) { + _selectionOverlay.hideMagnifier(); + if (!_selection.isCollapsed) { + _selectionOverlay.showToolbar(); + } } return; } - _selectionOverlay.hideMagnifier(); - if (!_selection.isCollapsed) { - _selectionOverlay.showToolbar( - context: context, - contextMenuBuilder: contextMenuBuilder, - ); + if (!draggingHandles) { + _selectionOverlay.hideMagnifier(); + if (!_selection.isCollapsed) { + _selectionOverlay.showToolbar( + context: context, + contextMenuBuilder: contextMenuBuilder, + ); + } } } @@ -2375,6 +2646,19 @@ class SelectionOverlay { : _toolbar != null || _spellCheckToolbarController.isShown; } + /// {@template flutter.widgets.SelectionOverlay.magnifierIsVisible} + /// Whether the magnifier is currently visible. + /// {@endtemplate} + bool get magnifierIsVisible => _magnifierController.shown; + + /// {@template flutter.widgets.SelectionOverlay.magnifierExists} + /// Whether the magnifier currently exists. + /// + /// This differs from [magnifierIsVisible] in that the magnifier may exist + /// in the overlay, but not be shown. + /// {@endtemplate} + bool get magnifierExists => _magnifierController.overlayEntry != null; + /// {@template flutter.widgets.SelectionOverlay.showMagnifier} /// Shows the magnifier, and hides the toolbar if it was showing when [showMagnifier] /// was called. This is safe to call on platforms not mobile, since @@ -2386,6 +2670,10 @@ class SelectionOverlay { /// [MagnifierController.shown]. /// {@endtemplate} void showMagnifier(MagnifierInfo initialMagnifierInfo) { + // Do not show the magnifier if one already exists. + if (_magnifierController.overlayEntry != null) { + return; + } if (toolbarIsVisible) { hideToolbar(); } @@ -2459,8 +2747,26 @@ class SelectionOverlay { markNeedsBuild(); } + // Whether a drag is in progress on the start handle. This differs from + // `_isDraggingStartHandle` in that it is not blocked by `_canDragStartHandle`. + bool _startHandleDragInProgress = false; + + /// Whether the selection start handle is currently being dragged. + bool get isDraggingStartHandle => + _isDraggingStartHandle || _startHandleDragInProgress; bool _isDraggingStartHandle = false; + // Whether the start handle can be dragged. + // + // On Apple and web platforms only one selection handle can be dragged + // at a time, so when the end handle is being dragged on these platforms + // the the start handle cannot be dragged. + bool get _canDragStartHandle => + !_isDraggingEndHandle || + (defaultTargetPlatform != TargetPlatform.iOS && + defaultTargetPlatform != TargetPlatform.macOS && + !kIsWeb); + /// Whether the start handle is visible. /// /// If the value changes, the start handle uses [FadeTransition] to transition @@ -2480,6 +2786,10 @@ class SelectionOverlay { _isDraggingStartHandle = false; return; } + _startHandleDragInProgress = true; + if (!_canDragStartHandle) { + return; + } _isDraggingStartHandle = details.kind == PointerDeviceKind.touch; onStartHandleDragStart?.call(details); } @@ -2491,6 +2801,22 @@ class SelectionOverlay { _isDraggingStartHandle = false; return; } + if (!_canDragStartHandle) { + return; + } + // The handle drag may have been blocked before on Apple platforms and the web + // while the opposite handle was being dragged. Ensure that any logic that was + // meant to be run in onStartHandleDragStart is still run. + if (!_isDraggingStartHandle) { + _isDraggingStartHandle = details.kind == PointerDeviceKind.touch; + final DragStartDetails startDetails = DragStartDetails( + globalPosition: details.globalPosition, + localPosition: details.localPosition, + sourceTimeStamp: details.sourceTimeStamp, + kind: details.kind, + ); + onStartHandleDragStart?.call(startDetails); + } onStartHandleDragUpdate?.call(details); } @@ -2508,6 +2834,10 @@ class SelectionOverlay { if (_handles == null) { return; } + _startHandleDragInProgress = false; + if (!_canDragStartHandle) { + return; + } onStartHandleDragEnd?.call(details); } @@ -2539,8 +2869,26 @@ class SelectionOverlay { markNeedsBuild(); } + // Whether a drag is in progress on the start handle. This differs from + // `_isDraggingEndHandle` in that it is not blocked by `_canDragEndHandle`. + bool _endHandleDragInProgress = false; + + /// Whether the selection end handle is currently being dragged. + bool get isDraggingEndHandle => + _isDraggingEndHandle || _endHandleDragInProgress; bool _isDraggingEndHandle = false; + // Whether the end handle can be dragged. + // + // On Apple and web platforms only one selection handle can be dragged + // at a time, so when the start handle is being dragged on these platforms + // the the end handle cannot be dragged. + bool get _canDragEndHandle => + !_isDraggingStartHandle || + (defaultTargetPlatform != TargetPlatform.iOS && + defaultTargetPlatform != TargetPlatform.macOS && + !kIsWeb); + /// Whether the end handle is visible. /// /// If the value changes, the end handle uses [FadeTransition] to transition @@ -2560,6 +2908,10 @@ class SelectionOverlay { _isDraggingEndHandle = false; return; } + _endHandleDragInProgress = true; + if (!_canDragEndHandle) { + return; + } _isDraggingEndHandle = details.kind == PointerDeviceKind.touch; onEndHandleDragStart?.call(details); } @@ -2571,6 +2923,22 @@ class SelectionOverlay { _isDraggingEndHandle = false; return; } + if (!_canDragEndHandle) { + return; + } + // The handle drag may have been blocked before on Apple platforms and the web + // while the opposite handle was being dragged. Ensure that any logic that was + // meant to be run in onStartHandleDragStart is still run. + if (!_isDraggingEndHandle) { + _isDraggingEndHandle = details.kind == PointerDeviceKind.touch; + final DragStartDetails startDetails = DragStartDetails( + globalPosition: details.globalPosition, + localPosition: details.localPosition, + sourceTimeStamp: details.sourceTimeStamp, + kind: details.kind, + ); + onEndHandleDragStart?.call(startDetails); + } onEndHandleDragUpdate?.call(details); } @@ -2588,6 +2956,10 @@ class SelectionOverlay { if (_handles == null) { return; } + _endHandleDragInProgress = false; + if (!_canDragEndHandle) { + return; + } onEndHandleDragEnd?.call(details); } diff --git a/pubspec.lock b/pubspec.lock index 809dc3eef..fee07e543 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -236,7 +236,7 @@ packages: source: hosted version: "2.1.5" characters: - dependency: transitive + dependency: "direct main" description: name: characters sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 diff --git a/pubspec.yaml b/pubspec.yaml index 101f00c6d..0cbce0172 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -238,6 +238,7 @@ dependencies: flutter_cache_manager: any http2: any screen_retriever: any + characters: any dependency_overrides: # screen_brightness: ^2.1.