Unverified Commit ce0ec01f authored by LongCatIsLooong's avatar LongCatIsLooong Committed by GitHub

squash commits (#68166)

parent 780752e8
......@@ -11,6 +11,23 @@ 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]
......@@ -57,16 +74,36 @@ enum MaxLengthEnforcement {
/// A [TextInputFormatter] can be optionally injected into an [EditableText]
/// to provide as-you-type validation and formatting of the text being edited.
///
/// Text modification should only be applied when text is being committed by the
/// IME and not on text under composition (i.e., only when
/// 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
/// [TextEditingValue.composing] is collapsed).
///
/// 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).
/// 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.
///
/// To create custom formatters, extend the [TextInputFormatter] class and
/// implement the [formatEditUpdate] method.
/// 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].
///
/// ## Handling emojis and other complex characters
/// {@macro flutter.widgets.EditableText.onChanged}
......@@ -77,7 +114,11 @@ enum MaxLengthEnforcement {
/// * [FilteringTextInputFormatter], a provided formatter for filtering
/// characters.
abstract class TextInputFormatter {
/// Called when text is being typed or cut/copy/pasted in the [EditableText].
/// Creates a new [TextInputFormatter].
const TextInputFormatter();
/// Called when text is being typed or cut/copy/pasted in the [EditableText]
/// by the user.
///
/// You can override the resulting text based on the previous text value and
/// the incoming new text value.
......@@ -96,14 +137,145 @@ 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);
}
/// Function signature expected for creating custom [TextInputFormatter]
/// shorthands via [TextInputFormatter.withFunction].
typedef TextInputFormatFunction = TextEditingValue Function(
TextEditingValue oldValue,
TextEditingValue newValue,
);
/// 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: <TextInputFormatter>[
/// CompositeTextInputFormatter(
/// <TextInputFormatter>[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<TextInputFormatter> newChild = newFormatter.formatters.iterator;
final Iterator<TextInputFormatter> 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<TextInputFormatter> 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<TextEditingValue>(
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),
);
}
}
/// Wiring for [TextInputFormatter.withFunction].
class _SimpleTextInputFormatter extends TextInputFormatter {
......@@ -280,6 +452,14 @@ 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].
......@@ -526,6 +706,23 @@ 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(
......
......@@ -34,6 +34,9 @@ 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);
......@@ -225,7 +228,7 @@ class TextEditingController extends ValueNotifier<TextEditingValue> {
/// change the controller's [value].
///
/// If the new selection if of non-zero length, or is outside the composing
/// range, the composing composing range is cleared.
/// range, the composing range is cleared.
set selection(TextSelection newSelection) {
if (!isSelectionWithinTextBounds(newSelection))
throw FlutterError('invalid text selection: $newSelection');
......@@ -272,6 +275,49 @@ class TextEditingController extends ValueNotifier<TextEditingValue> {
bool _isSelectionWithinComposingRange(TextSelection selection) {
return selection.start >= value.composing.start && selection.end <= value.composing.end;
}
List<TextInputFormatter>? _inputFormatters;
void _setInputFormatters(List<TextInputFormatter> 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<TextInputFormatter>? currentValue = _inputFormatters;
_inputFormatters = newValue;
if (newValue == currentValue || currentValue == null) {
return;
}
final Iterator<TextInputFormatter> oldFormatters = currentValue.iterator;
final Iterator<TextInputFormatter> 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].
......@@ -525,7 +571,7 @@ class EditableText extends StatefulWidget {
inputFormatters = maxLines == 1
? <TextInputFormatter>[
FilteringTextInputFormatter.singleLineFormatter,
...inputFormatters ?? const Iterable<TextInputFormatter>.empty(),
...?inputFormatters,
]
: inputFormatters,
showCursor = showCursor ?? !readOnly,
......@@ -1058,9 +1104,76 @@ class EditableText extends StatefulWidget {
/// {@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 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: <TextInputFormatter>[
/// FilteringTextInputFormatter.digitsOnly,
/// LengthLimitingTextInputFormatter(10),
/// usPhoneNumberFormatter,
/// ],
/// )
/// ```
/// {@end-tool}
///
/// {@endtemplate}
final List<TextInputFormatter>? inputFormatters;
......@@ -1550,6 +1663,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
void initState() {
super.initState();
_clipboardStatus?.addListener(_onChangedClipboardStatus);
widget.controller._setInputFormatters(widget.inputFormatters ?? const <TextInputFormatter>[]);
widget.controller.addListener(_didChangeTextEditingValue);
_focusAttachment = widget.focusNode.attach(context);
widget.focusNode.addListener(_handleFocusChanged);
......@@ -1586,11 +1700,11 @@ class EditableTextState extends State<EditableText> 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);
......@@ -1636,6 +1750,11 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
if (widget.selectionEnabled && pasteEnabled && widget.selectionControls?.canPaste(this) == true) {
_clipboardStatus?.update();
}
widget.controller._setInputFormatters(
widget.inputFormatters ?? const <TextInputFormatter>[]
);
endBatchEdit();
}
@override
......@@ -2225,7 +2344,13 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
_lastBottomViewInset = WidgetsBinding.instance!.window.viewInsets.bottom;
}
late final _WhitespaceDirectionalityFormatter _whitespaceFormatter = _WhitespaceDirectionalityFormatter(textDirection: _textDirection);
_WhitespaceDirectionalityFormatter? _lastUsedWhitespaceFormatter;
_WhitespaceDirectionalityFormatter get _whitespaceFormatter {
final _WhitespaceDirectionalityFormatter? lastUsed = _lastUsedWhitespaceFormatter;
if (lastUsed != null && lastUsed._baseDirection == _textDirection)
return lastUsed;
return _lastUsedWhitespaceFormatter = _WhitespaceDirectionalityFormatter(textDirection: _textDirection);
}
void _formatAndSetValue(TextEditingValue value) {
// Only apply input formatters if the text has changed (including uncommited
......@@ -2241,18 +2366,13 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
final bool selectionChanged = _value.selection != value.selection;
if (textChanged) {
value = widget.inputFormatters?.fold<TextEditingValue>(
final TextEditingValue formatted = widget.inputFormatters?.fold<TextEditingValue>(
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.
// 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);
value = _whitespaceFormatter.formatEditUpdate(_value, formatted);
}
// Put all optional user callback invocations in a batch edit to prevent
......
......@@ -628,4 +628,235 @@ 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 <Pattern>['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(
<TextInputFormatter>[
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(
<TextInputFormatter>[
FilteringTextInputFormatter.allow(RegExp(r'[a\*]'), replacementString: '**'),
LengthLimitingTextInputFormatter(3),
]
);
final CompositeTextInputFormatter newFormatter = CompositeTextInputFormatter(
<TextInputFormatter>[
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(
<TextInputFormatter>[
FilteringTextInputFormatter.allow(RegExp(r'[a\*]'), replacementString: '**'),
LengthLimitingTextInputFormatter(3),
]
);
final CompositeTextInputFormatter newFormatter = CompositeTextInputFormatter(
<TextInputFormatter>[
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(
<TextInputFormatter>[
FilteringTextInputFormatter.allow(RegExp(r'[a\*]'), replacementString: '**'),
LengthLimitingTextInputFormatter(3),
]
);
final CompositeTextInputFormatter newFormatter = CompositeTextInputFormatter(
<TextInputFormatter>[
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);
});
});
}
// 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<TextInputFormatter>? 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: <TextInputFormatter>[
LengthLimitingTextInputFormatter(null),
appendingFormatter,
]));
expect(defaultController.text, contains(previousText + 'a'));
previousText = defaultController.text;
// Change the first formatter.
await tester.pumpWidget(build(formatters: <TextInputFormatter>[
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: <TextInputFormatter>[
LengthLimitingTextInputFormatter(2000),
appendingFormatter,
]));
// No reformat needed since the length formatter relaxed its constraint
// (1000 -> 2000).
expect(defaultController.text, previousText);
await tester.pumpWidget(build(formatters: <TextInputFormatter>[
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: <TextInputFormatter>[
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: <TextInputFormatter>[LengthLimitingTextInputFormatter(controller1.text.length)],
);
final Widget editableText2 = build(
controller: controller2,
formatters: <TextInputFormatter>[LengthLimitingTextInputFormatter(controller2.text.length)],
);
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: Column(children: <Widget>[editableText1, editableText2]),
));
// The 2 input fields swap places. The input formatters should not rerun.
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: Column(children: <Widget>[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;
}
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