blob: 8e477b4032cb75849f8fecda9303a9fec147013b [file] [log] [blame]
// 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 'dart:async';
import 'dart:math' as math;
import 'dart:ui' as ui hide TextStyle;
import 'package:characters/characters.dart' show CharacterRange, StringCharacters;
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart' show DragStartBehavior;
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'actions.dart';
import 'autofill.dart';
import 'automatic_keep_alive.dart';
import 'basic.dart';
import 'binding.dart';
import 'constants.dart';
import 'debug.dart';
import 'default_selection_style.dart';
import 'focus_manager.dart';
import 'focus_scope.dart';
import 'focus_traversal.dart';
import 'framework.dart';
import 'localizations.dart';
import 'media_query.dart';
import 'scroll_configuration.dart';
import 'scroll_controller.dart';
import 'scroll_physics.dart';
import 'scrollable.dart';
import 'shortcuts.dart';
import 'text.dart';
import 'text_editing_intents.dart';
import 'text_selection.dart';
import 'ticker_provider.dart';
import 'widget_span.dart';
export 'package:flutter/services.dart' show SelectionChangedCause, TextEditingValue, TextSelection, TextInputType, SmartQuotesType, SmartDashesType;
/// Signature for the callback that reports when the user changes the selection
/// (including the cursor location).
typedef SelectionChangedCallback = void Function(TextSelection selection, SelectionChangedCause? cause);
/// Signature for the callback that reports the app private command results.
typedef AppPrivateCommandCallback = void Function(String, Map<String, dynamic>);
// The time it takes for the cursor to fade from fully opaque to fully
// transparent and vice versa. A full cursor blink, from transparent to opaque
// to transparent, is twice this duration.
const Duration _kCursorBlinkHalfPeriod = Duration(milliseconds: 500);
// The time the cursor is static in opacity before animating to become
// transparent.
const Duration _kCursorBlinkWaitForStart = Duration(milliseconds: 150);
// Number of cursor ticks during which the most recently entered character
// is shown in an obscured text field.
const int _kObscureShowLatestCharCursorTicks = 3;
// The minimum width of an iPad screen. The smallest iPad is currently the
// iPad Mini 6th Gen according to ios-resolution.com.
const double _kIPadWidth = 1488.0;
/// A controller for an editable text field.
///
/// Whenever the user modifies a text field with an associated
/// [TextEditingController], the text field updates [value] and the controller
/// notifies its listeners. Listeners can then read the [text] and [selection]
/// properties to learn what the user has typed or how the selection has been
/// updated.
///
/// Similarly, if you modify the [text] or [selection] properties, the text
/// field will be notified and will update itself appropriately.
///
/// A [TextEditingController] can also be used to provide an initial value for a
/// text field. If you build a text field with a controller that already has
/// [text], the text field will use that text as its initial value.
///
/// The [value] (as well as [text] and [selection]) of this controller can be
/// updated from within a listener added to this controller. Be aware of
/// infinite loops since the listener will also be notified of the changes made
/// from within itself. Modifying the composing region from within a listener
/// can also have a bad interaction with some input methods. Gboard, for
/// example, will try to restore the composing region of the text if it was
/// modified programmatically, creating an infinite loop of communications
/// between the framework and the input method. Consider using
/// [TextInputFormatter]s instead for as-you-type text modification.
///
/// If both the [text] or [selection] properties need to be changed, set the
/// controller's [value] instead.
///
/// Remember to [dispose] of the [TextEditingController] when it is no longer
/// needed. This will ensure we discard any resources used by the object.
/// {@tool dartpad}
/// This example creates a [TextField] with a [TextEditingController] whose
/// change listener forces the entered text to be lower case and keeps the
/// cursor at the end of the input.
///
/// ** See code in examples/api/lib/widgets/editable_text/text_editing_controller.0.dart **
/// {@end-tool}
///
/// See also:
///
/// * [TextField], which is a Material Design text field that can be controlled
/// with a [TextEditingController].
/// * [EditableText], which is a raw region of editable text that can be
/// controlled with a [TextEditingController].
/// * Learn how to use a [TextEditingController] in one of our [cookbook recipes](https://flutter.dev/docs/cookbook/forms/text-field-changes#2-use-a-texteditingcontroller).
class TextEditingController extends ValueNotifier<TextEditingValue> {
/// Creates a controller for an editable text field.
///
/// This constructor treats a null [text] argument as if it were the empty
/// string.
TextEditingController({ String? text })
: super(text == null ? TextEditingValue.empty : TextEditingValue(text: text));
/// Creates a controller for an editable text field from an initial [TextEditingValue].
///
/// This constructor treats a null [value] argument as if it were
/// [TextEditingValue.empty].
TextEditingController.fromValue(TextEditingValue? value)
: assert(
value == null || !value.composing.isValid || value.isComposingRangeValid,
'New TextEditingValue $value has an invalid non-empty composing range '
'${value.composing}. It is recommended to use a valid composing range, '
'even for readonly text fields',
),
super(value ?? TextEditingValue.empty);
/// The current string the user is editing.
String get text => value.text;
/// Setting this will notify all the listeners of this [TextEditingController]
/// that they need to update (it calls [notifyListeners]). For this reason,
/// this value should only be set between frames, e.g. in response to user
/// actions, not during the build, layout, or paint phases.
///
/// This property can be set from a listener added to this
/// [TextEditingController]; however, one should not also set [selection]
/// in a separate statement. To change both the [text] and the [selection]
/// change the controller's [value].
set text(String newText) {
value = value.copyWith(
text: newText,
selection: const TextSelection.collapsed(offset: -1),
composing: TextRange.empty,
);
}
@override
set value(TextEditingValue newValue) {
assert(
!newValue.composing.isValid || newValue.isComposingRangeValid,
'New TextEditingValue $newValue has an invalid non-empty composing range '
'${newValue.composing}. It is recommended to use a valid composing range, '
'even for readonly text fields',
);
super.value = newValue;
}
/// Builds [TextSpan] from current editing value.
///
/// By default makes text in composing range appear as underlined. Descendants
/// can override this method to customize appearance of text.
TextSpan buildTextSpan({required BuildContext context, TextStyle? style , required bool withComposing}) {
assert(!value.composing.isValid || !withComposing || value.isComposingRangeValid);
// If the composing range is out of range for the current text, ignore it to
// preserve the tree integrity, otherwise in release mode a RangeError will
// be thrown and this EditableText will be built with a broken subtree.
if (!value.isComposingRangeValid || !withComposing) {
return TextSpan(style: style, text: text);
}
final TextStyle composingStyle = style?.merge(const TextStyle(decoration: TextDecoration.underline))
?? const TextStyle(decoration: TextDecoration.underline);
return TextSpan(
style: style,
children: <TextSpan>[
TextSpan(text: value.composing.textBefore(value.text)),
TextSpan(
style: composingStyle,
text: value.composing.textInside(value.text),
),
TextSpan(text: value.composing.textAfter(value.text)),
],
);
}
/// The currently selected [text].
///
/// If the selection is collapsed, then this property gives the offset of the
/// cursor within the text.
TextSelection get selection => value.selection;
/// Setting this will notify all the listeners of this [TextEditingController]
/// that they need to update (it calls [notifyListeners]). For this reason,
/// this value should only be set between frames, e.g. in response to user
/// actions, not during the build, layout, or paint phases.
///
/// This property can be set from a listener added to this
/// [TextEditingController]; however, one should not also set [text]
/// in a separate statement. To change both the [text] and the [selection]
/// change the controller's [value].
///
/// If the new selection is of non-zero length, or is outside the composing
/// range, the composing range is cleared.
set selection(TextSelection newSelection) {
if (!isSelectionWithinTextBounds(newSelection))
throw FlutterError('invalid text selection: $newSelection');
final TextRange newComposing =
newSelection.isCollapsed && _isSelectionWithinComposingRange(newSelection)
? value.composing
: TextRange.empty;
value = value.copyWith(selection: newSelection, composing: newComposing);
}
/// Set the [value] to empty.
///
/// After calling this function, [text] will be the empty string and the
/// selection will be collapsed at zero offset.
///
/// Calling this will notify all the listeners of this [TextEditingController]
/// that they need to update (it calls [notifyListeners]). For this reason,
/// this method should only be called between frames, e.g. in response to user
/// actions, not during the build, layout, or paint phases.
void clear() {
value = const TextEditingValue(selection: TextSelection.collapsed(offset: 0));
}
/// Set the composing region to an empty range.
///
/// The composing region is the range of text that is still being composed.
/// Calling this function indicates that the user is done composing that
/// region.
///
/// Calling this will notify all the listeners of this [TextEditingController]
/// that they need to update (it calls [notifyListeners]). For this reason,
/// this method should only be called between frames, e.g. in response to user
/// actions, not during the build, layout, or paint phases.
void clearComposing() {
value = value.copyWith(composing: TextRange.empty);
}
/// Check that the [selection] is inside of the bounds of [text].
bool isSelectionWithinTextBounds(TextSelection selection) {
return selection.start <= text.length && selection.end <= text.length;
}
/// Check that the [selection] is inside of the composing range.
bool _isSelectionWithinComposingRange(TextSelection selection) {
return selection.start >= value.composing.start && selection.end <= value.composing.end;
}
}
/// Toolbar configuration for [EditableText].
///
/// Toolbar is a context menu that will show up when user right click or long
/// press the [EditableText]. It includes several options: cut, copy, paste,
/// and select all.
///
/// [EditableText] and its derived widgets have their own default [ToolbarOptions].
/// Create a custom [ToolbarOptions] if you want explicit control over the toolbar
/// option.
class ToolbarOptions {
/// Create a toolbar configuration with given options.
///
/// All options default to false if they are not explicitly set.
const ToolbarOptions({
this.copy = false,
this.cut = false,
this.paste = false,
this.selectAll = false,
}) : assert(copy != null),
assert(cut != null),
assert(paste != null),
assert(selectAll != null);
/// Whether to show copy option in toolbar.
///
/// Defaults to false. Must not be null.
final bool copy;
/// Whether to show cut option in toolbar.
///
/// If [EditableText.readOnly] is set to true, cut will be disabled regardless.
///
/// Defaults to false. Must not be null.
final bool cut;
/// Whether to show paste option in toolbar.
///
/// If [EditableText.readOnly] is set to true, paste will be disabled regardless.
///
/// Defaults to false. Must not be null.
final bool paste;
/// Whether to show select all option in toolbar.
///
/// Defaults to false. Must not be null.
final bool selectAll;
}
/// A basic text input field.
///
/// This widget interacts with the [TextInput] service to let the user edit the
/// text it contains. It also provides scrolling, selection, and cursor
/// 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
/// action button on the soft keyboard for Android and iOS. The default action
/// is [TextInputAction.done].
///
/// Many [TextInputAction]s are common between Android and iOS. However, if a
/// [textInputAction] is provided that is not supported by the current
/// platform in debug mode, an error will be thrown when the corresponding
/// EditableText receives focus. For example, providing iOS's "emergencyCall"
/// action when running on an Android device will result in an error when in
/// debug mode. In release mode, incompatible [TextInputAction]s are replaced
/// either with "unspecified" on Android, or "default" on iOS. Appropriate
/// [textInputAction]s can be chosen by checking the current platform and then
/// selecting the appropriate action.
///
/// {@template flutter.widgets.EditableText.lifeCycle}
/// ## Lifecycle
///
/// Upon completion of editing, like pressing the "done" button on the keyboard,
/// two actions take place:
///
/// 1st: Editing is finalized. The default behavior of this step includes
/// an invocation of [onChanged]. That default behavior can be overridden.
/// See [onEditingComplete] for details.
///
/// 2nd: [onSubmitted] is invoked with the user's input value.
///
/// [onSubmitted] can be used to manually move focus to another input widget
/// when a user finishes with the currently focused input widget.
///
/// When the widget has focus, it will prevent itself from disposing via
/// [AutomaticKeepAliveClientMixin.wantKeepAlive] in order to avoid losing the
/// selection. Removing the focus will allow it to be disposed.
/// {@endtemplate}
///
/// Rather than using this widget directly, consider using [TextField], which
/// is a full-featured, material-design text input field with placeholder text,
/// labels, and [Form] integration.
///
/// ## Text Editing [Intent]s and Their Default [Action]s
///
/// This widget provides default [Action]s for handling common text editing
/// [Intent]s such as deleting, copying and pasting in the text field. These
/// [Action]s can be directly invoked using [Actions.invoke] or the
/// [Actions.maybeInvoke] method. The default text editing keyboard [Shortcuts]
/// also use these [Intent]s and [Action]s to perform the text editing
/// operations they are bound to.
///
/// The default handling of a specific [Intent] can be overridden by placing an
/// [Actions] widget above this widget. See the [Action] class and the
/// [Action.overridable] constructor for more information on how a pre-defined
/// overridable [Action] can be overridden.
///
/// ### Intents for Deleting Text and Their Default Behavior
///
/// | **Intent Class** | **Default Behavior when there's selected text** | **Default Behavior when there is a [caret](https://en.wikipedia.org/wiki/Caret_navigation) (The selection is [TextSelection.collapsed])** |
/// | :------------------------------- | :--------------------------------------------------- | :----------------------------------------------------------------------- |
/// | [DeleteCharacterIntent] | Deletes the selected text | Deletes the user-perceived character before or after the caret location. |
/// | [DeleteToNextWordBoundaryIntent] | Deletes the selected text and the word before/after the selection's [TextSelection.extent] position | Deletes from the caret location to the previous or the next word boundary |
/// | [DeleteToLineBreakIntent] | Deletes the selected text, and deletes to the start/end of the line from the selection's [TextSelection.extent] position | Deletes from the caret location to the logical start or end of the current line |
///
/// ### Intents for Moving the [Caret](https://en.wikipedia.org/wiki/Caret_navigation)
///
/// | **Intent Class** | **Default Behavior when there's selected text** | **Default Behavior when there is a caret ([TextSelection.collapsed])** |
/// | :----------------------------------------------------------------------------------- | :--------------------------------------------------------------- | :---------------------------------------------------------------------- |
/// | [ExtendSelectionByCharacterIntent](`collapseSelection: true`) | Collapses the selection to the logical start/end of the selection | Moves the caret past the user-perceived character before or after the current caret location. |
/// | [ExtendSelectionToNextWordBoundaryIntent](`collapseSelection: true`) | Collapses the selection to the word boundary before/after the selection's [TextSelection.extent] position | Moves the caret to the previous/next word boundary. |
/// | [ExtendSelectionToNextWordBoundaryOrCaretLocationIntent](`collapseSelection: true`) | Collapses the selection to the word boundary before/after the selection's [TextSelection.extent] position, or [TextSelection.base], whichever is closest in the given direction | Moves the caret to the previous/next word boundary. |
/// | [ExtendSelectionToLineBreakIntent](`collapseSelection: true`) | Collapses the selection to the start/end of the line at the selection's [TextSelection.extent] position | Moves the caret to the start/end of the current line .|
/// | [ExtendSelectionVerticallyToAdjacentLineIntent](`collapseSelection: true`) | Collapses the selection to the position closest to the selection's [TextSelection.extent], on the previous/next adjacent line | Moves the caret to the closest position on the previous/next adjacent line. |
/// | [ExtendSelectionToDocumentBoundaryIntent](`collapseSelection: true`) | Collapses the selection to the start/end of the document | Moves the caret to the start/end of the document. |
///
/// #### Intents for Extending the Selection
///
/// | **Intent Class** | **Default Behavior when there's selected text** | **Default Behavior when there is a caret ([TextSelection.collapsed])** |
/// | :----------------------------------------------------------------------------------- | :--------------------------------------------------------------- | :---------------------------------------------------------------------- |
/// | [ExtendSelectionByCharacterIntent](`collapseSelection: false`) | Moves the selection's [TextSelection.extent] past the user-perceived character before/after it |
/// | [ExtendSelectionToNextWordBoundaryIntent](`collapseSelection: false`) | Moves the selection's [TextSelection.extent] to the previous/next word boundary |
/// | [ExtendSelectionToNextWordBoundaryOrCaretLocationIntent](`collapseSelection: false`) | Moves the selection's [TextSelection.extent] to the previous/next word boundary, or [TextSelection.base] whichever is closest in the given direction | Moves the selection's [TextSelection.extent] to the previous/next word boundary. |
/// | [ExtendSelectionToLineBreakIntent](`collapseSelection: false`) | Moves the selection's [TextSelection.extent] to the start/end of the line |
/// | [ExtendSelectionVerticallyToAdjacentLineIntent](`collapseSelection: false`) | Moves the selection's [TextSelection.extent] to the closest position on the previous/next adjacent line |
/// | [ExtendSelectionToDocumentBoundaryIntent](`collapseSelection: false`) | Moves the selection's [TextSelection.extent] to the start/end of the document |
/// | [SelectAllTextIntent] | Selects the entire document |
///
/// ### Other Intents
///
/// | **Intent Class** | **Default Behavior** |
/// | :-------------------------------------- | :--------------------------------------------------- |
/// | [DoNothingAndStopPropagationTextIntent] | Does nothing in the input field, and prevents the key event from further propagating in the widget tree. |
/// | [ReplaceTextIntent] | Replaces the current [TextEditingValue] in the input field's [TextEditingController], and triggers all related user callbacks and [TextInputFormatter]s. |
/// | [UpdateSelectionIntent] | Updates the current selection in the input field's [TextEditingController], and triggers the [onSelectionChanged] callback. |
/// | [CopySelectionTextIntent] | Copies or cuts the selected text into the clipboard |
/// | [PasteTextIntent] | Inserts the current text in the clipboard after the caret location, or replaces the selected text if the selection is not collapsed. |
///
/// ## Gesture Events Handling
///
/// This widget provides rudimentary, platform-agnostic gesture handling for
/// user actions such as tapping, long-pressing and scrolling when
/// [rendererIgnoresPointer] is false (false by default). To tightly conform
/// to the platform behavior with respect to input gestures in text fields, use
/// [TextField] or [CupertinoTextField]. For custom selection behavior, call
/// methods such as [RenderEditable.selectPosition],
/// [RenderEditable.selectWord], etc. programmatically.
///
/// {@template flutter.widgets.editableText.showCaretOnScreen}
/// ## Keep the caret visible when focused
///
/// When focused, this widget will make attempts to keep the text area and its
/// caret (even when [showCursor] is `false`) visible, on these occasions:
///
/// * When the user focuses this text field and it is not [readOnly].
/// * When the user changes the selection of the text field, or changes the
/// text when the text field is not [readOnly].
/// * When the virtual keyboard pops up.
/// {@endtemplate}
///
/// See also:
///
/// * [TextField], which is a full-featured, material-design text input field
/// with placeholder text, labels, and [Form] integration.
class EditableText extends StatefulWidget {
/// Creates a basic text input control.
///
/// The [maxLines] property can be set to null to remove the restriction on
/// the number of lines. By default, it is one, meaning this is a single-line
/// text field. [maxLines] must be null or greater than zero.
///
/// If [keyboardType] is not set or is null, its value will be inferred from
/// [autofillHints], if [autofillHints] is not empty. Otherwise it defaults to
/// [TextInputType.text] if [maxLines] is exactly one, and
/// [TextInputType.multiline] if [maxLines] is null or greater than one.
///
/// The text cursor is not shown if [showCursor] is false or if [showCursor]
/// is null (the default) and [readOnly] is true.
///
/// The [controller], [focusNode], [obscureText], [autocorrect], [autofocus],
/// [showSelectionHandles], [enableInteractiveSelection], [forceLine],
/// [style], [cursorColor], [cursorOpacityAnimates],[backgroundCursorColor],
/// [enableSuggestions], [paintCursorAboveText], [selectionHeightStyle],
/// [selectionWidthStyle], [textAlign], [dragStartBehavior], [scrollPadding],
/// [dragStartBehavior], [toolbarOptions], [rendererIgnoresPointer],
/// [readOnly], and [enableIMEPersonalizedLearning] arguments must not be null.
EditableText({
super.key,
required this.controller,
required this.focusNode,
this.readOnly = false,
this.obscuringCharacter = '•',
this.obscureText = false,
this.autocorrect = true,
SmartDashesType? smartDashesType,
SmartQuotesType? smartQuotesType,
this.enableSuggestions = true,
required this.style,
StrutStyle? strutStyle,
required this.cursorColor,
required this.backgroundCursorColor,
this.textAlign = TextAlign.start,
this.textDirection,
this.locale,
this.textScaleFactor,
this.maxLines = 1,
this.minLines,
this.expands = false,
this.forceLine = true,
this.textHeightBehavior,
this.textWidthBasis = TextWidthBasis.parent,
this.autofocus = false,
bool? showCursor,
this.showSelectionHandles = false,
this.selectionColor,
this.selectionControls,
TextInputType? keyboardType,
this.textInputAction,
this.textCapitalization = TextCapitalization.none,
this.onChanged,
this.onEditingComplete,
this.onSubmitted,
this.onAppPrivateCommand,
this.onSelectionChanged,
this.onSelectionHandleTapped,
List<TextInputFormatter>? inputFormatters,
this.mouseCursor,
this.rendererIgnoresPointer = false,
this.cursorWidth = 2.0,
this.cursorHeight,
this.cursorRadius,
this.cursorOpacityAnimates = false,
this.cursorOffset,
this.paintCursorAboveText = false,
this.selectionHeightStyle = ui.BoxHeightStyle.tight,
this.selectionWidthStyle = ui.BoxWidthStyle.tight,
this.scrollPadding = const EdgeInsets.all(20.0),
this.keyboardAppearance = Brightness.light,
this.dragStartBehavior = DragStartBehavior.start,
bool? enableInteractiveSelection,
this.scrollController,
this.scrollPhysics,
this.autocorrectionTextRectColor,
ToolbarOptions? toolbarOptions,
this.autofillHints = const <String>[],
this.autofillClient,
this.clipBehavior = Clip.hardEdge,
this.restorationId,
this.scrollBehavior,
this.scribbleEnabled = true,
this.enableIMEPersonalizedLearning = true,
}) : assert(controller != null),
assert(focusNode != null),
assert(obscuringCharacter != null && obscuringCharacter.length == 1),
assert(obscureText != null),
assert(autocorrect != null),
smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
assert(enableSuggestions != null),
assert(showSelectionHandles != null),
assert(readOnly != null),
assert(forceLine != null),
assert(style != null),
assert(cursorColor != null),
assert(cursorOpacityAnimates != null),
assert(paintCursorAboveText != null),
assert(backgroundCursorColor != null),
assert(selectionHeightStyle != null),
assert(selectionWidthStyle != null),
assert(textAlign != null),
assert(maxLines == null || maxLines > 0),
assert(minLines == null || minLines > 0),
assert(
(maxLines == null) || (minLines == null) || (maxLines >= minLines),
"minLines can't be greater than maxLines",
),
assert(expands != null),
assert(
!expands || (maxLines == null && minLines == null),
'minLines and maxLines must be null when expands is true.',
),
assert(!obscureText || maxLines == 1, 'Obscured fields cannot be multiline.'),
assert(autofocus != null),
assert(rendererIgnoresPointer != null),
assert(scrollPadding != null),
assert(dragStartBehavior != null),
enableInteractiveSelection = enableInteractiveSelection ?? (!readOnly || !obscureText),
toolbarOptions = toolbarOptions ??
(obscureText
? (readOnly
// No point in even offering "Select All" in a read-only obscured
// field.
? const ToolbarOptions()
// Writable, but obscured.
: const ToolbarOptions(
selectAll: true,
paste: true,
))
: (readOnly
// Read-only, not obscured.
? const ToolbarOptions(
selectAll: true,
copy: true,
)
// Writable, not obscured.
: const ToolbarOptions(
copy: true,
cut: true,
selectAll: true,
paste: true,
))),
assert(clipBehavior != null),
assert(enableIMEPersonalizedLearning != null),
_strutStyle = strutStyle,
keyboardType = keyboardType ?? _inferKeyboardType(autofillHints: autofillHints, maxLines: maxLines),
inputFormatters = maxLines == 1
? <TextInputFormatter>[
FilteringTextInputFormatter.singleLineFormatter,
...inputFormatters ?? const Iterable<TextInputFormatter>.empty(),
]
: inputFormatters,
showCursor = showCursor ?? !readOnly;
/// Controls the text being edited.
final TextEditingController controller;
/// Controls whether this widget has keyboard focus.
final FocusNode focusNode;
/// {@template flutter.widgets.editableText.obscuringCharacter}
/// Character used for obscuring text if [obscureText] is true.
///
/// Must be only a single character.
///
/// Defaults to the character U+2022 BULLET (•).
/// {@endtemplate}
final String obscuringCharacter;
/// {@template flutter.widgets.editableText.obscureText}
/// Whether to hide the text being edited (e.g., for passwords).
///
/// When this is set to true, all the characters in the text field are
/// replaced by [obscuringCharacter], and the text in the field cannot be
/// copied with copy or cut. If [readOnly] is also true, then the text cannot
/// be selected.
///
/// Defaults to false. Cannot be null.
/// {@endtemplate}
final bool obscureText;
/// {@macro dart.ui.textHeightBehavior}
final TextHeightBehavior? textHeightBehavior;
/// {@macro flutter.painting.textPainter.textWidthBasis}
final TextWidthBasis textWidthBasis;
/// {@template flutter.widgets.editableText.readOnly}
/// Whether the text can be changed.
///
/// When this is set to true, the text cannot be modified
/// by any shortcut or keyboard operation. The text is still selectable.
///
/// Defaults to false. Must not be null.
/// {@endtemplate}
final bool readOnly;
/// Whether the text will take the full width regardless of the text width.
///
/// When this is set to false, the width will be based on text width, which
/// will also be affected by [textWidthBasis].
///
/// Defaults to true. Must not be null.
///
/// See also:
///
/// * [textWidthBasis], which controls the calculation of text width.
final bool forceLine;
/// Configuration of toolbar options.
///
/// By default, all options are enabled. If [readOnly] is true, paste and cut
/// will be disabled regardless. If [obscureText] is true, cut and copy will
/// be disabled regardless. If [readOnly] and [obscureText] are both true,
/// select all will also be disabled.
final ToolbarOptions toolbarOptions;
/// Whether to show selection handles.
///
/// When a selection is active, there will be two handles at each side of
/// boundary, or one handle if the selection is collapsed. The handles can be
/// dragged to adjust the selection.
///
/// See also:
///
/// * [showCursor], which controls the visibility of the cursor.
final bool showSelectionHandles;
/// {@template flutter.widgets.editableText.showCursor}
/// Whether to show cursor.
///
/// The cursor refers to the blinking caret when the [EditableText] is focused.
/// {@endtemplate}
///
/// See also:
///
/// * [showSelectionHandles], which controls the visibility of the selection handles.
final bool showCursor;
/// {@template flutter.widgets.editableText.autocorrect}
/// Whether to enable autocorrection.
///
/// Defaults to true. Cannot be null.
/// {@endtemplate}
final bool autocorrect;
/// {@macro flutter.services.TextInputConfiguration.smartDashesType}
final SmartDashesType smartDashesType;
/// {@macro flutter.services.TextInputConfiguration.smartQuotesType}
final SmartQuotesType smartQuotesType;
/// {@macro flutter.services.TextInputConfiguration.enableSuggestions}
final bool enableSuggestions;
/// The text style to use for the editable text.
final TextStyle style;
/// {@template flutter.widgets.editableText.strutStyle}
/// The strut style used for the vertical layout.
///
/// [StrutStyle] is used to establish a predictable vertical layout.
/// Since fonts may vary depending on user input and due to font
/// fallback, [StrutStyle.forceStrutHeight] is enabled by default
/// to lock all lines to the height of the base [TextStyle], provided by
/// [style]. This ensures the typed text fits within the allotted space.
///
/// If null, the strut used will inherit values from the [style] and will
/// have [StrutStyle.forceStrutHeight] set to true. When no [style] is
/// passed, the theme's [TextStyle] will be used to generate [strutStyle]
/// instead.
///
/// To disable strut-based vertical alignment and allow dynamic vertical
/// layout based on the glyphs typed, use [StrutStyle.disabled].
///
/// Flutter's strut is based on [typesetting strut](https://en.wikipedia.org/wiki/Strut_(typesetting))
/// and CSS's [line-height](https://www.w3.org/TR/CSS2/visudet.html#line-height).
/// {@endtemplate}
///
/// Within editable text and text fields, [StrutStyle] will not use its standalone
/// default values, and will instead inherit omitted/null properties from the
/// [TextStyle] instead. See [StrutStyle.inheritFromTextStyle].
StrutStyle get strutStyle {
if (_strutStyle == null) {
return StrutStyle.fromTextStyle(style, forceStrutHeight: true);
}
return _strutStyle!.inheritFromTextStyle(style);
}
final StrutStyle? _strutStyle;
/// {@template flutter.widgets.editableText.textAlign}
/// How the text should be aligned horizontally.
///
/// Defaults to [TextAlign.start] and cannot be null.
/// {@endtemplate}
final TextAlign textAlign;
/// {@template flutter.widgets.editableText.textDirection}
/// The directionality of the text.
///
/// This decides how [textAlign] values like [TextAlign.start] and
/// [TextAlign.end] are interpreted.
///
/// This is also used to disambiguate how to render bidirectional text. For
/// example, if the text is an English phrase followed by a Hebrew phrase,
/// in a [TextDirection.ltr] context the English phrase will be on the left
/// and the Hebrew phrase to its right, while in a [TextDirection.rtl]
/// context, the English phrase will be on the right and the Hebrew phrase on
/// its left.
///
/// Defaults to the ambient [Directionality], if any.
/// {@endtemplate}
final TextDirection? textDirection;
/// {@template flutter.widgets.editableText.textCapitalization}
/// Configures how the platform keyboard will select an uppercase or
/// lowercase keyboard.
///
/// Only supports text keyboards, other keyboard types will ignore this
/// configuration. Capitalization is locale-aware.
///
/// Defaults to [TextCapitalization.none]. Must not be null.
///
/// See also:
///
/// * [TextCapitalization], for a description of each capitalization behavior.
///
/// {@endtemplate}
final TextCapitalization textCapitalization;
/// Used to select a font when the same Unicode character can
/// be rendered differently, depending on the locale.
///
/// It's rarely necessary to set this property. By default its value
/// is inherited from the enclosing app with `Localizations.localeOf(context)`.
///
/// See [RenderEditable.locale] for more information.
final Locale? locale;
/// {@template flutter.widgets.editableText.textScaleFactor}
/// The number of font pixels for each logical pixel.
///
/// For example, if the text scale factor is 1.5, text will be 50% larger than
/// the specified font size.
///
/// Defaults to the [MediaQueryData.textScaleFactor] obtained from the ambient
/// [MediaQuery], or 1.0 if there is no [MediaQuery] in scope.
/// {@endtemplate}
final double? textScaleFactor;
/// The color to use when painting the cursor.
///
/// Cannot be null.
final Color cursorColor;
/// The color to use when painting the autocorrection Rect.
///
/// For [CupertinoTextField]s, the value is set to the ambient
/// [CupertinoThemeData.primaryColor] with 20% opacity. For [TextField]s, the
/// value is null on non-iOS platforms and the same color used in [CupertinoTextField]
/// on iOS.
///
/// Currently the autocorrection Rect only appears on iOS.
///
/// Defaults to null, which disables autocorrection Rect painting.
final Color? autocorrectionTextRectColor;
/// The color to use when painting the background cursor aligned with the text
/// while rendering the floating cursor.
///
/// Cannot be null. By default it is the disabled grey color from
/// CupertinoColors.
final Color backgroundCursorColor;
/// {@template flutter.widgets.editableText.maxLines}
/// The maximum number of lines to show at one time, wrapping if necessary.
///
/// This affects the height of the field itself and does not limit the number
/// of lines that can be entered into the field.
///
/// If this is 1 (the default), the text will not wrap, but will scroll
/// horizontally instead.
///
/// If this is null, there is no limit to the number of lines, and the text
/// container will start with enough vertical space for one line and
/// automatically grow to accommodate additional lines as they are entered, up
/// to the height of its constraints.
///
/// If this is not null, the value must be greater than zero, and it will lock
/// the input to the given number of lines and take up enough horizontal space
/// to accommodate that number of lines. Setting [minLines] as well allows the
/// input to grow and shrink between the indicated range.
///
/// The full set of behaviors possible with [minLines] and [maxLines] are as
/// follows. These examples apply equally to [TextField], [TextFormField],
/// [CupertinoTextField], and [EditableText].
///
/// Input that occupies a single line and scrolls horizontally as needed.
/// ```dart
/// TextField()
/// ```
///
/// Input whose height grows from one line up to as many lines as needed for
/// the text that was entered. If a height limit is imposed by its parent, it
/// will scroll vertically when its height reaches that limit.
/// ```dart
/// TextField(maxLines: null)
/// ```
///
/// The input's height is large enough for the given number of lines. If
/// additional lines are entered the input scrolls vertically.
/// ```dart
/// TextField(maxLines: 2)
/// ```
///
/// Input whose height grows with content between a min and max. An infinite
/// max is possible with `maxLines: null`.
/// ```dart
/// TextField(minLines: 2, maxLines: 4)
/// ```
///
/// See also:
///
/// * [minLines], which sets the minimum number of lines visible.
/// {@endtemplate}
/// * [expands], which determines whether the field should fill the height of
/// its parent.
final int? maxLines;
/// {@template flutter.widgets.editableText.minLines}
/// The minimum number of lines to occupy when the content spans fewer lines.
///
/// This affects the height of the field itself and does not limit the number
/// of lines that can be entered into the field.
///
/// If this is null (default), text container starts with enough vertical space
/// for one line and grows to accommodate additional lines as they are entered.
///
/// This can be used in combination with [maxLines] for a varying set of behaviors.
///
/// If the value is set, it must be greater than zero. If the value is greater
/// than 1, [maxLines] should also be set to either null or greater than
/// this value.
///
/// When [maxLines] is set as well, the height will grow between the indicated
/// range of lines. When [maxLines] is null, it will grow as high as needed,
/// starting from [minLines].
///
/// A few examples of behaviors possible with [minLines] and [maxLines] are as follows.
/// These apply equally to [TextField], [TextFormField], [CupertinoTextField],
/// and [EditableText].
///
/// Input that always occupies at least 2 lines and has an infinite max.
/// Expands vertically as needed.
/// ```dart
/// TextField(minLines: 2)
/// ```
///
/// Input whose height starts from 2 lines and grows up to 4 lines at which
/// point the height limit is reached. If additional lines are entered it will
/// scroll vertically.
/// ```dart
/// TextField(minLines:2, maxLines: 4)
/// ```
///
/// Defaults to null.
///
/// See also:
///
/// * [maxLines], which sets the maximum number of lines visible, and has
/// several examples of how minLines and maxLines interact to produce
/// various behaviors.
/// {@endtemplate}
/// * [expands], which determines whether the field should fill the height of
/// its parent.
final int? minLines;
/// {@template flutter.widgets.editableText.expands}
/// Whether this widget's height will be sized to fill its parent.
///
/// If set to true and wrapped in a parent widget like [Expanded] or
/// [SizedBox], the input will expand to fill the parent.
///
/// [maxLines] and [minLines] must both be null when this is set to true,
/// otherwise an error is thrown.
///
/// Defaults to false.
///
/// See the examples in [maxLines] for the complete picture of how [maxLines],
/// [minLines], and [expands] interact to produce various behaviors.
///
/// Input that matches the height of its parent:
/// ```dart
/// Expanded(
/// child: TextField(maxLines: null, expands: true),
/// )
/// ```
/// {@endtemplate}
final bool expands;
/// {@template flutter.widgets.editableText.autofocus}
/// Whether this text field should focus itself if nothing else is already
/// focused.
///
/// If true, the keyboard will open as soon as this text field obtains focus.
/// Otherwise, the keyboard is only shown after the user taps the text field.
///
/// Defaults to false. Cannot be null.
/// {@endtemplate}
// See https://github.com/flutter/flutter/issues/7035 for the rationale for this
// keyboard behavior.
final bool autofocus;
/// The color to use when painting the selection.
///
/// If this property is null, this widget gets the selection color from the
/// [DefaultSelectionStyle].
///
/// For [CupertinoTextField]s, the value is set to the ambient
/// [CupertinoThemeData.primaryColor] with 20% opacity. For [TextField]s, the
/// value is set to the ambient [TextSelectionThemeData.selectionColor].
final Color? selectionColor;
/// {@template flutter.widgets.editableText.selectionControls}
/// Optional delegate for building the text selection handles and toolbar.
///
/// The [EditableText] widget used on its own will not trigger the display
/// of the selection toolbar by itself. The toolbar is shown by calling
/// [EditableTextState.showToolbar] in response to an appropriate user event.
///
/// See also:
///
/// * [CupertinoTextField], which wraps an [EditableText] and which shows the
/// selection toolbar upon user events that are appropriate on the iOS
/// platform.
/// * [TextField], a Material Design themed wrapper of [EditableText], which
/// shows the selection toolbar upon appropriate user events based on the
/// user's platform set in [ThemeData.platform].
/// {@endtemplate}
final TextSelectionControls? selectionControls;
/// {@template flutter.widgets.editableText.keyboardType}
/// The type of keyboard to use for editing the text.
///
/// Defaults to [TextInputType.text] if [maxLines] is one and
/// [TextInputType.multiline] otherwise.
/// {@endtemplate}
final TextInputType keyboardType;
/// The type of action button to use with the soft keyboard.
final TextInputAction? textInputAction;
/// {@template flutter.widgets.editableText.onChanged}
/// Called when the user initiates a change to the TextField's
/// value: when they have inserted or deleted text.
///
/// This callback doesn't run when the TextField's text is changed
/// programmatically, via the TextField's [controller]. Typically it
/// isn't necessary to be notified of such changes, since they're
/// initiated by the app itself.
///
/// To be notified of all changes to the TextField's text, cursor,
/// and selection, one can add a listener to its [controller] with
/// [TextEditingController.addListener].
///
/// [onChanged] is called before [onSubmitted] when user indicates completion
/// of editing, such as when pressing the "done" button on the keyboard. That default
/// behavior can be overridden. See [onEditingComplete] for details.
///
/// {@tool dartpad}
/// This example shows how onChanged could be used to check the TextField's
/// current value each time the user inserts or deletes a character.
///
/// ** See code in examples/api/lib/widgets/editable_text/editable_text.on_changed.0.dart **
/// {@end-tool}
/// {@endtemplate}
///
/// ## Handling emojis and other complex characters
/// {@template flutter.widgets.EditableText.onChanged}
/// It's important to always use
/// [characters](https://pub.dev/packages/characters) when dealing with user
/// input text that may contain complex characters. This will ensure that
/// extended grapheme clusters and surrogate pairs are treated as single
/// characters, as they appear to the user.
///
/// For example, when finding the length of some user input, use
/// `string.characters.length`. Do NOT use `string.length` or even
/// `string.runes.length`. For the complex character "👨‍👩‍👦", this
/// appears to the user as a single character, and `string.characters.length`
/// intuitively returns 1. On the other hand, `string.length` returns 8, and
/// `string.runes.length` returns 5!
/// {@endtemplate}
///
/// See also:
///
/// * [inputFormatters], which are called before [onChanged]
/// runs and can validate and change ("format") the input value.
/// * [onEditingComplete], [onSubmitted], [onSelectionChanged]:
/// which are more specialized input change notifications.
final ValueChanged<String>? onChanged;
/// {@template flutter.widgets.editableText.onEditingComplete}
/// Called when the user submits editable content (e.g., user presses the "done"
/// button on the keyboard).
///
/// The default implementation of [onEditingComplete] executes 2 different
/// behaviors based on the situation:
///
/// - When a completion action is pressed, such as "done", "go", "send", or
/// "search", the user's content is submitted to the [controller] and then
/// focus is given up.
///
/// - When a non-completion action is pressed, such as "next" or "previous",
/// the user's content is submitted to the [controller], but focus is not
/// given up because developers may want to immediately move focus to
/// another input widget within [onSubmitted].
///
/// Providing [onEditingComplete] prevents the aforementioned default behavior.
/// {@endtemplate}
final VoidCallback? onEditingComplete;
/// {@template flutter.widgets.editableText.onSubmitted}
/// Called when the user indicates that they are done editing the text in the
/// field.
///
/// By default, [onSubmitted] is called after [onChanged] when the user
/// has finalized editing; or, if the default behavior has been overridden,
/// after [onEditingComplete]. See [onEditingComplete] for details.
/// {@endtemplate}
final ValueChanged<String>? onSubmitted;
/// {@template flutter.widgets.editableText.onAppPrivateCommand}
/// This is used to receive a private command from the input method.
///
/// Called when the result of [TextInputClient.performPrivateCommand] is
/// received.
///
/// This can be used to provide domain-specific features that are only known
/// between certain input methods and their clients.
///
/// See also:
/// * [performPrivateCommand](https://developer.android.com/reference/android/view/inputmethod/InputConnection#performPrivateCommand\(java.lang.String,%20android.os.Bundle\)),
/// which is the Android documentation for performPrivateCommand, used to
/// send a command from the input method.
/// * [sendAppPrivateCommand](https://developer.android.com/reference/android/view/inputmethod/InputMethodManager#sendAppPrivateCommand),
/// which is the Android documentation for sendAppPrivateCommand, used to
/// send a command to the input method.
/// {@endtemplate}
final AppPrivateCommandCallback? onAppPrivateCommand;
/// {@template flutter.widgets.editableText.onSelectionChanged}
/// Called when the user changes the selection of text (including the cursor
/// location).
/// {@endtemplate}
final SelectionChangedCallback? onSelectionChanged;
/// {@macro flutter.widgets.SelectionOverlay.onSelectionHandleTapped}
final VoidCallback? onSelectionHandleTapped;
/// {@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.
/// {@endtemplate}
final List<TextInputFormatter>? inputFormatters;
/// The cursor for a mouse pointer when it enters or is hovering over the
/// widget.
///
/// If this property is null, [SystemMouseCursors.text] will be used.
///
/// The [mouseCursor] is the only property of [EditableText] that controls the
/// appearance of the mouse pointer. All other properties related to "cursor"
/// stands for the text cursor, which is usually a blinking vertical line at
/// the editing position.
final MouseCursor? mouseCursor;
/// If true, the [RenderEditable] created by this widget will not handle
/// pointer events, see [RenderEditable] and [RenderEditable.ignorePointer].
///
/// This property is false by default.
final bool rendererIgnoresPointer;
/// {@template flutter.widgets.editableText.cursorWidth}
/// How thick the cursor will be.
///
/// Defaults to 2.0.
///
/// The cursor will draw under the text. The cursor width will extend
/// to the right of the boundary between characters for left-to-right text
/// and to the left for right-to-left text. This corresponds to extending
/// downstream relative to the selected position. Negative values may be used
/// to reverse this behavior.
/// {@endtemplate}
final double cursorWidth;
/// {@template flutter.widgets.editableText.cursorHeight}
/// How tall the cursor will be.
///
/// If this property is null, [RenderEditable.preferredLineHeight] will be used.
/// {@endtemplate}
final double? cursorHeight;
/// {@template flutter.widgets.editableText.cursorRadius}
/// How rounded the corners of the cursor should be.
///
/// By default, the cursor has no radius.
/// {@endtemplate}
final Radius? cursorRadius;
/// Whether the cursor will animate from fully transparent to fully opaque
/// during each cursor blink.
///
/// By default, the cursor opacity will animate on iOS platforms and will not
/// animate on Android platforms.
final bool cursorOpacityAnimates;
///{@macro flutter.rendering.RenderEditable.cursorOffset}
final Offset? cursorOffset;
///{@macro flutter.rendering.RenderEditable.paintCursorAboveText}
final bool paintCursorAboveText;
/// Controls how tall the selection highlight boxes are computed to be.
///
/// See [ui.BoxHeightStyle] for details on available styles.
final ui.BoxHeightStyle selectionHeightStyle;
/// Controls how wide the selection highlight boxes are computed to be.
///
/// See [ui.BoxWidthStyle] for details on available styles.
final ui.BoxWidthStyle selectionWidthStyle;
/// The appearance of the keyboard.
///
/// This setting is only honored on iOS devices.
///
/// Defaults to [Brightness.light].
final Brightness keyboardAppearance;
/// {@template flutter.widgets.editableText.scrollPadding}
/// Configures padding to edges surrounding a [Scrollable] when the Textfield scrolls into view.
///
/// When this widget receives focus and is not completely visible (for example scrolled partially
/// off the screen or overlapped by the keyboard)
/// then it will attempt to make itself visible by scrolling a surrounding [Scrollable], if one is present.
/// This value controls how far from the edges of a [Scrollable] the TextField will be positioned after the scroll.
///
/// Defaults to EdgeInsets.all(20.0).
/// {@endtemplate}
final EdgeInsets scrollPadding;
/// {@template flutter.widgets.editableText.enableInteractiveSelection}
/// Whether to enable user interface affordances for changing the
/// text selection.
///
/// For example, setting this to true will enable features such as
/// long-pressing the TextField to select text and show the
/// cut/copy/paste menu, and tapping to move the text caret.
///
/// When this is false, the text selection cannot be adjusted by
/// the user, text cannot be copied, and the user cannot paste into
/// the text field from the clipboard.
///
/// Defaults to true.
/// {@endtemplate}
final bool enableInteractiveSelection;
/// Setting this property to true makes the cursor stop blinking or fading
/// on and off once the cursor appears on focus. This property is useful for
/// testing purposes.
///
/// It does not affect the necessity to focus the EditableText for the cursor
/// to appear in the first place.
///
/// Defaults to false, resulting in a typical blinking cursor.
static bool debugDeterministicCursor = false;
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
final DragStartBehavior dragStartBehavior;
/// {@template flutter.widgets.editableText.scrollController}
/// The [ScrollController] to use when vertically scrolling the input.
///
/// If null, it will instantiate a new ScrollController.
///
/// See [Scrollable.controller].
/// {@endtemplate}
final ScrollController? scrollController;
/// {@template flutter.widgets.editableText.scrollPhysics}
/// The [ScrollPhysics] to use when vertically scrolling the input.
///
/// If not specified, it will behave according to the current platform.
///
/// See [Scrollable.physics].
/// {@endtemplate}
///
/// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the
/// [ScrollPhysics] provided by that behavior will take precedence after
/// [scrollPhysics].
final ScrollPhysics? scrollPhysics;
/// {@template flutter.widgets.editableText.scribbleEnabled}
/// Whether iOS 14 Scribble features are enabled for this widget.
///
/// Only available on iPads.
///
/// Defaults to true.
/// {@endtemplate}
final bool scribbleEnabled;
/// {@template flutter.widgets.editableText.selectionEnabled}
/// Same as [enableInteractiveSelection].
///
/// This getter exists primarily for consistency with
/// [RenderEditable.selectionEnabled].
/// {@endtemplate}
bool get selectionEnabled => enableInteractiveSelection;
/// {@template flutter.widgets.editableText.autofillHints}
/// A list of strings that helps the autofill service identify the type of this
/// text input.
///
/// When set to null, this text input will not send its autofill information
/// to the platform, preventing it from participating in autofills triggered
/// by a different [AutofillClient], even if they're in the same
/// [AutofillScope]. Additionally, on Android and web, setting this to null
/// will disable autofill for this text field.
///
/// The minimum platform SDK version that supports Autofill is API level 26
/// for Android, and iOS 10.0 for iOS.
///
/// Defaults to an empty list.
///
/// ### Setting up iOS autofill:
///
/// To provide the best user experience and ensure your app fully supports
/// password autofill on iOS, follow these steps:
///
/// * Set up your iOS app's
/// [associated domains](https://developer.apple.com/documentation/safariservices/supporting_associated_domains_in_your_app).
/// * Some autofill hints only work with specific [keyboardType]s. For example,
/// [AutofillHints.name] requires [TextInputType.name] and [AutofillHints.email]
/// works only with [TextInputType.emailAddress]. Make sure the input field has a
/// compatible [keyboardType]. Empirically, [TextInputType.name] works well
/// with many autofill hints that are predefined on iOS.
///
/// ### Troubleshooting Autofill
///
/// Autofill service providers rely heavily on [autofillHints]. Make sure the
/// entries in [autofillHints] are supported by the autofill service currently
/// in use (the name of the service can typically be found in your mobile
/// device's system settings).
///
/// #### Autofill UI refuses to show up when I tap on the text field
///
/// Check the device's system settings and make sure autofill is turned on,
/// and there are available credentials stored in the autofill service.
///
/// * iOS password autofill: Go to Settings -> Password, turn on "Autofill
/// Passwords", and add new passwords for testing by pressing the top right
/// "+" button. Use an arbitrary "website" if you don't have associated
/// domains set up for your app. As long as there's at least one password
/// stored, you should be able to see a key-shaped icon in the quick type
/// bar on the software keyboard, when a password related field is focused.
///
/// * iOS contact information autofill: iOS seems to pull contact info from
/// the Apple ID currently associated with the device. Go to Settings ->
/// Apple ID (usually the first entry, or "Sign in to your iPhone" if you
/// haven't set up one on the device), and fill out the relevant fields. If
/// you wish to test more contact info types, try adding them in Contacts ->
/// My Card.
///
/// * Android autofill: Go to Settings -> System -> Languages & input ->
/// Autofill service. Enable the autofill service of your choice, and make
/// sure there are available credentials associated with your app.
///
/// #### I called `TextInput.finishAutofillContext` but the autofill save
/// prompt isn't showing
///
/// * iOS: iOS may not show a prompt or any other visual indication when it
/// saves user password. Go to Settings -> Password and check if your new
/// password is saved. Neither saving password nor auto-generating strong
/// password works without properly setting up associated domains in your
/// app. To set up associated domains, follow the instructions in
/// <https://developer.apple.com/documentation/safariservices/supporting_associated_domains_in_your_app>.
///
/// {@endtemplate}
/// {@macro flutter.services.AutofillConfiguration.autofillHints}
final Iterable<String>? autofillHints;
/// The [AutofillClient] that controls this input field's autofill behavior.
///
/// When null, this widget's [EditableTextState] will be used as the
/// [AutofillClient]. This property may override [autofillHints].
final AutofillClient? autofillClient;
/// {@macro flutter.material.Material.clipBehavior}
///
/// Defaults to [Clip.hardEdge].
final Clip clipBehavior;
/// Restoration ID to save and restore the scroll offset of the
/// [EditableText].
///
/// If a restoration id is provided, the [EditableText] will persist its
/// current scroll offset and restore it during state restoration.
///
/// The scroll offset is persisted in a [RestorationBucket] claimed from
/// the surrounding [RestorationScope] using the provided restoration ID.
///
/// Persisting and restoring the content of the [EditableText] is the
/// responsibility of the owner of the [controller], who may use a
/// [RestorableTextEditingController] for that purpose.
///
/// See also:
///
/// * [RestorationManager], which explains how state restoration works in
/// Flutter.
final String? restorationId;
/// {@template flutter.widgets.shadow.scrollBehavior}
/// A [ScrollBehavior] that will be applied to this widget individually.
///
/// Defaults to null, wherein the inherited [ScrollBehavior] is copied and
/// modified to alter the viewport decoration, like [Scrollbar]s.
/// {@endtemplate}
///
/// [ScrollBehavior]s also provide [ScrollPhysics]. If an explicit
/// [ScrollPhysics] is provided in [scrollPhysics], it will take precedence,
/// followed by [scrollBehavior], and then the inherited ancestor
/// [ScrollBehavior].
///
/// The [ScrollBehavior] of the inherited [ScrollConfiguration] will be
/// modified by default to only apply a [Scrollbar] if [maxLines] is greater
/// than 1.
final ScrollBehavior? scrollBehavior;
/// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning}
final bool enableIMEPersonalizedLearning;
bool get _userSelectionEnabled => enableInteractiveSelection && (!readOnly || !obscureText);
// Infer the keyboard type of an `EditableText` if it's not specified.
static TextInputType _inferKeyboardType({
required Iterable<String>? autofillHints,
required int? maxLines,
}) {
if (autofillHints == null || autofillHints.isEmpty) {
return maxLines == 1 ? TextInputType.text : TextInputType.multiline;
}
final String effectiveHint = autofillHints.first;
// On iOS oftentimes specifying a text content type is not enough to qualify
// the input field for autofill. The keyboard type also needs to be compatible
// with the content type. To get autofill to work by default on EditableText,
// the keyboard type inference on iOS is done differently from other platforms.
//
// The entries with "autofill not working" comments are the iOS text content
// types that should work with the specified keyboard type but won't trigger
// (even within a native app). Tested on iOS 13.5.
if (!kIsWeb) {
switch (defaultTargetPlatform) {
case TargetPlatform.iOS:
case TargetPlatform.macOS:
const Map<String, TextInputType> iOSKeyboardType = <String, TextInputType> {
AutofillHints.addressCity : TextInputType.name,
AutofillHints.addressCityAndState : TextInputType.name, // Autofill not working.
AutofillHints.addressState : TextInputType.name,
AutofillHints.countryName : TextInputType.name,
AutofillHints.creditCardNumber : TextInputType.number, // Couldn't test.
AutofillHints.email : TextInputType.emailAddress,
AutofillHints.familyName : TextInputType.name,
AutofillHints.fullStreetAddress : TextInputType.name,
AutofillHints.givenName : TextInputType.name,
AutofillHints.jobTitle : TextInputType.name, // Autofill not working.
AutofillHints.location : TextInputType.name, // Autofill not working.
AutofillHints.middleName : TextInputType.name, // Autofill not working.
AutofillHints.name : TextInputType.name,
AutofillHints.namePrefix : TextInputType.name, // Autofill not working.
AutofillHints.nameSuffix : TextInputType.name, // Autofill not working.
AutofillHints.newPassword : TextInputType.text,
AutofillHints.newUsername : TextInputType.text,
AutofillHints.nickname : TextInputType.name, // Autofill not working.
AutofillHints.oneTimeCode : TextInputType.number,
AutofillHints.organizationName : TextInputType.text, // Autofill not working.
AutofillHints.password : TextInputType.text,
AutofillHints.postalCode : TextInputType.name,
AutofillHints.streetAddressLine1 : TextInputType.name,
AutofillHints.streetAddressLine2 : TextInputType.name, // Autofill not working.
AutofillHints.sublocality : TextInputType.name, // Autofill not working.
AutofillHints.telephoneNumber : TextInputType.name,
AutofillHints.url : TextInputType.url, // Autofill not working.
AutofillHints.username : TextInputType.text,
};
final TextInputType? keyboardType = iOSKeyboardType[effectiveHint];
if (keyboardType != null) {
return keyboardType;
}
break;
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
break;
}
}
if (maxLines != 1) {
return TextInputType.multiline;
}
const Map<String, TextInputType> inferKeyboardType = <String, TextInputType> {
AutofillHints.addressCity : TextInputType.streetAddress,
AutofillHints.addressCityAndState : TextInputType.streetAddress,
AutofillHints.addressState : TextInputType.streetAddress,
AutofillHints.birthday : TextInputType.datetime,
AutofillHints.birthdayDay : TextInputType.datetime,
AutofillHints.birthdayMonth : TextInputType.datetime,
AutofillHints.birthdayYear : TextInputType.datetime,
AutofillHints.countryCode : TextInputType.number,
AutofillHints.countryName : TextInputType.text,
AutofillHints.creditCardExpirationDate : TextInputType.datetime,
AutofillHints.creditCardExpirationDay : TextInputType.datetime,
AutofillHints.creditCardExpirationMonth : TextInputType.datetime,
AutofillHints.creditCardExpirationYear : TextInputType.datetime,
AutofillHints.creditCardFamilyName : TextInputType.name,
AutofillHints.creditCardGivenName : TextInputType.name,
AutofillHints.creditCardMiddleName : TextInputType.name,
AutofillHints.creditCardName : TextInputType.name,
AutofillHints.creditCardNumber : TextInputType.number,
AutofillHints.creditCardSecurityCode : TextInputType.number,
AutofillHints.creditCardType : TextInputType.text,
AutofillHints.email : TextInputType.emailAddress,
AutofillHints.familyName : TextInputType.name,
AutofillHints.fullStreetAddress : TextInputType.streetAddress,
AutofillHints.gender : TextInputType.text,
AutofillHints.givenName : TextInputType.name,
AutofillHints.impp : TextInputType.url,
AutofillHints.jobTitle : TextInputType.text,
AutofillHints.language : TextInputType.text,
AutofillHints.location : TextInputType.streetAddress,
AutofillHints.middleInitial : TextInputType.name,
AutofillHints.middleName : TextInputType.name,
AutofillHints.name : TextInputType.name,
AutofillHints.namePrefix : TextInputType.name,
AutofillHints.nameSuffix : TextInputType.name,
AutofillHints.newPassword : TextInputType.text,
AutofillHints.newUsername : TextInputType.text,
AutofillHints.nickname : TextInputType.text,
AutofillHints.oneTimeCode : TextInputType.text,
AutofillHints.organizationName : TextInputType.text,
AutofillHints.password : TextInputType.text,
AutofillHints.photo : TextInputType.text,
AutofillHints.postalAddress : TextInputType.streetAddress,
AutofillHints.postalAddressExtended : TextInputType.streetAddress,
AutofillHints.postalAddressExtendedPostalCode : TextInputType.number,
AutofillHints.postalCode : TextInputType.number,
AutofillHints.streetAddressLevel1 : TextInputType.streetAddress,
AutofillHints.streetAddressLevel2 : TextInputType.streetAddress,
AutofillHints.streetAddressLevel3 : TextInputType.streetAddress,
AutofillHints.streetAddressLevel4 : TextInputType.streetAddress,
AutofillHints.streetAddressLine1 : TextInputType.streetAddress,
AutofillHints.streetAddressLine2 : TextInputType.streetAddress,
AutofillHints.streetAddressLine3 : TextInputType.streetAddress,
AutofillHints.sublocality : TextInputType.streetAddress,
AutofillHints.telephoneNumber : TextInputType.phone,
AutofillHints.telephoneNumberAreaCode : TextInputType.phone,
AutofillHints.telephoneNumberCountryCode : TextInputType.phone,
AutofillHints.telephoneNumberDevice : TextInputType.phone,
AutofillHints.telephoneNumberExtension : TextInputType.phone,
AutofillHints.telephoneNumberLocal : TextInputType.phone,
AutofillHints.telephoneNumberLocalPrefix : TextInputType.phone,
AutofillHints.telephoneNumberLocalSuffix : TextInputType.phone,
AutofillHints.telephoneNumberNational : TextInputType.phone,
AutofillHints.transactionAmount : TextInputType.numberWithOptions(decimal: true),
AutofillHints.transactionCurrency : TextInputType.text,
AutofillHints.url : TextInputType.url,
AutofillHints.username : TextInputType.text,
};
return inferKeyboardType[effectiveHint] ?? TextInputType.text;
}
@override
EditableTextState createState() => EditableTextState();
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<TextEditingController>('controller', controller));
properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode));
properties.add(DiagnosticsProperty<bool>('obscureText', obscureText, defaultValue: false));
properties.add(DiagnosticsProperty<bool>('readOnly', readOnly, defaultValue: false));
properties.add(DiagnosticsProperty<bool>('autocorrect', autocorrect, defaultValue: true));
properties.add(EnumProperty<SmartDashesType>('smartDashesType', smartDashesType, defaultValue: obscureText ? SmartDashesType.disabled : SmartDashesType.enabled));
properties.add(EnumProperty<SmartQuotesType>('smartQuotesType', smartQuotesType, defaultValue: obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled));
properties.add(DiagnosticsProperty<bool>('enableSuggestions', enableSuggestions, defaultValue: true));
style.debugFillProperties(properties);
properties.add(EnumProperty<TextAlign>('textAlign', textAlign, defaultValue: null));
properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
properties.add(DiagnosticsProperty<Locale>('locale', locale, defaultValue: null));
properties.add(DoubleProperty('textScaleFactor', textScaleFactor, defaultValue: null));
properties.add(IntProperty('maxLines', maxLines, defaultValue: 1));
properties.add(IntProperty('minLines', minLines, defaultValue: null));
properties.add(DiagnosticsProperty<bool>('expands', expands, defaultValue: false));
properties.add(DiagnosticsProperty<bool>('autofocus', autofocus, defaultValue: false));
properties.add(DiagnosticsProperty<TextInputType>('keyboardType', keyboardType, defaultValue: null));
properties.add(DiagnosticsProperty<ScrollController>('scrollController', scrollController, defaultValue: null));
properties.add(DiagnosticsProperty<ScrollPhysics>('scrollPhysics', scrollPhysics, defaultValue: null));
properties.add(DiagnosticsProperty<Iterable<String>>('autofillHints', autofillHints, defaultValue: null));
properties.add(DiagnosticsProperty<TextHeightBehavior>('textHeightBehavior', textHeightBehavior, defaultValue: null));
properties.add(DiagnosticsProperty<bool>('scribbleEnabled', scribbleEnabled, defaultValue: true));
properties.add(DiagnosticsProperty<bool>('enableIMEPersonalizedLearning', enableIMEPersonalizedLearning, defaultValue: true));
properties.add(DiagnosticsProperty<bool>('enableInteractiveSelection', enableInteractiveSelection, defaultValue: true));
}
}
/// State for a [EditableText].
class EditableTextState extends State<EditableText> with AutomaticKeepAliveClientMixin<EditableText>, WidgetsBindingObserver, TickerProviderStateMixin<EditableText>, TextSelectionDelegate implements TextInputClient, AutofillClient {
Timer? _cursorTimer;
bool _targetCursorVisibility = false;
final ValueNotifier<bool> _cursorVisibilityNotifier = ValueNotifier<bool>(true);
final GlobalKey _editableKey = GlobalKey();
final ClipboardStatusNotifier? _clipboardStatus = kIsWeb ? null : ClipboardStatusNotifier();
TextInputConnection? _textInputConnection;
TextSelectionOverlay? _selectionOverlay;
ScrollController? _internalScrollController;
ScrollController get _scrollController => widget.scrollController ?? (_internalScrollController ??= ScrollController());
AnimationController? _cursorBlinkOpacityController;
final LayerLink _toolbarLayerLink = LayerLink();
final LayerLink _startHandleLayerLink = LayerLink();
final LayerLink _endHandleLayerLink = LayerLink();
bool _didAutoFocus = false;
AutofillGroupState? _currentAutofillScope;
@override
AutofillScope? get currentAutofillScope => _currentAutofillScope;
AutofillClient get _effectiveAutofillClient => widget.autofillClient ?? this;
/// Whether to create an input connection with the platform for text editing
/// or not.
///
/// Read-only input fields do not need a connection with the platform since
/// there's no need for text editing capabilities (e.g. virtual keyboard).
///
/// On the web, we always need a connection because we want some browser
/// functionalities to continue to work on read-only input fields like:
///
/// - Relevant context menu.
/// - cmd/ctrl+c shortcut to copy.
/// - cmd/ctrl+a to select all.
/// - Changing the selection using a physical keyboard.
bool get _shouldCreateInputConnection => kIsWeb || !widget.readOnly;
// This value is an eyeball estimation of the time it takes for the iOS cursor
// to ease in and out.
static const Duration _fadeDuration = Duration(milliseconds: 250);
// The time it takes for the floating cursor to snap to the text aligned
// cursor position after the user has finished placing it.
static const Duration _floatingCursorResetTime = Duration(milliseconds: 125);
AnimationController? _floatingCursorResetController;
@override
bool get wantKeepAlive => widget.focusNode.hasFocus;
Color get _cursorColor => widget.cursorColor.withOpacity(_cursorBlinkOpacityController!.value);
@override
bool get cutEnabled => widget.toolbarOptions.cut && !widget.readOnly && !widget.obscureText;
@override
bool get copyEnabled => widget.toolbarOptions.copy && !widget.obscureText;
@override
bool get pasteEnabled => widget.toolbarOptions.paste && !widget.readOnly;
@override
bool get selectAllEnabled => widget.toolbarOptions.selectAll && (!widget.readOnly || !widget.obscureText) && widget.enableInteractiveSelection;
void _onChangedClipboardStatus() {
setState(() {
// Inform the widget that the value of clipboardStatus has changed.
});
}
TextEditingValue get _textEditingValueforTextLayoutMetrics {
final Widget? editableWidget =_editableKey.currentContext?.widget;
if (editableWidget is! _Editable) {
throw StateError('_Editable must be mounted.');
}
return editableWidget.value;
}
/// Copy current selection to [Clipboard].
@override
void copySelection(SelectionChangedCause cause) {
final TextSelection selection = textEditingValue.selection;
assert(selection != null);
if (selection.isCollapsed || widget.obscureText) {
return;
}
final String text = textEditingValue.text;
Clipboard.setData(ClipboardData(text: selection.textInside(text)));
if (cause == SelectionChangedCause.toolbar) {
bringIntoView(textEditingValue.selection.extent);
hideToolbar(false);
switch (defaultTargetPlatform) {
case TargetPlatform.iOS:
break;
case TargetPlatform.macOS:
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
// Collapse the selection and hide the toolbar and handles.
userUpdateTextEditingValue(
TextEditingValue(
text: textEditingValue.text,
selection: TextSelection.collapsed(offset: textEditingValue.selection.end),
),
SelectionChangedCause.toolbar,
);
break;
}
}
_clipboardStatus?.update();
}
/// Cut current selection to [Clipboard].
@override
void cutSelection(SelectionChangedCause cause) {
if (widget.readOnly || widget.obscureText) {
return;
}
final TextSelection selection = textEditingValue.selection;
final String text = textEditingValue.text;
assert(selection != null);
if (selection.isCollapsed) {
return;
}
Clipboard.setData(ClipboardData(text: selection.textInside(text)));
_replaceText(ReplaceTextIntent(textEditingValue, '', selection, cause));
if (cause == SelectionChangedCause.toolbar) {
// Schedule a call to bringIntoView() after renderEditable updates.
SchedulerBinding.instance.addPostFrameCallback((_) {
if (mounted) {
bringIntoView(textEditingValue.selection.extent);
}
});
hideToolbar();
}
_clipboardStatus?.update();
}
/// Paste text from [Clipboard].
@override
Future<void> pasteText(SelectionChangedCause cause) async {
if (widget.readOnly) {
return;
}
final TextSelection selection = textEditingValue.selection;
assert(selection != null);
if (!selection.isValid) {
return;
}
// Snapshot the input before using `await`.
// See https://github.com/flutter/flutter/issues/11427
final ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
if (data == null) {
return;
}
// After the paste, the cursor should be collapsed and located after the
// pasted content.
final int lastSelectionIndex = math.max(selection.baseOffset, selection.extentOffset);
final TextEditingValue collapsedTextEditingValue = textEditingValue.copyWith(
selection: TextSelection.collapsed(offset: lastSelectionIndex),
);
userUpdateTextEditingValue(
collapsedTextEditingValue.replaced(selection, data.text!),
cause,
);
if (cause == SelectionChangedCause.toolbar) {
// Schedule a call to bringIntoView() after renderEditable updates.
SchedulerBinding.instance.addPostFrameCallback((_) {
if (mounted) {
bringIntoView(textEditingValue.selection.extent);
}
});
hideToolbar();
}
}
/// Select the entire text value.
@override
void selectAll(SelectionChangedCause cause) {
if (widget.readOnly && widget.obscureText) {
// If we can't modify it, and we can't copy it, there's no point in
// selecting it.
return;
}
userUpdateTextEditingValue(
textEditingValue.copyWith(
selection: TextSelection(baseOffset: 0, extentOffset: textEditingValue.text.length),
),
cause,
);
if (cause == SelectionChangedCause.toolbar) {
bringIntoView(textEditingValue.selection.extent);
}
}
// State lifecycle:
@override
void initState() {
super.initState();
_cursorBlinkOpacityController = AnimationController(
vsync: this,
duration: _fadeDuration,
)..addListener(_onCursorColorTick);
_clipboardStatus?.addListener(_onChangedClipboardStatus);
widget.controller.addListener(_didChangeTextEditingValue);
widget.focusNode.addListener(_handleFocusChanged);
_scrollController.addListener(_updateSelectionOverlayForScroll);
_cursorVisibilityNotifier.value = widget.showCursor;
}
// Whether `TickerMode.of(context)` is true and animations (like blinking the
// cursor) are supposed to run.
bool _tickersEnabled = true;
@override
void didChangeDependencies() {
super.didChangeDependencies();
final AutofillGroupState? newAutofillGroup = AutofillGroup.of(context);
if (currentAutofillScope != newAutofillGroup) {
_currentAutofillScope?.unregister(autofillId);
_currentAutofillScope = newAutofillGroup;
_currentAutofillScope?.register(_effectiveAutofillClient);
}
if (!_didAutoFocus && widget.autofocus) {
_didAutoFocus = true;
SchedulerBinding.instance.addPostFrameCallback((_) {
if (mounted && renderEditable.hasSize) {
FocusScope.of(context).autofocus(widget.focusNode);
}
});
}
// Restart or stop the blinking cursor when TickerMode changes.
final bool newTickerEnabled = TickerMode.of(context);
if (_tickersEnabled != newTickerEnabled) {
_tickersEnabled = newTickerEnabled;
if (_tickersEnabled && _cursorActive) {
_startCursorTimer();
} else if (!_tickersEnabled && _cursorTimer != null) {
// Cannot use _stopCursorTimer because it would reset _cursorActive.
_cursorTimer!.cancel();
_cursorTimer = null;
}
}
}
@override
void didUpdateWidget(EditableText oldWidget) {
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);
}
_selectionOverlay?.handlesVisible = widget.showSelectionHandles;
if (widget.autofillClient != oldWidget.autofillClient) {
_currentAutofillScope?.unregister(oldWidget.autofillClient?.autofillId ?? autofillId);
_currentAutofillScope?.register(_effectiveAutofillClient);
}
if (widget.focusNode != oldWidget.focusNode) {
oldWidget.focusNode.removeListener(_handleFocusChanged);
widget.focusNode.addListener(_handleFocusChanged);
updateKeepAlive();
}
if (widget.scrollController != oldWidget.scrollController) {
(oldWidget.scrollController ?? _internalScrollController)?.removeListener(_updateSelectionOverlayForScroll);
_scrollController.addListener(_updateSelectionOverlayForScroll);
}
if (!_shouldCreateInputConnection) {
_closeInputConnectionIfNeeded();
} else if (oldWidget.readOnly && _hasFocus) {
_openInputConnection();
}
if (kIsWeb && _hasInputConnection) {
if (oldWidget.readOnly != widget.readOnly) {
_textInputConnection!.updateConfig(_effectiveAutofillClient.textInputConfiguration);
}
}
if (widget.style != oldWidget.style) {
final TextStyle style = widget.style;
// The _textInputConnection will pick up the new style when it attaches in
// _openInputConnection.
if (_hasInputConnection) {
_textInputConnection!.setStyle(
fontFamily: style.fontFamily,
fontSize: style.fontSize,
fontWeight: style.fontWeight,
textDirection: _textDirection,
textAlign: widget.textAlign,
);
}
}
if (widget.selectionEnabled && pasteEnabled && (widget.selectionControls?.canPaste(this) ?? false)) {
_clipboardStatus?.update();
}
}
@override
void dispose() {
_internalScrollController?.dispose();
_currentAutofillScope?.unregister(autofillId);
widget.controller.removeListener(_didChangeTextEditingValue);
_floatingCursorResetController?.dispose();
_floatingCursorResetController = null;
_closeInputConnectionIfNeeded();
assert(!_hasInputConnection);
_cursorTimer?.cancel();
_cursorTimer = null;
_cursorBlinkOpacityController?.dispose();
_cursorBlinkOpacityController = null;
_selectionOverlay?.dispose();
_selectionOverlay = null;
widget.focusNode.removeListener(_handleFocusChanged);
WidgetsBinding.instance.removeObserver(this);
_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;
@override
TextEditingValue get currentTextEditingValue => _value;
@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].
// Since we still have to support keyboard select, this is the best place
// to disable text updating.
if (!_shouldCreateInputConnection) {
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;
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
return;
}
if (value.text == _value.text && value.composing == _value.composing) {
// `selection` is the only change.
_handleSelectionChanged(value.selection, (_textInputConnection?.scribbleInProgress ?? false) ? SelectionChangedCause.scribble : SelectionChangedCause.keyboard);
} else {
hideToolbar();
_currentPromptRectRange = null;
final bool revealObscuredInput = _hasInputConnection
&& widget.obscureText
&& WidgetsBinding.instance.platformDispatcher.brieflyShowPassword
&& value.text.length == _value.text.length + 1;
_obscureShowCharTicksPending = revealObscuredInput ? _kObscureShowLatestCharCursorTicks : 0;
_obscureLatestCharIndex = revealObscuredInput ? _value.selection.baseOffset : null;
_formatAndSetValue(value, SelectionChangedCause.keyboard);
}
// Wherever the value is changed by the user, schedule a showCaretOnScreen
// to make sure the user can see the changes they just made. Programmatical
// changes to `textEditingValue` do not trigger the behavior even if the
// text field is focused.
_scheduleShowCaretOnScreen(withAnimation: true);
if (_hasInputConnection) {
// To keep the cursor from blinking while typing, we want to restart the
// cursor timer every time a new character is typed.
_stopCursorTimer(resetCharTicks: false);
_startCursorTimer();
}
}
@override
void performAction(TextInputAction action) {
switch (action) {
case TextInputAction.newline:
// If this is a multiline EditableText, do nothing for a "newline"
// action; The newline is already inserted. Otherwise, finalize
// editing.
if (!_isMultiline)
_finalizeEditing(action, shouldUnfocus: true);
break;
case TextInputAction.done:
case TextInputAction.go:
case TextInputAction.next:
case TextInputAction.previous:
case TextInputAction.search:
case TextInputAction.send:
_finalizeEditing(action, shouldUnfocus: true);
break;
case TextInputAction.continueAction:
case TextInputAction.emergencyCall:
case TextInputAction.join:
case TextInputAction.none:
case TextInputAction.route:
case TextInputAction.unspecified:
// Finalize editing, but don't give up focus because this keyboard
// action does not imply the user is done inputting information.
_finalizeEditing(action, shouldUnfocus: false);
break;
}
}
@override
void performPrivateCommand(String action, Map<String, dynamic> data) {
widget.onAppPrivateCommand!(action, data);
}
// The original position of the caret on FloatingCursorDragState.start.
Rect? _startCaretRect;
// The most recent text position as determined by the location of the floating
// cursor.
TextPosition? _lastTextPosition;
// The offset of the floating cursor as determined from the start call.
Offset? _pointOffsetOrigin;
// The most recent position of the floating cursor.
Offset? _lastBoundedOffset;
// Because the center of the cursor is preferredLineHeight / 2 below the touch
// origin, but the touch origin is used to determine which line the cursor is
// on, we need this offset to correctly render and move the cursor.
Offset get _floatingCursorOffset => Offset(0, renderEditable.preferredLineHeight / 2);
@override
void updateFloatingCursor(RawFloatingCursorPoint point) {
_floatingCursorResetController ??= AnimationController(
vsync: this,
)..addListener(_onFloatingCursorResetTick);
switch(point.state) {
case FloatingCursorDragState.Start:
if (_floatingCursorResetController!.isAnimating) {
_floatingCursorResetController!.stop();
_onFloatingCursorResetTick();
}
// We want to send in points that are centered around a (0,0) origin, so
// we cache the position.
_pointOffsetOrigin = point.offset;
final TextPosition currentTextPosition = TextPosition(offset: renderEditable.selection!.baseOffset);
_startCaretRect = renderEditable.getLocalRectForCaret(currentTextPosition);
_lastBoundedOffset = _startCaretRect!.center - _floatingCursorOffset;
_lastTextPosition = currentTextPosition;
renderEditable.setFloatingCursor(point.state, _lastBoundedOffset!, _lastTextPosition!);
break;
case FloatingCursorDragState.Update:
final Offset centeredPoint = point.offset! - _pointOffsetOrigin!;
final Offset rawCursorOffset = _startCaretRect!.center + centeredPoint - _floatingCursorOffset;
_lastBoundedOffset = renderEditable.calculateBoundedFloatingCursorOffset(rawCursorOffset);
_lastTextPosition = renderEditable.getPositionForPoint(renderEditable.localToGlobal(_lastBoundedOffset! + _floatingCursorOffset));
renderEditable.setFloatingCursor(point.state, _lastBoundedOffset!, _lastTextPosition!);
break;
case FloatingCursorDragState.End:
// We skip animation if no update has happened.
if (_lastTextPosition != null && _lastBoundedOffset != null) {
_floatingCursorResetController!.value = 0.0;
_floatingCursorResetController!.animateTo(1.0, duration: _floatingCursorResetTime, curve: Curves.decelerate);
}
break;
}
}
void _onFloatingCursorResetTick() {
final Offset finalPosition = renderEditable.getLocalRectForCaret(_lastTextPosition!).centerLeft - _floatingCursorOffset;
if (_floatingCursorResetController!.isCompleted) {
renderEditable.setFloatingCursor(FloatingCursorDragState.End, finalPosition, _lastTextPosition!);
if (_lastTextPosition!.offset != renderEditable.selection!.baseOffset)
// The cause is technically the force cursor, but the cause is listed as tap as the desired functionality is the same.
_handleSelectionChanged(TextSelection.collapsed(offset: _lastTextPosition!.offset), SelectionChangedCause.forcePress);
_startCaretRect = null;
_lastTextPosition = null;
_pointOffsetOrigin = null;
_lastBoundedOffset = null;
} else {
final double lerpValue = _floatingCursorResetController!.value;
final double lerpX = ui.lerpDouble(_lastBoundedOffset!.dx, finalPosition.dx, lerpValue)!;
final double lerpY = ui.lerpDouble(_lastBoundedOffset!.dy, finalPosition.dy, lerpValue)!;
renderEditable.setFloatingCursor(FloatingCursorDragState.Update, Offset(lerpX, lerpY), _lastTextPosition!, resetLerpValue: lerpValue);
}
}
@pragma('vm:notify-debugger-on-exception')
void _finalizeEditing(TextInputAction action, {required bool shouldUnfocus}) {
// Take any actions necessary now that the user has completed editing.
if (widget.onEditingComplete != null) {
try {
widget.onEditingComplete!();
} catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'widgets',
context: ErrorDescription('while calling onEditingComplete for $action'),
));
}
} else {
// Default behavior if the developer did not provide an
// onEditingComplete callback: Finalize editing and remove focus, or move
// it to the next/previous field, depending on the action.
widget.controller.clearComposing();
if (shouldUnfocus) {
switch (action) {
case TextInputAction.none:
case TextInputAction.unspecified:
case TextInputAction.done:
case TextInputAction.go:
case TextInputAction.search:
case TextInputAction.send:
case TextInputAction.continueAction:
case TextInputAction.join:
case TextInputAction.route:
case TextInputAction.emergencyCall:
case TextInputAction.newline:
widget.focusNode.unfocus();
break;
case TextInputAction.next:
widget.focusNode.nextFocus();
break;
case TextInputAction.previous:
widget.focusNode.previousFocus();
break;
}
}
}
final ValueChanged<String>? onSubmitted = widget.onSubmitted;
if (onSubmitted == null) {
return;
}
// Invoke optional callback with the user's submitted content.
try {
onSubmitted(_value.text);
} catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'widgets',
context: ErrorDescription('while calling onSubmitted for $action'),
));
}
// If `shouldUnfocus` is true, the text field should no longer be focused
// after the microtask queue is drained. But in case the developer cancelled
// the focus change in the `onSubmitted` callback by focusing this input
// field again, reset the soft keyboard.
// See https://github.com/flutter/flutter/issues/84240.
//
// `_restartConnectionIfNeeded` creates a new TextInputConnection to replace
// the current one. This on iOS switches to a new input view and on Android
// restarts the input method, and in both cases the soft keyboard will be
// reset.
if (shouldUnfocus) {
_scheduleRestartConnection();
}
}
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 outermost 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)
return;
final TextEditingValue localValue = _value;
if (localValue == _lastKnownRemoteTextEditingValue)
return;
_textInputConnection!.setEditingState(localValue);
_lastKnownRemoteTextEditingValue = localValue;
}
TextEditingValue get _value => widget.controller.value;
set _value(TextEditingValue value) {
widget.controller.value = value;
}
bool get _hasFocus => widget.focusNode.hasFocus;
bool get _isMultiline => widget.maxLines != 1;
// Finds the closest scroll offset to the current scroll offset that fully
// reveals the given caret rect. If the given rect's main axis extent is too
// large to be fully revealed in `renderEditable`, it will be centered along
// the main axis.
//
// If this is a multiline EditableText (which means the Editable can only
// scroll vertically), the given rect's height will first be extended to match
// `renderEditable.preferredLineHeight`, before the target scroll offset is
// calculated.
RevealedOffset _getOffsetToRevealCaret(Rect rect) {
if (!_scrollController.position.allowImplicitScrolling)
return RevealedOffset(offset: _scrollController.offset, rect: rect);
final Size editableSize = renderEditable.size;
final double additionalOffset;
final Offset unitOffset;
if (!_isMultiline) {
additionalOffset = rect.width >= editableSize.width
// Center `rect` if it's oversized.
? editableSize.width / 2 - rect.center.dx
// Valid additional offsets range from (rect.right - size.width)
// to (rect.left). Pick the closest one if out of range.
: 0.0.clamp(rect.right - editableSize.width, rect.left);
unitOffset = const Offset(1, 0);
} else {
// The caret is vertically centered within the line. Expand the caret's
// height so that it spans the line because we're going to ensure that the
// entire expanded caret is scrolled into view.
final Rect expandedRect = Rect.fromCenter(
center: rect.center,
width: rect.width,
height: math.max(rect.height, renderEditable.preferredLineHeight),
);
additionalOffset = expandedRect.height >= editableSize.height
? editableSize.height / 2 - expandedRect.center.dy
: 0.0.clamp(expandedRect.bottom - editableSize.height, expandedRect.top);
unitOffset = const Offset(0, 1);
}
// No overscrolling when encountering tall fonts/scripts that extend past
// the ascent.
final double targetOffset = (additionalOffset + _scrollController.offset)
.clamp(
_scrollController.position.minScrollExtent,
_scrollController.position.maxScrollExtent,
);
final double offsetDelta = _scrollController.offset - targetOffset;
return RevealedOffset(rect: rect.shift(unitOffset * offsetDelta), offset: targetOffset);
}
bool get _hasInputConnection => _textInputConnection?.attached ?? false;
/// Whether to send the autofill information to the autofill service. True by
/// default.
bool get _needsAutofill => _effectiveAutofillClient.textInputConfiguration.autofillConfiguration.enabled;
void _openInputConnection() {
if (!_shouldCreateInputConnection) {
return;
}
if (!_hasInputConnection) {
final TextEditingValue localValue = _value;
// When _needsAutofill == true && currentAutofillScope == null, autofill
// is allowed but saving the user input from the text field is
// discouraged.
//
// In case the autofillScope changes from a non-null value to null, or
// _needsAutofill changes to false from true, the platform needs to be
// notified to exclude this field from the autofill context. So we need to
// provide the autofillId.
_textInputConnection = _needsAutofill && currentAutofillScope != null
? currentAutofillScope!.attach(this, _effectiveAutofillClient.textInputConfiguration)
: TextInput.attach(this, _effectiveAutofillClient.textInputConfiguration);
_updateSizeAndTransform();
_updateComposingRectIfNeeded();
_updateCaretRectIfNeeded();
final TextStyle style = widget.style;
_textInputConnection!
..setStyle(
fontFamily: style.fontFamily,
fontSize: style.fontSize,
fontWeight: style.fontWeight,
textDirection: _textDirection,
textAlign: widget.textAlign,
)
..setEditingState(localValue)
..show();
if (_needsAutofill) {
// Request autofill AFTER the size and the transform have been sent to
// the platform text input plugin.
_textInputConnection!.requestAutofill();
}
_lastKnownRemoteTextEditingValue = localValue;
} else {
_textInputConnection!.show();
}
}
void _closeInputConnectionIfNeeded() {
if (_hasInputConnection) {
_textInputConnection!.close();
_textInputConnection = null;
_lastKnownRemoteTextEditingValue = null;
}
}
void _openOrCloseInputConnectionIfNeeded() {
if (_hasFocus && widget.focusNode.consumeKeyboardToken()) {
_openInputConnection();
} else if (!_hasFocus) {
_closeInputConnectionIfNeeded();
widget.controller.clearComposing();
}
}
bool _restartConnectionScheduled = false;
void _scheduleRestartConnection() {
if (_restartConnectionScheduled) {
return;
}
_restartConnectionScheduled = true;
scheduleMicrotask(_restartConnectionIfNeeded);
}
// Discards the current [TextInputConnection] and establishes a new one.
//
// This method is rarely needed. This is currently used to reset the input
// type when the "submit" text input action is triggered and the developer
// puts the focus back to this input field..
void _restartConnectionIfNeeded() {
_restartConnectionScheduled = false;
if (!_hasInputConnection || !_shouldCreateInputConnection) {
return;
}
_textInputConnection!.close();
_textInputConnection = null;
_lastKnownRemoteTextEditingValue = null;
final AutofillScope? currentAutofillScope = _needsAutofill ? this.currentAutofillScope : null;
final TextInputConnection newConnection = currentAutofillScope?.attach(this, textInputConfiguration)
?? TextInput.attach(this, _effectiveAutofillClient.textInputConfiguration);
_textInputConnection = newConnection;
final TextStyle style = widget.style;
newConnection
..show()
..setStyle(
fontFamily: style.fontFamily,
fontSize: style.fontSize,
fontWeight: style.fontWeight,
textDirection: _textDirection,
textAlign: widget.textAlign,
)
..setEditingState(_value);
_lastKnownRemoteTextEditingValue = _value;
}
@override
void connectionClosed() {
if (_hasInputConnection) {
_textInputConnection!.connectionClosedReceived();
_textInputConnection = null;
_lastKnownRemoteTextEditingValue = null;
_finalizeEditing(TextInputAction.done, shouldUnfocus: true);
}
}
/// Express interest in interacting with the keyboard.
///
/// If this control is already attached to the keyboard, this function will
/// request that the keyboard become visible. Otherwise, this function will
/// ask the focus system that it become focused. If successful in acquiring
/// focus, the control will then attach to the keyboard and request that the
/// keyboard become visible.
void requestKeyboard() {
if (_hasFocus) {
_openInputConnection();
} else {
widget.focusNode.requestFocus(); // This eventually calls _openInputConnection also, see _handleFocusChanged.
}
}
void _updateOrDisposeSelectionOverlayIfNeeded() {
if (_selectionOverlay != null) {
if (_hasFocus) {
_selectionOverlay!.update(_value);
} else {
_selectionOverlay!.dispose();
_selectionOverlay = null;
}
}
}
void _updateSelectionOverlayForScroll() {
_selectionOverlay?.updateForScroll();
}
@pragma('vm:notify-debugger-on-exception')
void _handleSelectionChanged(TextSelection selection, SelectionChangedCause? cause) {
// We return early if the selection is not valid. This can happen when the
// text of [EditableText] is updated at the same time as the selection is
// changed by a gesture event.
if (!widget.controller.isSelectionWithinTextBounds(selection))
return;
widget.controller.selection = selection;
// This will show the keyboard for all selection changes on the
// EditableText except for those triggered by a keyboard input.
// Typically EditableText shouldn't take user keyboard input if
// it's not focused already. If the EditableText is being
// autofilled it shouldn't request focus.
switch (cause) {
case null:
case SelectionChangedCause.doubleTap:
case SelectionChangedCause.drag:
case SelectionChangedCause.forcePress:
case SelectionChangedCause.longPress:
case SelectionChangedCause.scribble:
case SelectionChangedCause.tap:
case SelectionChangedCause.toolbar:
requestKeyboard();
break;
case SelectionChangedCause.keyboard:
if (_hasFocus) {
requestKeyboard();
}
break;
}
if (widget.selectionControls == null) {
_selectionOverlay?.dispose();
_selectionOverlay = null;
} else {
if (_selectionOverlay == null) {
_selectionOverlay = TextSelectionOverlay(
clipboardStatus: _clipboardStatus,
context: context,
value: _value,
debugRequiredFor: widget,
toolbarLayerLink: _toolbarLayerLink,
startHandleLayerLink: _startHandleLayerLink,
endHandleLayerLink: _endHandleLayerLink,
renderObject: renderEditable,
selectionControls: widget.selectionControls,
selectionDelegate: this,
dragStartBehavior: widget.dragStartBehavior,
onSelectionHandleTapped: widget.onSelectionHandleTapped,
);
} else {
_selectionOverlay!.update(_value);
}
_selectionOverlay!.handlesVisible = widget.showSelectionHandles;
_selectionOverlay!.showHandles();
}
// TODO(chunhtai): we should make sure selection actually changed before
// we call the onSelectionChanged.
// https://github.com/flutter/flutter/issues/76349.
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'),
));
}
// To keep the cursor from blinking while it moves, restart the timer here.
if (_cursorTimer != null) {
_stopCursorTimer(resetCharTicks: false);
_startCursorTimer();
}
}
Rect? _currentCaretRect;
// ignore: use_setters_to_change_properties, (this is used as a callback, can't be a setter)
void _handleCaretChanged(Rect caretRect) {
_currentCaretRect = caretRect;
}
// Animation configuration for scrolling the caret back on screen.
static const Duration _caretAnimationDuration = Duration(milliseconds: 100);
static const Curve _caretAnimationCurve = Curves.fastOutSlowIn;
bool _showCaretOnScreenScheduled = false;
void _scheduleShowCaretOnScreen({required bool withAnimation}) {
if (_showCaretOnScreenScheduled) {
return;
}
_showCaretOnScreenScheduled = true;
SchedulerBinding.instance.addPostFrameCallback((Duration _) {
_showCaretOnScreenScheduled = false;
if (_currentCaretRect == null || !_scrollController.hasClients) {
return;
}
final double lineHeight = renderEditable.preferredLineHeight;
// Enlarge the target rect by scrollPadding to ensure that caret is not
// positioned directly at the edge after scrolling.
double bottomSpacing = widget.scrollPadding.bottom;
if (_selectionOverlay?.selectionControls != null) {
final double handleHeight = _selectionOverlay!.selectionControls!
.getHandleSize(lineHeight).height;
final double interactiveHandleHeight = math.max(
handleHeight,
kMinInteractiveDimension,
);
final Offset anchor = _selectionOverlay!.selectionControls!
.getHandleAnchor(
TextSelectionHandleType.collapsed,
lineHeight,
);
final double handleCenter = handleHeight / 2 - anchor.dy;
bottomSpacing = math.max(
handleCenter + interactiveHandleHeight / 2,
bottomSpacing,
);
}
final EdgeInsets caretPadding = widget.scrollPadding
.copyWith(bottom: bottomSpacing);
final RevealedOffset targetOffset = _getOffsetToRevealCaret(_currentCaretRect!);
if (withAnimation) {
_scrollController.animateTo(
targetOffset.offset,
duration: _caretAnimationDuration,
curve: _caretAnimationCurve,
);
renderEditable.showOnScreen(
rect: caretPadding.inflateRect(targetOffset.rect),
duration: _caretAnimationDuration,
curve: _caretAnimationCurve,
);
} else {
_scrollController.jumpTo(targetOffset.offset);
renderEditable.showOnScreen(
rect: caretPadding.inflateRect(targetOffset.rect),
);
}
});
}
late double _lastBottomViewInset;
@override
void didChangeMetrics() {
if (_lastBottomViewInset != WidgetsBinding.instance.window.viewInsets.bottom) {
SchedulerBinding.instance.addPostFrameCallback((Duration _) {
_selectionOverlay?.updateForScroll();
});
if (_lastBottomViewInset < WidgetsBinding.instance.window.viewInsets.bottom) {
// Because the metrics change signal from engine will come here every frame
// (on both iOS and Android). So we don't need to show caret with animation.
_scheduleShowCaretOnScreen(withAnimation: false);
}
}
_lastBottomViewInset = WidgetsBinding.instance.window.viewInsets.bottom;
}
@pragma('vm:notify-debugger-on-exception')
void _formatAndSetValue(TextEditingValue value, SelectionChangedCause? cause, {bool userInteraction = false}) {
// Only apply input formatters if the text has changed (including uncommitted
// text in the composing region), or when the user committed the composing
// text.
// Gboard is very persistent in restoring the composing region. Applying
// input formatters on composing-region-only changes (except clearing the
// current composing region) is very infinite-loop-prone: the formatters
// will keep trying to modify the composing region while Gboard will keep
// trying to restore the original composing region.
final bool textChanged = _value.text != value.text
|| (!_value.composing.isCollapsed && value.composing.isCollapsed);
final bool selectionChanged = _value.selection != value.selection;
if (textChanged) {
try {
value = widget.inputFormatters?.fold<TextEditingValue>(
value,
(TextEditingValue newValue, TextInputFormatter formatter) => formatter.formatEditUpdate(_value, newValue),
) ?? value;
} catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'widgets',
context: ErrorDescription('while applying input formatters'),
));
}
}
// Put all optional user callback invocations in a batch edit to prevent
// sending multiple `TextInput.updateEditingValue` messages.
beginBatchEdit();
_value = value;
// Changes made by the keyboard can sometimes be "out of band" for listening
// components, so always send those events, even if we didn't think it
// changed. Also, the user long pressing should always send a selection change
// as well.
if (selectionChanged ||
(userInteraction &&
(cause == SelectionChangedCause.longPress ||
cause == SelectionChangedCause.keyboard))) {
_handleSelectionChanged(_value.selection, cause);
}
if (textChanged) {
try {
widget.onChanged?.call(_value.text);
} catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'widgets',
context: ErrorDescription('while calling onChanged'),
));
}
}
endBatchEdit();
}
void _onCursorColorTick() {
renderEditable.cursorColor = widget.cursorColor.withOpacity(_cursorBlinkOpacityController!.value);
_cursorVisibilityNotifier.value = widget.showCursor && _cursorBlinkOpacityController!.value > 0;
}
/// Whether the blinking cursor is actually visible at this precise moment
/// (it's hidden half the time, since it blinks).
@visibleForTesting
bool get cursorCurrentlyVisible => _cursorBlinkOpacityController!.value > 0;
/// The cursor blink interval (the amount of time the cursor is in the "on"
/// state or the "off" state). A complete cursor blink period is twice this
/// value (half on, half off).
@visibleForTesting
Duration get cursorBlinkInterval => _kCursorBlinkHalfPeriod;
/// The current status of the text selection handles.
@visibleForTesting
TextSelectionOverlay? get selectionOverlay => _selectionOverlay;
int _obscureShowCharTicksPending = 0;
int? _obscureLatestCharIndex;
void _cursorTick(Timer timer) {
_targetCursorVisibility = !_targetCursorVisibility;
final double targetOpacity = _targetCursorVisibility ? 1.0 : 0.0;
if (widget.cursorOpacityAnimates) {
// If we want to show the cursor, we will animate the opacity to the value
// of 1.0, and likewise if we want to make it disappear, to 0.0. An easing
// curve is used for the animation to mimic the aesthetics of the native
// iOS cursor.
//
// These values and curves have been obtained through eyeballing, so are
// likely not exactly the same as the values for native iOS.
_cursorBlinkOpacityController!.animateTo(targetOpacity, curve: Curves.easeOut);
} else {
_cursorBlinkOpacityController!.value = targetOpacity;
}
if (_obscureShowCharTicksPending > 0) {
setState(() {
_obscureShowCharTicksPending = WidgetsBinding.instance.platformDispatcher.brieflyShowPassword
? _obscureShowCharTicksPending - 1
: 0;
});
}
}
void _cursorWaitForStart(Timer timer) {
assert(_kCursorBlinkHalfPeriod > _fadeDuration);
assert(!EditableText.debugDeterministicCursor);
_cursorTimer?.cancel();
_cursorTimer = Timer.periodic(_kCursorBlinkHalfPeriod, _cursorTick);
}
// Indicates whether the cursor should be blinking right now (but it may
// actually not blink because it's disabled via TickerMode.of(context)).
bool _cursorActive = false;
void _startCursorTimer() {
assert(_cursorTimer == null);
_cursorActive = true;
if (!_tickersEnabled) {
return;
}
_targetCursorVisibility = true;
_cursorBlinkOpacityController!.value = 1.0;
if (EditableText.debugDeterministicCursor)
return;
if (widget.cursorOpacityAnimates) {
_cursorTimer = Timer.periodic(_kCursorBlinkWaitForStart, _cursorWaitForStart);
} else {
_cursorTimer = Timer.periodic(_kCursorBlinkHalfPeriod, _cursorTick);
}
}
void _stopCursorTimer({ bool resetCharTicks = true }) {
_cursorActive = false;
_cursorTimer?.cancel();
_cursorTimer = null;
_targetCursorVisibility = false;
_cursorBlinkOpacityController!.value = 0.0;
if (EditableText.debugDeterministicCursor)
return;
if (resetCharTicks)
_obscureShowCharTicksPending = 0;
if (widget.cursorOpacityAnimates) {
_cursorBlinkOpacityController!.stop();
_cursorBlinkOpacityController!.value = 0.0;
}
}
void _startOrStopCursorTimerIfNeeded() {
if (_cursorTimer == null && _hasFocus && _value.selection.isCollapsed)
_startCursorTimer();
else if (_cursorActive && (!_hasFocus || !_value.selection.isCollapsed))
_stopCursorTimer();
}
void _didChangeTextEditingValue() {
_updateRemoteEditingValueIfNeeded();
_startOrStopCursorTimerIfNeeded();
_updateOrDisposeSelectionOverlayIfNeeded();
// TODO(abarth): Teach RenderEditable about ValueNotifier<TextEditingValue>
// to avoid this setState().
setState(() { /* We use widget.controller.value in build(). */ });
_adjacentLineAction.stopCurrentVerticalRunIfSelectionChanges();
}
void _handleFocusChanged() {
_openOrCloseInputConnectionIfNeeded();
_startOrStopCursorTimerIfNeeded();
_updateOrDisposeSelectionOverlayIfNeeded();
if (_hasFocus) {
// Listen for changing viewInsets, which indicates keyboard showing up.
WidgetsBinding.instance.addObserver(this);
_lastBottomViewInset = WidgetsBinding.instance.window.viewInsets.bottom;
if (!widget.readOnly) {
_scheduleShowCaretOnScreen(withAnimation: true);
}
if (!_value.selection.isValid) {
// Place cursor at the end if the selection is invalid when we receive focus.
_handleSelectionChanged(TextSelection.collapsed(offset: _value.text.length), null);
}
_cachedText = '';
_cachedFirstRect = null;
_cachedSize = Size.zero;
_cachedPlaceholder = -1;
} else {
WidgetsBinding.instance.removeObserver(this);
setState(() { _currentPromptRectRange = null; });
}
updateKeepAlive();
}
String _cachedText = '';
Rect? _cachedFirstRect;
Size _cachedSize = Size.zero;
int _cachedPlaceholder = -1;
TextStyle? _cachedTextStyle;
void _updateSelectionRects({bool force = false}) {
if (!widget.scribbleEnabled)
return;
if (defaultTargetPlatform != TargetPlatform.iOS)
return;
// This is to avoid sending selection rects on non-iPad devices.
if (WidgetsBinding.instance.window.physicalSize.shortestSide < _kIPadWidth)
return;
final String text = renderEditable.text?.toPlainText(includeSemanticsLabels: false) ?? '';
final List<Rect> firstSelectionBoxes = renderEditable.getBoxesForSelection(const TextSelection(baseOffset: 0, extentOffset: 1));
final Rect? firstRect = firstSelectionBoxes.isNotEmpty ? firstSelectionBoxes.first : null;
final ScrollDirection scrollDirection = _scrollController.position.userScrollDirection;
final Size size = renderEditable.size;
final bool textChanged = text != _cachedText;
final bool textStyleChanged = _cachedTextStyle != widget.style;
final bool firstRectChanged = _cachedFirstRect != firstRect;
final bool sizeChanged = _cachedSize != size;
final bool placeholderChanged = _cachedPlaceholder != _placeholderLocation;
if (scrollDirection == ScrollDirection.idle && (force || textChanged || textStyleChanged || firstRectChanged || sizeChanged || placeholderChanged)) {
_cachedText = text;
_cachedFirstRect = firstRect;
_cachedTextStyle = widget.style;
_cachedSize = size;
_cachedPlaceholder = _placeholderLocation;
bool belowRenderEditableBottom = false;
final List<SelectionRect> rects = List<SelectionRect?>.generate(
_cachedText.characters.length,
(int i) {
if (belowRenderEditableBottom)
return null;
final int offset = _cachedText.characters.getRange(0, i).string.length;
final List<Rect> boxes = renderEditable.getBoxesForSelection(TextSelection(baseOffset: offset, extentOffset: offset + _cachedText.characters.characterAt(i).string.length));
if (boxes.isEmpty)
return null;
final SelectionRect selectionRect = SelectionRect(
bounds: boxes.first,
position: offset,
);
if (renderEditable.paintBounds.bottom < selectionRect.bounds.top) {
belowRenderEditableBottom = true;
return null;
}
return selectionRect;
},
).where((SelectionRect? selectionRect) {
if (selectionRect == null)
return false;
if (renderEditable.paintBounds.right < selectionRect.bounds.left || selectionRect.bounds.right < renderEditable.paintBounds.left)
return false;
if (renderEditable.paintBounds.bottom < selectionRect.bounds.top || selectionRect.bounds.bottom < renderEditable.paintBounds.top)
return false;
return true;
}).map<SelectionRect>((SelectionRect? selectionRect) => selectionRect!).toList();
_textInputConnection!.setSelectionRects(rects);
}
}
void _updateSizeAndTransform() {
if (_hasInputConnection) {
final Size size = renderEditable.size;
final Matrix4 transform = renderEditable.getTransformTo(null);
_textInputConnection!.setEditableSizeAndTransform(size, transform);
_updateSelectionRects();
SchedulerBinding.instance.addPostFrameCallback((Duration _) => _updateSizeAndTransform());
} else if (_placeholderLocation != -1) {
removeTextPlaceholder();
}
}
// Sends the current composing rect to the iOS text input plugin via the text
// input channel. We need to keep sending the information even if no text is
// currently marked, as the information usually lags behind. The text input
// plugin needs to estimate the composing rect based on the latest caret rect,
// when the composing rect info didn't arrive in time.
void _updateComposingRectIfNeeded() {
final TextRange composingRange = _value.composing;
if (_hasInputConnection) {
assert(mounted);
Rect? composingRect = renderEditable.getRectForComposingRange(composingRange);
// Send the caret location instead if there's no marked text yet.
if (composingRect == null) {
assert(!composingRange.isValid || composingRange.isCollapsed);
final int offset = composingRange.isValid ? composingRange.start : 0;
composingRect = renderEditable.getLocalRectForCaret(TextPosition(offset: offset));
}
assert(composingRect != null);
_textInputConnection!.setComposingRect(composingRect);
SchedulerBinding.instance.addPostFrameCallback((Duration _) => _updateComposingRectIfNeeded());
}
}
void _updateCaretRectIfNeeded() {
if (_hasInputConnection) {
if (renderEditable.selection != null && renderEditable.selection!.isValid &&
renderEditable.selection!.isCollapsed) {
final TextPosition currentTextPosition = TextPosition(offset: renderEditable.selection!.baseOffset);
final Rect caretRect = renderEditable.getLocalRectForCaret(currentTextPosition);
_textInputConnection!.setCaretRect(caretRect);
}
SchedulerBinding.instance.addPostFrameCallback((Duration _) => _updateCaretRectIfNeeded());
}
}
TextDirection get _textDirection {
final TextDirection result = widget.textDirection ?? Directionality.of(context);
assert(result != null, '$runtimeType created without a textDirection and with no ambient Directionality.');
return result;
}
/// The renderer for this widget's descendant.
///
/// This property is typically used to notify the renderer of input gestures
/// when [RenderEditable.ignorePointer] is true.
RenderEditable get renderEditable => _editableKey.currentContext!.findRenderObject()! as RenderEditable;
@override
TextEditingValue get textEditingValue => _value;
double get _devicePixelRatio => MediaQuery.of(context).devicePixelRatio;
@override
void userUpdateTextEditingValue(TextEditingValue value, SelectionChangedCause? cause) {
// Compare the current TextEditingValue with the pre-format new
// TextEditingValue value, in case the formatter would reject the change.
final bool shouldShowCaret = widget.readOnly
? _value.selection != value.selection
: _value != value;
if (shouldShowCaret) {
_scheduleShowCaretOnScreen(withAnimation: true);
}
_formatAndSetValue(value, cause, userInteraction: true);
}
@override
void bringIntoView(TextPosition position) {
final Rect localRect = renderEditable.getLocalRectForCaret(position);
final RevealedOffset targetOffset = _getOffsetToRevealCaret(localRect);
_scrollController.jumpTo(targetOffset.offset);
renderEditable.showOnScreen(rect: targetOffset.rect);
}
/// Shows the selection toolbar at the location of the current cursor.
///
/// Returns `false` if a toolbar couldn't be shown, such as when the toolbar
/// is already shown, or when no text selection currently exists.
@override
bool showToolbar() {
// Web is using native dom elements to enable clipboard functionality of the
// toolbar: copy, paste, select, cut. It might also provide additional
// functionality depending on the browser (such as translate). Due to this
// we should not show a Flutter toolbar for the editable text elements.
if (kIsWeb) {
return false;
}
if (_selectionOverlay == null || _selectionOverlay!.toolbarIsVisible) {
return false;
}
_clipboardStatus?.update();
_selectionOverlay!.showToolbar();
return true;
}
@override
void hideToolbar([bool hideHandles = true]) {
if (hideHandles) {
// Hide the handles and the toolbar.
_selectionOverlay?.hide();
} else if (_selectionOverlay?.toolbarIsVisible ?? false) {
// Hide only the toolbar but not the handles.
_selectionOverlay?.hideToolbar();
}
}
/// Toggles the visibility of the toolbar.
void toggleToolbar() {
assert(_selectionOverlay != null);
if (_selectionOverlay!.toolbarIsVisible) {
hideToolbar();
} else {
showToolbar();
}
}
// Tracks the location a [_ScribblePlaceholder] should be rendered in the
// text.
//
// A value of -1 indicates there should be no placeholder, otherwise the
// value should be between 0 and the length of the text, inclusive.
int _placeholderLocation = -1;
@override
void insertTextPlaceholder(Size size) {
if (!widget.scribbleEnabled)
return;
if (!widget.controller.selection.isValid)
return;
setState(() {
_placeholderLocation = _value.text.length - widget.controller.selection.end;
});
}
@override
void removeTextPlaceholder() {
if (!widget.scribbleEnabled)
return;
setState(() {
_placeholderLocation = -1;
});
}
@override
String get autofillId => 'EditableText-$hashCode';
@override
TextInputConfiguration get textInputConfiguration {
final List<String>? autofillHints = widget.autofillHints?.toList(growable: false);
final AutofillConfiguration autofillConfiguration = autofillHints != null
? AutofillConfiguration(
uniqueIdentifier: autofillId,
autofillHints: autofillHints,
currentEditingValue: currentTextEditingValue,
)
: AutofillConfiguration.disabled;
return TextInputConfiguration(
inputType: widget.keyboardType,
readOnly: widget.readOnly,
obscureText: widget.obscureText,
autocorrect: widget.autocorrect,
smartDashesType: widget.smartDashesType,
smartQuotesType: widget.smartQuotesType,
enableSuggestions: widget.enableSuggestions,
enableInteractiveSelection: widget._userSelectionEnabled,
inputAction: widget.textInputAction ?? (widget.keyboardType == TextInputType.multiline
? TextInputAction.newline
: TextInputAction.done
),
textCapitalization: widget.textCapitalization,
keyboardAppearance: widget.keyboardAppearance,
autofillConfiguration: autofillConfiguration,
enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning,
);
}
@override
void autofill(TextEditingValue value) => updateEditingValue(value);
// null if no promptRect should be shown.
TextRange? _currentPromptRectRange;
@override
void showAutocorrectionPromptRect(int start, int end) {
setState(() {
_currentPromptRectRange = TextRange(start: start, end: end);
});
}
VoidCallback? _semanticsOnCopy(TextSelectionControls? controls) {
return widget.selectionEnabled
&& copyEnabled
&& _hasFocus
&& (controls?.canCopy(this) ?? false)
? () => controls!.handleCopy(this)
: null;
}
VoidCallback? _semanticsOnCut(TextSelectionControls? controls) {
return widget.selectionEnabled
&& cutEnabled
&& _hasFocus
&& (controls?.canCut(this) ?? false)
? () => controls!.handleCut(this)
: null;
}
VoidCallback? _semanticsOnPaste(TextSelectionControls? controls) {
return widget.selectionEnabled
&& pasteEnabled
&& _hasFocus
&& (controls?.canPaste(this) ?? false)
&& (_clipboardStatus == null || _clipboardStatus!.value == ClipboardStatus.pasteable)
? () => controls!.handlePaste(this)
: null;
}
// --------------------------- Text Editing Actions ---------------------------
_TextBoundary _characterBoundary(DirectionalTextEditingIntent intent) {
final _TextBoundary atomicTextBoundary = widget.obscureText ? _CodeUnitBoundary(_value) : _CharacterBoundary(_value);
return _CollapsedSelectionBoundary(atomicTextBoundary, intent.forward);
}
_TextBoundary _nextWordBoundary(DirectionalTextEditingIntent intent) {
final _TextBoundary atomicTextBoundary;
final _TextBoundary boundary;
if (widget.obscureText) {
atomicTextBoundary = _CodeUnitBoundary(_value);
boundary = _DocumentBoundary(_value);
} else {
final TextEditingValue textEditingValue = _textEditingValueforTextLayoutMetrics;
atomicTextBoundary = _CharacterBoundary(textEditingValue);
// This isn't enough. Newline characters.
boundary = _ExpandedTextBoundary(_WhitespaceBoundary(textEditingValue), _WordBoundary(renderEditable, textEditingValue));
}
final _MixedBoundary mixedBoundary = intent.forward
? _MixedBoundary(atomicTextBoundary, boundary)
: _MixedBoundary(boundary, atomicTextBoundary);
// Use a _MixedBoundary to make sure we don't leave invalid c