blob: a4c8e7d0a7b4096f26e5300734512f30db365ec9 [file]
// 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/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:meta/meta.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;
const Duration _kCursorBlinkHalfPeriod = const Duration(milliseconds: 500);
/// 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 editiable 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, 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) {
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 1, meaning this is a single-line
/// text field. If it is not null, it must be greater than zero.
///
/// The [controller], [focusNode], [style], and [cursorColor] arguments must
/// not be null.
EditableText({
Key key,
@required this.controller,
@required this.focusNode,
this.obscureText: false,
@required this.style,
@required this.cursorColor,
this.textAlign,
this.textScaleFactor,
this.maxLines: 1,
this.autofocus: false,
this.selectionColor,
this.selectionControls,
this.keyboardType,
this.onChanged,
this.onSubmitted,
List<TextInputFormatter> inputFormatters,
}) : assert(controller != null),
assert(focusNode != null),
assert(obscureText != null),
assert(style != null),
assert(cursorColor != null),
assert(maxLines == null || maxLines > 0),
assert(autofocus != null),
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;
/// The text style to use for the editable text.
final TextStyle style;
/// How the text should be aligned horizontally.
final TextAlign textAlign;
/// 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;
/// Optional input validation and formatting overrides. Formatters are run
/// in the provided order when the text input changes.
final List<TextInputFormatter> inputFormatters;
@override
EditableTextState createState() => new EditableTextState();
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('controller: $controller');
description.add('focusNode: $focusNode');
if (obscureText != false)
description.add('obscureText: $obscureText');
description.add('$style');
if (textAlign != null)
description.add('$textAlign');
if (textScaleFactor != null)
description.add('textScaleFactor: $textScaleFactor');
if (maxLines != 1)
description.add('maxLines: $maxLines');
if (autofocus != false)
description.add('autofocus: $autofocus');
if (keyboardType != null)
description.add('keyboardType: $keyboardType');
}
}
/// State for a [EditableText].
class EditableTextState extends State<EditableText> implements TextInputClient {
Timer _cursorTimer;
final ValueNotifier<bool> _showCursor = new ValueNotifier<bool>(false);
TextInputConnection _textInputConnection;
TextSelectionOverlay _selectionOverlay;
final ScrollController _scrollController = new ScrollController();
bool _didAutoFocus = false;
// State lifecycle:
@override
void initState() {
super.initState();
widget.controller.addListener(_didChangeTextEditingValue);
widget.focusNode.addListener(_handleFocusChanged);
}
@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);
}
}
@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();
_lastKnownRemoteTextEditingValue = value;
_formatAndSetValue(value);
if (widget.onChanged != null)
widget.onChanged(value.text);
}
@override
void performAction(TextInputAction action) {
widget.controller.clearComposing();
widget.focusNode.unfocus();
if (widget.onSubmitted != null)
widget.onSubmitted(_value.text);
}
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))
..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, bool longPress) {
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,
renderObject: renderObject,
onSelectionOverlayChanged: _handleSelectionOverlayChanged,
selectionControls: widget.selectionControls,
);
if (_value.text.isNotEmpty || longPress)
_selectionOverlay.showHandles();
if (longPress)
_selectionOverlay.showToolbar();
}
}
void _handleSelectionOverlayChanged(TextEditingValue value, Rect caretRect) {
assert(!value.composing.isValid); // composing range must be empty while selecting.
_formatAndSetValue(value);
_scrollController.jumpTo(_getScrollOffsetForCaret(caretRect));
}
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;
_scrollController.animateTo(
_getScrollOffsetForCaret(caretRect),
curve: Curves.fastOutSlowIn,
duration: const Duration(milliseconds: 50),
);
}
}
void _formatAndSetValue(TextEditingValue value) {
if (widget.inputFormatters != null && widget.inputFormatters.isNotEmpty) {
for (TextInputFormatter formatter in widget.inputFormatters)
value = formatter.formatEditUpdate(_value, value);
_value = value;
_updateRemoteEditingValueIfNeeded();
} else {
_value = value;
}
}
/// 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;
void _cursorTick(Timer timer) {
_showCursor.value = !_showCursor.value;
}
void _startCursorTimer() {
_showCursor.value = true;
_cursorTimer = new Timer.periodic(_kCursorBlinkHalfPeriod, _cursorTick);
}
void _stopCursorTimer() {
_cursorTimer?.cancel();
_cursorTimer = null;
_showCursor.value = false;
}
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();
}
@override
Widget build(BuildContext context) {
FocusScope.of(context).reparentIfNeeded(widget.focusNode);
return new Scrollable(
axisDirection: _isMultiline ? AxisDirection.down : AxisDirection.right,
controller: _scrollController,
physics: const ClampingScrollPhysics(),
viewportBuilder: (BuildContext context, ViewportOffset offset) {
return new _Editable(
value: _value,
style: widget.style,
cursorColor: widget.cursorColor,
showCursor: _showCursor,
maxLines: widget.maxLines,
selectionColor: widget.selectionColor,
textScaleFactor: widget.textScaleFactor ?? MediaQuery.of(context, nullOk: true)?.textScaleFactor ?? 1.0,
textAlign: widget.textAlign,
obscureText: widget.obscureText,
offset: offset,
onSelectionChanged: _handleSelectionChanged,
onCaretChanged: _handleCaretChanged,
);
},
);
}
}
class _Editable extends LeafRenderObjectWidget {
const _Editable({
Key key,
this.value,
this.style,
this.cursorColor,
this.showCursor,
this.maxLines,
this.selectionColor,
this.textScaleFactor,
this.textAlign,
this.obscureText,
this.offset,
this.onSelectionChanged,
this.onCaretChanged,
}) : super(key: key);
final TextEditingValue value;
final TextStyle style;
final Color cursorColor;
final ValueNotifier<bool> showCursor;
final int maxLines;
final Color selectionColor;
final double textScaleFactor;
final TextAlign textAlign;
final bool obscureText;
final ViewportOffset offset;
final SelectionChangedHandler onSelectionChanged;
final CaretChangedHandler onCaretChanged;
@override
RenderEditable createRenderObject(BuildContext context) {
return new RenderEditable(
text: _styledTextSpan,
cursorColor: cursorColor,
showCursor: showCursor,
maxLines: maxLines,
selectionColor: selectionColor,
textScaleFactor: textScaleFactor,
textAlign: textAlign,
selection: value.selection,
offset: offset,
onSelectionChanged: onSelectionChanged,
onCaretChanged: onCaretChanged,
);
}
@override
void updateRenderObject(BuildContext context, RenderEditable renderObject) {
renderObject
..text = _styledTextSpan
..cursorColor = cursorColor
..showCursor = showCursor
..maxLines = maxLines
..selectionColor = selectionColor
..textScaleFactor = textScaleFactor
..textAlign = textAlign
..selection = value.selection
..offset = offset
..onSelectionChanged = onSelectionChanged
..onCaretChanged = onCaretChanged;
}
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 = new String.fromCharCodes(new List<int>.filled(text.length, 0x2022));
return new TextSpan(style: style, text: text);
}
}