Revert "More EditableText docs (#66864)" (#68025)
This reverts commit daa6b2cc29b5ff1f166660f9e70fb38d6c0de6cc.
diff --git a/packages/flutter/lib/src/services/text_input.dart b/packages/flutter/lib/src/services/text_input.dart
index c98f5a2..b273db7 100644
--- a/packages/flutter/lib/src/services/text_input.dart
+++ b/packages/flutter/lib/src/services/text_input.dart
@@ -756,17 +756,7 @@
/// Gets the current text input.
TextEditingValue get textEditingValue;
- /// Indicates that the user has requested the delegate to replace its current
- /// text editing state with [value].
- ///
- /// The new [value] is treated as user input and thus may subject to input
- /// formatting.
- ///
- /// See also:
- ///
- /// * [EditableTextState.textEditingValue]: an implementation that applies
- /// additional pre-processing to the specified [value], before updating the
- /// text editing state.
+ /// Sets the current text input (replaces the whole line).
set textEditingValue(TextEditingValue value);
/// Hides the text selection toolbar.
@@ -794,7 +784,6 @@
/// See also:
///
/// * [TextInput.attach]
-/// * [EditableText], a [TextInputClient] implementation.
abstract class TextInputClient {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
@@ -816,9 +805,6 @@
AutofillScope? get currentAutofillScope;
/// Requests that this client update its editing state to the given value.
- ///
- /// The new [value] is treated as user input and thus may subject to input
- /// formatting.
void updateEditingValue(TextEditingValue value);
/// Requests that this client perform the given action.
@@ -846,10 +832,7 @@
///
/// See also:
///
-/// * [TextInput.attach], a method used to establish a [TextInputConnection]
-/// between the system's text input and a [TextInputClient].
-/// * [EditableText], a [TextInputClient] that connects to and interacts with
-/// the system's text input using a [TextInputConnection].
+/// * [TextInput.attach]
class TextInputConnection {
TextInputConnection._(this._client)
: assert(_client != null),
@@ -906,8 +889,7 @@
TextInput._instance._updateConfig(configuration);
}
- /// Requests that the text input control change its internal state to match
- /// the given state.
+ /// Requests that the text input control change its internal state to match the given state.
void setEditingState(TextEditingValue value) {
assert(attached);
TextInput._instance._setEditingState(value);
@@ -1060,57 +1042,9 @@
/// An low-level interface to the system's text input control.
///
-/// To start interacting with the system's text input control, call [attach] to
-/// establish a [TextInputConnection] between the system's text input control
-/// and a [TextInputClient]. The majority of commands available for
-/// interacting with the text input control reside in the returned
-/// [TextInputConnection]. The communication between the system text input and
-/// the [TextInputClient] is asynchronous.
-///
-/// The platform text input plugin (which represents the system's text input)
-/// and the [TextInputClient] usually maintain their own text editing states
-/// ([TextEditingValue]) separately. They must be kept in sync as long as the
-/// [TextInputClient] is connected. The following methods can be used to send
-/// [TextEditingValue] to update the other party, when either party's text
-/// editing states change:
-///
-/// * The [TextInput.attach] method allows a [TextInputClient] to establish a
-/// connection to the text input. An optional field in its `configuration`
-/// parameter can be used to specify an initial value for the platform text
-/// input plugin's [TextEditingValue].
-///
-/// * The [TextInputClient] sends its [TextEditingValue] to the platform text
-/// input plugin using [TextInputConnection.setEditingState].
-///
-/// * The platform text input plugin sends its [TextEditingValue] to the
-/// connected [TextInputClient] via a "TextInput.setEditingState" message.
-///
-/// * When autofill happens on a disconnected [TextInputClient], the platform
-/// text input plugin sends the [TextEditingValue] to the connected
-/// [TextInputClient]'s [AutofillScope], and the [AutofillScope] will further
-/// relay the value to the correct [TextInputClient].
-///
-/// When synchronizing the [TextEditingValue]s, the communication may get stuck
-/// in an infinite when both parties are trying to send their own update. To
-/// mitigate the problem, only [TextInputClient]s are allowed to alter the
-/// received [TextEditingValue]s while platform text input plugins are to accept
-/// the received [TextEditingValue]s unmodified. More specifically:
-///
-/// * When a [TextInputClient] receives a new [TextEditingValue] from the
-/// platform text input plugin, it's allowed to modify the value (for example,
-/// apply [TextInputFormatter]s). If it decides to do so, it must send the
-/// updated [TextEditingValue] back to the platform text input plugin to keep
-/// the [TextEditingValue]s in sync.
-///
-/// * When the platform text input plugin receives a new value from the
-/// connected [TextInputClient], it must accept the new value as-is, to avoid
-/// sending back an updated value.
-///
/// See also:
///
/// * [TextField], a widget in which the user may enter text.
-/// * [EditableText], a [TextInputClient] that connects to [TextInput] when it
-/// wants to take user input from the keyboard.
class TextInput {
TextInput._() {
_channel = SystemChannels.textInput;
diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart
index ddd12b3..97a8dd1 100644
--- a/packages/flutter/lib/src/widgets/editable_text.dart
+++ b/packages/flutter/lib/src/widgets/editable_text.dart
@@ -320,19 +320,6 @@
/// movement. This widget does not provide any focus management (e.g.,
/// tap-to-focus).
///
-/// ## Handling User Input
-///
-/// Currently the user may change the text this widget contains via keyboard or
-/// the text selection menu. When the user inserted or deleted text, you will be
-/// notified of the change and get a chance to modify the new text value:
-///
-/// * The [inputFormatters] will be first applied to the user input.
-///
-/// * The [controller]'s [TextEditingController.value] will be updated with the
-/// formatted result, and the [controller]'s listeners will be notified.
-///
-/// * The [onChanged] callback, if specified, will be called last.
-///
/// ## Input Actions
///
/// A [TextInputAction] can be provided to customize the appearance of the
@@ -1095,9 +1082,7 @@
/// {@template flutter.widgets.editableText.inputFormatters}
/// Optional input validation and formatting overrides.
///
- /// Formatters are run in the provided order when the text input changes. When
- /// this parameter changes, the new formatters will not be applied until the
- /// next time the user inserts or deletes text.
+ /// Formatters are run in the provided order when the text input changes.
/// {@endtemplate}
final List<TextInputFormatter>? inputFormatters;
@@ -1652,66 +1637,61 @@
_clipboardStatus?.removeListener(_onChangedClipboardStatus);
_clipboardStatus?.dispose();
super.dispose();
- assert(_batchEditDepth <= 0, 'unfinished batch edits: $_batchEditDepth');
}
// TextInputClient implementation:
- /// The last known [TextEditingValue] of the platform text input plugin.
- ///
- /// This value is updated when the platform text input plugin sends a new
- /// update via [updateEditingValue], or when [EditableText] calls
- /// [TextInputConnection.setEditingState] to overwrite the platform text input
- /// plugin's [TextEditingValue].
- ///
- /// Used in [_updateRemoteEditingValueIfNeeded] to determine whether the
- /// remote value is outdated and needs updating.
- TextEditingValue? _lastKnownRemoteTextEditingValue;
+ // _lastFormattedUnmodifiedTextEditingValue tracks the last value
+ // that the formatter ran on and is used to prevent double-formatting.
+ TextEditingValue? _lastFormattedUnmodifiedTextEditingValue;
+ // _lastFormattedValue tracks the last post-format value, so that it can be
+ // reused without rerunning the formatter when the input value is repeated.
+ TextEditingValue? _lastFormattedValue;
+ // _receivedRemoteTextEditingValue is the direct value last passed in
+ // updateEditingValue. This value does not get updated with the formatted
+ // version.
+ TextEditingValue? _receivedRemoteTextEditingValue;
@override
TextEditingValue get currentTextEditingValue => _value;
+ bool _updateEditingValueInProgress = false;
+
@override
void updateEditingValue(TextEditingValue value) {
- // This method handles text editing state updates from the platform text
- // input plugin. The [EditableText] may not have the focus or an open input
- // connection, as autofill can update a disconnected [EditableText].
-
+ _updateEditingValueInProgress = true;
// Since we still have to support keyboard select, this is the best place
// to disable text updating.
if (!_shouldCreateInputConnection) {
+ _updateEditingValueInProgress = false;
return;
}
-
if (widget.readOnly) {
// In the read-only case, we only care about selection changes, and reject
// everything else.
value = _value.copyWith(selection: value.selection);
}
- _lastKnownRemoteTextEditingValue = value;
+ _receivedRemoteTextEditingValue = value;
+ if (value.text != _value.text) {
+ hideToolbar();
+ _showCaretOnScreen();
+ _currentPromptRectRange = null;
+ if (widget.obscureText && value.text.length == _value.text.length + 1) {
+ _obscureShowCharTicksPending = _kObscureShowLatestCharCursorTicks;
+ _obscureLatestCharIndex = _value.selection.baseOffset;
+ }
+ }
if (value == _value) {
// This is possible, for example, when the numeric keyboard is input,
// the engine will notify twice for the same value.
// Track at https://github.com/flutter/flutter/issues/65811
+ _updateEditingValueInProgress = false;
return;
- }
-
- if (value.text == _value.text && value.composing == _value.composing) {
+ } else if (value.text == _value.text && value.composing == _value.composing && value.selection != _value.selection) {
// `selection` is the only change.
_handleSelectionChanged(value.selection, renderEditable, SelectionChangedCause.keyboard);
} else {
- hideToolbar();
- _currentPromptRectRange = null;
-
- if (_hasInputConnection) {
- _showCaretOnScreen();
- if (widget.obscureText && value.text.length == _value.text.length + 1) {
- _obscureShowCharTicksPending = _kObscureShowLatestCharCursorTicks;
- _obscureLatestCharIndex = _value.selection.baseOffset;
- }
- }
-
_formatAndSetValue(value);
}
@@ -1721,6 +1701,7 @@
_stopCursorTimer(resetCharTicks: false);
_startCursorTimer();
}
+ _updateEditingValueInProgress = false;
}
@override
@@ -1878,52 +1859,33 @@
}
// Invoke optional callback with the user's submitted content.
- try {
- widget.onSubmitted?.call(_value.text);
- } catch (exception, stack) {
- FlutterError.reportError(FlutterErrorDetails(
- exception: exception,
- stack: stack,
- library: 'widgets',
- context: ErrorDescription('while calling onSubmitted for $action'),
- ));
+ if (widget.onSubmitted != null) {
+ try {
+ widget.onSubmitted!(_value.text);
+ } catch (exception, stack) {
+ FlutterError.reportError(FlutterErrorDetails(
+ exception: exception,
+ stack: stack,
+ library: 'widgets',
+ context: ErrorDescription('while calling onSubmitted for $action'),
+ ));
+ }
}
}
- int _batchEditDepth = 0;
-
- /// Begins a new batch edit, within which new updates made to the text editing
- /// value will not be sent to the platform text input plugin.
- ///
- /// Batch edits nest. When the outmost batch edit finishes, [endBatchEdit]
- /// will attempt to send [currentTextEditingValue] to the text input plugin if
- /// it detected a change.
- void beginBatchEdit() {
- _batchEditDepth += 1;
- }
-
- /// Ends the current batch edit started by the last call to [beginBatchEdit],
- /// and send [currentTextEditingValue] to the text input plugin if needed.
- ///
- /// Throws an error in debug mode if this [EditableText] is not in a batch
- /// edit.
- void endBatchEdit() {
- _batchEditDepth -= 1;
- assert(
- _batchEditDepth >= 0,
- 'Unbalanced call to endBatchEdit: beginBatchEdit must be called first.',
- );
- _updateRemoteEditingValueIfNeeded();
- }
-
void _updateRemoteEditingValueIfNeeded() {
- if (_batchEditDepth > 0 || !_hasInputConnection)
+ if (!_hasInputConnection)
return;
final TextEditingValue localValue = _value;
- if (localValue == _lastKnownRemoteTextEditingValue)
+ // We should not update back the value notified by the remote(engine) in reverse, this is redundant.
+ // Unless we modify this value for some reason during processing, such as `TextInputFormatter`.
+ if (_updateEditingValueInProgress && localValue == _receivedRemoteTextEditingValue)
return;
+ // In other cases, as long as the value of the [widget.controller.value] is modified,
+ // `setEditingState` should be called as we do not want to skip sending real changes
+ // to the engine.
+ // Also see https://github.com/flutter/flutter/issues/65059#issuecomment-690254379
_textInputConnection!.setEditingState(localValue);
- _lastKnownRemoteTextEditingValue = localValue;
}
TextEditingValue get _value => widget.controller.value;
@@ -1987,7 +1949,7 @@
return RevealedOffset(rect: rect.shift(unitOffset * offsetDelta), offset: targetOffset);
}
- bool get _hasInputConnection => _textInputConnection?.attached ?? false;
+ bool get _hasInputConnection => _textInputConnection != null && _textInputConnection!.attached;
bool get _needsAutofill => widget.autofillHints?.isNotEmpty ?? false;
bool get _shouldBeInAutofillContext => _needsAutofill && currentAutofillScope != null;
@@ -1997,6 +1959,7 @@
}
if (!_hasInputConnection) {
final TextEditingValue localValue = _value;
+ _lastFormattedUnmodifiedTextEditingValue = localValue;
// When _needsAutofill == true && currentAutofillScope == null, autofill
// is allowed but saving the user input from the text field is
@@ -2037,7 +2000,8 @@
if (_hasInputConnection) {
_textInputConnection!.close();
_textInputConnection = null;
- _lastKnownRemoteTextEditingValue = null;
+ _lastFormattedUnmodifiedTextEditingValue = null;
+ _receivedRemoteTextEditingValue = null;
}
}
@@ -2055,7 +2019,8 @@
if (_hasInputConnection) {
_textInputConnection!.connectionClosedReceived();
_textInputConnection = null;
- _lastKnownRemoteTextEditingValue = null;
+ _lastFormattedUnmodifiedTextEditingValue = null;
+ _receivedRemoteTextEditingValue = null;
_finalizeEditing(TextInputAction.done, shouldUnfocus: true);
}
}
@@ -2119,15 +2084,17 @@
);
_selectionOverlay!.handlesVisible = widget.showSelectionHandles;
_selectionOverlay!.showHandles();
- try {
- widget.onSelectionChanged?.call(selection, cause);
- } catch (exception, stack) {
- FlutterError.reportError(FlutterErrorDetails(
- exception: exception,
- stack: stack,
- library: 'widgets',
- context: ErrorDescription('while calling onSelectionChanged for $cause'),
- ));
+ if (widget.onSelectionChanged != null) {
+ try {
+ widget.onSelectionChanged!(selection, cause);
+ } catch (exception, stack) {
+ FlutterError.reportError(FlutterErrorDetails(
+ exception: exception,
+ stack: stack,
+ library: 'widgets',
+ context: ErrorDescription('while calling onSelectionChanged for $cause'),
+ ));
+ }
}
}
}
@@ -2215,35 +2182,53 @@
_lastBottomViewInset = WidgetsBinding.instance!.window.viewInsets.bottom;
}
- late final _WhitespaceDirectionalityFormatter _whitespaceFormatter = _WhitespaceDirectionalityFormatter(textDirection: _textDirection);
+ _WhitespaceDirectionalityFormatter? _whitespaceFormatter;
void _formatAndSetValue(TextEditingValue value) {
+ _whitespaceFormatter ??= _WhitespaceDirectionalityFormatter(textDirection: _textDirection);
+
// Check if the new value is the same as the current local value, or is the same
// as the pre-formatting value of the previous pass (repeat call).
- final bool textChanged = _value.text != value.text || _value.composing != value.composing;
+ final bool textChanged = _value.text != value.text;
+ final bool isRepeat = value == _lastFormattedUnmodifiedTextEditingValue;
- if (textChanged) {
+ // There's no need to format when starting to compose or when continuing
+ // an existing composition.
+ final bool isComposing = value.composing.isValid;
+ final bool isPreviouslyComposing = _lastFormattedUnmodifiedTextEditingValue?.composing.isValid ?? false;
+
+ if ((textChanged || (!isComposing && isPreviouslyComposing)) &&
+ widget.inputFormatters != null &&
+ widget.inputFormatters!.isNotEmpty) {
// Only format when the text has changed and there are available formatters.
// Pass through the formatter regardless of repeat status if the input value is
// different than the stored value.
- value = widget.inputFormatters?.fold<TextEditingValue>(
- value,
- (TextEditingValue newValue, TextInputFormatter formatter) => formatter.formatEditUpdate(_value, newValue),
- ) ?? value;
-
+ for (final TextInputFormatter formatter in widget.inputFormatters!) {
+ value = formatter.formatEditUpdate(_value, value);
+ }
// Always pass the text through the whitespace directionality formatter to
// maintain expected behavior with carets on trailing whitespace.
- value = _whitespaceFormatter.formatEditUpdate(_value, value);
+ value = _whitespaceFormatter!.formatEditUpdate(_value, value);
+ _lastFormattedValue = value;
}
- // Put all optional user callback invocations in a batch edit to prevent
- // sending multiple `TextInput.updateEditingValue` messages.
- beginBatchEdit();
+ if (value == _value) {
+ // If the value was modified by the formatter, the remote should be notified to keep in sync,
+ // if not modified, it will short-circuit.
+ _updateRemoteEditingValueIfNeeded();
+ } else {
+ // Setting _value here ensures the selection and composing region info is passed.
+ _value = value;
+ }
- _value = value;
- if (textChanged) {
+ // Use the last formatted value when an identical repeat pass is detected.
+ if (isRepeat && textChanged && _lastFormattedValue != null) {
+ _value = _lastFormattedValue!;
+ }
+
+ if (textChanged && widget.onChanged != null) {
try {
- widget.onChanged?.call(value.text);
+ widget.onChanged!(value.text);
} catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetails(
exception: exception,
@@ -2253,8 +2238,7 @@
));
}
}
-
- endBatchEdit();
+ _lastFormattedUnmodifiedTextEditingValue = _receivedRemoteTextEditingValue;
}
void _onCursorColorTick() {
diff --git a/packages/flutter/test/widgets/editable_text_test.dart b/packages/flutter/test/widgets/editable_text_test.dart
index 7491fbc..01f8942 100644
--- a/packages/flutter/test/widgets/editable_text_test.dart
+++ b/packages/flutter/test/widgets/editable_text_test.dart
@@ -4736,246 +4736,6 @@
);
});
- group('batch editing', () {
- final TextEditingController controller = TextEditingController(text: testText);
- final EditableText editableText = EditableText(
- showSelectionHandles: true,
- maxLines: 2,
- controller: controller,
- focusNode: FocusNode(),
- cursorColor: Colors.red,
- backgroundCursorColor: Colors.blue,
- style: Typography.material2018(platform: TargetPlatform.android).black.subtitle1.copyWith(fontFamily: 'Roboto'),
- keyboardType: TextInputType.text,
- );
-
- final Widget widget = MediaQuery(
- data: const MediaQueryData(),
- child: Directionality(
- textDirection: TextDirection.ltr,
- child: editableText,
- ),
- );
-
- testWidgets('batch editing works', (WidgetTester tester) async {
- await tester.pumpWidget(widget);
-
- // Connect.
- await tester.showKeyboard(find.byType(EditableText));
-
- final EditableTextState state = tester.state<EditableTextState>(find.byWidget(editableText));
- state.updateEditingValue(const TextEditingValue(text: 'remote value'));
- tester.testTextInput.log.clear();
-
- state.beginBatchEdit();
-
- controller.text = 'new change 1';
- expect(state.currentTextEditingValue.text, 'new change 1');
- expect(tester.testTextInput.log, isEmpty);
-
- // Nesting.
- state.beginBatchEdit();
- controller.text = 'new change 2';
- expect(state.currentTextEditingValue.text, 'new change 2');
- expect(tester.testTextInput.log, isEmpty);
-
- // End the innermost batch edit. Not yet.
- state.endBatchEdit();
- expect(tester.testTextInput.log, isEmpty);
-
- controller.text = 'new change 3';
- expect(state.currentTextEditingValue.text, 'new change 3');
- expect(tester.testTextInput.log, isEmpty);
-
- // Finish the outermost batch edit.
- state.endBatchEdit();
- expect(tester.testTextInput.log, hasLength(1));
- expect(
- tester.testTextInput.log,
- contains(matchesMethodCall('TextInput.setEditingState', args: containsPair('text', 'new change 3'))),
- );
- });
-
- testWidgets('batch edits need to be nested properly', (WidgetTester tester) async {
- await tester.pumpWidget(widget);
-
- // Connect.
- await tester.showKeyboard(find.byType(EditableText));
-
- final EditableTextState state = tester.state<EditableTextState>(find.byWidget(editableText));
- state.updateEditingValue(const TextEditingValue(text: 'remote value'));
- tester.testTextInput.log.clear();
-
- String errorString;
- try {
- state.endBatchEdit();
- } catch (e) {
- errorString = e.toString();
- }
-
- expect(errorString, contains('Unbalanced call to endBatchEdit'));
- });
-
- testWidgets('catch unfinished batch edits on disposal', (WidgetTester tester) async {
- await tester.pumpWidget(widget);
-
- // Connect.
- await tester.showKeyboard(find.byType(EditableText));
-
- final EditableTextState state = tester.state<EditableTextState>(find.byWidget(editableText));
- state.updateEditingValue(const TextEditingValue(text: 'remote value'));
- tester.testTextInput.log.clear();
-
- state.beginBatchEdit();
- expect(tester.takeException(), isNull);
-
- await tester.pumpWidget(Container());
- expect(tester.takeException(), isNotNull);
- });
- });
-
- group('EditableText does not send editing values more than once', () {
- final TextEditingController controller = TextEditingController(text: testText);
- final EditableText editableText = EditableText(
- showSelectionHandles: true,
- maxLines: 2,
- controller: controller,
- focusNode: FocusNode(),
- cursorColor: Colors.red,
- backgroundCursorColor: Colors.blue,
- style: Typography.material2018(platform: TargetPlatform.android).black.subtitle1.copyWith(fontFamily: 'Roboto'),
- keyboardType: TextInputType.text,
- inputFormatters: <TextInputFormatter>[LengthLimitingTextInputFormatter(6)],
- onChanged: (String s) => controller.text += ' onChanged',
- );
-
- final Widget widget = MediaQuery(
- data: const MediaQueryData(),
- child: Directionality(
- textDirection: TextDirection.ltr,
- child: editableText,
- ),
- );
-
- controller.addListener(() {
- if (!controller.text.endsWith('listener'))
- controller.text += ' listener';
- });
-
- testWidgets('input from text input plugin', (WidgetTester tester) async {
- await tester.pumpWidget(widget);
-
- // Connect.
- await tester.showKeyboard(find.byType(EditableText));
- tester.testTextInput.log.clear();
-
- final EditableTextState state = tester.state<EditableTextState>(find.byWidget(editableText));
- state.updateEditingValue(const TextEditingValue(text: 'remoteremoteremote'));
-
- // Apply in order: length formatter -> listener -> onChanged -> listener.
- expect(controller.text, 'remote listener onChanged listener');
- final List<TextEditingValue> updates = tester.testTextInput.log
- .where((MethodCall call) => call.method == 'TextInput.setEditingState')
- .map((MethodCall call) => TextEditingValue.fromJSON(call.arguments as Map<String, dynamic>))
- .toList(growable: false);
-
- expect(updates, const <TextEditingValue>[TextEditingValue(text: 'remote listener onChanged listener')]);
-
- tester.testTextInput.log.clear();
-
- // If by coincidence the text input plugin sends the same value back,
- // do nothing.
- state.updateEditingValue(const TextEditingValue(text: 'remote listener onChanged listener'));
- expect(controller.text, 'remote listener onChanged listener');
- expect(tester.testTextInput.log, isEmpty);
- });
-
- testWidgets('input from text selection menu', (WidgetTester tester) async {
- await tester.pumpWidget(widget);
-
- // Connect.
- await tester.showKeyboard(find.byType(EditableText));
- tester.testTextInput.log.clear();
-
- final EditableTextState state = tester.state<EditableTextState>(find.byWidget(editableText));
- state.textEditingValue = const TextEditingValue(text: 'remoteremoteremote');
-
- // Apply in order: length formatter -> listener -> onChanged -> listener.
- expect(controller.text, 'remote listener onChanged listener');
- final List<TextEditingValue> updates = tester.testTextInput.log
- .where((MethodCall call) => call.method == 'TextInput.setEditingState')
- .map((MethodCall call) => TextEditingValue.fromJSON(call.arguments as Map<String, dynamic>))
- .toList(growable: false);
-
- expect(updates, const <TextEditingValue>[TextEditingValue(text: 'remote listener onChanged listener')]);
-
- tester.testTextInput.log.clear();
- });
-
- testWidgets('input from controller', (WidgetTester tester) async {
- await tester.pumpWidget(widget);
-
- // Connect.
- await tester.showKeyboard(find.byType(EditableText));
- tester.testTextInput.log.clear();
-
- controller.text = 'remoteremoteremote';
- final List<TextEditingValue> updates = tester.testTextInput.log
- .where((MethodCall call) => call.method == 'TextInput.setEditingState')
- .map((MethodCall call) => TextEditingValue.fromJSON(call.arguments as Map<String, dynamic>))
- .toList(growable: false);
-
- expect(updates, const <TextEditingValue>[TextEditingValue(text: 'remoteremoteremote listener')]);
- });
-
- testWidgets('input from changing controller', (WidgetTester tester) async {
- final TextEditingController controller = TextEditingController(text: testText);
- Widget build({ TextEditingController textEditingController }) {
- return MediaQuery(
- data: const MediaQueryData(),
- child: Directionality(
- textDirection: TextDirection.ltr,
- child: EditableText(
- showSelectionHandles: true,
- maxLines: 2,
- controller: textEditingController ?? controller,
- focusNode: FocusNode(),
- cursorColor: Colors.red,
- backgroundCursorColor: Colors.blue,
- style: Typography.material2018(platform: TargetPlatform.android).black.subtitle1.copyWith(fontFamily: 'Roboto'),
- keyboardType: TextInputType.text,
- inputFormatters: <TextInputFormatter>[LengthLimitingTextInputFormatter(6)],
- ),
- ),
- );
- }
-
- await tester.pumpWidget(build());
-
- // Connect.
- await tester.showKeyboard(find.byType(EditableText));
- tester.testTextInput.log.clear();
- await tester.pumpWidget(build(textEditingController: TextEditingController(text: 'new text')));
-
- List<TextEditingValue> updates = tester.testTextInput.log
- .where((MethodCall call) => call.method == 'TextInput.setEditingState')
- .map((MethodCall call) => TextEditingValue.fromJSON(call.arguments as Map<String, dynamic>))
- .toList(growable: false);
-
- expect(updates, const <TextEditingValue>[TextEditingValue(text: 'new text')]);
-
- tester.testTextInput.log.clear();
- await tester.pumpWidget(build(textEditingController: TextEditingController(text: 'new new text')));
-
- updates = tester.testTextInput.log
- .where((MethodCall call) => call.method == 'TextInput.setEditingState')
- .map((MethodCall call) => TextEditingValue.fromJSON(call.arguments as Map<String, dynamic>))
- .toList(growable: false);
-
- expect(updates, const <TextEditingValue>[TextEditingValue(text: 'new new text')]);
- });
- });
-
testWidgets('input imm channel calls are ordered correctly', (WidgetTester tester) async {
const String testText = 'flutter is the best!';
final TextEditingController controller = TextEditingController(text: testText);
@@ -5475,12 +5235,12 @@
expect(formatter.formatCallCount, 3);
state.updateEditingValue(const TextEditingValue(text: '0123', selection: TextSelection.collapsed(offset: 2))); // No text change, does not format
expect(formatter.formatCallCount, 3);
- state.updateEditingValue(const TextEditingValue(text: '0123', selection: TextSelection.collapsed(offset: 2), composing: TextRange(start: 1, end: 2))); // Composing change triggers reformat
- expect(formatter.formatCallCount, 4);
+ state.updateEditingValue(const TextEditingValue(text: '0123', selection: TextSelection.collapsed(offset: 2), composing: TextRange(start: 1, end: 2))); // Composing change does not reformat
+ expect(formatter.formatCallCount, 3);
expect(formatter.lastOldValue.composing, const TextRange(start: -1, end: -1));
- expect(formatter.lastNewValue.composing, const TextRange(start: 1, end: 2)); // The new composing was registered in formatter.
+ expect(formatter.lastNewValue.composing, const TextRange(start: -1, end: -1)); // Since did not format, the new composing was not registered in formatter.
state.updateEditingValue(const TextEditingValue(text: '01234', selection: TextSelection.collapsed(offset: 2))); // Formats, with oldValue containing composing region.
- expect(formatter.formatCallCount, 5);
+ expect(formatter.formatCallCount, 4);
expect(formatter.lastOldValue.composing, const TextRange(start: 1, end: 2));
expect(formatter.lastNewValue.composing, const TextRange(start: -1, end: -1));
@@ -5491,10 +5251,8 @@
'[2]: normal aaaa',
'[3]: 012, 0123',
'[3]: normal aaaaaa',
- '[4]: 0123, 0123',
- '[4]: normal aaaaaaaa',
- '[5]: 0123, 01234',
- '[5]: normal aaaaaaaaaa',
+ '[4]: 0123, 01234',
+ '[4]: normal aaaaaaaa'
];
expect(formatter.log, referenceLog);