Skip to content

Commit 0a7f8af

Browse files
Support clearing selection programmatically through SelectableRegionState (flutter#152882)
This change exposes: * `SelectableRegionState.clearSelection()` to allow a user to programmatically clear the selection. * `SelectionAreaState`/`SelectionAreaState.selectableRegion` to allow a user to access public API in `SelectableRegion` from `SelectionArea`. Fixes flutter#126980
1 parent 0847228 commit 0a7f8af

File tree

3 files changed

+82
-19
lines changed

3 files changed

+82
-19
lines changed

packages/flutter/lib/src/material/selection_area.dart

+7-2
Original file line numberDiff line numberDiff line change
@@ -104,12 +104,16 @@ class SelectionArea extends StatefulWidget {
104104
}
105105

106106
@override
107-
State<StatefulWidget> createState() => _SelectionAreaState();
107+
State<StatefulWidget> createState() => SelectionAreaState();
108108
}
109109

110-
class _SelectionAreaState extends State<SelectionArea> {
110+
/// State for a [SelectionArea].
111+
class SelectionAreaState extends State<SelectionArea> {
111112
FocusNode get _effectiveFocusNode => widget.focusNode ?? (_internalNode ??= FocusNode());
112113
FocusNode? _internalNode;
114+
final GlobalKey<SelectableRegionState> _selectableRegionKey = GlobalKey<SelectableRegionState>();
115+
/// The [State] of the [SelectableRegion] for which this [SelectionArea] wraps.
116+
SelectableRegionState get selectableRegion => _selectableRegionKey.currentState!;
113117

114118
@override
115119
void dispose() {
@@ -127,6 +131,7 @@ class _SelectionAreaState extends State<SelectionArea> {
127131
TargetPlatform.macOS => cupertinoDesktopTextSelectionHandleControls,
128132
};
129133
return SelectableRegion(
134+
key: _selectableRegionKey,
130135
selectionControls: controls,
131136
focusNode: _effectiveFocusNode,
132137
contextMenuBuilder: widget.contextMenuBuilder,

packages/flutter/lib/src/widgets/selectable_region.dart

+17-17
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
450450
// the new window causing the Flutter application to go inactive. In this
451451
// case we want to retain the selection so it remains when we return to
452452
// the Flutter application.
453-
_clearSelection();
453+
clearSelection();
454454
}
455455
}
456456
if (kIsWeb) {
@@ -559,7 +559,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
559559
..onDragStart = _handleMouseDragStart
560560
..onDragUpdate = _handleMouseDragUpdate
561561
..onDragEnd = _handleMouseDragEnd
562-
..onCancel = _clearSelection
562+
..onCancel = clearSelection
563563
..dragStartBehavior = DragStartBehavior.down;
564564
},
565565
);
@@ -607,7 +607,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
607607
..onDragStart = _handleMouseDragStart
608608
..onDragUpdate = _handleMouseDragUpdate
609609
..onDragEnd = _handleMouseDragEnd
610-
..onCancel = _clearSelection
610+
..onCancel = clearSelection
611611
..dragStartBehavior = DragStartBehavior.down;
612612
},
613613
);
@@ -1228,7 +1228,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
12281228
/// See also:
12291229
/// * [_selectStartTo], which sets or updates selection start edge.
12301230
/// * [_finalizeSelection], which stops the `continuous` updates.
1231-
/// * [_clearSelection], which clears the ongoing selection.
1231+
/// * [clearSelection], which clears the ongoing selection.
12321232
/// * [_selectWordAt], which selects a whole word at the location.
12331233
/// * [_selectParagraphAt], which selects an entire paragraph at the location.
12341234
/// * [_collapseSelectionAt], which collapses the selection at the location.
@@ -1269,7 +1269,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
12691269
/// See also:
12701270
/// * [_selectEndTo], which sets or updates selection end edge.
12711271
/// * [_finalizeSelection], which stops the `continuous` updates.
1272-
/// * [_clearSelection], which clears the ongoing selection.
1272+
/// * [clearSelection], which clears the ongoing selection.
12731273
/// * [_selectWordAt], which selects a whole word at the location.
12741274
/// * [_selectParagraphAt], which selects an entire paragraph at the location.
12751275
/// * [_collapseSelectionAt], which collapses the selection at the location.
@@ -1293,7 +1293,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
12931293
/// * [_selectStartTo], which sets or updates selection start edge.
12941294
/// * [_selectEndTo], which sets or updates selection end edge.
12951295
/// * [_finalizeSelection], which stops the `continuous` updates.
1296-
/// * [_clearSelection], which clears the ongoing selection.
1296+
/// * [clearSelection], which clears the ongoing selection.
12971297
/// * [_selectWordAt], which selects a whole word at the location.
12981298
/// * [_selectParagraphAt], which selects an entire paragraph at the location.
12991299
/// * [selectAll], which selects the entire content.
@@ -1307,7 +1307,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
13071307
/// The `offset` is in global coordinates.
13081308
///
13091309
/// If the whole word is already in the current selection, selection won't
1310-
/// change. One call [_clearSelection] first if the selection needs to be
1310+
/// change. One call [clearSelection] first if the selection needs to be
13111311
/// updated even if the word is already covered by the current selection.
13121312
///
13131313
/// One can also use [_selectEndTo] or [_selectStartTo] to adjust the selection
@@ -1317,7 +1317,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
13171317
/// * [_selectStartTo], which sets or updates selection start edge.
13181318
/// * [_selectEndTo], which sets or updates selection end edge.
13191319
/// * [_finalizeSelection], which stops the `continuous` updates.
1320-
/// * [_clearSelection], which clears the ongoing selection.
1320+
/// * [clearSelection], which clears the ongoing selection.
13211321
/// * [_collapseSelectionAt], which collapses the selection at the location.
13221322
/// * [_selectParagraphAt], which selects an entire paragraph at the location.
13231323
/// * [selectAll], which selects the entire content.
@@ -1332,7 +1332,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
13321332
/// The `offset` is in global coordinates.
13331333
///
13341334
/// If the paragraph is already in the current selection, selection won't
1335-
/// change. One call [_clearSelection] first if the selection needs to be
1335+
/// change. One call [clearSelection] first if the selection needs to be
13361336
/// updated even if the paragraph is already covered by the current selection.
13371337
///
13381338
/// One can also use [_selectEndTo] or [_selectStartTo] to adjust the selection
@@ -1342,7 +1342,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
13421342
/// * [_selectStartTo], which sets or updates selection start edge.
13431343
/// * [_selectEndTo], which sets or updates selection end edge.
13441344
/// * [_finalizeSelection], which stops the `continuous` updates.
1345-
/// * [_clearSelection], which clear the ongoing selection.
1345+
/// * [clearSelection], which clear the ongoing selection.
13461346
/// * [_selectWordAt], which selects a whole word at the location.
13471347
/// * [selectAll], which selects the entire content.
13481348
void _selectParagraphAt({required Offset offset}) {
@@ -1353,7 +1353,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
13531353

13541354
/// Stops any ongoing selection updates.
13551355
///
1356-
/// This method is different from [_clearSelection] that it does not remove
1356+
/// This method is different from [clearSelection] that it does not remove
13571357
/// the current selection. It only stops the continuous updates.
13581358
///
13591359
/// A continuous update can happen as result of calling [_selectStartTo] or
@@ -1365,8 +1365,8 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
13651365
_stopSelectionStartEdgeUpdate();
13661366
}
13671367

