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
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() {
@ -127,6 +131,7 @@ class _SelectionAreaState extends State<SelectionArea> {
TargetPlatform.macOS => cupertinoDesktopTextSelectionHandleControls,
};
return SelectableRegion(
key: _selectableRegionKey,
selectionControls: controls,
focusNode: _effectiveFocusNode,
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
// 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<SelectableRegion> with TextSelectionDe
..onDragStart = _handleMouseDragStart
..onDragUpdate = _handleMouseDragUpdate
..onDragEnd = _handleMouseDragEnd
..onCancel = _clearSelection
..onCancel = clearSelection
..dragStartBehavior = DragStartBehavior.down;
},
);
@ -607,7 +607,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
..onDragStart = _handleMouseDragStart
..onDragUpdate = _handleMouseDragUpdate
..onDragEnd = _handleMouseDragEnd
..onCancel = _clearSelection
..onCancel = clearSelection
..dragStartBehavior = DragStartBehavior.down;
},
);
@ -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.
@ -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.
@ -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.
@ -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
@ -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.
@ -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
@ -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}) {
@ -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
@ -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;
@ -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:
@ -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:
@ -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();
@ -1635,7 +1635,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
@override
void copySelection(SelectionChangedCause cause) {
_copy();
_clearSelection();
clearSelection();
}
@Deprecated(

View File

@ -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