text_field.dart 42.4 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.

5 6
import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle;

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

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

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

25 26 27
/// Signature for the [TextField.buildCounter] callback.
typedef InputCounterWidgetBuilder = Widget Function(
  /// The build context for the TextField
28 29 30 31 32 33 34 35 36
  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,
});
37

38 39
class _TextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDetectorBuilder {
  _TextFieldSelectionGestureDetectorBuilder({
40
    @required _TextFieldState state,
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
  }) : _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:
64
        case TargetPlatform.macOS:
65 66 67 68 69 70 71
          renderEditable.selectPositionAt(
            from: details.globalPosition,
            cause: SelectionChangedCause.longPress,
          );
          break;
        case TargetPlatform.android:
        case TargetPlatform.fuchsia:
72 73
        case TargetPlatform.linux:
        case TargetPlatform.windows:
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
          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:
90
        case TargetPlatform.macOS:
91 92 93 94
          renderEditable.selectWordEdge(cause: SelectionChangedCause.tap);
          break;
        case TargetPlatform.android:
        case TargetPlatform.fuchsia:
95 96
        case TargetPlatform.linux:
        case TargetPlatform.windows:
97 98 99 100 101 102 103 104 105 106 107 108 109 110
          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:
111
        case TargetPlatform.macOS:
112 113 114 115 116 117 118
          renderEditable.selectPositionAt(
            from: details.globalPosition,
            cause: SelectionChangedCause.longPress,
          );
          break;
        case TargetPlatform.android:
        case TargetPlatform.fuchsia:
119 120
        case TargetPlatform.linux:
        case TargetPlatform.windows:
121
          renderEditable.selectWord(cause: SelectionChangedCause.longPress);
122 123 124 125 126 127 128
          Feedback.forLongPress(_state.context);
          break;
      }
    }
  }
}

129
/// A material design text field.
130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151
///
/// 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
152
/// one of its ancestors to be a [Material] widget.
153 154 155 156
///
/// To integrate the [TextField] into a [Form] with other [FormField] widgets,
/// consider using [TextFormField].
///
157 158 159
/// Remember to [dispose] of the [TextEditingController] when it is no longer needed.
/// This will ensure we discard any resources used by the object.
///
160
/// {@tool snippet}
161 162 163 164
/// 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.
///
165
/// ![](https://flutter.github.io/assets-for-api-docs/assets/material/text_field.png)
166
///
167 168 169 170 171 172 173 174 175 176 177
/// ```dart
/// TextField(
///   obscureText: true,
///   decoration: InputDecoration(
///     border: OutlineInputBorder(),
///     labelText: 'Password',
///   ),
/// )
/// ```
/// {@end-tool}
///
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 235 236 237 238 239 240 241 242
/// ## 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].
///
243 244
/// See also:
///
245
///  * <https://material.io/design/components/text-fields.html>
246 247 248 249
///  * [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
250
///    [TextField]. The [EditableText] widget is rarely used directly unless
251
///    you are implementing an entirely different design language, such as
252
///    Cupertino.
253 254
///  * 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.
255 256 257 258 259 260 261 262 263
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.
264 265
  ///
  /// The [maxLines] property can be set to null to remove the restriction on
266
  /// the number of lines. By default, it is one, meaning this is a single-line
267
  /// text field. [maxLines] must not be zero.
268 269 270
  ///
  /// The [maxLength] property is set to null by default, which means the
  /// number of characters allowed in the text field is not restricted. If
271 272 273 274 275
  /// [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.
276 277
  ///
  /// After [maxLength] characters have been input, additional input
278
  /// is ignored, unless [maxLengthEnforced] is set to false. The text field
279 280 281 282 283 284 285
  /// 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.
286
  ///
287 288 289
  /// The text cursor is not shown if [showCursor] is false or if [showCursor]
  /// is null (the default) and [readOnly] is true.
  ///
290 291 292 293 294
  /// The [selectionHeightStyle] and [selectionWidthStyle] properties allow
  /// changing the shape of the selection highlighting. These properties default
  /// to [ui.BoxHeightStyle.tight] and [ui.BoxWidthStyle.tight] respectively and
  /// must not be null.
  ///
295
  /// The [textAlign], [autofocus], [obscureText], [readOnly], [autocorrect],
