From 4220e00fff07be405b32c06d6b81f3ed35b321c3 Mon Sep 17 00:00:00 2001 From: LongCatIsLooong <31859944+LongCatIsLooong@users.noreply.github.com> Date: Tue, 29 Dec 2020 11:39:04 -0800 Subject: [PATCH] Revert "squash commits (#68166)" (#73067) --- .../lib/src/services/text_formatter.dart | 225 ++--------------- .../lib/src/widgets/editable_text.dart | 148 ++--------- .../test/services/text_formatter_test.dart | 231 ------------------ .../editable_text_didUpdateWidget_test.dart | 149 ----------- 4 files changed, 28 insertions(+), 725 deletions(-) delete mode 100644 packages/flutter/test/widgets/editable_text_didUpdateWidget_test.dart diff --git a/packages/flutter/lib/src/services/text_formatter.dart b/packages/flutter/lib/src/services/text_formatter.dart index e739a4d857..c10cc3a699 100644 --- a/packages/flutter/lib/src/services/text_formatter.dart +++ b/packages/flutter/lib/src/services/text_formatter.dart @@ -11,23 +11,6 @@ import 'package:flutter/foundation.dart'; import 'text_editing.dart'; import 'text_input.dart'; -// Examples can assume: -// late int maxLength; - -/// Function signature expected for creating custom [TextInputFormatter] -/// shorthands via [TextInputFormatter.withFunction]. -typedef TextInputFormatFunction = TextEditingValue Function( - TextEditingValue oldValue, - TextEditingValue newValue, -); - -/// Function signature for creating a custom -/// [CompositeTextInputFormatter.shouldReformat] implementation. -typedef ShouldReformatPredicate = bool Function( - TextInputFormatter oldFormatter, - CompositeTextInputFormatter newFormatter, -); - /// {@template flutter.services.textFormatter.maxLengthEnforcement} /// ### [MaxLengthEnforcement.enforced] versus /// [MaxLengthEnforcement.truncateAfterCompositionEnds] @@ -74,36 +57,16 @@ enum MaxLengthEnforcement { /// A [TextInputFormatter] can be optionally injected into an [EditableText] /// to provide as-you-type validation and formatting of the text being edited. /// -/// An [EditableText] formats its [TextEditingValue] when the user changes the -/// text, or when its [EditableText.inputFormatters] parameter changes. -/// [EditableText] may repetitively apply the same formatter against the input -/// text, therefore a formatter generally should not further modify a -/// [TextEditingValue] if the value has already been formatted by the same -/// formatter. -/// -/// See also the [FilteringTextInputFormatter], a subclass that removes -/// characters that the user tries to enter if they do, or do not, match a given -/// pattern (as applicable). -/// -/// ## Writing a Custom [TextInputFormatter]. -/// -/// To create custom formatters, extend the [TextInputFormatter] class. -/// Generally, text modification should only be applied when text is being -/// committed by the IME and not on text under composition (i.e., only when +/// Text modification should only be applied when text is being committed by the +/// IME and not on text under composition (i.e., only when /// [TextEditingValue.composing] is collapsed). /// -/// It is often eaiser to achieve the desired effects by combining -/// [TextInputFormatter]s, as opposed to creating a dedicated -/// [TextInputFormatter] from the ground up. See [EditableText.inputFormatters] -/// for an example that implements an idempotent US telephone number formatter -/// using composition. +/// See also the [FilteringTextInputFormatter], a subclass that +/// removes characters that the user tries to enter if they do, or do +/// not, match a given pattern (as applicable). /// -/// If your input formatter is expensive to run, or the document itself is -/// expensive to format, consider overriding [shouldReformat] to avoid unnessary -/// reformats when the [EditableText] widget rebuilds. If you wish to change the -/// [shouldReformat] strategy used by an existing formatter, consider wrapping -/// it in a [CompositeTextInputFormatter] and providing it with the desired -/// reformat strategy in [CompositeTextInputFormatter.shouldReformatPredicate]. +/// To create custom formatters, extend the [TextInputFormatter] class and +/// implement the [formatEditUpdate] method. /// /// ## Handling emojis and other complex characters /// {@macro flutter.widgets.EditableText.onChanged} @@ -114,11 +77,7 @@ enum MaxLengthEnforcement { /// * [FilteringTextInputFormatter], a provided formatter for filtering /// characters. abstract class TextInputFormatter { - /// Creates a new [TextInputFormatter]. - const TextInputFormatter(); - - /// Called when text is being typed or cut/copy/pasted in the [EditableText] - /// by the user. + /// Called when text is being typed or cut/copy/pasted in the [EditableText]. /// /// You can override the resulting text based on the previous text value and /// the incoming new text value. @@ -137,145 +96,14 @@ abstract class TextInputFormatter { ) { return _SimpleTextInputFormatter(formatFunction); } - - /// Whether this [TextInputFormatter] can replace another [TextInputFormatter] - /// without triggering a reformat. - /// - /// This method is called by the associated [EditableText] when it rebuilds, - /// to determine whether it can avoid calling [format]. See also - /// [LengthLimitingTextInputFormatter.shouldReformat] for an example that - /// skips reformatting whenever possible. - /// - /// An easy way to determine whether [oldFormatter] can be safely replaced - /// without having to rerun this [TextInputFormatter], is to manually apply - /// [format] to every possible return value of [oldFormatter]'s [format]. If - /// none of the return values changes, it's always safe to return false. - /// - /// The default implementation always returns true. - bool shouldReformat(TextInputFormatter oldFormatter) => true; - - /// Called by [EditableText] when this formatter is added to its - /// [EditableText.inputFormatters]. - /// - /// [EditableText] may repetitively apply this method to the same input text, - /// thus the implementation of this method should not further modify a - /// [TextEditingValue] if the value has already been formatted by the same - /// formatter (by this method or [formatEditUpdate]). - /// - /// If the formatting operation is expensive, try avoid unnecessary [format] - /// calls by returning `false` in [shouldReformat] as much as possible. - TextEditingValue format(TextEditingValue value) => formatEditUpdate(value, value); } -/// A [TextInputFormatter] that composes one or more child [TextInputFormatter]s. -/// -/// Applying this [CompositeTextInputFormatter] is equivalent to applying all -/// its child [TextInputFormatter]s in the given order. -/// -/// Aside from combining the effects of multiple [TextInputFormatter]s, -/// [CompositeTextInputFormatter] can also be used to create an ad-hoc formatter -/// with a different reformat strategy, without subclassing. -/// -/// {@tool snippet} -/// -/// The following code creates a [LengthLimitingTextInputFormatter] with a -/// varying `maxLength`, but when the `TextField` rebuilds with a smaller -/// `maxLength` value, the new character limit won't be enforced until the user -/// changes the context of the `TextField`. -/// -/// ```dart -/// TextField( -/// inputFormatters: [ -/// CompositeTextInputFormatter( -/// [LengthLimitingTextInputFormatter(maxLength)], -/// shouldReformatPredicate: CompositeTextInputFormatter.neverReformat, -/// ) -/// ] -/// ) -/// -/// ``` -/// {@end-tool} -class CompositeTextInputFormatter implements TextInputFormatter { - /// Creates a [CompositeTextInputFormatter] with a list of child `formatters` - /// and a reformat strategy. - const CompositeTextInputFormatter(this.formatters, { - this.shouldReformatPredicate = anyChildNeedsReformat, - }) : assert(formatters != null), - assert(formatters.length > 0), - assert(shouldReformatPredicate != null); - - /// Only skip reformatting if the [oldFormatter] is also a - /// [CompositeTextInputFormatter] and none of the child input formatters - /// requires reformatting. - /// - /// This is the default [shouldReformat] strategy employed by - /// [CompositeTextInputFormatter]. - static bool anyChildNeedsReformat(TextInputFormatter oldFormatter, CompositeTextInputFormatter newFormatter) { - if (identical(oldFormatter, newFormatter)) - return false; - - if (oldFormatter is! CompositeTextInputFormatter - || newFormatter.formatters.length != oldFormatter.formatters.length) { - return true; - } - - final Iterator newChild = newFormatter.formatters.iterator; - final Iterator oldChild = oldFormatter.formatters.iterator; - while(newChild.moveNext() && oldChild.moveNext()) { - if (newChild.current.shouldReformat(oldChild.current)) - return true; - } - return false; - } - - /// A [ShouldReformatPredicate] that indicates this [CompositeTextInputFormatter] - /// should never perform reformat when replacing another [TextInputFormatter]. - static bool neverReformat(TextInputFormatter oldFormatter, CompositeTextInputFormatter newFormatter) => false; - - /// A [ShouldReformatPredicate] that indicates this [CompositeTextInputFormatter] - /// should always reformat when replacing another [TextInputFormatter]. - static bool alwaysReformat(TextInputFormatter oldFormatter, CompositeTextInputFormatter newFormatter) => true; - - /// The list of child formatters that will be run in the provided order. - /// - /// Must not be null or empty. - final Iterable formatters; - - /// The [shouldReformat] strategy this [CompositeTextInputFormatter] employs. - /// - /// This class provides 3 predefined reformat strategies: - /// * [neverReformat]: the resulting [CompositeTextInputFormatter] never - /// reformats when the [EditableText] it is associated with rebuilds. - /// * [alwaysReformat]: the resulting [CompositeTextInputFormatter] always - /// reformats the [TextEditingValue] when its [EditableText] rebuilds. - /// * [anyChildNeedsReformat]: the resulting [CompositeTextInputFormatter] - /// reformats the [TextEditingValue] when its [EditableText] rebuilds, - /// unless the old formatter is also a [CompositeTextInputFormatter], has - /// the same number of child formatters, and none of the new child input - /// formatters requests reformatting. - /// - /// Defaults to [anyChildNeedsReformat]. - final ShouldReformatPredicate shouldReformatPredicate; - - @override - TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) { - return formatters.fold( - oldValue, - (TextEditingValue newValue, TextInputFormatter formatter) => formatter.formatEditUpdate(oldValue, newValue), - ); - } - - @override - bool shouldReformat(TextInputFormatter oldFormatter) => shouldReformatPredicate(oldFormatter, this); - - @override - TextEditingValue format(TextEditingValue value) { - return formatters.fold( - value, - (TextEditingValue newValue, TextInputFormatter formatter) => formatter.format(value), - ); - } -} +/// Function signature expected for creating custom [TextInputFormatter] +/// shorthands via [TextInputFormatter.withFunction]. +typedef TextInputFormatFunction = TextEditingValue Function( + TextEditingValue oldValue, + TextEditingValue newValue, +); /// Wiring for [TextInputFormatter.withFunction]. class _SimpleTextInputFormatter extends TextInputFormatter { @@ -452,14 +280,6 @@ class FilteringTextInputFormatter extends TextInputFormatter { /// A [TextInputFormatter] that takes in digits `[0-9]` only. static final TextInputFormatter digitsOnly = FilteringTextInputFormatter.allow(RegExp(r'[0-9]')); - - @override - bool shouldReformat(TextInputFormatter oldFormatter) { - return oldFormatter is! FilteringTextInputFormatter - || allow != oldFormatter.allow - || filterPattern != oldFormatter.filterPattern - || replacementString != oldFormatter.replacementString; - } } /// Old name for [FilteringTextInputFormatter.deny]. @@ -706,23 +526,6 @@ class LengthLimitingTextInputFormatter extends TextInputFormatter { return truncate(newValue, maxLength); } } - - @override - bool shouldReformat(TextInputFormatter oldFormatter) { - // With maxLength == null or -1, this formatter is basically an identity - // function and imposes no constraints on the user input. Thus it can be - // used to update an arbitrary formatter without re-formatting. - final int? maxLength = this.maxLength; - if (maxLength == null || maxLength == -1) - return false; - - if (oldFormatter is! LengthLimitingTextInputFormatter) - return true; - - final int? maxLengthOld = oldFormatter.maxLength; - return (maxLengthOld == null || maxLengthOld == -1) - || maxLength < maxLengthOld; - } } TextEditingValue _selectionAwareTextManipulation( diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart index dc228de163..2dc8a620fa 100644 --- a/packages/flutter/lib/src/widgets/editable_text.dart +++ b/packages/flutter/lib/src/widgets/editable_text.dart @@ -34,9 +34,6 @@ import 'ticker_provider.dart'; export 'package:flutter/rendering.dart' show SelectionChangedCause; export 'package:flutter/services.dart' show TextEditingValue, TextSelection, TextInputType, SmartQuotesType, SmartDashesType; -// Examples can assume: -// late TextInputFormatter usPhoneNumberFormatter; - /// Signature for the callback that reports when the user changes the selection /// (including the cursor location). typedef SelectionChangedCallback = void Function(TextSelection selection, SelectionChangedCause? cause); @@ -228,7 +225,7 @@ class TextEditingController extends ValueNotifier { /// change the controller's [value]. /// /// If the new selection if of non-zero length, or is outside the composing - /// range, the composing range is cleared. + /// range, the composing composing range is cleared. set selection(TextSelection newSelection) { if (!isSelectionWithinTextBounds(newSelection)) throw FlutterError('invalid text selection: $newSelection'); @@ -275,49 +272,6 @@ class TextEditingController extends ValueNotifier { bool _isSelectionWithinComposingRange(TextSelection selection) { return selection.start >= value.composing.start && selection.end <= value.composing.end; } - - List? _inputFormatters; - void _setInputFormatters(List newValue) { - // The setter does not take null values: if currentValue is null that means - // this is the first formatter list ever set, and we should not reformat. - final List? currentValue = _inputFormatters; - _inputFormatters = newValue; - if (newValue == currentValue || currentValue == null) { - return; - } - - final Iterator oldFormatters = currentValue.iterator; - final Iterator newFormatters = newValue.iterator; - - // Determining how many new input formatters need to be rerun: - // - // * The entire `newValue` list needs to be rerun if it has less formatters - // than the current list, or any of the new input formatter requests - // reformatting. - // * Otherwise, only apply the new input formatters whose index is larger - // than newValue.length. - bool needsReformat = currentValue.length > newValue.length; - while (!needsReformat && oldFormatters.moveNext() && newFormatters.moveNext()) { - if (newFormatters.current.shouldReformat(oldFormatters.current)) { - needsReformat = true; - } - } - - TextEditingValue formatted = value; - - if (needsReformat || oldFormatters.moveNext()) { - formatted = newValue.fold( - formatted, - (TextEditingValue v, TextInputFormatter formatter) => formatter.format(v), - ); - } else { - while (newFormatters.moveNext()) { - formatted = newFormatters.current.format(formatted); - } - } - - value = formatted; - } } /// Toolbar configuration for [EditableText]. @@ -571,7 +525,7 @@ class EditableText extends StatefulWidget { inputFormatters = maxLines == 1 ? [ FilteringTextInputFormatter.singleLineFormatter, - ...?inputFormatters, + ...inputFormatters ?? const Iterable.empty(), ] : inputFormatters, showCursor = showCursor ?? !readOnly, @@ -1104,76 +1058,9 @@ class EditableText extends StatefulWidget { /// {@template flutter.widgets.editableText.inputFormatters} /// Optional input validation and formatting overrides. /// - /// Formatters are run in the provided order when the user changes the text - /// contained in the widget. They're not applied when the changes are - /// selection only, or not initiated by the user. - /// - /// When this widget rebuilds, each input formatter in the new widget's - /// [inputFormatters] list checks the configuration of the input formatter - /// from the same location in the old [inputFormatters], to determine if the - /// new formatters need to be re-applied to the current [TextEditingValue] of - /// this widget. - /// - /// {@tool snippet} - /// - /// The following code uses a combination of 2 [TextInputFormatter]s and a - /// `UsPhoneNumberFormatter` (which simply adds parentheses and hypens), to - /// turn user input into a valid United States telephone number (for example, - /// (123)456-7890). - /// - /// The combined effect of the 3 formatters is idempotent, meaning applying - /// them together to an already formatted value is a no-op. The - /// `UsPhoneNumberFormatter` is not idempotent, thus should not be used by - /// itself. - /// - /// ```dart - /// class UsPhoneNumberFormatter extends TextInputFormatter { - /// const UsPhoneNumberFormatter(); - /// - /// @override - /// TextEditingValue format(TextEditingValue value) { - /// final int inputLength = value.text.length; - /// if (inputLength <= 3) - /// return value; - /// - /// final StringBuffer newText = StringBuffer(); - /// - /// newText.write('('); - /// newText.write(value.text.substring(0, 3)); - /// newText.write(')'); - /// newText.write(value.text.substring(3, math.min(6, inputLength))); - /// - /// if (inputLength > 6) { - /// newText.write('-'); - /// newText.write(value.text.substring(6)); - /// } - /// - /// final int selectionOffset = value.selection.end <= 3 ? 1 : value.selection.end <= 6 ? 2 : 3; - /// return TextEditingValue( - /// text: newText.toString(), - /// selection: TextSelection.collapsed(offset: value.selection.end + selectionOffset), - /// ); - /// } - /// - /// @override - /// TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) => format(newValue); - /// - /// @override - /// bool shouldReformat(TextInputFormatter oldFormatter) => oldFormatter is! UsPhoneNumberFormatter; - /// } - /// ``` - /// - /// ```dart - /// TextField( - /// inputFormatters: [ - /// FilteringTextInputFormatter.digitsOnly, - /// LengthLimitingTextInputFormatter(10), - /// usPhoneNumberFormatter, - /// ], - /// ) - /// ``` - /// {@end-tool} - /// + /// 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. /// {@endtemplate} final List? inputFormatters; @@ -1663,7 +1550,6 @@ class EditableTextState extends State with AutomaticKeepAliveClien void initState() { super.initState(); _clipboardStatus?.addListener(_onChangedClipboardStatus); - widget.controller._setInputFormatters(widget.inputFormatters ?? const []); widget.controller.addListener(_didChangeTextEditingValue); _focusAttachment = widget.focusNode.attach(context); widget.focusNode.addListener(_handleFocusChanged); @@ -1700,11 +1586,11 @@ class EditableTextState extends State with AutomaticKeepAliveClien @override void didUpdateWidget(EditableText oldWidget) { - beginBatchEdit(); super.didUpdateWidget(oldWidget); if (widget.controller != oldWidget.controller) { oldWidget.controller.removeListener(_didChangeTextEditingValue); widget.controller.addListener(_didChangeTextEditingValue); + _updateRemoteEditingValueIfNeeded(); } if (widget.controller.selection != oldWidget.controller.selection) { _selectionOverlay?.update(_value); @@ -1750,11 +1636,6 @@ class EditableTextState extends State with AutomaticKeepAliveClien if (widget.selectionEnabled && pasteEnabled && widget.selectionControls?.canPaste(this) == true) { _clipboardStatus?.update(); } - - widget.controller._setInputFormatters( - widget.inputFormatters ?? const [] - ); - endBatchEdit(); } @override @@ -2344,13 +2225,7 @@ class EditableTextState extends State with AutomaticKeepAliveClien _lastBottomViewInset = WidgetsBinding.instance!.window.viewInsets.bottom; } - _WhitespaceDirectionalityFormatter? _lastUsedWhitespaceFormatter; - _WhitespaceDirectionalityFormatter get _whitespaceFormatter { - final _WhitespaceDirectionalityFormatter? lastUsed = _lastUsedWhitespaceFormatter; - if (lastUsed != null && lastUsed._baseDirection == _textDirection) - return lastUsed; - return _lastUsedWhitespaceFormatter = _WhitespaceDirectionalityFormatter(textDirection: _textDirection); - } + late final _WhitespaceDirectionalityFormatter _whitespaceFormatter = _WhitespaceDirectionalityFormatter(textDirection: _textDirection); void _formatAndSetValue(TextEditingValue value) { // Only apply input formatters if the text has changed (including uncommited @@ -2366,13 +2241,18 @@ class EditableTextState extends State with AutomaticKeepAliveClien final bool selectionChanged = _value.selection != value.selection; if (textChanged) { - final TextEditingValue formatted = widget.inputFormatters?.fold( + value = widget.inputFormatters?.fold( value, (TextEditingValue newValue, TextInputFormatter formatter) => formatter.formatEditUpdate(_value, newValue), ) ?? value; + // Always pass the text through the whitespace directionality formatter to // maintain expected behavior with carets on trailing whitespace. - value = _whitespaceFormatter.formatEditUpdate(_value, formatted); + // TODO(LongCatIsLooong): The if statement here is for retaining the + // previous behavior. The input formatter logic will be updated in an + // upcoming PR. + if (widget.inputFormatters?.isNotEmpty ?? false) + value = _whitespaceFormatter.formatEditUpdate(_value, value); } // Put all optional user callback invocations in a batch edit to prevent diff --git a/packages/flutter/test/services/text_formatter_test.dart b/packages/flutter/test/services/text_formatter_test.dart index 267199fb73..b5cb9dd37a 100644 --- a/packages/flutter/test/services/text_formatter_test.dart +++ b/packages/flutter/test/services/text_formatter_test.dart @@ -628,235 +628,4 @@ void main() { // cursor must be now at fourth position (right after the number 9) expect(formatted.selection.baseOffset, equals(4)); }); - - group('provided formatters implement shouldReformat correctly', () { - test('length limiting formatter', () { - expect( - LengthLimitingTextInputFormatter(-1).shouldReformat(LengthLimitingTextInputFormatter(null)), - isFalse, - ); - - expect( - LengthLimitingTextInputFormatter(null).shouldReformat(LengthLimitingTextInputFormatter(-1)), - isFalse, - ); - - expect( - LengthLimitingTextInputFormatter(null).shouldReformat(LengthLimitingTextInputFormatter(null)), - isFalse, - ); - - expect( - LengthLimitingTextInputFormatter(3).shouldReformat(LengthLimitingTextInputFormatter(3)), - isFalse, - ); - - // We're relaxing the length constraint. No reformatting needed. - expect( - LengthLimitingTextInputFormatter(-1).shouldReformat(LengthLimitingTextInputFormatter(3)), - isFalse, - ); - - // We're relaxing the length constraint. No reformatting needed. - expect( - LengthLimitingTextInputFormatter(4).shouldReformat(LengthLimitingTextInputFormatter(3)), - isFalse, - ); - - expect( - LengthLimitingTextInputFormatter(3).shouldReformat(LengthLimitingTextInputFormatter(4)), - isTrue, - ); - - expect( - LengthLimitingTextInputFormatter(3).shouldReformat(LengthLimitingTextInputFormatter(null)), - isTrue, - ); - - expect( - LengthLimitingTextInputFormatter(3).shouldReformat(LengthLimitingTextInputFormatter(-1)), - isTrue, - ); - }); - - test('FliteringTextInputFormatter', () { - expect( - FilteringTextInputFormatter('a', allow: true, replacementString: 'b').shouldReformat( - FilteringTextInputFormatter('a', allow: true, replacementString: 'b'), - ), - isFalse, - ); - - expect( - FilteringTextInputFormatter('a', allow: true, replacementString: 'b').shouldReformat( - FilteringTextInputFormatter('a', allow: true, replacementString: 'c'), - ), - isTrue, - ); - - expect( - FilteringTextInputFormatter('a', allow: true, replacementString: 'b').shouldReformat( - FilteringTextInputFormatter('a', allow: false, replacementString: 'b'), - ), - isTrue, - ); - - expect( - FilteringTextInputFormatter('a', allow: true, replacementString: 'b').shouldReformat( - FilteringTextInputFormatter('c', allow: true, replacementString: 'b'), - ), - isTrue, - ); - - expect( - FilteringTextInputFormatter('a', allow: true, replacementString: 'b').shouldReformat( - FilteringTextInputFormatter('c', allow: true), - ), - isTrue, - ); - }); - }); - - group('provided formatters do not further modify a formatted value', () { - // Framework-provided TextInputFormatters must be idempotent in order to be - // used alone. - void verifyFormatterIdempotency( - TextInputFormatter formatter, - TextEditingValue input, - ) { - final TextEditingValue formatted = formatter.format(input); - expect(formatter.format(formatted), formatted); - } - - setUp(() { - // a1b(2c3 - // d4)e5f6 - // where the parentheses are the selection range. - testNewValue = const TextEditingValue( - text: 'a1b2c3\nd4e5f6', - selection: TextSelection( - baseOffset: 3, - extentOffset: 9, - ), - ); - }); - - test('FliteringTextInputFormatter with replacementString', () { - const TextEditingValue selectedIntoTheWoods = TextEditingValue( - text: 'Into the Woods', - selection: TextSelection(baseOffset: 11, extentOffset: 14), - ); - - for (final Pattern p in ['o', RegExp('o+')]) { - verifyFormatterIdempotency( - FilteringTextInputFormatter(p, allow: true, replacementString: '*'), - selectedIntoTheWoods, - ); - verifyFormatterIdempotency( - FilteringTextInputFormatter(p, allow: false, replacementString: '*'), - selectedIntoTheWoods, - ); - } - }); - - test('single line formatter', () { - verifyFormatterIdempotency( - FilteringTextInputFormatter.singleLineFormatter, - testNewValue, - ); - }); - - test('digits only formatter', () { - verifyFormatterIdempotency( - FilteringTextInputFormatter.digitsOnly, - testNewValue, - ); - }); - - test('length limiting formatter', () { - verifyFormatterIdempotency( - LengthLimitingTextInputFormatter(5), - testNewValue, - ); - }); - }); - - group('CompositeTextInputFormatter', () { - test('combine effects, in provided order', () { - final CompositeTextInputFormatter formatter = CompositeTextInputFormatter( - [ - FilteringTextInputFormatter.allow(RegExp(r'[a\*]'), replacementString: '**'), - LengthLimitingTextInputFormatter(3), - ] - ); - - expect(formatter.format(const TextEditingValue(text: 'aab')).text, 'aab'); - expect( - formatter.formatEditUpdate(const TextEditingValue(text: 'aaa'), const TextEditingValue(text: 'aab')).text, - 'aaa', - ); - }); - - test('anyChildNeedsReformat', () { - final CompositeTextInputFormatter oldFormatter = CompositeTextInputFormatter( - [ - FilteringTextInputFormatter.allow(RegExp(r'[a\*]'), replacementString: '**'), - LengthLimitingTextInputFormatter(3), - ] - ); - - final CompositeTextInputFormatter newFormatter = CompositeTextInputFormatter( - [ - FilteringTextInputFormatter.allow(RegExp(r'[a\*]'), replacementString: '**'), - LengthLimitingTextInputFormatter(1), - ] - ); - - expect(newFormatter.shouldReformat(newFormatter), isFalse); - expect(oldFormatter.shouldReformat(oldFormatter), isFalse); - expect(newFormatter.shouldReformat(oldFormatter), isTrue); - }); - - test('neverReformat', () { - final CompositeTextInputFormatter oldFormatter = CompositeTextInputFormatter( - [ - FilteringTextInputFormatter.allow(RegExp(r'[a\*]'), replacementString: '**'), - LengthLimitingTextInputFormatter(3), - ] - ); - - final CompositeTextInputFormatter newFormatter = CompositeTextInputFormatter( - [ - FilteringTextInputFormatter.allow(RegExp(r'[a\*]'), replacementString: '**'), - LengthLimitingTextInputFormatter(1), - ], - shouldReformatPredicate: CompositeTextInputFormatter.neverReformat, - ); - - expect(newFormatter.shouldReformat(newFormatter), isFalse); - expect(oldFormatter.shouldReformat(oldFormatter), isFalse); - expect(newFormatter.shouldReformat(oldFormatter), isFalse); - }); - - test('alwaysReformat', () { - final CompositeTextInputFormatter oldFormatter = CompositeTextInputFormatter( - [ - FilteringTextInputFormatter.allow(RegExp(r'[a\*]'), replacementString: '**'), - LengthLimitingTextInputFormatter(3), - ] - ); - - final CompositeTextInputFormatter newFormatter = CompositeTextInputFormatter( - [ - FilteringTextInputFormatter.allow(RegExp(r'[a\*]'), replacementString: '**'), - LengthLimitingTextInputFormatter(999), - ], - shouldReformatPredicate: CompositeTextInputFormatter.alwaysReformat, - ); - - expect(newFormatter.shouldReformat(newFormatter), isTrue); - expect(oldFormatter.shouldReformat(oldFormatter), isFalse); - expect(newFormatter.shouldReformat(oldFormatter), isTrue); - }); - }); } diff --git a/packages/flutter/test/widgets/editable_text_didUpdateWidget_test.dart b/packages/flutter/test/widgets/editable_text_didUpdateWidget_test.dart deleted file mode 100644 index ae0b20ab3e..0000000000 --- a/packages/flutter/test/widgets/editable_text_didUpdateWidget_test.dart +++ /dev/null @@ -1,149 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter/services.dart'; - -void main() { - final FocusNode focusNode = FocusNode(debugLabel: 'EditableText Node'); - const TextStyle textStyle = TextStyle(); - const Color cursorColor = Color.fromARGB(0xFF, 0xFF, 0x00, 0x00); - const Color backgroundColor = Color.fromARGB(0xFF, 0xFF, 0x00, 0x00); - late TextEditingController defaultController; - - group('didUpdateWidget', () { - final _AppendingFormatter appendingFormatter = _AppendingFormatter(); - - Widget build({ - TextDirection textDirection = TextDirection.ltr, - List? formatters, - TextEditingController? controller, - }) { - return MediaQuery( - data: const MediaQueryData(devicePixelRatio: 1.0), - child: Directionality( - textDirection: textDirection, - child: EditableText( - backgroundCursorColor: backgroundColor, - controller: controller ?? defaultController, - maxLines: null, // Remove the builtin newline formatter. - focusNode: focusNode, - style: textStyle, - cursorColor: cursorColor, - inputFormatters: formatters, - ), - ), - ); - } - - testWidgets('EditableText only reformats when needed', (WidgetTester tester) async { - appendingFormatter.needsReformat = false; - defaultController = TextEditingController(text: 'initialText'); - String previousText = defaultController.text; - - // Initial build, do not apply formatters. - await tester.pumpWidget(build()); - expect(defaultController.text, previousText); - - await tester.pumpWidget(build(formatters: [ - LengthLimitingTextInputFormatter(null), - appendingFormatter, - ])); - - expect(defaultController.text, contains(previousText + 'a')); - previousText = defaultController.text; - - // Change the first formatter. - await tester.pumpWidget(build(formatters: [ - LengthLimitingTextInputFormatter(1000), - appendingFormatter, - ])); - - // Reformat since the length formatter changed and it becomes more - // strict (null -> 1000). - expect(defaultController.text, contains(previousText + 'a')); - previousText = defaultController.text; - - await tester.pumpWidget(build(formatters: [ - LengthLimitingTextInputFormatter(2000), - appendingFormatter, - ])); - - // No reformat needed since the length formatter relaxed its constraint - // (1000 -> 2000). - expect(defaultController.text, previousText); - - await tester.pumpWidget(build(formatters: [ - appendingFormatter, - ])); - - // Reformat since we reduced the number of new formatters. - expect(defaultController.text, previousText + 'a'); - previousText = defaultController.text; - - // Now the the appending formatter always requests a reformat when - // didUpdateWidget is called. - appendingFormatter.needsReformat = true; - - await tester.pumpWidget(build(formatters: [ - appendingFormatter, - ])); - - // Reformat since appendingFormatter now always requests a rerun. - expect(defaultController.text, contains(previousText + 'a')); - previousText = defaultController.text; - }); - - testWidgets( - 'Changing the controller along with the formatter does not reformat', - (WidgetTester tester) async { - // This test verifies that the `shouldReformat` predicate is run against - // the previous formatter associated with the *TextEditingController*, - // instead of the one associated with the widget, to avoid unnecessary - // rebuilds. - final TextEditingController controller1 = TextEditingController(text: 'shorttxt'); - final TextEditingController controller2 = TextEditingController(text: 'looooong text'); - - final Widget editableText1 = build( - controller: controller1, - formatters: [LengthLimitingTextInputFormatter(controller1.text.length)], - ); - final Widget editableText2 = build( - controller: controller2, - formatters: [LengthLimitingTextInputFormatter(controller2.text.length)], - ); - - await tester.pumpWidget(Directionality( - textDirection: TextDirection.ltr, - child: Column(children: [editableText1, editableText2]), - )); - - // The 2 input fields swap places. The input formatters should not rerun. - await tester.pumpWidget(Directionality( - textDirection: TextDirection.ltr, - child: Column(children: [editableText2, editableText1]), - )); - - expect(controller1.text, 'shorttxt'); - expect(controller2.text, 'looooong text'); - }); -}); - -} - - -// A TextInputFormatter that appends 'a' to the current editing value every time -// it runs. -class _AppendingFormatter extends TextInputFormatter { - bool needsReformat = true; - - @override - TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) { - return newValue.copyWith(text: newValue.text + 'a'); - } - - @override - bool shouldReformat(TextInputFormatter oldFormatter) => needsReformat; -}