| // 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:flutter/foundation.dart'; |
| import 'package:flutter/painting.dart'; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter/scheduler.dart'; |
| import 'package:flutter/services.dart'; |
| import 'package:flutter/gestures.dart' show DragStartBehavior; |
| |
| import 'automatic_keep_alive.dart'; |
| import 'basic.dart'; |
| import 'binding.dart'; |
| import 'constants.dart'; |
| import 'debug.dart'; |
| import 'focus_manager.dart'; |
| import 'focus_scope.dart'; |
| import 'framework.dart'; |
| import 'localizations.dart'; |
| import 'media_query.dart'; |
| import 'scroll_controller.dart'; |
| import 'scroll_physics.dart'; |
| import 'scrollable.dart'; |
| import 'text_selection.dart'; |
| import 'ticker_provider.dart'; |
| |
| export 'package:flutter/services.dart' show TextEditingValue, TextSelection, TextInputType, SmartQuotesType, SmartDashesType; |
| export 'package:flutter/rendering.dart' show SelectionChangedCause; |
| |
| /// Signature for the callback that reports when the user changes the selection |
| /// (including the cursor location). |
| typedef SelectionChangedCallback = void Function(TextSelection selection, SelectionChangedCause cause); |
| |
| // The time it takes for the cursor to fade from fully opaque to fully |
| // transparent and vice versa. A full cursor blink, from transparent to opaque |
| // to transparent, is twice this duration. |
| const Duration _kCursorBlinkHalfPeriod = Duration(milliseconds: 500); |
| |
| // The time the cursor is static in opacity before animating to become |
| // transparent. |
| const Duration _kCursorBlinkWaitForStart = Duration(milliseconds: 150); |
| |
| // Number of cursor ticks during which the most recently entered character |
| // is shown in an obscured text field. |
| const int _kObscureShowLatestCharCursorTicks = 3; |
| |
| /// 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 [text] or [selection] properties can be set from within a listener |
| /// added to this controller. If both properties need to be changed then the |
| /// controller's [value] should be set 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 --template=stateful_widget_material} |
| /// 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. |
| /// |
| /// ```dart |
| /// final _controller = TextEditingController(); |
| /// |
| /// void initState() { |
| /// super.initState(); |
| /// _controller.addListener(() { |
| /// final text = _controller.text.toLowerCase(); |
| /// _controller.value = _controller.value.copyWith( |
| /// text: text, |
| /// selection: TextSelection(baseOffset: text.length, extentOffset: text.length), |
| /// composing: TextRange.empty, |
| /// ); |
| /// }); |
| /// } |
| /// |
| /// void dispose() { |
| /// _controller.dispose(); |
| /// super.dispose(); |
| /// } |
| /// |
| /// Widget build(BuildContext context) { |
| /// return Scaffold( |
| /// body: Container( |
| /// alignment: Alignment.center, |
| /// padding: const EdgeInsets.all(6), |
| /// child: TextFormField( |
| /// controller: _controller, |
| /// decoration: InputDecoration(border: OutlineInputBorder()), |
| /// ), |
| /// ), |
| /// ); |
| /// } |
| /// ``` |
| /// {@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 recipe]s.(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) |
| : 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, |
| ); |
| } |
| |
| /// 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({TextStyle style , bool withComposing}) { |
| if (!value.composing.isValid || !withComposing) { |
| return TextSpan(style: style, text: text); |
| } |
| final TextStyle composingStyle = style.merge( |
| 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]. |
| set selection(TextSelection newSelection) { |
| if (!isSelectionWithinTextBounds(newSelection)) |
| throw FlutterError('invalid text selection: $newSelection'); |
| value = value.copyWith(selection: newSelection, composing: TextRange.empty); |
| } |
| |
| /// Set the [value] to empty. |
| /// |
| /// After calling this function, [text] will be the empty string and the |
| /// selection will be invalid. |
| /// |
| /// 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 = TextEditingValue.empty; |
| } |
| |
| /// 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; |
| } |
| } |
| |
| /// Toolbar configuration for [EditableText]. |
| /// |
| /// Toolbar is a context menu that will show up when user right click or long |
| /// press the [EditableText]. It includes several options: cut, copy, paste, |
| /// and select all. |
| /// |
| /// [EditableText] and its derived widgets have their own default [ToolbarOptions]. |
| /// Create a custom [ToolbarOptions] if you want explicit control over the toolbar |
| /// option. |
| class ToolbarOptions { |
| /// Create a toolbar configuration with given options. |
| /// |
| /// All options default to false if they are not explicitly set. |
| const ToolbarOptions({ |
| this.copy = false, |
| this.cut = false, |
| this.paste = false, |
| this.selectAll = false, |
| }) : assert(copy != null), |
| assert(cut != null), |
| assert(paste != null), |
| assert(selectAll != null); |
| |
| /// Whether to show copy option in toolbar. |
| /// |
| /// Defaults to false. Must not be null. |
| final bool copy; |
| |
| /// Whether to show cut option in toolbar. |
| /// |
| /// If [EditableText.readOnly] is set to true, cut will be disabled regardless. |
| /// |
| /// Defaults to false. Must not be null. |
| final bool cut; |
| |
| /// Whether to show paste option in toolbar. |
| /// |
| /// If [EditableText.readOnly] is set to true, paste will be disabled regardless. |
| /// |
| /// Defaults to false. Must not be null. |
| final bool paste; |
| |
| /// Whether to show select all option in toolbar. |
| /// |
| /// Defaults to false. Must not be null. |
| final bool selectAll; |
| } |
| |
| /// A basic text input field. |
| /// |
| /// This widget interacts with the [TextInput] service to let the user edit the |
| /// text it contains. It also provides scrolling, selection, and cursor |
| /// movement. This widget does not provide any focus management (e.g., |
| /// tap-to-focus). |
| /// |
| /// ## 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 an |
| /// [inputAction] 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 |
| /// [inputAction]s can be chosen by checking the current platform and then |
| /// selecting the appropriate action. |
| /// |
| /// ## 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. |
| /// |
| /// 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. |
| /// |
| /// ## 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. |
| /// |
| /// 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, it will default to |
| /// [TextInputType.text] unless [maxLines] is greater than one, when it will |
| /// default to [TextInputType.multiline]. |
| /// |
| /// 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], and |
| /// [readOnly] arguments must not be null. |
| EditableText({ |
| Key key, |
| @required this.controller, |
| @required this.focusNode, |
| this.readOnly = false, |
| 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.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.onSelectionChanged, |
| this.onSelectionHandleTapped, |
| List<TextInputFormatter> inputFormatters, |
| this.rendererIgnoresPointer = false, |
| this.cursorWidth = 2.0, |
| 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, |
| this.enableInteractiveSelection = true, |
| this.scrollController, |
| this.scrollPhysics, |
| this.toolbarOptions = const ToolbarOptions( |
| copy: true, |
| cut: true, |
| paste: true, |
| selectAll: true, |
| ), |
| }) : assert(controller != null), |
| assert(focusNode != null), |
| assert(obscureText != null), |
| assert(autocorrect != null), |
| smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled), |
| smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled), |
| assert(enableSuggestions != null), |
| assert(showSelectionHandles != null), |
| assert(enableInteractiveSelection != null), |
| assert(readOnly != null), |
| assert(forceLine != null), |
| assert(style != null), |
| assert(cursorColor != null), |
| assert(cursorOpacityAnimates != null), |
| assert(paintCursorAboveText != null), |
| assert(backgroundCursorColor != null), |
| assert(selectionHeightStyle != null), |
| assert(selectionWidthStyle != null), |
| assert(textAlign != null), |
| assert(maxLines == null || maxLines > 0), |
| assert(minLines == null || minLines > 0), |
| assert( |
| (maxLines == null) || (minLines == null) || (maxLines >= minLines), |
| "minLines can't be greater than maxLines", |
| ), |
| assert(expands != null), |
| assert( |
| !expands || (maxLines == null && minLines == null), |
| 'minLines and maxLines must be null when expands is true.', |
| ), |
| assert(!obscureText || maxLines == 1, 'Obscured fields cannot be multiline.'), |
| assert(autofocus != null), |
| assert(rendererIgnoresPointer != null), |
| assert(scrollPadding != null), |
| assert(dragStartBehavior != null), |
| assert(toolbarOptions != null), |
| _strutStyle = strutStyle, |
| keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline), |
| inputFormatters = maxLines == 1 |
| ? <TextInputFormatter>[ |
| BlacklistingTextInputFormatter.singleLineFormatter, |
| ...inputFormatters ?? const Iterable<TextInputFormatter>.empty(), |
| ] |
| : inputFormatters, |
| showCursor = showCursor ?? !readOnly, |
| super(key: key); |
| |
| /// Controls the text being edited. |
| final TextEditingController controller; |
| |
| /// Controls whether this widget has keyboard focus. |
| final FocusNode focusNode; |
| |
| /// {@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 U+2022 BULLET characters (•). |
| /// |
| /// Defaults to false. Cannot be null. |
| /// {@endtemplate} |
| final bool obscureText; |
| |
| /// {@macro flutter.widgets.text.DefaultTextStyle.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. |
| 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.textInput.smartDashesType} |
| final SmartDashesType smartDashesType; |
| |
| /// {@macro flutter.services.textInput.smartQuotesType} |
| final SmartQuotesType smartQuotesType; |
| |
| /// {@macro flutter.services.textInput.enableSuggestions} |
| final bool enableSuggestions; |
| |
| /// The text style to use for the editable text. |
| final TextStyle style; |
| |
| /// {@template flutter.widgets.editableText.strutStyle} |
| /// The strut style used for the vertical layout. |
| /// |
| /// [StrutStyle] is used to establish a predictable vertical layout. |
| /// Since fonts may vary depending on user input and due to font |
| /// fallback, [StrutStyle.forceStrutHeight] is enabled by default |
| /// to lock all lines to the height of the base [TextStyle], provided by |
| /// [style]. This ensures the typed text fits within the allotted space. |
| /// |
| /// If null, the strut used will is 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 style != null ? StrutStyle.fromTextStyle(style, forceStrutHeight: true) : const StrutStyle(); |
| } |
| 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. |
| /// |
| /// See also: |
| /// |
| /// * {@macro flutter.gestures.monodrag.dragStartExample} |
| /// |
| /// {@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; |
| |
| /// 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. |
| final double textScaleFactor; |
| |
| /// The color to use when painting the cursor. |
| /// |
| /// Cannot be null. |
| final Color cursorColor; |
| |
| /// 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 for the text to span, wrapping if necessary. |
| /// |
| /// 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. |
| /// |
| /// 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 between the indicated range. |
| /// |
| /// The full set of behaviors possible with [minLines] and [maxLines] are as |
| /// follows. These examples apply equally to `TextField`, `TextFormField`, and |
| /// `EditableText`. |
| /// |
| /// Input that occupies a single line and scrolls horizontally as needed. |
| /// ```dart |
| /// TextField() |
| /// ``` |
| /// |
| /// Input whose height grows from one line up to as many lines as needed for |
| /// the text that was entered. If a height limit is imposed by its parent, it |
| /// will scroll vertically when its height reaches that limit. |
| /// ```dart |
| /// TextField(maxLines: null) |
| /// ``` |
| /// |
| /// The input's height is large enough for the given number of lines. If |
| /// additional lines are entered the input scrolls vertically. |
| /// ```dart |
| /// TextField(maxLines: 2) |
| /// ``` |
| /// |
| /// Input whose height grows with content between a min and max. An infinite |
| /// max is possible with `maxLines: null`. |
| /// ```dart |
| /// TextField(minLines: 2, maxLines: 4) |
| /// ``` |
| /// {@endtemplate} |
| final int maxLines; |
| |
| /// {@template flutter.widgets.editableText.minLines} |
| /// The minimum number of lines to occupy when the content spans fewer lines. |
| |
| /// 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]. |
| /// |
| /// See the examples in [maxLines] for the complete picture of how [maxLines] |
| /// and [minLines] interact to produce various behaviors. |
| /// |
| /// Defaults to null. |
| /// {@endtemplate} |
| final int minLines; |
| |
| /// {@template flutter.widgets.editableText.expands} |
| /// Whether this widget's height will be sized to fill its parent. |
| /// |
| /// If set to true and wrapped in a parent widget like [Expanded] or |
| /// [SizedBox], the input will expand to fill the parent. |
| /// |
| /// [maxLines] and [minLines] must both be null when this is set to true, |
| /// otherwise an error is thrown. |
| /// |
| /// Defaults to false. |
| /// |
| /// See the examples in [maxLines] for the complete picture of how [maxLines], |
| /// [minLines], and [expands] interact to produce various behaviors. |
| /// |
| /// Input that matches the height of its parent |
| /// ```dart |
| /// Expanded( |
| /// child: TextField(maxLines: null, expands: true), |
| /// ) |
| /// ``` |
| /// {@endtemplate} |
| final bool expands; |
| |
| /// {@template flutter.widgets.editableText.autofocus} |
| /// Whether this text field should focus itself if nothing else is already |
| /// focused. |
| /// |
| /// If true, the keyboard will open as soon as this text field obtains focus. |
| /// Otherwise, the keyboard is only shown after the user taps the text field. |
| /// |
| /// Defaults to false. Cannot be null. |
| /// {@endtemplate} |
| // See https://github.com/flutter/flutter/issues/7035 for the rationale for this |
| // keyboard behavior. |
| final bool autofocus; |
| |
| /// The color to use when painting the selection. |
| final Color selectionColor; |
| |
| /// 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]. |
| 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]. |
| /// |
| /// {@tool dartpad --template=stateful_widget_material} |
| /// |
| /// This example shows how onChanged could be used to check the TextField's |
| /// current value each time the user inserts or deletes a character. |
| /// |
| /// ```dart |
| /// TextEditingController _controller; |
| /// |
| /// void initState() { |
| /// super.initState(); |
| /// _controller = TextEditingController(); |
| /// } |
| /// |
| /// void dispose() { |
| /// _controller.dispose(); |
| /// super.dispose(); |
| /// } |
| /// |
| /// Widget build(BuildContext context) { |
| /// return Scaffold( |
| /// body: Column( |
| /// mainAxisAlignment: MainAxisAlignment.center, |
| /// children: <Widget>[ |
| /// const Text('What number comes next in the sequence?'), |
| /// const Text('1, 1, 2, 3, 5, 8...?'), |
| /// TextField( |
| /// controller: _controller, |
| /// onChanged: (String value) async { |
| /// if (value != '13') { |
| /// return; |
| /// } |
| /// await showDialog<void>( |
| /// context: context, |
| /// builder: (BuildContext context) { |
| /// return AlertDialog( |
| /// title: const Text('Thats correct!'), |
| /// content: Text ('13 is the right answer.'), |
| /// actions: <Widget>[ |
| /// FlatButton( |
| /// onPressed: () { Navigator.pop(context); }, |
| /// child: const Text('OK'), |
| /// ), |
| /// ], |
| /// ); |
| /// }, |
| /// ); |
| /// }, |
| /// ), |
| /// ], |
| /// ), |
| /// ); |
| /// } |
| /// ``` |
| /// {@end-tool} |
| /// {@endtemplate} |
| /// |
| /// See also: |
| /// |
| /// * [inputFormatters], which are called before [onChanged] |
| /// runs and can validate and change ("format") the input value. |
| /// * [onEditingComplete], [onSubmitted], [onSelectionChanged]: |
| /// which are more specialized input change notifications. |
| final ValueChanged<String> onChanged; |
| |
| /// {@template flutter.widgets.editableText.onEditingComplete} |
| /// Called when the user submits editable content (e.g., user presses the "done" |
| /// button on the keyboard). |
| /// |
| /// The default implementation of [onEditingComplete] executes 2 different |
| /// behaviors based on the situation: |
| /// |
| /// - When a completion action is pressed, such as "done", "go", "send", or |
| /// "search", the user's content is submitted to the [controller] and then |
| /// focus is given up. |
| /// |
| /// - When a non-completion action is pressed, such as "next" or "previous", |
| /// the user's content is submitted to the [controller], but focus is not |
| /// given up because developers may want to immediately move focus to |
| /// another input widget within [onSubmitted]. |
| /// |
| /// Providing [onEditingComplete] prevents the aforementioned default behavior. |
| /// {@endtemplate} |
| final VoidCallback onEditingComplete; |
| |
| /// {@template flutter.widgets.editableText.onSubmitted} |
| /// Called when the user indicates that they are done editing the text in the |
| /// field. |
| /// {@endtemplate} |
| /// |
| /// {@tool dartpad --template=stateful_widget_material} |
| /// When a non-completion action is pressed, such as "next" or "previous", it |
| /// is often desirable to move the focus to the next or previous field. To do |
| /// this, handle it as in this example, by calling [FocusNode.focusNext] in |
| /// the [TextFormField.onFieldSubmitted] callback ([TextFormField] wraps |
| /// [EditableText] internally, and uses the value of `onFieldSubmitted` as its |
| /// [onSubmitted]). |
| /// |
| /// ```dart |
| /// FocusScopeNode _focusScopeNode = FocusScopeNode(); |
| /// final _controller1 = TextEditingController(); |
| /// final _controller2 = TextEditingController(); |
| /// |
| /// void dispose() { |
| /// _focusScopeNode.dispose(); |
| /// _controller1.dispose(); |
| /// _controller2.dispose(); |
| /// super.dispose(); |
| /// } |
| /// |
| /// void _handleSubmitted(String value) { |
| /// _focusScopeNode.nextFocus(); |
| /// } |
| /// |
| /// Widget build(BuildContext context) { |
| /// return Scaffold( |
| /// body: FocusScope( |
| /// node: _focusScopeNode, |
| /// child: Column( |
| /// mainAxisAlignment: MainAxisAlignment.center, |
| /// children: <Widget>[ |
| /// Padding( |
| /// padding: const EdgeInsets.all(8.0), |
| /// child: TextFormField( |
| /// textInputAction: TextInputAction.next, |
| /// onFieldSubmitted: _handleSubmitted, |
| /// controller: _controller1, |
| /// decoration: InputDecoration(border: OutlineInputBorder()), |
| /// ), |
| /// ), |
| /// Padding( |
| /// padding: const EdgeInsets.all(8.0), |
| /// child: TextFormField( |
| /// textInputAction: TextInputAction.next, |
| /// onFieldSubmitted: _handleSubmitted, |
| /// controller: _controller2, |
| /// decoration: InputDecoration(border: OutlineInputBorder()), |
| /// ), |
| /// ), |
| /// ], |
| /// ), |
| /// ), |
| /// ); |
| /// } |
| /// ``` |
| /// {@end-tool} |
| final ValueChanged<String> onSubmitted; |
| |
| /// Called when the user changes the selection of text (including the cursor |
| /// location). |
| final SelectionChangedCallback onSelectionChanged; |
| |
| /// {@macro flutter.widgets.textSelection.onSelectionHandleTapped} |
| final VoidCallback onSelectionHandleTapped; |
| |
| /// {@template flutter.widgets.editableText.inputFormatters} |
| /// Optional input validation and formatting overrides. |
| /// |
| /// Formatters are run in the provided order when the text input changes. |
| /// {@endtemplate} |
| final List<TextInputFormatter> inputFormatters; |
| |
| /// 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.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.editable.cursorOffset} |
| final Offset cursorOffset; |
| |
| ///{@macro flutter.rendering.editable.paintCursorOnTop} |
| 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} |
| /// If true, then long-pressing this TextField will select text and show the |
| /// cut/copy/paste menu, and tapping will move the text caret. |
| /// |
| /// True by default. |
| /// |
| /// If false, most of the accessibility support for selecting text, copy |
| /// and paste, and moving the caret will be disabled. |
| /// {@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} |
| final ScrollPhysics scrollPhysics; |
| |
| /// {@macro flutter.rendering.editable.selectionEnabled} |
| bool get selectionEnabled => enableInteractiveSelection; |
| |
| @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>('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)); |
| } |
| } |
| |
| /// State for a [EditableText]. |
| class EditableTextState extends State<EditableText> with AutomaticKeepAliveClientMixin<EditableText>, WidgetsBindingObserver, TickerProviderStateMixin<EditableText> implements TextInputClient, TextSelectionDelegate { |
| Timer _cursorTimer; |
| bool _targetCursorVisibility = false; |
| final ValueNotifier<bool> _cursorVisibilityNotifier = ValueNotifier<bool>(true); |
| final GlobalKey _editableKey = GlobalKey(); |
| |
| TextInputConnection _textInputConnection; |
| TextSelectionOverlay _selectionOverlay; |
| |
| ScrollController _scrollController; |
| |
| AnimationController _cursorBlinkOpacityController; |
| |
| final LayerLink _toolbarLayerLink = LayerLink(); |
| final LayerLink _startHandleLayerLink = LayerLink(); |
| final LayerLink _endHandleLayerLink = LayerLink(); |
| |
| bool _didAutoFocus = false; |
| FocusAttachment _focusAttachment; |
| |
| // This value is an eyeball estimation of the time it takes for the iOS cursor |
| // to ease in and out. |
| static const Duration _fadeDuration = Duration(milliseconds: 250); |
| |
| // The time it takes for the floating cursor to snap to the text aligned |
| // cursor position after the user has finished placing it. |
| static const Duration _floatingCursorResetTime = Duration(milliseconds: 125); |
| |
| AnimationController _floatingCursorResetController; |
| |
| @override |
| bool get wantKeepAlive => widget.focusNode.hasFocus; |
| |
| Color get _cursorColor => widget.cursorColor.withOpacity(_cursorBlinkOpacityController.value); |
| |
| @override |
| bool get cutEnabled => widget.toolbarOptions.cut && !widget.readOnly; |
| |
| @override |
| bool get copyEnabled => widget.toolbarOptions.copy; |
| |
| @override |
| bool get pasteEnabled => widget.toolbarOptions.paste && !widget.readOnly; |
| |
| @override |
| bool get selectAllEnabled => widget.toolbarOptions.selectAll; |
| |
| // State lifecycle: |
| |
| @override |
| void initState() { |
| super.initState(); |
| widget.controller.addListener(_didChangeTextEditingValue); |
| _focusAttachment = widget.focusNode.attach(context); |
| widget.focusNode.addListener(_handleFocusChanged); |
| _scrollController = widget.scrollController ?? ScrollController(); |
| _scrollController.addListener(() { _selectionOverlay?.updateForScroll(); }); |
| _cursorBlinkOpacityController = AnimationController(vsync: this, duration: _fadeDuration); |
| _cursorBlinkOpacityController.addListener(_onCursorColorTick); |
| _floatingCursorResetController = AnimationController(vsync: this); |
| _floatingCursorResetController.addListener(_onFloatingCursorResetTick); |
| _cursorVisibilityNotifier.value = widget.showCursor; |
| } |
| |
| @override |
| void didChangeDependencies() { |
| super.didChangeDependencies(); |
| if (!_didAutoFocus && widget.autofocus) { |
| _didAutoFocus = true; |
| SchedulerBinding.instance.addPostFrameCallback((_) { |
| if (mounted) { |
| FocusScope.of(context).autofocus(widget.focusNode); |
| } |
| }); |
| } |
| } |
| |
| @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.focusNode != oldWidget.focusNode) { |
| oldWidget.focusNode.removeListener(_handleFocusChanged); |
| _focusAttachment?.detach(); |
| _focusAttachment = widget.focusNode.attach(context); |
| widget.focusNode.addListener(_handleFocusChanged); |
| updateKeepAlive(); |
| } |
| if (widget.readOnly) { |
| _closeInputConnectionIfNeeded(); |
| } else { |
| if (oldWidget.readOnly && _hasFocus) |
| _openInputConnection(); |
| } |
| if (widget.style != oldWidget.style) { |
| final TextStyle style = widget.style; |
| // The _textInputConnection will pick up the new style when it attaches in |
| // _openInputConnection. |
| if (_textInputConnection != null && _textInputConnection.attached) { |
| _textInputConnection.setStyle( |
| fontFamily: style.fontFamily, |
| fontSize: style.fontSize, |
| fontWeight: style.fontWeight, |
| textDirection: _textDirection, |
| textAlign: widget.textAlign, |
| ); |
| } |
| } |
| } |
| |
| @override |
| void dispose() { |
| widget.controller.removeListener(_didChangeTextEditingValue); |
| _cursorBlinkOpacityController.removeListener(_onCursorColorTick); |
| _floatingCursorResetController.removeListener(_onFloatingCursorResetTick); |
| _closeInputConnectionIfNeeded(); |
| assert(!_hasInputConnection); |
| _stopCursorTimer(); |
| assert(_cursorTimer == null); |
| _selectionOverlay?.dispose(); |
| _selectionOverlay = null; |
| _focusAttachment.detach(); |
| widget.focusNode.removeListener(_handleFocusChanged); |
| super.dispose(); |
| } |
| |
| // TextInputClient implementation: |
| |
| // _lastFormattedUnmodifiedTextEditingValue tracks the last value |
| // that the formatter ran on and is used to prevent double-formatting. |
| TextEditingValue _lastFormattedUnmodifiedTextEditingValue; |
| // _receivedRemoteTextEditingValue is the direct value last passed in |
| // updateEditingValue. This value does not get updated with the formatted |
| // version. |
| TextEditingValue _receivedRemoteTextEditingValue; |
| |
| @override |
| TextEditingValue get currentTextEditingValue => _value; |
| |
| @override |
| void updateEditingValue(TextEditingValue value) { |
| // Since we still have to support keyboard select, this is the best place |
| // to disable text updating. |
| if (widget.readOnly) { |
| return; |
| } |
| _receivedRemoteTextEditingValue = value; |
| if (value.text != _value.text) { |
| hideToolbar(); |
| _showCaretOnScreen(); |
| if (widget.obscureText && value.text.length == _value.text.length + 1) { |
| _obscureShowCharTicksPending = _kObscureShowLatestCharCursorTicks; |
| _obscureLatestCharIndex = _value.selection.baseOffset; |
| } |
| } |
| |
| _formatAndSetValue(value); |
| |
| // To keep the cursor from blinking while typing, we want to restart the |
| // cursor timer every time a new character is typed. |
| _stopCursorTimer(resetCharTicks: false); |
| _startCursorTimer(); |
| } |
| |
| @override |
| void performAction(TextInputAction action) { |
| switch (action) { |
| case TextInputAction.newline: |
| // If this is a multiline EditableText, do nothing for a "newline" |
| // action; The newline is already inserted. Otherwise, finalize |
| // editing. |
| if (!_isMultiline) |
| _finalizeEditing(true); |
| break; |
| case TextInputAction.done: |
| case TextInputAction.go: |
| case TextInputAction.send: |
| case TextInputAction.search: |
| _finalizeEditing(true); |
| break; |
| default: |
| // Finalize editing, but don't give up focus because this keyboard |
| // action does not imply the user is done inputting information. |
| _finalizeEditing(false); |
| break; |
| } |
| } |
| |
| // 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 first update 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) { |
| switch(point.state){ |
| case FloatingCursorDragState.Start: |
| if (_floatingCursorResetController.isAnimating) { |
| _floatingCursorResetController.stop(); |
| _onFloatingCursorResetTick(); |
| } |
| final TextPosition currentTextPosition = TextPosition(offset: renderEditable.selection.baseOffset); |
| _startCaretRect = renderEditable.getLocalRectForCaret(currentTextPosition); |
| renderEditable.setFloatingCursor(point.state, _startCaretRect.center - _floatingCursorOffset, currentTextPosition); |
| break; |
| case FloatingCursorDragState.Update: |
| // We want to send in points that are centered around a (0,0) origin, so we cache the |
| // position on the first update call. |
| if (_pointOffsetOrigin != null) { |
| 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); |
| } else { |
| _pointOffsetOrigin = point.offset; |
| } |
| break; |
| case FloatingCursorDragState.End: |
| // We skip animation if no update has happened. |
| if (_lastTextPosition != null && _lastBoundedOffset != null) { |
| _floatingCursorResetController.value = 0.0; |
| _floatingCursorResetController.animateTo(1.0, duration: _floatingCursorResetTime, curve: Curves.decelerate); |
| } |
| break; |
| } |
| } |
| |
| void _onFloatingCursorResetTick() { |
| final Offset finalPosition = renderEditable.getLocalRectForCaret(_lastTextPosition).centerLeft - _floatingCursorOffset; |
| if (_floatingCursorResetController.isCompleted) { |
| renderEditable.setFloatingCursor(FloatingCursorDragState.End, finalPosition, _lastTextPosition); |
| if (_lastTextPosition.offset != renderEditable.selection.baseOffset) |
| // The cause is technically the force cursor, but the cause is listed as tap as the desired functionality is the same. |
| _handleSelectionChanged(TextSelection.collapsed(offset: _lastTextPosition.offset), renderEditable, 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); |
| } |
| } |
| |
| void _finalizeEditing(bool shouldUnfocus) { |
| // Take any actions necessary now that the user has completed editing. |
| if (widget.onEditingComplete != null) { |
| widget.onEditingComplete(); |
| } else { |
| // Default behavior if the developer did not provide an |
| // onEditingComplete callback: Finalize editing and remove focus. |
| widget.controller.clearComposing(); |
| if (shouldUnfocus) |
| widget.focusNode.unfocus(); |
| } |
| |
| // Invoke optional callback with the user's submitted content. |
| if (widget.onSubmitted != null) |
| widget.onSubmitted(_value.text); |
| } |
| |
| void _updateRemoteEditingValueIfNeeded() { |
| if (!_hasInputConnection) |
| return; |
| final TextEditingValue localValue = _value; |
| if (localValue == _receivedRemoteTextEditingValue) |
| return; |
| _textInputConnection.setEditingState(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; |
| |
| // Calculate the new scroll offset so the cursor remains visible. |
| double _getScrollOffsetForCaret(Rect caretRect) { |
| double caretStart; |
| double caretEnd; |
| if (_isMultiline) { |
| // 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 double lineHeight = renderEditable.preferredLineHeight; |
| final double caretOffset = (lineHeight - caretRect.height) / 2; |
| caretStart = caretRect.top - caretOffset; |
| caretEnd = caretRect.bottom + caretOffset; |
| } else { |
| // Scrolls horizontally for single-line fields. |
| caretStart = caretRect.left; |
| caretEnd = caretRect.right; |
| } |
| |
| double scrollOffset = _scrollController.offset; |
| final double viewportExtent = _scrollController.position.viewportDimension; |
| if (caretStart < 0.0) { // cursor before start of bounds |
| scrollOffset += caretStart; |
| } else if (caretEnd >= viewportExtent) { // cursor after end of bounds |
| scrollOffset += caretEnd - viewportExtent; |
| } |
| |
| if (_isMultiline) { |
| // Clamp the final results to prevent programmatically scrolling to |
| // out-of-paragraph-bounds positions when encountering tall fonts/scripts that |
| // extend past the ascent. |
| scrollOffset = scrollOffset.clamp(0.0, renderEditable.maxScrollExtent) as double; |
| } |
| return scrollOffset; |
| } |
| |
| // Calculates where the `caretRect` would be if `_scrollController.offset` is set to `scrollOffset`. |
| Rect _getCaretRectAtScrollOffset(Rect caretRect, double scrollOffset) { |
| final double offsetDiff = _scrollController.offset - scrollOffset; |
| return _isMultiline ? caretRect.translate(0.0, offsetDiff) : caretRect.translate(offsetDiff, 0.0); |
| } |
| |
| bool get _hasInputConnection => _textInputConnection != null && _textInputConnection.attached; |
| |
| void _openInputConnection() { |
| if (widget.readOnly) { |
| return; |
| } |
| if (!_hasInputConnection) { |
| final TextEditingValue localValue = _value; |
| _lastFormattedUnmodifiedTextEditingValue = localValue; |
| _textInputConnection = TextInput.attach( |
| this, |
| TextInputConfiguration( |
| inputType: widget.keyboardType, |
| obscureText: widget.obscureText, |
| autocorrect: widget.autocorrect, |
| smartDashesType: widget.smartDashesType ?? (widget.obscureText ? SmartDashesType.disabled : SmartDashesType.enabled), |
| smartQuotesType: widget.smartQuotesType ?? (widget.obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled), |
| enableSuggestions: widget.enableSuggestions, |
| inputAction: widget.textInputAction ?? (widget.keyboardType == TextInputType.multiline |
| ? TextInputAction.newline |
| : TextInputAction.done |
| ), |
| textCapitalization: widget.textCapitalization, |
| keyboardAppearance: widget.keyboardAppearance, |
| ), |
| ); |
| _textInputConnection.show(); |
| |
| _updateSizeAndTransform(); |
| final TextStyle style = widget.style; |
| _textInputConnection |
| ..setStyle( |
| fontFamily: style.fontFamily, |
| fontSize: style.fontSize, |
| fontWeight: style.fontWeight, |
| textDirection: _textDirection, |
| textAlign: widget.textAlign, |
| ) |
| ..setEditingState(localValue); |
| } else { |
| _textInputConnection.show(); |
| } |
| } |
| |
| void _closeInputConnectionIfNeeded() { |
| if (_hasInputConnection) { |
| _textInputConnection.close(); |
| _textInputConnection = null; |
| _lastFormattedUnmodifiedTextEditingValue = null; |
| _receivedRemoteTextEditingValue = null; |
| } |
| } |
| |
| void _openOrCloseInputConnectionIfNeeded() { |
| if (_hasFocus && widget.focusNode.consumeKeyboardToken()) { |
| _openInputConnection(); |
| } else if (!_hasFocus) { |
| _closeInputConnectionIfNeeded(); |
| widget.controller.clearComposing(); |
| } |
| } |
| |
| @override |
| void connectionClosed() { |
| if (_hasInputConnection) { |
| _textInputConnection.connectionClosedReceived(); |
| _textInputConnection = null; |
| _lastFormattedUnmodifiedTextEditingValue = null; |
| _receivedRemoteTextEditingValue = null; |
| _finalizeEditing(true); |
| } |
| } |
| |
| /// Express interest in interacting with the keyboard. |
| /// |
| /// If this control is already attached to the keyboard, this function will |
| /// request that the keyboard become visible. Otherwise, this function will |
| /// ask the focus system that it become focused. If successful in acquiring |
| /// focus, the control will then attach to the keyboard and request that the |
| /// keyboard become visible. |
| void requestKeyboard() { |
| if (_hasFocus) { |
| _openInputConnection(); |
| } else { |
| widget.focusNode.requestFocus(); |
| } |
| } |
| |
| void _updateOrDisposeSelectionOverlayIfNeeded() { |
| if (_selectionOverlay != null) { |
| if (_hasFocus) { |
| _selectionOverlay.update(_value); |
| } else { |
| _selectionOverlay.dispose(); |
| _selectionOverlay = null; |
| } |
| } |
| } |
| |
| void _handleSelectionChanged(TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) { |
| // We return early if the selection is not valid. This can happen when the |
| // text of [EditableText] is updated at the same time as the selection is |
| // changed by a gesture event. |
| if (!widget.controller.isSelectionWithinTextBounds(selection)) |
| return; |
| |
| widget.controller.selection = selection; |
| |
| // This will show the keyboard for all selection changes on the |
| // EditableWidget, not just changes triggered by user gestures. |
| requestKeyboard(); |
| |
| _selectionOverlay?.hide(); |
| _selectionOverlay = null; |
| |
| if (widget.selectionControls != null) { |
| _selectionOverlay = TextSelectionOverlay( |
| context: context, |
| value: _value, |
| debugRequiredFor: widget, |
| toolbarLayerLink: _toolbarLayerLink, |
| startHandleLayerLink: _startHandleLayerLink, |
| endHandleLayerLink: _endHandleLayerLink, |
| renderObject: renderObject, |
| selectionControls: widget.selectionControls, |
| selectionDelegate: this, |
| dragStartBehavior: widget.dragStartBehavior, |
| onSelectionHandleTapped: widget.onSelectionHandleTapped, |
| ); |
| _selectionOverlay.handlesVisible = widget.showSelectionHandles; |
| _selectionOverlay.showHandles(); |
| if (widget.onSelectionChanged != null) |
| widget.onSelectionChanged(selection, cause); |
| } |
| } |
| |
| bool _textChangedSinceLastCaretUpdate = false; |
| Rect _currentCaretRect; |
| |
| void _handleCaretChanged(Rect caretRect) { |
| _currentCaretRect = caretRect; |
| // If the caret location has changed due to an update to the text or |
| // selection, then scroll the caret into view. |
| if (_textChangedSinceLastCaretUpdate) { |
| _textChangedSinceLastCaretUpdate = false; |
| _showCaretOnScreen(); |
| } |
| } |
| |
| // Animation configuration for scrolling the caret back on screen. |
| static const Duration _caretAnimationDuration = Duration(milliseconds: 100); |
| static const Curve _caretAnimationCurve = Curves.fastOutSlowIn; |
| |
| bool _showCaretOnScreenScheduled = false; |
| |
| void _showCaretOnScreen() { |
| if (_showCaretOnScreenScheduled) { |
| return; |
| } |
| _showCaretOnScreenScheduled = true; |
| SchedulerBinding.instance.addPostFrameCallback((Duration _) { |
| _showCaretOnScreenScheduled = false; |
| if (_currentCaretRect == null || !_scrollController.hasClients) { |
| return; |
| } |
| final double scrollOffsetForCaret = _getScrollOffsetForCaret(_currentCaretRect); |
| _scrollController.animateTo( |
| scrollOffsetForCaret, |
| duration: _caretAnimationDuration, |
| curve: _caretAnimationCurve, |
| ); |
| final Rect newCaretRect = _getCaretRectAtScrollOffset(_currentCaretRect, scrollOffsetForCaret); |
| // Enlarge newCaretRect by scrollPadding to ensure that caret is not |
| // positioned directly at the edge after scrolling. |
| double bottomSpacing = widget.scrollPadding.bottom; |
| if (_selectionOverlay?.selectionControls != null) { |
| final double handleHeight = _selectionOverlay.selectionControls |
| .getHandleSize(renderEditable.preferredLineHeight).height; |
| final double interactiveHandleHeight = math.max( |
| handleHeight, |
| kMinInteractiveDimension, |
| ); |
| final Offset anchor = _selectionOverlay.selectionControls |
| .getHandleAnchor( |
| TextSelectionHandleType.collapsed, |
| renderEditable.preferredLineHeight, |
| ); |
| final double handleCenter = handleHeight / 2 - anchor.dy; |
| bottomSpacing = math.max( |
| handleCenter + interactiveHandleHeight / 2, |
| bottomSpacing, |
| ); |
| } |
| final Rect inflatedRect = Rect.fromLTRB( |
| newCaretRect.left - widget.scrollPadding.left, |
| newCaretRect.top - widget.scrollPadding.top, |
| newCaretRect.right + widget.scrollPadding.right, |
| newCaretRect.bottom + bottomSpacing, |
| ); |
| _editableKey.currentContext.findRenderObject().showOnScreen( |
| rect: inflatedRect, |
| duration: _caretAnimationDuration, |
| curve: _caretAnimationCurve, |
| ); |
| }); |
| } |
| |
| double _lastBottomViewInset; |
| |
| @override |
| void didChangeMetrics() { |
| if (_lastBottomViewInset < WidgetsBinding.instance.window.viewInsets.bottom) { |
| _showCaretOnScreen(); |
| } |
| _lastBottomViewInset = WidgetsBinding.instance.window.viewInsets.bottom; |
| } |
| |
| _WhitespaceDirectionalityFormatter _whitespaceFormatter; |
| |
| void _formatAndSetValue(TextEditingValue value) { |
| _whitespaceFormatter ??= _WhitespaceDirectionalityFormatter(textDirection: _textDirection); |
| |
| // Check if the new value is the same as the current local value, or is the same |
| // as the post-formatting value of the previous pass. |
| final bool textChanged = _value?.text != value?.text; |
| final bool isRepeat = value?.text == _lastFormattedUnmodifiedTextEditingValue?.text; |
| if (textChanged && !isRepeat && widget.inputFormatters != null && widget.inputFormatters.isNotEmpty) { |
| for (final TextInputFormatter formatter in widget.inputFormatters) |
| value = formatter.formatEditUpdate(_value, value); |
| _value = value; |
| _updateRemoteEditingValueIfNeeded(); |
| } else { |
| _value = value; |
| } |
| if (textChanged && widget.onChanged != null) |
| widget.onChanged(value.text); |
| _lastFormattedUnmodifiedTextEditingValue = _receivedRemoteTextEditingValue; |
| } |
| |
| void _onCursorColorTick() { |
| renderEditable.cursorColor = widget.cursorColor.withOpacity(_cursorBlinkOpacityController.value); |
| _cursorVisibilityNotifier.value = widget.showCursor && _cursorBlinkOpacityController.value > 0; |
| } |
| |
| /// Whether the blinking cursor is actually visible at this precise moment |
| /// (it's hidden half the time, since it blinks). |
| @visibleForTesting |
| bool get cursorCurrentlyVisible => _cursorBlinkOpacityController.value > 0; |
| |
| /// The cursor blink interval (the amount of time the cursor is in the "on" |
| /// state or the "off" state). A complete cursor blink period is twice this |
| /// value (half on, half off). |
| @visibleForTesting |
| Duration get cursorBlinkInterval => _kCursorBlinkHalfPeriod; |
| |
| /// The current status of the text selection handles. |
| @visibleForTesting |
| TextSelectionOverlay get selectionOverlay => _selectionOverlay; |
| |
| int _obscureShowCharTicksPending = 0; |
| int _obscureLatestCharIndex; |
| |
| void _cursorTick(Timer timer) { |
| _targetCursorVisibility = !_targetCursorVisibility; |
| final double targetOpacity = _targetCursorVisibility ? 1.0 : 0.0; |
| if (widget.cursorOpacityAnimates) { |
| // If we want to show the cursor, we will animate the opacity to the value |
| // of 1.0, and likewise if we want to make it disappear, to 0.0. An easing |
| // curve is used for the animation to mimic the aesthetics of the native |
| // iOS cursor. |
| // |
| // These values and curves have been obtained through eyeballing, so are |
| // likely not exactly the same as the values for native iOS. |
| _cursorBlinkOpacityController.animateTo(targetOpacity, curve: Curves.easeOut); |
| } else { |
| _cursorBlinkOpacityController.value = targetOpacity; |
| } |
| |
| if (_obscureShowCharTicksPending > 0) { |
| setState(() { |
| _obscureShowCharTicksPending--; |
| }); |
| } |
| } |
| |
| void _cursorWaitForStart(Timer timer) { |
| assert(_kCursorBlinkHalfPeriod > _fadeDuration); |
| _cursorTimer?.cancel(); |
| _cursorTimer = Timer.periodic(_kCursorBlinkHalfPeriod, _cursorTick); |
| } |
| |
| void _startCursorTimer() { |
| _targetCursorVisibility = true; |
| _cursorBlinkOpacityController.value = 1.0; |
| if (EditableText.debugDeterministicCursor) |
| return; |
| if (widget.cursorOpacityAnimates) { |
| _cursorTimer = Timer.periodic(_kCursorBlinkWaitForStart, _cursorWaitForStart); |
| } else { |
| _cursorTimer = Timer.periodic(_kCursorBlinkHalfPeriod, _cursorTick); |
| } |
| } |
| |
| void _stopCursorTimer({ bool resetCharTicks = true }) { |
| _cursorTimer?.cancel(); |
| _cursorTimer = null; |
| _targetCursorVisibility = false; |
| _cursorBlinkOpacityController.value = 0.0; |
| if (EditableText.debugDeterministicCursor) |
| return; |
| if (resetCharTicks) |
| _obscureShowCharTicksPending = 0; |
| if (widget.cursorOpacityAnimates) { |
| _cursorBlinkOpacityController.stop(); |
| _cursorBlinkOpacityController.value = 0.0; |
| } |
| } |
| |
| void _startOrStopCursorTimerIfNeeded() { |
| if (_cursorTimer == null && _hasFocus && _value.selection.isCollapsed) |
| _startCursorTimer(); |
| else if (_cursorTimer != null && (!_hasFocus || !_value.selection.isCollapsed)) |
| _stopCursorTimer(); |
| } |
| |
| void _didChangeTextEditingValue() { |
| _updateRemoteEditingValueIfNeeded(); |
| _startOrStopCursorTimerIfNeeded(); |
| _updateOrDisposeSelectionOverlayIfNeeded(); |
| _textChangedSinceLastCaretUpdate = true; |
| // TODO(abarth): Teach RenderEditable about ValueNotifier<TextEditingValue> |
| // to avoid this setState(). |
| setState(() { /* We use widget.controller.value in build(). */ }); |
| } |
| |
| void _handleFocusChanged() { |
| _openOrCloseInputConnectionIfNeeded(); |
| _startOrStopCursorTimerIfNeeded(); |
| _updateOrDisposeSelectionOverlayIfNeeded(); |
| if (_hasFocus) { |
| // Listen for changing viewInsets, which indicates keyboard showing up. |
| WidgetsBinding.instance.addObserver(this); |
| _lastBottomViewInset = WidgetsBinding.instance.window.viewInsets.bottom; |
| _showCaretOnScreen(); |
| if (!_value.selection.isValid) { |
| // Place cursor at the end if the selection is invalid when we receive focus. |
| _handleSelectionChanged(TextSelection.collapsed(offset: _value.text.length), renderEditable, null); |
| } |
| } else { |
| WidgetsBinding.instance.removeObserver(this); |
| // Clear the selection and composition state if this widget lost focus. |
| _value = TextEditingValue(text: _value.text); |
| } |
| updateKeepAlive(); |
| } |
| |
| void _updateSizeAndTransform() { |
| if (_hasInputConnection) { |
| final Size size = renderEditable.size; |
| final Matrix4 transform = renderEditable.getTransformTo(null); |
| _textInputConnection.setEditableSizeAndTransform(size, transform); |
| SchedulerBinding.instance |
| .addPostFrameCallback((Duration _) => _updateSizeAndTransform()); |
| } |
| } |
| |
| TextDirection get _textDirection { |
| final TextDirection result = widget.textDirection ?? Directionality.of(context); |
| assert(result != null, '$runtimeType created without a textDirection and with no ambient Directionality.'); |
| return result; |
| } |
| |
| /// The renderer for this widget's [Editable] descendant. |
| /// |
| /// This property is typically used to notify the renderer of input gestures |
| /// when [ignorePointer] is true. See [RenderEditable.ignorePointer]. |
| RenderEditable get renderEditable => _editableKey.currentContext.findRenderObject() as RenderEditable; |
| |
| @override |
| TextEditingValue get textEditingValue => _value; |
| |
| double get _devicePixelRatio => MediaQuery.of(context).devicePixelRatio ?? 1.0; |
| |
| @override |
| set textEditingValue(TextEditingValue value) { |
| _selectionOverlay?.update(value); |
| _formatAndSetValue(value); |
| } |
| |
| @override |
| void bringIntoView(TextPosition position) { |
| _scrollController.jumpTo(_getScrollOffsetForCaret(renderEditable.getLocalRectForCaret(position))); |
| } |
| |
| /// Shows the selection toolbar at the location of the current cursor. |
| /// |
| /// Returns `false` if a toolbar couldn't be shown, such as when the toolbar |
| /// is already shown, or when no text selection currently exists. |
| bool showToolbar() { |
| // Web is using native dom elements to enable clipboard functionality of the |
| // toolbar: copy, paste, select, cut. It might also provide additional |
| // functionality depending on the browser (such as translate). Due to this |
| // we should not show a Flutter toolbar for the editable text elements. |
| if (kIsWeb) { |
| return false; |
| } |
| |
| if (_selectionOverlay == null || _selectionOverlay.toolbarIsVisible) { |
| return false; |
| } |
| |
| _selectionOverlay.showToolbar(); |
| return true; |
| } |
| |
| @override |
| void hideToolbar() { |
| _selectionOverlay?.hide(); |
| } |
| |
| /// Toggles the visibility of the toolbar. |
| void toggleToolbar() { |
| assert(_selectionOverlay != null); |
| if (_selectionOverlay.toolbarIsVisible) { |
| hideToolbar(); |
| } else { |
| showToolbar(); |
| } |
| } |
| |
| VoidCallback _semanticsOnCopy(TextSelectionControls controls) { |
| return widget.selectionEnabled && copyEnabled && _hasFocus && controls?.canCopy(this) == true |
| ? () => controls.handleCopy(this) |
| : null; |
| } |
| |
| VoidCallback _semanticsOnCut(TextSelectionControls controls) { |
| return widget.selectionEnabled && cutEnabled && _hasFocus && controls?.canCut(this) == true |
| ? () => controls.handleCut(this) |
| : null; |
| } |
| |
| VoidCallback _semanticsOnPaste(TextSelectionControls controls) { |
| return widget.selectionEnabled && pasteEnabled &&_hasFocus && controls?.canPaste(this) == true |
| ? () => controls.handlePaste(this) |
| : null; |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| assert(debugCheckHasMediaQuery(context)); |
| _focusAttachment.reparent(); |
| super.build(context); // See AutomaticKeepAliveClientMixin. |
| |
| final TextSelectionControls controls = widget.selectionControls; |
| return Scrollable( |
| excludeFromSemantics: true, |
| axisDirection: _isMultiline ? AxisDirection.down : AxisDirection.right, |
| controller: _scrollController, |
| physics: widget.scrollPhysics, |
| dragStartBehavior: widget.dragStartBehavior, |
| viewportBuilder: (BuildContext context, ViewportOffset offset) { |
| return CompositedTransformTarget( |
| link: _toolbarLayerLink, |
| child: Semantics( |
| onCopy: _semanticsOnCopy(controls), |
| onCut: _semanticsOnCut(controls), |
| onPaste: _semanticsOnPaste(controls), |
| child: _Editable( |
| key: _editableKey, |
| startHandleLayerLink: _startHandleLayerLink, |
| endHandleLayerLink: _endHandleLayerLink, |
| textSpan: buildTextSpan(), |
| value: _value, |
| cursorColor: _cursorColor, |
| backgroundCursorColor: widget.backgroundCursorColor, |
| showCursor: EditableText.debugDeterministicCursor |
| ? ValueNotifier<bool>(widget.showCursor) |
| : _cursorVisibilityNotifier, |
| forceLine: widget.forceLine, |
| readOnly: widget.readOnly, |
| hasFocus: _hasFocus, |
| maxLines: widget.maxLines, |
| minLines: widget.minLines, |
| expands: widget.expands, |
| strutStyle: widget.strutStyle, |
| selectionColor: widget.selectionColor, |
| textScaleFactor: widget.textScaleFactor ?? MediaQuery.textScaleFactorOf(context), |
| textAlign: widget.textAlign, |
| textDirection: _textDirection, |
| locale: widget.locale, |
| textWidthBasis: widget.textWidthBasis, |
| obscureText: widget.obscureText, |
| autocorrect: widget.autocorrect, |
| smartDashesType: widget.smartDashesType, |
| smartQuotesType: widget.smartQuotesType, |
| enableSuggestions: widget.enableSuggestions, |
| offset: offset, |
| onSelectionChanged: _handleSelectionChanged, |
| onCaretChanged: _handleCaretChanged, |
| rendererIgnoresPointer: widget.rendererIgnoresPointer, |
| cursorWidth: widget.cursorWidth, |
| cursorRadius: widget.cursorRadius, |
| cursorOffset: widget.cursorOffset, |
| selectionHeightStyle: widget.selectionHeightStyle, |
| selectionWidthStyle: widget.selectionWidthStyle, |
| paintCursorAboveText: widget.paintCursorAboveText, |
| enableInteractiveSelection: widget.enableInteractiveSelection, |
| textSelectionDelegate: this, |
| devicePixelRatio: _devicePixelRatio, |
| ), |
| ), |
| ); |
| }, |
| ); |
| } |
| |
| /// 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() { |
| if (widget.obscureText) { |
| String text = _value.text; |
| text = RenderEditable.obscuringCharacter * text.length; |
| final int o = |
| _obscureShowCharTicksPending > 0 ? _obscureLatestCharIndex : null; |
| if (o != null && o >= 0 && o < text.length) |
| text = text.replaceRange(o, o + 1, _value.text.substring(o, o + 1)); |
| return TextSpan(style: widget.style, text: text); |
| } |
| // Read only mode should not paint text composing. |
| return widget.controller.buildTextSpan( |
| style: widget.style, |
| withComposing: !widget.readOnly, |
| ); |
| } |
| } |
| |
| class _Editable extends LeafRenderObjectWidget { |
| const _Editable({ |
| Key key, |
| this.textSpan, |
| this.value, |
| this.startHandleLayerLink, |
| this.endHandleLayerLink, |
| this.cursorColor, |
| this.backgroundCursorColor, |
| this.showCursor, |
| this.forceLine, |
| this.readOnly, |
| this.textWidthBasis, |
| this.hasFocus, |
| this.maxLines, |
| this.minLines, |
| this.expands, |
| this.strutStyle, |
| this.selectionColor, |
| this.textScaleFactor, |
| this.textAlign, |
| @required this.textDirection, |
| this.locale, |
| this.obscureText, |
| this.autocorrect, |
| this.smartDashesType, |
| this.smartQuotesType, |
| this.enableSuggestions, |
| this.offset, |
| this.onSelectionChanged, |
| this.onCaretChanged, |
| this.rendererIgnoresPointer = false, |
| this.cursorWidth, |
| this.cursorRadius, |
| this.cursorOffset, |
| this.paintCursorAboveText, |
| this.selectionHeightStyle = ui.BoxHeightStyle.tight, |
| this.selectionWidthStyle = ui.BoxWidthStyle.tight, |
| this.enableInteractiveSelection = true, |
| this.textSelectionDelegate, |
| this.devicePixelRatio, |
| }) : assert(textDirection != null), |
| assert(rendererIgnoresPointer != null), |
| super(key: key); |
| |
| final TextSpan textSpan; |
| final TextEditingValue value; |
| final Color cursorColor; |
| final LayerLink startHandleLayerLink; |
| final LayerLink endHandleLayerLink; |
| final Color backgroundCursorColor; |
| final ValueNotifier<bool> showCursor; |
| final bool forceLine; |
| final bool readOnly; |
| final bool hasFocus; |
| final int maxLines; |
| final int minLines; |
| final bool expands; |
| final StrutStyle strutStyle; |
| final Color selectionColor; |
| final double textScaleFactor; |
| final TextAlign textAlign; |
| final TextDirection textDirection; |
| final Locale locale; |
| final bool obscureText; |
| final TextWidthBasis textWidthBasis; |
| final bool autocorrect; |
| final SmartDashesType smartDashesType; |
| final SmartQuotesType smartQuotesType; |
| final bool enableSuggestions; |
| final ViewportOffset offset; |
| final SelectionChangedHandler onSelectionChanged; |
| final CaretChangedHandler onCaretChanged; |
| final bool rendererIgnoresPointer; |
| final double cursorWidth; |
| final Radius cursorRadius; |
| final Offset cursorOffset; |
| final bool paintCursorAboveText; |
| final ui.BoxHeightStyle selectionHeightStyle; |
| final ui.BoxWidthStyle selectionWidthStyle; |
| final bool enableInteractiveSelection; |
| final TextSelectionDelegate textSelectionDelegate; |
| final double devicePixelRatio; |
| |
| @override |
| RenderEditable createRenderObject(BuildContext context) { |
| return RenderEditable( |
| text: textSpan, |
| cursorColor: cursorColor, |
| startHandleLayerLink: startHandleLayerLink, |
| endHandleLayerLink: endHandleLayerLink, |
| backgroundCursorColor: backgroundCursorColor, |
| showCursor: showCursor, |
| forceLine: forceLine, |
| readOnly: readOnly, |
| hasFocus: hasFocus, |
| maxLines: maxLines, |
| minLines: minLines, |
| expands: expands, |
| strutStyle: strutStyle, |
| selectionColor: selectionColor, |
| textScaleFactor: textScaleFactor, |
| textAlign: textAlign, |
| textDirection: textDirection, |
| locale: locale ?? Localizations.localeOf(context, nullOk: true), |
| selection: value.selection, |
| offset: offset, |
| onSelectionChanged: onSelectionChanged, |
| onCaretChanged: onCaretChanged, |
| ignorePointer: rendererIgnoresPointer, |
| obscureText: obscureText, |
| textWidthBasis: textWidthBasis, |
| cursorWidth: cursorWidth, |
| cursorRadius: cursorRadius, |
| cursorOffset: cursorOffset, |
| paintCursorAboveText: paintCursorAboveText, |
| selectionHeightStyle: selectionHeightStyle, |
| selectionWidthStyle: selectionWidthStyle, |
| enableInteractiveSelection: enableInteractiveSelection, |
| textSelectionDelegate: textSelectionDelegate, |
| devicePixelRatio: devicePixelRatio, |
| ); |
| } |
| |
| @override |
| void updateRenderObject(BuildContext context, RenderEditable renderObject) { |
| renderObject |
| ..text = textSpan |
| ..cursorColor = cursorColor |
| ..startHandleLayerLink = startHandleLayerLink |
| ..endHandleLayerLink = endHandleLayerLink |
| ..showCursor = showCursor |
| ..forceLine = forceLine |
| ..readOnly = readOnly |
| ..hasFocus = hasFocus |
| ..maxLines = maxLines |
| ..minLines = minLines |
| ..expands = expands |
| ..strutStyle = strutStyle |
| ..selectionColor = selectionColor |
| ..textScaleFactor = textScaleFactor |
| ..textAlign = textAlign |
| ..textDirection = textDirection |
| ..locale = locale ?? Localizations.localeOf(context, nullOk: true) |
| ..selection = value.selection |
| ..offset = offset |
| ..onSelectionChanged = onSelectionChanged |
| ..onCaretChanged = onCaretChanged |
| ..ignorePointer = rendererIgnoresPointer |
| ..textWidthBasis = textWidthBasis |
| ..obscureText = obscureText |
| ..cursorWidth = cursorWidth |
| ..cursorRadius = cursorRadius |
| ..cursorOffset = cursorOffset |
| ..selectionHeightStyle = selectionHeightStyle |
| ..selectionWidthStyle = selectionWidthStyle |
| ..textSelectionDelegate = textSelectionDelegate |
| ..devicePixelRatio = devicePixelRatio |
| ..paintCursorAboveText = paintCursorAboveText; |
| } |
| } |
| |
| // This formatter inserts [Unicode.RLM] and [Unicode.LRM] into the |
| // string in order to preserve expected caret behavior when trailing |
| // whitespace is inserted. |
| // |
| // When typing in a direction that opposes the base direction |
| // of the paragraph, un-enclosed whitespace gets the directionality |
| // of the paragraph. This is often at odds with what is immeditely |
| // being typed causing the caret to jump to the wrong side of the text. |
| // This formatter makes use of the RLM and LRM to cause the text |
| // shaper to inherently treat the whitespace as being surrounded |
| // by the directionality of the previous non-whitespace codepoint. |
| class _WhitespaceDirectionalityFormatter extends TextInputFormatter { |
| // The [textDirection] should be the base directionality of the |
| // paragraph/editable. |
| _WhitespaceDirectionalityFormatter({TextDirection textDirection}) |
| : _baseDirection = textDirection, |
| _previousNonWhitespaceDirection = textDirection; |
| |
| // Using regex here instead of ICU is suboptimal, but is enough |
| // to produce the correct results for any reasonable input where this |
| // is even relevant. Using full ICU would be a much heavier change, |
| // requiring exposure of the C++ ICU API. |
| // |
| // LTR covers most scripts and symbols, including but not limited to Latin, |
| // ideographic scripts (Chinese, Japanese, etc), Cyrilic, Indic, and |
| // SE Asian scripts. |
| final RegExp _ltrRegExp = RegExp(r'[A-Za-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02B8\u0300-\u0590\u0800-\u1FFF\u2C00-\uFB1C\uFDFE-\uFE6F\uFEFD-\uFFFF]'); |
| // RTL covers Arabic, Hebrew, and other RTL languages such as Urdu, |
| // Aramic, Farsi, Dhivehi. |
| final RegExp _rtlRegExp = RegExp(r'[\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC]'); |
| // Although whitespaces are not the only codepoints that have weak directionality, |
| // these are the primary cause of the caret being misplaced. |
| final RegExp _whitespaceRegExp = RegExp(r'\s'); |
| |
| final TextDirection _baseDirection; |
| // Tracks the directionality of the most recently encountered |
| // codepoint that was not whitespace. This becomes the direction of |
| // marker inserted to fully surround ambiguous whitespace. |
| TextDirection _previousNonWhitespaceDirection; |
| |
| // Prevents the formatter from attempting more expensive formatting |
| // operations mixed directionality is found. |
| bool _hasOpposingDirection = false; |
| |
| // See [Unicode.RLM] and [Unicode.LRM]. |
| // |
| // We do not directly use the [Unicode] constants since they are strings. |
| static const int _rlm = 0x200F; |
| static const int _lrm = 0x200E; |
| |
| @override |
| TextEditingValue formatEditUpdate( |
| TextEditingValue oldValue, |
| TextEditingValue newValue, |
| ) { |
| // Skip formatting (which can be more expensive) if there are no cases of |
| // mixing directionality. Once a case of mixed directionality is found, |
| // always perform the formatting. |
| if (!_hasOpposingDirection) { |
| _hasOpposingDirection = _baseDirection == TextDirection.ltr ? |
| _rtlRegExp.hasMatch(newValue.text) : _ltrRegExp.hasMatch(newValue.text); |
| } |
| |
| if (_hasOpposingDirection) { |
| _previousNonWhitespaceDirection = _baseDirection; |
| |
| final List<int> outputCodepoints = <int>[]; |
| |
| // We add/subtract from these as we insert/remove markers. |
| int selectionBase = newValue.selection.baseOffset; |
| int selectionExtent = newValue.selection.extentOffset; |
| |
| void addToSelection() { |
| selectionBase += outputCodepoints.length <= selectionBase ? 1 : 0; |
| selectionExtent += outputCodepoints.length <= selectionExtent ? 1 : 0; |
| } |
| void subtractFromSelection() { |
| selectionBase -= outputCodepoints.length < selectionBase ? 1 : 0; |
| selectionExtent -= outputCodepoints.length < selectionExtent ? 1 : 0; |
| } |
| |
| bool previousWasWhitespace = false; |
| bool previousWasDirectionalityMarker = false; |
| int previousNonWhitespaceCodepoint; |
| for (final int codepoint in newValue.text.runes) { |
| if (isWhitespace(codepoint)) { |
| // Only compute the directionality of the non-whitespace |
| // when the value is needed. |
| if (!previousWasWhitespace && previousNonWhitespaceCodepoint != null) { |
| _previousNonWhitespaceDirection = getDirection(previousNonWhitespaceCodepoint); |
| } |
| // If we already added directionality for this run of whitespace, |
| // "shift" the marker added to the end of the whitespace run. |
| if (previousWasWhitespace) { |
| subtractFromSelection(); |
| outputCodepoints.removeLast(); |
| } |
| outputCodepoints.add(codepoint); |
| addToSelection(); |
| outputCodepoints.add(_previousNonWhitespaceDirection == TextDirection.rtl ? _rlm : _lrm); |
| |
| previousWasWhitespace = true; |
| previousWasDirectionalityMarker = false; |
| } else if (isDirectionalityMarker(codepoint)) { |
| // Handle pre-existing directionality markers. Use pre-existing marker |
| // instead of the one we add. |
| if (previousWasWhitespace) { |
| subtractFromSelection(); |
| outputCodepoints.removeLast(); |
| } |
| outputCodepoints.add(codepoint); |
| |
| previousWasWhitespace = false; |
| previousWasDirectionalityMarker = true; |
| } else { |
| // If the whitespace was already enclosed by the same directionality, |
| // we can remove the artifically added marker. |
| if (!previousWasDirectionalityMarker && |
| previousWasWhitespace && |
| getDirection(codepoint) == _previousNonWhitespaceDirection) { |
| subtractFromSelection(); |
| outputCodepoints.removeLast(); |
| } |
| // Normal character, track its codepoint add it to the string. |
| previousNonWhitespaceCodepoint = codepoint; |
| outputCodepoints.add(codepoint); |
| |
| previousWasWhitespace = false; |
| previousWasDirectionalityMarker = false; |
| } |
| } |
| final String formatted = String.fromCharCodes(outputCodepoints); |
| return TextEditingValue( |
| text: formatted, |
| selection: TextSelection( |
| baseOffset: selectionBase, |
| extentOffset: selectionExtent, |
| affinity: newValue.selection.affinity, |
| isDirectional: newValue.selection.isDirectional |
| ), |
| ); |
| } |
| return newValue; |
| } |
| |
| bool isWhitespace(int value) { |
| return _whitespaceRegExp.hasMatch(String.fromCharCode(value)); |
| } |
| |
| bool isDirectionalityMarker(int value) { |
| return value == _rlm || value == _lrm; |
| } |
| |
| TextDirection getDirection(int value) { |
| // Use the LTR version as short-circuiting will be more efficient since |
| // there are more LTR codepoints. |
| return _ltrRegExp.hasMatch(String.fromCharCode(value)) ? TextDirection.ltr : TextDirection.rtl; |
| } |
| } |