Unverified Commit 58081821 authored by chunhtai's avatar chunhtai Committed by GitHub

Editable text should call onSelectionChanged when selection changes a… (#72011)

parent f123f64d
...@@ -756,12 +756,12 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -756,12 +756,12 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
newSelection = TextSelection.fromPosition(TextPosition(offset: newOffset)); newSelection = TextSelection.fromPosition(TextPosition(offset: newOffset));
} }
// Update the text selection delegate so that the engine knows what we did.
textSelectionDelegate.textEditingValue = textSelectionDelegate.textEditingValue.copyWith(selection: newSelection);
_handleSelectionChange( _handleSelectionChange(
newSelection, newSelection,
SelectionChangedCause.keyboard, SelectionChangedCause.keyboard,
); );
// Update the text selection delegate so that the engine knows what we did.
textSelectionDelegate.textEditingValue = textSelectionDelegate.textEditingValue.copyWith(selection: newSelection);
} }
// Handles shortcut functionality including cut, copy, paste and select all // Handles shortcut functionality including cut, copy, paste and select all
...@@ -778,39 +778,44 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -778,39 +778,44 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
} }
return; return;
} }
TextEditingValue? value;
if (key == LogicalKeyboardKey.keyX && !_readOnly) { if (key == LogicalKeyboardKey.keyX && !_readOnly) {
if (!selection.isCollapsed) { if (!selection.isCollapsed) {
Clipboard.setData(ClipboardData(text: selection.textInside(text))); Clipboard.setData(ClipboardData(text: selection.textInside(text)));
textSelectionDelegate.textEditingValue = TextEditingValue( value = TextEditingValue(
text: selection.textBefore(text) + selection.textAfter(text), text: selection.textBefore(text) + selection.textAfter(text),
selection: TextSelection.collapsed(offset: math.min(selection.start, selection.end)), selection: TextSelection.collapsed(offset: math.min(selection.start, selection.end)),
); );
} }
return; } else if (key == LogicalKeyboardKey.keyV && !_readOnly) {
}
if (key == LogicalKeyboardKey.keyV && !_readOnly) {
// Snapshot the input before using `await`. // Snapshot the input before using `await`.
// See https://github.com/flutter/flutter/issues/11427 // See https://github.com/flutter/flutter/issues/11427
final ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain); final ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
if (data != null) { if (data != null) {
textSelectionDelegate.textEditingValue = TextEditingValue( value = TextEditingValue(
text: selection.textBefore(text) + data.text! + selection.textAfter(text), text: selection.textBefore(text) + data.text! + selection.textAfter(text),
selection: TextSelection.collapsed( selection: TextSelection.collapsed(
offset: math.min(selection.start, selection.end) + data.text!.length, offset: math.min(selection.start, selection.end) + data.text!.length,
), ),
); );
} }
return; } else if (key == LogicalKeyboardKey.keyA) {
} value = TextEditingValue(
if (key == LogicalKeyboardKey.keyA) { text: text,
_handleSelectionChange( selection: selection.copyWith(
selection.copyWith(
baseOffset: 0, baseOffset: 0,
extentOffset: textSelectionDelegate.textEditingValue.text.length, extentOffset: textSelectionDelegate.textEditingValue.text.length,
), ),
);
}
if (value != null) {
if (textSelectionDelegate.textEditingValue.selection != value.selection) {
_handleSelectionChange(
value.selection,
SelectionChangedCause.keyboard, SelectionChangedCause.keyboard,
); );
return; }
textSelectionDelegate.textEditingValue = value;
} }
} }
...@@ -836,9 +841,16 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -836,9 +841,16 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
textAfter = textAfter.substring(deleteCount); textAfter = textAfter.substring(deleteCount);
} }
} }
final TextSelection newSelection = TextSelection.collapsed(offset: cursorPosition);
if (selection != newSelection) {
_handleSelectionChange(
newSelection,
SelectionChangedCause.keyboard,
);
}
textSelectionDelegate.textEditingValue = TextEditingValue( textSelectionDelegate.textEditingValue = TextEditingValue(
text: textBefore + textAfter, text: textBefore + textAfter,
selection: TextSelection.collapsed(offset: cursorPosition), selection: newSelection,
); );
} }
......
...@@ -2243,6 +2243,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2243,6 +2243,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
// trying to restore the original composing region. // trying to restore the original composing region.
final bool textChanged = _value.text != value.text final bool textChanged = _value.text != value.text
|| (!_value.composing.isCollapsed && value.composing.isCollapsed); || (!_value.composing.isCollapsed && value.composing.isCollapsed);
final bool selectionChanged = _value.selection != value.selection;
if (textChanged) { if (textChanged) {
value = widget.inputFormatters?.fold<TextEditingValue>( value = widget.inputFormatters?.fold<TextEditingValue>(
...@@ -2262,7 +2263,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2262,7 +2263,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
// Put all optional user callback invocations in a batch edit to prevent // Put all optional user callback invocations in a batch edit to prevent
// sending multiple `TextInput.updateEditingValue` messages. // sending multiple `TextInput.updateEditingValue` messages.
beginBatchEdit(); beginBatchEdit();
_value = value; _value = value;
if (textChanged) { if (textChanged) {
try { try {
...@@ -2277,6 +2277,19 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2277,6 +2277,19 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
} }
} }
if (selectionChanged) {
try {
widget.onSelectionChanged?.call(value.selection, null);
} catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'widgets',
context: ErrorDescription('while calling onSelectionChanged'),
));
}
}
endBatchEdit(); endBatchEdit();
} }
......
...@@ -4253,7 +4253,7 @@ void main() { ...@@ -4253,7 +4253,7 @@ void main() {
selection, selection,
equals( equals(
const TextSelection( const TextSelection(
baseOffset: 10, baseOffset: 4,
extentOffset: 4, extentOffset: 4,
affinity: TextAffinity.downstream, affinity: TextAffinity.downstream,
), ),
...@@ -4289,7 +4289,7 @@ void main() { ...@@ -4289,7 +4289,7 @@ void main() {
equals( equals(
const TextSelection( const TextSelection(
baseOffset: 10, baseOffset: 10,
extentOffset: 4, extentOffset: 10,
affinity: TextAffinity.downstream, affinity: TextAffinity.downstream,
), ),
), ),
...@@ -4335,7 +4335,7 @@ void main() { ...@@ -4335,7 +4335,7 @@ void main() {
equals( equals(
const TextSelection( const TextSelection(
baseOffset: 0, baseOffset: 0,
extentOffset: 72, extentOffset: 0,
affinity: TextAffinity.downstream, affinity: TextAffinity.downstream,
), ),
), ),
......
...@@ -859,6 +859,58 @@ void main() { ...@@ -859,6 +859,58 @@ void main() {
expect(controller.selection.extentOffset, 11); expect(controller.selection.extentOffset, 11);
}); });
testWidgets('Dragging handles calls onSelectionChanged', (WidgetTester tester) async {
TextSelection? newSelection;
await tester.pumpWidget(
MaterialApp(
home: Material(
child: SelectableText(
'abc def ghi',
dragStartBehavior: DragStartBehavior.down,
onSelectionChanged: (TextSelection selection, SelectionChangedCause? cause) {
expect(newSelection, isNull);
newSelection = selection;
},
),
),
),
);
// Long press the 'e' to select 'def'.
final Offset ePos = textOffsetToPosition(tester, 5);
TestGesture gesture = await tester.startGesture(ePos, pointer: 7);
await tester.pump(const Duration(seconds: 2));
await gesture.up();
await tester.pump();
await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero
expect(newSelection!.baseOffset, 4);
expect(newSelection!.extentOffset, 7);
final RenderEditable renderEditable = findRenderEditable(tester);
final List<TextSelectionPoint> endpoints = globalize(
renderEditable.getEndpointsForSelection(newSelection!),
renderEditable,
);
expect(endpoints.length, 2);
newSelection = null;
// Drag the right handle 2 letters to the right.
// We use a small offset because the endpoint is on the very corner
// of the handle.
final Offset handlePos = endpoints[1].point + const Offset(1.0, 1.0);
final Offset newHandlePos = textOffsetToPosition(tester, 9);
gesture = await tester.startGesture(handlePos, pointer: 7);
await tester.pump();
await gesture.moveTo(newHandlePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(newSelection!.baseOffset, 4);
expect(newSelection!.extentOffset, 9);
});
testWidgets('Cannot drag one handle past the other', (WidgetTester tester) async { testWidgets('Cannot drag one handle past the other', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
const MaterialApp( const MaterialApp(
...@@ -1546,6 +1598,53 @@ void main() { ...@@ -1546,6 +1598,53 @@ void main() {
expect(controller.selection.extentOffset, 31); expect(controller.selection.extentOffset, 31);
}); });
testWidgets('keyboard selection should call onSelectionChanged', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
TextSelection? newSelection;
const String testValue = 'a big house\njumped over a mouse';
await tester.pumpWidget(
MaterialApp(
home: Material(
child: RawKeyboardListener(
focusNode: focusNode,
onKey: null,
child: SelectableText(
testValue,
maxLines: 3,
onSelectionChanged: (TextSelection selection, SelectionChangedCause? cause) {
expect(newSelection, isNull);
newSelection = selection;
},
),
),
),
),
);
final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first);
final TextEditingController controller = editableTextWidget.controller;
focusNode.requestFocus();
await tester.pump();
await tester.tap(find.byType(SelectableText));
await tester.pumpAndSettle();
expect(newSelection!.baseOffset, 31);
expect(newSelection!.extentOffset, 31);
newSelection = null;
controller.selection = const TextSelection.collapsed(offset: 0);
await tester.pump();
// Select the first 5 characters
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
for (int i = 0; i < 5; i += 1) {
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pumpAndSettle();
expect(newSelection!.baseOffset, 0);
expect(newSelection!.extentOffset, i + 1);
newSelection = null;
}
});
testWidgets('Changing positions of selectable text', (WidgetTester tester) async { testWidgets('Changing positions of selectable text', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
final List<RawKeyEvent> events = <RawKeyEvent>[]; final List<RawKeyEvent> events = <RawKeyEvent>[];
...@@ -4056,6 +4155,44 @@ void main() { ...@@ -4056,6 +4155,44 @@ void main() {
}), }),
); );
testWidgets('The Select All calls on selection changed', (WidgetTester tester) async {
TextSelection? newSelection;
await tester.pumpWidget(
MaterialApp(
home: Material(
child: SelectableText(
'abc def ghi',
onSelectionChanged: (TextSelection selection, SelectionChangedCause? cause) {
expect(newSelection, isNull);
newSelection = selection;
},
),
),
),
);
// Long press at 'e' in 'def'.
final Offset ePos = textOffsetToPosition(tester, 5);
await tester.longPressAt(ePos);
await tester.pumpAndSettle();
expect(newSelection!.baseOffset, 4);
expect(newSelection!.extentOffset, 7);
newSelection = null;
await tester.tap(find.text('Select all'));
await tester.pump();
expect(newSelection!.baseOffset, 0);
expect(newSelection!.extentOffset, 11);
},
variant: const TargetPlatformVariant(<TargetPlatform>{
TargetPlatform.android,
TargetPlatform.fuchsia,
TargetPlatform.linux,
TargetPlatform.windows,
}),
);
testWidgets('Does not show handles when updated from the web engine', (WidgetTester tester) async { testWidgets('Does not show handles when updated from the web engine', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
const MaterialApp( const MaterialApp(
...@@ -4118,11 +4255,12 @@ void main() { ...@@ -4118,11 +4255,12 @@ void main() {
await tester.longPressAt(aLocation); await tester.longPressAt(aLocation);
await tester.pump(); await tester.pump();
expect(onSelectionChangedCallCount, equals(1)); expect(onSelectionChangedCallCount, equals(1));
// Long press to select 'def'.
// Tap on 'Select all' option to select the whole text.
await tester.longPressAt(textOffsetToPosition(tester, 5)); await tester.longPressAt(textOffsetToPosition(tester, 5));
await tester.pump(); await tester.pump();
await tester.tap(find.text('Select all'));
expect(onSelectionChangedCallCount, equals(2)); expect(onSelectionChangedCallCount, equals(2));
// Tap on 'Select all' option to select the whole text.
await tester.tap(find.text('Select all'));
expect(onSelectionChangedCallCount, equals(3));
}); });
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment