diff --git a/packages/flutter/lib/src/material/selection_area.dart b/packages/flutter/lib/src/material/selection_area.dart index 5d8b1a3828..5d305589c5 100644 --- a/packages/flutter/lib/src/material/selection_area.dart +++ b/packages/flutter/lib/src/material/selection_area.dart @@ -104,12 +104,16 @@ class SelectionArea extends StatefulWidget { } @override - State createState() => _SelectionAreaState(); + State createState() => SelectionAreaState(); } -class _SelectionAreaState extends State { +/// State for a [SelectionArea]. +class SelectionAreaState extends State { FocusNode get _effectiveFocusNode => widget.focusNode ?? (_internalNode ??= FocusNode()); FocusNode? _internalNode; + final GlobalKey _selectableRegionKey = GlobalKey(); + /// The [State] of the [SelectableRegion] for which this [SelectionArea] wraps. + SelectableRegionState get selectableRegion => _selectableRegionKey.currentState!; @override void dispose() { @@ -127,6 +131,7 @@ class _SelectionAreaState extends State { TargetPlatform.macOS => cupertinoDesktopTextSelectionHandleControls, }; return SelectableRegion( + key: _selectableRegionKey, selectionControls: controls, focusNode: _effectiveFocusNode, contextMenuBuilder: widget.contextMenuBuilder, diff --git a/packages/flutter/lib/src/widgets/selectable_region.dart b/packages/flutter/lib/src/widgets/selectable_region.dart index 295813662d..be87330454 100644 --- a/packages/flutter/lib/src/widgets/selectable_region.dart +++ b/packages/flutter/lib/src/widgets/selectable_region.dart @@ -450,7 +450,7 @@ class SelectableRegionState extends State with TextSelectionDe // the new window causing the Flutter application to go inactive. In this // case we want to retain the selection so it remains when we return to // the Flutter application. - _clearSelection(); + clearSelection(); } } if (kIsWeb) { @@ -559,7 +559,7 @@ class SelectableRegionState extends State with TextSelectionDe ..onDragStart = _handleMouseDragStart ..onDragUpdate = _handleMouseDragUpdate ..onDragEnd = _handleMouseDragEnd - ..onCancel = _clearSelection + ..onCancel = clearSelection ..dragStartBehavior = DragStartBehavior.down; }, ); @@ -607,7 +607,7 @@ class SelectableRegionState extends State with TextSelectionDe ..onDragStart = _handleMouseDragStart ..onDragUpdate = _handleMouseDragUpdate ..onDragEnd = _handleMouseDragEnd - ..onCancel = _clearSelection + ..onCancel = clearSelection ..dragStartBehavior = DragStartBehavior.down; }, ); @@ -1228,7 +1228,7 @@ class SelectableRegionState extends State with TextSelectionDe /// See also: /// * [_selectStartTo], which sets or updates selection start edge. /// * [_finalizeSelection], which stops the `continuous` updates. - /// * [_clearSelection], which clears the ongoing selection. + /// * [clearSelection], which clears the ongoing selection. /// * [_selectWordAt], which selects a whole word at the location. /// * [_selectParagraphAt], which selects an entire paragraph at the location. /// * [_collapseSelectionAt], which collapses the selection at the location. @@ -1269,7 +1269,7 @@ class SelectableRegionState extends State with TextSelectionDe /// See also: /// * [_selectEndTo], which sets or updates selection end edge. /// * [_finalizeSelection], which stops the `continuous` updates. - /// * [_clearSelection], which clears the ongoing selection. + /// * [clearSelection], which clears the ongoing selection. /// * [_selectWordAt], which selects a whole word at the location. /// * [_selectParagraphAt], which selects an entire paragraph at the location. /// * [_collapseSelectionAt], which collapses the selection at the location. @@ -1293,7 +1293,7 @@ class SelectableRegionState extends State with TextSelectionDe /// * [_selectStartTo], which sets or updates selection start edge. /// * [_selectEndTo], which sets or updates selection end edge. /// * [_finalizeSelection], which stops the `continuous` updates. - /// * [_clearSelection], which clears the ongoing selection. + /// * [clearSelection], which clears the ongoing selection. /// * [_selectWordAt], which selects a whole word at the location. /// * [_selectParagraphAt], which selects an entire paragraph at the location. /// * [selectAll], which selects the entire content. @@ -1307,7 +1307,7 @@ class SelectableRegionState extends State with TextSelectionDe /// The `offset` is in global coordinates. /// /// If the whole word is already in the current selection, selection won't - /// change. One call [_clearSelection] first if the selection needs to be + /// change. One call [clearSelection] first if the selection needs to be /// updated even if the word is already covered by the current selection. /// /// One can also use [_selectEndTo] or [_selectStartTo] to adjust the selection @@ -1317,7 +1317,7 @@ class SelectableRegionState extends State with TextSelectionDe /// * [_selectStartTo], which sets or updates selection start edge. /// * [_selectEndTo], which sets or updates selection end edge. /// * [_finalizeSelection], which stops the `continuous` updates. - /// * [_clearSelection], which clears the ongoing selection. + /// * [clearSelection], which clears the ongoing selection. /// * [_collapseSelectionAt], which collapses the selection at the location. /// * [_selectParagraphAt], which selects an entire paragraph at the location. /// * [selectAll], which selects the entire content. @@ -1332,7 +1332,7 @@ class SelectableRegionState extends State with TextSelectionDe /// The `offset` is in global coordinates. /// /// If the paragraph is already in the current selection, selection won't - /// change. One call [_clearSelection] first if the selection needs to be + /// change. One call [clearSelection] first if the selection needs to be /// updated even if the paragraph is already covered by the current selection. /// /// One can also use [_selectEndTo] or [_selectStartTo] to adjust the selection @@ -1342,7 +1342,7 @@ class SelectableRegionState extends State with TextSelectionDe /// * [_selectStartTo], which sets or updates selection start edge. /// * [_selectEndTo], which sets or updates selection end edge. /// * [_finalizeSelection], which stops the `continuous` updates. - /// * [_clearSelection], which clear the ongoing selection. + /// * [clearSelection], which clear the ongoing selection. /// * [_selectWordAt], which selects a whole word at the location. /// * [selectAll], which selects the entire content. void _selectParagraphAt({required Offset offset}) { @@ -1353,7 +1353,7 @@ class SelectableRegionState extends State with TextSelectionDe /// Stops any ongoing selection updates. /// - /// This method is different from [_clearSelection] that it does not remove + /// This method is different from [clearSelection] that it does not remove /// the current selection. It only stops the continuous updates. /// /// A continuous update can happen as result of calling [_selectStartTo] or @@ -1365,8 +1365,8 @@ class SelectableRegionState extends State with TextSelectionDe _stopSelectionStartEdgeUpdate(); } - /// Removes the ongoing selection. - void _clearSelection() { + /// Removes the ongoing selection for this [SelectableRegion]. + void clearSelection() { _finalizeSelection(); _directionalHorizontalBaseline = null; _adjustingSelectionEnd = null; @@ -1496,7 +1496,7 @@ class SelectableRegionState extends State with TextSelectionDe switch (defaultTargetPlatform) { case TargetPlatform.android: case TargetPlatform.fuchsia: - _clearSelection(); + clearSelection(); case TargetPlatform.iOS: hideToolbar(false); case TargetPlatform.linux: @@ -1525,7 +1525,7 @@ class SelectableRegionState extends State with TextSelectionDe switch (defaultTargetPlatform) { case TargetPlatform.android: case TargetPlatform.fuchsia: - _clearSelection(); + clearSelection(); case TargetPlatform.iOS: hideToolbar(false); case TargetPlatform.linux: @@ -1619,7 +1619,7 @@ class SelectableRegionState extends State with TextSelectionDe @override void selectAll([SelectionChangedCause? cause]) { - _clearSelection(); + clearSelection(); _selectable?.dispatchSelectionEvent(const SelectAllSelectionEvent()); if (cause == SelectionChangedCause.toolbar) { _showToolbar(); @@ -1635,7 +1635,7 @@ class SelectableRegionState extends State with TextSelectionDe @override void copySelection(SelectionChangedCause cause) { _copy(); - _clearSelection(); + clearSelection(); } @Deprecated( diff --git a/packages/flutter/test/widgets/selectable_region_test.dart b/packages/flutter/test/widgets/selectable_region_test.dart index 47e7b0554d..9cd3a3a6bf 100644 --- a/packages/flutter/test/widgets/selectable_region_test.dart +++ b/packages/flutter/test/widgets/selectable_region_test.dart @@ -4697,6 +4697,64 @@ void main() { skip: kIsWeb, // [intended] Web uses its native context menu. ); + testWidgets('can clear selection through SelectableRegionState', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + + await tester.pumpWidget( + MaterialApp( + home: SelectableRegion( + focusNode: focusNode, + selectionControls: materialTextSelectionControls, + child: const Column( + children: [ + Text('How are you?'), + Text('Good, and you?'), + Text('Fine, thank you.'), + ], + ), + ), + ), + ); + + final SelectableRegionState state = + tester.state(find.byType(SelectableRegion)); + final RenderParagraph paragraph1 = tester.renderObject(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText))); + final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph1, 2), kind: PointerDeviceKind.mouse); + addTearDown(gesture.removePointer); + await tester.pump(); + await gesture.up(); + await tester.pump(); + + await gesture.down(textOffsetToPosition(paragraph1, 2)); + await tester.pumpAndSettle(); + expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3)); + + await gesture.moveTo(textOffsetToPosition(paragraph1, 4)); + await tester.pump(); + expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 7)); + + final RenderParagraph paragraph2 = tester.renderObject(find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText))); + await gesture.moveTo(textOffsetToPosition(paragraph2, 5)); + // Should select the rest of paragraph 1. + expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 12)); + expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 6)); + + final RenderParagraph paragraph3 = tester.renderObject(find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText))); + await gesture.moveTo(textOffsetToPosition(paragraph3, 6)); + expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 12)); + expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 14)); + expect(paragraph3.selections[0], const TextSelection(baseOffset: 0, extentOffset: 11)); + await gesture.up(); + await tester.pumpAndSettle(); + + // Clear selection programatically. + state.clearSelection(); + expect(paragraph1.selections, isEmpty); + expect(paragraph2.selections, isEmpty); + expect(paragraph3.selections, isEmpty); + }, skip: kIsWeb); // https://github.com/flutter/flutter/issues/125582. + testWidgets('Text processing actions are added to the toolbar', (WidgetTester tester) async { final MockProcessTextHandler mockProcessTextHandler = MockProcessTextHandler(); TestWidgetsFlutterBinding.ensureInitialized().defaultBinaryMessenger