-
Notifications
You must be signed in to change notification settings - Fork 29.8k
Description
-
_TextEditingHistoryStateregisters a listener to be notified when aTextEditingControllervalue is updated in order to call_TextEditingHistoryState._push(which might add an entry in the history state). -
After an undo/redo (
_TextEditingHistoryState._undoor_redo),_TextEditingHistoryStatemight call the_TextEditingHistory.onTriggeredcallback. -
Current
_TextEditingHistory.onTriggeredimplementation callsEditableTextState.userUpdateTextEditingValuewhich will change theTextEditingControllervalue and leads to_TextEditingHistoryStatebeing notified and_TextEditingHistoryState._push.
This reentrant call brings complexity when debugging text editing history issues and can lead to complex issues like the following existing test failing when adding a delay between two undos:
Code sample (existing test updated with a delay between two calls to undo).
testWidgets('does save composing changes on Android', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
final FocusNode focusNode = FocusNode();
await tester.pumpWidget(
MaterialApp(
home: EditableText(
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: Colors.blue,
backgroundCursorColor: Colors.grey,
cursorOpacityAnimates: true,
autofillHints: null,
),
),
);
expect(
controller.value,
TextEditingValue.empty,
);
focusNode.requestFocus();
expect(
controller.value,
TextEditingValue.empty,
);
await tester.pump();
expect(
controller.value,
const TextEditingValue(
selection: TextSelection.collapsed(offset: 0),
),
);
// Wait for the throttling.
await tester.pump(const Duration(milliseconds: 500));
// Enter some regular non-composing text that is undoable.
await tester.enterText(find.byType(EditableText), '1 ');
expect(
controller.value,
const TextEditingValue(
text: '1 ',
selection: TextSelection.collapsed(offset: 2),
),
);
await tester.pump(const Duration(milliseconds: 500));
// Enter some composing text.
final EditableTextState state =
tester.state<EditableTextState>(find.byType(EditableText));
state.userUpdateTextEditingValue(
const TextEditingValue(
text: '1 ni',
composing: TextRange(start: 2, end: 4),
selection: TextSelection.collapsed(offset: 4),
),
SelectionChangedCause.keyboard,
);
await tester.pump(const Duration(milliseconds: 500));
// Enter some more composing text.
state.userUpdateTextEditingValue(
const TextEditingValue(
text: '1 nihao',
composing: TextRange(start: 2, end: 7),
selection: TextSelection.collapsed(offset: 7),
),
SelectionChangedCause.keyboard,
);
await tester.pump(const Duration(milliseconds: 500));
// Commit the composing text.
state.userUpdateTextEditingValue(
const TextEditingValue(
text: '1 你好',
selection: TextSelection.collapsed(offset: 4),
),
SelectionChangedCause.keyboard,
);
await tester.pump(const Duration(milliseconds: 500));
expect(
controller.value,
const TextEditingValue(
text: '1 你好',
selection: TextSelection.collapsed(offset: 4),
),
);
// Undo/redo includes the composing changes.
await sendUndo(tester);
expect(
controller.value,
const TextEditingValue(
text: '1 nihao',
selection: TextSelection.collapsed(offset: 7),
),
);
// Waiting before two undos should have no effect.
await tester.pump(const Duration(milliseconds: 500));
await sendUndo(tester);
expect(
controller.value,
const TextEditingValue(
text: '1 ni',
selection: TextSelection.collapsed(offset: 4),
),
);
await sendUndo(tester);
expect(
controller.value,
const TextEditingValue(
text: '1 ',
selection: TextSelection.collapsed(offset: 2),
),
);
await sendRedo(tester);
expect(
controller.value,
const TextEditingValue(
text: '1 ni',
selection: TextSelection.collapsed(offset: 4),
),
);
await sendRedo(tester);
expect(
controller.value,
const TextEditingValue(
text: '1 nihao',
selection: TextSelection.collapsed(offset: 7),
),
);
await sendRedo(tester);
expect(
controller.value,
const TextEditingValue(
text: '1 你好',
selection: TextSelection.collapsed(offset: 4),
),
);
await sendRedo(tester);
expect(
controller.value,
const TextEditingValue(
text: '1 你好',
selection: TextSelection.collapsed(offset: 4),
),
);
await sendUndo(tester);
expect(
controller.value,
const TextEditingValue(
text: '1 nihao',
selection: TextSelection.collapsed(offset: 7),
),
);
await sendUndo(tester);
expect(
controller.value,
const TextEditingValue(
text: '1 ni',
selection: TextSelection.collapsed(offset: 4),
),
);
await sendUndo(tester);
expect(
controller.value,
const TextEditingValue(
text: '1 ',
selection: TextSelection.collapsed(offset: 2),
),
);
await sendUndo(tester);
expect(
controller.value,
const TextEditingValue(
selection: TextSelection.collapsed(offset: 0),
),
);
await sendUndo(tester);
expect(
controller.value,
const TextEditingValue(
selection: TextSelection.collapsed(offset: 0),
),
);
await sendRedo(tester);
expect(
controller.value,
const TextEditingValue(
text: '1 ',
selection: TextSelection.collapsed(offset: 2),
),
);
await sendRedo(tester);
expect(
controller.value,
const TextEditingValue(
text: '1 ni',
selection: TextSelection.collapsed(offset: 4),
),
);
await sendRedo(tester);
expect(
controller.value,
const TextEditingValue(
text: '1 nihao',
selection: TextSelection.collapsed(offset: 7),
),
);
await sendRedo(tester);
expect(
controller.value,
const TextEditingValue(
text: '1 你好',
selection: TextSelection.collapsed(offset: 4),
),
);
// On web, these keyboard shortcuts are handled by the browser.
}, variant: TargetPlatformVariant.only(TargetPlatform.android), skip: kIsWeb); // [intended]
Expected result:
The test should succeed.
Actual result:
The test fails (because the first undo leads to a change in the history).
Preventing history insertion during an undo/redo will be a first step before fixing #120194 and making progress on #99186.
Metadata
Metadata
Assignees
Labels
Type
Projects
Status