text_field.dart 23.6 KB
Newer Older
1 2 3 4
// 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.

5 6
import 'dart:collection';

xster's avatar
xster committed
7
import 'package:flutter/cupertino.dart';
8
import 'package:flutter/rendering.dart';
9 10 11
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';

12
import 'debug.dart';
13
import 'feedback.dart';
14
import 'ink_well.dart' show InteractiveInkFeature;
15
import 'input_decorator.dart';
16
import 'material.dart';
17 18 19
import 'text_selection.dart';
import 'theme.dart';

20
export 'package:flutter/services.dart' show TextInputType, TextInputAction, TextCapitalization;
21

22
/// A material design text field.
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
///
/// A text field lets the user enter text, either with hardware keyboard or with
/// an onscreen keyboard.
///
/// The text field calls the [onChanged] callback whenever the user changes the
/// text in the field. If the user indicates that they are done typing in the
/// field (e.g., by pressing a button on the soft keyboard), the text field
/// calls the [onSubmitted] callback.
///
/// To control the text that is displayed in the text field, use the
/// [controller]. For example, to set the initial value of the text field, use
/// a [controller] that already contains some text. The [controller] can also
/// control the selection and composing region (and to observe changes to the
/// text, selection, and composing region).
///
/// By default, a text field has a [decoration] that draws a divider below the
/// text field. You can use the [decoration] property to control the decoration,
/// for example by adding a label or an icon. If you set the [decoration]
/// property to null, the decoration will be removed entirely, including the
/// extra padding introduced by the decoration to save space for the labels.
///
/// If [decoration] is non-null (which is the default), the text field requires
45 46 47
/// one of its ancestors to be a [Material] widget. When the [TextField] is
/// tapped an ink splash that paints on the material is triggered, see
/// [ThemeData.splashFactory].
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
///
/// To integrate the [TextField] into a [Form] with other [FormField] widgets,
/// consider using [TextFormField].
///
/// See also:
///
///  * <https://material.google.com/components/text-fields.html>
///  * [TextFormField], which integrates with the [Form] widget.
///  * [InputDecorator], which shows the labels and other visual elements that
///    surround the actual text editing widget.
///  * [EditableText], which is the raw text editing control at the heart of a
///    [TextField]. (The [EditableText] widget is rarely used directly unless
///    you are implementing an entirely different design language, such as
///    Cupertino.)
class TextField extends StatefulWidget {
  /// Creates a Material Design text field.
  ///
  /// If [decoration] is non-null (which is the default), the text field requires
  /// one of its ancestors to be a [Material] widget.
  ///
  /// To remove the decoration entirely (including the extra padding introduced
  /// by the decoration to save space for the labels), set the [decoration] to
  /// null.
71 72
  ///
  /// The [maxLines] property can be set to null to remove the restriction on
73 74
  /// the number of lines. By default, it is one, meaning this is a single-line
  /// text field. [maxLines] must not be zero. If [maxLines] is not one, then
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
  /// [keyboardType] is ignored, and the [TextInputType.multiline] keyboard
  /// type is used.
  ///
  /// The [maxLength] property is set to null by default, which means the
  /// number of characters allowed in the text field is not restricted. If
  /// [maxLength] is set, a character counter will be displayed below the
  /// field, showing how many characters have been entered and how many are
  /// allowed. After [maxLength] characters have been input, additional input
  /// is ignored, unless [maxLengthEnforced] is set to false. The TextField
  /// enforces the length with a [LengthLimitingTextInputFormatter], which is
  /// evaluated after the supplied [inputFormatters], if any. The [maxLength]
  /// value must be either null or greater than zero.
  ///
  /// If [maxLengthEnforced] is set to false, then more than [maxLength]
  /// characters may be entered, and the error counter and divider will
  /// switch to the [decoration.errorStyle] when the limit is exceeded.
91
  ///
Ian Hickson's avatar
Ian Hickson committed
92 93
  /// The [keyboardType], [textAlign], [autofocus], [obscureText], and
  /// [autocorrect] arguments must not be null.
94 95 96 97 98
  ///
  /// See also:
  ///
  ///  * [maxLength], which discusses the precise meaning of "number of
  ///    characters" and how it may differ from the intuitive meaning.
99
  const TextField({
100 101 102
    Key key,
    this.controller,
    this.focusNode,
103 104
    this.decoration = const InputDecoration(),
    TextInputType keyboardType = TextInputType.text,
105
    this.textInputAction = TextInputAction.done,
106
    this.textCapitalization = TextCapitalization.none,
107
    this.style,
108 109 110 111 112
    this.textAlign = TextAlign.start,
    this.autofocus = false,
    this.obscureText = false,
    this.autocorrect = true,
    this.maxLines = 1,
113
    this.maxLength,
114
    this.maxLengthEnforced = true,
115
    this.onChanged,
116
    this.onEditingComplete,
117
    this.onSubmitted,
118
    this.inputFormatters,
119
    this.enabled,
120 121 122
    this.cursorWidth = 2.0,
    this.cursorRadius,
    this.cursorColor,
123
    this.keyboardAppearance,
124
    this.scrollPadding = const EdgeInsets.all(20.0),
125
  }) : assert(keyboardType != null),
126
       assert(textInputAction != null),
Ian Hickson's avatar
Ian Hickson committed
127
       assert(textAlign != null),
128 129
       assert(autofocus != null),
       assert(obscureText != null),
130
       assert(autocorrect != null),
131
       assert(maxLengthEnforced != null),
132
       assert(scrollPadding != null),
133
       assert(maxLines == null || maxLines > 0),
134
       assert(maxLength == null || maxLength > 0),
135
       keyboardType = maxLines == 1 ? keyboardType : TextInputType.multiline,
136
       super(key: key);
137 138 139