296
  /// [maxLengthEnforced], [scrollPadding], [maxLines], [maxLength],
297 298
  /// [selectionHeightStyle], [selectionWidthStyle], and [enableSuggestions]
  /// arguments must not be null.
299 300 301 302 303
  ///
  /// See also:
  ///
  ///  * [maxLength], which discusses the precise meaning of "number of
  ///    characters" and how it may differ from the intuitive meaning.
304
  const TextField({
305 306 307
    Key key,
    this.controller,
    this.focusNode,
308
    this.decoration = const InputDecoration(),
309 310
    TextInputType keyboardType,
    this.textInputAction,
311
    this.textCapitalization = TextCapitalization.none,
312
    this.style,
313
    this.strutStyle,
314
    this.textAlign = TextAlign.start,
315
    this.textAlignVertical,
316
    this.textDirection,
317
    this.readOnly = false,
318
    ToolbarOptions toolbarOptions,
319
    this.showCursor,
320 321 322
    this.autofocus = false,
    this.obscureText = false,
    this.autocorrect = true,
323 324
    SmartDashesType smartDashesType,
    SmartQuotesType smartQuotesType,
325
    this.enableSuggestions = true,
326
    this.maxLines = 1,
327 328
    this.minLines,
    this.expands = false,
329
    this.maxLength,
330
    this.maxLengthEnforced = true,
331
    this.onChanged,
332
    this.onEditingComplete,
333
    this.onSubmitted,
334
    this.inputFormatters,
335
    this.enabled,
336 337 338
    this.cursorWidth = 2.0,
    this.cursorRadius,
    this.cursorColor,
339 340
    this.selectionHeightStyle = ui.BoxHeightStyle.tight,
    this.selectionWidthStyle = ui.BoxWidthStyle.tight,
341
    this.keyboardAppearance,
342
    this.scrollPadding = const EdgeInsets.all(20.0),
343
    this.dragStartBehavior = DragStartBehavior.start,
344
    this.enableInteractiveSelection = true,
345
    this.onTap,
346
    this.buildCounter,
347
    this.scrollController,
348
    this.scrollPhysics,
349
  }) : assert(textAlign != null),
350
       assert(readOnly != null),
351 352
       assert(autofocus != null),
       assert(obscureText != null),
353
       assert(autocorrect != null),
354 355
       smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
       smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
356
       assert(enableSuggestions != null),
357
       assert(enableInteractiveSelection != null),
358
       assert(maxLengthEnforced != null),
359
       assert(scrollPadding != null),
360
       assert(dragStartBehavior != null),
361 362
       assert(selectionHeightStyle != null),
       assert(selectionWidthStyle != null),
363
       assert(maxLines == null || maxLines > 0),
364 365 366
       assert(minLines == null || minLines > 0),
       assert(
         (maxLines == null) || (minLines == null) || (maxLines >= minLines),
367
         "minLines can't be greater than maxLines",
368 369 370 371 372 373
       ),
       assert(expands != null),
       assert(
         !expands || (maxLines == null && minLines == null),
         'minLines and maxLines must be null when expands is true.',
       ),
374
       assert(!obscureText || maxLines == 1, 'Obscured fields cannot be multiline.'),
375
       assert(maxLength == null || maxLength == TextField.noMaxLength || maxLength > 0),
376
       keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
377
       toolbarOptions = toolbarOptions ?? (obscureText ?
378 379 380 381 382 383 384 385 386
         const ToolbarOptions(
           selectAll: true,
           paste: true,
         ) :
         const ToolbarOptions(
           copy: true,
           cut: true,
           selectAll: true,
           paste: true,
387
         )),
388
       super(key: key);
389 390 391

  /// Controls the text being edited.
  ///
392
  /// If null, this widget will create its own [TextEditingController].
393 394
  final TextEditingController controller;

395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414
  /// 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); });
  /// ```
415 416
  ///
  /// If null, this widget will create its own [FocusNode].
417 418 419
  ///
  /// ## Keyboard
  ///
Gary Qian's avatar
Gary Qian committed
420
  /// Requesting the focus will typically cause the keyboard to be shown
421 422
  /// if it's not showing already.
  ///
423
  /// On Android, the user can hide the keyboard - without changing the focus -
