text_field.dart 41 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

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

12
import 'debug.dart';
13
import 'feedback.dart';
14
import 'input_decorator.dart';
15
import 'material.dart';
16
import 'material_localizations.dart';
17
import 'selectable_text.dart' show iOSHorizontalOffset;
18 19 20
import 'text_selection.dart';
import 'theme.dart';

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

23 24 25
/// Signature for the [TextField.buildCounter] callback.
typedef InputCounterWidgetBuilder = Widget Function(
  /// The build context for the TextField
26 27 28 29 30 31 32 33 34
  BuildContext context, {
  /// The length of the string currently in the input.
  @required int currentLength,
  /// The maximum string length that can be entered into the TextField.
  @required int maxLength,
  /// Whether or not the TextField is currently focused.  Mainly provided for
  /// the [liveRegion] parameter in the [Semantics] widget for accessibility.
  @required bool isFocused,
});
35

36 37
class _TextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDetectorBuilder {
  _TextFieldSelectionGestureDetectorBuilder({
38
    @required _TextFieldState state,
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
  }) : _state = state,
       super(delegate: state);

  final _TextFieldState _state;

  @override
  void onForcePressStart(ForcePressDetails details) {
    super.onForcePressStart(details);
    if (delegate.selectionEnabled && shouldShowSelectionToolbar) {
      editableText.showToolbar();
    }
  }

  @override
  void onForcePressEnd(ForcePressDetails details) {
    // Not required.
  }

  @override
  void onSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) {
    if (delegate.selectionEnabled) {
      switch (Theme.of(_state.context).platform) {
        case TargetPlatform.iOS:
62
        case TargetPlatform.macOS:
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
          renderEditable.selectPositionAt(
            from: details.globalPosition,
            cause: SelectionChangedCause.longPress,
          );
          break;
        case TargetPlatform.android:
        case TargetPlatform.fuchsia:
          renderEditable.selectWordsInRange(
            from: details.globalPosition - details.offsetFromOrigin,
            to: details.globalPosition,
            cause: SelectionChangedCause.longPress,
          );
          break;
      }
    }
  }

  @override
  void onSingleTapUp(TapUpDetails details) {
    editableText.hideToolbar();
    if (delegate.selectionEnabled) {
      switch (Theme.of(_state.context).platform) {
        case TargetPlatform.iOS:
86
        case TargetPlatform.macOS:
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104
          renderEditable.selectWordEdge(cause: SelectionChangedCause.tap);
          break;
        case TargetPlatform.android:
        case TargetPlatform.fuchsia:
          renderEditable.selectPosition(cause: SelectionChangedCause.tap);
          break;
      }
    }
    _state._requestKeyboard();
    if (_state.widget.onTap != null)
      _state.widget.onTap();
  }

  @override
  void onSingleLongTapStart(LongPressStartDetails details) {
    if (delegate.selectionEnabled) {
      switch (Theme.of(_state.context).platform) {
        case TargetPlatform.iOS:
105
        case TargetPlatform.macOS:
106 107 108 109 110 111 112
          renderEditable.selectPositionAt(
            from: details.globalPosition,
            cause: SelectionChangedCause.longPress,
          );
          break;
        case TargetPlatform.android:
        case TargetPlatform.fuchsia:
113
          renderEditable.selectWord(cause: SelectionChangedCause.longPress);
114 115 116 117 118 119 120
          Feedback.forLongPress(_state.context);
          break;
      }
    }
  }
}