  /// Controls the text being edited.
  ///
140
  /// If null, this widget will create its own [TextEditingController].
141 142 143 144 145 146 147 148 149
  final TextEditingController controller;

  /// Controls whether this widget has keyboard focus.
  ///
  /// If null, this widget will create its own [FocusNode].
  final FocusNode focusNode;

  /// The decoration to show around the text field.
  ///
150
  /// By default, draws a horizontal line under the text field but can be
151 152
  /// configured to show an icon, label, hint text, and error text.
  ///
153
  /// Specify null to remove the decoration entirely (including the
154 155 156 157
  /// extra padding introduced by the decoration to save space for the labels).
  final InputDecoration decoration;

  /// The type of keyboard to use for editing the text.
158
  ///
159 160 161
  /// Defaults to [TextInputType.text]. Must not be null. If
  /// [maxLines] is not one, then [keyboardType] is ignored, and the
  /// [TextInputType.multiline] keyboard type is used.
162 163
  final TextInputType keyboardType;

164 165 166 167 168
  /// The type of action button to use for the keyboard.
  ///
  /// Defaults to [TextInputAction.done]. Must not be null.
  final TextInputAction textInputAction;

169
  /// Configures how the platform keyboard will select an uppercase or
170 171 172 173 174 175
  /// 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.
176
  ///
177
  /// See also:
178
  ///
179 180 181
  ///   * [TextCapitalization], for a description of each capitalization behavior.
  final TextCapitalization textCapitalization;

182 183 184 185
  /// The style to use for the text being edited.
  ///
  /// This text style is also used as the base style for the [decoration].
  ///
186
  /// If null, defaults to the `subhead` text style from the current [Theme].
187 188
  final TextStyle style;

189
  /// How the text being edited should be aligned horizontally.
Ian Hickson's avatar
Ian Hickson committed
190 191
  ///
  /// Defaults to [TextAlign.start].
192 193 194
  final TextAlign textAlign;