424 425 426 427 428 429 430 431 432
  /// 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()].
433 434 435 436
  final FocusNode focusNode;

  /// The decoration to show around the text field.
  ///
437
  /// By default, draws a horizontal line under the text field but can be
438 439
  /// configured to show an icon, label, hint text, and error text.
  ///
440
  /// Specify null to remove the decoration entirely (including the
441 442 443
  /// extra padding introduced by the decoration to save space for the labels).
  final InputDecoration decoration;

444
  /// {@macro flutter.widgets.editableText.keyboardType}
445 446
  final TextInputType keyboardType;

447 448
  /// The type of action button to use for the keyboard.
  ///
449 450
  /// Defaults to [TextInputAction.newline] if [keyboardType] is
  /// [TextInputType.multiline] and [TextInputAction.done] otherwise.
451 452
  final TextInputAction textInputAction;

453
  /// {@macro flutter.widgets.editableText.textCapitalization}
454 455
  final TextCapitalization textCapitalization;

456 457 458 459
  /// The style to use for the text being edited.
  ///
  /// This text style is also used as the base style for the [decoration].
  ///
460
  /// If null, defaults to the `subtitle1` text style from the current [Theme].
461 462
  final TextStyle style;

463 464 465
  /// {@macro flutter.widgets.editableText.strutStyle}
  final StrutStyle strutStyle;

466
  /// {@macro flutter.widgets.editableText.textAlign}
467 468
  final TextAlign textAlign;

Dan Field's avatar
Dan Field committed
469
  /// {@macro flutter.widgets.inputDecorator.textAlignVertical}
470 471
  final TextAlignVertical textAlignVertical;

472 473 474
  /// {@macro flutter.widgets.editableText.textDirection}
  final TextDirection textDirection;

475
  /// {@macro flutter.widgets.editableText.autofocus}
476 477
  final bool autofocus;

478
  /// {@macro flutter.widgets.editableText.obscureText}
479 480
  final bool obscureText;

481
  /// {@macro flutter.widgets.editableText.autocorrect}
482 483
  final bool autocorrect;

484 485 486 487 488 489
  /// {@macro flutter.services.textInput.smartDashesType}
  final SmartDashesType smartDashesType;

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

490 491 492
  /// {@macro flutter.services.textInput.enableSuggestions}
  final bool enableSuggestions;

493
  /// {@macro flutter.widgets.editableText.maxLines}
494 495
  final int maxLines;

496 497 498 499 500 501
  /// {@macro flutter.widgets.editableText.minLines}
  final int minLines;

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

502 503 504
  /// {@macro flutter.widgets.editableText.readOnly}
  final bool readOnly;

505 506 507 508 509 510 511
  /// 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;

512 513 514
  /// {@macro flutter.widgets.editableText.showCursor}
  final bool showCursor;

515 516
  /// If [maxLength] is set to this value, only the "current input length"
  /// part of the character counter is shown.
517
  static const int noMaxLength = -1;
518

519 520 521 522
  /// The maximum number of characters (Unicode scalar values) to allow in the
  /// text field.
  ///
  /// If set, a character counter will be displayed below the
523
  /// field showing how many characters have been entered. If set to a number
524
  /// greater than 0, it will also display the maximum number allowed. If set
525 526 527
  /// to [TextField.noMaxLength] then only the current character count is displayed.
  ///
  /// After [maxLength] characters have been input, additional input
528
  /// is ignored, unless [maxLengthEnforced] is set to false. The text field
529 530 531
  /// enforces the length with a [LengthLimitingTextInputFormatter], which is
  /// evaluated after the supplied [inputFormatters], if any.
  ///
532 533 534 535
  /// 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.
536 537 538 539 540 541 542 543
  ///
  /// 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.
  ///
544 545
  /// ## Limitations
  ///
546
  /// The text field does not currently count Unicode grapheme clusters (i.e.
547 548 549 550 551 552 553 554 555
  /// 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
556
  /// SMALL LETTER O WITH DIAERESIS". In the first case, the text field will
557 558 559 560 561 562 563 564 565 566
  /// 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:
567
  ///
568 569 570 571 572 573 574 575 576 577 578 579
  ///  * [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;

580
  /// {@macro flutter.widgets.editableText.onChanged}