121
/// A material design text field.
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143
///
/// 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
144
/// one of its ancestors to be a [Material] widget.
145 146 147 148
///
/// To integrate the [TextField] into a [Form] with other [FormField] widgets,
/// consider using [TextFormField].
///
149 150 151
/// Remember to [dispose] of the [TextEditingController] when it is no longer needed.
/// This will ensure we discard any resources used by the object.
///
152
/// {@tool snippet}
153 154 155 156
/// This example shows how to create a [TextField] that will obscure input. The
/// [InputDecoration] surrounds the field in a border using [OutlineInputBorder]
/// and adds a label.
///
157
/// ![](https://flutter.github.io/assets-for-api-docs/assets/material/text_field.png)
158
///
159 160 161 162 163 164 165 166 167 168 169
/// ```dart
/// TextField(
///   obscureText: true,
///   decoration: InputDecoration(
///     border: OutlineInputBorder(),
///     labelText: 'Password',
///   ),
/// )
/// ```
/// {@end-tool}
///
170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234
/// ## Reading values
///
/// A common way to read a value from a TextField is to use the [onSubmitted]
/// callback. This callback is applied to the text field's current value when
/// the user finishes editing.
///
/// {@tool dartpad --template=stateful_widget_material}
///
/// This sample shows how to get a value from a TextField via the [onSubmitted]
/// callback.
///
/// ```dart
/// TextEditingController _controller;
///
/// void initState() {
///   super.initState();
///   _controller = TextEditingController();
/// }
///
/// void dispose() {
///   _controller.dispose();
///   super.dispose();
/// }
///
/// Widget build(BuildContext context) {
///   return Scaffold(
///     body: Center(
///       child: TextField(
///         controller: _controller,
///         onSubmitted: (String value) async {
///           await showDialog<void>(
///             context: context,
///             builder: (BuildContext context) {
///               return AlertDialog(
///                 title: const Text('Thanks!'),
///                 content: Text ('You typed "$value".'),
///                 actions: <Widget>[
///                   FlatButton(
///                     onPressed: () { Navigator.pop(context); },
///                     child: const Text('OK'),
///                   ),
///                 ],
///               );
///             },
///           );
///         },
///       ),
///     ),
///   );
/// }
/// ```
/// {@end-tool}
///
/// For most applications the [onSubmitted] callback will be sufficient for
/// reacting to user input.
///
/// The [onEditingComplete] callback also runs when the user finishes editing.
/// It's different from [onSubmitted] because it has a default value which
/// updates the text controller and yields the keyboard focus. Applications that
/// require different behavior can override the default [onEditingComplete]
/// callback.
///
/// Keep in mind you can also always read the current string from a TextField's
/// [TextEditingController] using [TextEditingController.text].
///
235 236
/// See also:
///
237
///  * <https://material.io/design/components/text-fields.html>
238 239 240 241
///  * [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
242
///    [TextField]. The [EditableText] widget is rarely used directly unless
243
///    you are implementing an entirely different design language, such as
244
///    Cupertino.
245 246
///  * Learn how to use a [TextEditingController] in one of our
///    [cookbook recipe](https://flutter.dev/docs/cookbook/forms/text-field-changes#2-use-a-texteditingcontroller)s.
247 248 249 250 251 252 253 254 255
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.
256 257
  ///
  /// The [maxLines] property can be set to null to remove the restriction on
258
  /// the number of lines. By default, it is one, meaning this is a single-line
259
  /// text field. [maxLines] must not be zero.
260 261 262
  ///
  /// The [maxLength] property is set to null by default, which means the
  /// number of characters allowed in the text field is not restricted. If
263 264 265 266 267
  /// [maxLength] is set a character counter will be displayed below the
  /// field showing how many characters have been entered. If the value is
  /// set to a positive integer it will also display the maximum allowed
  /// number of characters to be entered.  If the value is set to
  /// [TextField.noMaxLength] then only the current length is displayed.
268 269
  ///
  /// After [maxLength] characters have been input, additional input
270
  /// is ignored, unless [maxLengthEnforced] is set to false. The text field
271 272 273 274 275 276 277
  /// 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.
278
  ///
279 280 281 282
  /// The text cursor is not shown if [showCursor] is false or if [showCursor]
  /// is null (the default) and [readOnly] is true.
  ///
  /// The [textAlign], [autofocus], [obscureText], [readOnly], [autocorrect],
283 284
  /// [maxLengthEnforced], [scrollPadding], [maxLines], [maxLength],
  /// and [enableSuggestions] arguments must not be null.
285 286 287 288 289
  ///
  /// See also:
  ///
  ///  * [maxLength], which discusses the precise meaning of "number of
  ///    characters" and how it may differ from the intuitive meaning.
290
  const TextField({
291 292 293
    Key key,
    this.controller,
    this.focusNode,
294
    this.decoration = const InputDecoration(),
295 296
    TextInputType keyboardType,
    this.textInputAction,
297
    this.textCapitalization = TextCapitalization.none,
298
    this.style,
299
    this.strutStyle,
300
    this.textAlign = TextAlign.start,
301
    this.textAlignVertical,
302
    this.textDirection,
303
    this.readOnly = false,
304
    ToolbarOptions toolbarOptions,
305
    this.showCursor,
306 307 308
    this.autofocus = false,
    this.obscureText = false,
    this.autocorrect = true,
309 310
    SmartDashesType smartDashesType,
    SmartQuotesType smartQuotesType,
311
    this.enableSuggestions = true,
312
    this.maxLines = 1,
313 314
    this.minLines,
    this.expands = false,
315
    this.maxLength,
316
    this.maxLengthEnforced = true,
317
    this.onChanged,
318
    this.onEditingComplete,
319
    this.onSubmitted,
320
    this.inputFormatters,
321
    this.enabled,
322 323 324
    this.cursorWidth = 2.0,
    this.cursorRadius,
    this.cursorColor,
325
    this.keyboardAppearance,
326
    this.scrollPadding = const EdgeInsets.all(20.0),
327
    this.dragStartBehavior = DragStartBehavior.start,
328
    this.enableInteractiveSelection = true,
329
    this.onTap,
330
    this.buildCounter,
331
    this.scrollController,
332
    this.scrollPhysics,
333
  }) : assert(textAlign != null),
