| // Copyright 2015 The Chromium 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 'package:flutter/rendering.dart'; |
| import 'package:flutter/services.dart'; |
| |
| import 'automatic_keep_alive.dart'; |
| import 'basic.dart'; |
| import 'focus_manager.dart'; |
| import 'focus_scope.dart'; |
| import 'framework.dart'; |
| import 'media_query.dart'; |
| import 'scroll_controller.dart'; |
| import 'scroll_physics.dart'; |
| import 'scrollable.dart'; |
| import 'text_selection.dart'; |
| |
| export 'package:flutter/services.dart' show TextEditingValue, TextSelection, TextInputType; |
| export 'package:flutter/rendering.dart' show SelectionChangedCause; |
| |
| /// Signature for the callback that reports when the user changes the selection |
| /// (including the cursor location). |
| typedef void SelectionChangedCallback(TextSelection selection, SelectionChangedCause cause); |
| |
| const Duration _kCursorBlinkHalfPeriod = const 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; |
| |
| /// 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. |
| /// |
| /// 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]. |
| 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 : new 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. |
| set text(String newText) { |
| value = value.copyWith(text: newText, |
| selection: const TextSelection.collapsed(offset: -1), |
| composing: TextRange.empty); |
| } |
| |
| /// 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. |
| set selection(TextSelection newSelection) { |
| if (newSelection.start > text.length || newSelection.end > text.length) |
| throw new 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); |
| } |
| } |
| |
| /// 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). |
| /// |
| /// 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. |
| /// |
| /// 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 [controller], [focusNode], [style], [cursorColor], [textAlign], |
| /// and [rendererIgnoresPointer], arguments must not be null. |
| EditableText({ |
| Key key, |
| @required this.controller, |
| @required this.focusNode, |
| this.obscureText: false, |
| this.autocorrect: true, |
| @required this.style, |
| @required this.cursorColor, |
| this.textAlign: TextAlign.start, |
| this.textDirection, |
| this.textScaleFactor, |
| this.maxLines: 1, |
| this.autofocus: false, |
| this.selectionColor, |
| this.selectionControls, |
| TextInputType keyboardType, |
| this.onChanged, |
| this.onSubmitted, |
| this.onSelectionChanged, |
| List<TextInputFormatter> inputFormatters, |
| this.rendererIgnoresPointer: false, |
| }) : assert(controller != null), |
| assert(focusNode != null), |
| assert(obscureText != null), |
| assert(autocorrect != null), |
| assert(style != null), |
| assert(cursorColor != null), |
| assert(textAlign != null), |
| assert(maxLines == null || maxLines > 0), |
| assert(autofocus != null), |
| assert(rendererIgnoresPointer != null), |
| keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline), |
| inputFormatters = maxLines == 1 |
| ? ( |
| <TextInputFormatter>[BlacklistingTextInputFormatter.singleLineFormatter] |
| ..addAll(inputFormatters ?? const Iterable<TextInputFormatter>.empty()) |
| ) |
| : inputFormatters, |
| super(key: key); |
| |
| /// Controls the text being edited. |
| final TextEditingController controller; |
| |
| /// Controls whether this widget has keyboard focus. |
| final FocusNode focusNode; |
| |
| /// Whether to hide the text being edited (e.g., for passwords). |
| /// |
| /// Defaults to false. |
| final bool obscureText; |
| |
| /// Whether to enable autocorrection. |
| /// |
| /// Defaults to true. |
| final bool autocorrect; |
| |
| /// The text style to use for the editable text. |
| final TextStyle style; |
| |
| /// How the text should be aligned horizontally. |
| /// |
| /// Defaults to [TextAlign.start]. |
| final TextAlign textAlign; |
| |
| /// 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. |
| final TextDirection textDirection; |
| |
| /// 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. |
| final Color cursorColor; |
| |
| /// 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. If it is not |
| /// null, the value must be greater than zero. |
| final int maxLines; |
| |
| /// Whether this input field should focus itself if nothing else is already focused. |
| /// If true, the keyboard will open as soon as this input obtains focus. Otherwise, |
| /// the keyboard is only shown after the user taps the text field. |
| /// |
| /// Defaults to false. |
| final bool autofocus; |
| |
| /// The color to use when painting the selection. |
| final Color selectionColor; |
| |
| /// Optional delegate for building the text selection handles and toolbar. |
| final TextSelectionControls selectionControls; |
| |
| /// The type of keyboard to use for editing the text. |
| final TextInputType keyboardType; |
| |
| /// Called when the text being edited changes. |
| final ValueChanged<String> onChanged; |
| |
| /// Called when the user indicates that they are done editing the text in the field. |
| final ValueChanged<String> onSubmitted; |
| |
| /// Called when the user changes the selection of text (including the cursor |
| /// location). |
| final SelectionChangedCallback onSelectionChanged; |
| |
| /// Optional input validation and formatting overrides. Formatters are run |
| /// in the provided order when the text input changes. |
| 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; |
| |
| @override |
| EditableTextState createState() => new EditableTextState(); |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| super.debugFillProperties(properties); |
| properties.add(new DiagnosticsProperty<TextEditingController>('controller', controller)); |
| properties.add(new DiagnosticsProperty<FocusNode>('focusNode', focusNode)); |
| properties.add(new DiagnosticsProperty<bool>('obscureText', obscureText, defaultValue: false)); |
| properties.add(new DiagnosticsProperty<bool>('autocorrect', autocorrect, defaultValue: true)); |
| style?.debugFillProperties(properties); |
| properties.add(new EnumProperty<TextAlign>('textAlign', textAlign, defaultValue: null)); |
| properties.add(new EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null)); |
| properties.add(new DoubleProperty('textScaleFactor', textScaleFactor, defaultValue: null)); |
| properties.add(new IntProperty('maxLines', maxLines, defaultValue: 1)); |
| properties.add(new DiagnosticsProperty<bool>('autofocus', autofocus, defaultValue: false)); |
| properties.add(new DiagnosticsProperty<TextInputType>('keyboardType', keyboardType, defaultValue: null)); |
| } |
| } |
| |
| /// State for a [EditableText]. |
| class EditableTextState extends State<EditableText> with AutomaticKeepAliveClientMixin implements TextInputClient, TextSelectionDelegate { |
| Timer _cursorTimer; |
| final ValueNotifier<bool> _showCursor = new ValueNotifier<bool>(false); |
| final GlobalKey _editableKey = new GlobalKey(); |
| |
| TextInputConnection _textInputConnection; |
| TextSelectionOverlay _selectionOverlay; |
| |
| final ScrollController _scrollController = new ScrollController(); |
| final LayerLink _layerLink = new LayerLink(); |
| bool _didAutoFocus = false; |
| |
| @override |
| bool get wantKeepAlive => widget.focusNode.hasFocus; |
| |
| // State lifecycle: |
| |
| @override |
| void initState() { |
| super.initState(); |
| widget.controller.addListener(_didChangeTextEditingValue); |
| widget.focusNode.addListener(_handleFocusChanged); |
| _scrollController.addListener(() { _selectionOverlay?.updateForScroll(); }); |
| } |
| |
| @override |
| void didChangeDependencies() { |
| super.didChangeDependencies(); |
| if (!_didAutoFocus && widget.autofocus) { |
| FocusScope.of(context).autofocus(widget.focusNode); |
| _didAutoFocus = true; |
| } |
| } |
| |
| @override |
| void didUpdateWidget(EditableText oldWidget) { |
| super.didUpdateWidget(oldWidget); |
| if (widget.controller != oldWidget.controller) { |
| oldWidget.controller.removeListener(_didChangeTextEditingValue); |
| widget.controller.addListener(_didChangeTextEditingValue); |
| _updateRemoteEditingValueIfNeeded(); |
| } |
| if (widget.focusNode != oldWidget.focusNode) { |
| oldWidget.focusNode.removeListener(_handleFocusChanged); |
| widget.focusNode.addListener(_handleFocusChanged); |
| updateKeepAlive(); |
| } |
| } |
| |
| @override |
| void dispose() { |
| widget.controller.removeListener(_didChangeTextEditingValue); |
| _closeInputConnectionIfNeeded(); |
| assert(!_hasInputConnection); |
| _stopCursorTimer(); |
| assert(_cursorTimer == null); |
| _selectionOverlay?.dispose(); |
| _selectionOverlay = null; |
| widget.focusNode.removeListener(_handleFocusChanged); |
| super.dispose(); |
| } |
| |
| // TextInputClient implementation: |
| |
| TextEditingValue _lastKnownRemoteTextEditingValue; |
| |
| @override |
| void updateEditingValue(TextEditingValue value) { |
| if (value.text != _value.text) { |
| _hideSelectionOverlayIfNeeded(); |
| if (widget.obscureText && value.text.length == _value.text.length + 1) { |
| _obscureShowCharTicksPending = _kObscureShowLatestCharCursorTicks; |
| _obscureLatestCharIndex = _value.selection.baseOffset; |
| } |
| } |
| _lastKnownRemoteTextEditingValue = value; |
| _formatAndSetValue(value); |
| } |
| |
| @override |
| void performAction(TextInputAction action) { |
| switch (action) { |
| case TextInputAction.done: |
| widget.controller.clearComposing(); |
| widget.focusNode.unfocus(); |
| if (widget.onSubmitted != null) |
| widget.onSubmitted(_value.text); |
| break; |
| case TextInputAction.newline: |
| // Do nothing for a "newline" action: the newline is already inserted. |
| break; |
| } |
| } |
| |
| void _updateRemoteEditingValueIfNeeded() { |
| if (!_hasInputConnection) |
| return; |
| final TextEditingValue localValue = _value; |
| if (localValue == _lastKnownRemoteTextEditingValue) |
| return; |
| _lastKnownRemoteTextEditingValue = localValue; |
| _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) { |
| final double caretStart = _isMultiline ? caretRect.top : caretRect.left; |
| final double caretEnd = _isMultiline ? caretRect.bottom : 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; |
| return scrollOffset; |
| } |
| |
| bool get _hasInputConnection => _textInputConnection != null && _textInputConnection.attached; |
| |
| void _openInputConnection() { |
| if (!_hasInputConnection) { |
| final TextEditingValue localValue = _value; |
| _lastKnownRemoteTextEditingValue = localValue; |
| _textInputConnection = TextInput.attach(this, |
| new TextInputConfiguration( |
| inputType: widget.keyboardType, |
| obscureText: widget.obscureText, |
| autocorrect: widget.autocorrect, |
| inputAction: widget.keyboardType == TextInputType.multiline |
| ? TextInputAction.newline |
| : TextInputAction.done |
| ) |
| )..setEditingState(localValue); |
| } |
| _textInputConnection.show(); |
| } |
| |
| void _closeInputConnectionIfNeeded() { |
| if (_hasInputConnection) { |
| _textInputConnection.close(); |
| _textInputConnection = null; |
| _lastKnownRemoteTextEditingValue = null; |
| } |
| } |
| |
| void _openOrCloseInputConnectionIfNeeded() { |
| if (_hasFocus && widget.focusNode.consumeKeyboardToken()) { |
| _openInputConnection(); |
| } else if (!_hasFocus) { |
| _closeInputConnectionIfNeeded(); |
| widget.controller.clearComposing(); |
| } |
| } |
| |
| /// 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 |
| FocusScope.of(context).requestFocus(widget.focusNode); |
| } |
| |
| void _hideSelectionOverlayIfNeeded() { |
| _selectionOverlay?.hide(); |
| _selectionOverlay = null; |
| } |
| |
| void _updateOrDisposeSelectionOverlayIfNeeded() { |
| if (_selectionOverlay != null) { |
| if (_hasFocus) { |
| _selectionOverlay.update(_value); |
| } else { |
| _selectionOverlay.dispose(); |
| _selectionOverlay = null; |
| } |
| } |
| } |
| |
| void _handleSelectionChanged(TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) { |
| widget.controller.selection = selection; |
| |
| // This will show the keyboard for all selection changes on the |
| // EditableWidget, not just changes triggered by user gestures. |
| requestKeyboard(); |
| |
| _hideSelectionOverlayIfNeeded(); |
| |
| if (widget.selectionControls != null) { |
| _selectionOverlay = new TextSelectionOverlay( |
| context: context, |
| value: _value, |
| debugRequiredFor: widget, |
| layerLink: _layerLink, |
| renderObject: renderObject, |
| selectionControls: widget.selectionControls, |
| selectionDelegate: this, |
| ); |
| final bool longPress = cause == SelectionChangedCause.longPress; |
| if (cause != SelectionChangedCause.keyboard && (_value.text.isNotEmpty || longPress)) |
| _selectionOverlay.showHandles(); |
| if (longPress) |
| _selectionOverlay.showToolbar(); |
| if (widget.onSelectionChanged != null) |
| widget.onSelectionChanged(selection, cause); |
| } |
| } |
| |
| bool _textChangedSinceLastCaretUpdate = false; |
| |
| void _handleCaretChanged(Rect 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; |
| scheduleMicrotask(() { |
| _scrollController.animateTo( |
| _getScrollOffsetForCaret(caretRect), |
| curve: Curves.fastOutSlowIn, |
| duration: const Duration(milliseconds: 50), |
| ); |
| }); |
| } |
| } |
| |
| void _formatAndSetValue(TextEditingValue value) { |
| final bool textChanged = _value?.text != value?.text; |
| if (widget.inputFormatters != null && widget.inputFormatters.isNotEmpty) { |
| for (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); |
| } |
| |
| /// Whether the blinking cursor is actually visible at this precise moment |
| /// (it's hidden half the time, since it blinks). |
| @visibleForTesting |
| bool get cursorCurrentlyVisible => _showCursor.value; |
| |
| /// 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) { |
| _showCursor.value = !_showCursor.value; |
| if (_obscureShowCharTicksPending > 0) { |
| setState(() { _obscureShowCharTicksPending--; }); |
| } |
| } |
| |
| void _startCursorTimer() { |
| _showCursor.value = true; |
| _cursorTimer = new Timer.periodic(_kCursorBlinkHalfPeriod, _cursorTick); |
| } |
| |
| void _stopCursorTimer() { |
| _cursorTimer?.cancel(); |
| _cursorTimer = null; |
| _showCursor.value = false; |
| _obscureShowCharTicksPending = 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) { |
| // Clear the selection and composition state if this widget lost focus. |
| _value = new TextEditingValue(text: _value.text); |
| } |
| updateKeepAlive(); |
| } |
| |
| 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(); |
| |
| @override |
| TextEditingValue get textEditingValue => _value; |
| |
| @override |
| set textEditingValue(TextEditingValue value) { |
| _selectionOverlay?.update(value); |
| _formatAndSetValue(value); |
| } |
| |
| @override |
| void bringIntoView(TextPosition position) { |
| _scrollController.jumpTo(_getScrollOffsetForCaret(renderEditable.getLocalRectForCaret(position))); |
| } |
| |
| @override |
| void hideToolbar() { |
| _selectionOverlay?.hide(); |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| FocusScope.of(context).reparentIfNeeded(widget.focusNode); |
| super.build(context); // See AutomaticKeepAliveClientMixin. |
| final TextSelectionControls controls = widget.selectionControls; |
| return new Scrollable( |
| excludeFromSemantics: true, |
| axisDirection: _isMultiline ? AxisDirection.down : AxisDirection.right, |
| controller: _scrollController, |
| physics: const ClampingScrollPhysics(), |
| viewportBuilder: (BuildContext context, ViewportOffset offset) { |
| return new CompositedTransformTarget( |
| link: _layerLink, |
| child: new Semantics( |
| onCopy: _hasFocus && controls?.canCopy(this) == true ? () => controls.handleCopy(this) : null, |
| onCut: _hasFocus && controls?.canCut(this) == true ? () => controls.handleCut(this) : null, |
| onPaste: _hasFocus && controls?.canPaste(this) == true ? () => controls.handlePaste(this) : null, |
| child: new _Editable( |
| key: _editableKey, |
| value: _value, |
| style: widget.style, |
| cursorColor: widget.cursorColor, |
| showCursor: _showCursor, |
| hasFocus: _hasFocus, |
| maxLines: widget.maxLines, |
| selectionColor: widget.selectionColor, |
| textScaleFactor: widget.textScaleFactor ?? MediaQuery.of(context, nullOk: true)?.textScaleFactor ?? 1.0, |
| textAlign: widget.textAlign, |
| textDirection: _textDirection, |
| obscureText: widget.obscureText, |
| obscureShowCharacterAtIndex: _obscureShowCharTicksPending > 0 ? _obscureLatestCharIndex : null, |
| autocorrect: widget.autocorrect, |
| offset: offset, |
| onSelectionChanged: _handleSelectionChanged, |
| onCaretChanged: _handleCaretChanged, |
| rendererIgnoresPointer: widget.rendererIgnoresPointer, |
| ), |
| ), |
| ); |
| }, |
| ); |
| } |
| } |
| |
| class _Editable extends LeafRenderObjectWidget { |
| const _Editable({ |
| Key key, |
| this.value, |
| this.style, |
| this.cursorColor, |
| this.showCursor, |
| this.hasFocus, |
| this.maxLines, |
| this.selectionColor, |
| this.textScaleFactor, |
| this.textAlign, |
| @required this.textDirection, |
| this.obscureText, |
| this.obscureShowCharacterAtIndex, |
| this.autocorrect, |
| this.offset, |
| this.onSelectionChanged, |
| this.onCaretChanged, |
| this.rendererIgnoresPointer: false, |
| }) : assert(textDirection != null), |
| assert(rendererIgnoresPointer != null), |
| super(key: key); |
| |
| final TextEditingValue value; |
| final TextStyle style; |
| final Color cursorColor; |
| final ValueNotifier<bool> showCursor; |
| final bool hasFocus; |
| final int maxLines; |
| final Color selectionColor; |
| final double textScaleFactor; |
| final TextAlign textAlign; |
| final TextDirection textDirection; |
| final bool obscureText; |
| final int obscureShowCharacterAtIndex; |
| final bool autocorrect; |
| final ViewportOffset offset; |
| final SelectionChangedHandler onSelectionChanged; |
| final CaretChangedHandler onCaretChanged; |
| final bool rendererIgnoresPointer; |
| |
| @override |
| RenderEditable createRenderObject(BuildContext context) { |
| return new RenderEditable( |
| text: _styledTextSpan, |
| cursorColor: cursorColor, |
| showCursor: showCursor, |
| hasFocus: hasFocus, |
| maxLines: maxLines, |
| selectionColor: selectionColor, |
| textScaleFactor: textScaleFactor, |
| textAlign: textAlign, |
| textDirection: textDirection, |
| selection: value.selection, |
| offset: offset, |
| onSelectionChanged: onSelectionChanged, |
| onCaretChanged: onCaretChanged, |
| ignorePointer: rendererIgnoresPointer, |
| obscureText: obscureText, |
| ); |
| } |
| |
| @override |
| void updateRenderObject(BuildContext context, RenderEditable renderObject) { |
| renderObject |
| ..text = _styledTextSpan |
| ..cursorColor = cursorColor |
| ..showCursor = showCursor |
| ..hasFocus = hasFocus |
| ..maxLines = maxLines |
| ..selectionColor = selectionColor |
| ..textScaleFactor = textScaleFactor |
| ..textAlign = textAlign |
| ..textDirection = textDirection |
| ..selection = value.selection |
| ..offset = offset |
| ..onSelectionChanged = onSelectionChanged |
| ..onCaretChanged = onCaretChanged |
| ..ignorePointer = rendererIgnoresPointer |
| ..obscureText = obscureText; |
| } |
| |
| TextSpan get _styledTextSpan { |
| if (!obscureText && value.composing.isValid) { |
| final TextStyle composingStyle = style.merge( |
| const TextStyle(decoration: TextDecoration.underline) |
| ); |
| |
| return new TextSpan( |
| style: style, |
| children: <TextSpan>[ |
| new TextSpan(text: value.composing.textBefore(value.text)), |
| new TextSpan( |
| style: composingStyle, |
| text: value.composing.textInside(value.text) |
| ), |
| new TextSpan(text: value.composing.textAfter(value.text)) |
| ]); |
| } |
| |
| String text = value.text; |
| if (obscureText) { |
| text = RenderEditable.obscuringCharacter * text.length; |
| final int o = obscureShowCharacterAtIndex; |
| if (o != null && o >= 0 && o < text.length) |
| text = text.replaceRange(o, o + 1, value.text.substring(o, o + 1)); |
| } |
| return new TextSpan(style: style, text: text); |
| } |
| } |