From 358c55e6dacdb2c9336f5b9379ad892e4aa01fc4 Mon Sep 17 00:00:00 2001 From: zmtzawqlp Date: Fri, 18 Aug 2023 00:32:59 +0800 Subject: [PATCH] ## 12.1.0 * Migrate to Flutter 3.13.0 --- .github/workflows/checker.yml | 19 - .github/workflows/pub_dry_run.yml | 19 - .github/workflows/pub_publish.yml | 21 - .github/workflows/pub_publish_manually.yml | 19 - .github/workflows/publish.yml | 19 + .github/workflows/publishable.yml | 26 + .github/workflows/runnable.yml | 79 +++ CHANGELOG.md | 4 + analysis_options.yaml | 4 +- example/pubspec.lock | 60 +- example/pubspec.yaml | 10 +- .../spell_check_suggestions_toolbar.dart | 2 +- lib/src/extended/rendering/editable.dart | 92 +-- lib/src/extended/widgets/editable_text.dart | 3 - lib/src/extended/widgets/text_selection.dart | 6 +- .../official/material/selectable_text.dart | 7 +- lib/src/official/rendering/editable.dart | 294 ++-------- lib/src/official/widgets/editable_text.dart | 549 +++++++++++------- lib/src/official/widgets/spell_check.dart | 1 + lib/src/official/widgets/text_field.dart | 45 +- lib/src/official/widgets/text_selection.dart | 205 ++++--- pubspec.yaml | 6 +- 22 files changed, 744 insertions(+), 746 deletions(-) delete mode 100644 .github/workflows/checker.yml delete mode 100644 .github/workflows/pub_dry_run.yml delete mode 100644 .github/workflows/pub_publish.yml delete mode 100644 .github/workflows/pub_publish_manually.yml create mode 100644 .github/workflows/publish.yml create mode 100644 .github/workflows/publishable.yml create mode 100644 .github/workflows/runnable.yml diff --git a/.github/workflows/checker.yml b/.github/workflows/checker.yml deleted file mode 100644 index f1c81ff..0000000 --- a/.github/workflows/checker.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: No Free usage issue checker - -on: - issues: - types: [opened, reopened] - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - name: Check issue actor - uses: ./ - with: - repo: $GITHUB_REPOSITORY - user: $GITHUB_ACTOR - token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/pub_dry_run.yml b/.github/workflows/pub_dry_run.yml deleted file mode 100644 index 7b49e74..0000000 --- a/.github/workflows/pub_dry_run.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: Pub Publish dry run - -on: [push] - -jobs: - publish: - - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v1 - - name: Publish - uses: sakebook/actions-flutter-pub-publisher@v1.3.0 - with: - credential: ${{ secrets.CREDENTIAL_JSON }} - flutter_package: true - skip_test: true - dry_run: true diff --git a/.github/workflows/pub_publish.yml b/.github/workflows/pub_publish.yml deleted file mode 100644 index 6dd65b1..0000000 --- a/.github/workflows/pub_publish.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: Pub Publish plugin - -on: - release: - types: [published] - -jobs: - publish: - - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v1 - - name: Publish - uses: sakebook/actions-flutter-pub-publisher@v1.3.0 - with: - credential: ${{ secrets.CREDENTIAL_JSON }} - flutter_package: true - skip_test: true - dry_run: false diff --git a/.github/workflows/pub_publish_manually.yml b/.github/workflows/pub_publish_manually.yml deleted file mode 100644 index 6d60ac8..0000000 --- a/.github/workflows/pub_publish_manually.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: Pub Publish plugin - -on: workflow_dispatch - -jobs: - publish: - - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v1 - - name: Publish - uses: sakebook/actions-flutter-pub-publisher@v1.3.0 - with: - credential: ${{ secrets.CREDENTIAL_JSON }} - flutter_package: true - skip_test: true - dry_run: false diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..4d0474e --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,19 @@ +name: Publish + +on: + release: + types: [ published ] + workflow_dispatch: + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v3 + - name: Publish + uses: k-paxian/dart-package-publisher@master + with: + credentialJson: ${{ secrets.CREDENTIAL_JSON }} + flutter: true + skipTests: true \ No newline at end of file diff --git a/.github/workflows/publishable.yml b/.github/workflows/publishable.yml new file mode 100644 index 0000000..8786a9d --- /dev/null +++ b/.github/workflows/publishable.yml @@ -0,0 +1,26 @@ +name: Publishable + +on: + push: + branches: + - main + pull_request: + branches: + - main + paths: + - "**.md" + - "**.yaml" + - "**.yml" + +jobs: + publish-dry-run: + name: Publish dry-run with packages + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: k-paxian/dart-package-publisher@master + with: + credentialJson: 'MockCredentialJson' + flutter: true + dryRunOnly: true + skipTests: true \ No newline at end of file diff --git a/.github/workflows/runnable.yml b/.github/workflows/runnable.yml new file mode 100644 index 0000000..c3abfbd --- /dev/null +++ b/.github/workflows/runnable.yml @@ -0,0 +1,79 @@ +name: Runnable (stable) + +on: + push: + branches: + - main + pull_request: + branches: + - main + paths-ignore: + - "**.md" + +jobs: + analyze: + name: Analyze on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ ubuntu-latest ] + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + distribution: 'adopt' + java-version: '11.x' + - uses: subosito/flutter-action@v2 + with: + channel: 'stable' + - name: Log Dart/Flutter versions + run: | + dart --version + flutter --version + - name: Prepare dependencies + run: flutter pub get + - name: Analyse the repo + run: flutter analyze lib example/lib + - name: Run tests + run: flutter test + - name: Generate docs + run: | + dart pub global activate dartdoc + dart pub global run dartdoc . + + test_iOS: + needs: analyze + name: Test iOS + runs-on: macos-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + distribution: 'adopt' + java-version: '11.x' + - uses: subosito/flutter-action@v2.8.0 + with: + channel: stable + - run: dart --version + - run: flutter --version + - run: flutter pub get + - run: cd example; flutter build ios --no-codesign + + test_android: + needs: analyze + name: Test Android + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + distribution: 'adopt' + java-version: '11.x' + - uses: subosito/flutter-action@v2.8.0 + with: + channel: stable + - run: dart --version + - run: flutter --version + - run: flutter pub get + - run: sudo echo "y" | sudo $ANDROID_HOME/tools/bin/sdkmanager "ndk;21.4.7075529" + - run: cd example; flutter build apk --debug \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 85402df..a1ac982 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 12.1.0 + +* Migrate to Flutter 3.13.0 + ## 12.0.1 * Fix issue that wrong cursor position on macos. (https://github.com/fluttercandies/extended_text_field/issues/210) diff --git a/analysis_options.yaml b/analysis_options.yaml index 46bd6c7..c0b98ec 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -104,12 +104,12 @@ linter: - hash_and_equals - implementation_imports # - invariant_booleans # too many false positives: https://github.com/dart-lang/linter/issues/811 - - iterable_contains_unrelated_type + # - iterable_contains_unrelated_type # - join_return_with_assignment # not yet tested - library_names - library_prefixes # - lines_longer_than_80_chars # not yet tested - - list_remove_unrelated_type + # - list_remove_unrelated_type # - literal_only_boolean_expressions # too many false positives: https://github.com/dart-lang/sdk/issues/34181 # - missing_whitespace_between_adjacent_strings # not yet tested - no_adjacent_strings_in_list diff --git a/example/pubspec.lock b/example/pubspec.lock index f185e8b..0da41d3 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: transitive description: name: collection - sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 url: "https://pub.flutter-io.cn" source: hosted - version: "1.17.1" + version: "1.17.2" csslib: dependency: transitive description: @@ -76,11 +76,10 @@ packages: extended_text: dependency: "direct main" description: - name: extended_text - sha256: "75ddf28ce7d5be33a050ff2179b6567b4b98e6225ad3e61e4c3748f7448c25f7" - url: "https://pub.flutter-io.cn" - source: hosted - version: "11.0.0" + path: "../../extended_text" + relative: true + source: path + version: "11.0.1" extended_text_field: dependency: "direct overridden" description: @@ -89,13 +88,12 @@ packages: source: path version: "12.0.1" extended_text_library: - dependency: transitive + dependency: "direct overridden" description: - name: extended_text_library - sha256: c06fbd8e3b6eedadf50cd6c109bbbd80921a6c43e4422d3b4ec9d4cb36ce4555 - url: "https://pub.flutter-io.cn" - source: hosted - version: "11.0.2" + path: "../../extended_text_library" + relative: true + source: path + version: "11.1.0" fake_async: dependency: transitive description: @@ -143,14 +141,6 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.15.1" - js: - dependency: transitive - description: - name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.6.7" loading_more_list: dependency: "direct main" description: @@ -171,18 +161,18 @@ packages: dependency: transitive description: name: matcher - sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" url: "https://pub.flutter-io.cn" source: hosted - version: "0.12.15" + version: "0.12.16" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" url: "https://pub.flutter-io.cn" source: hosted - version: "0.2.0" + version: "0.5.0" meta: dependency: transitive description: @@ -224,10 +214,10 @@ packages: dependency: transitive description: name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.flutter-io.cn" source: hosted - version: "1.9.1" + version: "1.10.0" stack_trace: dependency: transitive description: @@ -264,10 +254,10 @@ packages: dependency: transitive description: name: test_api - sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" url: "https://pub.flutter-io.cn" source: hosted - version: "0.5.1" + version: "0.6.0" url_launcher: dependency: "direct main" description: @@ -348,6 +338,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "3.0.2" + web: + dependency: transitive + description: + name: web + sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.4-beta" sdks: - dart: ">=3.0.0-0 <4.0.0" - flutter: ">=3.10.0" + dart: ">=3.1.0-185.0.dev <4.0.0" + flutter: ">=3.13.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index aee9914..65a5827 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -14,8 +14,8 @@ publish_to: none version: 1.0.0+1 environment: - sdk: '>=2.17.0 <4.0.0' - flutter: ">=3.10.0" + sdk: '>=3.0.0 <4.0.0' + flutter: ">=3.13.0" dependencies: # The following adds the Cupertino Icons font to your application. @@ -45,16 +45,16 @@ dependency_overrides: # extended_text_library: # version: ^11.0.0-dev.1 # hosted: "https://pub.dev" - # extended_text: + #extended_text: # # git: # # url: https://github.com/fluttercandies/extended_text.git # # ref: refactor - # path: ../../extended_text + path: ../../extended_text #extended_text_library: # # git: # # url: https://github.com/fluttercandies/extended_text_library.git # # ref: refactor - #path: ../../extended_text_library + #path: ../../extended_text_library # For information on the generic Dart part of this file, see the # following page: https://www.dartlang.org/tools/pub/pubspec diff --git a/lib/src/extended/cupertino/spell_check_suggestions_toolbar.dart b/lib/src/extended/cupertino/spell_check_suggestions_toolbar.dart index 6b47473..951b26c 100644 --- a/lib/src/extended/cupertino/spell_check_suggestions_toolbar.dart +++ b/lib/src/extended/cupertino/spell_check_suggestions_toolbar.dart @@ -82,7 +82,7 @@ class ExtendedCupertinoSpellCheckSuggestionsToolbar extends StatelessWidget { CupertinoLocalizations.of(editableTextState.context); return [ ContextMenuButtonItem( - onPressed: () {}, + onPressed: null, label: localizations.noSpellCheckReplacementsLabel, ) ]; diff --git a/lib/src/extended/rendering/editable.dart b/lib/src/extended/rendering/editable.dart index b3a9aa9..aa73043 100644 --- a/lib/src/extended/rendering/editable.dart +++ b/lib/src/extended/rendering/editable.dart @@ -20,7 +20,6 @@ class ExtendedRenderEditable extends _RenderEditable { super.textScaleFactor = 1.0, super.selection, required super.offset, - super.onCaretChanged, super.ignorePointer = false, super.readOnly = false, super.forceLine = true, @@ -47,33 +46,40 @@ class ExtendedRenderEditable extends _RenderEditable { super.foregroundPainter, super.children, this.supportSpecialText = false, - }); + }) { + _findSpecialInlineSpanBase(text); + } - bool _hasSpecialInlineSpanBase = false; bool supportSpecialText = false; - + bool _hasSpecialInlineSpanBase = false; bool get hasSpecialInlineSpanBase => supportSpecialText && _hasSpecialInlineSpanBase; - @override - String get plainText { - return ExtendedTextLibraryUtils.textSpanToActualText(_textPainter.text!); - } - - @override - void _extractPlaceholderSpans(InlineSpan? span) { - _placeholderSpans = []; + void _findSpecialInlineSpanBase(InlineSpan? span) { + _hasSpecialInlineSpanBase = false; span?.visitChildren((InlineSpan span) { - if (span is PlaceholderSpan) { - _placeholderSpans.add(span); - } if (span is SpecialInlineSpanBase) { _hasSpecialInlineSpanBase = true; + return false; } return true; }); } + @override + set text(InlineSpan? value) { + if (_textPainter.text == value) { + return; + } + _findSpecialInlineSpanBase(value); + super.text = value; + } + + @override + String get plainText { + return ExtendedTextLibraryUtils.textSpanToActualText(_textPainter.text!); + } + @override void selectWordEdge({required SelectionChangedCause cause}) { _computeTextMetricsIfNeeded(); @@ -131,8 +137,8 @@ class ExtendedRenderEditable extends _RenderEditable { } @override - TextSelection _getWordAtOffset(TextPosition position) { - final TextSelection selection = super._getWordAtOffset(position); + TextSelection getWordAtOffset(TextPosition position) { + final TextSelection selection = super.getWordAtOffset(position); /// zmt return hasSpecialInlineSpanBase @@ -144,63 +150,13 @@ class ExtendedRenderEditable extends _RenderEditable { @override List getEndpointsForSelection(TextSelection selection) { - _computeTextMetricsIfNeeded(); - - final Offset paintOffset = _paintOffset; - // zmtzawqlp if (hasSpecialInlineSpanBase) { selection = ExtendedTextLibraryUtils .convertTextInputSelectionToTextPainterSelection(text!, selection); } - final List boxes = selection.isCollapsed - ? [] - : _textPainter.getBoxesForSelection(selection, - boxHeightStyle: selectionHeightStyle, - boxWidthStyle: selectionWidthStyle); - if (boxes.isEmpty) { - // TODO(mpcomplete): This doesn't work well at an RTL/LTR boundary. - - final Offset caretOffset = - _textPainter.getOffsetForCaret(selection.extent, _caretPrototype); - final Offset start = - Offset(0.0, preferredLineHeight) + caretOffset + paintOffset; - - // zmtzawqlp - // double? caretHeight; - // final ValueChanged caretHeightCallBack = (double value) { - // caretHeight = value; - // }; - - // final Offset caretOffset = ExtendedTextLibraryUtils.getCaretOffset( - // TextPosition( - // offset: selection.extentOffset, - // affinity: selection.extent.affinity), - // _textPainter, - // _placeholderSpans.isNotEmpty, - // caretHeightCallBack: caretHeightCallBack, - // effectiveOffset: _paintOffset, - // caretPrototype: _caretPrototype, - // ); - - // final Offset start = - // Offset(0.0, caretHeight ?? preferredLineHeight) + caretOffset; - return [TextSelectionPoint(start, null)]; - } else { - final Offset start = Offset( - clampDouble(boxes.first.start, 0, _textPainter.size.width), - boxes.first.bottom) + - paintOffset; - final Offset end = Offset( - clampDouble(boxes.last.end, 0, _textPainter.size.width), - boxes.last.bottom) + - paintOffset; - return [ - TextSelectionPoint(start, boxes.first.direction), - TextSelectionPoint(end, boxes.last.direction), - ]; - } + return super.getEndpointsForSelection(selection); } @override diff --git a/lib/src/extended/widgets/editable_text.dart b/lib/src/extended/widgets/editable_text.dart index fb3e21d..13b1a3f 100644 --- a/lib/src/extended/widgets/editable_text.dart +++ b/lib/src/extended/widgets/editable_text.dart @@ -346,7 +346,6 @@ class ExtendedEditableTextState extends _EditableTextState { obscuringCharacter: widget.obscuringCharacter, obscureText: widget.obscureText, offset: offset, - onCaretChanged: _handleCaretChanged, rendererIgnoresPointer: widget.rendererIgnoresPointer, cursorWidth: widget.cursorWidth, @@ -848,7 +847,6 @@ class _ExtendedEditable extends _Editable { required super.obscuringCharacter, required super.obscureText, required super.offset, - super.onCaretChanged, super.rendererIgnoresPointer = false, required super.cursorWidth, super.cursorHeight, @@ -891,7 +889,6 @@ class _ExtendedEditable extends _Editable { locale: locale ?? Localizations.maybeLocaleOf(context), selection: value.selection, offset: offset, - onCaretChanged: onCaretChanged, ignorePointer: rendererIgnoresPointer, obscuringCharacter: obscuringCharacter, obscureText: obscureText, diff --git a/lib/src/extended/widgets/text_selection.dart b/lib/src/extended/widgets/text_selection.dart index a3aa1ed..011fc3a 100644 --- a/lib/src/extended/widgets/text_selection.dart +++ b/lib/src/extended/widgets/text_selection.dart @@ -40,7 +40,6 @@ class ExtendedTextSelectionOverlay extends _TextSelectionOverlay { ExtendedTextLibraryUtils.convertTextPainterPostionToTextInputPostion( renderObject.text!, position)!; } - if (_selection.isCollapsed) { _selectionOverlay.updateMagnifier(_buildMagnifier( currentTextPosition: position, @@ -66,7 +65,6 @@ class ExtendedTextSelectionOverlay extends _TextSelectionOverlay { if (newSelection.extentOffset >= _selection.end) { return; // Don't allow order swapping. } - break; case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.linux: @@ -106,13 +104,12 @@ class ExtendedTextSelectionOverlay extends _TextSelectionOverlay { TextPosition position = renderObject.getPositionForPoint(adjustedOffset); - /// zmtzawqlp + // zmtzawqlp if ((renderObject as ExtendedRenderEditable).hasSpecialInlineSpanBase) { position = ExtendedTextLibraryUtils.convertTextPainterPostionToTextInputPostion( renderObject.text!, position)!; } - if (_selection.isCollapsed) { _selectionOverlay.updateMagnifier(_buildMagnifier( currentTextPosition: position, @@ -138,7 +135,6 @@ class ExtendedTextSelectionOverlay extends _TextSelectionOverlay { if (position.offset <= _selection.start) { return; // Don't allow order swapping. } - break; case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.linux: diff --git a/lib/src/official/material/selectable_text.dart b/lib/src/official/material/selectable_text.dart index 0813ea9..a4fd999 100644 --- a/lib/src/official/material/selectable_text.dart +++ b/lib/src/official/material/selectable_text.dart @@ -84,7 +84,6 @@ class _SelectableTextSelectionGestureDetectorBuilder case TargetPlatform.iOS: case TargetPlatform.macOS: renderEditable.selectWordEdge(cause: SelectionChangedCause.tap); - break; case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.linux: @@ -106,7 +105,7 @@ class _SelectableTextSelectionGestureDetectorBuilder /// A run of selectable text with a single style. /// -/// The [_SelectableText] widget displays a string of text with a single style. +/// The [SelectableText] widget displays a string of text with a single style. /// The string might break across multiple lines or might all be displayed on /// the same line depending on the layout constraints. /// @@ -132,7 +131,7 @@ class _SelectableTextSelectionGestureDetectorBuilder /// ``` /// {@end-tool} /// -/// Using the [SelectableText.rich] constructor, the [_SelectableText] widget can +/// Using the [SelectableText.rich] constructor, the [SelectableText] widget can /// display a paragraph with differently styled [TextSpan]s. The sample /// that follows displays "Hello beautiful world" with different styles /// for each word. @@ -154,7 +153,7 @@ class _SelectableTextSelectionGestureDetectorBuilder /// /// ## Interactivity /// -/// To make [_SelectableText] react to touch events, use callback [onTap] to achieve +/// To make [SelectableText] react to touch events, use callback [onTap] to achieve /// the desired behavior. /// /// See also: diff --git a/lib/src/official/rendering/editable.dart b/lib/src/official/rendering/editable.dart index 6d7da2a..47f61e6 100644 --- a/lib/src/official/rendering/editable.dart +++ b/lib/src/official/rendering/editable.dart @@ -5,11 +5,11 @@ const double _kCaretHeightOffset = 2.0; // pixels // The additional size on the x and y axis with which to expand the prototype // cursor to render the floating cursor in pixels. -const EdgeInsets _kFloatingCaretSizeIncrease = +const EdgeInsets _kFloatingCursorSizeIncrease = EdgeInsets.symmetric(horizontal: 0.5, vertical: 1.0); // The corner radius of the floating cursor in pixels. -const Radius _kFloatingCaretRadius = Radius.circular(1.0); +const Radius _kFloatingCursorRadius = Radius.circular(1.0); /// The consecutive sequence of [TextPosition]s that the caret should move to /// when the user navigates the paragraph using the upward arrow key or the @@ -185,6 +185,7 @@ class VerticalCaretMovementRun implements Iterator { } } +/// [RenderEditable] /// Displays some text in a scrollable container with a potentially blinking /// cursor and with gesture recognizers. /// @@ -209,7 +210,7 @@ class _RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, ContainerRenderObjectMixin, - RenderBoxContainerDefaultsMixin + RenderInlineChildrenContainerDefaults implements TextLayoutMetrics { /// Creates a render object that implements the visual aspects of a text field. /// @@ -243,7 +244,6 @@ class _RenderEditable extends RenderBox double textScaleFactor = 1.0, TextSelection? selection, required ViewportOffset offset, - this.onCaretChanged, this.ignorePointer = false, bool readOnly = false, bool forceLine = true, @@ -330,14 +330,6 @@ class _RenderEditable extends RenderBox _updateForegroundPainter(foregroundPainter); _updatePainter(painter); addAll(children); - _extractPlaceholderSpans(text); - } - - @override - void setupParentData(RenderBox child) { - if (child.parentData is! TextParentData) { - child.parentData = TextParentData(); - } } /// Child render objects @@ -381,17 +373,6 @@ class _RenderEditable extends RenderBox _foregroundPainter = newPainter; } - late List _placeholderSpans; - void _extractPlaceholderSpans(InlineSpan? span) { - _placeholderSpans = []; - span?.visitChildren((InlineSpan span) { - if (span is PlaceholderSpan) { - _placeholderSpans.add(span); - } - return true; - }); - } - /// The [RenderEditablePainter] to use for painting above this /// [RenderEditable]'s text content. /// @@ -440,9 +421,8 @@ class _RenderEditable extends RenderBox } // Caret Painters: - // The floating painter. This painter paints the regular caret as well. - late final _FloatingCursorPainter _caretPainter = - _FloatingCursorPainter(_onCaretChanged); + // A single painter for both the regular caret and the floating cursor. + late final _CaretPainter _caretPainter = _CaretPainter(); // Text Highlight painters: final _TextHighlightPainter _selectionPainter = _TextHighlightPainter(); @@ -485,19 +465,6 @@ class _RenderEditable extends RenderBox ); } - Rect? _lastCaretRect; - // TODO(LongCatIsLooong): currently EditableText uses this callback to keep - // the text field visible. But we don't always paint the caret, for example - // when the selection is not collapsed. - /// Called during the paint phase when the caret location changes. - CaretChangedHandler? onCaretChanged; - void _onCaretChanged(Rect caretRect) { - if (_lastCaretRect != caretRect) { - onCaretChanged?.call(caretRect); - } - _lastCaretRect = onCaretChanged == null ? null : caretRect; - } - /// Whether the [handleEvent] will propagate pointer events to selection /// handlers. /// @@ -786,7 +753,7 @@ class _RenderEditable extends RenderBox _textPainter.text = value; _cachedAttributedValue = null; _cachedCombinedSemanticsInfos = null; - _extractPlaceholderSpans(value); + _canComputeIntrinsicsCached = null; markNeedsTextLayout(); markNeedsSemanticsUpdate(); } @@ -1249,7 +1216,9 @@ class _RenderEditable extends RenderBox // [assembleSemanticsNode] invocations. LinkedHashMap? _cachedChildNodes; - /// Returns a list of rects that bound the given selection. + /// Returns a list of rects that bound the given selection, and the text + /// direction. The text direction is used by the engine to calculate + /// the closest position to a given point. /// /// See [TextPainter.getBoxesForSelection] for more details. List getBoxesForSelection(TextSelection selection) { @@ -1389,13 +1358,7 @@ class _RenderEditable extends RenderBox final SemanticsNode childNode = children.elementAt(childIndex); final TextParentData parentData = child!.parentData! as TextParentData; - assert(parentData.scale != null); - childNode.rect = Rect.fromLTWH( - childNode.rect.left, - childNode.rect.top, - childNode.rect.width * parentData.scale!, - childNode.rect.height * parentData.scale!, - ); + assert(parentData.offset != null); newChildren.add(childNode); childIndex += 1; } @@ -1827,7 +1790,6 @@ class _RenderEditable extends RenderBox caretRect.width, caretRect.height, ); - break; case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.linux: @@ -1841,7 +1803,6 @@ class _RenderEditable extends RenderBox caretRect.width, caretHeight, ); - break; } caretRect = caretRect.shift(_paintOffset); @@ -1945,9 +1906,6 @@ class _RenderEditable extends RenderBox @override @protected bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { - // Hit test text spans. - bool hitText = false; - final InlineSpan? textSpan = _textPainter.text; if (textSpan != null) { final Offset effectivePosition = position - _paintOffset; @@ -1956,46 +1914,10 @@ class _RenderEditable extends RenderBox final Object? span = textSpan.getSpanForPosition(textPosition); if (span is HitTestTarget) { result.add(HitTestEntry(span)); - hitText = true; - } - } - // Hit test render object children - RenderBox? child = firstChild; - int childIndex = 0; - while (child != null && - childIndex < _textPainter.inlinePlaceholderBoxes!.length) { - final TextParentData textParentData = child.parentData! as TextParentData; - final Matrix4 transform = Matrix4.translationValues( - textParentData.offset.dx, - textParentData.offset.dy, - 0.0, - )..scale( - textParentData.scale, - textParentData.scale, - textParentData.scale, - ); - final bool isHit = result.addWithPaintTransform( - transform: transform, - position: position, - hitTest: (BoxHitTestResult result, Offset transformed) { - assert(() { - final Offset manualPosition = - (position - textParentData.offset) / textParentData.scale!; - return (transformed.dx - manualPosition.dx).abs() < - precisionErrorTolerance && - (transformed.dy - manualPosition.dy).abs() < - precisionErrorTolerance; - }()); - return child!.hitTest(result, position: transformed); - }, - ); - if (isHit) { return true; } - child = childAfter(child); - childIndex += 1; } - return hitText; + return hitTestInlineChildren(result, position); } late TapGestureRecognizer _tap; @@ -2154,12 +2076,12 @@ class _RenderEditable extends RenderBox _computeTextMetricsIfNeeded(); final TextPosition fromPosition = _textPainter.getPositionForOffset(globalToLocal(from - _paintOffset)); - final TextSelection fromWord = _getWordAtOffset(fromPosition); + final TextSelection fromWord = getWordAtOffset(fromPosition); final TextPosition toPosition = to == null ? fromPosition : _textPainter.getPositionForOffset(globalToLocal(to - _paintOffset)); final TextSelection toWord = - toPosition == fromPosition ? fromWord : _getWordAtOffset(toPosition); + toPosition == fromPosition ? fromWord : getWordAtOffset(toPosition); final bool isFromWordBeforeToWord = fromWord.start < toWord.end; _setSelection( @@ -2194,7 +2116,10 @@ class _RenderEditable extends RenderBox _setSelection(newSelection, cause); } - TextSelection _getWordAtOffset(TextPosition position) { + /// Returns a [TextSelection] that encompasses the word at the given + /// [TextPosition]. + @visibleForTesting + TextSelection getWordAtOffset(TextPosition position) { debugAssertLayoutUpToDate(); // When long-pressing past the end of the text, we want a collapsed cursor. if (position.offset >= plainText.length) { @@ -2211,10 +2136,10 @@ class _RenderEditable extends RenderBox case TextAffinity.upstream: // upstream affinity is effectively -1 in text position. effectiveOffset = position.offset - 1; - break; case TextAffinity.downstream: effectiveOffset = position.offset; } + assert(effectiveOffset >= 0); // On iOS, select the previous word if there is a previous word, or select // to the end of the next word if there is a next word. Select nothing if @@ -2223,8 +2148,8 @@ class _RenderEditable extends RenderBox // If the platform is Android and the text is read only, try to select the // previous word if there is one; otherwise, select the single whitespace at // the position. - if (TextLayoutMetrics.isWhitespace(plainText.codeUnitAt(effectiveOffset)) && - effectiveOffset > 0) { + if (effectiveOffset > 0 && + TextLayoutMetrics.isWhitespace(plainText.codeUnitAt(effectiveOffset))) { final TextRange? previousWord = _getPreviousWord(word.start); switch (defaultTargetPlatform) { case TargetPlatform.iOS: @@ -2255,7 +2180,6 @@ class _RenderEditable extends RenderBox extentOffset: position.offset, ); } - break; case TargetPlatform.fuchsia: case TargetPlatform.macOS: case TargetPlatform.linux: @@ -2274,84 +2198,6 @@ class _RenderEditable extends RenderBox // restored to the original values before final layout and painting. List? _placeholderDimensions; - // Layout the child inline widgets. We then pass the dimensions of the - // children to _textPainter so that appropriate placeholders can be inserted - // into the LibTxt layout. This does not do anything if no inline widgets were - // specified. - List _layoutChildren(BoxConstraints constraints, - {bool dry = false}) { - if (childCount == 0) { - _textPainter.setPlaceholderDimensions([]); - return []; - } - RenderBox? child = firstChild; - final List placeholderDimensions = - List.filled( - childCount, PlaceholderDimensions.empty); - int childIndex = 0; - // Only constrain the width to the maximum width of the paragraph. - // Leave height unconstrained, which will overflow if expanded past. - BoxConstraints boxConstraints = - BoxConstraints(maxWidth: constraints.maxWidth); - // The content will be enlarged by textScaleFactor during painting phase. - // We reduce constraints by textScaleFactor, so that the content will fit - // into the box once it is enlarged. - boxConstraints = boxConstraints / textScaleFactor; - while (child != null) { - double? baselineOffset; - final Size childSize; - if (!dry) { - child.layout( - boxConstraints, - parentUsesSize: true, - ); - childSize = child.size; - switch (_placeholderSpans[childIndex].alignment) { - case ui.PlaceholderAlignment.baseline: - baselineOffset = child.getDistanceToBaseline( - _placeholderSpans[childIndex].baseline!, - ); - break; - case ui.PlaceholderAlignment.aboveBaseline: - case ui.PlaceholderAlignment.belowBaseline: - case ui.PlaceholderAlignment.bottom: - case ui.PlaceholderAlignment.middle: - case ui.PlaceholderAlignment.top: - baselineOffset = null; - } - } else { - assert(_placeholderSpans[childIndex].alignment != - ui.PlaceholderAlignment.baseline); - childSize = child.getDryLayout(boxConstraints); - } - placeholderDimensions[childIndex] = PlaceholderDimensions( - size: childSize, - alignment: _placeholderSpans[childIndex].alignment, - baseline: _placeholderSpans[childIndex].baseline, - baselineOffset: baselineOffset, - ); - child = childAfter(child); - childIndex += 1; - } - return placeholderDimensions; - } - - void _setParentData() { - RenderBox? child = firstChild; - int childIndex = 0; - while (child != null && - childIndex < _textPainter.inlinePlaceholderBoxes!.length) { - final TextParentData textParentData = child.parentData! as TextParentData; - textParentData.offset = Offset( - _textPainter.inlinePlaceholderBoxes![childIndex].left, - _textPainter.inlinePlaceholderBoxes![childIndex].top, - ); - textParentData.scale = _textPainter.inlinePlaceholderScales![childIndex]; - child = childAfter(child); - childIndex += 1; - } - } - void _layoutText({double minWidth = 0.0, double maxWidth = double.infinity}) { final double availableMaxWidth = math.max(0.0, maxWidth - _caretMargin); final double availableMinWidth = math.min(minWidth, availableMaxWidth); @@ -2403,7 +2249,6 @@ class _RenderEditable extends RenderBox case TargetPlatform.macOS: _caretPrototype = Rect.fromLTWH(0.0, 0.0, cursorWidth, cursorHeight + 2); - break; case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.linux: @@ -2430,36 +2275,38 @@ class _RenderEditable extends RenderBox ); } - bool _canComputeDryLayout() { - // Dry layout cannot be calculated without a full layout for - // alignments that require the baseline (baseline, aboveBaseline, - // belowBaseline). - for (final PlaceholderSpan span in _placeholderSpans) { - switch (span.alignment) { - case ui.PlaceholderAlignment.baseline: - case ui.PlaceholderAlignment.aboveBaseline: - case ui.PlaceholderAlignment.belowBaseline: - return false; - case ui.PlaceholderAlignment.top: - case ui.PlaceholderAlignment.middle: - case ui.PlaceholderAlignment.bottom: - continue; - } - } - return true; + bool _canComputeDryLayoutForInlineWidgets() { + return text?.visitChildren((InlineSpan span) { + return (span is! PlaceholderSpan) || + switch (span.alignment) { + ui.PlaceholderAlignment.baseline || + ui.PlaceholderAlignment.aboveBaseline || + ui.PlaceholderAlignment.belowBaseline => + false, + ui.PlaceholderAlignment.top || + ui.PlaceholderAlignment.middle || + ui.PlaceholderAlignment.bottom => + true, + }; + }) ?? + true; } + bool? _canComputeIntrinsicsCached; + bool get _canComputeIntrinsics => + _canComputeIntrinsicsCached ??= _canComputeDryLayoutForInlineWidgets(); + @override Size computeDryLayout(BoxConstraints constraints) { - if (!_canComputeDryLayout()) { + if (!_canComputeIntrinsics) { assert(debugCannotComputeDryLayout( reason: 'Dry layout not available for alignments that require baseline.', )); return Size.zero; } - _textPainter - .setPlaceholderDimensions(_layoutChildren(constraints, dry: true)); + _textPainter.setPlaceholderDimensions(layoutInlineChildren( + constraints.maxWidth, ChildLayoutHelper.dryLayoutChild)); _layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth); final double width = forceLine ? constraints.maxWidth @@ -2471,10 +2318,11 @@ class _RenderEditable extends RenderBox @override void performLayout() { final BoxConstraints constraints = this.constraints; - _placeholderDimensions = _layoutChildren(constraints); + _placeholderDimensions = layoutInlineChildren( + constraints.maxWidth, ChildLayoutHelper.layoutChild); _textPainter.setPlaceholderDimensions(_placeholderDimensions); _computeTextMetricsIfNeeded(); - _setParentData(); + positionInlineChildren(_textPainter.inlinePlaceholderBoxes!); _computeCaretPrototype(); // We grab _textPainter.size here because assigning to `size` on the next // line will trigger us to validate our intrinsic sizes, which will change @@ -2594,8 +2442,8 @@ class _RenderEditable extends RenderBox final double? animationValue = _resetFloatingCursorAnimationValue; final EdgeInsets sizeAdjustment = animationValue != null ? EdgeInsets.lerp( - _kFloatingCaretSizeIncrease, EdgeInsets.zero, animationValue)! - : _kFloatingCaretSizeIncrease; + _kFloatingCursorSizeIncrease, EdgeInsets.zero, animationValue)! + : _kFloatingCursorSizeIncrease; _caretPainter.floatingCursorRect = sizeAdjustment.inflateRect(_caretPrototype).shift(boundedOffset); } else { @@ -2674,32 +2522,7 @@ class _RenderEditable extends RenderBox } _textPainter.paint(context.canvas, effectiveOffset); - - RenderBox? child = firstChild; - int childIndex = 0; - // childIndex might be out of index of placeholder boxes. This can happen - // if engine truncates children due to ellipsis. Sadly, we would not know - // it until we finish layout, and RenderObject is in immutable state at - // this point. - while (child != null && - childIndex < _textPainter.inlinePlaceholderBoxes!.length) { - final TextParentData textParentData = child.parentData! as TextParentData; - - final double scale = textParentData.scale!; - context.pushTransform( - needsCompositing, - effectiveOffset + textParentData.offset, - Matrix4.diagonal3Values(scale, scale, scale), - (PaintingContext context, Offset offset) { - context.paintChild( - child!, - offset, - ); - }, - ); - child = childAfter(child); - childIndex += 1; - } + paintInlineChildren(context, effectiveOffset); if (foregroundChild != null) { context.paintChild(foregroundChild, offset); @@ -2732,6 +2555,14 @@ class _RenderEditable extends RenderBox } } + @override + void applyPaintTransform(RenderBox child, Matrix4 transform) { + if (child == _foregroundRenderObject || child == _backgroundRenderObject) { + return; + } + defaultApplyPaintTransform(child, transform); + } + @override void paint(PaintingContext context, Offset offset) { _computeTextMetricsIfNeeded(); @@ -3011,8 +2842,8 @@ class _TextHighlightPainter extends RenderEditablePainter { } } -class _FloatingCursorPainter extends RenderEditablePainter { - _FloatingCursorPainter(this.caretPaintCallback); +class _CaretPainter extends RenderEditablePainter { + _CaretPainter(); bool get shouldPaint => _shouldPaint; bool _shouldPaint = true; @@ -3024,8 +2855,6 @@ class _FloatingCursorPainter extends RenderEditablePainter { notifyListeners(); } - CaretChangedHandler caretPaintCallback; - bool showRegularCaret = false; final Paint caretPaint = Paint(); @@ -3099,7 +2928,6 @@ class _FloatingCursorPainter extends RenderEditablePainter { canvas.drawRRect(caretRRect, caretPaint); } } - caretPaintCallback(integralRect); } // zmtzawqlp @@ -3141,7 +2969,7 @@ class _FloatingCursorPainter extends RenderEditablePainter { } canvas.drawRRect( - RRect.fromRectAndRadius(floatingCursorRect, _kFloatingCaretRadius), + RRect.fromRectAndRadius(floatingCursorRect, _kFloatingCursorRadius), floatingCursorPaint..color = floatingCursorColor, ); } @@ -3155,7 +2983,7 @@ class _FloatingCursorPainter extends RenderEditablePainter { if (oldDelegate == null) { return shouldPaint; } - return oldDelegate is! _FloatingCursorPainter || + return oldDelegate is! _CaretPainter || oldDelegate.shouldPaint != shouldPaint || oldDelegate.showRegularCaret != showRegularCaret || oldDelegate.caretColor != caretColor || diff --git a/lib/src/official/widgets/editable_text.dart b/lib/src/official/widgets/editable_text.dart index 0806943..9f2a534 100644 --- a/lib/src/official/widgets/editable_text.dart +++ b/lib/src/official/widgets/editable_text.dart @@ -4,6 +4,7 @@ part of 'package:extended_text_field/src/extended/widgets/text_field.dart'; // [TextPosition] after applying the given [TextBoundary]. typedef _ApplyTextBoundary = TextPosition Function( TextPosition, bool, TextBoundary); + // The time it takes for the cursor to fade from fully opaque to fully // transparent and vice versa. A full cursor blink, from transparent to opaque // to transparent, is twice this duration. @@ -13,6 +14,19 @@ const Duration _kCursorBlinkHalfPeriod = Duration(milliseconds: 500); // is shown in an obscured text field. const int _kObscureShowLatestCharCursorTicks = 3; +/// The default mime types to be used when allowedMimeTypes is not provided. +/// +/// The default value supports inserting images of any supported format. +const List kDefaultContentInsertionMimeTypes = [ + 'image/png', + 'image/bmp', + 'image/jpg', + 'image/tiff', + 'image/gif', + 'image/jpeg', + 'image/webp' +]; + class _CompositionCallback extends SingleChildRenderObjectWidget { const _CompositionCallback( {required this.compositeCallback, required this.enabled, super.child}); @@ -147,6 +161,7 @@ class _DiscreteKeyFrameSimulation extends Simulation { } } +/// [EditableText] /// A basic text input field. /// /// This widget interacts with the [TextInput] service to let the user edit the @@ -461,7 +476,7 @@ class _EditableText extends StatefulWidget { assert( spellCheckConfiguration == null || spellCheckConfiguration == - const _SpellCheckConfiguration.disabled() || + const SpellCheckConfiguration.disabled() || spellCheckConfiguration.misspelledTextStyle != null, 'spellCheckConfiguration must specify a misspelledTextStyle if spell check behavior is desired', ), @@ -555,7 +570,7 @@ class _EditableText extends StatefulWidget { /// {@template flutter.widgets.editableText.showCursor} /// Whether to show cursor. /// - /// The cursor refers to the blinking caret when the [_EditableText] is focused. + /// The cursor refers to the blinking caret when the [EditableText] is focused. /// {@endtemplate} /// /// See also: @@ -725,7 +740,7 @@ class _EditableText extends StatefulWidget { /// /// The full set of behaviors possible with [minLines] and [maxLines] are as /// follows. These examples apply equally to [TextField], [TextFormField], - /// [CupertinoTextField], and [_EditableText]. + /// [CupertinoTextField], and [EditableText]. /// /// Input that occupies a single line and scrolls horizontally as needed. /// ```dart @@ -780,7 +795,7 @@ class _EditableText extends StatefulWidget { /// /// A few examples of behaviors possible with [minLines] and [maxLines] are as follows. /// These apply equally to [TextField], [TextFormField], [CupertinoTextField], - /// and [_EditableText]. + /// and [EditableText]. /// /// Input that always occupies at least 2 lines and has an infinite max. /// Expands vertically as needed. @@ -856,16 +871,16 @@ class _EditableText extends StatefulWidget { /// {@template flutter.widgets.editableText.selectionControls} /// Optional delegate for building the text selection handles and toolbar. /// - /// The [_EditableText] widget used on its own will not trigger the display + /// The [EditableText] widget used on its own will not trigger the display /// of the selection toolbar by itself. The toolbar is shown by calling - /// [_EditableTextState.showToolbar] in response to an appropriate user event. + /// [EditableTextState.showToolbar] in response to an appropriate user event. /// /// See also: /// - /// * [CupertinoTextField], which wraps an [_EditableText] and which shows the + /// * [CupertinoTextField], which wraps an [EditableText] and which shows the /// selection toolbar upon user events that are appropriate on the iOS /// platform. - /// * [TextField], a Material Design themed wrapper of [_EditableText], which + /// * [TextField], a Material Design themed wrapper of [EditableText], which /// shows the selection toolbar upon appropriate user events based on the /// user's platform set in [ThemeData.platform]. /// {@endtemplate} @@ -1064,7 +1079,7 @@ class _EditableText extends StatefulWidget { /// /// If this property is null, [SystemMouseCursors.text] will be used. /// - /// The [mouseCursor] is the only property of [_EditableText] that controls the + /// The [mouseCursor] is the only property of [EditableText] that controls the /// appearance of the mouse pointer. All other properties related to "cursor" /// stands for the text cursor, which is usually a blinking vertical line at /// the editing position. @@ -1289,7 +1304,7 @@ class _EditableText extends StatefulWidget { /// The [AutofillClient] that controls this input field's autofill behavior. /// - /// When null, this widget's [_EditableTextState] will be used as the + /// When null, this widget's [EditableTextState] will be used as the /// [AutofillClient]. This property may override [autofillHints]. final AutofillClient? autofillClient; @@ -1299,15 +1314,15 @@ class _EditableText extends StatefulWidget { final Clip clipBehavior; /// Restoration ID to save and restore the scroll offset of the - /// [_EditableText]. + /// [EditableText]. /// - /// If a restoration id is provided, the [_EditableText] will persist its + /// If a restoration id is provided, the [EditableText] will persist its /// current scroll offset and restore it during state restoration. /// /// The scroll offset is persisted in a [RestorationBucket] claimed from /// the surrounding [RestorationScope] using the provided restoration ID. /// - /// Persisting and restoring the content of the [_EditableText] is the + /// Persisting and restoring the content of the [EditableText] is the /// responsibility of the owner of the [controller], who may use a /// [RestorableTextEditingController] for that purpose. /// @@ -1432,12 +1447,12 @@ class _EditableText extends StatefulWidget { /// Returns the [ContextMenuButtonItem]s representing the buttons in this /// platform's default selection menu for an editable field. /// - /// For example, [_EditableText] uses this to generate the default buttons for + /// For example, [EditableText] uses this to generate the default buttons for /// its context menu. /// /// See also: /// - /// * [_EditableTextState.contextMenuButtonItems], which gives the + /// * [EditableTextState.contextMenuButtonItems], which gives the /// [ContextMenuButtonItem]s for a specific EditableText. /// * [SelectableRegion.getSelectableButtonItems], which performs a similar /// role but for content that is selectable but not editable. @@ -1452,36 +1467,49 @@ class _EditableText extends StatefulWidget { required final VoidCallback? onCut, required final VoidCallback? onPaste, required final VoidCallback? onSelectAll, + required final VoidCallback? onLiveTextInput, }) { - // If the paste button is enabled, don't render anything until the state - // of the clipboard is known, since it's used to determine if paste is - // shown. - if (onPaste != null && clipboardStatus == ClipboardStatus.unknown) { - return []; + final List resultButtonItem = + []; + + // Configure button items with clipboard. + if (onPaste == null || clipboardStatus != ClipboardStatus.unknown) { + // If the paste button is enabled, don't render anything until the state + // of the clipboard is known, since it's used to determine if paste is + // shown. + resultButtonItem.addAll([ + if (onCut != null) + ContextMenuButtonItem( + onPressed: onCut, + type: ContextMenuButtonType.cut, + ), + if (onCopy != null) + ContextMenuButtonItem( + onPressed: onCopy, + type: ContextMenuButtonType.copy, + ), + if (onPaste != null) + ContextMenuButtonItem( + onPressed: onPaste, + type: ContextMenuButtonType.paste, + ), + if (onSelectAll != null) + ContextMenuButtonItem( + onPressed: onSelectAll, + type: ContextMenuButtonType.selectAll, + ), + ]); } - return [ - if (onCut != null) - ContextMenuButtonItem( - onPressed: onCut, - type: ContextMenuButtonType.cut, - ), - if (onCopy != null) - ContextMenuButtonItem( - onPressed: onCopy, - type: ContextMenuButtonType.copy, - ), - if (onPaste != null) - ContextMenuButtonItem( - onPressed: onPaste, - type: ContextMenuButtonType.paste, - ), - if (onSelectAll != null) - ContextMenuButtonItem( - onPressed: onSelectAll, - type: ContextMenuButtonType.selectAll, - ), - ]; + // Config button items with Live Text. + if (onLiveTextInput != null) { + resultButtonItem.add(ContextMenuButtonItem( + onPressed: onLiveTextInput, + type: ContextMenuButtonType.liveTextInput, + )); + } + + return resultButtonItem; } // Infer the keyboard type of an `EditableText` if it's not specified. @@ -1551,7 +1579,6 @@ class _EditableText extends StatefulWidget { if (keyboardType != null) { return keyboardType; } - break; case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.linux: @@ -1705,9 +1732,9 @@ class _EditableText extends StatefulWidget { properties.add(DiagnosticsProperty( 'undoController', undoController, defaultValue: null)); - properties.add(DiagnosticsProperty<_SpellCheckConfiguration>( - 'spellCheckConfiguration', spellCheckConfiguration, - defaultValue: null)); + // properties.add(DiagnosticsProperty( + // 'spellCheckConfiguration', spellCheckConfiguration, + // defaultValue: null)); properties.add(DiagnosticsProperty>('contentCommitMimeTypes', contentInsertionConfiguration?.allowedMimeTypes ?? const [], defaultValue: contentInsertionConfiguration == null @@ -1716,7 +1743,7 @@ class _EditableText extends StatefulWidget { } } -/// State for a [_EditableText]. +/// State for a [EditableText]. /// zmtzawqlp class _EditableTextState extends State<_EditableText> with @@ -1744,9 +1771,17 @@ class _EditableTextState extends State<_EditableText> /// Detects whether the clipboard can paste. final ClipboardStatusNotifier clipboardStatus = ClipboardStatusNotifier(); + /// Detects whether the Live Text input is enabled. + /// + /// See also: + /// * [LiveText], where the availability of Live Text input can be obtained. + final LiveTextInputStatusNotifier? _liveTextInputStatus = + kIsWeb ? null : LiveTextInputStatusNotifier(); + TextInputConnection? _textInputConnection; bool get _hasInputConnection => _textInputConnection?.attached ?? false; + /// zmtzawqlp _TextSelectionOverlay? _selectionOverlay; final GlobalKey _scrollableKey = GlobalKey(); @@ -1767,6 +1802,7 @@ class _EditableTextState extends State<_EditableText> AutofillClient get _effectiveAutofillClient => widget.autofillClient ?? this; + /// zmtzawqlp late _SpellCheckConfiguration _spellCheckConfiguration; late TextStyle _style; @@ -1886,12 +1922,26 @@ class _EditableTextState extends State<_EditableText> } } + @override + bool get liveTextInputEnabled { + return _liveTextInputStatus?.value == LiveTextInputStatus.enabled && + !widget.obscureText && + !widget.readOnly && + textEditingValue.selection.isCollapsed; + } + void _onChangedClipboardStatus() { setState(() { // Inform the widget that the value of clipboardStatus has changed. }); } + void _onChangedLiveTextInputStatus() { + setState(() { + // Inform the widget that the value of liveTextInputStatus has changed. + }); + } + TextEditingValue get _textEditingValueforTextLayoutMetrics { final Widget? editableWidget = _editableKey.currentContext?.widget; if (editableWidget is! _Editable) { @@ -2027,7 +2077,6 @@ class _EditableTextState extends State<_EditableText> case TargetPlatform.linux: case TargetPlatform.windows: hideToolbar(); - break; } switch (defaultTargetPlatform) { case TargetPlatform.android: @@ -2035,7 +2084,6 @@ class _EditableTextState extends State<_EditableText> case TargetPlatform.linux: case TargetPlatform.windows: bringIntoView(textEditingValue.selection.extent); - break; case TargetPlatform.macOS: case TargetPlatform.iOS: break; @@ -2043,6 +2091,18 @@ class _EditableTextState extends State<_EditableText> } } + void _startLiveTextInput(SelectionChangedCause cause) { + if (!liveTextInputEnabled) { + return; + } + if (_hasInputConnection) { + LiveText.startLiveTextInput(); + } + if (cause == SelectionChangedCause.toolbar) { + hideToolbar(); + } + } + /// Finds specified [SuggestionSpan] that matches the provided index using /// binary search. /// @@ -2090,7 +2150,7 @@ class _EditableTextState extends State<_EditableText> final SpellCheckService? spellCheckService = configuration?.spellCheckService; final bool spellCheckAutomaticallyDisabled = configuration == null || - configuration == const _SpellCheckConfiguration.disabled(); + configuration == const SpellCheckConfiguration.disabled(); final bool spellCheckServiceIsConfigured = spellCheckService != null || spellCheckService == null && WidgetsBinding @@ -2243,7 +2303,7 @@ class _EditableTextState extends State<_EditableText> /// /// See also: /// - /// * [_EditableText.getEditableButtonItems], which performs a similar role, + /// * [EditableText.getEditableButtonItems], which performs a similar role, /// but for any editable field, not just specifically EditableText. /// * [SelectableRegionState.contextMenuButtonItems], which performs a similar /// role but for content that is selectable but not editable. @@ -2256,7 +2316,7 @@ class _EditableTextState extends State<_EditableText> /// button Widgets for the current platform given [ContextMenuButtonItem]s. List get contextMenuButtonItems { return buttonItemsForToolbarOptions() ?? - _EditableText.getEditableButtonItems( + EditableText.getEditableButtonItems( clipboardStatus: clipboardStatus.value, onCopy: copyEnabled ? () => copySelection(SelectionChangedCause.toolbar) @@ -2270,6 +2330,9 @@ class _EditableTextState extends State<_EditableText> onSelectAll: selectAllEnabled ? () => selectAll(SelectionChangedCause.toolbar) : null, + onLiveTextInput: liveTextInputEnabled + ? () => _startLiveTextInput(SelectionChangedCause.toolbar) + : null, ); } @@ -2278,6 +2341,7 @@ class _EditableTextState extends State<_EditableText> @override void initState() { super.initState(); + _liveTextInputStatus?.addListener(_onChangedLiveTextInputStatus); clipboardStatus.addListener(_onChangedClipboardStatus); widget.controller.addListener(_didChangeTextEditingValue); widget.focusNode.addListener(_handleFocusChanged); @@ -2321,12 +2385,10 @@ class _EditableTextState extends State<_EditableText> final bool newTickerEnabled = TickerMode.of(context); if (_tickersEnabled != newTickerEnabled) { _tickersEnabled = newTickerEnabled; - if (_tickersEnabled && _cursorActive) { + if (_showBlinkingCursor) { _startCursorBlink(); } else if (!_tickersEnabled && _cursorTimer != null) { - // Cannot use _stopCursorBlink because it would reset _cursorActive. - _cursorTimer!.cancel(); - _cursorTimer = null; + _stopCursorBlink(); } } @@ -2386,7 +2448,11 @@ class _EditableTextState extends State<_EditableText> if (!_shouldCreateInputConnection) { _closeInputConnectionIfNeeded(); } else if (oldWidget.readOnly && _hasFocus) { - _openInputConnection(); + // _openInputConnection must be called after layout information is available. + // See https://github.com/flutter/flutter/issues/126312 + SchedulerBinding.instance.addPostFrameCallback((Duration _) { + _openInputConnection(); + }); } if (kIsWeb && _hasInputConnection) { @@ -2396,6 +2462,13 @@ class _EditableTextState extends State<_EditableText> } } + if (_hasInputConnection) { + if (oldWidget.obscureText != widget.obscureText) { + _textInputConnection! + .updateConfig(_effectiveAutofillClient.textInputConfiguration); + } + } + if (widget.style != oldWidget.style) { // The _textInputConnection will pick up the new style when it attaches in // _openInputConnection. @@ -2412,6 +2485,10 @@ class _EditableTextState extends State<_EditableText> ); } } + + if (widget.showCursor != oldWidget.showCursor) { + _startOrStopCursorTimerIfNeeded(); + } final bool canPaste = widget.selectionControls is TextSelectionHandleControls ? pasteEnabled @@ -2438,6 +2515,8 @@ class _EditableTextState extends State<_EditableText> _selectionOverlay = null; widget.focusNode.removeListener(_handleFocusChanged); WidgetsBinding.instance.removeObserver(this); + _liveTextInputStatus?.removeListener(_onChangedLiveTextInputStatus); + _liveTextInputStatus?.dispose(); clipboardStatus.removeListener(_onChangedClipboardStatus); clipboardStatus.dispose(); _cursorVisibilityNotifier.dispose(); @@ -2500,6 +2579,7 @@ class _EditableTextState extends State<_EditableText> if (_textInputConnection?.scribbleInProgress ?? false) { cause = SelectionChangedCause.scribble; } else if (_pointOffsetOrigin != null) { + // For floating cursor selection when force pressing the space bar. cause = SelectionChangedCause.forcePress; } else { cause = SelectionChangedCause.keyboard; @@ -2526,17 +2606,17 @@ class _EditableTextState extends State<_EditableText> _formatAndSetValue(value, SelectionChangedCause.keyboard); } + if (_showBlinkingCursor && _cursorTimer != null) { + // To keep the cursor from blinking while typing, restart the timer here. + _stopCursorBlink(resetCharTicks: false); + _startCursorBlink(); + } + // Wherever the value is changed by the user, schedule a showCaretOnScreen // to make sure the user can see the changes they just made. Programmatic // changes to `textEditingValue` do not trigger the behavior even if the // text field is focused. _scheduleShowCaretOnScreen(withAnimation: true); - if (_hasInputConnection) { - // To keep the cursor from blinking while typing, we want to restart the - // cursor timer every time a new character is typed. - _stopCursorBlink(resetCharTicks: false); - _startCursorBlink(); - } } bool _checkNeedsAdjustAffinity(TextEditingValue value) { @@ -2557,7 +2637,6 @@ class _EditableTextState extends State<_EditableText> if (!_isMultiline) { _finalizeEditing(action, shouldUnfocus: true); } - break; case TextInputAction.done: case TextInputAction.go: case TextInputAction.next: @@ -2565,7 +2644,6 @@ class _EditableTextState extends State<_EditableText> case TextInputAction.search: case TextInputAction.send: _finalizeEditing(action, shouldUnfocus: true); - break; case TextInputAction.continueAction: case TextInputAction.emergencyCall: case TextInputAction.join: @@ -2575,7 +2653,6 @@ class _EditableTextState extends State<_EditableText> // Finalize editing, but don't give up focus because this keyboard // action does not imply the user is done inputting information. _finalizeEditing(action, shouldUnfocus: false); - break; } } @@ -2639,7 +2716,6 @@ class _EditableTextState extends State<_EditableText> _lastTextPosition = currentTextPosition; renderEditable.setFloatingCursor( point.state, _lastBoundedOffset!, _lastTextPosition!); - break; case FloatingCursorDragState.Update: final Offset centeredPoint = point.offset! - _pointOffsetOrigin!; final Offset rawCursorOffset = @@ -2651,7 +2727,6 @@ class _EditableTextState extends State<_EditableText> .localToGlobal(_lastBoundedOffset! + _floatingCursorOffset)); renderEditable.setFloatingCursor( point.state, _lastBoundedOffset!, _lastTextPosition!); - break; case FloatingCursorDragState.End: // Resume cursor blinking. _startCursorBlink(); @@ -2672,8 +2747,22 @@ class _EditableTextState extends State<_EditableText> if (_floatingCursorResetController!.isCompleted) { renderEditable.setFloatingCursor( FloatingCursorDragState.End, finalPosition, _lastTextPosition!); - // Only change if the current selection range is collapsed, to prevent - // overwriting the result of the iOS keyboard selection gesture. + // During a floating cursor's move gesture (1 finger), a cursor is + // animated only visually, without actually updating the selection. + // Only after move gesture is complete, this function will be called + // to actually update the selection to the new cursor location with + // zero selection length. + + // However, During a floating cursor's selection gesture (2 fingers), the + // selection is constantly updated by the engine throughout the gesture. + // Thus when the gesture is complete, we should not update the selection + // to the cursor location with zero selection length, because that would + // overwrite the selection made by floating cursor selection. + + // Here we use `isCollapsed` to distinguish between floating cursor's + // move gesture (1 finger) vs selection gesture (2 fingers), as + // the engine does not provide information other than notifying a + // new selection during with selection gesture (2 fingers). if (renderEditable.selection!.isCollapsed) { // The cause is technically the force cursor, but the cause is listed as tap as the desired functionality is the same. _handleSelectionChanged(TextSelection.fromPosition(_lastTextPosition!), @@ -2730,13 +2819,10 @@ class _EditableTextState extends State<_EditableText> case TextInputAction.emergencyCall: case TextInputAction.newline: widget.focusNode.unfocus(); - break; case TextInputAction.next: widget.focusNode.nextFocus(); - break; case TextInputAction.previous: widget.focusNode.previousFocus(); - break; } } } @@ -2788,7 +2874,7 @@ class _EditableTextState extends State<_EditableText> /// Ends the current batch edit started by the last call to [beginBatchEdit], /// and send [currentTextEditingValue] to the text input plugin if needed. /// - /// Throws an error in debug mode if this [_EditableText] is not in a batch + /// Throws an error in debug mode if this [EditableText] is not in a batch /// edit. void endBatchEdit() { _batchEditDepth -= 1; @@ -2880,6 +2966,8 @@ class _EditableTextState extends State<_EditableText> bool get _needsAutofill => _effectiveAutofillClient .textInputConfiguration.autofillConfiguration.enabled; + // Must be called after layout. + // See https://github.com/flutter/flutter/issues/126312 void _openInputConnection() { if (!_shouldCreateInputConnection) { return; @@ -3000,7 +3088,11 @@ class _EditableTextState extends State<_EditableText> _textInputConnection!.connectionClosedReceived(); _textInputConnection = null; _lastKnownRemoteTextEditingValue = null; - _finalizeEditing(TextInputAction.done, shouldUnfocus: true); + if (kIsWeb) { + _finalizeEditing(TextInputAction.done, shouldUnfocus: true); + } else { + widget.focusNode.unfocus(); + } } } @@ -3054,7 +3146,10 @@ class _EditableTextState extends State<_EditableText> _scribbleCacheKey = null; } + /// zmtzawqlp _TextSelectionOverlay _createSelectionOverlay() { + // final EditableTextContextMenuBuilder? contextMenuBuilder = + // widget.contextMenuBuilder; final _TextSelectionOverlay selectionOverlay = _TextSelectionOverlay( clipboardStatus: clipboardStatus, context: context, @@ -3069,10 +3164,10 @@ class _EditableTextState extends State<_EditableText> dragStartBehavior: widget.dragStartBehavior, onSelectionHandleTapped: widget.onSelectionHandleTapped, // zmtzawqlp - // contextMenuBuilder: widget.contextMenuBuilder == null + // contextMenuBuilder: contextMenuBuilder == null // ? null // : (BuildContext context) { - // return widget.contextMenuBuilder!( + // return contextMenuBuilder( // context, // this, // ); @@ -3110,12 +3205,10 @@ class _EditableTextState extends State<_EditableText> case SelectionChangedCause.tap: case SelectionChangedCause.toolbar: requestKeyboard(); - break; case SelectionChangedCause.keyboard: if (_hasFocus) { requestKeyboard(); } - break; } if (widget.selectionControls == null && widget.contextMenuBuilder == null) { _selectionOverlay?.dispose(); @@ -3145,18 +3238,12 @@ class _EditableTextState extends State<_EditableText> } // To keep the cursor from blinking while it moves, restart the timer here. - if (_cursorTimer != null) { + if (_showBlinkingCursor && _cursorTimer != null) { _stopCursorBlink(resetCharTicks: false); _startCursorBlink(); } } - Rect? _currentCaretRect; - // ignore: use_setters_to_change_properties, (this is used as a callback, can't be a setter) - void _handleCaretChanged(Rect caretRect) { - _currentCaretRect = caretRect; - } - // Animation configuration for scrolling the caret back on screen. static const Duration _caretAnimationDuration = Duration(milliseconds: 100); static const Curve _caretAnimationCurve = Curves.fastOutSlowIn; @@ -3170,7 +3257,13 @@ class _EditableTextState extends State<_EditableText> _showCaretOnScreenScheduled = true; SchedulerBinding.instance.addPostFrameCallback((Duration _) { _showCaretOnScreenScheduled = false; - if (_currentCaretRect == null || !_scrollController.hasClients) { + // Since we are in a post frame callback, check currentContext in case + // RenderEditable has been disposed (in which case it will be null). + final _RenderEditable? renderEditable = + _editableKey.currentContext?.findRenderObject() as _RenderEditable?; + if (renderEditable == null || + !(renderEditable.selection?.isValid ?? false) || + !_scrollController.hasClients) { return; } @@ -3202,8 +3295,9 @@ class _EditableTextState extends State<_EditableText> final EdgeInsets caretPadding = widget.scrollPadding.copyWith(bottom: bottomSpacing); - final RevealedOffset targetOffset = - _getOffsetToRevealCaret(_currentCaretRect!); + final Rect caretRect = + renderEditable.getLocalRectForCaret(renderEditable.selection!.extent); + final RevealedOffset targetOffset = _getOffsetToRevealCaret(caretRect); final Rect rectToReveal; final TextSelection selection = textEditingValue.selection; @@ -3379,7 +3473,6 @@ class _EditableTextState extends State<_EditableText> cause == SelectionChangedCause.drag) { bringIntoView(newSelection.extent); } - break; case TargetPlatform.linux: case TargetPlatform.windows: case TargetPlatform.fuchsia: @@ -3391,7 +3484,6 @@ class _EditableTextState extends State<_EditableText> bringIntoView(newSelection.extent); } } - break; } } @@ -3402,6 +3494,12 @@ class _EditableTextState extends State<_EditableText> widget.showCursor && _cursorBlinkOpacityController.value > 0; } + bool get _showBlinkingCursor => + _hasFocus && + _value.selection.isCollapsed && + widget.showCursor && + _tickersEnabled; + /// Whether the blinking cursor is actually visible at this precise moment /// (it's hidden half the time, since it blinks). @visibleForTesting @@ -3414,7 +3512,7 @@ class _EditableTextState extends State<_EditableText> Duration get cursorBlinkInterval => _kCursorBlinkHalfPeriod; /// The current status of the text selection handles. - // @override + // @visibleForTesting // zmtzawqlp // TextSelectionOverlay? get selectionOverlay => _selectionOverlay; @@ -3422,20 +3520,18 @@ class _EditableTextState extends State<_EditableText> int _obscureShowCharTicksPending = 0; int? _obscureLatestCharIndex; - // Indicates whether the cursor should be blinking right now (but it may - // actually not blink because it's disabled via TickerMode.of(context)). - bool _cursorActive = false; - void _startCursorBlink() { assert(!(_cursorTimer?.isActive ?? false) || !(_backingCursorBlinkOpacityController?.isAnimating ?? false)); - _cursorActive = true; + if (!widget.showCursor) { + return; + } if (!_tickersEnabled) { return; } _cursorTimer?.cancel(); _cursorBlinkOpacityController.value = 1.0; - if (_EditableText.debugDeterministicCursor) { + if (EditableText.debugDeterministicCursor) { return; } if (widget.cursorOpacityAnimates) { @@ -3481,7 +3577,6 @@ class _EditableTextState extends State<_EditableText> } void _stopCursorBlink({bool resetCharTicks = true}) { - _cursorActive = false; _cursorBlinkOpacityController.value = 0.0; _cursorTimer?.cancel(); _cursorTimer = null; @@ -3491,14 +3586,23 @@ class _EditableTextState extends State<_EditableText> } void _startOrStopCursorTimerIfNeeded() { - if (_cursorTimer == null && _hasFocus && _value.selection.isCollapsed) { - _startCursorBlink(); - } else if (_cursorActive && (!_hasFocus || !_value.selection.isCollapsed)) { + if (!_showBlinkingCursor) { _stopCursorBlink(); + } else if (_cursorTimer == null) { + _startCursorBlink(); } } void _didChangeTextEditingValue() { + if (_hasFocus && !_value.selection.isValid) { + // If this field is focused and the selection is invalid, place the cursor at + // the end. Does not rely on _handleFocusChanged because it makes selection + // handles visible on Android. + // Unregister as a listener to the text controller while making the change. + widget.controller.removeListener(_didChangeTextEditingValue); + widget.controller.selection = _adjustedSelectionWhenFocused()!; + widget.controller.addListener(_didChangeTextEditingValue); + } _updateRemoteEditingValueIfNeeded(); _startOrStopCursorTimerIfNeeded(); _updateOrDisposeSelectionOverlayIfNeeded(); @@ -3519,24 +3623,9 @@ class _EditableTextState extends State<_EditableText> if (!widget.readOnly) { _scheduleShowCaretOnScreen(withAnimation: true); } - final bool shouldSelectAll = widget.selectionEnabled && - kIsWeb && - !_isMultiline && - !_nextFocusChangeIsInternal; - if (shouldSelectAll) { - // On native web, single line tags select all when receiving - // focus. - _handleSelectionChanged( - TextSelection( - baseOffset: 0, - extentOffset: _value.text.length, - ), - null, - ); - } else if (!_value.selection.isValid) { - // Place cursor at the end if the selection is invalid when we receive focus. - _handleSelectionChanged( - TextSelection.collapsed(offset: _value.text.length), null); + final TextSelection? updatedSelection = _adjustedSelectionWhenFocused(); + if (updatedSelection != null) { + _handleSelectionChanged(updatedSelection, null); } } else { WidgetsBinding.instance.removeObserver(this); @@ -3547,6 +3636,26 @@ class _EditableTextState extends State<_EditableText> updateKeepAlive(); } + TextSelection? _adjustedSelectionWhenFocused() { + TextSelection? selection; + final bool shouldSelectAll = widget.selectionEnabled && + kIsWeb && + !_isMultiline && + !_nextFocusChangeIsInternal; + if (shouldSelectAll) { + // On native web, single line tags select all when receiving + // focus. + selection = TextSelection( + baseOffset: 0, + extentOffset: _value.text.length, + ); + } else if (!_value.selection.isValid) { + // Place cursor at the end if the selection is invalid when we receive focus. + selection = TextSelection.collapsed(offset: _value.text.length); + } + return selection; + } + void _compositeCallback(Layer layer) { // The callback can be invoked when the layer is detached. // The input connection can be closed by the platform in which case this @@ -3559,6 +3668,8 @@ class _EditableTextState extends State<_EditableText> _updateSizeAndTransform(); } + // Must be called after layout. + // See https://github.com/flutter/flutter/issues/126312 void _updateSizeAndTransform() { final Size size = renderEditable.size; final Matrix4 transform = renderEditable.getTransformTo(null); @@ -3634,8 +3745,11 @@ class _EditableTextState extends State<_EditableText> if (paintBounds.bottom <= box.top) { break; } - if (paintBounds.contains(Offset(box.left, box.top)) || - paintBounds.contains(Offset(box.right, box.bottom))) { + // Include any TextBox which intersects with the RenderEditable. + if (paintBounds.left <= box.right && + box.left <= paintBounds.right && + paintBounds.top <= box.bottom) { + // At least some part of the letter is visible within the text field. rects.add(SelectionRect( position: graphemeStart, bounds: box.toRect(), @@ -3686,11 +3800,9 @@ class _EditableTextState extends State<_EditableText> /// /// This property is typically used to notify the renderer of input gestures /// when [RenderEditable.ignorePointer] is true. + /// zmtzawqlp late final _RenderEditable renderEditable = _editableKey.currentContext!.findRenderObject()! as _RenderEditable; - // // zmtzawqlp - // late final _RenderEditable _renderEditable = - // _editableKey.currentContext!.findRenderObject()! as _RenderEditable; @override TextEditingValue get textEditingValue => _value; @@ -3750,6 +3862,7 @@ class _EditableTextState extends State<_EditableText> if (_selectionOverlay == null) { return false; } + _liveTextInputStatus?.update(); clipboardStatus.update(); _selectionOverlay!.showToolbar(); return true; @@ -3805,15 +3918,16 @@ class _EditableTextState extends State<_EditableText> 'SpellCheckConfiguration to show a toolbar with spell check ' 'suggestions', ); - - // TODO - // _selectionOverlay!.showSpellCheckSuggestionsToolbar( - // (BuildContext context) { - // return _spellCheckConfiguration.spellCheckSuggestionsToolbarBuilder!( - // context, - // this, - // ); - // }, + // zmtzawqlp + // _selectionOverlay! + // .showSpellCheckSuggestionsToolbar( + // (BuildContext context) { + // return _spellCheckConfiguration + // .spellCheckSuggestionsToolbarBuilder!( + // context, + // this, + // ); + // }, // ); return true; } @@ -4028,10 +4142,8 @@ class _EditableTextState extends State<_EditableText> // boundary, and do this instead: // final int graphemeStart = CharacterRange.at(string, extent.offset).stringBeforeLength - 1; caretOffset = math.max(0, extent.offset - 1); - break; case TextAffinity.downstream: caretOffset = extent.offset; - break; } // The line boundary range does not include some control characters // (most notably, Line Feed), in which case there's @@ -4052,7 +4164,7 @@ class _EditableTextState extends State<_EditableText> // --------------------------- Text Editing Actions --------------------------- TextBoundary _characterBoundary() => widget.obscureText - ? _CodeUnitBoundary(_value.text) + ? _CodePointBoundary(_value.text) : CharacterBoundary(_value.text); TextBoundary _nextWordBoundary() => widget.obscureText ? _documentBoundary() @@ -4143,7 +4255,7 @@ class _EditableTextState extends State<_EditableText> } /// Handles [ScrollIntent] by scrolling the [Scrollable] inside of - /// [_EditableText]. + /// [EditableText]. void _scroll(ScrollIntent intent) { if (intent.type != ScrollIncrementType.page) { return; @@ -4278,23 +4390,19 @@ class _EditableTextState extends State<_EditableText> if (kIsWeb) { widget.focusNode.unfocus(); } - break; case ui.PointerDeviceKind.mouse: case ui.PointerDeviceKind.stylus: case ui.PointerDeviceKind.invertedStylus: case ui.PointerDeviceKind.unknown: widget.focusNode.unfocus(); - break; case ui.PointerDeviceKind.trackpad: throw UnimplementedError( 'Unexpected pointer down event for trackpad'); } - break; case TargetPlatform.linux: case TargetPlatform.macOS: case TargetPlatform.windows: widget.focusNode.unfocus(); - break; } } @@ -4402,11 +4510,9 @@ class _EditableTextState extends State<_EditableText> // child: UndoHistory( // value: widget.controller, // onTriggered: (TextEditingValue value) { - // userUpdateTextEditingValue( - // value, SelectionChangedCause.keyboard); + // userUpdateTextEditingValue(value, SelectionChangedCause.keyboard); // }, - // shouldChangeUndoStack: - // (TextEditingValue? oldValue, TextEditingValue newValue) { + // shouldChangeUndoStack: (TextEditingValue? oldValue, TextEditingValue newValue) { // if (!newValue.selection.isValid) { // return false; // } @@ -4425,7 +4531,6 @@ class _EditableTextState extends State<_EditableText> // if (!widget.controller.value.composing.isCollapsed) { // return false; // } - // break; // case TargetPlatform.android: // // Gboard on Android puts non-CJK words in composing regions. Coalesce // // composing text in order to allow the saving of partial words in that @@ -4433,8 +4538,7 @@ class _EditableTextState extends State<_EditableText> // break; // } - // return oldValue.text != newValue.text || - // oldValue.composing != newValue.composing; + // return oldValue.text != newValue.text || oldValue.composing != newValue.composing; // }, // focusNode: widget.focusNode, // controller: widget.undoController, @@ -4445,8 +4549,7 @@ class _EditableTextState extends State<_EditableText> // child: Scrollable( // key: _scrollableKey, // excludeFromSemantics: true, - // axisDirection: - // _isMultiline ? AxisDirection.down : AxisDirection.right, + // axisDirection: _isMultiline ? AxisDirection.down : AxisDirection.right, // controller: _scrollController, // physics: widget.scrollPhysics, // dragStartBehavior: widget.dragStartBehavior, @@ -4454,13 +4557,11 @@ class _EditableTextState extends State<_EditableText> // // If a ScrollBehavior is not provided, only apply scrollbars when // // multiline. The overscroll indicator should not be applied in // // either case, glowing or stretching. - // scrollBehavior: widget.scrollBehavior ?? - // ScrollConfiguration.of(context).copyWith( - // scrollbars: _isMultiline, - // overscroll: false, - // ), - // viewportBuilder: - // (BuildContext context, ViewportOffset offset) { + // scrollBehavior: widget.scrollBehavior ?? ScrollConfiguration.of(context).copyWith( + // scrollbars: _isMultiline, + // overscroll: false, + // ), + // viewportBuilder: (BuildContext context, ViewportOffset offset) { // return CompositedTransformTarget( // link: _toolbarLayerLink, // child: Semantics( @@ -4483,7 +4584,7 @@ class _EditableTextState extends State<_EditableText> // value: _value, // cursorColor: _cursorColor, // backgroundCursorColor: widget.backgroundCursorColor, - // showCursor: _EditableText.debugDeterministicCursor + // showCursor: EditableText.debugDeterministicCursor // ? ValueNotifier(widget.showCursor) // : _cursorVisibilityNotifier, // forceLine: widget.forceLine, @@ -4493,27 +4594,19 @@ class _EditableTextState extends State<_EditableText> // minLines: widget.minLines, // expands: widget.expands, // strutStyle: widget.strutStyle, - // selectionColor: - // _selectionOverlay?.spellCheckToolbarIsVisible ?? - // false - // ? _spellCheckConfiguration - // .misspelledSelectionColor ?? - // widget.selectionColor - // : widget.selectionColor, - // textScaleFactor: widget.textScaleFactor ?? - // MediaQuery.textScaleFactorOf(context), + // selectionColor: _selectionOverlay?.spellCheckToolbarIsVisible ?? false + // ? _spellCheckConfiguration.misspelledSelectionColor ?? widget.selectionColor + // : widget.selectionColor, + // textScaleFactor: widget.textScaleFactor ?? MediaQuery.textScaleFactorOf(context), // textAlign: widget.textAlign, // textDirection: _textDirection, // locale: widget.locale, - // textHeightBehavior: widget.textHeightBehavior ?? - // DefaultTextHeightBehavior.maybeOf(context), + // textHeightBehavior: widget.textHeightBehavior ?? DefaultTextHeightBehavior.maybeOf(context), // textWidthBasis: widget.textWidthBasis, // obscuringCharacter: widget.obscuringCharacter, // obscureText: widget.obscureText, // offset: offset, - // onCaretChanged: _handleCaretChanged, - // rendererIgnoresPointer: - // widget.rendererIgnoresPointer, + // rendererIgnoresPointer: widget.rendererIgnoresPointer, // cursorWidth: widget.cursorWidth, // cursorHeight: widget.cursorHeight, // cursorRadius: widget.cursorRadius, @@ -4521,8 +4614,7 @@ class _EditableTextState extends State<_EditableText> // selectionHeightStyle: widget.selectionHeightStyle, // selectionWidthStyle: widget.selectionWidthStyle, // paintCursorAboveText: widget.paintCursorAboveText, - // enableInteractiveSelection: - // widget._userSelectionEnabled, + // enableInteractiveSelection: widget._userSelectionEnabled, // textSelectionDelegate: this, // devicePixelRatio: _devicePixelRatio, // promptRectRange: _currentPromptRectRange, @@ -4650,7 +4742,6 @@ class _Editable extends MultiChildRenderObjectWidget { required this.obscuringCharacter, required this.obscureText, required this.offset, - this.onCaretChanged, this.rendererIgnoresPointer = false, required this.cursorWidth, this.cursorHeight, @@ -4665,20 +4756,9 @@ class _Editable extends MultiChildRenderObjectWidget { this.promptRectRange, this.promptRectColor, required this.clipBehavior, - }) : super(children: _extractChildren(inlineSpan)); - - // Traverses the InlineSpan tree and depth-first collects the list of - // child widgets that are created in WidgetSpans. - static List _extractChildren(InlineSpan span) { - final List result = []; - span.visitChildren((InlineSpan span) { - if (span is WidgetSpan) { - result.add(span.child); - } - return true; - }); - return result; - } + }) : super( + children: + WidgetSpan.extractFromInlineSpan(inlineSpan, textScaleFactor)); final InlineSpan inlineSpan; final TextEditingValue value; @@ -4704,7 +4784,6 @@ class _Editable extends MultiChildRenderObjectWidget { final TextHeightBehavior? textHeightBehavior; final TextWidthBasis textWidthBasis; final ViewportOffset offset; - final CaretChangedHandler? onCaretChanged; final bool rendererIgnoresPointer; final double cursorWidth; final double? cursorHeight; @@ -4744,7 +4823,6 @@ class _Editable extends MultiChildRenderObjectWidget { locale: locale ?? Localizations.maybeLocaleOf(context), selection: value.selection, offset: offset, - onCaretChanged: onCaretChanged, ignorePointer: rendererIgnoresPointer, obscuringCharacter: obscuringCharacter, obscureText: obscureText, @@ -4790,7 +4868,6 @@ class _Editable extends MultiChildRenderObjectWidget { ..locale = locale ?? Localizations.maybeLocaleOf(context) ..selection = value.selection ..offset = offset - ..onCaretChanged = onCaretChanged ..ignorePointer = rendererIgnoresPointer ..textHeightBehavior = textHeightBehavior ..textWidthBasis = textWidthBasis @@ -4937,7 +5014,8 @@ class _ScribbleFocusableState extends State<_ScribbleFocusable> } final Rect intersection = calculatedBounds.intersect(rect); final HitTestResult result = HitTestResult(); - WidgetsBinding.instance.hitTest(result, intersection.center); + WidgetsBinding.instance + .hitTestInView(result, intersection.center, View.of(context).viewId); return result.path .any((HitTestEntry entry) => entry.target == renderEditable); } @@ -4962,13 +5040,8 @@ class _ScribbleFocusableState extends State<_ScribbleFocusable> class _ScribblePlaceholder extends WidgetSpan { const _ScribblePlaceholder({ required super.child, - super.alignment, - super.baseline, required this.size, - }) : assert(baseline != null || - !(identical(alignment, ui.PlaceholderAlignment.aboveBaseline) || - identical(alignment, ui.PlaceholderAlignment.belowBaseline) || - identical(alignment, ui.PlaceholderAlignment.baseline))); + }); /// The size of the span, used in place of adding a placeholder size to the [TextPainter]. final Size size; @@ -4993,23 +5066,72 @@ class _ScribblePlaceholder extends WidgetSpan { } } -/// A text boundary that uses code units as logical boundaries. +/// A text boundary that uses code points as logical boundaries. +/// +/// A code point represents a single character. This may be smaller than what is +/// represented by a user-perceived character, or grapheme. For example, a +/// single grapheme (in this case a Unicode extended grapheme cluster) like +/// "👨‍👩‍👦" consists of five code points: the man emoji, a zero +/// width joiner, the woman emoji, another zero width joiner, and the boy emoji. +/// The [String] has a length of eight because each emoji consists of two code +/// units. /// -/// This text boundary treats every character in input string as an utf-16 code -/// unit. This can be useful when handling text without any grapheme cluster, -/// e.g. password input in [_EditableText]. If you are handling text that may -/// include grapheme clusters, consider using [CharacterBoundary]. -class _CodeUnitBoundary extends TextBoundary { - const _CodeUnitBoundary(this._text); +/// Code units are the units by which Dart's String class is measured, which is +/// encoded in UTF-16. +/// +/// See also: +/// +/// * [String.runes], which deals with code points like this class. +/// * [String.characters], which deals with graphemes. +/// * [CharacterBoundary], which is a [TextBoundary] like this class, but whose +/// boundaries are graphemes instead of code points. +class _CodePointBoundary extends TextBoundary { + const _CodePointBoundary(this._text); final String _text; + // Returns true if the given position falls in the center of a surrogate pair. + bool _breaksSurrogatePair(int position) { + assert(position > 0 && position < _text.length && _text.length > 1); + return TextPainter.isHighSurrogate(_text.codeUnitAt(position - 1)) && + TextPainter.isLowSurrogate(_text.codeUnitAt(position)); + } + @override - int getLeadingTextBoundaryAt(int position) => - position.clamp(0, _text.length); // ignore_clamp_double_lint + int? getLeadingTextBoundaryAt(int position) { + if (_text.isEmpty || position < 0) { + return null; + } + if (position == 0) { + return 0; + } + if (position >= _text.length) { + return _text.length; + } + if (_text.length <= 1) { + return position; + } + + return _breaksSurrogatePair(position) ? position - 1 : position; + } + @override - int getTrailingTextBoundaryAt(int position) => - (position + 1).clamp(0, _text.length); // ignore_clamp_double_lint + int? getTrailingTextBoundaryAt(int position) { + if (_text.isEmpty || position >= _text.length) { + return null; + } + if (position < 0) { + return 0; + } + if (position == _text.length - 1) { + return _text.length; + } + if (_text.length <= 1) { + return position; + } + + return _breaksSurrogatePair(position + 1) ? position + 2 : position + 1; + } } // ------------------------------- Text Actions ------------------------------- @@ -5017,6 +5139,7 @@ class _DeleteTextAction extends ContextAction { _DeleteTextAction(this.state, this.getTextBoundary, this._applyTextBoundary); + /// zmtzawqlp final _EditableTextState state; final TextBoundary Function() getTextBoundary; final _ApplyTextBoundary _applyTextBoundary; @@ -5080,6 +5203,7 @@ class _UpdateTextSelectionAction this.extentAtIndex = false, }); + /// zmtzawqlp final _EditableTextState state; final bool ignoreNonCollapsedSelection; final bool isExpand; @@ -5187,6 +5311,7 @@ class _UpdateTextSelectionVerticallyAction< T extends DirectionalCaretMovementIntent> extends ContextAction { _UpdateTextSelectionVerticallyAction(this.state); + /// zmtzawqlp final _EditableTextState state; VerticalCaretMovementRun? _verticalMovementRun; @@ -5264,6 +5389,7 @@ class _UpdateTextSelectionVerticallyAction< class _SelectAllAction extends ContextAction { _SelectAllAction(this.state); + /// zmtzawqlp final _EditableTextState state; @override @@ -5285,6 +5411,7 @@ class _SelectAllAction extends ContextAction { class _CopySelectionAction extends ContextAction { _CopySelectionAction(this.state); + /// zmtzawqlp final _EditableTextState state; @override diff --git a/lib/src/official/widgets/spell_check.dart b/lib/src/official/widgets/spell_check.dart index ade95af..fb9b1d3 100644 --- a/lib/src/official/widgets/spell_check.dart +++ b/lib/src/official/widgets/spell_check.dart @@ -1,5 +1,6 @@ part of 'package:extended_text_field/src/extended/widgets/text_field.dart'; +/// [SpellCheckConfiguration] /// Controls how spell check is performed for text input. /// /// This configuration determines the [SpellCheckService] used to fetch the diff --git a/lib/src/official/widgets/text_field.dart b/lib/src/official/widgets/text_field.dart index 6b60102..67379e0 100644 --- a/lib/src/official/widgets/text_field.dart +++ b/lib/src/official/widgets/text_field.dart @@ -1,6 +1,6 @@ part of 'package:extended_text_field/src/extended/widgets/text_field.dart'; -/// [_TextField] +/// [TextField] class _TextFieldSelectionGestureDetectorBuilder extends _TextSelectionGestureDetectorBuilder { _TextFieldSelectionGestureDetectorBuilder({ @@ -73,7 +73,7 @@ class _TextFieldSelectionGestureDetectorBuilder /// If [decoration] is non-null (which is the default), the text field requires /// one of its ancestors to be a [Material] widget. /// -/// To integrate the [_TextField] into a [Form] with other [FormField] widgets, +/// To integrate the [TextField] into a [Form] with other [FormField] widgets, /// consider using [TextFormField]. /// /// {@template flutter.material.textfield.wantKeepAlive} @@ -90,7 +90,7 @@ class _TextFieldSelectionGestureDetectorBuilder /// ## Obscured Input /// /// {@tool dartpad} -/// This example shows how to create a [_TextField] that will obscure input. The +/// This example shows how to create a [TextField] that will obscure input. The /// [InputDecoration] surrounds the field in a border using [OutlineInputBorder] /// and adds a label. /// @@ -142,7 +142,7 @@ class _TextFieldSelectionGestureDetectorBuilder /// * [InputDecorator], which shows the labels and other visual elements that /// surround the actual text editing widget. /// * [EditableText], which is the raw text editing control at the heart of a -/// [_TextField]. The [EditableText] widget is rarely used directly unless +/// [TextField]. The [EditableText] widget is rarely used directly unless /// you are implementing an entirely different design language, such as /// Cupertino. /// * @@ -170,7 +170,7 @@ class _TextField extends StatefulWidget { /// field showing how many characters have been entered. If the value is /// set to a positive integer it will also display the maximum allowed /// number of characters to be entered. If the value is set to - /// [_TextField.noMaxLength] then only the current length is displayed. + /// [TextField.noMaxLength] then only the current length is displayed. /// /// After [maxLength] characters have been input, additional input /// is ignored, unless [maxLengthEnforcement] is set to @@ -463,7 +463,7 @@ class _TextField extends StatefulWidget { /// If set, a character counter will be displayed below the /// 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 [_TextField.noMaxLength] then only the current character count is displayed. + /// to [TextField.noMaxLength] then only the current character count is displayed. /// /// After [maxLength] characters have been input, additional input /// is ignored, unless [maxLengthEnforcement] is set to @@ -472,9 +472,9 @@ class _TextField extends StatefulWidget { /// The text field enforces the length with a [LengthLimitingTextInputFormatter], /// which is evaluated after the supplied [inputFormatters], if any. /// - /// This value must be either null, [_TextField.noMaxLength], or greater than 0. + /// This value must be either null, [TextField.noMaxLength], or greater than 0. /// If null (the default) then there is no limit to the number of characters - /// that can be entered. If set to [_TextField.noMaxLength], then no limit will + /// that can be entered. If set to [TextField.noMaxLength], then no limit will /// be enforced, but the number of characters entered will still be displayed. /// /// Whitespace characters (e.g. newline, space, tab) are included in the @@ -612,7 +612,7 @@ class _TextField extends StatefulWidget { /// /// {@tool dartpad} /// This example shows how to use a `TextFieldTapRegion` to wrap a set of - /// "spinner" buttons that increment and decrement a value in the [_TextField] + /// "spinner" buttons that increment and decrement a value in the [TextField] /// without causing the text field to lose keyboard focus. /// /// This example includes a generic `SpinnerField` class that you can copy @@ -639,7 +639,7 @@ class _TextField extends StatefulWidget { /// /// If this property is null, [MaterialStateMouseCursor.textable] will be used. /// - /// The [mouseCursor] is the only property of [_TextField] that controls the + /// The [mouseCursor] is the only property of [TextField] that controls the /// appearance of the mouse pointer. All other properties related to "cursor" /// stand for the text cursor, which is usually a blinking vertical line at /// the editing position. @@ -748,7 +748,7 @@ class _TextField extends StatefulWidget { /// {@macro flutter.widgets.EditableText.spellCheckConfiguration} /// - /// If [_SpellCheckConfiguration.misspelledTextStyle] is not specified in this + /// If [SpellCheckConfiguration.misspelledTextStyle] is not specified in this /// configuration, then [materialMisspelledTextStyle] is used by default. final SpellCheckConfiguration? spellCheckConfiguration; @@ -759,22 +759,23 @@ class _TextField extends StatefulWidget { /// mark misspelled words with. /// * [CupertinoTextField.cupertinoMisspelledTextStyle], the style configured /// to mark misspelled words with in the Cupertino style. + // ignore: unused_field static const TextStyle materialMisspelledTextStyle = TextStyle( decoration: TextDecoration.underline, decorationColor: Colors.red, decorationStyle: TextDecorationStyle.wavy, ); - /// Default builder for [_TextField]'s spell check suggestions toolbar. + /// Default builder for [TextField]'s spell check suggestions toolbar. /// /// On Apple platforms, builds an iOS-style toolbar. Everywhere else, builds /// an Android-style toolbar. /// /// See also: /// * [spellCheckConfiguration], where this is typically specified for - /// [_TextField]. - /// * [_SpellCheckConfiguration.spellCheckSuggestionsToolbarBuilder], the - /// parameter for which this is the default value for [_TextField]. + /// [TextField]. + /// * [SpellCheckConfiguration.spellCheckSuggestionsToolbarBuilder], the + /// parameter for which this is the default value for [TextField]. /// * [CupertinoTextField.defaultSpellCheckSuggestionsToolbarBuilder], which /// is like this but specifies the default for [CupertinoTextField]. @visibleForTesting @@ -798,7 +799,7 @@ class _TextField extends StatefulWidget { } } - /// Returns a new [_SpellCheckConfiguration] where the given configuration has + /// Returns a new [SpellCheckConfiguration] where the given configuration has /// had any missing values replaced with their defaults for the Android /// platform. static _SpellCheckConfiguration inferAndroidSpellCheckConfiguration( @@ -1153,6 +1154,7 @@ class _TextFieldState extends State<_TextField> super.dispose(); } + /// zmtzawqlp _EditableTextState? get _editableText => editableTextKey.currentState; void _requestKeyboard() { @@ -1338,7 +1340,6 @@ class _TextFieldState extends State<_TextField> CupertinoTextField.inferIOSSpellCheckConfiguration( widget.spellCheckConfiguration, ); - break; case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.linux: @@ -1346,7 +1347,6 @@ class _TextFieldState extends State<_TextField> spellCheckConfiguration = TextField.inferAndroidSpellCheckConfiguration( widget.spellCheckConfiguration, ); - break; } TextSelectionControls? textSelectionControls = widget.selectionControls; @@ -1401,7 +1401,7 @@ class _TextFieldState extends State<_TextField> _effectiveFocusNode.requestFocus(); } }; - break; + case TargetPlatform.android: case TargetPlatform.fuchsia: forcePressEnabled = false; @@ -1415,7 +1415,7 @@ class _TextFieldState extends State<_TextField> theme.colorScheme.primary; selectionColor = selectionStyle.selectionColor ?? theme.colorScheme.primary.withOpacity(0.40); - break; + case TargetPlatform.linux: forcePressEnabled = false; textSelectionControls ??= desktopTextSelectionHandleControls; @@ -1428,7 +1428,7 @@ class _TextFieldState extends State<_TextField> theme.colorScheme.primary; selectionColor = selectionStyle.selectionColor ?? theme.colorScheme.primary.withOpacity(0.40); - break; + case TargetPlatform.windows: forcePressEnabled = false; textSelectionControls ??= desktopTextSelectionHandleControls; @@ -1448,7 +1448,6 @@ class _TextFieldState extends State<_TextField> _effectiveFocusNode.requestFocus(); } }; - break; } Widget child = RepaintBoundary( @@ -1618,8 +1617,6 @@ TextStyle _m2CounterErrorStyle(BuildContext context) => Theme.of(context) // Design token database by the script: // dev/tools/gen_defaults/bin/gen_defaults.dart. -// Token database version: v0_162 - TextStyle? _m3StateInputStyle(BuildContext context) => MaterialStateTextStyle.resolveWith((Set states) { if (states.contains(MaterialState.disabled)) { diff --git a/lib/src/official/widgets/text_selection.dart b/lib/src/official/widgets/text_selection.dart index 6456a25..86f09a2 100644 --- a/lib/src/official/widgets/text_selection.dart +++ b/lib/src/official/widgets/text_selection.dart @@ -1,11 +1,12 @@ part of 'package:extended_text_field/src/extended/widgets/text_field.dart'; +/// [TextSelectionOverlay] /// An object that manages a pair of text selection handles for a /// [RenderEditable]. /// -/// This class is a wrapper of [_SelectionOverlay] to provide APIs specific for +/// This class is a wrapper of [SelectionOverlay] to provide APIs specific for /// [RenderEditable]s. To manage selection handles for custom widgets, use -/// [_SelectionOverlay] instead. +/// [SelectionOverlay] instead. class _TextSelectionOverlay { /// Creates an object that manages overlay entries for selection handles. /// @@ -72,15 +73,10 @@ class _TextSelectionOverlay { /// {@endtemplate} final BuildContext context; - /// Controls the fade-in and fade-out animations for the toolbar and handles. - @Deprecated('Use `SelectionOverlay.fadeDuration` instead. ' - 'This feature was deprecated after v2.12.0-4.1.pre.') - static const Duration fadeDuration = _SelectionOverlay.fadeDuration; - // TODO(mpcomplete): what if the renderObject is removed or replaced, or // moves? Not sure what cases I need to handle, or how to handle them. /// The editable line in which the selected text is being displayed. - // zmtzawqlp + /// zmtzawqlp final _RenderEditable renderObject; /// {@macro flutter.widgets.SelectionOverlay.selectionControls} @@ -89,6 +85,7 @@ class _TextSelectionOverlay { /// {@macro flutter.widgets.SelectionOverlay.selectionDelegate} final TextSelectionDelegate selectionDelegate; + /// zmtzawqlp late final _SelectionOverlay _selectionOverlay; /// {@macro flutter.widgets.EditableText.contextMenuBuilder} @@ -271,15 +268,13 @@ class _TextSelectionOverlay { bool get handlesAreVisible => _selectionOverlay._handles != null && handlesVisible; - /// Whether the toolbar is currently visible. - /// - /// Includes both the text selection toolbar and the spell check menu. + /// {@macro flutter.widgets.SelectionOverlay.toolbarIsVisible} /// /// See also: /// /// * [spellCheckToolbarIsVisible], which is only whether the spell check menu /// specifically is visible. - bool get toolbarIsVisible => _selectionOverlay._toolbarIsVisible; + bool get toolbarIsVisible => _selectionOverlay.toolbarIsVisible; /// Whether the magnifier is currently visible. bool get magnifierIsVisible => _selectionOverlay._magnifierController.shown; @@ -480,7 +475,6 @@ class _TextSelectionOverlay { if (position.offset <= _selection.start) { return; // Don't allow order swapping. } - break; case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.linux: @@ -580,7 +574,6 @@ class _TextSelectionOverlay { if (newSelection.extentOffset >= _selection.end) { return; // Don't allow order swapping. } - break; case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.linux: @@ -710,19 +703,24 @@ class _SelectionOverlay { /// [MagnifierController.show] and [MagnifierController.hide] should not be called directly, except /// from inside [showMagnifier] and [hideMagnifier]. If it is desired to show or hide the magnifier, /// call [showMagnifier] or [hideMagnifier]. This is because the magnifier needs to orchestrate - /// with other properties in [_SelectionOverlay]. + /// with other properties in [SelectionOverlay]. final MagnifierController _magnifierController = MagnifierController(); /// {@macro flutter.widgets.magnifier.TextMagnifierConfiguration.intro} /// /// {@macro flutter.widgets.magnifier.intro} /// - /// By default, [_SelectionOverlay]'s [TextMagnifierConfiguration] is disabled. + /// By default, [SelectionOverlay]'s [TextMagnifierConfiguration] is disabled. /// /// {@macro flutter.widgets.magnifier.TextMagnifierConfiguration.details} final TextMagnifierConfiguration magnifierConfiguration; - bool get _toolbarIsVisible { + /// {@template flutter.widgets.SelectionOverlay.toolbarIsVisible} + /// Whether the toolbar is currently visible. + /// + /// Includes both the text selection toolbar and the spell check menu. + /// {@endtemplate} + bool get toolbarIsVisible { return selectionControls is TextSelectionHandleControls ? _contextMenuController.isShown || _spellCheckToolbarController.isShown : _toolbar != null || _spellCheckToolbarController.isShown; @@ -739,7 +737,7 @@ class _SelectionOverlay { /// [MagnifierController.shown]. /// {@endtemplate} void showMagnifier(MagnifierInfo initialMagnifierInfo) { - if (_toolbarIsVisible) { + if (toolbarIsVisible) { hideToolbar(); } @@ -826,10 +824,26 @@ class _SelectionOverlay { void _handleStartHandleDragStart(DragStartDetails details) { assert(!_isDraggingStartHandle); + // Calling OverlayEntry.remove may not happen until the following frame, so + // it's possible for the handles to receive a gesture after calling remove. + if (_handles == null) { + _isDraggingStartHandle = false; + return; + } _isDraggingStartHandle = details.kind == PointerDeviceKind.touch; onStartHandleDragStart?.call(details); } + void _handleStartHandleDragUpdate(DragUpdateDetails details) { + // Calling OverlayEntry.remove may not happen until the following frame, so + // it's possible for the handles to receive a gesture after calling remove. + if (_handles == null) { + _isDraggingStartHandle = false; + return; + } + onStartHandleDragUpdate?.call(details); + } + /// Called when the users drag the start selection handles to new locations. final ValueChanged? onStartHandleDragUpdate; @@ -839,6 +853,11 @@ class _SelectionOverlay { void _handleStartHandleDragEnd(DragEndDetails details) { _isDraggingStartHandle = false; + // Calling OverlayEntry.remove may not happen until the following frame, so + // it's possible for the handles to receive a gesture after calling remove. + if (_handles == null) { + return; + } onStartHandleDragEnd?.call(details); } @@ -885,10 +904,26 @@ class _SelectionOverlay { void _handleEndHandleDragStart(DragStartDetails details) { assert(!_isDraggingEndHandle); + // Calling OverlayEntry.remove may not happen until the following frame, so + // it's possible for the handles to receive a gesture after calling remove. + if (_handles == null) { + _isDraggingEndHandle = false; + return; + } _isDraggingEndHandle = details.kind == PointerDeviceKind.touch; onEndHandleDragStart?.call(details); } + void _handleEndHandleDragUpdate(DragUpdateDetails details) { + // Calling OverlayEntry.remove may not happen until the following frame, so + // it's possible for the handles to receive a gesture after calling remove. + if (_handles == null) { + _isDraggingEndHandle = false; + return; + } + onEndHandleDragUpdate?.call(details); + } + /// Called when the users drag the end selection handles to new locations. final ValueChanged? onEndHandleDragUpdate; @@ -898,6 +933,11 @@ class _SelectionOverlay { void _handleEndHandleDragEnd(DragEndDetails details) { _isDraggingEndHandle = false; + // Calling OverlayEntry.remove may not happen until the following frame, so + // it's possible for the handles to receive a gesture after calling remove. + if (_handles == null) { + return; + } onEndHandleDragEnd?.call(details); } @@ -919,7 +959,6 @@ class _SelectionOverlay { switch (defaultTargetPlatform) { case TargetPlatform.android: HapticFeedback.selectionClick(); - break; case TargetPlatform.fuchsia: case TargetPlatform.iOS: case TargetPlatform.linux: @@ -1025,9 +1064,6 @@ class _SelectionOverlay { markNeedsBuild(); } - /// Controls the fade-in and fade-out animations for the toolbar and handles. - static const Duration fadeDuration = Duration(milliseconds: 150); - /// A pair of handles. If this is non-null, there are always 2, though the /// second is hidden when the selection is collapsed. List? _handles; @@ -1218,7 +1254,7 @@ class _SelectionOverlay { handleLayerLink: startHandleLayerLink, onSelectionHandleTapped: onSelectionHandleTapped, onSelectionHandleDragStart: _handleStartHandleDragStart, - onSelectionHandleDragUpdate: onStartHandleDragUpdate, + onSelectionHandleDragUpdate: _handleStartHandleDragUpdate, onSelectionHandleDragEnd: _handleStartHandleDragEnd, selectionControls: selectionControls, visibility: startHandlesVisible, @@ -1246,7 +1282,7 @@ class _SelectionOverlay { handleLayerLink: endHandleLayerLink, onSelectionHandleTapped: onSelectionHandleTapped, onSelectionHandleDragStart: _handleEndHandleDragStart, - onSelectionHandleDragUpdate: onEndHandleDragUpdate, + onSelectionHandleDragUpdate: _handleEndHandleDragUpdate, onSelectionHandleDragEnd: _handleEndHandleDragEnd, selectionControls: selectionControls, visibility: endHandlesVisible, @@ -1368,7 +1404,7 @@ class _SelectionToolbarWrapperState extends State<_SelectionToolbarWrapper> super.initState(); _controller = AnimationController( - duration: _SelectionOverlay.fadeDuration, vsync: this); + duration: SelectionOverlay.fadeDuration, vsync: this); _toolbarVisibilityChanged(); widget.visibility?.addListener(_toolbarVisibilityChanged); @@ -1461,7 +1497,7 @@ class _SelectionHandleOverlayState extends State<_SelectionHandleOverlay> super.initState(); _controller = AnimationController( - duration: _SelectionOverlay.fadeDuration, vsync: this); + duration: SelectionOverlay.fadeDuration, vsync: this); _handleVisibilityChanged(); widget.visibility?.addListener(_handleVisibilityChanged); @@ -1578,7 +1614,7 @@ class _SelectionHandleOverlayState extends State<_SelectionHandleOverlay> /// The class implements sensible defaults for many user interactions /// with an [EditableText] (see the documentation of the various gesture handler /// methods, e.g. [onTapDown], [onForcePressStart], etc.). Subclasses of -/// [_TextSelectionGestureDetectorBuilder] can change the behavior performed in +/// [TextSelectionGestureDetectorBuilder] can change the behavior performed in /// responds to these gesture events by overriding the corresponding handler /// methods of this class. /// @@ -1599,7 +1635,7 @@ class _TextSelectionGestureDetectorBuilder { required this.delegate, }); - /// The delegate for this [_TextSelectionGestureDetectorBuilder]. + /// The delegate for this [TextSelectionGestureDetectorBuilder]. /// /// The delegate provides the builder with information about what actions can /// currently be performed on the text field. Based on this, the builder adds @@ -1607,6 +1643,33 @@ class _TextSelectionGestureDetectorBuilder { @protected final _TextSelectionGestureDetectorBuilderDelegate delegate; + // Shows the magnifier on supported platforms at the given offset, currently + // only Android and iOS. + void _showMagnifierIfSupportedByPlatform(Offset positionToShow) { + switch (defaultTargetPlatform) { + case TargetPlatform.android: + case TargetPlatform.iOS: + editableText.showMagnifier(positionToShow); + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.macOS: + case TargetPlatform.windows: + } + } + + // Hides the magnifier on supported platforms, currently only Android and iOS. + void _hideMagnifierIfSupportedByPlatform() { + switch (defaultTargetPlatform) { + case TargetPlatform.android: + case TargetPlatform.iOS: + editableText.hideMagnifier(); + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.macOS: + case TargetPlatform.windows: + } + } + /// Returns true if lastSecondaryTapDownPosition was on selection. bool get _lastSecondaryTapWasOnSelection { assert(renderEditable.lastSecondaryTapDownPosition != null); @@ -1735,6 +1798,8 @@ class _TextSelectionGestureDetectorBuilder { /// The [State] of the [EditableText] for which the builder will provide a /// [TextSelectionGestureDetector]. @protected + + /// zmtzawqlp _EditableTextState get editableText => delegate.editableTextKey.currentState!; /// The [RenderObject] of the [EditableText] for which the builder will @@ -1822,10 +1887,13 @@ class _TextSelectionGestureDetectorBuilder { switch (defaultTargetPlatform) { case TargetPlatform.android: case TargetPlatform.fuchsia: + // On mobile platforms the selection is set on tap up. + editableText.hideToolbar(false); case TargetPlatform.iOS: // On mobile platforms the selection is set on tap up. break; case TargetPlatform.macOS: + editableText.hideToolbar(); // On macOS, a shift-tapped unfocused field expands from 0, not from the // previous selection. if (isShiftPressedValid) { @@ -1844,15 +1912,14 @@ class _TextSelectionGestureDetectorBuilder { // then the selection moves to the closest word edge, instead of a // precise position. renderEditable.selectPosition(cause: SelectionChangedCause.tap); - break; case TargetPlatform.linux: case TargetPlatform.windows: + editableText.hideToolbar(); if (isShiftPressedValid) { _extendSelection(details.globalPosition, SelectionChangedCause.tap); return; } renderEditable.selectPosition(cause: SelectionChangedCause.tap); - break; } } @@ -1923,26 +1990,21 @@ class _TextSelectionGestureDetectorBuilder { case TargetPlatform.linux: case TargetPlatform.macOS: case TargetPlatform.windows: - editableText.hideToolbar(); break; // On desktop platforms the selection is set on tap down. case TargetPlatform.android: - editableText.hideToolbar(); if (isShiftPressedValid) { _extendSelection(details.globalPosition, SelectionChangedCause.tap); return; } renderEditable.selectPosition(cause: SelectionChangedCause.tap); editableText.showSpellCheckSuggestionsToolbar(); - break; case TargetPlatform.fuchsia: - editableText.hideToolbar(); if (isShiftPressedValid) { _extendSelection(details.globalPosition, SelectionChangedCause.tap); return; } renderEditable.selectPosition(cause: SelectionChangedCause.tap); - break; case TargetPlatform.iOS: if (isShiftPressedValid) { // On iOS, a shift-tapped unfocused field expands from 0, not from @@ -1967,7 +2029,6 @@ 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); - break; case PointerDeviceKind.touch: case PointerDeviceKind.unknown: // If the word that was tapped is misspelled, select the word and show the spell check suggestions @@ -2063,26 +2124,14 @@ class _TextSelectionGestureDetectorBuilder { cause: SelectionChangedCause.longPress, ); } - break; case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.windows: renderEditable.selectWord(cause: SelectionChangedCause.longPress); - break; } - switch (defaultTargetPlatform) { - case TargetPlatform.android: - case TargetPlatform.iOS: - editableText.showMagnifier(details.globalPosition); - break; - case TargetPlatform.fuchsia: - case TargetPlatform.linux: - case TargetPlatform.macOS: - case TargetPlatform.windows: - break; - } + _showMagnifierIfSupportedByPlatform(details.globalPosition); _dragStartViewportOffset = renderEditable.offset.pixels; _dragStartScrollOffset = _scrollPosition; @@ -2129,7 +2178,6 @@ class _TextSelectionGestureDetectorBuilder { cause: SelectionChangedCause.longPress, ); } - break; case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.linux: @@ -2144,17 +2192,7 @@ class _TextSelectionGestureDetectorBuilder { ); } - switch (defaultTargetPlatform) { - case TargetPlatform.android: - case TargetPlatform.iOS: - editableText.showMagnifier(details.globalPosition); - break; - case TargetPlatform.fuchsia: - case TargetPlatform.linux: - case TargetPlatform.macOS: - case TargetPlatform.windows: - break; - } + _showMagnifierIfSupportedByPlatform(details.globalPosition); } } @@ -2168,17 +2206,7 @@ class _TextSelectionGestureDetectorBuilder { /// callback. @protected void onSingleLongTapEnd(LongPressEndDetails details) { - switch (defaultTargetPlatform) { - case TargetPlatform.android: - case TargetPlatform.iOS: - editableText.hideMagnifier(); - break; - case TargetPlatform.fuchsia: - case TargetPlatform.linux: - case TargetPlatform.macOS: - case TargetPlatform.windows: - break; - } + _hideMagnifierIfSupportedByPlatform(); if (shouldShowSelectionToolbar) { editableText.showToolbar(); } @@ -2205,7 +2233,6 @@ class _TextSelectionGestureDetectorBuilder { editableText.hideToolbar(); editableText.showToolbar(); } - break; case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.linux: @@ -2348,7 +2375,6 @@ class _TextSelectionGestureDetectorBuilder { case TargetPlatform.windows: _selectParagraphsInRange( from: details.globalPosition, cause: SelectionChangedCause.tap); - break; case TargetPlatform.linux: _selectLinesInRange( from: details.globalPosition, cause: SelectionChangedCause.tap); @@ -2380,6 +2406,8 @@ class _TextSelectionGestureDetectorBuilder { _dragStartSelection = renderEditable.selection; _dragStartScrollOffset = _scrollPosition; _dragStartViewportOffset = renderEditable.offset.pixels; + _dragBeganOnPreviousSelection = + _positionOnSelection(details.globalPosition, _dragStartSelection); if (_TextSelectionGestureDetectorState._getEffectiveConsecutiveTapCount( details.consecutiveTapCount) > @@ -2397,7 +2425,6 @@ class _TextSelectionGestureDetectorBuilder { case TargetPlatform.iOS: case TargetPlatform.macOS: _expandSelection(details.globalPosition, SelectionChangedCause.drag); - break; case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.linux: @@ -2407,18 +2434,40 @@ class _TextSelectionGestureDetectorBuilder { } else { switch (defaultTargetPlatform) { case TargetPlatform.iOS: - case TargetPlatform.android: - case TargetPlatform.fuchsia: switch (details.kind) { case PointerDeviceKind.mouse: case PointerDeviceKind.trackpad: + renderEditable.selectPositionAt( + from: details.globalPosition, + cause: SelectionChangedCause.drag, + ); case PointerDeviceKind.stylus: case PointerDeviceKind.invertedStylus: + case PointerDeviceKind.touch: + case PointerDeviceKind.unknown: + // For iOS platforms, a touch drag does not initiate unless the + // editable has focus and the drag began on the previous selection. + assert(_dragBeganOnPreviousSelection != null); + if (renderEditable.hasFocus && _dragBeganOnPreviousSelection!) { + renderEditable.selectPositionAt( + from: details.globalPosition, + cause: SelectionChangedCause.drag, + ); + _showMagnifierIfSupportedByPlatform(details.globalPosition); + } + case null: + } + case TargetPlatform.android: + case TargetPlatform.fuchsia: + switch (details.kind) { + case PointerDeviceKind.mouse: + case PointerDeviceKind.trackpad: renderEditable.selectPositionAt( from: details.globalPosition, cause: SelectionChangedCause.drag, ); - break; + case PointerDeviceKind.stylus: + case PointerDeviceKind.invertedStylus: case PointerDeviceKind.touch: case PointerDeviceKind.unknown: // For Android, Fucshia, and iOS platforms, a touch drag diff --git a/pubspec.yaml b/pubspec.yaml index 2cb3f91..e3a86e5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,11 +1,11 @@ name: extended_text_field description: Extended official text field to build special text like inline image, @somebody, custom background etc quickly.It also support to build custom seleciton toolbar and handles. -version: 12.0.1 +version: 12.1.0 homepage: https://github.com/fluttercandies/extended_text_field environment: - sdk: '>=2.17.0 <4.0.0' - flutter: ">=3.10.0" + sdk: '>=3.0.0 <4.0.0' + flutter: ">=3.13.0" dependencies: extended_text_library: ^11.0.2