Skip to content

Commit

Permalink
Support clearing selection programmatically through SelectableRegionS…
Browse files Browse the repository at this point in the history
…tate (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
  • Loading branch information
Renzo-Olivares authored Aug 6, 2024
1 parent 0847228 commit 0a7f8af
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 19 deletions.
9 changes: 7 additions & 2 deletions packages/flutter/lib/src/material/selection_area.dart
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,16 @@ class SelectionArea extends StatefulWidget {
}

@override
State<StatefulWidget> createState() => _SelectionAreaState();
State<StatefulWidget> createState() => SelectionAreaState();
}

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

@override
void dispose() {
Expand All @@ -127,6 +131,7 @@ class _SelectionAreaState extends State<SelectionArea> {
TargetPlatform.macOS => cupertinoDesktopTextSelectionHandleControls,
};
return SelectableRegion(
key: _selectableRegionKey,
selectionControls: controls,
focusNode: _effectiveFocusNode,
contextMenuBuilder: widget.contextMenuBuilder,
Expand Down
34 changes: 17 additions & 17 deletions packages/flutter/lib/src/widgets/selectable_region.dart
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,7 @@ class SelectableRegionState extends State<SelectableRegion> 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) {
Expand Down Expand Up @@ -559,7 +559,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
..onDragStart = _handleMouseDragStart
..onDragUpdate = _handleMouseDragUpdate
..onDragEnd = _handleMouseDragEnd
..onCancel = _clearSelection
..onCancel = clearSelection
..dragStartBehavior = DragStartBehavior.down;
},
);
Expand Down Expand Up @@ -607,7 +607,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
..onDragStart = _handleMouseDragStart
..onDragUpdate = _handleMouseDragUpdate
..onDragEnd = _handleMouseDragEnd
..onCancel = _clearSelection
..onCancel = clearSelection
..dragStartBehavior = DragStartBehavior.down;
},
);
Expand Down Expand Up @@ -1228,7 +1228,7 @@ class SelectableRegionState extends State<SelectableRegion> 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.
Expand Down Expand Up @@ -1269,7 +1269,7 @@ class SelectableRegionState extends State<SelectableRegion> 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.
Expand All @@ -1293,7 +1293,7 @@ class SelectableRegionState extends State<SelectableRegion> 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.
Expand All @@ -1307,7 +1307,7 @@ class SelectableRegionState extends State<SelectableRegion> 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
Expand All @@ -1317,7 +1317,7 @@ class SelectableRegionState extends State<SelectableRegion> 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.
Expand All @@ -1332,7 +1332,7 @@ class SelectableRegionState extends State<SelectableRegion> 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
Expand All @@ -1342,7 +1342,7 @@ class SelectableRegionState extends State<SelectableRegion> 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}) {
Expand All @@ -1353,7 +1353,7 @@ class SelectableRegionState extends State<SelectableRegion> 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
Expand All @@ -1365,8 +1365,8 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
_stopSelectionStartEdgeUpdate();
}

/// Removes the ongoing selection.
void _clearSelection() {
/// Removes the ongoing selection for this [SelectableRegion].
void clearSelection() {
_finalizeSelection();
_directionalHorizontalBaseline = null;
_adjustingSelectionEnd = null;
Expand Down Expand Up @@ -1496,7 +1496,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
_clearSelection();
clearSelection();
case TargetPlatform.iOS:
hideToolbar(false);
case TargetPlatform.linux:
Expand Down Expand Up @@ -1525,7 +1525,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
_clearSelection();
clearSelection();
case TargetPlatform.iOS:
hideToolbar(false);
case TargetPlatform.linux:
Expand Down Expand Up @@ -1619,7 +1619,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe

@override
void selectAll([SelectionChangedCause? cause]) {
_clearSelection();
clearSelection();
_selectable?.dispatchSelectionEvent(const SelectAllSelectionEvent());
if (cause == SelectionChangedCause.toolbar) {
_showToolbar();
Expand All @@ -1635,7 +1635,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
@override
void copySelection(SelectionChangedCause cause) {
_copy();
_clearSelection();
clearSelection();
}

@Deprecated(
Expand Down
58 changes: 58 additions & 0 deletions packages/flutter/test/widgets/selectable_region_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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: <Widget>[
Text('How are you?'),
Text('Good, and you?'),
Text('Fine, thank you.'),
],
),
),
),
);

final SelectableRegionState state =
tester.state<SelectableRegionState>(find.byType(SelectableRegion));
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(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<RenderParagraph>(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<RenderParagraph>(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
Expand Down

0 comments on commit 0a7f8af

Please sign in to comment.