581 582 583 584 585 586 587
  ///
  /// 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.
588 589
  final ValueChanged<String> onChanged;

590
  /// {@macro flutter.widgets.editableText.onEditingComplete}
591 592
  final VoidCallback onEditingComplete;

593
  /// {@macro flutter.widgets.editableText.onSubmitted}
594 595 596 597 598 599
  ///
  /// 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].
600 601
  final ValueChanged<String> onSubmitted;

602
  /// {@macro flutter.widgets.editableText.inputFormatters}
603 604
  final List<TextInputFormatter> inputFormatters;

605
  /// If false the text field is "disabled": it ignores taps and its
606 607 608 609 610 611
  /// [decoration] is rendered in grey.
  ///
  /// If non-null this property overrides the [decoration]'s
  /// [Decoration.enabled] property.
  final bool enabled;

612
  /// {@macro flutter.widgets.editableText.cursorWidth}
613 614
  final double cursorWidth;

615
  /// {@macro flutter.widgets.editableText.cursorRadius}
616 617 618
  final Radius cursorRadius;

  /// The color to use when painting the cursor.
619
  ///
620 621
  /// Defaults to [ThemeData.cursorColor] or [CupertinoTheme.primaryColor]
  /// depending on [ThemeData.platform].
622 623
  final Color cursorColor;

624 625 626 627 628 629 630 631 632 633
  /// Controls how tall the selection highlight boxes are computed to be.
  ///
  /// See [ui.BoxHeightStyle] for details on available styles.
  final ui.BoxHeightStyle selectionHeightStyle;

  /// Controls how wide the selection highlight boxes are computed to be.
  ///
  /// See [ui.BoxWidthStyle] for details on available styles.
  final ui.BoxWidthStyle selectionWidthStyle;

634
  /// The appearance of the keyboard.
635
  ///
636
  /// This setting is only honored on iOS devices.
637
  ///
638 639 640
  /// If unset, defaults to the brightness of [ThemeData.primaryColorBrightness].
  final Brightness keyboardAppearance;

641
  /// {@macro flutter.widgets.editableText.scrollPadding}
642 643
  final EdgeInsets scrollPadding;

644 645 646
  /// {@macro flutter.widgets.editableText.enableInteractiveSelection}
  final bool enableInteractiveSelection;

647 648 649
  /// {@macro flutter.widgets.scrollable.dragStartBehavior}
  final DragStartBehavior dragStartBehavior;

650
  /// {@macro flutter.rendering.editable.selectionEnabled}
651
  bool get selectionEnabled => enableInteractiveSelection;
652

653 654
  /// {@template flutter.material.textfield.onTap}
  /// Called for each distinct tap except for every second tap of a double tap.
655
  ///
656
  /// The text field builds a [GestureDetector] to handle input events like tap,
657
  /// to trigger focus requests, to move the caret, adjust the selection, etc.
658
  /// Handling some of those events by wrapping the text field with a competing
659 660
  /// GestureDetector is problematic.
  ///
661
  /// To unconditionally handle taps, without interfering with the text field's
662 663
  /// internal gesture detector, provide this callback.
  ///
664
  /// If the text field is created with [enabled] false, taps will not be
665 666
  /// recognized.
  ///
667
  /// To be notified when the text field gains or loses the focus, provide a
668 669 670
  /// [focusNode] and add a listener to that.
  ///
  /// To listen to arbitrary pointer events without competing with the
671
  /// text field's internal gesture detector, use a [Listener].
672
  /// {@endtemplate}
673 674
  final GestureTapCallback onTap;

675 676 677 678 679 680 681 682 683 684
  /// 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.
  ///
685
  /// {@tool snippet}
686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701
  /// ```dart
  /// Widget counter(
  ///   BuildContext context,
  ///   {
  ///     int currentLength,
  ///     int maxLength,
  ///     bool isFocused,
  ///   }
  /// ) {
  ///   return Text(
  ///     '$currentLength of $maxLength characters',
  ///     semanticsLabel: 'character count',
  ///   );
  /// }
  /// ```
  /// {@end-tool}
702 703 704
  ///
  /// If buildCounter returns null, then no counter and no Semantics widget will
  /// be created at all.
