Unverified Commit 83d4d63a authored by Gary Qian's avatar Gary Qian Committed by GitHub

Track lastKnownRemoteTextEditingValue separately from received data (#49406)

parent 25f798a1
...@@ -1204,7 +1204,13 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1204,7 +1204,13 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
// TextInputClient implementation: // TextInputClient implementation:
TextEditingValue _lastKnownRemoteTextEditingValue; // _lastFormattedUnmodifiedTextEditingValue tracks the last value
// that the formatter ran on and is used to prevent double-formatting.
TextEditingValue _lastFormattedUnmodifiedTextEditingValue;
// _receivedRemoteTextEditingValue is the direct value last passed in
// updateEditingValue. This value does not get updated with the formatted
// version.
TextEditingValue _receivedRemoteTextEditingValue;
@override @override
TextEditingValue get currentTextEditingValue => _value; TextEditingValue get currentTextEditingValue => _value;
...@@ -1216,6 +1222,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1216,6 +1222,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
if (widget.readOnly) { if (widget.readOnly) {
return; return;
} }
_receivedRemoteTextEditingValue = value;
if (value.text != _value.text) { if (value.text != _value.text) {
hideToolbar(); hideToolbar();
_showCaretOnScreen(); _showCaretOnScreen();
...@@ -1224,7 +1231,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1224,7 +1231,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
_obscureLatestCharIndex = _value.selection.baseOffset; _obscureLatestCharIndex = _value.selection.baseOffset;
} }
} }
_lastKnownRemoteTextEditingValue = value;
_formatAndSetValue(value); _formatAndSetValue(value);
// To keep the cursor from blinking while typing, we want to restart the // To keep the cursor from blinking while typing, we want to restart the
...@@ -1251,7 +1258,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1251,7 +1258,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
break; break;
default: default:
// Finalize editing, but don't give up focus because this keyboard // Finalize editing, but don't give up focus because this keyboard
// action does not imply the user is done inputting information. // action does not imply the user is done inputting information.
_finalizeEditing(false); _finalizeEditing(false);
break; break;
} }
...@@ -1351,9 +1358,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1351,9 +1358,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
if (!_hasInputConnection) if (!_hasInputConnection)
return; return;
final TextEditingValue localValue = _value; final TextEditingValue localValue = _value;
if (localValue == _lastKnownRemoteTextEditingValue) if (localValue == _receivedRemoteTextEditingValue)
return; return;
_lastKnownRemoteTextEditingValue = localValue;
_textInputConnection.setEditingState(localValue); _textInputConnection.setEditingState(localValue);
} }
...@@ -1414,7 +1420,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1414,7 +1420,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
} }
if (!_hasInputConnection) { if (!_hasInputConnection) {
final TextEditingValue localValue = _value; final TextEditingValue localValue = _value;
_lastKnownRemoteTextEditingValue = localValue; _lastFormattedUnmodifiedTextEditingValue = localValue;
_textInputConnection = TextInput.attach( _textInputConnection = TextInput.attach(
this, this,
TextInputConfiguration( TextInputConfiguration(
...@@ -1454,7 +1460,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1454,7 +1460,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
if (_hasInputConnection) { if (_hasInputConnection) {
_textInputConnection.close(); _textInputConnection.close();
_textInputConnection = null; _textInputConnection = null;
_lastKnownRemoteTextEditingValue = null; _lastFormattedUnmodifiedTextEditingValue = null;
_receivedRemoteTextEditingValue = null;
} }
} }
...@@ -1472,7 +1479,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1472,7 +1479,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
if (_hasInputConnection) { if (_hasInputConnection) {
_textInputConnection.connectionClosedReceived(); _textInputConnection.connectionClosedReceived();
_textInputConnection = null; _textInputConnection = null;
_lastKnownRemoteTextEditingValue = null; _lastFormattedUnmodifiedTextEditingValue = null;
_receivedRemoteTextEditingValue = null;
_finalizeEditing(true); _finalizeEditing(true);
} }
} }
...@@ -1616,17 +1624,21 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1616,17 +1624,21 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
} }
void _formatAndSetValue(TextEditingValue value) { void _formatAndSetValue(TextEditingValue value) {
// Check if the new value is the same as the current local value, or is the same
// as the post-formatting value of the previous pass.
final bool textChanged = _value?.text != value?.text; final bool textChanged = _value?.text != value?.text;
if (textChanged && widget.inputFormatters != null && widget.inputFormatters.isNotEmpty) { final bool isRepeat = value?.text == _lastFormattedUnmodifiedTextEditingValue?.text;
if (textChanged && !isRepeat && widget.inputFormatters != null && widget.inputFormatters.isNotEmpty) {
for (final TextInputFormatter formatter in widget.inputFormatters) for (final TextInputFormatter formatter in widget.inputFormatters)
value = formatter.formatEditUpdate(_value, value); value = formatter.formatEditUpdate(_value, value);
_value = value; _value = value;
_updateRemoteEditingValueIfNeeded(); _updateRemoteEditingValueIfNeeded();
} else { } else if (!isRepeat || !textChanged) {
_value = value; _value = value;
} }
if (textChanged && widget.onChanged != null) if (textChanged && widget.onChanged != null)
widget.onChanged(value.text); widget.onChanged(value.text);
_lastFormattedUnmodifiedTextEditingValue = _receivedRemoteTextEditingValue;
} }
void _onCursorColorTick() { void _onCursorColorTick() {
......
...@@ -4178,6 +4178,114 @@ void main() { ...@@ -4178,6 +4178,114 @@ void main() {
} }
expect(tester.testTextInput.editingState['text'], 'flutter is the best!...'); expect(tester.testTextInput.editingState['text'], 'flutter is the best!...');
}); });
testWidgets('updateEditingValue filters multiple calls from formatter', (WidgetTester tester) async {
final MockTextFormatter formatter = MockTextFormatter();
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(devicePixelRatio: 1.0),
child: Directionality(
textDirection: TextDirection.ltr,
child: FocusScope(
node: focusScopeNode,
autofocus: true,
child: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller,
focusNode: focusNode,
maxLines: 1, // Sets text keyboard implicitly.
style: textStyle,
cursorColor: cursorColor,
inputFormatters: <TextInputFormatter>[formatter],
),
),
),
),
);
await tester.tap(find.byType(EditableText));
await tester.showKeyboard(find.byType(EditableText));
controller.text = '';
await tester.idle();
final EditableTextState state =
tester.state<EditableTextState>(find.byType(EditableText));
expect(tester.testTextInput.editingState['text'], equals(''));
expect(state.wantKeepAlive, true);
state.updateEditingValue(const TextEditingValue(text: ''));
state.updateEditingValue(const TextEditingValue(text: 'a'));
state.updateEditingValue(const TextEditingValue(text: 'aa'));
state.updateEditingValue(const TextEditingValue(text: 'aaa'));
state.updateEditingValue(const TextEditingValue(text: 'aa'));
state.updateEditingValue(const TextEditingValue(text: 'aaa'));
state.updateEditingValue(const TextEditingValue(text: 'aaaa'));
state.updateEditingValue(const TextEditingValue(text: 'aa'));
state.updateEditingValue(const TextEditingValue(text: 'aaaaaaa'));
state.updateEditingValue(const TextEditingValue(text: 'aa'));
state.updateEditingValue(const TextEditingValue(text: 'aaaaaaaaa'));
state.updateEditingValue(const TextEditingValue(text: 'aaaaaaaaa')); // Skipped
const List<String> referenceLog = <String>[
'[1]: , a',
'[1]: normal aa',
'[2]: aa, aaa',
'[2]: normal aaaa',
'[3]: aaaa, aa',
'[3]: deleting a',
'[4]: a, aaa',
'[4]: normal aaaaaaaa',
'[5]: aaaaaaaa, aaaa',
'[5]: deleting aaa',
'[6]: aaa, aa',
'[6]: deleting aaaa',
'[7]: aaaa, aaaaaaa',
'[7]: normal aaaaaaaaaaaaaa',
'[8]: aaaaaaaaaaaaaa, aa',
'[8]: deleting aaaaaa',
'[9]: aaaaaa, aaaaaaaaa',
'[9]: normal aaaaaaaaaaaaaaaaaa',
];
expect(formatter.log, referenceLog);
});
}
class MockTextFormatter extends TextInputFormatter {
MockTextFormatter() : _counter = 0, log = <String>[];
int _counter;
List<String> log;
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
_counter++;
log.add('[$_counter]: ${oldValue.text}, ${newValue.text}');
TextEditingValue finalValue;
if (newValue.text.length < oldValue.text.length) {
finalValue = _handleTextDeletion(oldValue, newValue);
} else {
finalValue = _formatText(newValue);
}
return finalValue;
}
TextEditingValue _handleTextDeletion(
TextEditingValue oldValue, TextEditingValue newValue) {
final String result = 'a' * (_counter - 2);
log.add('[$_counter]: deleting $result');
return TextEditingValue(text: result);
}
TextEditingValue _formatText(TextEditingValue value) {
final String result = 'a' * _counter * 2;
log.add('[$_counter]: normal $result');
return TextEditingValue(text: result);
}
} }
class MockTextSelectionControls extends Mock implements TextSelectionControls { class MockTextSelectionControls extends Mock implements TextSelectionControls {
......
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