334
       assert(readOnly != null),
335 336
       assert(autofocus != null),
       assert(obscureText != null),
337
       assert(autocorrect != null),
338 339
       smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
       smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
340
       assert(enableSuggestions != null),
341
       assert(enableInteractiveSelection != null),
342
       assert(maxLengthEnforced != null),
343
       assert(scrollPadding != null),
344
       assert(dragStartBehavior != null),
345
       assert(maxLines == null || maxLines > 0),
346 347 348 349 350 351 352 353 354 355
       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.',
       ),
356
       assert(!obscureText || maxLines == 1, 'Obscured fields cannot be multiline.'),
357
       assert(maxLength == null || maxLength == TextField.noMaxLength || maxLength > 0),
358
       keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
359
       toolbarOptions = toolbarOptions ?? (obscureText ?
360 361 362 363 364 365 366 367 368
         const ToolbarOptions(
           selectAll: true,
           paste: true,
         ) :
         const ToolbarOptions(
           copy: true,
           cut: true,
           selectAll: true,
           paste: true,
369
         )),
370
       super(key: key);
371 372 373

  /// Controls the text being edited.
  ///
374
  /// If null, this widget will create its own [TextEditingController].
375 376
  final TextEditingController controller;

377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396
  /// Defines the keyboard focus for this widget.
  ///
  /// The [focusNode] is a long-lived object that's typically managed by a
  /// [StatefulWidget] parent. See [FocusNode] for more information.
  ///
  /// To give the keyboard focus to this widget, provide a [focusNode] and then
  /// use the current [FocusScope] to request the focus:
  ///
  /// ```dart
  /// FocusScope.of(context).requestFocus(myFocusNode);
  /// ```
  ///
  /// This happens automatically when the widget is tapped.
  ///
  /// To be notified when the widget gains or loses the focus, add a listener
  /// to the [focusNode]:
  ///
  /// ```dart
  /// focusNode.addListener(() { print(myFocusNode.hasFocus); });
  /// ```
397 398
  ///
  /// If null, this widget will create its own [FocusNode].
399 400 401
  ///
  /// ## Keyboard
  ///
Gary Qian's avatar
Gary Qian committed
402
  /// Requesting the focus will typically cause the keyboard to be shown
403 404
  /// if it's not showing already.
  ///
405
  /// On Android, the user can hide the keyboard - without changing the focus -
406 407 408 409 410 411 412 413 414
  /// with the system back button. They can restore the keyboard's visibility
  /// by tapping on a text field.  The user might hide the keyboard and
  /// switch to a physical keyboard, or they might just need to get it
  /// out of the way for a moment, to expose something it's
  /// obscuring. In this case requesting the focus again will not
  /// cause the focus to change, and will not make the keyboard visible.
  ///
  /// This widget builds an [EditableText] and will ensure that the keyboard is
  /// showing when it is tapped by calling [EditableTextState.requestKeyboard()].
415 416 417 418
  final FocusNode focusNode;

  /// The decoration to show around the text field.
  ///
419
  /// By default, draws a horizontal line under the text field but can be
420 421
  /// configured to show an icon, label, hint text, and error text.
  ///
422
  /// Specify null to remove the decoration entirely (including the
423 424 425
  /// extra padding introduced by the decoration to save space for the labels).
  final InputDecoration decoration;

426
  /// {@macro flutter.widgets.editableText.keyboardType}
427 428
  final TextInputType keyboardType;

429 430
  /// The type of action button to use for the keyboard.
  ///
431 432
  /// Defaults to [TextInputAction.newline] if [keyboardType] is
  /// [TextInputType.multiline] and [TextInputAction.done] otherwise.
433 434
  final TextInputAction textInputAction;

435
  /// {@macro flutter.widgets.editableText.textCapitalization}
436 437
  final TextCapitalization textCapitalization;

438 439 440 441
  /// The style to use for the text being edited.
  ///
  /// This text style is also used as the base style for the [decoration].
  ///
442
  /// If null, defaults to the `subhead` text style from the current [Theme].
443 444
  final TextStyle style;

445 446 447
  /// {@macro flutter.widgets.editableText.strutStyle}
  final StrutStyle strutStyle;

448
  /// {@macro flutter.widgets.editableText.textAlign}
449 450
  final TextAlign textAlign;

Dan Field's avatar
Dan Field committed
451
  /// {@macro flutter.widgets.inputDecorator.textAlignVertical}
452 453
  final TextAlignVertical textAlignVertical;

454 455 456
  /// {@macro flutter.widgets.editableText.textDirection}
  final TextDirection textDirection;

457
  /// {@macro flutter.widgets.editableText.autofocus}
458 459
  final bool autofocus;

460
  /// {@macro flutter.widgets.editableText.obscureText}
461 462
  final bool obscureText;

463
  /// {@macro flutter.widgets.editableText.autocorrect}
464 465
  final bool autocorrect;