705 706
  final InputCounterWidgetBuilder buildCounter;

Dan Field's avatar
Dan Field committed
707
  /// {@macro flutter.widgets.editableText.scrollPhysics}
708 709
  final ScrollPhysics scrollPhysics;

710 711 712
  /// {@macro flutter.widgets.editableText.scrollController}
  final ScrollController scrollController;

713
  @override
714
  _TextFieldState createState() => _TextFieldState();
715 716

  @override
717 718
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
719 720
    properties.add(DiagnosticsProperty<TextEditingController>('controller', controller, defaultValue: null));
    properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode, defaultValue: null));
721
    properties.add(DiagnosticsProperty<bool>('enabled', enabled, defaultValue: null));
722
    properties.add(DiagnosticsProperty<InputDecoration>('decoration', decoration, defaultValue: const InputDecoration()));
723 724 725 726
    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));
727
    properties.add(DiagnosticsProperty<bool>('autocorrect', autocorrect, defaultValue: true));
728 729
    properties.add(EnumProperty<SmartDashesType>('smartDashesType', smartDashesType, defaultValue: obscureText ? SmartDashesType.disabled : SmartDashesType.enabled));
    properties.add(EnumProperty<SmartQuotesType>('smartQuotesType', smartQuotesType, defaultValue: obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled));
730
    properties.add(DiagnosticsProperty<bool>('enableSuggestions', enableSuggestions, defaultValue: true));
731
    properties.add(IntProperty('maxLines', maxLines, defaultValue: 1));
732 733
    properties.add(IntProperty('minLines', minLines, defaultValue: null));
    properties.add(DiagnosticsProperty<bool>('expands', expands, defaultValue: false));
734
    properties.add(IntProperty('maxLength', maxLength, defaultValue: null));
735 736 737 738
    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));
739
    properties.add(DiagnosticsProperty<TextAlignVertical>('textAlignVertical', textAlignVertical, defaultValue: null));
740 741 742
    properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
    properties.add(DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0));
    properties.add(DiagnosticsProperty<Radius>('cursorRadius', cursorRadius, defaultValue: null));
743
    properties.add(ColorProperty('cursorColor', cursorColor, defaultValue: null));
744 745 746
    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'));
747
    properties.add(DiagnosticsProperty<ScrollController>('scrollController', scrollController, defaultValue: null));
748
    properties.add(DiagnosticsProperty<ScrollPhysics>('scrollPhysics', scrollPhysics, defaultValue: null));
749 750 751
  }
}

752
class _TextFieldState extends State<TextField> implements TextSelectionGestureDetectorBuilderDelegate {
753
  TextEditingController _controller;
754
  TextEditingController get _effectiveController => widget.controller ?? _controller;
755 756

  FocusNode _focusNode;
757
  FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode());
758

759 760
  bool _isHovering = false;

761 762 763 764
  bool get needsCounter => widget.maxLength != null
    && widget.decoration != null
    && widget.decoration.counterText == null;

765 766
  bool _showSelectionHandles = false;

767 768 769 770 771 772 773 774 775 776 777 778 779
  _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.

780 781
  bool get _isEnabled =>  widget.enabled ?? widget.decoration?.enabled ?? true;

782 783
  int get _currentLength => _effectiveController.value.text.runes.length;