  /// Whether this text field should focus itself if nothing else is already
195 196
  /// focused.
  ///
197
  /// If true, the keyboard will open as soon as this text field obtains focus.
198 199
  /// Otherwise, the keyboard is only shown after the user taps the text field.
  ///
200
  /// Defaults to false. Cannot be null.
201 202 203 204 205 206
  // See https://github.com/flutter/flutter/issues/7035 for the rationale for this
  // keyboard behavior.
  final bool autofocus;

  /// Whether to hide the text being edited (e.g., for passwords).
  ///
207 208
  /// When this is set to true, all the characters in the text field are
  /// replaced by U+2022 BULLET characters (•).
209
  ///
210
  /// Defaults to false. Cannot be null.
211 212
  final bool obscureText;

213 214 215 216 217
  /// Whether to enable autocorrection.
  ///
  /// Defaults to true. Cannot be null.
  final bool autocorrect;

218 219 220 221
  /// 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.
222 223 224
  ///
  /// 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.
225 226
  final int maxLines;

227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246
  /// The maximum number of characters (Unicode scalar values) to allow in the
  /// text field.
  ///
  /// If set, a character counter will be displayed below the
  /// field, showing how many characters have been entered and how many are
  /// allowed. After [maxLength] characters have been input, additional input
  /// is ignored, unless [maxLengthEnforced] is set to false. The TextField
  /// enforces the length with a [LengthLimitingTextInputFormatter], which is
  /// evaluated after the supplied [inputFormatters], if any.
  ///
  /// This value must be either null or greater than zero. If set to null
  /// (the default), there is no limit to the number of characters allowed.
  ///
  /// Whitespace characters (e.g. newline, space, tab) are included in the
  /// character count.
  ///
  /// If [maxLengthEnforced] is set to false, then more than [maxLength]
  /// characters may be entered, but the error counter and divider will
  /// switch to the [decoration.errorStyle] when the limit is exceeded.
  ///
247 248
  /// ## Limitations
  ///
249 250 251 252 253 254 255 256 257 258
  /// The TextField does not currently count Unicode grapheme clusters (i.e.
  /// characters visible to the user), it counts Unicode scalar values, which
  /// leaves out a number of useful possible characters (like many emoji and
  /// composed characters), so this will be inaccurate in the presence of those
  /// characters. If you expect to encounter these kinds of characters, be
  /// generous in the maxLength used.
  ///
  /// For instance, the character "ö" can be represented as '\u{006F}\u{0308}',
  /// which is the letter "o" followed by a composed diaeresis "¨", or it can
  /// be represented as '\u{00F6}', which is the Unicode scalar value "LATIN
259
  /// SMALL LETTER O WITH DIAERESIS". In the first case, the text field will
260 261 262 263 264 265 266 267 268 269
  /// count two characters, and the second case will be counted as one
  /// character, even though the user can see no difference in the input.
  ///
  /// Similarly, some emoji are represented by multiple scalar values. The
  /// Unicode "THUMBS UP SIGN + MEDIUM SKIN TONE MODIFIER", "👍🏽", should be
  /// counted as a single character, but because it is a combination of two
  /// Unicode scalar values, '\u{1F44D}\u{1F3FD}', it is counted as two
  /// characters.
  ///
  /// See also:
270
  ///
271 272 273 274 275 276 277 278 279 280 281 282
  ///  * [LengthLimitingTextInputFormatter] for more information on how it
  ///    counts characters, and how it may differ from the intuitive meaning.
  final int maxLength;

