Support clearing selection programmatically through SelectableRegionState (#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 #126980
This commit is contained in:
Renzo Olivares 2024-08-06 16:05:03 -07:00 committed by GitHub
parent 0847228315
commit 0a7f8af6d1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 82 additions and 19 deletions

View File

@ -104,12 +104,16 @@ class SelectionArea extends StatefulWidget {
} }
@override @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 get _effectiveFocusNode => widget.focusNode ?? (_internalNode ??= FocusNode());
FocusNode? _internalNode; FocusNode? _internalNode;
final GlobalKey<SelectableRegionState> _selectableRegionKey = GlobalKey<SelectableRegionState>();
/// The [State] of the [SelectableRegion] for which this [SelectionArea] wraps.
SelectableRegionState get selectableRegion => _selectableRegionKey.currentState!;
@override @override
void dispose() { void dispose() {
@ -127,6 +131,7 @@ class _SelectionAreaState extends State<SelectionArea> {
TargetPlatform.macOS => cupertinoDesktopTextSelectionHandleControls, TargetPlatform.macOS => cupertinoDesktopTextSelectionHandleControls,
}; };
return SelectableRegion( return SelectableRegion(
key: _selectableRegionKey,
selectionControls: controls, selectionControls: controls,
focusNode: _effectiveFocusNode, focusNode: _effectiveFocusNode,
contextMenuBuilder: widget.contextMenuBuilder, contextMenuBuilder: widget.contextMenuBuilder,

View File

@ -450,7 +450,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
// the new window causing the Flutter application to go inactive. In this // 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 // case we want to retain the selection so it remains when we return to
// the Flutter application. // the Flutter application.
_clearSelection(); clearSelection();
} }
} }
if (kIsWeb) { if (kIsWeb) {
@ -559,7 +559,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
..onDragStart = _handleMouseDragStart ..onDragStart = _handleMouseDragStart
..onDragUpdate = _handleMouseDragUpdate ..onDragUpdate = _handleMouseDragUpdate
..onDragEnd = _handleMouseDragEnd ..onDragEnd = _handleMouseDragEnd
..onCancel = _clearSelection ..onCancel = clearSelection
..dragStartBehavior = DragStartBehavior.down; ..dragStartBehavior = DragStartBehavior.down;
}, },
); );
@ -607,7 +607,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
..onDragStart = _handleMouseDragStart ..onDragStart = _handleMouseDragStart
..onDragUpdate = _handleMouseDragUpdate ..onDragUpdate = _handleMouseDragUpdate
..onDragEnd = _handleMouseDragEnd ..onDragEnd = _handleMouseDragEnd
..onCancel = _clearSelection ..onCancel = clearSelection
..dragStartBehavior = DragStartBehavior.down; ..dragStartBehavior = DragStartBehavior.down;
}, },
); );
@ -1228,7 +1228,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
/// See also: /// See also:
/// * [_selectStartTo], which sets or updates selection start edge. /// * [_selectStartTo], which sets or updates selection start edge.
/// * [_finalizeSelection], which stops the `continuous` updates. /// * [_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. /// * [_selectWordAt], which selects a whole word at the location.
/// * [_selectParagraphAt], which selects an entire paragraph at the location. /// * [_selectParagraphAt], which selects an entire paragraph at the location.
/// * [_collapseSelectionAt], which collapses the selection at the location. /// * [_collapseSelectionAt], which collapses the selection at the location.
@ -1269,7 +1269,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
/// See also: /// See also:
/// * [_selectEndTo], which sets or updates selection end edge. /// * [_selectEndTo], which sets or updates selection end edge.
/// * [_finalizeSelection], which stops the `continuous` updates. /// * [_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. /// * [_selectWordAt], which selects a whole word at the location.
/// * [_selectParagraphAt], which selects an entire paragraph at the location. /// * [_selectParagraphAt], which selects an entire paragraph at the location.
/// * [_collapseSelectionAt], which collapses the selection at the location. /// * [_collapseSelectionAt], which collapses the selection at the location.
@ -1293,7 +1293,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
/// * [_selectStartTo], which sets or updates selection start edge. /// * [_selectStartTo], which sets or updates selection start edge.
/// * [_selectEndTo], which sets or updates selection end edge. /// * [_selectEndTo], which sets or updates selection end edge.
/// * [_finalizeSelection], which stops the `continuous` updates. /// * [_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. /// * [_selectWordAt], which selects a whole word at the location.
/// * [_selectParagraphAt], which selects an entire paragraph at the location. /// * [_selectParagraphAt], which selects an entire paragraph at the location.
/// * [selectAll], which selects the entire content. /// * [selectAll], which selects the entire content.
@ -1307,7 +1307,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
/// The `offset` is in global coordinates. /// The `offset` is in global coordinates.
/// ///
/// If the whole word is already in the current selection, selection won't /// 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. /// updated even if the word is already covered by the current selection.
/// ///
/// One can also use [_selectEndTo] or [_selectStartTo] to adjust the selection /// One can also use [_selectEndTo] or [_selectStartTo] to adjust the selection
@ -1317,7 +1317,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
/// * [_selectStartTo], which sets or updates selection start edge. /// * [_selectStartTo], which sets or updates selection start edge.
/// * [_selectEndTo], which sets or updates selection end edge. /// * [_selectEndTo], which sets or updates selection end edge.
/// * [_finalizeSelection], which stops the `continuous` updates. /// * [_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. /// * [_collapseSelectionAt], which collapses the selection at the location.
/// * [_selectParagraphAt], which selects an entire paragraph at the location. /// * [_selectParagraphAt], which selects an entire paragraph at the location.
/// * [selectAll], which selects the entire content. /// * [selectAll], which selects the entire content.
@ -1332,7 +1332,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
/// The `offset` is in global coordinates. /// The `offset` is in global coordinates.
/// ///
/// If the paragraph is already in the current selection, selection won't /// 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. /// updated even if the paragraph is already covered by the current selection.
/// ///
/// One can also use [_selectEndTo] or [_selectStartTo] to adjust the selection /// One can also use [_selectEndTo] or [_selectStartTo] to adjust the selection
@ -1342,7 +1342,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
/// * [_selectStartTo], which sets or updates selection start edge. /// * [_selectStartTo], which sets or updates selection start edge.
/// * [_selectEndTo], which sets or updates selection end edge. /// * [_selectEndTo], which sets or updates selection end edge.
/// * [_finalizeSelection], which stops the `continuous` updates. /// * [_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. /// * [_selectWordAt], which selects a whole word at the location.
/// * [selectAll], which selects the entire content. /// * [selectAll], which selects the entire content.
void _selectParagraphAt({required Offset offset}) { void _selectParagraphAt({required Offset offset}) {
@ -1353,7 +1353,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
/// Stops any ongoing selection updates. /// 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. /// the current selection. It only stops the continuous updates.
/// ///
/// A continuous update can happen as result of calling [_selectStartTo] or /// A continuous update can happen as result of calling [_selectStartTo] or
@ -1365,8 +1365,8 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
_stopSelectionStartEdgeUpdate(); _stopSelectionStartEdgeUpdate();
} }
/// Removes the ongoing selection. /// Removes the ongoing selection for this [SelectableRegion].
void _clearSelection() { void clearSelection() {
_finalizeSelection(); _finalizeSelection();
_directionalHorizontalBaseline = null; _directionalHorizontalBaseline = null;
_adjustingSelectionEnd = null; _adjustingSelectionEnd = null;
@ -1496,7 +1496,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
switch (defaultTargetPlatform) { switch (defaultTargetPlatform) {
case TargetPlatform.android: case TargetPlatform.android:
case TargetPlatform.fuchsia: case TargetPlatform.fuchsia:
_clearSelection(); clearSelection();
case TargetPlatform.iOS: case TargetPlatform.iOS:
hideToolbar(false); hideToolbar(false);
case TargetPlatform.linux: case TargetPlatform.linux:
@ -1525,7 +1525,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
switch (defaultTargetPlatform) { switch (defaultTargetPlatform) {
case TargetPlatform.android: case TargetPlatform.android:
case TargetPlatform.fuchsia: case TargetPlatform.fuchsia:
_clearSelection(); clearSelection();
case TargetPlatform.iOS: case TargetPlatform.iOS:
hideToolbar(false); hideToolbar(false);
case TargetPlatform.linux: case TargetPlatform.linux:
@ -1619,7 +1619,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
@override @override
void selectAll([SelectionChangedCause? cause]) { void selectAll([SelectionChangedCause? cause]) {
_clearSelection(); clearSelection();
_selectable?.dispatchSelectionEvent(const SelectAllSelectionEvent()); _selectable?.dispatchSelectionEvent(const SelectAllSelectionEvent());
if (cause == SelectionChangedCause.toolbar) { if (cause == SelectionChangedCause.toolbar) {
_showToolbar(); _showToolbar();
@ -1635,7 +1635,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
@override @override
void copySelection(SelectionChangedCause cause) { void copySelection(SelectionChangedCause cause) {
_copy(); _copy();
_clearSelection(); clearSelection();
} }
@Deprecated( @Deprecated(

View File

@ -4697,6 +4697,64 @@ void main() {
skip: kIsWeb, // [intended] Web uses its native context menu. 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 { testWidgets('Text processing actions are added to the toolbar', (WidgetTester tester) async {
final MockProcessTextHandler mockProcessTextHandler = MockProcessTextHandler(); final MockProcessTextHandler mockProcessTextHandler = MockProcessTextHandler();
TestWidgetsFlutterBinding.ensureInitialized().defaultBinaryMessenger TestWidgetsFlutterBinding.ensureInitialized().defaultBinaryMessenger