| // 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 'context_menu_button_item.dart'; |
| import 'debug.dart'; |
| import 'default_selection_style.dart'; |
| import 'default_text_editing_shortcuts.dart'; |
| import 'focus_manager.dart'; |
| import 'focus_scope.dart'; |
| import 'focus_traversal.dart'; |
| import 'framework.dart'; |
| import 'localizations.dart'; |
| import 'magnifier.dart'; |
| import 'media_query.dart'; |
| import 'scroll_configuration.dart'; |
| import 'scroll_controller.dart'; |
| import 'scroll_physics.dart'; |
| import 'scroll_position.dart'; |
| import 'scrollable.dart'; |
| import 'scrollable_helpers.dart'; |
| import 'shortcuts.dart'; |
| import 'spell_check.dart'; |
| import 'tap_region.dart'; |
| import 'text.dart'; |
| import 'text_editing_intents.dart'; |
| import 'text_selection.dart'; |
| import 'text_selection_toolbar_anchors.dart'; |
| import 'ticker_provider.dart'; |
| import 'undo_history.dart'; |
| import 'view.dart'; |
| import 'widget_span.dart'; |
| |
| export 'package:flutter/services.dart' show KeyboardInsertedContent, SelectionChangedCause, SmartDashesType, SmartQuotesType, TextEditingValue, TextInputType, TextSelection; |
| |
| // Examples can assume: |
| // late BuildContext context; |
| // late WidgetTester tester; |
| |
| /// 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>); |
| |
| /// Signature for a widget builder that builds a context menu for the given |
| /// [EditableTextState]. |
| /// |
| /// See also: |
| /// |
| /// * [SelectableRegionContextMenuBuilder], which performs the same role for |
| /// [SelectableRegion]. |
| typedef EditableTextContextMenuBuilder = Widget Function( |
| BuildContext context, |
| EditableTextState editableTextState, |
| ); |
| |
| // Signature for a function that determines the target location of the given |
| // [TextPosition] after applying the given [TextBoundary]. |
| typedef _ApplyTextBoundary = TextPosition Function(TextPosition, bool, TextBoundary); |
| |
| // 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); |
| |
| // Number of cursor ticks during which the most recently entered character |
| // is shown in an obscured text field. |
| const int _kObscureShowLatestCharCursorTicks = 3; |
| |
| /// The default mime types to be used when allowedMimeTypes is not provided. |
| /// |
| /// The default value supports inserting images of any supported format. |
| const List<String> kDefaultContentInsertionMimeTypes = <String>[ |
| 'image/png', |
| 'image/bmp', |
| 'image/jpg', |
| 'image/tiff', |
| 'image/gif', |
| 'image/jpeg', |
| 'image/webp' |
| ]; |
| |
| class _CompositionCallback extends SingleChildRenderObjectWidget { |
| const _CompositionCallback({ required this.compositeCallback, required this.enabled, super.child }); |
| final CompositionCallback compositeCallback; |
| final bool enabled; |
| |
| @override |
| RenderObject createRenderObject(BuildContext context) { |
| return _RenderCompositionCallback(compositeCallback, enabled); |
| } |
| @override |
| void updateRenderObject(BuildContext context, _RenderCompositionCallback renderObject) { |
| super.updateRenderObject(context, renderObject); |
| // _EditableTextState always uses the same callback. |
| assert(renderObject.compositeCallback == compositeCallback); |
| renderObject.enabled = enabled; |
| } |
| } |
| |
| class _RenderCompositionCallback extends RenderProxyBox { |
| _RenderCompositionCallback(this.compositeCallback, this._enabled); |
| |
| final CompositionCallback compositeCallback; |
| VoidCallback? _cancelCallback; |
| |
| bool get enabled => _enabled; |
| bool _enabled = false; |
| set enabled(bool newValue) { |
| _enabled = newValue; |
| if (!newValue) { |
| _cancelCallback?.call(); |
| _cancelCallback = null; |
| } else if (_cancelCallback == null) { |
| markNeedsPaint(); |
| } |
| } |
| |
| @override |
| void paint(PaintingContext context, ui.Offset offset) { |
| if (enabled) { |
| _cancelCallback ??= context.addCompositionCallback(compositeCallback); |
| } |
| super.paint(context, offset); |
| } |
| } |
| |
| /// 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. |
| final bool composingRegionOutOfRange = !value.isComposingRangeValid || !withComposing; |
| |
| if (composingRegionOutOfRange) { |
| 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. |
| @Deprecated( |
| 'Use `contextMenuBuilder` instead. ' |
| 'This feature was deprecated after v3.3.0-0.5.pre.', |
| ) |
| class ToolbarOptions { |
| /// Create a toolbar configuration with given options. |
| /// |
| /// All options default to false if they are not explicitly set. |
| @Deprecated( |
| 'Use `contextMenuBuilder` instead. ' |
| 'This feature was deprecated after v3.3.0-0.5.pre.', |
| ) |
| const ToolbarOptions({ |
| this.copy = false, |
| this.cut = false, |
| this.paste = false, |
| this.selectAll = false, |
| }); |
| |
| /// An instance of [ToolbarOptions] with no options enabled. |
| static const ToolbarOptions empty = ToolbarOptions(); |
| |
| /// 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; |
| } |
| |
| /// Configures the ability to insert media content through the soft keyboard. |
| /// |
| /// The configuration provides a handler for any rich content inserted through |
| /// the system input method, and also provides the ability to limit the mime |
| /// types of the inserted content. |
| /// |
| /// See also: |
| /// |
| /// * [EditableText.contentInsertionConfiguration] |
| class ContentInsertionConfiguration { |
| /// Creates a content insertion configuration with the specified options. |
| /// |
| /// A handler for inserted content, in the form of [onContentInserted], must |
| /// be supplied. |
| /// |
| /// The allowable mime types of inserted content may also |
| /// be provided via [allowedMimeTypes], which cannot be an empty list. |
| ContentInsertionConfiguration({ |
| required this.onContentInserted, |
| this.allowedMimeTypes = kDefaultContentInsertionMimeTypes, |
| }) : assert(allowedMimeTypes.isNotEmpty); |
| |
| /// Called when a user inserts content through the virtual / on-screen keyboard, |
| /// currently only used on Android. |
| /// |
| /// [KeyboardInsertedContent] holds the data representing the inserted content. |
| /// |
| /// {@tool dartpad} |
| /// |
| /// This example shows how to access the data for inserted content in your |
| /// `TextField`. |
| /// |
| /// ** See code in examples/api/lib/widgets/editable_text/editable_text.on_content_inserted.0.dart ** |
| /// {@end-tool} |
| /// |
| /// See also: |
| /// |
| /// * <https://developer.android.com/guide/topics/text/image-keyboard> |
| final ValueChanged<KeyboardInsertedContent> onContentInserted; |
| |
| /// {@template flutter.widgets.contentInsertionConfiguration.allowedMimeTypes} |
| /// Used when a user inserts image-based content through the device keyboard, |
| /// currently only used on Android. |
| /// |
| /// The passed list of strings will determine which MIME types are allowed to |
| /// be inserted via the device keyboard. |
| /// |
| /// The default mime types are given by [kDefaultContentInsertionMimeTypes]. |
| /// These are all the mime types that are able to be handled and inserted |
| /// from keyboards. |
| /// |
| /// This field cannot be an empty list. |
| /// |
| /// {@tool dartpad} |
| /// This example shows how to limit image insertion to specific file types. |
| /// |
| /// ** See code in examples/api/lib/widgets/editable_text/editable_text.on_content_inserted.0.dart ** |
| /// {@end-tool} |
| /// |
| /// See also: |
| /// |
| /// * <https://developer.android.com/guide/topics/text/image-keyboard> |
| /// {@endtemplate} |
| final List<String> allowedMimeTypes; |
| } |
| |
| // A time-value pair that represents a key frame in an animation. |
| class _KeyFrame { |
| const _KeyFrame(this.time, this.value); |
| // Values extracted from iOS 15.4 UIKit. |
| static const List<_KeyFrame> iOSBlinkingCaretKeyFrames = <_KeyFrame>[ |
| _KeyFrame(0, 1), // 0 |
| _KeyFrame(0.5, 1), // 1 |
| _KeyFrame(0.5375, 0.75), // 2 |
| _KeyFrame(0.575, 0.5), // 3 |
| _KeyFrame(0.6125, 0.25), // 4 |
| _KeyFrame(0.65, 0), // 5 |
| _KeyFrame(0.85, 0), // 6 |
| _KeyFrame(0.8875, 0.25), // 7 |
| _KeyFrame(0.925, 0.5), // 8 |
| _KeyFrame(0.9625, 0.75), // 9 |
| _KeyFrame(1, 1), // 10 |
| ]; |
| |
| // The timing, in seconds, of the specified animation `value`. |
| final double time; |
| final double value; |
| } |
| |
| class _DiscreteKeyFrameSimulation extends Simulation { |
| _DiscreteKeyFrameSimulation.iOSBlinkingCaret() : this._(_KeyFrame.iOSBlinkingCaretKeyFrames, 1); |
| _DiscreteKeyFrameSimulation._(this._keyFrames, this.maxDuration) |
| : assert(_keyFrames.isNotEmpty), |
| assert(_keyFrames.last.time <= maxDuration), |
| assert(() { |
| for (int i = 0; i < _keyFrames.length -1; i += 1) { |
| if (_keyFrames[i].time > _keyFrames[i + 1].time) { |
| return false; |
| } |
| } |
| return true; |
| }(), 'The key frame sequence must be sorted by time.'); |
| |
| final double maxDuration; |
| |
| final List<_KeyFrame> _keyFrames; |
| |
| @override |
| double dx(double time) => 0; |
| |
| @override |
| bool isDone(double time) => time >= maxDuration; |
| |
| // The index of the KeyFrame corresponds to the most recent input `time`. |
| int _lastKeyFrameIndex = 0; |
| |
| @override |
| double x(double time) { |
| final int length = _keyFrames.length; |
| |
| // Perform a linear search in the sorted key frame list, starting from the |
| // last key frame found, since the input `time` usually monotonically |
| // increases by a small amount. |
| int searchIndex; |
| final int endIndex; |
| if (_keyFrames[_lastKeyFrameIndex].time > time) { |
| // The simulation may have restarted. Search within the index range |
| // [0, _lastKeyFrameIndex). |
| searchIndex = 0; |
| endIndex = _lastKeyFrameIndex; |
| } else { |
| searchIndex = _lastKeyFrameIndex; |
| endIndex = length; |
| } |
| |
| // Find the target key frame. Don't have to check (endIndex - 1): if |
| // (endIndex - 2) doesn't work we'll have to pick (endIndex - 1) anyways. |
| while (searchIndex < endIndex - 1) { |
| assert(_keyFrames[searchIndex].time <= time); |
| final _KeyFrame next = _keyFrames[searchIndex + 1]; |
| if (time < next.time) { |
| break; |
| } |
| searchIndex += 1; |
| } |
| |
| _lastKeyFrameIndex = searchIndex; |
| return _keyFrames[_lastKeyFrameIndex].value; |
| } |
| } |
| |
| /// 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. | |
| /// | [ExtendSelectionVerticallyToAdjacentPageIntent](`collapseSelection: true`) | Collapses the selection to the position closest to the selection's [TextSelection.extent], on the previous/next adjacent page | Moves the caret to the closest position on the previous/next adjacent page. | |
| /// | [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 | |
| /// | [ExtendSelectionVerticallyToAdjacentPageIntent](`collapseSelection: false`) | Moves the selection's [TextSelection.extent] to the closest position on the previous/next adjacent page | |
| /// | [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} |
| /// |
| /// {@template flutter.widgets.editableText.accessibility} |
| /// ## Troubleshooting Common Accessibility Issues |
| /// |
| /// ### Customizing User Input Accessibility Announcements |
| /// |
| /// To customize user input accessibility announcements triggered by text |
| /// changes, use [SemanticsService.announce] to make the desired |
| /// accessibility announcement. |
| /// |
| /// On iOS, the on-screen keyboard may announce the most recent input |
| /// incorrectly when a [TextInputFormatter] inserts a thousands separator to |
| /// a currency value text field. The following example demonstrates how to |
| /// suppress the default accessibility announcements by always announcing |
| /// the content of the text field as a US currency value (the `\$` inserts |
| /// a dollar sign, the `$newText interpolates the `newText` variable): |
| /// |
| /// ```dart |
| /// onChanged: (String newText) { |
| /// if (newText.isNotEmpty) { |
| /// SemanticsService.announce('\$$newText', Directionality.of(context)); |
| /// } |
| /// } |
| /// ``` |
| /// |
| /// {@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, |
| this.onTapOutside, |
| 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, |
| @Deprecated( |
| 'Use `contextMenuBuilder` instead. ' |
| 'This feature was deprecated after v3.3.0-0.5.pre.', |
| ) |
| ToolbarOptions? toolbarOptions, |
| this.autofillHints = const <String>[], |
| this.autofillClient, |
| this.clipBehavior = Clip.hardEdge, |
| this.restorationId, |
| this.scrollBehavior, |
| this.scribbleEnabled = true, |
| this.enableIMEPersonalizedLearning = true, |
| this.contentInsertionConfiguration, |
| this.contextMenuBuilder, |
| this.spellCheckConfiguration, |
| this.magnifierConfiguration = TextMagnifierConfiguration.disabled, |
| this.undoController, |
| }) : assert(obscuringCharacter.length == 1), |
| smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled), |
| smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled), |
| assert(minLines == null || minLines > 0), |
| assert( |
| (maxLines == null) || (minLines == null) || (maxLines >= minLines), |
| "minLines can't be greater than maxLines", |
| ), |
| 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.'), |
| enableInteractiveSelection = enableInteractiveSelection ?? (!readOnly || !obscureText), |
| toolbarOptions = selectionControls is TextSelectionHandleControls && toolbarOptions == null ? ToolbarOptions.empty : toolbarOptions ?? |
| (obscureText |
| ? (readOnly |
| // No point in even offering "Select All" in a read-only obscured |
| // field. |
| ? ToolbarOptions.empty |
| // 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( |
| spellCheckConfiguration == null || |
| spellCheckConfiguration == const SpellCheckConfiguration.disabled() || |
| spellCheckConfiguration.misspelledTextStyle != null, |
| 'spellCheckConfiguration must specify a misspelledTextStyle if spell check behavior is desired', |
| ), |
| _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; |
| |
| /// Controls the undo state of the current editable text. |
| /// |
| /// If null, this widget will create its own [UndoHistoryController]. |
| final UndoHistoryController? undoController; |
| |
| /// {@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 |
| /// const 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 |
| /// const 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 |
| /// const TextField(maxLines: 2) |
| /// ``` |
| /// |
| /// Input whose height grows with content between a min and max. An infinite |
| /// max is possible with `maxLines: null`. |
| /// ```dart |
| /// const 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 |
| /// const 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 |
| /// const 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. |
| /// * [TextEditingController], which implements the [Listenable] interface |
| /// and notifies its listeners on [TextEditingValue] changes. |
| 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. |
| /// |
| /// ## Testing |
| /// The following is the recommended way to trigger [onSubmitted] in a test: |
| /// |
| /// ```dart |
| /// await tester.testTextInput.receiveAction(TextInputAction.done); |
| /// ``` |
| /// |
| /// Sending a `LogicalKeyboardKey.enter` via `tester.sendKeyEvent` will not |
| /// trigger [onSubmitted]. This is because on a real device, the engine |
| /// translates the enter key to a done action, but `tester.sendKeyEvent` sends |
| /// the key to the framework only. |
| /// {@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.onTapOutside} |
| /// Called for each tap that occurs outside of the[TextFieldTapRegion] group |
| /// when the text field is focused. |
| /// |
| /// If this is null, [FocusNode.unfocus] will be called on the [focusNode] for |
| /// this text field when a [PointerDownEvent] is received on another part of |
| /// the UI. However, it will not unfocus as a result of mobile application |
| /// touch events (which does not include mouse clicks), to conform with the |
| /// platform conventions. To change this behavior, a callback may be set here |
| /// that operates differently from the default. |
| /// |
| /// When adding additional controls to a text field (for example, a spinner, a |
| /// button that copies the selected text, or modifies formatting), it is |
| /// helpful if tapping on that control doesn't unfocus the text field. In |
| /// order for an external widget to be considered as part of the text field |
| /// for the purposes of tapping "outside" of the field, wrap the control in a |
| /// [TextFieldTapRegion]. |
| /// |
| /// The [PointerDownEvent] passed to the function is the event that caused the |
| /// notification. It is possible that the event may occur outside of the |
| /// immediate bounding box defined by the text field, although it will be |
| /// within the bounding box of a [TextFieldTapRegion] member. |
| /// {@endtemplate} |
| /// |
| /// {@tool dartpad} |
| /// This example shows how to use a `TextFieldTapRegion` to wrap a set of |
| /// "spinner" buttons that increment and decrement a value in the [TextField] |
| /// without causing the text field to lose keyboard focus. |
| /// |
| /// This example includes a generic `SpinnerField<T>` class that you can copy |
| /// into your own project and customize. |
| /// |
| /// ** See code in examples/api/lib/widgets/tap_region/text_field_tap_region.0.dart ** |
| /// {@end-tool} |
| /// |
| /// See also: |
| /// |
| /// * [TapRegion] for how the region group is determined. |
| final TapRegionCallback? onTapOutside; |
| |
| /// {@template flutter.widgets.editableText.inputFormatters} |
| /// Optional input validation and formatting overrides. |
| /// |
| /// Formatters are run in the provided order when the user changes the text |
| /// this widget contains. When this parameter changes, the new formatters will |
| /// not be applied until the next time the user inserts or deletes text. |
| /// Similar to the [onChanged] callback, formatters don't run when the text is |
| /// changed programmatically via [controller]. |
| /// |
| /// See also: |
| /// |
| /// * [TextEditingController], which implements the [Listenable] interface |
| /// and notifies its listeners on [TextEditingValue] changes. |
| /// {@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; |
| |
| /// {@template flutter.widgets.editableText.contentInsertionConfiguration} |
| /// Configuration of handler for media content inserted via the system input |
| /// method. |
| /// |
| /// Defaults to null in which case media content insertion will be disabled, |
| /// and the system will display a message informing the user that the text field |
| /// does not support inserting media content. |
| /// |
| /// Set [ContentInsertionConfiguration.onContentInserted] to provide a handler. |
| /// Additionally, set [ContentInsertionConfiguration.allowedMimeTypes] |
| /// to limit the allowable mime types for inserted content. |
| /// |
| /// {@tool dartpad} |
| /// |
| /// This example shows how to access the data for inserted content in your |
| /// `TextField`. |
| /// |
| /// ** See code in examples/api/lib/widgets/editable_text/editable_text.on_content_inserted.0.dart ** |
| /// {@end-tool} |
| /// |
| /// If [contentInsertionConfiguration] is not provided, by default |
| /// an empty list of mime types will be sent to the Flutter Engine. |
| /// A handler function must be provided in order to customize the allowable |
| /// mime types for inserted content. |
| /// |
| /// If rich content is inserted without a handler, the system will display |
| /// a message informing the user that the current text input does not support |
| /// inserting rich content. |
| /// {@endtemplate} |
| final ContentInsertionConfiguration? contentInsertionConfiguration; |
| |
| /// {@template flutter.widgets.EditableText.contextMenuBuilder} |
| /// Builds the text selection toolbar when requested by the user. |
| /// |
| /// `primaryAnchor` is the desired anchor position for the context menu, while |
| /// `secondaryAnchor` is the fallback location if the menu doesn't fit. |
| /// |
| /// `buttonItems` represents the buttons that would be built by default for |
| /// this widget. |
| /// |
| /// {@tool dartpad} |
| /// This example shows how to customize the menu, in this case by keeping the |
| /// default buttons for the platform but modifying their appearance. |
| /// |
| /// ** See code in examples/api/lib/material/context_menu/editable_text_toolbar_builder.0.dart ** |
| /// {@end-tool} |
| /// |
| /// {@tool dartpad} |
| /// This example shows how to show a custom button only when an email address |
| /// is currently selected. |
| /// |
| /// ** See code in examples/api/lib/material/context_menu/editable_text_toolbar_builder.1.dart ** |
| /// {@end-tool} |
| /// |
| /// See also: |
| /// * [AdaptiveTextSelectionToolbar], which builds the default text selection |
| /// toolbar for the current platform, but allows customization of the |
| /// buttons. |
| /// * [AdaptiveTextSelectionToolbar.getAdaptiveButtons], which builds the |
| /// button Widgets for the current platform given |
| /// [ContextMenuButtonItem]s. |
| /// * [BrowserContextMenu], which allows the browser's context menu on web |
| /// to be disabled and Flutter-rendered context menus to appear. |
| /// {@endtemplate} |
| /// |
| /// If not provided, no context menu will be shown. |
| final EditableTextContextMenuBuilder? contextMenuBuilder; |
| |
| /// {@template flutter.widgets.EditableText.spellCheckConfiguration} |
| /// Configuration that details how spell check should be performed. |
| /// |
| /// Specifies the [SpellCheckService] used to spell check text input and the |
| /// [TextStyle] used to style text with misspelled words. |
| /// |
| /// If the [SpellCheckService] is left null, spell check is disabled by |
| /// default unless the [DefaultSpellCheckService] is supported, in which case |
| /// it is used. It is currently supported only on Android and iOS. |
| /// |
| /// If this configuration is left null, then spell check is disabled by default. |
| /// {@endtemplate} |
| final SpellCheckConfiguration? spellCheckConfiguration; |
| |
| /// {@macro flutter.widgets.magnifier.TextMagnifierConfiguration.intro} |
| /// |
| /// {@macro flutter.widgets.magnifier.intro} |
| /// |
| /// {@macro flutter.widgets.magnifier.TextMagnifierConfiguration.details} |
| final TextMagnifierConfiguration magnifierConfiguration; |
| |
| bool get _userSelectionEnabled => enableInteractiveSelection && (!readOnly || !obscureText); |
| |
| /// Returns the [ContextMenuButtonItem]s representing the buttons in this |
| /// platform's default selection menu for an editable field. |
| /// |
| /// For example, [EditableText] uses this to generate the default buttons for |
| /// its context menu. |
| /// |
| /// See also: |
| /// |
| /// * [EditableTextState.contextMenuButtonItems], which gives the |
| /// [ContextMenuButtonItem]s for a specific EditableText. |
| /// * [SelectableRegion.getSelectableButtonItems], which performs a similar |
| /// role but for content that is selectable but not editable. |
| /// * [AdaptiveTextSelectionToolbar], which builds the toolbar itself, and can |
| /// take a list of [ContextMenuButtonItem]s with |
| /// [AdaptiveTextSelectionToolbar.buttonItems]. |
| /// * [AdaptiveTextSelectionToolbar.getAdaptiveButtons], which builds the button |
| /// Widgets for the current platform given [ContextMenuButtonItem]s. |
| static List<ContextMenuButtonItem> getEditableButtonItems({ |
| required final ClipboardStatus? clipboardStatus, |
| required final VoidCallback? onCopy, |
| required final VoidCallback? onCut, |
| required final VoidCallback? onPaste, |
| required final VoidCallback? onSelectAll, |
| }) { |
| // If the paste button is enabled, don't render anything until the state |
| // of the clipboard is known, since it's used to determine if paste is |
| // shown. |
| if (onPaste != null && clipboardStatus == ClipboardStatus.unknown) { |
| return <ContextMenuButtonItem>[]; |
| } |
| |
| return <ContextMenuButtonItem>[ |
| if (onCut != null) |
| ContextMenuButtonItem( |
| onPressed: onCut, |
| type: ContextMenuButtonType.cut, |
| ), |
| if (onCopy != null) |
| ContextMenuButtonItem( |
| onPressed: onCopy, |
| type: ContextMenuButtonType.copy, |
| ), |
| if (onPaste != null) |
| ContextMenuButtonItem( |
| onPressed: onPaste, |
| type: ContextMenuButtonType.paste, |
| ), |
| if (onSelectAll != null) |
| ContextMenuButtonItem( |
| onPressed: onSelectAll, |
| type: ContextMenuButtonType.selectAll, |
| ), |
| ]; |
| } |
| |
| // 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)); |
| properties.add(DiagnosticsProperty<UndoHistoryController>('undoController', undoController, defaultValue: null)); |
| properties.add(DiagnosticsProperty<SpellCheckConfiguration>('spellCheckConfiguration', spellCheckConfiguration, defaultValue: null)); |
| properties.add(DiagnosticsProperty<List<String>>('contentCommitMimeTypes', contentInsertionConfiguration?.allowedMimeTypes ?? const <String>[], defaultValue: contentInsertionConfiguration == null ? const <String>[] : kDefaultContentInsertionMimeTypes)); |
| } |
| } |
| |
| /// State for a [EditableText]. |
| class EditableTextState extends State<EditableText> with AutomaticKeepAliveClientMixin<EditableText>, WidgetsBindingObserver, TickerProviderStateMixin<EditableText>, TextSelectionDelegate, TextInputClient implements AutofillClient { |
| Timer? _cursorTimer; |
| AnimationController get _cursorBlinkOpacityController { |
| return _backingCursorBlinkOpacityController ??= AnimationController( |
| vsync: this, |
| )..addListener(_onCursorColorTick); |
| } |
| AnimationController? _backingCursorBlinkOpacityController; |
| late final Simulation _iosBlinkCursorSimulation = _DiscreteKeyFrameSimulation.iOSBlinkingCaret(); |
| |
| final ValueNotifier<bool> _cursorVisibilityNotifier = ValueNotifier<bool>(true); |
| final GlobalKey _editableKey = GlobalKey(); |
| |
| /// Detects whether the clipboard can paste. |
| final ClipboardStatusNotifier clipboardStatus = ClipboardStatusNotifier(); |
| |
| TextInputConnection? _textInputConnection; |
| bool get _hasInputConnection => _textInputConnection?.attached ?? false; |
| |
| TextSelectionOverlay? _selectionOverlay; |
| |
| final GlobalKey _scrollableKey = GlobalKey(); |
| ScrollController? _internalScrollController; |
| ScrollController get _scrollController => widget.scrollController ?? (_internalScrollController ??= ScrollController()); |
| |
| 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; |
| |
| late SpellCheckConfiguration _spellCheckConfiguration; |
| late TextStyle _style; |
| |
| /// Configuration that determines how spell check will be performed. |
| /// |
| /// If possible, this configuration will contain a default for the |
| /// [SpellCheckService] if it is not otherwise specified. |
| /// |
| /// See also: |
| /// * [DefaultSpellCheckService], the spell check service used by default. |
| @visibleForTesting |
| SpellCheckConfiguration get spellCheckConfiguration => _spellCheckConfiguration; |
| |
| /// Whether or not spell check is enabled. |
| /// |
| /// Spell check is enabled when a [SpellCheckConfiguration] has been specified |
| /// for the widget. |
| bool get spellCheckEnabled => _spellCheckConfiguration.spellCheckEnabled; |
| |
| /// The most up-to-date spell check results for text input. |
| /// |
| /// These results will be updated via calls to spell check through a |
| /// [SpellCheckService] and used by this widget to build the [TextSpan] tree |
| /// for text input and menus for replacement suggestions of misspelled words. |
| SpellCheckResults? spellCheckResults; |
| |
| bool get _spellCheckResultsReceived => spellCheckEnabled && spellCheckResults != null && spellCheckResults!.suggestionSpans.isNotEmpty; |
| |
| /// 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; |
| |
| // 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; |
| |
| Orientation? _lastOrientation; |
| |
| @override |
| bool get wantKeepAlive => widget.focusNode.hasFocus; |
| |
| Color get _cursorColor => widget.cursorColor.withOpacity(_cursorBlinkOpacityController.value); |
| |
| @override |
| bool get cutEnabled { |
| if (widget.selectionControls is! TextSelectionHandleControls) { |
| return widget.toolbarOptions.cut && !widget.readOnly && !widget.obscureText; |
| } |
| return !widget.readOnly |
| && !widget.obscureText |
| && !textEditingValue.selection.isCollapsed; |
| } |
| |
| @override |
| bool get copyEnabled { |
| if (widget.selectionControls is! TextSelectionHandleControls) { |
| return widget.toolbarOptions.copy && !widget.obscureText; |
| } |
| return !widget.obscureText |
| && !textEditingValue.selection.isCollapsed; |
| } |
| |
| @override |
| bool get pasteEnabled { |
| if (widget.selectionControls is! TextSelectionHandleControls) { |
| return widget.toolbarOptions.paste && !widget.readOnly; |
| } |
| return !widget.readOnly |
| && (clipboardStatus.value == ClipboardStatus.pasteable); |
| } |
| |
| @override |
| bool get selectAllEnabled { |
| if (widget.selectionControls is! TextSelectionHandleControls) { |
| return widget.toolbarOptions.selectAll && (!widget.readOnly || !widget.obscureText) && widget.enableInteractiveSelection; |
| } |
| |
| if (!widget.enableInteractiveSelection |
| || (widget.readOnly |
| && widget.obscureText)) { |
| return false; |
| } |
| |
| switch (defaultTargetPlatform) { |
| case TargetPlatform.macOS: |
| return false; |
| case TargetPlatform.iOS: |
| return textEditingValue.text.isNotEmpty |
| && textEditingValue.selection.isCollapsed; |
| case TargetPlatform.android: |
| case TargetPlatform.fuchsia: |
| case TargetPlatform.linux: |
| case TargetPlatform.windows: |
| return textEditingValue.text.isNotEmpty |
| && !(textEditingValue.selection.start == 0 |
| && textEditingValue.selection.end == textEditingValue.text.length); |
| } |
| } |
| |
| 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; |
| 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: |
| case TargetPlatform.macOS: |
| case TargetPlatform.linux: |
| case TargetPlatform.windows: |
| break; |
| case TargetPlatform.android: |
| case TargetPlatform.fuchsia: |
| // 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; |
| 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; |
| 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) { |
| switch (defaultTargetPlatform) { |
| case TargetPlatform.android: |
| case TargetPlatform.iOS: |
| case TargetPlatform.fuchsia: |
| break; |
| case TargetPlatform.macOS: |
| case TargetPlatform.linux: |
| case TargetPlatform.windows: |
| hideToolbar(); |
| } |
| switch (defaultTargetPlatform) { |
| case TargetPlatform.android: |
| case TargetPlatform.fuchsia: |
| case TargetPlatform.linux: |
| case TargetPlatform.windows: |
| bringIntoView(textEditingValue.selection.extent); |
| break; |
| case TargetPlatform.macOS: |
| case TargetPlatform.iOS: |
| break; |
| } |
| } |
| } |
| |
| /// Replace composing region with specified text. |
| void replaceComposingRegion(SelectionChangedCause cause, String text) { |
| // Replacement cannot be performed if the text is read only or obscured. |
| assert(!widget.readOnly && !widget.obscureText); |
| |
| _replaceText(ReplaceTextIntent(textEditingValue, text, textEditingValue.composing, cause)); |
| |
| if (cause == SelectionChangedCause.toolbar) { |
| // Schedule a call to bringIntoView() after renderEditable updates. |
| SchedulerBinding.instance.addPostFrameCallback((_) { |
| if (mounted) { |
| bringIntoView(textEditingValue.selection.extent); |
| } |
| }); |
| hideToolbar(); |
| } |
| } |
| |
| /// Finds specified [SuggestionSpan] that matches the provided index using |
| /// binary search. |
| /// |
| /// See also: |
| /// |
| /// * [SpellCheckSuggestionsToolbar], the Material style spell check |
| /// suggestions toolbar that uses this method to render the correct |
| /// suggestions in the toolbar for a misspelled word. |
| SuggestionSpan? findSuggestionSpanAtCursorIndex(int cursorIndex) { |
| if (!_spellCheckResultsReceived |
| || spellCheckResults!.suggestionSpans.last.range.end < cursorIndex) { |
| // No spell check results have been received or the cursor index is out |
| // of range that suggestionSpans covers. |
| return null; |
| } |
| |
| final List<SuggestionSpan> suggestionSpans = spellCheckResults!.suggestionSpans; |
| int leftIndex = 0; |
| int rightIndex = suggestionSpans.length - 1; |
| int midIndex = 0; |
| |
| while (leftIndex <= rightIndex) { |
| midIndex = ((leftIndex + rightIndex) / 2).floor(); |
| final int currentSpanStart = suggestionSpans[midIndex].range.start; |
| final int currentSpanEnd = suggestionSpans[midIndex].range.end; |
| |
| if (cursorIndex <= currentSpanEnd && cursorIndex >= currentSpanStart) { |
| return suggestionSpans[midIndex]; |
| } |
| else if (cursorIndex <= currentSpanStart) { |
| rightIndex = midIndex - 1; |
| } |
| else { |
| leftIndex = midIndex + 1; |
| } |
| } |
| return null; |
| } |
| |
| /// Infers the [SpellCheckConfiguration] used to perform spell check. |
| /// |
| /// If spell check is enabled, this will try to infer a value for |
| /// the [SpellCheckService] if left unspecified. |
| static SpellCheckConfiguration _inferSpellCheckConfiguration(SpellCheckConfiguration? configuration) { |
| if (configuration == null || configuration == const SpellCheckConfiguration.disabled()) { |
| return const SpellCheckConfiguration.disabled(); |
| } |
| |
| SpellCheckService? spellCheckService = configuration.spellCheckService; |
| |
| assert( |
| spellCheckService != null |
| || WidgetsBinding.instance.platformDispatcher.nativeSpellCheckServiceDefined, |
| 'spellCheckService must be specified for this platform because no default service available', |
| ); |
| |
| spellCheckService = spellCheckService ?? DefaultSpellCheckService(); |
| |
| return configuration.copyWith(spellCheckService: spellCheckService); |
| } |
| |
| /// Returns the [ContextMenuButtonItem]s for the given [ToolbarOptions]. |
| @Deprecated( |
| 'Use `contextMenuBuilder` instead of `toolbarOptions`. ' |
| 'This feature was deprecated after v3.3.0-0.5.pre.', |
| ) |
| List<ContextMenuButtonItem>? buttonItemsForToolbarOptions([TargetPlatform? targetPlatform]) { |
| final ToolbarOptions toolbarOptions = widget.toolbarOptions; |
| if (toolbarOptions == ToolbarOptions.empty) { |
| return null; |
| } |
| return <ContextMenuButtonItem>[ |
| if (toolbarOptions.cut && cutEnabled) |
| ContextMenuButtonItem( |
| onPressed: () { |
| cutSelection(SelectionChangedCause.toolbar); |
| }, |
| type: ContextMenuButtonType.cut, |
| ), |
| if (toolbarOptions.copy && copyEnabled) |
| ContextMenuButtonItem( |
| onPressed: () { |
| copySelection(SelectionChangedCause.toolbar); |
| }, |
| type: ContextMenuButtonType.copy, |
| ), |
| if (toolbarOptions.paste && pasteEnabled) |
| ContextMenuButtonItem( |
| onPressed: () { |
| pasteText(SelectionChangedCause.toolbar); |
| }, |
| type: ContextMenuButtonType.paste, |
| ), |
| if (toolbarOptions.selectAll && selectAllEnabled) |
| ContextMenuButtonItem( |
| onPressed: () { |
| selectAll(SelectionChangedCause.toolbar); |
| }, |
| type: ContextMenuButtonType.selectAll, |
| ), |
| ]; |
| } |
| |
| /// Gets the line heights at the start and end of the selection for the given |
| /// [EditableTextState]. |
| _GlyphHeights _getGlyphHeights() { |
| final TextSelection selection = textEditingValue.selection; |
| |
| // Only calculate handle rects if the text in the previous frame |
| // is the same as the text in the current frame. This is done because |
| // widget.renderObject contains the renderEditable from the previous frame. |
| // If the text changed between the current and previous frames then |
| // widget.renderObject.getRectForComposingRange might fail. In cases where |
| // the current frame is different from the previous we fall back to |
| // renderObject.preferredLineHeight. |
| final InlineSpan span = renderEditable.text!; |
| final String prevText = span.toPlainText(); |
| final String currText = textEditingValue.text; |
| if (prevText != currText || !selection.isValid || selection.isCollapsed) { |
| return _GlyphHeights( |
| start: renderEditable.preferredLineHeight, |
| end: renderEditable.preferredLineHeight, |
| ); |
| } |
| |
| final String selectedGraphemes = selection.textInside(currText); |
| final int firstSelectedGraphemeExtent = selectedGraphemes.characters.first.length; |
| final Rect? startCharacterRect = renderEditable.getRectForComposingRange(TextRange( |
| start: selection.start, |
| end: selection.start + firstSelectedGraphemeExtent, |
| )); |
| final int lastSelectedGraphemeExtent = selectedGraphemes.characters.last.length; |
| final Rect? endCharacterRect = renderEditable.getRectForComposingRange(TextRange( |
| start: selection.end - lastSelectedGraphemeExtent, |
| end: selection.end, |
| )); |
| return _GlyphHeights( |
| start: startCharacterRect?.height ?? renderEditable.preferredLineHeight, |
| end: endCharacterRect?.height ?? renderEditable.preferredLineHeight, |
| ); |
| } |
| |
| /// {@template flutter.widgets.EditableText.getAnchors} |
| /// Returns the anchor points for the default context menu. |
| /// {@endtemplate} |
| /// |
| /// See also: |
| /// |
| /// * [contextMenuButtonItems], which provides the [ContextMenuButtonItem]s |
| /// for the default context menu buttons. |
| TextSelectionToolbarAnchors get contextMenuAnchors { |
| if (renderEditable.lastSecondaryTapDownPosition != null) { |
| return TextSelectionToolbarAnchors( |
| primaryAnchor: renderEditable.lastSecondaryTapDownPosition!, |
| ); |
| } |
| |
| final _GlyphHeights glyphHeights = _getGlyphHeights(); |
| final TextSelection selection = textEditingValue.selection; |
| final List<TextSelectionPoint> points = |
| renderEditable.getEndpointsForSelection(selection); |
| return TextSelectionToolbarAnchors.fromSelection( |
| renderBox: renderEditable, |
| startGlyphHeight: glyphHeights.start, |
| endGlyphHeight: glyphHeights.end, |
| selectionEndpoints: points, |
| ); |
| } |
| |
| /// Returns the [ContextMenuButtonItem]s representing the buttons in this |
| /// platform's default selection menu for [EditableText]. |
| /// |
| /// See also: |
| /// |
| /// * [EditableText.getEditableButtonItems], which performs a similar role, |
| /// but for any editable field, not just specifically EditableText. |
| /// * [SelectableRegionState.contextMenuButtonItems], which performs a similar |
| /// role but for content that is selectable but not editable. |
| /// * [contextMenuAnchors], which provides the anchor points for the default |
| /// context menu. |
| /// * [AdaptiveTextSelectionToolbar], which builds the toolbar itself, and can |
| /// take a list of [ContextMenuButtonItem]s with |
| /// [AdaptiveTextSelectionToolbar.buttonItems]. |
| /// * [AdaptiveTextSelectionToolbar.getAdaptiveButtons], which builds the |
| /// button Widgets for the current platform given [ContextMenuButtonItem]s. |
| List<ContextMenuButtonItem> get contextMenuButtonItems { |
| return buttonItemsForToolbarOptions() ?? EditableText.getEditableButtonItems( |
| clipboardStatus: clipboardStatus.value, |
| onCopy: copyEnabled |
| ? () => copySelection(SelectionChangedCause.toolbar) |
| : null, |
| onCut: cutEnabled |
| ? () => cutSelection(SelectionChangedCause.toolbar) |
| : null, |
| onPaste: pasteEnabled |
| ? () => pasteText(SelectionChangedCause.toolbar) |
| : null, |
| onSelectAll: selectAllEnabled |
| ? () => selectAll(SelectionChangedCause.toolbar) |
| : null, |
| ); |
| } |
| |
| // State lifecycle: |
| |
| @override |
| void initState() { |
| super.initState(); |
| clipboardStatus.addListener(_onChangedClipboardStatus); |
| widget.controller.addListener(_didChangeTextEditingValue); |
| widget.focusNode.addListener(_handleFocusChanged); |
| _scrollController.addListener(_onEditableScroll); |
| _cursorVisibilityNotifier.value = widget.showCursor; |
| _spellCheckConfiguration = _inferSpellCheckConfiguration(widget.spellCheckConfiguration); |
| } |
| |
| // Whether `TickerMode.of(context)` is true and animations (like blinking the |
| // cursor) are supposed to run. |
| bool _tickersEnabled = true; |
| |
| @override |
| void didChangeDependencies() { |
| super.didChangeDependencies(); |
| |
| _style = MediaQuery.boldTextOf(context) |
| ? widget.style.merge(const TextStyle(fontWeight: FontWeight.bold)) |
| : widget.style; |
| |
| final AutofillGroupState? newAutofillGroup = AutofillGroup.maybeOf(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) { |
| _flagInternalFocus(); |
| 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) { |
| _startCursorBlink(); |
| } else if (!_tickersEnabled && _cursorTimer != null) { |
| // Cannot use _stopCursorBlink because it would reset _cursorActive. |
| _cursorTimer!.cancel(); |
| _cursorTimer = null; |
| } |
| } |
| |
| if (defaultTargetPlatform != TargetPlatform.iOS && defaultTargetPlatform != TargetPlatform.android) { |
| return; |
| } |
| |
| // Hide the text selection toolbar on mobile when orientation changes. |
| final Orientation orientation = MediaQuery.orientationOf(context); |
| if (_lastOrientation == null) { |
| _lastOrientation = orientation; |
| return; |
| } |
| if (orientation != _lastOrientation) { |
| _lastOrientation = orientation; |
| if (defaultTargetPlatform == TargetPlatform.iOS) { |
| hideToolbar(false); |
| } |
| if (defaultTargetPlatform == TargetPlatform.android) { |
| hideToolbar(); |
| } |
| } |
| } |
| |
| @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(_onEditableScroll); |
| _scrollController.addListener(_onEditableScroll); |
| } |
| |
| 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) { |
| // The _textInputConnection will pick up the new style when it attaches in |
| // _openInputConnection. |
| _style = MediaQuery.boldTextOf(context) |
| ? widget.style.merge(const TextStyle(fontWeight: FontWeight.bold)) |
| : widget.style; |
| if (_hasInputConnection) { |
| _textInputConnection!.setStyle( |
| fontFamily: _style.fontFamily, |
| fontSize: _style.fontSize, |
| fontWeight: _style.fontWeight, |
| textDirection: _textDirection, |
| textAlign: widget.textAlign, |
| ); |
| } |
| } |
| final bool canPaste = widget.selectionControls is TextSelectionHandleControls |
| ? pasteEnabled |
| : widget.selectionControls?.canPaste(this) ?? false; |
| if (widget.selectionEnabled && pasteEnabled && canPaste) { |
| 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; |
| _backingCursorBlinkOpacityController?.dispose(); |
| _backingCursorBlinkOpacityController = null; |
| _selectionOverlay?.dispose(); |
| _selectionOverlay = null; |
| widget.focusNode.removeListener(_handleFocusChanged); |
| WidgetsBinding.instance.removeObserver(this); |
| clipboardStatus.removeListener(_onChangedClipboardStatus); |
| clipboardStatus.dispose(); |
| _cursorVisibilityNotifier.dispose(); |
| FocusManager.instance.removeListener(_unflagInternalFocus); |
| 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 (_checkNeedsAdjustAffinity(value)) { |
| value = value.copyWith(selection: value.selection.copyWith(affinity: _value.selection.affinity)); |
| } |
| |
| 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. |
| SelectionChangedCause cause; |
| if (_textInputConnection?.scribbleInProgress ?? false) { |
| cause = SelectionChangedCause.scribble; |
| } else if (_pointOffsetOrigin != null) { |
| cause = SelectionChangedCause.forcePress; |
| } else { |
| cause = SelectionChangedCause.keyboard; |
| } |
| _handleSelectionChanged(value.selection, cause); |
| } else { |
| if (value.text != _value.text) { |
| // Hide the toolbar if the text was changed, but only hide the toolbar |
| // overlay; the selection handle's visibility will be handled |
| // by `_handleSelectionChanged`. https://github.com/flutter/flutter/issues/108673 |
| hideToolbar(false); |
| } |
| _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. Programmatic |
| // 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. |
| _stopCursorBlink(resetCharTicks: false); |
| _startCursorBlink(); |
| } |
| } |
| |
| bool _checkNeedsAdjustAffinity(TextEditingValue value) { |
| // Trust the engine affinity if the text changes or selection changes. |
| return value.text == _value.text && |
| value.selection.isCollapsed == _value.selection.isCollapsed && |
| value.selection.start == _value.selection.start && |
| value.selection.affinity != _value.selection.affinity; |
| } |
| |
| @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?.call(action, data); |
| } |
| |
| @override |
| void insertContent(KeyboardInsertedContent content) { |
| assert(widget.contentInsertionConfiguration?.allowedMimeTypes.contains(content.mimeType) ?? false); |
| widget.contentInsertionConfiguration?.onContentInserted.call(content); |
| } |
| |
| // 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(); |
| } |
| // Stop cursor blinking and making it visible. |
| _stopCursorBlink(resetCharTicks: false); |
| _cursorBlinkOpacityController.value = 1.0; |
| // 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, affinity: renderEditable.selection!.affinity); |
| _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: |
| // Resume cursor blinking. |
| _startCursorBlink(); |
| // 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!); |
| // Only change if the current selection range is collapsed, to prevent |
| // overwriting the result of the iOS keyboard selection gesture. |
| if (renderEditable.selection!.isCollapsed) { |
| // The cause is technically the force cursor, but the cause is listed as tap as the desired functionality is the same. |
| _handleSelectionChanged(TextSelection.fromPosition(_lastTextPosition!), 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. |
| : clampDouble(0.0, 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 |
| : clampDouble(0.0, 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 = clampDouble( |
| additionalOffset + _scrollController.offset, |
| _scrollController.position.minScrollExtent, |
| _scrollController.position.maxScrollExtent, |
| ); |
| |
| final double offsetDelta = _scrollController.offset - targetOffset; |
| return RevealedOffset(rect: rect.shift(unitOffset * offsetDelta), offset: targetOffset); |
| } |
| |
| /// 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(); |
| _schedulePeriodicPostFrameCallbacks(); |
| _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(); |
| } |
| |