  /// If true, prevents the field from allowing more than [maxLength]
  /// characters.
  ///
  /// If [maxLength] is set, [maxLengthEnforced] indicates whether or not to
  /// enforce the limit, or merely provide a character counter and warning when
  /// [maxLength] is exceeded.
  final bool maxLengthEnforced;

283 284 285
  /// Called when the text being edited changes.
  final ValueChanged<String> onChanged;

286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303
  /// 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.
  final VoidCallback onEditingComplete;

304 305 306 307
  /// Called when the user indicates that they are done editing the text in the
  /// field.
  final ValueChanged<String> onSubmitted;

xster's avatar
xster committed
308 309 310
  /// Optional input validation and formatting overrides.
  ///
  /// Formatters are run in the provided order when the text input changes.
311 312
  final List<TextInputFormatter> inputFormatters;

313 314 315 316 317 318 319
  /// If false the textfield is "disabled": it ignores taps and its
  /// [decoration] is rendered in grey.
  ///
  /// If non-null this property overrides the [decoration]'s
  /// [Decoration.enabled] property.
  final bool enabled;

320 321 322 323 324 325 326 327 328 329 330 331
  /// How thick the cursor will be.
  ///
  /// Defaults to 2.0.
  final double cursorWidth;

  /// How rounded the corners of the cursor should be.
  /// By default, the cursor has a null Radius
  final Radius cursorRadius;

  /// The color to use when painting the cursor.
  final Color cursorColor;

332
  /// The appearance of the keyboard.
333
  ///
334
  /// This setting is only honored on iOS devices.
335
  ///
336 337 338
  /// If unset, defaults to the brightness of [ThemeData.primaryColorBrightness].
  final Brightness keyboardAppearance;

339 340 341 342 343 344 345 346 347 348
  /// 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 EdgeInserts.all(20.0).
  final EdgeInsets scrollPadding;

349 350 351 352
  @override
  _TextFieldState createState() => new _TextFieldState();

  @override
353 354 355 356 357
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(new DiagnosticsProperty<TextEditingController>('controller', controller, defaultValue: null));
    properties.add(new DiagnosticsProperty<FocusNode>('focusNode', focusNode, defaultValue: null));
    properties.add(new DiagnosticsProperty<InputDecoration>('decoration', decoration));
358
    properties.add(new DiagnosticsProperty<TextInputType>('keyboardType', keyboardType, defaultValue: TextInputType.text));
359 360 361 362 363 364 365
    properties.add(new DiagnosticsProperty<TextStyle>('style', style, defaultValue: null));
    properties.add(new DiagnosticsProperty<bool>('autofocus', autofocus, defaultValue: false));
    properties.add(new DiagnosticsProperty<bool>('obscureText', obscureText, defaultValue: false));
    properties.add(new DiagnosticsProperty<bool>('autocorrect', autocorrect, defaultValue: false));
    properties.add(new IntProperty('maxLines', maxLines, defaultValue: 1));
    properties.add(new IntProperty('maxLength', maxLength, defaultValue: null));
    properties.add(new FlagProperty('maxLengthEnforced', value: maxLengthEnforced, ifTrue: 'max length enforced'));
366 367 368
  }
}

369
class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixin {
370 371
  final GlobalKey<EditableTextState> _editableTextKey = new GlobalKey<EditableTextState>();

372 373 374
  Set<InteractiveInkFeature> _splashes;
  InteractiveInkFeature _currentSplash;

375
  TextEditingController _controller;
376
  TextEditingController get _effectiveController => widget.controller ?? _controller;
377 378

  FocusNode _focusNode;
379
  FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= new FocusNode());
380

381 382 383 384 385
  bool get needsCounter => widget.maxLength != null
    && widget.decoration != null
    && widget.decoration.counterText == null;

  InputDecoration _getEffectiveDecoration() {
386
    final InputDecoration effectiveDecoration = (widget.decoration ?? const InputDecoration())
387 388 389 390
      .applyDefaults(Theme.of(context).inputDecorationTheme)
      .copyWith(
        enabled: widget.enabled,
      );
391

392
    if (!needsCounter)
393
      return effectiveDecoration;
394

395
    final String counterText = '${_effectiveController.value.text.runes.length}/${widget.maxLength}';
396 397 398 399 400 401 402 403 404 405 406 407
    if (_effectiveController.value.text.runes.length > widget.maxLength) {
      final ThemeData themeData = Theme.of(context);
      return effectiveDecoration.copyWith(
        errorText: effectiveDecoration.errorText ?? '',
        counterStyle: effectiveDecoration.errorStyle
          ?? themeData.textTheme.caption.copyWith(color: themeData.errorColor),
        counterText: counterText,
      );
    }
    return effectiveDecoration.copyWith(counterText: counterText);
  }