784
  InputDecoration _getEffectiveDecoration() {
785
    final MaterialLocalizations localizations = MaterialLocalizations.of(context);
786
    final ThemeData themeData = Theme.of(context);
787
    final InputDecoration effectiveDecoration = (widget.decoration ?? const InputDecoration())
788
      .applyDefaults(themeData.inputDecorationTheme)
789 790
      .copyWith(
        enabled: widget.enabled,
791
        hintMaxLines: widget.decoration?.hintMaxLines ?? widget.maxLines,
792
      );
793

794 795
    // No need to build anything if counter or counterText were given directly.
    if (effectiveDecoration.counter != null || effectiveDecoration.counterText != null)
796
      return effectiveDecoration;
797

798 799
    // If buildCounter was provided, use it to generate a counter widget.
    Widget counter;
800
    final int currentLength = _currentLength;
801 802 803 804
    if (effectiveDecoration.counter == null
        && effectiveDecoration.counterText == null
        && widget.buildCounter != null) {
      final bool isFocused = _effectiveFocusNode.hasFocus;
805 806 807 808 809
      final Widget builtCounter = widget.buildCounter(
        context,
        currentLength: currentLength,
        maxLength: widget.maxLength,
        isFocused: isFocused,
810
      );
811 812 813 814 815 816 817 818
      // If buildCounter returns null, don't add a counter widget to the field.
      if (builtCounter != null) {
        counter = Semantics(
          container: true,
          liveRegion: isFocused,
          child: builtCounter,
        );
      }
819 820 821 822 823 824
      return effectiveDecoration.copyWith(counter: counter);
    }

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

825 826 827
    String counterText = '$currentLength';
    String semanticCounterText = '';

828 829 830
    // Handle a real maxLength (positive number)
    if (widget.maxLength > 0) {
      // Show the maxLength in the counter
831
      counterText += '/${widget.maxLength}';
832
      final int remaining = (widget.maxLength - currentLength).clamp(0, widget.maxLength) as int;
833 834
      semanticCounterText = localizations.remainingTextFieldCharacterCount(remaining);

835 836 837 838 839 840 841 842 843 844
      // 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,
        );
      }
845
    }
846

847 848 849 850
    return effectiveDecoration.copyWith(
      counterText: counterText,
      semanticCounterText: semanticCounterText,
    );
851 852
  }

853 854 855
  @override
  void initState() {
    super.initState();
856
    _selectionGestureDetectorBuilder = _TextFieldSelectionGestureDetectorBuilder(state: this);
857
    if (widget.controller == null) {
858
      _controller = TextEditingController();
859 860
    }
    _effectiveFocusNode.canRequestFocus = _isEnabled;
861 862 863
  }

  @override
864
  void didUpdateWidget(TextField oldWidget) {
865
    super.didUpdateWidget(oldWidget);
866
    if (widget.controller == null && oldWidget.controller != null)
867
      _controller = TextEditingController.fromValue(oldWidget.controller.value);
868
    else if (widget.controller != null && oldWidget.controller == null)
869
      _controller = null;
870
    _effectiveFocusNode.canRequestFocus = _isEnabled;
871 872 873 874 875
    if (_effectiveFocusNode.hasFocus && widget.readOnly != oldWidget.readOnly) {
      if(_effectiveController.selection.isCollapsed) {
        _showSelectionHandles = !widget.readOnly;
      }
    }
876 877 878 879 880 881 882 883
  }

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

884
  EditableTextState get _editableText => editableTextKey.currentState;
885

886
  void _requestKeyboard() {
887 888 889 890 891 892
    _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.
893
    if (!_selectionGestureDetectorBuilder.shouldShowSelectionToolbar)
894 895 896 897 898
      return false;

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

899 900 901
    if (widget.readOnly && _effectiveController.selection.isCollapsed)
      return false;

902 903 904 905 906 907 908
    if (cause == SelectionChangedCause.longPress)
      return true;

    if (_effectiveController.text.isNotEmpty)
      return true;

    return false;
909 910
  }

911
  void _handleSelectionChanged(TextSelection selection, SelectionChangedCause cause) {
912 913 914 915 916
    final bool willShowSelectionHandles = _shouldShowSelectionHandles(cause);
    if (willShowSelectionHandles != _showSelectionHandles) {
      setState(() {
        _showSelectionHandles = willShowSelectionHandles;
      });
917 918
    }

919 920
    switch (Theme.of(context).platform) {
      case TargetPlatform.iOS:
921
      case TargetPlatform.macOS:
922
        if (cause == SelectionChangedCause.longPress) {
923
          _editableText?.bringIntoView(selection.base);
924 925 926 927
        }
        return;
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
928 929
      case TargetPlatform.linux:
      case TargetPlatform.windows:
930 931 932 933
        // Do nothing.
    }
  }

934 935 936 937 938 939 940
  /// Toggle the toolbar when a selection handle is tapped.
  void _handleSelectionHandleTapped() {
    if (_effectiveController.selection.isCollapsed) {
      _editableText.toggleToolbar();
    }
  }

941 942 943
  void _handleHover(bool hovering) {
    if (hovering != _isHovering) {
      setState(() {
944
        _isHovering = hovering;
945 946 947 948
      });
    }
  }

