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 {
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(
newSelection,
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
......@@ -778,39 +778,44 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
}
return;
}
TextEditingValue? value;
if (key == LogicalKeyboardKey.keyX && !_readOnly) {
if (!selection.isCollapsed) {
Clipboard.setData(ClipboardData(text: selection.textInside(text)));
textSelectionDelegate.textEditingValue = TextEditingValue(
value = TextEditingValue(
text: selection.textBefore(text) + selection.textAfter(text),
selection: TextSelection.collapsed(offset: math.min(selection.start, selection.end)),
);
}
return;
}
if (key == LogicalKeyboardKey.keyV && !_readOnly) {
} else if (key == LogicalKeyboardKey.keyV && !_readOnly) {
// Snapshot the input before using `await`.
// See https://github.com/flutter/flutter/issues/11427
final ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
if (data != null) {
textSelectionDelegate.textEditingValue = TextEditingValue(
value = TextEditingValue(
text: selection.textBefore(text) + data.text! + selection.textAfter(text),
selection: TextSelection.collapsed(
offset: math.min(selection.start, selection.end) + data.text!.length,
),
);
}
return;
}
if (key == LogicalKeyboardKey.keyA) {
_handleSelectionChange(
selection.copyWith(
} else if (key == LogicalKeyboardKey.keyA) {
value = TextEditingValue(
text: text,
selection: selection.copyWith(
baseOffset: 0,
extentOffset: textSelectionDelegate.textEditingValue.text.length,
),
);
}
if (value != null) {
if (textSelectionDelegate.textEditingValue.selection != value.selection) {
_handleSelectionChange(
value.selection,
SelectionChangedCause.keyboard,
);
return;
}
textSelectionDelegate.textEditingValue = value;
}
}
......@@ -836,9 +841,16 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
textAfter = textAfter.substring(deleteCount);
}
}
final TextSelection newSelection = TextSelection.collapsed(offset: cursorPosition);
if (selection != newSelection) {
_handleSelectionChange(
newSelection,
SelectionChangedCause.keyboard,
);
}
textSelectionDelegate.textEditingValue = TextEditingValue(
text: textBefore + textAfter,
selection: TextSelection.collapsed(offset: cursorPosition),
selection: newSelection,
);
}
......
......@@ -2243,6 +2243,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
// trying to restore the original composing region.
final bool textChanged = _value.text != value.text
|| (!_value.composing.isCollapsed && value.composing.isCollapsed);
final bool selectionChanged = _value.selection != value.selection;
if (textChanged) {
value = widget.inputFormatters?.fold<TextEditingValue>(
......@@ -2262,7 +2263,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
// Put all optional user callback invocations in a batch edit to prevent
// sending multiple `TextInput.updateEditingValue` messages.
beginBatchEdit();
_value = value;
if (textChanged) {
try {
......@@ -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();
}
......
......@@ -4253,7 +4253,7 @@ void main() {
selection,
equals(
const TextSelection(
baseOffset: 10,
baseOffset: 4,
extentOffset: 4,
affinity: TextAffinity.downstream,
),
......@@ -4289,7 +4289,7 @@ void main() {
equals(
const TextSelection(
baseOffset: 10,
extentOffset: 4,
extentOffset: 10,
affinity: TextAffinity.downstream,
),
),
......@@ -4335,7 +4335,7 @@ void main() {
equals(
const TextSelection(
baseOffset: 0,
extentOffset: 72,
extentOffset: 0,
affinity: TextAffinity.downstream,
),
),
......
......@@ -859,6 +859,58 @@ void main() {
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 {
await tester.pumpWidget(
const MaterialApp(
......@@ -1546,6 +1598,53 @@ void main() {
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 {
final FocusNode focusNode = FocusNode();
final List<RawKeyEvent> events = <RawKeyEvent>[];
......@@ -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 {
await tester.pumpWidget(
const MaterialApp(
......@@ -4118,11 +4255,12 @@ void main() {
await tester.longPressAt(aLocation);
await tester.pump();
expect(onSelectionChangedCallCount, equals(1));
// Tap on 'Select all' option to select the whole text.
// Long press to select 'def'.
await tester.longPressAt(textOffsetToPosition(tester, 5));
await tester.pump();
await tester.tap(find.text('Select all'));
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