408 409 410
  @override
  void initState() {
    super.initState();
411
    if (widget.controller == null)
412 413 414 415
      _controller = new TextEditingController();
  }

  @override
416
  void didUpdateWidget(TextField oldWidget) {
417
    super.didUpdateWidget(oldWidget);
418
    if (widget.controller == null && oldWidget.controller != null)
419
      _controller = new TextEditingController.fromValue(oldWidget.controller.value);
420
    else if (widget.controller != null && oldWidget.controller == null)
421
      _controller = null;
422 423 424 425 426
    final bool isEnabled = widget.enabled ?? widget.decoration?.enabled ?? true;
    final bool wasEnabled = oldWidget.enabled ?? oldWidget.decoration?.enabled ?? true;
    if (wasEnabled && !isEnabled) {
      _effectiveFocusNode.unfocus();
    }
427 428 429 430 431 432 433 434 435 436 437 438
  }

  @override
  void dispose() {
    _focusNode?.dispose();
    super.dispose();
  }

  void _requestKeyboard() {
    _editableTextKey.currentState?.requestKeyboard();
  }

439
  void _handleSelectionChanged(TextSelection selection, SelectionChangedCause cause) {
440
    if (cause == SelectionChangedCause.longPress)
441 442 443
      Feedback.forLongPress(context);
  }

444 445
  InteractiveInkFeature _createInkFeature(TapDownDetails details) {
    final MaterialInkController inkController = Material.of(context);
Hans Muller's avatar
Hans Muller committed
446 447
    final BuildContext editableContext = _editableTextKey.currentContext;
    final RenderBox referenceBox = InputDecorator.containerOf(editableContext) ?? editableContext.findRenderObject();
448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532
    final Offset position = referenceBox.globalToLocal(details.globalPosition);
    final Color color = Theme.of(context).splashColor;

    InteractiveInkFeature splash;
    void handleRemoved() {
      if (_splashes != null) {
        assert(_splashes.contains(splash));
        _splashes.remove(splash);
        if (_currentSplash == splash)
          _currentSplash = null;
        updateKeepAlive();
      } // else we're probably in deactivate()
    }

    splash = Theme.of(context).splashFactory.create(
      controller: inkController,
      referenceBox: referenceBox,
      position: position,
      color: color,
      containedInkWell: true,
      // TODO(hansmuller): splash clip borderRadius should match the input decorator's border.
      borderRadius: BorderRadius.zero,
      onRemoved: handleRemoved,
    );

    return splash;
  }

  RenderEditable get _renderEditable => _editableTextKey.currentState.renderEditable;

  void _handleTapDown(TapDownDetails details) {
    _renderEditable.handleTapDown(details);
    _startSplash(details);
  }

  void _handleTap() {
    _renderEditable.handleTap();
    _requestKeyboard();
    _confirmCurrentSplash();
  }

  void _handleTapCancel() {
    _cancelCurrentSplash();
  }

  void _handleLongPress() {
    _renderEditable.handleLongPress();
    _confirmCurrentSplash();
  }

  void _startSplash(TapDownDetails details) {
    if (_effectiveFocusNode.hasFocus)
      return;
    final InteractiveInkFeature splash = _createInkFeature(details);
    _splashes ??= new HashSet<InteractiveInkFeature>();
    _splashes.add(splash);
    _currentSplash = splash;
    updateKeepAlive();
  }

  void _confirmCurrentSplash() {
    _currentSplash?.confirm();
    _currentSplash = null;
  }

  void _cancelCurrentSplash() {
    _currentSplash?.cancel();
  }

  @override
  bool get wantKeepAlive => _splashes != null && _splashes.isNotEmpty;

  @override
  void deactivate() {
    if (_splashes != null) {
      final Set<InteractiveInkFeature> splashes = _splashes;
      _splashes = null;
      for (InteractiveInkFeature splash in splashes)
        splash.dispose();
      _currentSplash = null;
    }
    assert(_currentSplash == null);
    super.deactivate();
  }