466 467 468 469 470 471
  /// {@macro flutter.services.textInput.smartDashesType}
  final SmartDashesType smartDashesType;

  /// {@macro flutter.services.textInput.smartQuotesType}
  final SmartQuotesType smartQuotesType;

472 473 474
  /// {@macro flutter.services.textInput.enableSuggestions}
  final bool enableSuggestions;

475
  /// {@macro flutter.widgets.editableText.maxLines}
476 477
  final int maxLines;

478 479 480 481 482 483
  /// {@macro flutter.widgets.editableText.minLines}
  final int minLines;

  /// {@macro flutter.widgets.editableText.expands}
  final bool expands;

484 485 486
  /// {@macro flutter.widgets.editableText.readOnly}
  final bool readOnly;

487 488 489 490 491 492 493
  /// Configuration of toolbar options.
  ///
  /// If not set, select all and paste will default to be enabled. Copy and cut
  /// will be disabled if [obscureText] is true. If [readOnly] is true,
  /// paste and cut will be disabled regardless.
  final ToolbarOptions toolbarOptions;

494 495 496
  /// {@macro flutter.widgets.editableText.showCursor}
  final bool showCursor;

497 498
  /// If [maxLength] is set to this value, only the "current input length"
  /// part of the character counter is shown.
499
  static const int noMaxLength = -1;
500

501 502 503 504
  /// The maximum number of characters (Unicode scalar values) to allow in the
  /// text field.
  ///
  /// If set, a character counter will be displayed below the
505
  /// field showing how many characters have been entered. If set to a number
506
  /// greater than 0, it will also display the maximum number allowed. If set
507 508 509
  /// to [TextField.noMaxLength] then only the current character count is displayed.
  ///
  /// After [maxLength] characters have been input, additional input
510
  /// is ignored, unless [maxLengthEnforced] is set to false. The text field
511 512 513
  /// enforces the length with a [LengthLimitingTextInputFormatter], which is
  /// evaluated after the supplied [inputFormatters], if any.
  ///
514 515 516 517
  /// This value must be either null, [TextField.noMaxLength], or greater than 0.
  /// If null (the default) then there is no limit to the number of characters
  /// that can be entered. If set to [TextField.noMaxLength], then no limit will
  /// be enforced, but the number of characters entered will still be displayed.
518 519 520 521 522 523 524 525
  ///
  /// 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.
  ///
526 527
  /// ## Limitations
  ///
528
  /// The text field does not currently count Unicode grapheme clusters (i.e.
529 530 531 532 533 534 535 536 537
  /// 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
538
  /// SMALL LETTER O WITH DIAERESIS". In the first case, the text field will
539 540 541 542 543 544 545 546 547 548
  /// 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:
549
  ///
550 551 552 553 554 555 556 557 558 559 560 561
  ///  * [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;

562
  /// {@macro flutter.widgets.editableText.onChanged}
563 564 565 566 567 568 569
  ///
  /// 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.
570 571
  final ValueChanged<String> onChanged;

572
  /// {@macro flutter.widgets.editableText.onEditingComplete}
573 574
  final VoidCallback onEditingComplete;

575
  /// {@macro flutter.widgets.editableText.onSubmitted}
576 577 578 579 580 581
  ///
  /// See also:
  ///
  ///  * [EditableText.onSubmitted] for an example of how to handle moving to
  ///    the next/previous field when using [TextInputAction.next] and
  ///    [TextInputAction.previous] for [textInputAction].
582 583
  final ValueChanged<String> onSubmitted;

584
  /// {@macro flutter.widgets.editableText.inputFormatters}
585 586
  final List<TextInputFormatter> inputFormatters;

587
  /// If false the text field is "disabled": it ignores taps and its
588 589 590 591 592 593
  /// [decoration] is rendered in grey.
  ///
  /// If non-null this property overrides the [decoration]'s
  /// [Decoration.enabled] property.
  final bool enabled;

594
  /// {@macro flutter.widgets.editableText.cursorWidth}
595 596
  final double cursorWidth;

597
  /// {@macro flutter.widgets.editableText.cursorRadius}
598 599 600
  final Radius cursorRadius;

  /// The color to use when painting the cursor.
601
  ///
602 603
  /// Defaults to [ThemeData.cursorColor] or [CupertinoTheme.primaryColor]
  /// depending on [ThemeData.platform].
604 605
  final Color cursorColor;

606
  /// The appearance of the keyboard.
607
  ///
608
  /// This setting is only honored on iOS devices.
609
  ///
610 611 612
  /// If unset, defaults to the brightness of [ThemeData.primaryColorBrightness].
  final Brightness keyboardAppearance;

613
  /// {@macro flutter.widgets.editableText.scrollPadding}
614 615
  final EdgeInsets scrollPadding;

616 617 618
  /// {@macro flutter.widgets.editableText.enableInteractiveSelection}
  final bool enableInteractiveSelection;