1368-
/// Removes the ongoing selection.
1369-
void _clearSelection() {
1368+
/// Removes the ongoing selection for this [SelectableRegion].
1369+
void clearSelection() {
13701370
_finalizeSelection();
13711371
_directionalHorizontalBaseline = null;
13721372
_adjustingSelectionEnd = null;
@@ -1496,7 +1496,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
14961496
switch (defaultTargetPlatform) {
14971497
case TargetPlatform.android:
14981498
case TargetPlatform.fuchsia:
1499-
_clearSelection();
1499+
clearSelection();
15001500
case TargetPlatform.iOS:
15011501
hideToolbar(false);
15021502
case TargetPlatform.linux:
@@ -1525,7 +1525,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
15251525
switch (defaultTargetPlatform) {
15261526
case TargetPlatform.android:
15271527
case TargetPlatform.fuchsia:
1528-
_clearSelection();
1528+
clearSelection();
15291529
case TargetPlatform.iOS:
15301530
hideToolbar(false);
15311531
case TargetPlatform.linux:
@@ -1619,7 +1619,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
16191619

16201620
@override
16211621
void selectAll([SelectionChangedCause? cause]) {
1622-
_clearSelection();
1622+
clearSelection();
16231623
_selectable?.dispatchSelectionEvent(const SelectAllSelectionEvent());
16241624
if (cause == SelectionChangedCause.toolbar) {
16251625
_showToolbar();
@@ -1635,7 +1635,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
16351635
@override
16361636
void copySelection(SelectionChangedCause cause) {
16371637
_copy();
1638-
_clearSelection();
1638+
clearSelection();
16391639
}
16401640

16411641
@Deprecated(

packages/flutter/test/widgets/selectable_region_test.dart

+58
Original file line numberDiff line numberDiff line change
@@ -4697,6 +4697,64 @@ void main() {
46974697
skip: kIsWeb, // [intended] Web uses its native context menu.
46984698
);
46994699

4700+
testWidgets('can clear selection through SelectableRegionState', (WidgetTester tester) async {
4701+
final FocusNode focusNode = FocusNode();
4702+
addTearDown(focusNode.dispose);
4703+
4704+
await tester.pumpWidget(
4705+
MaterialApp(
4706+
home: SelectableRegion(
4707+
focusNode: focusNode,
4708+
selectionControls: materialTextSelectionControls,
4709+
child: const Column(
4710+
children: <Widget>[
4711+
Text('How are you?'),
4712+
Text('Good, and you?'),
4713+
Text('Fine, thank you.'),
4714+
],
4715+
),
4716+
),
4717+
),
4718+
);
4719+
4720+
final SelectableRegionState state =
4721+
tester.state<SelectableRegionState>(find.byType(SelectableRegion));
4722+
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)));
4723+
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph1, 2), kind: PointerDeviceKind.mouse);
4724+
addTearDown(gesture.removePointer);
4725+
await tester.pump();
4726+
await gesture.up();
4727+
await tester.pump();
4728+
4729+
await gesture.down(textOffsetToPosition(paragraph1, 2));
4730+
await tester.pumpAndSettle();
4731+
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
4732+
4733+
await gesture.moveTo(textOffsetToPosition(paragraph1, 4));
4734+
await tester.pump();
4735+
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 7));
4736+
4737+
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)));
4738+
await gesture.moveTo(textOffsetToPosition(paragraph2, 5));
4739+
// Should select the rest of paragraph 1.
4740+
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 12));
4741+
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 6));
4742+
4743+
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)));
4744+
await gesture.moveTo(textOffsetToPosition(paragraph3, 6));
4745+
expect(paragraph1.selections[0], const TextSelection(baseOffset: 0, extentOffset: 12));
4746+
expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 14));
4747+
expect(paragraph3.selections[0], const TextSelection(baseOffset: 0, extentOffset: 11));
4748+
await gesture.up();
4749+
await tester.pumpAndSettle();
4750+
4751+
// Clear selection programatically.
4752+
state.clearSelection();
4753+
expect(paragraph1.selections, isEmpty);
4754+
expect(paragraph2.selections, isEmpty);
4755+
expect(paragraph3.selections, isEmpty);
4756+
}, skip: kIsWeb); // https://github.com/flutter/flutter/issues/125582.
4757+
47004758
testWidgets('Text processing actions are added to the toolbar', (WidgetTester tester) async {
47014759
final MockProcessTextHandler mockProcessTextHandler = MockProcessTextHandler();
47024760
TestWidgetsFlutterBinding.ensureInitialized().defaultBinaryMessenger

0 commit comments

Comments
 (0)