Michael Goderbauer 5491c8c146
Auto-format Framework (#160545)
This auto-formats all *.dart files in the repository outside of the
`engine` subdirectory and enforces that these files stay formatted with
a presubmit check.

**Reviewers:** Please carefully review all the commits except for the
one titled "formatted". The "formatted" commit was auto-generated by
running `dev/tools/format.sh -a -f`. The other commits were hand-crafted
to prepare the repo for the formatting change. I recommend reviewing the
commits one-by-one via the "Commits" tab and avoiding Github's "Files
changed" tab as it will likely slow down your browser because of the
size of this PR.

---------

Co-authored-by: Kate Lovett <katelovett@google.com>
Co-authored-by: LongCatIsLooong <31859944+LongCatIsLooong@users.noreply.github.com>
2024-12-19 20:06:21 +00:00

606 lines
25 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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.
/// @docImport 'package:flutter/material.dart';
library;
import 'dart:math' as math;
import 'package:characters/characters.dart';
import 'package:flutter/foundation.dart';
import 'text_input.dart';
export 'package:flutter/foundation.dart' show TargetPlatform;
export 'text_input.dart' show TextEditingValue;
// Examples can assume:
// late RegExp _pattern;
/// Mechanisms for enforcing maximum length limits.
///
/// This is used by [TextField] to specify how the [TextField.maxLength] should
/// be applied.
///
/// {@template flutter.services.textFormatter.maxLengthEnforcement}
/// ### [MaxLengthEnforcement.enforced] versus
/// [MaxLengthEnforcement.truncateAfterCompositionEnds]
///
/// Both [MaxLengthEnforcement.enforced] and
/// [MaxLengthEnforcement.truncateAfterCompositionEnds] make sure the final
/// length of the text does not exceed the max length specified. The difference
/// is that [MaxLengthEnforcement.enforced] truncates all text while
/// [MaxLengthEnforcement.truncateAfterCompositionEnds] allows composing text to
/// exceed the limit. Allowing this "placeholder" composing text to exceed the
/// limit may provide a better user experience on some platforms for entering
/// ideographic characters (e.g. CJK characters) via composing on phonetic
/// keyboards.
///
/// Some input methods (Gboard on Android for example) initiate text composition
/// even for Latin characters, in which case the best experience may be to
/// truncate those composing characters with [MaxLengthEnforcement.enforced].
///
/// In fields that strictly support only a small subset of characters, such as
/// verification code fields, [MaxLengthEnforcement.enforced] may provide the
/// best experience.
/// {@endtemplate}
///
/// See also:
///
/// * [TextField.maxLengthEnforcement] which is used in conjunction with
/// [TextField.maxLength] to limit the length of user input. [TextField] also
/// provides a character counter to provide visual feedback.
enum MaxLengthEnforcement {
/// No enforcement applied to the editing value. It's possible to exceed the
/// max length.
none,
/// Keep the length of the text input from exceeding the max length even when
/// the text has an unfinished composing region.
enforced,
/// Users can still input text if the current value is composing even after
/// reaching the max length limit. After composing ends, the value will be
/// truncated.
truncateAfterCompositionEnds,
}
/// 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
/// [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).
///
/// To create custom formatters, extend the [TextInputFormatter] class and
/// implement the [formatEditUpdate] method.
///
/// ## Handling emojis and other complex characters
/// {@macro flutter.widgets.EditableText.onChanged}
///
/// See also:
///
/// * [EditableText] on which the formatting apply.
/// * [FilteringTextInputFormatter], a provided formatter for filtering
/// characters.
abstract class TextInputFormatter {
/// This constructor enables subclasses to provide const constructors so that they can be used in const expressions.
const TextInputFormatter();
/// A shorthand to creating a custom [TextInputFormatter] which formats
/// incoming text input changes with the given function.
const factory TextInputFormatter.withFunction(TextInputFormatFunction formatFunction) =
_SimpleTextInputFormatter;
/// 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.
///
/// When formatters are chained, `oldValue` reflects the initial value of
/// [TextEditingValue] at the beginning of the chain.
TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue);
}
/// 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 {
const _SimpleTextInputFormatter(this.formatFunction);
final TextInputFormatFunction formatFunction;
@override
TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
return formatFunction(oldValue, newValue);
}
}
// A mutable, half-open range [`base`, `extent`) within a string.
class _MutableTextRange {
_MutableTextRange(this.base, this.extent);
static _MutableTextRange? fromComposingRange(TextRange range) {
return range.isValid && !range.isCollapsed ? _MutableTextRange(range.start, range.end) : null;
}
static _MutableTextRange? fromTextSelection(TextSelection selection) {
return selection.isValid
? _MutableTextRange(selection.baseOffset, selection.extentOffset)
: null;
}
/// The start index of the range, inclusive.
///
/// The value of [base] should always be greater than or equal to 0, and can
/// be larger than, smaller than, or equal to [extent].
int base;
/// The end index of the range, exclusive.
///
/// The value of [extent] should always be greater than or equal to 0, and can
/// be larger than, smaller than, or equal to [base].
int extent;
}
// The intermediate state of a [FilteringTextInputFormatter] when it's
// formatting a new user input.
class _TextEditingValueAccumulator {
_TextEditingValueAccumulator(this.inputValue)
: selection = _MutableTextRange.fromTextSelection(inputValue.selection),
composingRegion = _MutableTextRange.fromComposingRange(inputValue.composing);
// The original string that was sent to the [FilteringTextInputFormatter] as
// input.
final TextEditingValue inputValue;
/// The [StringBuffer] that contains the string which has already been
/// formatted.
///
/// In a [FilteringTextInputFormatter], typically the replacement string,
/// instead of the original string within the given range, is written to this
/// [StringBuffer].
final StringBuffer stringBuffer = StringBuffer();
/// The updated selection, as well as the original selection from the input
/// [TextEditingValue] of the [FilteringTextInputFormatter].
///
/// This parameter will be null if the input [TextEditingValue.selection] is
/// invalid.
final _MutableTextRange? selection;
/// The updated composing region, as well as the original composing region
/// from the input [TextEditingValue] of the [FilteringTextInputFormatter].
///
/// This parameter will be null if the input [TextEditingValue.composing] is
/// invalid or collapsed.
final _MutableTextRange? composingRegion;
// Whether this state object has reached its end-of-life.
bool debugFinalized = false;
TextEditingValue finalize() {
debugFinalized = true;
final _MutableTextRange? selection = this.selection;
final _MutableTextRange? composingRegion = this.composingRegion;
return TextEditingValue(
text: stringBuffer.toString(),
composing:
composingRegion == null || composingRegion.base == composingRegion.extent
? TextRange.empty
: TextRange(start: composingRegion.base, end: composingRegion.extent),
selection:
selection == null
? const TextSelection.collapsed(offset: -1)
: TextSelection(
baseOffset: selection.base,
extentOffset: selection.extent,
// Try to preserve the selection affinity and isDirectional. This
// may not make sense if the selection has changed.
affinity: inputValue.selection.affinity,
isDirectional: inputValue.selection.isDirectional,
),
);
}
}
/// A [TextInputFormatter] that prevents the insertion of characters matching
/// (or not matching) a particular pattern, by replacing the characters with the
/// given [replacementString].
///
/// Instances of filtered characters found in the new [TextEditingValue]s
/// will be replaced by the [replacementString] which defaults to the empty
/// string, and the current [TextEditingValue.selection] and
/// [TextEditingValue.composing] region will be adjusted to account for the
/// replacement.
///
/// This formatter is typically used to match potentially recurring [Pattern]s
/// in the new [TextEditingValue]. It never completely rejects the new
/// [TextEditingValue] and falls back to the current [TextEditingValue] when the
/// given [filterPattern] fails to match. Consider using a different
/// [TextInputFormatter] such as:
///
/// ```dart
/// // _pattern is a RegExp or other Pattern object
/// TextInputFormatter.withFunction(
/// (TextEditingValue oldValue, TextEditingValue newValue) {
/// return _pattern.hasMatch(newValue.text) ? newValue : oldValue;
/// },
/// ),
/// ```
///
/// for accepting/rejecting new input based on a predicate on the full string.
/// As an example, [FilteringTextInputFormatter] typically shouldn't be used
/// with [RegExp]s that contain positional matchers (`^` or `$`) since these
/// patterns are usually meant for matching the whole string.
///
/// ### Quote characters on iOS
///
/// When filtering single (`'`) or double (`"`) quote characters, be aware that
/// the default iOS keyboard actually inserts special directional versions of
/// these characters (`` and `` for single quote, and `“` and `”` for double
/// quote). Consider including all three variants in your regular expressions to
/// support iOS.
class FilteringTextInputFormatter extends TextInputFormatter {
/// Creates a formatter that replaces banned patterns with the given
/// [replacementString].
///
/// If [allow] is true, then the filter pattern is an allow list,
/// and characters must match the pattern to be accepted. See also
/// the [FilteringTextInputFormatter.allow()] constructor.
///
/// If [allow] is false, then the filter pattern is a deny list,
/// and characters that match the pattern are rejected. See also
/// the [FilteringTextInputFormatter.deny] constructor.
FilteringTextInputFormatter(
this.filterPattern, {
required this.allow,
this.replacementString = '',
});
/// Creates a formatter that only allows characters matching a pattern.
FilteringTextInputFormatter.allow(Pattern filterPattern, {String replacementString = ''})
: this(filterPattern, allow: true, replacementString: replacementString);
/// Creates a formatter that blocks characters matching a pattern.
FilteringTextInputFormatter.deny(Pattern filterPattern, {String replacementString = ''})
: this(filterPattern, allow: false, replacementString: replacementString);
/// A [Pattern] to match or replace in incoming [TextEditingValue]s.
///
/// The behavior of the pattern depends on the [allow] property. If
/// it is true, then this is an allow list, specifying a pattern that
/// characters must match to be accepted. Otherwise, it is a deny list,
/// specifying a pattern that characters must not match to be accepted.
///
/// {@tool snippet}
/// Typically the pattern is a regular expression, as in:
///
/// ```dart
/// FilteringTextInputFormatter onlyDigits = FilteringTextInputFormatter.allow(RegExp(r'[0-9]'));
/// ```
/// {@end-tool}
///
/// {@tool snippet}
/// If the pattern is a single character, a pattern consisting of a
/// [String] can be used:
///
/// ```dart
/// FilteringTextInputFormatter noTabs = FilteringTextInputFormatter.deny('\t');
/// ```
/// {@end-tool}
final Pattern filterPattern;
/// Whether the pattern is an allow list or not.
///
/// When true, [filterPattern] denotes an allow list: characters
/// must match the filter to be allowed.
///
/// When false, [filterPattern] denotes a deny list: characters
/// that match the filter are disallowed.
final bool allow;
/// String used to replace banned patterns.
///
/// For deny lists ([allow] is false), each match of the
/// [filterPattern] is replaced with this string. If [filterPattern]
/// can match more than one character at a time, then this can
/// result in multiple characters being replaced by a single
/// instance of this [replacementString].
///
/// For allow lists ([allow] is true), sequences between matches of
/// [filterPattern] are replaced as one, regardless of the number of
/// characters.
///
/// For example, consider a [filterPattern] consisting of just the
/// letter "o", applied to text field whose initial value is the
/// string "Into The Woods", with the [replacementString] set to
/// `*`.
///
/// If [allow] is true, then the result will be "*o*oo*". Each
/// sequence of characters not matching the pattern is replaced by
/// its own single copy of the replacement string, regardless of how
/// many characters are in that sequence.
///
/// If [allow] is false, then the result will be "Int* the W**ds".
/// Every matching sequence is replaced, and each "o" matches the
/// pattern separately.
///
/// If the pattern was the [RegExp] `o+`, the result would be the
/// same in the case where [allow] is true, but in the case where
/// [allow] is false, the result would be "Int* the W*ds" (with the
/// two "o"s replaced by a single occurrence of the replacement
/// string) because both of the "o"s would be matched simultaneously
/// by the pattern.
///
/// The filter may adjust the selection and the composing region of the text
/// after applying the text replacement, such that they still cover the same
/// text. For instance, if the pattern was `o+` and the last character "s" was
/// selected: "Into The Wood|s|", then the result will be "Into The W*d|s|",
/// with the selection still around the same character "s" despite that it is
/// now the 12th character.
///
/// In the case where one end point of the selection (or the composing region)
/// is strictly inside the banned pattern (for example, "Into The |Wo|ods"),
/// that endpoint will be moved to the end of the replacement string (it will
/// become "Into The |W*|ds" if the pattern was `o+` and the original text and
/// selection were "Into The |Wo|ods").
final String replacementString;
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue, // unused.
TextEditingValue newValue,
) {
final _TextEditingValueAccumulator formatState = _TextEditingValueAccumulator(newValue);
assert(!formatState.debugFinalized);
final Iterable<Match> matches = filterPattern.allMatches(newValue.text);
Match? previousMatch;
for (final Match match in matches) {
assert(match.end >= match.start);
// Compute the non-match region between this `Match` and the previous
// `Match`. Depending on the value of `allow`, either the match region or
// the non-match region is the banned pattern.
//
// The non-matching region.
_processRegion(allow, previousMatch?.end ?? 0, match.start, formatState);
assert(!formatState.debugFinalized);
// The matched region.
_processRegion(!allow, match.start, match.end, formatState);
assert(!formatState.debugFinalized);
previousMatch = match;
}
// Handle the last non-matching region between the last match region and the
// end of the text.
_processRegion(allow, previousMatch?.end ?? 0, newValue.text.length, formatState);
assert(!formatState.debugFinalized);
return formatState.finalize();
}
void _processRegion(
bool isBannedRegion,
int regionStart,
int regionEnd,
_TextEditingValueAccumulator state,
) {
final String replacementString =
isBannedRegion
? (regionStart == regionEnd ? '' : this.replacementString)
: state.inputValue.text.substring(regionStart, regionEnd);
state.stringBuffer.write(replacementString);
if (replacementString.length == regionEnd - regionStart) {
// We don't have to adjust the indices if the replaced string and the
// replacement string have the same length.
return;
}
int adjustIndex(int originalIndex) {
// The length added by adding the replacementString.
final int replacedLength =
originalIndex <= regionStart && originalIndex < regionEnd ? 0 : replacementString.length;
// The length removed by removing the replacementRange.
final int removedLength = originalIndex.clamp(regionStart, regionEnd) - regionStart;
return replacedLength - removedLength;
}
state.selection?.base += adjustIndex(state.inputValue.selection.baseOffset);
state.selection?.extent += adjustIndex(state.inputValue.selection.extentOffset);
state.composingRegion?.base += adjustIndex(state.inputValue.composing.start);
state.composingRegion?.extent += adjustIndex(state.inputValue.composing.end);
}
/// A [TextInputFormatter] that forces input to be a single line.
static final TextInputFormatter singleLineFormatter = FilteringTextInputFormatter.deny('\n');
/// A [TextInputFormatter] that takes in digits `[0-9]` only.
static final TextInputFormatter digitsOnly = FilteringTextInputFormatter.allow(RegExp(r'[0-9]'));
}
/// A [TextInputFormatter] that prevents the insertion of more characters
/// than allowed.
///
/// Since this formatter only prevents new characters from being added to the
/// text, it preserves the existing [TextEditingValue.selection].
///
/// Characters are counted as user-perceived characters using the
/// [characters](https://pub.dev/packages/characters) package, so even complex
/// characters like extended grapheme clusters and surrogate pairs are counted
/// as single characters.
///
/// See also:
/// * [maxLength], which discusses the precise meaning of "number of
/// characters".
class LengthLimitingTextInputFormatter extends TextInputFormatter {
/// Creates a formatter that prevents the insertion of more characters than a
/// limit.
///
/// The [maxLength] must be null, -1 or greater than zero. If it is null or -1
/// then no limit is enforced.
LengthLimitingTextInputFormatter(this.maxLength, {this.maxLengthEnforcement})
: assert(maxLength == null || maxLength == -1 || maxLength > 0);
/// The limit on the number of user-perceived characters that this formatter
/// will allow.
///
/// The value must be null or greater than zero. If it is null or -1, then no
/// limit is enforced.
///
/// {@template flutter.services.lengthLimitingTextInputFormatter.maxLength}
/// ## Characters
///
/// For a specific definition of what is considered a character, see the
/// [characters](https://pub.dev/packages/characters) package on Pub, which is
/// what Flutter uses to delineate characters. In general, even complex
/// characters like surrogate pairs and extended grapheme clusters are
/// correctly interpreted by Flutter as each being a single user-perceived
/// character.
///
/// For instance, the character "ö" can be represented as '\u{006F}\u{0308}',
/// which is the letter "o" followed by a composed diaeresis "¨", or it can
/// be represented as '\u{00F6}', which is the Unicode scalar value "LATIN
/// SMALL LETTER O WITH DIAERESIS". It will be counted as a single character
/// in both cases.
///
/// Similarly, some emoji are represented by multiple scalar values. The
/// Unicode "THUMBS UP SIGN + MEDIUM SKIN TONE MODIFIER", "👍🏽"is counted as
/// a single character, even though it is a combination of two Unicode scalar
/// values, '\u{1F44D}\u{1F3FD}'.
/// {@endtemplate}
///
/// ### Composing text behaviors
///
/// There is no guarantee for the final value before the composing ends.
/// So while the value is composing, the constraint of [maxLength] will be
/// temporary lifted until the composing ends.
///
/// In addition, if the current value already reached the [maxLength],
/// composing is not allowed.
final int? maxLength;
/// Determines how the [maxLength] limit should be enforced.
///
/// Defaults to [MaxLengthEnforcement.enforced].
///
/// {@macro flutter.services.textFormatter.maxLengthEnforcement}
final MaxLengthEnforcement? maxLengthEnforcement;
/// Returns a [MaxLengthEnforcement] that follows the specified [platform]'s
/// convention.
///
/// {@template flutter.services.textFormatter.effectiveMaxLengthEnforcement}
/// ### Platform specific behaviors
///
/// Different platforms follow different behaviors by default, according to
/// their native behavior.
/// * Android, Windows: [MaxLengthEnforcement.enforced]. The native behavior
/// of these platforms is enforced. The composing will be handled by the
/// IME while users are entering CJK characters.
/// * iOS: [MaxLengthEnforcement.truncateAfterCompositionEnds]. iOS has no
/// default behavior and it requires users implement the behavior
/// themselves. Allow the composition to exceed to avoid breaking CJK input.
/// * Web, macOS, linux, fuchsia:
/// [MaxLengthEnforcement.truncateAfterCompositionEnds]. These platforms
/// allow the composition to exceed by default.
/// {@endtemplate}
static MaxLengthEnforcement getDefaultMaxLengthEnforcement([TargetPlatform? platform]) {
if (kIsWeb) {
return MaxLengthEnforcement.truncateAfterCompositionEnds;
} else {
switch (platform ?? defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.windows:
return MaxLengthEnforcement.enforced;
case TargetPlatform.iOS:
case TargetPlatform.macOS:
case TargetPlatform.linux:
case TargetPlatform.fuchsia:
return MaxLengthEnforcement.truncateAfterCompositionEnds;
}
}
}
/// Truncate the given TextEditingValue to maxLength user-perceived
/// characters.
///
/// See also:
/// * [Dart's characters package](https://pub.dev/packages/characters).
/// * [Dart's documentation on runes and grapheme clusters](https://dart.dev/guides/language/language-tour#runes-and-grapheme-clusters).
@visibleForTesting
static TextEditingValue truncate(TextEditingValue value, int maxLength) {
final CharacterRange iterator = CharacterRange(value.text);
if (value.text.characters.length > maxLength) {
iterator.expandNext(maxLength);
}
final String truncated = iterator.current;
return TextEditingValue(
text: truncated,
selection: value.selection.copyWith(
baseOffset: math.min(value.selection.start, truncated.length),
extentOffset: math.min(value.selection.end, truncated.length),
),
composing:
!value.composing.isCollapsed && truncated.length > value.composing.start
? TextRange(
start: value.composing.start,
end: math.min(value.composing.end, truncated.length),
)
: TextRange.empty,
);
}
@override
TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
final int? maxLength = this.maxLength;
if (maxLength == null || maxLength == -1 || newValue.text.characters.length <= maxLength) {
return newValue;
}
assert(maxLength > 0);
switch (maxLengthEnforcement ?? getDefaultMaxLengthEnforcement()) {
case MaxLengthEnforcement.none:
return newValue;
case MaxLengthEnforcement.enforced:
// If already at the maximum and tried to enter even more, and has no
// selection, keep the old value.
if (oldValue.text.characters.length == maxLength && oldValue.selection.isCollapsed) {
return oldValue;
}
// Enforced to return a truncated value.
return truncate(newValue, maxLength);
case MaxLengthEnforcement.truncateAfterCompositionEnds:
// If already at the maximum and tried to enter even more, and the old
// value is not composing, keep the old value.
if (oldValue.text.characters.length == maxLength && !oldValue.composing.isValid) {
return oldValue;
}
// Temporarily exempt `newValue` from the maxLength limit if it has a
// composing text going and no enforcement to the composing value, until
// the composing is finished.
if (newValue.composing.isValid) {
return newValue;
}
return truncate(newValue, maxLength);
}
}
}