619 620 621
  /// {@macro flutter.widgets.scrollable.dragStartBehavior}
  final DragStartBehavior dragStartBehavior;

622
  /// {@macro flutter.rendering.editable.selectionEnabled}
623
  bool get selectionEnabled => enableInteractiveSelection;
624

625 626
  /// {@template flutter.material.textfield.onTap}
  /// Called for each distinct tap except for every second tap of a double tap.
627
  ///
628
  /// The text field builds a [GestureDetector] to handle input events like tap,
629
  /// to trigger focus requests, to move the caret, adjust the selection, etc.
630
  /// Handling some of those events by wrapping the text field with a competing
631 632
  /// GestureDetector is problematic.
  ///
633
  /// To unconditionally handle taps, without interfering with the text field's
634 635
  /// internal gesture detector, provide this callback.
  ///
636
  /// If the text field is created with [enabled] false, taps will not be
637 638
  /// recognized.
  ///
639
  /// To be notified when the text field gains or loses the focus, provide a
640 641 642
  /// [focusNode] and add a listener to that.
  ///
  /// To listen to arbitrary pointer events without competing with the
643
  /// text field's internal gesture detector, use a [Listener].
644
  /// {@endtemplate}
645 646
  final GestureTapCallback onTap;

647 648 649 650 651 652 653 654 655 656
  /// Callback that generates a custom [InputDecorator.counter] widget.
  ///
  /// See [InputCounterWidgetBuilder] for an explanation of the passed in
  /// arguments.  The returned widget will be placed below the line in place of
  /// the default widget built when [counterText] is specified.
  ///
  /// The returned widget will be wrapped in a [Semantics] widget for
  /// accessibility, but it also needs to be accessible itself.  For example,
  /// if returning a Text widget, set the [semanticsLabel] property.
  ///
657
  /// {@tool snippet}
658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673
  /// ```dart
  /// Widget counter(
  ///   BuildContext context,
  ///   {
  ///     int currentLength,
  ///     int maxLength,
  ///     bool isFocused,
  ///   }
  /// ) {
  ///   return Text(
  ///     '$currentLength of $maxLength characters',
  ///     semanticsLabel: 'character count',
  ///   );
  /// }
  /// ```
  /// {@end-tool}
674 675 676
  ///
  /// If buildCounter returns null, then no counter and no Semantics widget will
  /// be created at all.
677 678
  final InputCounterWidgetBuilder buildCounter;

Dan Field's avatar
Dan Field committed
679
  /// {@macro flutter.widgets.editableText.scrollPhysics}
680 681
  final ScrollPhysics scrollPhysics;

682 683 684
  /// {@macro flutter.widgets.editableText.scrollController}
  final ScrollController scrollController;

685
  @override
686
  _TextFieldState createState() => _TextFieldState();
687 688

  @override
689 690
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
691 692
    properties.add(DiagnosticsProperty<TextEditingController>('controller', controller, defaultValue: null));
    properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode, defaultValue: null));
693
    properties.add(DiagnosticsProperty<bool>('enabled', enabled, defaultValue: null));
694
    properties.add(DiagnosticsProperty<InputDecoration>('decoration', decoration, defaultValue: const InputDecoration()));
695 696 697 698
    properties.add(DiagnosticsProperty<TextInputType>('keyboardType', keyboardType, defaultValue: TextInputType.text));
    properties.add(DiagnosticsProperty<TextStyle>('style', style, defaultValue: null));
    properties.add(DiagnosticsProperty<bool>('autofocus', autofocus, defaultValue: false));
    properties.add(DiagnosticsProperty<bool>('obscureText', obscureText, defaultValue: false));
699
    properties.add(DiagnosticsProperty<bool>('autocorrect', autocorrect, defaultValue: true));
700 701
    properties.add(EnumProperty<SmartDashesType>('smartDashesType', smartDashesType, defaultValue: obscureText ? SmartDashesType.disabled : SmartDashesType.enabled));
    properties.add(EnumProperty<SmartQuotesType>('smartQuotesType', smartQuotesType, defaultValue: obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled));
702
    properties.add(DiagnosticsProperty<bool>('enableSuggestions', enableSuggestions, defaultValue: true));
703
    properties.add(IntProperty('maxLines', maxLines, defaultValue: 1));
704 705
    properties.add(IntProperty('minLines', minLines, defaultValue: null));
    properties.add(DiagnosticsProperty<bool>('expands', expands, defaultValue: false));
706
    properties.add(IntProperty('maxLength', maxLength, defaultValue: null));
707 708 709 710
    properties.add(FlagProperty('maxLengthEnforced', value: maxLengthEnforced, defaultValue: true, ifFalse: 'maxLength not enforced'));
    properties.add(EnumProperty<TextInputAction>('textInputAction', textInputAction, defaultValue: null));
    properties.add(EnumProperty<TextCapitalization>('textCapitalization', textCapitalization, defaultValue: TextCapitalization.none));
    properties.add(EnumProperty<TextAlign>('textAlign', textAlign, defaultValue: TextAlign.start));