949 950
  @override
  Widget build(BuildContext context) {
951
    assert(debugCheckHasMaterial(context));
952 953
    // TODO(jonahwilliams): uncomment out this check once we have migrated tests.
    // assert(debugCheckHasMaterialLocalizations(context));
954
    assert(debugCheckHasDirectionality(context));
955 956
    assert(
      !(widget.style != null && widget.style.inherit == false &&
957
        (widget.style.fontSize == null || widget.style.textBaseline == null)),
958 959 960
      'inherit false style must supply fontSize and textBaseline',
    );

961
    final ThemeData themeData = Theme.of(context);
962
    final TextStyle style = themeData.textTheme.subtitle1.merge(widget.style);
963
    final Brightness keyboardAppearance = widget.keyboardAppearance ?? themeData.primaryColorBrightness;
964 965
    final TextEditingController controller = _effectiveController;
    final FocusNode focusNode = _effectiveFocusNode;
966 967
    final List<TextInputFormatter> formatters = widget.inputFormatters ?? <TextInputFormatter>[];
    if (widget.maxLength != null && widget.maxLengthEnforced)
968
      formatters.add(LengthLimitingTextInputFormatter(widget.maxLength));
969

970
    TextSelectionControls textSelectionControls;
971 972 973 974 975 976
    bool paintCursorAboveText;
    bool cursorOpacityAnimates;
    Offset cursorOffset;
    Color cursorColor = widget.cursorColor;
    Radius cursorRadius = widget.cursorRadius;

977 978
    switch (themeData.platform) {
      case TargetPlatform.iOS:
979
      case TargetPlatform.macOS:
980 981
        forcePressEnabled = true;
        textSelectionControls = cupertinoTextSelectionControls;
982 983 984 985
        paintCursorAboveText = true;
        cursorOpacityAnimates = true;
        cursorColor ??= CupertinoTheme.of(context).primaryColor;
        cursorRadius ??= const Radius.circular(2.0);
986
        cursorOffset = Offset(iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0);
987
        break;
988

989 990
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
991 992
      case TargetPlatform.linux:
      case TargetPlatform.windows:
993 994
        forcePressEnabled = false;
        textSelectionControls = materialTextSelectionControls;
995 996 997
        paintCursorAboveText = false;
        cursorOpacityAnimates = false;
        cursorColor ??= themeData.cursorColor;
998 999 1000
        break;
    }

1001
    Widget child = RepaintBoundary(
1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019
      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,
1020 1021
        smartDashesType: widget.smartDashesType,
        smartQuotesType: widget.smartQuotesType,
1022
        enableSuggestions: widget.enableSuggestions,
1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037
        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,
1038 1039
        selectionHeightStyle: widget.selectionHeightStyle,
        selectionWidthStyle: widget.selectionWidthStyle,
1040 1041 1042 1043 1044 1045 1046 1047 1048 1049
        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,
1050 1051 1052
      ),
    );

1053
    if (widget.decoration != null) {
1054 1055
      child = AnimatedBuilder(
        animation: Listenable.merge(<Listenable>[ focusNode, controller ]),
1056
        builder: (BuildContext context, Widget child) {
1057
          return InputDecorator(
1058
            decoration: _getEffectiveDecoration(),
1059 1060
            baseStyle: widget.style,
            textAlign: widget.textAlign,
1061
            textAlignVertical: widget.textAlignVertical,
1062
            isHovering: _isHovering,
1063 1064
            isFocused: focusNode.hasFocus,
            isEmpty: controller.value.text.isEmpty,
1065
            expands: widget.expands,
1066 1067 1068 1069 1070 1071
            child: child,
          );
        },
        child: child,
      );
    }
1072 1073
    return IgnorePointer(
      ignoring: !_isEnabled,
1074
      child: MouseRegion(
1075 1076
        onEnter: (PointerEnterEvent event) => _handleHover(true),
        onExit: (PointerExitEvent event) => _handleHover(false),
1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092
        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,
            );
          },
1093
          child: _selectionGestureDetectorBuilder.buildGestureDetector(
1094 1095 1096
            behavior: HitTestBehavior.translucent,
            child: child,
          ),
1097
        ),
1098
      ),
1099 1100
    );
  }
1101
}