Editable text should call onSelectionChanged when selection changes a… (#72011)
This commit is contained in:
parent
f123f64deb
commit
58081821ab
@ -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));
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user