711
    properties.add(DiagnosticsProperty<TextAlignVertical>('textAlignVertical', textAlignVertical, defaultValue: null));
712 713 714
    properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
    properties.add(DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0));
    properties.add(DiagnosticsProperty<Radius>('cursorRadius', cursorRadius, defaultValue: null));
715
    properties.add(ColorProperty('cursorColor', cursorColor, defaultValue: null));
716 717 718
    properties.add(DiagnosticsProperty<Brightness>('keyboardAppearance', keyboardAppearance, defaultValue: null));
    properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('scrollPadding', scrollPadding, defaultValue: const EdgeInsets.all(20.0)));
    properties.add(FlagProperty('selectionEnabled', value: selectionEnabled, defaultValue: true, ifFalse: 'selection disabled'));
719
    properties.add(DiagnosticsProperty<ScrollController>('scrollController', scrollController, defaultValue: null));
720
    properties.add(DiagnosticsProperty<ScrollPhysics>('scrollPhysics', scrollPhysics, defaultValue: null));
721 722 723
  }
}

724
class _TextFieldState extends State<TextField> implements TextSelectionGestureDetectorBuilderDelegate {
725
  TextEditingController _controller;
726
  TextEditingController get _effectiveController => widget.controller ?? _controller;
727 728

  FocusNode _focusNode;
729
  FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode());
730

731 732
  bool _isHovering = false;

733 734 735 736
  bool get needsCounter => widget.maxLength != null
    && widget.decoration != null
    && widget.decoration.counterText == null;

737 738
  bool _showSelectionHandles = false;

739 740 741 742 743 744 745 746 747 748 749 750 751
  _TextFieldSelectionGestureDetectorBuilder _selectionGestureDetectorBuilder;

  // API for TextSelectionGestureDetectorBuilderDelegate.
  @override
  bool forcePressEnabled;

  @override
  final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>();

  @override
  bool get selectionEnabled => widget.selectionEnabled;
  // End of API for TextSelectionGestureDetectorBuilderDelegate.

752 753
  bool get _isEnabled =>  widget.enabled ?? widget.decoration?.enabled ?? true;

754 755
  int get _currentLength => _effectiveController.value.text.runes.length;

756
  InputDecoration _getEffectiveDecoration() {
757
    final MaterialLocalizations localizations = MaterialLocalizations.of(context);
758
    final ThemeData themeData = Theme.of(context);
759
    final InputDecoration effectiveDecoration = (widget.decoration ?? const InputDecoration())
760
      .applyDefaults(themeData.inputDecorationTheme)
761 762
      .copyWith(
        enabled: widget.enabled,
763
        hintMaxLines: widget.decoration?.hintMaxLines ?? widget.maxLines,
764
      );
765

766 767
    // No need to build anything if counter or counterText were given directly.
    if (effectiveDecoration.counter != null || effectiveDecoration.counterText != null)
768
      return effectiveDecoration;
769

770 771
    // If buildCounter was provided, use it to generate a counter widget.
    Widget counter;
772
    final int currentLength = _currentLength;
773 774 775 776
    if (effectiveDecoration.counter == null
        && effectiveDecoration.counterText == null
        && widget.buildCounter != null) {
      final bool isFocused = _effectiveFocusNode.hasFocus;
777 778 779 780 781
      final Widget builtCounter = widget.buildCounter(
        context,
        currentLength: currentLength,
        maxLength: widget.maxLength,
        isFocused: isFocused,
782
      );
783 784 785 786 787 788 789 790
      // If buildCounter returns null, don't add a counter widget to the field.
      if (builtCounter != null) {
        counter = Semantics(
          container: true,
          liveRegion: isFocused,
          child: builtCounter,
        );
      }
791 792 793 794 795 796
      return effectiveDecoration.copyWith(counter: counter);
    }

    if (widget.maxLength == null)
      return effectiveDecoration; // No counter widget

797 798 799
    String counterText = '$currentLength';
    String semanticCounterText = '';

800 801 802
    // Handle a real maxLength (positive number)
    if (widget.maxLength > 0) {
      // Show the maxLength in the counter
803
      counterText += '/${widget.maxLength}';
804
      final int remaining = (widget.maxLength - currentLength).clamp(0, widget.maxLength) as int;
805 806
      semanticCounterText = localizations.remainingTextFieldCharacterCount(remaining);

807 808 809 810 811 812 813 814 815 816
      // Handle length exceeds maxLength
      if (_effectiveController.value.text.runes.length > widget.maxLength) {
        return effectiveDecoration.copyWith(
          errorText: effectiveDecoration.errorText ?? '',
          counterStyle: effectiveDecoration.errorStyle
            ?? themeData.textTheme.caption.copyWith(color: themeData.errorColor),
          counterText: counterText,
          semanticCounterText: semanticCounterText,
        );
      }
817
    }
818

819 820 821 822
    return effectiveDecoration.copyWith(
      counterText: counterText,
      semanticCounterText: semanticCounterText,
    );
823 824
  }