533 534
  @override
  Widget build(BuildContext context) {
535
    super.build(context); // See AutomaticKeepAliveClientMixin.
536
    assert(debugCheckHasMaterial(context));
537
    final ThemeData themeData = Theme.of(context);
538
    final TextStyle style = widget.style ?? themeData.textTheme.subhead;
539
    final Brightness keyboardAppearance = widget.keyboardAppearance ?? themeData.primaryColorBrightness;
540 541
    final TextEditingController controller = _effectiveController;
    final FocusNode focusNode = _effectiveFocusNode;
542 543 544
    final List<TextInputFormatter> formatters = widget.inputFormatters ?? <TextInputFormatter>[];
    if (widget.maxLength != null && widget.maxLengthEnforced)
      formatters.add(new LengthLimitingTextInputFormatter(widget.maxLength));
545 546 547 548 549 550

    Widget child = new RepaintBoundary(
      child: new EditableText(
        key: _editableTextKey,
        controller: controller,
        focusNode: focusNode,
551
        keyboardType: widget.keyboardType,
552
        textInputAction: widget.textInputAction,
553
        textCapitalization: widget.textCapitalization,
554
        style: style,
555 556 557
        textAlign: widget.textAlign,
        autofocus: widget.autofocus,
        obscureText: widget.obscureText,
558
        autocorrect: widget.autocorrect,
559
        maxLines: widget.maxLines,
560
        selectionColor: themeData.textSelectionColor,
xster's avatar
xster committed
561 562 563
        selectionControls: themeData.platform == TargetPlatform.iOS
            ? cupertinoTextSelectionControls
            : materialTextSelectionControls,
564
        onChanged: widget.onChanged,
565
        onEditingComplete: widget.onEditingComplete,
566
        onSubmitted: widget.onSubmitted,
567
        onSelectionChanged: _handleSelectionChanged,
568
        inputFormatters: formatters,
569
        rendererIgnoresPointer: true,
570 571 572
        cursorWidth: widget.cursorWidth,
        cursorRadius: widget.cursorRadius,
        cursorColor: widget.cursorColor ?? Theme.of(context).cursorColor,
573
        scrollPadding: widget.scrollPadding,
574
        keyboardAppearance: keyboardAppearance,
575 576 577
      ),
    );

578
    if (widget.decoration != null) {
579 580 581 582
      child = new AnimatedBuilder(
        animation: new Listenable.merge(<Listenable>[ focusNode, controller ]),
        builder: (BuildContext context, Widget child) {
          return new InputDecorator(
583
            decoration: _getEffectiveDecoration(),
584 585
            baseStyle: widget.style,
            textAlign: widget.textAlign,
586 587 588 589 590 591 592 593 594
            isFocused: focusNode.hasFocus,
            isEmpty: controller.value.text.isEmpty,
            child: child,
          );
        },
        child: child,
      );
    }

595 596
    return new Semantics(
      onTap: () {
597 598
        if (!_effectiveController.selection.isValid)
          _effectiveController.selection = new TextSelection.collapsed(offset: _effectiveController.text.length);
599 600
        _requestKeyboard();
      },
601 602 603 604 605 606 607 608 609 610 611
      child: new IgnorePointer(
        ignoring: !(widget.enabled ?? widget.decoration?.enabled ?? true),
        child: new GestureDetector(
          behavior: HitTestBehavior.translucent,
          onTapDown: _handleTapDown,
          onTap: _handleTap,
          onTapCancel: _handleTapCancel,
          onLongPress: _handleLongPress,
          excludeFromSemantics: true,
          child: child,
        ),
612
      ),
613 614
    );
  }
615
}