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));
|
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 {
|
|||||||
}
|
}
|
||||||
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,
|
||||||
),
|
),
|
||||||
SelectionChangedCause.keyboard,
|
|
||||||
);
|
);
|
||||||
return;
|
}
|
||||||
|
if (value != null) {
|
||||||
|
if (textSelectionDelegate.textEditingValue.selection != value.selection) {
|
||||||
|
_handleSelectionChange(
|
||||||
|
value.selection,
|
||||||
|
SelectionChangedCause.keyboard,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
textSelectionDelegate.textEditingValue = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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
|
|||||||
// 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
|
|||||||
// 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
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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() {
|
|||||||
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));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user