825 826 827
  @override
  void initState() {
    super.initState();
828
    _selectionGestureDetectorBuilder = _TextFieldSelectionGestureDetectorBuilder(state: this);
829
    if (widget.controller == null) {
830
      _controller = TextEditingController();
831 832
    }
    _effectiveFocusNode.canRequestFocus = _isEnabled;
833 834 835
  }

  @override
836
  void didUpdateWidget(TextField oldWidget) {
837
    super.didUpdateWidget(oldWidget);
838
    if (widget.controller == null && oldWidget.controller != null)
839
      _controller = TextEditingController.fromValue(oldWidget.controller.value);
840
    else if (widget.controller != null && oldWidget.controller == null)
841
      _controller = null;
842
    _effectiveFocusNode.canRequestFocus = _isEnabled;
843 844 845 846 847
    if (_effectiveFocusNode.hasFocus && widget.readOnly != oldWidget.readOnly) {
      if(_effectiveController.selection.isCollapsed) {
        _showSelectionHandles = !widget.readOnly;
      }
    }
848 849 850 851 852 853 854 855
  }

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

856
  EditableTextState get _editableText => editableTextKey.currentState;
857

858
  void _requestKeyboard() {
859 860 861 862 863 864
    _editableText?.requestKeyboard();
  }

  bool _shouldShowSelectionHandles(SelectionChangedCause cause) {
    // When the text field is activated by something that doesn't trigger the
    // selection overlay, we shouldn't show the handles either.
865
    if (!_selectionGestureDetectorBuilder.shouldShowSelectionToolbar)
866 867 868 869 870
      return false;

    if (cause == SelectionChangedCause.keyboard)
      return false;

871 872 873
    if (widget.readOnly && _effectiveController.selection.isCollapsed)
      return false;

874 875 876 877 878 879 880
    if (cause == SelectionChangedCause.longPress)
      return true;

    if (_effectiveController.text.isNotEmpty)
      return true;

    return false;
881 882
  }

883
  void _handleSelectionChanged(TextSelection selection, SelectionChangedCause cause) {
884 885 886 887 888
    final bool willShowSelectionHandles = _shouldShowSelectionHandles(cause);
    if (willShowSelectionHandles != _showSelectionHandles) {
      setState(() {
        _showSelectionHandles = willShowSelectionHandles;
      });
889 890
    }

891 892
    switch (Theme.of(context).platform) {
      case TargetPlatform.iOS:
893
      case TargetPlatform.macOS:
894
        if (cause == SelectionChangedCause.longPress) {
895
          _editableText?.bringIntoView(selection.base);
896 897 898 899 900 901 902 903
        }
        return;
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
        // Do nothing.
    }
  }

904 905 906 907 908 909 910
  /// Toggle the toolbar when a selection handle is tapped.
  void _handleSelectionHandleTapped() {
    if (_effectiveController.selection.isCollapsed) {
      _editableText.toggleToolbar();
    }
  }

911 912 913 914 915 916 917 918
  void _handleHover(bool hovering) {
    if (hovering != _isHovering) {
      setState(() {
        return _isHovering = hovering;
      });
    }
  }

919 920
  @override
  Widget build(BuildContext context) {
921
    assert(debugCheckHasMaterial(context));
922 923
    // TODO(jonahwilliams): uncomment out this check once we have migrated tests.
    // assert(debugCheckHasMaterialLocalizations(context));
924
    assert(debugCheckHasDirectionality(context));
925 926
    assert(
      !(widget.style != null && widget.style.inherit == false &&
927
        (widget.style.fontSize == null || widget.style.textBaseline == null)),
928 929 930
      'inherit false style must supply fontSize and textBaseline',
    );

931
    final ThemeData themeData = Theme.of(context);
932
    final TextStyle style = themeData.textTheme.subhead.merge(widget.style);
933
    final Brightness keyboardAppearance = widget.keyboardAppearance ?? themeData.primaryColorBrightness;
934 935
    final TextEditingController controller = _effectiveController;
    final FocusNode focusNode = _effectiveFocusNode;
936 937
    final List<TextInputFormatter> formatters = widget.inputFormatters ?? <TextInputFormatter>[];
    if (widget.maxLength != null && widget.maxLengthEnforced)
938
      formatters.add(LengthLimitingTextInputFormatter(widget.maxLength));
939

940
    TextSelectionControls textSelectionControls;
941 942 943 944 945 946
    bool paintCursorAboveText;
    bool cursorOpacityAnimates;
    Offset cursorOffset;
    Color cursorColor = widget.cursorColor;
    Radius cursorRadius = widget.cursorRadius;

947 948
    switch (themeData.platform) {
      case TargetPlatform.iOS:
949
      case TargetPlatform.macOS:
950 951
        forcePressEnabled = true;
        textSelectionControls = cupertinoTextSelectionControls;
952 953 954 955
        paintCursorAboveText = true;
        cursorOpacityAnimates = true;
        cursorColor ??= CupertinoTheme.of(context).primaryColor;
        cursorRadius ??= const Radius.circular(2.0);
956
        cursorOffset = Offset(iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0);
957
        break;
958

959 960 961 962
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
        forcePressEnabled = false;
        textSelectionControls = materialTextSelectionControls;
963 964 965
        paintCursorAboveText = false;
        cursorOpacityAnimates = false;
        cursorColor ??= themeData.cursorColor;
966 967 968
        break;
    }

969
    Widget child = RepaintBoundary(
970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987
      child: EditableText(
        key: editableTextKey,
        readOnly: widget.readOnly,
        toolbarOptions: widget.toolbarOptions,
        showCursor: widget.showCursor,
        showSelectionHandles: _showSelectionHandles,
        controller: controller,
        focusNode: focusNode,
        keyboardType: widget.keyboardType,
        textInputAction: widget.textInputAction,
        textCapitalization: widget.textCapitalization,
        style: style,
        strutStyle: widget.strutStyle,
        textAlign: widget.textAlign,
        textDirection: widget.textDirection,
        autofocus: widget.autofocus,
        obscureText: widget.obscureText,
        autocorrect: widget.autocorrect,
988 989
        smartDashesType: widget.smartDashesType,
        smartQuotesType: widget.smartQuotesType,
990
        enableSuggestions: widget.enableSuggestions,
991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015
        maxLines: widget.maxLines,
        minLines: widget.minLines,
        expands: widget.expands,
        selectionColor: themeData.textSelectionColor,
        selectionControls: widget.selectionEnabled ? textSelectionControls : null,
        onChanged: widget.onChanged,
        onSelectionChanged: _handleSelectionChanged,
        onEditingComplete: widget.onEditingComplete,
        onSubmitted: widget.onSubmitted,
        onSelectionHandleTapped: _handleSelectionHandleTapped,
        inputFormatters: formatters,
        rendererIgnoresPointer: true,
        cursorWidth: widget.cursorWidth,
        cursorRadius: cursorRadius,
        cursorColor: cursorColor,
        cursorOpacityAnimates: cursorOpacityAnimates,
        cursorOffset: cursorOffset,
        paintCursorAboveText: paintCursorAboveText,
        backgroundCursorColor: CupertinoColors.inactiveGray,
        scrollPadding: widget.scrollPadding,
        keyboardAppearance: keyboardAppearance,
        enableInteractiveSelection: widget.enableInteractiveSelection,
        dragStartBehavior: widget.dragStartBehavior,
        scrollController: widget.scrollController,
        scrollPhysics: widget.scrollPhysics,
1016 1017 1018
      ),
    );

1019
    if (widget.decoration != null) {
1020 1021
      child = AnimatedBuilder(
        animation: Listenable.merge(<Listenable>[ focusNode, controller ]),
1022
        builder: (BuildContext context, Widget child) {
1023
          return InputDecorator(
1024
            decoration: _getEffectiveDecoration(),
1025 1026
            baseStyle: widget.style,
            textAlign: widget.textAlign,
1027
            textAlignVertical: widget.textAlignVertical,
1028
            isHovering: _isHovering,
1029 1030
            isFocused: focusNode.hasFocus,
            isEmpty: controller.value.text.isEmpty,
1031
            expands: widget.expands,
1032 1033 1034 1035 1036 1037
            child: child,
          );
        },
        child: child,
      );
    }
1038 1039
    return IgnorePointer(
      ignoring: !_isEnabled,
1040
      child: MouseRegion(
1041 1042
        onEnter: (PointerEnterEvent event) => _handleHover(true),
        onExit: (PointerExitEvent event) => _handleHover(false),
1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058
        child: AnimatedBuilder(
          animation: controller, // changes the _currentLength
          builder: (BuildContext context, Widget child) {
            return Semantics(
              maxValueLength: widget.maxLengthEnforced && widget.maxLength != null && widget.maxLength > 0
                  ? widget.maxLength
                  : null,
              currentValueLength: _currentLength,
              onTap: () {
                if (!_effectiveController.selection.isValid)
                  _effectiveController.selection = TextSelection.collapsed(offset: _effectiveController.text.length);
                _requestKeyboard();
              },
              child: child,
            );
          },
1059
          child: _selectionGestureDetectorBuilder.buildGestureDetector(
1060 1061 1062
            behavior: HitTestBehavior.translucent,
            child: child,
          ),
1063
        ),
1064
      ),
1065 1066
    );
  }
1067
}