
This change affects Android and iOS devices using the TextField's context menu. After this change the context menu will fade out when scrolling the text and fade in when the scroll ends. If the scroll ends and the selection is outside of the view, then the toolbar will be scheduled to show in a future scroll end. This toolbar scheduling can be invalidated if the `TextEditingValue` changed anytime between the scheduling and when the toolbar is ready to be shown. This change also fixes a regression where the TextField context menu would not fade when the selection handles where not visible. When using the native browser context menu this behavior is not controlled by Flutter. https://github.com/flutter/flutter/assets/948037/3f46bcbb-ba6f-456c-8473-e42919b9d572 Fixes #52425 Fixes #105804 Fixes #52426
1636 lines
66 KiB
Dart
1636 lines
66 KiB
Dart
// Copyright 2014 The Flutter Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle;
|
|
|
|
import 'package:flutter/cupertino.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/gestures.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
import 'package:flutter/services.dart';
|
|
|
|
import 'adaptive_text_selection_toolbar.dart';
|
|
import 'color_scheme.dart';
|
|
import 'colors.dart';
|
|
import 'debug.dart';
|
|
import 'desktop_text_selection.dart';
|
|
import 'feedback.dart';
|
|
import 'input_decorator.dart';
|
|
import 'magnifier.dart';
|
|
import 'material_localizations.dart';
|
|
import 'material_state.dart';
|
|
import 'selectable_text.dart' show iOSHorizontalOffset;
|
|
import 'spell_check_suggestions_toolbar.dart';
|
|
import 'text_selection.dart';
|
|
import 'theme.dart';
|
|
|
|
export 'package:flutter/services.dart' show SmartDashesType, SmartQuotesType, TextCapitalization, TextInputAction, TextInputType;
|
|
|
|
// Examples can assume:
|
|
// late BuildContext context;
|
|
// late FocusNode myFocusNode;
|
|
|
|
/// Signature for the [TextField.buildCounter] callback.
|
|
typedef InputCounterWidgetBuilder = Widget? Function(
|
|
/// The build context for the TextField.
|
|
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,
|
|
});
|
|
|
|
class _TextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDetectorBuilder {
|
|
_TextFieldSelectionGestureDetectorBuilder({
|
|
required _TextFieldState state,
|
|
}) : _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
|
|
bool get onUserTapAlwaysCalled => _state.widget.onTapAlwaysCalled;
|
|
|
|
@override
|
|
void onUserTap() {
|
|
_state.widget.onTap?.call();
|
|
}
|
|
|
|
@override
|
|
void onSingleLongTapStart(LongPressStartDetails details) {
|
|
super.onSingleLongTapStart(details);
|
|
if (delegate.selectionEnabled) {
|
|
switch (Theme.of(_state.context).platform) {
|
|
case TargetPlatform.iOS:
|
|
case TargetPlatform.macOS:
|
|
break;
|
|
case TargetPlatform.android:
|
|
case TargetPlatform.fuchsia:
|
|
case TargetPlatform.linux:
|
|
case TargetPlatform.windows:
|
|
Feedback.forLongPress(_state.context);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A Material Design text field.
|
|
///
|
|
/// 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
|
|
/// one of its ancestors to be a [Material] widget.
|
|
///
|
|
/// To integrate the [TextField] into a [Form] with other [FormField] widgets,
|
|
/// consider using [TextFormField].
|
|
///
|
|
/// {@template flutter.material.textfield.wantKeepAlive}
|
|
/// When the widget has focus, it will prevent itself from disposing via its
|
|
/// underlying [EditableText]'s [AutomaticKeepAliveClientMixin.wantKeepAlive] in
|
|
/// order to avoid losing the selection. Removing the focus will allow it to be
|
|
/// disposed.
|
|
/// {@endtemplate}
|
|
///
|
|
/// Remember to call [TextEditingController.dispose] of the [TextEditingController]
|
|
/// when it is no longer needed. This will ensure we discard any resources used
|
|
/// by the object.
|
|
///
|
|
/// ## Obscured Input
|
|
///
|
|
/// {@tool dartpad}
|
|
/// 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.
|
|
///
|
|
/// ** See code in examples/api/lib/material/text_field/text_field.0.dart **
|
|
/// {@end-tool}
|
|
///
|
|
/// ## 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}
|
|
/// This sample shows how to get a value from a TextField via the [onSubmitted]
|
|
/// callback.
|
|
///
|
|
/// ** See code in examples/api/lib/material/text_field/text_field.1.dart **
|
|
/// {@end-tool}
|
|
///
|
|
/// {@macro flutter.widgets.EditableText.lifeCycle}
|
|
///
|
|
/// 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].
|
|
///
|
|
/// ## Handling emojis and other complex characters
|
|
/// {@macro flutter.widgets.EditableText.onChanged}
|
|
///
|
|
/// In the live Dartpad example above, try typing the emoji 👨👩👦
|
|
/// into the field and submitting. Because the example code measures the length
|
|
/// with `value.characters.length`, the emoji is correctly counted as a single
|
|
/// character.
|
|
///
|
|
/// {@macro flutter.widgets.editableText.showCaretOnScreen}
|
|
///
|
|
/// {@macro flutter.widgets.editableText.accessibility}
|
|
///
|
|
/// {@tool dartpad}
|
|
/// This sample shows how to style a text field to match a filled or outlined
|
|
/// Material Design 3 text field.
|
|
///
|
|
/// ** See code in examples/api/lib/material/text_field/text_field.2.dart **
|
|
/// {@end-tool}
|
|
///
|
|
/// ## Scrolling Considerations
|
|
///
|
|
/// If this [TextField] is not a descendant of [Scaffold] and is being used
|
|
/// within a [Scrollable] or nested [Scrollable]s, consider placing a
|
|
/// [ScrollNotificationObserver] above the root [Scrollable] that contains this
|
|
/// [TextField] to ensure proper scroll coordination for [TextField] and its
|
|
/// components like [TextSelectionOverlay].
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [TextFormField], which integrates with the [Form] widget.
|
|
/// * [InputDecorator], which shows the labels and other visual elements that
|
|
/// surround the actual text editing widget.
|
|
/// * [EditableText], which is the raw text editing control at the heart of a
|
|
/// [TextField]. The [EditableText] widget is rarely used directly unless
|
|
/// you are implementing an entirely different design language, such as
|
|
/// Cupertino.
|
|
/// * <https://material.io/design/components/text-fields.html>
|
|
/// * Cookbook: [Create and style a text field](https://flutter.dev/docs/cookbook/forms/text-input)
|
|
/// * Cookbook: [Handle changes to a text field](https://flutter.dev/docs/cookbook/forms/text-field-changes)
|
|
/// * Cookbook: [Retrieve the value of a text field](https://flutter.dev/docs/cookbook/forms/retrieve-input)
|
|
/// * Cookbook: [Focus and text fields](https://flutter.dev/docs/cookbook/forms/focus)
|
|
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.
|
|
///
|
|
/// The [maxLines] property can be set to null to remove the restriction on
|
|
/// the number of lines. By default, it is one, meaning this is a single-line
|
|
/// text field. [maxLines] must not be zero.
|
|
///
|
|
/// The [maxLength] property is set to null by default, which means the
|
|
/// number of characters allowed in the text field is not restricted. If
|
|
/// [maxLength] is set a character counter will be displayed below the
|
|
/// field showing how many characters have been entered. 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.
|
|
///
|
|
/// After [maxLength] characters have been input, additional input
|
|
/// is ignored, unless [maxLengthEnforcement] is set to
|
|
/// [MaxLengthEnforcement.none].
|
|
/// The text field 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 [maxLengthEnforcement] is set to [MaxLengthEnforcement.none], 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.
|
|
///
|
|
/// The text cursor is not shown if [showCursor] is false or if [showCursor]
|
|
/// is null (the default) and [readOnly] is true.
|
|
///
|
|
/// 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.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [maxLength], which discusses the precise meaning of "number of
|
|
/// characters" and how it may differ from the intuitive meaning.
|
|
const TextField({
|
|
super.key,
|
|
this.controller,
|
|
this.focusNode,
|
|
this.undoController,
|
|
this.decoration = const InputDecoration(),
|
|
TextInputType? keyboardType,
|
|
this.textInputAction,
|
|
this.textCapitalization = TextCapitalization.none,
|
|
this.style,
|
|
this.strutStyle,
|
|
this.textAlign = TextAlign.start,
|
|
this.textAlignVertical,
|
|
this.textDirection,
|
|
this.readOnly = false,
|
|
@Deprecated(
|
|
'Use `contextMenuBuilder` instead. '
|
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
|
)
|
|
this.toolbarOptions,
|
|
this.showCursor,
|
|
this.autofocus = false,
|
|
this.statesController,
|
|
this.obscuringCharacter = '•',
|
|
this.obscureText = false,
|
|
this.autocorrect = true,
|
|
SmartDashesType? smartDashesType,
|
|
SmartQuotesType? smartQuotesType,
|
|
this.enableSuggestions = true,
|
|
this.maxLines = 1,
|
|
this.minLines,
|
|
this.expands = false,
|
|
this.maxLength,
|
|
this.maxLengthEnforcement,
|
|
this.onChanged,
|
|
this.onEditingComplete,
|
|
this.onSubmitted,
|
|
this.onAppPrivateCommand,
|
|
this.inputFormatters,
|
|
this.enabled,
|
|
this.ignorePointers,
|
|
this.cursorWidth = 2.0,
|
|
this.cursorHeight,
|
|
this.cursorRadius,
|
|
this.cursorOpacityAnimates,
|
|
this.cursorColor,
|
|
this.cursorErrorColor,
|
|
this.selectionHeightStyle = ui.BoxHeightStyle.tight,
|
|
this.selectionWidthStyle = ui.BoxWidthStyle.tight,
|
|
this.keyboardAppearance,
|
|
this.scrollPadding = const EdgeInsets.all(20.0),
|
|
this.dragStartBehavior = DragStartBehavior.start,
|
|
bool? enableInteractiveSelection,
|
|
this.selectionControls,
|
|
this.onTap,
|
|
this.onTapAlwaysCalled = false,
|
|
this.onTapOutside,
|
|
this.mouseCursor,
|
|
this.buildCounter,
|
|
this.scrollController,
|
|
this.scrollPhysics,
|
|
this.autofillHints = const <String>[],
|
|
this.contentInsertionConfiguration,
|
|
this.clipBehavior = Clip.hardEdge,
|
|
this.restorationId,
|
|
this.scribbleEnabled = true,
|
|
this.enableIMEPersonalizedLearning = true,
|
|
this.contextMenuBuilder = _defaultContextMenuBuilder,
|
|
this.canRequestFocus = true,
|
|
this.spellCheckConfiguration,
|
|
this.magnifierConfiguration,
|
|
}) : assert(obscuringCharacter.length == 1),
|
|
smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
|
|
smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
|
|
assert(maxLines == null || maxLines > 0),
|
|
assert(minLines == null || minLines > 0),
|
|
assert(
|
|
(maxLines == null) || (minLines == null) || (maxLines >= minLines),
|
|
"minLines can't be greater than maxLines",
|
|
),
|
|
assert(
|
|
!expands || (maxLines == null && minLines == null),
|
|
'minLines and maxLines must be null when expands is true.',
|
|
),
|
|
assert(!obscureText || maxLines == 1, 'Obscured fields cannot be multiline.'),
|
|
assert(maxLength == null || maxLength == TextField.noMaxLength || maxLength > 0),
|
|
// Assert the following instead of setting it directly to avoid surprising the user by silently changing the value they set.
|
|
assert(
|
|
!identical(textInputAction, TextInputAction.newline) ||
|
|
maxLines == 1 ||
|
|
!identical(keyboardType, TextInputType.text),
|
|
'Use keyboardType TextInputType.multiline when using TextInputAction.newline on a multiline TextField.',
|
|
),
|
|
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
|
|
enableInteractiveSelection = enableInteractiveSelection ?? (!readOnly || !obscureText);
|
|
|
|
/// {@macro flutter.widgets.magnifier.TextMagnifierConfiguration.intro}
|
|
///
|
|
/// {@macro flutter.widgets.magnifier.intro}
|
|
///
|
|
/// {@macro flutter.widgets.magnifier.TextMagnifierConfiguration.details}
|
|
///
|
|
/// By default, builds a [CupertinoTextMagnifier] on iOS and [TextMagnifier]
|
|
/// on Android, and builds nothing on all other platforms. If it is desired to
|
|
/// suppress the magnifier, consider passing [TextMagnifierConfiguration.disabled].
|
|
///
|
|
/// {@tool dartpad}
|
|
/// This sample demonstrates how to customize the magnifier that this text field uses.
|
|
///
|
|
/// ** See code in examples/api/lib/widgets/text_magnifier/text_magnifier.0.dart **
|
|
/// {@end-tool}
|
|
final TextMagnifierConfiguration? magnifierConfiguration;
|
|
|
|
/// Controls the text being edited.
|
|
///
|
|
/// If null, this widget will create its own [TextEditingController].
|
|
final TextEditingController? controller;
|
|
|
|
/// 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
|
|
/// myFocusNode.addListener(() { print(myFocusNode.hasFocus); });
|
|
/// ```
|
|
///
|
|
/// If null, this widget will create its own [FocusNode].
|
|
///
|
|
/// ## Keyboard
|
|
///
|
|
/// Requesting the focus will typically cause the keyboard to be shown
|
|
/// if it's not showing already.
|
|
///
|
|
/// On Android, the user can hide the keyboard - without changing the focus -
|
|
/// 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()].
|
|
final FocusNode? focusNode;
|
|
|
|
/// The decoration to show around the text field.
|
|
///
|
|
/// By default, draws a horizontal line under the text field but can be
|
|
/// configured to show an icon, label, hint text, and error text.
|
|
///
|
|
/// Specify null to remove the decoration entirely (including the
|
|
/// extra padding introduced by the decoration to save space for the labels).
|
|
final InputDecoration? decoration;
|
|
|
|
/// {@macro flutter.widgets.editableText.keyboardType}
|
|
final TextInputType keyboardType;
|
|
|
|
/// {@template flutter.widgets.TextField.textInputAction}
|
|
/// The type of action button to use for the keyboard.
|
|
///
|
|
/// Defaults to [TextInputAction.newline] if [keyboardType] is
|
|
/// [TextInputType.multiline] and [TextInputAction.done] otherwise.
|
|
/// {@endtemplate}
|
|
final TextInputAction? textInputAction;
|
|
|
|
/// {@macro flutter.widgets.editableText.textCapitalization}
|
|
final TextCapitalization textCapitalization;
|
|
|
|
/// The style to use for the text being edited.
|
|
///
|
|
/// This text style is also used as the base style for the [decoration].
|
|
///
|
|
/// If null, [TextTheme.bodyLarge] will be used. When the text field is disabled,
|
|
/// [TextTheme.bodyLarge] with an opacity of 0.38 will be used instead.
|
|
///
|
|
/// If null and [ThemeData.useMaterial3] is false, [TextTheme.titleMedium] will
|
|
/// be used. When the text field is disabled, [TextTheme.titleMedium] with
|
|
/// [ThemeData.disabledColor] will be used instead.
|
|
final TextStyle? style;
|
|
|
|
/// {@macro flutter.widgets.editableText.strutStyle}
|
|
final StrutStyle? strutStyle;
|
|
|
|
/// {@macro flutter.widgets.editableText.textAlign}
|
|
final TextAlign textAlign;
|
|
|
|
/// {@macro flutter.material.InputDecorator.textAlignVertical}
|
|
final TextAlignVertical? textAlignVertical;
|
|
|
|
/// {@macro flutter.widgets.editableText.textDirection}
|
|
final TextDirection? textDirection;
|
|
|
|
/// {@macro flutter.widgets.editableText.autofocus}
|
|
final bool autofocus;
|
|
|
|
/// Represents the interactive "state" of this widget in terms of a set of
|
|
/// [MaterialState]s, including [MaterialState.disabled], [MaterialState.hovered],
|
|
/// [MaterialState.error], and [MaterialState.focused].
|
|
///
|
|
/// Classes based on this one can provide their own
|
|
/// [MaterialStatesController] to which they've added listeners.
|
|
/// They can also update the controller's [MaterialStatesController.value]
|
|
/// however, this may only be done when it's safe to call
|
|
/// [State.setState], like in an event handler.
|
|
///
|
|
/// The controller's [MaterialStatesController.value] represents the set of
|
|
/// states that a widget's visual properties, typically [MaterialStateProperty]
|
|
/// values, are resolved against. It is _not_ the intrinsic state of the widget.
|
|
/// The widget is responsible for ensuring that the controller's
|
|
/// [MaterialStatesController.value] tracks its intrinsic state. For example
|
|
/// one cannot request the keyboard focus for a widget by adding [MaterialState.focused]
|
|
/// to its controller. When the widget gains the or loses the focus it will
|
|
/// [MaterialStatesController.update] its controller's [MaterialStatesController.value]
|
|
/// and notify listeners of the change.
|
|
final MaterialStatesController? statesController;
|
|
|
|
/// {@macro flutter.widgets.editableText.obscuringCharacter}
|
|
final String obscuringCharacter;
|
|
|
|
/// {@macro flutter.widgets.editableText.obscureText}
|
|
final bool obscureText;
|
|
|
|
/// {@macro flutter.widgets.editableText.autocorrect}
|
|
final bool autocorrect;
|
|
|
|
/// {@macro flutter.services.TextInputConfiguration.smartDashesType}
|
|
final SmartDashesType smartDashesType;
|
|
|
|
/// {@macro flutter.services.TextInputConfiguration.smartQuotesType}
|
|
final SmartQuotesType smartQuotesType;
|
|
|
|
/// {@macro flutter.services.TextInputConfiguration.enableSuggestions}
|
|
final bool enableSuggestions;
|
|
|
|
/// {@macro flutter.widgets.editableText.maxLines}
|
|
/// * [expands], which determines whether the field should fill the height of
|
|
/// its parent.
|
|
final int? maxLines;
|
|
|
|
/// {@macro flutter.widgets.editableText.minLines}
|
|
/// * [expands], which determines whether the field should fill the height of
|
|
/// its parent.
|
|
final int? minLines;
|
|
|
|
/// {@macro flutter.widgets.editableText.expands}
|
|
final bool expands;
|
|
|
|
/// {@macro flutter.widgets.editableText.readOnly}
|
|
final bool readOnly;
|
|
|
|
/// 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.
|
|
@Deprecated(
|
|
'Use `contextMenuBuilder` instead. '
|
|
'This feature was deprecated after v3.3.0-0.5.pre.',
|
|
)
|
|
final ToolbarOptions? toolbarOptions;
|
|
|
|
/// {@macro flutter.widgets.editableText.showCursor}
|
|
final bool? showCursor;
|
|
|
|
/// If [maxLength] is set to this value, only the "current input length"
|
|
/// part of the character counter is shown.
|
|
static const int noMaxLength = -1;
|
|
|
|
/// The maximum number of characters (Unicode grapheme clusters) to allow in
|
|
/// the text field.
|
|
///
|
|
/// If set, a character counter will be displayed below the
|
|
/// field showing how many characters have been entered. If set to a number
|
|
/// greater than 0, it will also display the maximum number allowed. If set
|
|
/// to [TextField.noMaxLength] then only the current character count is displayed.
|
|
///
|
|
/// After [maxLength] characters have been input, additional input
|
|
/// is ignored, unless [maxLengthEnforcement] is set to
|
|
/// [MaxLengthEnforcement.none].
|
|
///
|
|
/// The text field enforces the length with a [LengthLimitingTextInputFormatter],
|
|
/// which is evaluated after the supplied [inputFormatters], if any.
|
|
///
|
|
/// 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.
|
|
///
|
|
/// Whitespace characters (e.g. newline, space, tab) are included in the
|
|
/// character count.
|
|
///
|
|
/// If [maxLengthEnforcement] is [MaxLengthEnforcement.none], then more than
|
|
/// [maxLength] characters may be entered, but the error counter and divider
|
|
/// will switch to the [decoration]'s [InputDecoration.errorStyle] when the
|
|
/// limit is exceeded.
|
|
///
|
|
/// {@macro flutter.services.lengthLimitingTextInputFormatter.maxLength}
|
|
final int? maxLength;
|
|
|
|
/// Determines how the [maxLength] limit should be enforced.
|
|
///
|
|
/// {@macro flutter.services.textFormatter.effectiveMaxLengthEnforcement}
|
|
///
|
|
/// {@macro flutter.services.textFormatter.maxLengthEnforcement}
|
|
final MaxLengthEnforcement? maxLengthEnforcement;
|
|
|
|
/// {@macro flutter.widgets.editableText.onChanged}
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [inputFormatters], which are called before [onChanged]
|
|
/// runs and can validate and change ("format") the input value.
|
|
/// * [onEditingComplete], [onSubmitted]:
|
|
/// which are more specialized input change notifications.
|
|
final ValueChanged<String>? onChanged;
|
|
|
|
/// {@macro flutter.widgets.editableText.onEditingComplete}
|
|
final VoidCallback? onEditingComplete;
|
|
|
|
/// {@macro flutter.widgets.editableText.onSubmitted}
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [TextInputAction.next] and [TextInputAction.previous], which
|
|
/// automatically shift the focus to the next/previous focusable item when
|
|
/// the user is done editing.
|
|
final ValueChanged<String>? onSubmitted;
|
|
|
|
/// {@macro flutter.widgets.editableText.onAppPrivateCommand}
|
|
final AppPrivateCommandCallback? onAppPrivateCommand;
|
|
|
|
/// {@macro flutter.widgets.editableText.inputFormatters}
|
|
final List<TextInputFormatter>? inputFormatters;
|
|
|
|
/// If false the text field is "disabled": it ignores taps and its
|
|
/// [decoration] is rendered in grey.
|
|
///
|
|
/// If non-null this property overrides the [decoration]'s
|
|
/// [InputDecoration.enabled] property.
|
|
final bool? enabled;
|
|
|
|
/// Determines whether this widget ignores pointer events.
|
|
///
|
|
/// Defaults to null, and when null, does nothing.
|
|
final bool? ignorePointers;
|
|
|
|
/// {@macro flutter.widgets.editableText.cursorWidth}
|
|
final double cursorWidth;
|
|
|
|
/// {@macro flutter.widgets.editableText.cursorHeight}
|
|
final double? cursorHeight;
|
|
|
|
/// {@macro flutter.widgets.editableText.cursorRadius}
|
|
final Radius? cursorRadius;
|
|
|
|
/// {@macro flutter.widgets.editableText.cursorOpacityAnimates}
|
|
final bool? cursorOpacityAnimates;
|
|
|
|
/// The color of the cursor.
|
|
///
|
|
/// The cursor indicates the current location of text insertion point in
|
|
/// the field.
|
|
///
|
|
/// If this is null it will default to the ambient
|
|
/// [DefaultSelectionStyle.cursorColor]. If that is null, and the
|
|
/// [ThemeData.platform] is [TargetPlatform.iOS] or [TargetPlatform.macOS]
|
|
/// it will use [CupertinoThemeData.primaryColor]. Otherwise it will use
|
|
/// the value of [ColorScheme.primary] of [ThemeData.colorScheme].
|
|
final Color? cursorColor;
|
|
|
|
/// The color of the cursor when the [InputDecorator] is showing an error.
|
|
///
|
|
/// If this is null it will default to [TextStyle.color] of
|
|
/// [InputDecoration.errorStyle]. If that is null, it will use
|
|
/// [ColorScheme.error] of [ThemeData.colorScheme].
|
|
final Color? cursorErrorColor;
|
|
|
|
/// 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;
|
|
|
|
/// The appearance of the keyboard.
|
|
///
|
|
/// This setting is only honored on iOS devices.
|
|
///
|
|
/// If unset, defaults to [ThemeData.brightness].
|
|
final Brightness? keyboardAppearance;
|
|
|
|
/// {@macro flutter.widgets.editableText.scrollPadding}
|
|
final EdgeInsets scrollPadding;
|
|
|
|
/// {@macro flutter.widgets.editableText.enableInteractiveSelection}
|
|
final bool enableInteractiveSelection;
|
|
|
|
/// {@macro flutter.widgets.editableText.selectionControls}
|
|
final TextSelectionControls? selectionControls;
|
|
|
|
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
|
|
final DragStartBehavior dragStartBehavior;
|
|
|
|
/// {@macro flutter.widgets.editableText.selectionEnabled}
|
|
bool get selectionEnabled => enableInteractiveSelection;
|
|
|
|
/// {@template flutter.material.textfield.onTap}
|
|
/// Called for the first tap in a series of taps.
|
|
///
|
|
/// The text field builds a [GestureDetector] to handle input events like tap,
|
|
/// to trigger focus requests, to move the caret, adjust the selection, etc.
|
|
/// Handling some of those events by wrapping the text field with a competing
|
|
/// GestureDetector is problematic.
|
|
///
|
|
/// To unconditionally handle taps, without interfering with the text field's
|
|
/// internal gesture detector, provide this callback.
|
|
///
|
|
/// If the text field is created with [enabled] false, taps will not be
|
|
/// recognized.
|
|
///
|
|
/// To be notified when the text field gains or loses the focus, provide a
|
|
/// [focusNode] and add a listener to that.
|
|
///
|
|
/// To listen to arbitrary pointer events without competing with the
|
|
/// text field's internal gesture detector, use a [Listener].
|
|
/// {@endtemplate}
|
|
///
|
|
/// If [onTapAlwaysCalled] is enabled, this will also be called for consecutive
|
|
/// taps.
|
|
final GestureTapCallback? onTap;
|
|
|
|
/// Whether [onTap] should be called for every tap.
|
|
///
|
|
/// Defaults to false, so [onTap] is only called for each distinct tap. When
|
|
/// enabled, [onTap] is called for every tap including consecutive taps.
|
|
final bool onTapAlwaysCalled;
|
|
|
|
/// {@macro flutter.widgets.editableText.onTapOutside}
|
|
///
|
|
/// {@tool dartpad}
|
|
/// This example shows how to use a `TextFieldTapRegion` to wrap a set of
|
|
/// "spinner" buttons that increment and decrement a value in the [TextField]
|
|
/// without causing the text field to lose keyboard focus.
|
|
///
|
|
/// This example includes a generic `SpinnerField<T>` class that you can copy
|
|
/// into your own project and customize.
|
|
///
|
|
/// ** See code in examples/api/lib/widgets/tap_region/text_field_tap_region.0.dart **
|
|
/// {@end-tool}
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [TapRegion] for how the region group is determined.
|
|
final TapRegionCallback? onTapOutside;
|
|
|
|
/// The cursor for a mouse pointer when it enters or is hovering over the
|
|
/// widget.
|
|
///
|
|
/// If [mouseCursor] is a [MaterialStateProperty<MouseCursor>],
|
|
/// [MaterialStateProperty.resolve] is used for the following [MaterialState]s:
|
|
///
|
|
/// * [MaterialState.error].
|
|
/// * [MaterialState.hovered].
|
|
/// * [MaterialState.focused].
|
|
/// * [MaterialState.disabled].
|
|
///
|
|
/// If this property is null, [MaterialStateMouseCursor.textable] will be used.
|
|
///
|
|
/// The [mouseCursor] is the only property of [TextField] that controls the
|
|
/// appearance of the mouse pointer. All other properties related to "cursor"
|
|
/// stand for the text cursor, which is usually a blinking vertical line at
|
|
/// the editing position.
|
|
final MouseCursor? mouseCursor;
|
|
|
|
/// Callback that generates a custom [InputDecoration.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 [InputDecoration.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 [Text.semanticsLabel] property.
|
|
///
|
|
/// {@tool snippet}
|
|
/// ```dart
|
|
/// Widget counter(
|
|
/// BuildContext context,
|
|
/// {
|
|
/// required int currentLength,
|
|
/// required int? maxLength,
|
|
/// required bool isFocused,
|
|
/// }
|
|
/// ) {
|
|
/// return Text(
|
|
/// '$currentLength of $maxLength characters',
|
|
/// semanticsLabel: 'character count',
|
|
/// );
|
|
/// }
|
|
/// ```
|
|
/// {@end-tool}
|
|
///
|
|
/// If buildCounter returns null, then no counter and no Semantics widget will
|
|
/// be created at all.
|
|
final InputCounterWidgetBuilder? buildCounter;
|
|
|
|
/// {@macro flutter.widgets.editableText.scrollPhysics}
|
|
final ScrollPhysics? scrollPhysics;
|
|
|
|
/// {@macro flutter.widgets.editableText.scrollController}
|
|
final ScrollController? scrollController;
|
|
|
|
/// {@macro flutter.widgets.editableText.autofillHints}
|
|
/// {@macro flutter.services.AutofillConfiguration.autofillHints}
|
|
final Iterable<String>? autofillHints;
|
|
|
|
/// {@macro flutter.material.Material.clipBehavior}
|
|
///
|
|
/// Defaults to [Clip.hardEdge].
|
|
final Clip clipBehavior;
|
|
|
|
/// {@template flutter.material.textfield.restorationId}
|
|
/// Restoration ID to save and restore the state of the text field.
|
|
///
|
|
/// If non-null, the text field will persist and restore its current scroll
|
|
/// offset and - if no [controller] has been provided - the content of the
|
|
/// text field. If a [controller] has been provided, it is the responsibility
|
|
/// of the owner of that controller to persist and restore it, e.g. by using
|
|
/// a [RestorableTextEditingController].
|
|
///
|
|
/// The state of this widget is persisted in a [RestorationBucket] claimed
|
|
/// from the surrounding [RestorationScope] using the provided restoration ID.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [RestorationManager], which explains how state restoration works in
|
|
/// Flutter.
|
|
/// {@endtemplate}
|
|
final String? restorationId;
|
|
|
|
/// {@macro flutter.widgets.editableText.scribbleEnabled}
|
|
final bool scribbleEnabled;
|
|
|
|
/// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning}
|
|
final bool enableIMEPersonalizedLearning;
|
|
|
|
/// {@macro flutter.widgets.editableText.contentInsertionConfiguration}
|
|
final ContentInsertionConfiguration? contentInsertionConfiguration;
|
|
|
|
/// {@macro flutter.widgets.EditableText.contextMenuBuilder}
|
|
///
|
|
/// If not provided, will build a default menu based on the platform.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [AdaptiveTextSelectionToolbar], which is built by default.
|
|
final EditableTextContextMenuBuilder? contextMenuBuilder;
|
|
|
|
/// Determine whether this text field can request the primary focus.
|
|
///
|
|
/// Defaults to true. If false, the text field will not request focus
|
|
/// when tapped, or when its context menu is displayed. If false it will not
|
|
/// be possible to move the focus to the text field with tab key.
|
|
final bool canRequestFocus;
|
|
|
|
/// {@macro flutter.widgets.undoHistory.controller}
|
|
final UndoHistoryController? undoController;
|
|
|
|
static Widget _defaultContextMenuBuilder(BuildContext context, EditableTextState editableTextState) {
|
|
return AdaptiveTextSelectionToolbar.editableText(
|
|
editableTextState: editableTextState,
|
|
);
|
|
}
|
|
|
|
/// {@macro flutter.widgets.EditableText.spellCheckConfiguration}
|
|
///
|
|
/// If [SpellCheckConfiguration.misspelledTextStyle] is not specified in this
|
|
/// configuration, then [materialMisspelledTextStyle] is used by default.
|
|
final SpellCheckConfiguration? spellCheckConfiguration;
|
|
|
|
/// The [TextStyle] used to indicate misspelled words in the Material style.
|
|
///
|
|
/// See also:
|
|
/// * [SpellCheckConfiguration.misspelledTextStyle], the style configured to
|
|
/// mark misspelled words with.
|
|
/// * [CupertinoTextField.cupertinoMisspelledTextStyle], the style configured
|
|
/// to mark misspelled words with in the Cupertino style.
|
|
static const TextStyle materialMisspelledTextStyle =
|
|
TextStyle(
|
|
decoration: TextDecoration.underline,
|
|
decorationColor: Colors.red,
|
|
decorationStyle: TextDecorationStyle.wavy,
|
|
);
|
|
|
|
/// Default builder for [TextField]'s spell check suggestions toolbar.
|
|
///
|
|
/// On Apple platforms, builds an iOS-style toolbar. Everywhere else, builds
|
|
/// an Android-style toolbar.
|
|
///
|
|
/// See also:
|
|
/// * [spellCheckConfiguration], where this is typically specified for
|
|
/// [TextField].
|
|
/// * [SpellCheckConfiguration.spellCheckSuggestionsToolbarBuilder], the
|
|
/// parameter for which this is the default value for [TextField].
|
|
/// * [CupertinoTextField.defaultSpellCheckSuggestionsToolbarBuilder], which
|
|
/// is like this but specifies the default for [CupertinoTextField].
|
|
@visibleForTesting
|
|
static Widget defaultSpellCheckSuggestionsToolbarBuilder(
|
|
BuildContext context,
|
|
EditableTextState editableTextState,
|
|
) {
|
|
switch (defaultTargetPlatform) {
|
|
case TargetPlatform.iOS:
|
|
case TargetPlatform.macOS:
|
|
return CupertinoSpellCheckSuggestionsToolbar.editableText(
|
|
editableTextState: editableTextState,
|
|
);
|
|
case TargetPlatform.android:
|
|
case TargetPlatform.fuchsia:
|
|
case TargetPlatform.linux:
|
|
case TargetPlatform.windows:
|
|
return SpellCheckSuggestionsToolbar.editableText(
|
|
editableTextState: editableTextState,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Returns a new [SpellCheckConfiguration] where the given configuration has
|
|
/// had any missing values replaced with their defaults for the Android
|
|
/// platform.
|
|
static SpellCheckConfiguration inferAndroidSpellCheckConfiguration(
|
|
SpellCheckConfiguration? configuration,
|
|
) {
|
|
if (configuration == null
|
|
|| configuration == const SpellCheckConfiguration.disabled()) {
|
|
return const SpellCheckConfiguration.disabled();
|
|
}
|
|
return configuration.copyWith(
|
|
misspelledTextStyle: configuration.misspelledTextStyle
|
|
?? TextField.materialMisspelledTextStyle,
|
|
spellCheckSuggestionsToolbarBuilder:
|
|
configuration.spellCheckSuggestionsToolbarBuilder
|
|
?? TextField.defaultSpellCheckSuggestionsToolbarBuilder,
|
|
);
|
|
}
|
|
|
|
@override
|
|
State<TextField> createState() => _TextFieldState();
|
|
|
|
@override
|
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
|
super.debugFillProperties(properties);
|
|
properties.add(DiagnosticsProperty<TextEditingController>('controller', controller, defaultValue: null));
|
|
properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode, defaultValue: null));
|
|
properties.add(DiagnosticsProperty<UndoHistoryController>('undoController', undoController, defaultValue: null));
|
|
properties.add(DiagnosticsProperty<bool>('enabled', enabled, defaultValue: null));
|
|
properties.add(DiagnosticsProperty<InputDecoration>('decoration', decoration, defaultValue: const InputDecoration()));
|
|
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<String>('obscuringCharacter', obscuringCharacter, defaultValue: '•'));
|
|
properties.add(DiagnosticsProperty<bool>('obscureText', obscureText, defaultValue: false));
|
|
properties.add(DiagnosticsProperty<bool>('autocorrect', autocorrect, defaultValue: true));
|
|
properties.add(EnumProperty<SmartDashesType>('smartDashesType', smartDashesType, defaultValue: obscureText ? SmartDashesType.disabled : SmartDashesType.enabled));
|
|
properties.add(EnumProperty<SmartQuotesType>('smartQuotesType', smartQuotesType, defaultValue: obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled));
|
|
properties.add(DiagnosticsProperty<bool>('enableSuggestions', enableSuggestions, defaultValue: true));
|
|
properties.add(IntProperty('maxLines', maxLines, defaultValue: 1));
|
|
properties.add(IntProperty('minLines', minLines, defaultValue: null));
|
|
properties.add(DiagnosticsProperty<bool>('expands', expands, defaultValue: false));
|
|
properties.add(IntProperty('maxLength', maxLength, defaultValue: null));
|
|
properties.add(EnumProperty<MaxLengthEnforcement>('maxLengthEnforcement', maxLengthEnforcement, defaultValue: null));
|
|
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));
|
|
properties.add(DiagnosticsProperty<TextAlignVertical>('textAlignVertical', textAlignVertical, defaultValue: null));
|
|
properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
|
|
properties.add(DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0));
|
|
properties.add(DoubleProperty('cursorHeight', cursorHeight, defaultValue: null));
|
|
properties.add(DiagnosticsProperty<Radius>('cursorRadius', cursorRadius, defaultValue: null));
|
|
properties.add(DiagnosticsProperty<bool>('cursorOpacityAnimates', cursorOpacityAnimates, defaultValue: null));
|
|
properties.add(ColorProperty('cursorColor', cursorColor, defaultValue: null));
|
|
properties.add(ColorProperty('cursorErrorColor', cursorErrorColor, defaultValue: null));
|
|
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'));
|
|
properties.add(DiagnosticsProperty<TextSelectionControls>('selectionControls', selectionControls, defaultValue: null));
|
|
properties.add(DiagnosticsProperty<ScrollController>('scrollController', scrollController, defaultValue: null));
|
|
properties.add(DiagnosticsProperty<ScrollPhysics>('scrollPhysics', scrollPhysics, defaultValue: null));
|
|
properties.add(DiagnosticsProperty<Clip>('clipBehavior', clipBehavior, defaultValue: Clip.hardEdge));
|
|
properties.add(DiagnosticsProperty<bool>('scribbleEnabled', scribbleEnabled, defaultValue: true));
|
|
properties.add(DiagnosticsProperty<bool>('enableIMEPersonalizedLearning', enableIMEPersonalizedLearning, defaultValue: true));
|
|
properties.add(DiagnosticsProperty<SpellCheckConfiguration>('spellCheckConfiguration', spellCheckConfiguration, defaultValue: null));
|
|
properties.add(DiagnosticsProperty<List<String>>('contentCommitMimeTypes', contentInsertionConfiguration?.allowedMimeTypes ?? const <String>[], defaultValue: contentInsertionConfiguration == null ? const <String>[] : kDefaultContentInsertionMimeTypes));
|
|
}
|
|
}
|
|
|
|
class _TextFieldState extends State<TextField> with RestorationMixin implements TextSelectionGestureDetectorBuilderDelegate, AutofillClient {
|
|
RestorableTextEditingController? _controller;
|
|
TextEditingController get _effectiveController => widget.controller ?? _controller!.value;
|
|
|
|
FocusNode? _focusNode;
|
|
FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode());
|
|
|
|
MaxLengthEnforcement get _effectiveMaxLengthEnforcement => widget.maxLengthEnforcement
|
|
?? LengthLimitingTextInputFormatter.getDefaultMaxLengthEnforcement(Theme.of(context).platform);
|
|
|
|
bool _isHovering = false;
|
|
|
|
bool get needsCounter => widget.maxLength != null
|
|
&& widget.decoration != null
|
|
&& widget.decoration!.counterText == null;
|
|
|
|
bool _showSelectionHandles = false;
|
|
|
|
late _TextFieldSelectionGestureDetectorBuilder _selectionGestureDetectorBuilder;
|
|
|
|
// API for TextSelectionGestureDetectorBuilderDelegate.
|
|
@override
|
|
late bool forcePressEnabled;
|
|
|
|
@override
|
|
final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>();
|
|
|
|
@override
|
|
bool get selectionEnabled => widget.selectionEnabled && _isEnabled;
|
|
// End of API for TextSelectionGestureDetectorBuilderDelegate.
|
|
|
|
bool get _isEnabled => widget.enabled ?? widget.decoration?.enabled ?? true;
|
|
|
|
int get _currentLength => _effectiveController.value.text.characters.length;
|
|
|
|
bool get _hasIntrinsicError => widget.maxLength != null &&
|
|
widget.maxLength! > 0 &&
|
|
(widget.controller == null ?
|
|
!restorePending && _effectiveController.value.text.characters.length > widget.maxLength! :
|
|
_effectiveController.value.text.characters.length > widget.maxLength!);
|
|
|
|
bool get _hasError => widget.decoration?.errorText != null || widget.decoration?.error != null || _hasIntrinsicError;
|
|
|
|
Color get _errorColor => widget.cursorErrorColor ?? _getEffectiveDecoration().errorStyle?.color ?? Theme.of(context).colorScheme.error;
|
|
|
|
InputDecoration _getEffectiveDecoration() {
|
|
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
|
|
final ThemeData themeData = Theme.of(context);
|
|
final InputDecoration effectiveDecoration = (widget.decoration ?? const InputDecoration())
|
|
.applyDefaults(themeData.inputDecorationTheme)
|
|
.copyWith(
|
|
enabled: _isEnabled,
|
|
hintMaxLines: widget.decoration?.hintMaxLines ?? widget.maxLines,
|
|
);
|
|
|
|
// No need to build anything if counter or counterText were given directly.
|
|
if (effectiveDecoration.counter != null || effectiveDecoration.counterText != null) {
|
|
return effectiveDecoration;
|
|
}
|
|
|
|
// If buildCounter was provided, use it to generate a counter widget.
|
|
Widget? counter;
|
|
final int currentLength = _currentLength;
|
|
if (effectiveDecoration.counter == null
|
|
&& effectiveDecoration.counterText == null
|
|
&& widget.buildCounter != null) {
|
|
final bool isFocused = _effectiveFocusNode.hasFocus;
|
|
final Widget? builtCounter = widget.buildCounter!(
|
|
context,
|
|
currentLength: currentLength,
|
|
maxLength: widget.maxLength,
|
|
isFocused: isFocused,
|
|
);
|
|
// If buildCounter returns null, don't add a counter widget to the field.
|
|
if (builtCounter != null) {
|
|
counter = Semantics(
|
|
container: true,
|
|
liveRegion: isFocused,
|
|
child: builtCounter,
|
|
);
|
|
}
|
|
return effectiveDecoration.copyWith(counter: counter);
|
|
}
|
|
|
|
if (widget.maxLength == null) {
|
|
return effectiveDecoration;
|
|
} // No counter widget
|
|
|
|
String counterText = '$currentLength';
|
|
String semanticCounterText = '';
|
|
|
|
// Handle a real maxLength (positive number)
|
|
if (widget.maxLength! > 0) {
|
|
// Show the maxLength in the counter
|
|
counterText += '/${widget.maxLength}';
|
|
final int remaining = (widget.maxLength! - currentLength).clamp(0, widget.maxLength!);
|
|
semanticCounterText = localizations.remainingTextFieldCharacterCount(remaining);
|
|
}
|
|
|
|
if (_hasIntrinsicError) {
|
|
return effectiveDecoration.copyWith(
|
|
errorText: effectiveDecoration.errorText ?? '',
|
|
counterStyle: effectiveDecoration.errorStyle
|
|
?? (themeData.useMaterial3 ? _m3CounterErrorStyle(context): _m2CounterErrorStyle(context)),
|
|
counterText: counterText,
|
|
semanticCounterText: semanticCounterText,
|
|
);
|
|
}
|
|
|
|
return effectiveDecoration.copyWith(
|
|
counterText: counterText,
|
|
semanticCounterText: semanticCounterText,
|
|
);
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_selectionGestureDetectorBuilder = _TextFieldSelectionGestureDetectorBuilder(state: this);
|
|
if (widget.controller == null) {
|
|
_createLocalController();
|
|
}
|
|
_effectiveFocusNode.canRequestFocus = widget.canRequestFocus && _isEnabled;
|
|
_effectiveFocusNode.addListener(_handleFocusChanged);
|
|
_initStatesController();
|
|
}
|
|
|
|
bool get _canRequestFocus {
|
|
final NavigationMode mode = MediaQuery.maybeNavigationModeOf(context) ?? NavigationMode.traditional;
|
|
switch (mode) {
|
|
case NavigationMode.traditional:
|
|
return widget.canRequestFocus && _isEnabled;
|
|
case NavigationMode.directional:
|
|
return true;
|
|
}
|
|
}
|
|
|
|
@override
|
|
void didChangeDependencies() {
|
|
super.didChangeDependencies();
|
|
_effectiveFocusNode.canRequestFocus = _canRequestFocus;
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(TextField oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
if (widget.controller == null && oldWidget.controller != null) {
|
|
_createLocalController(oldWidget.controller!.value);
|
|
} else if (widget.controller != null && oldWidget.controller == null) {
|
|
unregisterFromRestoration(_controller!);
|
|
_controller!.dispose();
|
|
_controller = null;
|
|
}
|
|
|
|
if (widget.focusNode != oldWidget.focusNode) {
|
|
(oldWidget.focusNode ?? _focusNode)?.removeListener(_handleFocusChanged);
|
|
(widget.focusNode ?? _focusNode)?.addListener(_handleFocusChanged);
|
|
}
|
|
|
|
_effectiveFocusNode.canRequestFocus = _canRequestFocus;
|
|
|
|
if (_effectiveFocusNode.hasFocus && widget.readOnly != oldWidget.readOnly && _isEnabled) {
|
|
if (_effectiveController.selection.isCollapsed) {
|
|
_showSelectionHandles = !widget.readOnly;
|
|
}
|
|
}
|
|
|
|
if (widget.statesController == oldWidget.statesController) {
|
|
_statesController.update(MaterialState.disabled, !_isEnabled);
|
|
_statesController.update(MaterialState.hovered, _isHovering);
|
|
_statesController.update(MaterialState.focused, _effectiveFocusNode.hasFocus);
|
|
_statesController.update(MaterialState.error, _hasError);
|
|
} else {
|
|
oldWidget.statesController?.removeListener(_handleStatesControllerChange);
|
|
if (widget.statesController != null) {
|
|
_internalStatesController?.dispose();
|
|
_internalStatesController = null;
|
|
}
|
|
_initStatesController();
|
|
}
|
|
}
|
|
|
|
@override
|
|
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
|
|
if (_controller != null) {
|
|
_registerController();
|
|
}
|
|
}
|
|
|
|
void _registerController() {
|
|
assert(_controller != null);
|
|
registerForRestoration(_controller!, 'controller');
|
|
}
|
|
|
|
void _createLocalController([TextEditingValue? value]) {
|
|
assert(_controller == null);
|
|
_controller = value == null
|
|
? RestorableTextEditingController()
|
|
: RestorableTextEditingController.fromValue(value);
|
|
if (!restorePending) {
|
|
_registerController();
|
|
}
|
|
}
|
|
|
|
@override
|
|
String? get restorationId => widget.restorationId;
|
|
|
|
@override
|
|
void dispose() {
|
|
_effectiveFocusNode.removeListener(_handleFocusChanged);
|
|
_focusNode?.dispose();
|
|
_controller?.dispose();
|
|
_statesController.removeListener(_handleStatesControllerChange);
|
|
_internalStatesController?.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
EditableTextState? get _editableText => editableTextKey.currentState;
|
|
|
|
void _requestKeyboard() {
|
|
_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.
|
|
if (!_selectionGestureDetectorBuilder.shouldShowSelectionToolbar) {
|
|
return false;
|
|
}
|
|
|
|
if (cause == SelectionChangedCause.keyboard) {
|
|
return false;
|
|
}
|
|
|
|
if (widget.readOnly && _effectiveController.selection.isCollapsed) {
|
|
return false;
|
|
}
|
|
|
|
if (!_isEnabled) {
|
|
return false;
|
|
}
|
|
|
|
if (cause == SelectionChangedCause.longPress || cause == SelectionChangedCause.scribble) {
|
|
return true;
|
|
}
|
|
|
|
if (_effectiveController.text.isNotEmpty) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void _handleFocusChanged() {
|
|
setState(() {
|
|
// Rebuild the widget on focus change to show/hide the text selection
|
|
// highlight.
|
|
});
|
|
_statesController.update(MaterialState.focused, _effectiveFocusNode.hasFocus);
|
|
}
|
|
|
|
void _handleSelectionChanged(TextSelection selection, SelectionChangedCause? cause) {
|
|
final bool willShowSelectionHandles = _shouldShowSelectionHandles(cause);
|
|
if (willShowSelectionHandles != _showSelectionHandles) {
|
|
setState(() {
|
|
_showSelectionHandles = willShowSelectionHandles;
|
|
});
|
|
}
|
|
|
|
switch (Theme.of(context).platform) {
|
|
case TargetPlatform.iOS:
|
|
case TargetPlatform.macOS:
|
|
case TargetPlatform.linux:
|
|
case TargetPlatform.windows:
|
|
case TargetPlatform.fuchsia:
|
|
case TargetPlatform.android:
|
|
if (cause == SelectionChangedCause.longPress) {
|
|
_editableText?.bringIntoView(selection.extent);
|
|
}
|
|
}
|
|
|
|
switch (Theme.of(context).platform) {
|
|
case TargetPlatform.iOS:
|
|
case TargetPlatform.fuchsia:
|
|
case TargetPlatform.android:
|
|
break;
|
|
case TargetPlatform.macOS:
|
|
case TargetPlatform.linux:
|
|
case TargetPlatform.windows:
|
|
if (cause == SelectionChangedCause.drag) {
|
|
_editableText?.hideToolbar();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Toggle the toolbar when a selection handle is tapped.
|
|
void _handleSelectionHandleTapped() {
|
|
if (_effectiveController.selection.isCollapsed) {
|
|
_editableText!.toggleToolbar();
|
|
}
|
|
}
|
|
|
|
void _handleHover(bool hovering) {
|
|
if (hovering != _isHovering) {
|
|
setState(() {
|
|
_isHovering = hovering;
|
|
});
|
|
_statesController.update(MaterialState.hovered, _isHovering);
|
|
}
|
|
}
|
|
|
|
// Material states controller.
|
|
MaterialStatesController? _internalStatesController;
|
|
|
|
void _handleStatesControllerChange() {
|
|
// Force a rebuild to resolve MaterialStateProperty properties.
|
|
setState(() { });
|
|
}
|
|
|
|
MaterialStatesController get _statesController => widget.statesController ?? _internalStatesController!;
|
|
|
|
void _initStatesController() {
|
|
if (widget.statesController == null) {
|
|
_internalStatesController = MaterialStatesController();
|
|
}
|
|
_statesController.update(MaterialState.disabled, !_isEnabled);
|
|
_statesController.update(MaterialState.hovered, _isHovering);
|
|
_statesController.update(MaterialState.focused, _effectiveFocusNode.hasFocus);
|
|
_statesController.update(MaterialState.error, _hasError);
|
|
_statesController.addListener(_handleStatesControllerChange);
|
|
}
|
|
|
|
// AutofillClient implementation start.
|
|
@override
|
|
String get autofillId => _editableText!.autofillId;
|
|
|
|
@override
|
|
void autofill(TextEditingValue newEditingValue) => _editableText!.autofill(newEditingValue);
|
|
|
|
@override
|
|
TextInputConfiguration get textInputConfiguration {
|
|
final List<String>? autofillHints = widget.autofillHints?.toList(growable: false);
|
|
final AutofillConfiguration autofillConfiguration = autofillHints != null
|
|
? AutofillConfiguration(
|
|
uniqueIdentifier: autofillId,
|
|
autofillHints: autofillHints,
|
|
currentEditingValue: _effectiveController.value,
|
|
hintText: (widget.decoration ?? const InputDecoration()).hintText,
|
|
)
|
|
: AutofillConfiguration.disabled;
|
|
|
|
return _editableText!.textInputConfiguration.copyWith(autofillConfiguration: autofillConfiguration);
|
|
}
|
|
// AutofillClient implementation end.
|
|
|
|
TextStyle _getInputStyleForState(TextStyle style) {
|
|
final ThemeData theme = Theme.of(context);
|
|
final TextStyle stateStyle = MaterialStateProperty.resolveAs(theme.useMaterial3 ? _m3StateInputStyle(context)! : _m2StateInputStyle(context)!, _statesController.value);
|
|
final TextStyle providedStyle = MaterialStateProperty.resolveAs(style, _statesController.value);
|
|
return providedStyle.merge(stateStyle);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
assert(debugCheckHasMaterial(context));
|
|
assert(debugCheckHasMaterialLocalizations(context));
|
|
assert(debugCheckHasDirectionality(context));
|
|
assert(
|
|
!(widget.style != null && !widget.style!.inherit &&
|
|
(widget.style!.fontSize == null || widget.style!.textBaseline == null)),
|
|
'inherit false style must supply fontSize and textBaseline',
|
|
);
|
|
|
|
final ThemeData theme = Theme.of(context);
|
|
final DefaultSelectionStyle selectionStyle = DefaultSelectionStyle.of(context);
|
|
final TextStyle? providedStyle = MaterialStateProperty.resolveAs(widget.style, _statesController.value);
|
|
final TextStyle style = _getInputStyleForState(theme.useMaterial3 ? _m3InputStyle(context) : theme.textTheme.titleMedium!).merge(providedStyle);
|
|
final Brightness keyboardAppearance = widget.keyboardAppearance ?? theme.brightness;
|
|
final TextEditingController controller = _effectiveController;
|
|
final FocusNode focusNode = _effectiveFocusNode;
|
|
final List<TextInputFormatter> formatters = <TextInputFormatter>[
|
|
...?widget.inputFormatters,
|
|
if (widget.maxLength != null)
|
|
LengthLimitingTextInputFormatter(
|
|
widget.maxLength,
|
|
maxLengthEnforcement: _effectiveMaxLengthEnforcement,
|
|
),
|
|
];
|
|
|
|
// Set configuration as disabled if not otherwise specified. If specified,
|
|
// ensure that configuration uses the correct style for misspelled words for
|
|
// the current platform, unless a custom style is specified.
|
|
final SpellCheckConfiguration spellCheckConfiguration;
|
|
switch (defaultTargetPlatform) {
|
|
case TargetPlatform.iOS:
|
|
case TargetPlatform.macOS:
|
|
spellCheckConfiguration =
|
|
CupertinoTextField.inferIOSSpellCheckConfiguration(
|
|
widget.spellCheckConfiguration,
|
|
);
|
|
case TargetPlatform.android:
|
|
case TargetPlatform.fuchsia:
|
|
case TargetPlatform.linux:
|
|
case TargetPlatform.windows:
|
|
spellCheckConfiguration = TextField.inferAndroidSpellCheckConfiguration(
|
|
widget.spellCheckConfiguration,
|
|
);
|
|
}
|
|
|
|
TextSelectionControls? textSelectionControls = widget.selectionControls;
|
|
final bool paintCursorAboveText;
|
|
bool? cursorOpacityAnimates = widget.cursorOpacityAnimates;
|
|
Offset? cursorOffset;
|
|
final Color cursorColor;
|
|
final Color selectionColor;
|
|
Color? autocorrectionTextRectColor;
|
|
Radius? cursorRadius = widget.cursorRadius;
|
|
VoidCallback? handleDidGainAccessibilityFocus;
|
|
VoidCallback? handleDidLoseAccessibilityFocus;
|
|
|
|
switch (theme.platform) {
|
|
case TargetPlatform.iOS:
|
|
final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context);
|
|
forcePressEnabled = true;
|
|
textSelectionControls ??= cupertinoTextSelectionHandleControls;
|
|
paintCursorAboveText = true;
|
|
cursorOpacityAnimates ??= true;
|
|
cursorColor = _hasError ? _errorColor : widget.cursorColor ?? selectionStyle.cursorColor ?? cupertinoTheme.primaryColor;
|
|
selectionColor = selectionStyle.selectionColor ?? cupertinoTheme.primaryColor.withOpacity(0.40);
|
|
cursorRadius ??= const Radius.circular(2.0);
|
|
cursorOffset = Offset(iOSHorizontalOffset / MediaQuery.devicePixelRatioOf(context), 0);
|
|
autocorrectionTextRectColor = selectionColor;
|
|
|
|
case TargetPlatform.macOS:
|
|
final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context);
|
|
forcePressEnabled = false;
|
|
textSelectionControls ??= cupertinoDesktopTextSelectionHandleControls;
|
|
paintCursorAboveText = true;
|
|
cursorOpacityAnimates ??= false;
|
|
cursorColor = _hasError ? _errorColor : widget.cursorColor ?? selectionStyle.cursorColor ?? cupertinoTheme.primaryColor;
|
|
selectionColor = selectionStyle.selectionColor ?? cupertinoTheme.primaryColor.withOpacity(0.40);
|
|
cursorRadius ??= const Radius.circular(2.0);
|
|
cursorOffset = Offset(iOSHorizontalOffset / MediaQuery.devicePixelRatioOf(context), 0);
|
|
handleDidGainAccessibilityFocus = () {
|
|
// Automatically activate the TextField when it receives accessibility focus.
|
|
if (!_effectiveFocusNode.hasFocus && _effectiveFocusNode.canRequestFocus) {
|
|
_effectiveFocusNode.requestFocus();
|
|
}
|
|
};
|
|
handleDidLoseAccessibilityFocus = () {
|
|
_effectiveFocusNode.unfocus();
|
|
};
|
|
|
|
case TargetPlatform.android:
|
|
case TargetPlatform.fuchsia:
|
|
forcePressEnabled = false;
|
|
textSelectionControls ??= materialTextSelectionHandleControls;
|
|
paintCursorAboveText = false;
|
|
cursorOpacityAnimates ??= false;
|
|
cursorColor = _hasError ? _errorColor : widget.cursorColor ?? selectionStyle.cursorColor ?? theme.colorScheme.primary;
|
|
selectionColor = selectionStyle.selectionColor ?? theme.colorScheme.primary.withOpacity(0.40);
|
|
|
|
case TargetPlatform.linux:
|
|
forcePressEnabled = false;
|
|
textSelectionControls ??= desktopTextSelectionHandleControls;
|
|
paintCursorAboveText = false;
|
|
cursorOpacityAnimates ??= false;
|
|
cursorColor = _hasError ? _errorColor : widget.cursorColor ?? selectionStyle.cursorColor ?? theme.colorScheme.primary;
|
|
selectionColor = selectionStyle.selectionColor ?? theme.colorScheme.primary.withOpacity(0.40);
|
|
handleDidGainAccessibilityFocus = () {
|
|
// Automatically activate the TextField when it receives accessibility focus.
|
|
if (!_effectiveFocusNode.hasFocus && _effectiveFocusNode.canRequestFocus) {
|
|
_effectiveFocusNode.requestFocus();
|
|
}
|
|
};
|
|
handleDidLoseAccessibilityFocus = () {
|
|
_effectiveFocusNode.unfocus();
|
|
};
|
|
|
|
case TargetPlatform.windows:
|
|
forcePressEnabled = false;
|
|
textSelectionControls ??= desktopTextSelectionHandleControls;
|
|
paintCursorAboveText = false;
|
|
cursorOpacityAnimates ??= false;
|
|
cursorColor = _hasError ? _errorColor : widget.cursorColor ?? selectionStyle.cursorColor ?? theme.colorScheme.primary;
|
|
selectionColor = selectionStyle.selectionColor ?? theme.colorScheme.primary.withOpacity(0.40);
|
|
handleDidGainAccessibilityFocus = () {
|
|
// Automatically activate the TextField when it receives accessibility focus.
|
|
if (!_effectiveFocusNode.hasFocus && _effectiveFocusNode.canRequestFocus) {
|
|
_effectiveFocusNode.requestFocus();
|
|
}
|
|
};
|
|
handleDidLoseAccessibilityFocus = () {
|
|
_effectiveFocusNode.unfocus();
|
|
};
|
|
}
|
|
|
|
Widget child = RepaintBoundary(
|
|
child: UnmanagedRestorationScope(
|
|
bucket: bucket,
|
|
child: EditableText(
|
|
key: editableTextKey,
|
|
readOnly: widget.readOnly || !_isEnabled,
|
|
toolbarOptions: widget.toolbarOptions,
|
|
showCursor: widget.showCursor,
|
|
showSelectionHandles: _showSelectionHandles,
|
|
controller: controller,
|
|
focusNode: focusNode,
|
|
undoController: widget.undoController,
|
|
keyboardType: widget.keyboardType,
|
|
textInputAction: widget.textInputAction,
|
|
textCapitalization: widget.textCapitalization,
|
|
style: style,
|
|
strutStyle: widget.strutStyle,
|
|
textAlign: widget.textAlign,
|
|
textDirection: widget.textDirection,
|
|
autofocus: widget.autofocus,
|
|
obscuringCharacter: widget.obscuringCharacter,
|
|
obscureText: widget.obscureText,
|
|
autocorrect: widget.autocorrect,
|
|
smartDashesType: widget.smartDashesType,
|
|
smartQuotesType: widget.smartQuotesType,
|
|
enableSuggestions: widget.enableSuggestions,
|
|
maxLines: widget.maxLines,
|
|
minLines: widget.minLines,
|
|
expands: widget.expands,
|
|
// Only show the selection highlight when the text field is focused.
|
|
selectionColor: focusNode.hasFocus ? selectionColor : null,
|
|
selectionControls: widget.selectionEnabled ? textSelectionControls : null,
|
|
onChanged: widget.onChanged,
|
|
onSelectionChanged: _handleSelectionChanged,
|
|
onEditingComplete: widget.onEditingComplete,
|
|
onSubmitted: widget.onSubmitted,
|
|
onAppPrivateCommand: widget.onAppPrivateCommand,
|
|
onSelectionHandleTapped: _handleSelectionHandleTapped,
|
|
onTapOutside: widget.onTapOutside,
|
|
inputFormatters: formatters,
|
|
rendererIgnoresPointer: true,
|
|
mouseCursor: MouseCursor.defer, // TextField will handle the cursor
|
|
cursorWidth: widget.cursorWidth,
|
|
cursorHeight: widget.cursorHeight,
|
|
cursorRadius: cursorRadius,
|
|
cursorColor: cursorColor,
|
|
selectionHeightStyle: widget.selectionHeightStyle,
|
|
selectionWidthStyle: widget.selectionWidthStyle,
|
|
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,
|
|
autofillClient: this,
|
|
autocorrectionTextRectColor: autocorrectionTextRectColor,
|
|
clipBehavior: widget.clipBehavior,
|
|
restorationId: 'editable',
|
|
scribbleEnabled: widget.scribbleEnabled,
|
|
enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning,
|
|
contentInsertionConfiguration: widget.contentInsertionConfiguration,
|
|
contextMenuBuilder: widget.contextMenuBuilder,
|
|
spellCheckConfiguration: spellCheckConfiguration,
|
|
magnifierConfiguration: widget.magnifierConfiguration ?? TextMagnifier.adaptiveMagnifierConfiguration,
|
|
),
|
|
),
|
|
);
|
|
|
|
if (widget.decoration != null) {
|
|
child = AnimatedBuilder(
|
|
animation: Listenable.merge(<Listenable>[ focusNode, controller ]),
|
|
builder: (BuildContext context, Widget? child) {
|
|
return InputDecorator(
|
|
decoration: _getEffectiveDecoration(),
|
|
baseStyle: widget.style,
|
|
textAlign: widget.textAlign,
|
|
textAlignVertical: widget.textAlignVertical,
|
|
isHovering: _isHovering,
|
|
isFocused: focusNode.hasFocus,
|
|
isEmpty: controller.value.text.isEmpty,
|
|
expands: widget.expands,
|
|
child: child,
|
|
);
|
|
},
|
|
child: child,
|
|
);
|
|
}
|
|
final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor>(
|
|
widget.mouseCursor ?? MaterialStateMouseCursor.textable,
|
|
_statesController.value,
|
|
);
|
|
|
|
final int? semanticsMaxValueLength;
|
|
if (_effectiveMaxLengthEnforcement != MaxLengthEnforcement.none &&
|
|
widget.maxLength != null &&
|
|
widget.maxLength! > 0) {
|
|
semanticsMaxValueLength = widget.maxLength;
|
|
} else {
|
|
semanticsMaxValueLength = null;
|
|
}
|
|
|
|
return MouseRegion(
|
|
cursor: effectiveMouseCursor,
|
|
onEnter: (PointerEnterEvent event) => _handleHover(true),
|
|
onExit: (PointerExitEvent event) => _handleHover(false),
|
|
child: TextFieldTapRegion(
|
|
child: IgnorePointer(
|
|
ignoring: widget.ignorePointers ?? !_isEnabled,
|
|
child: AnimatedBuilder(
|
|
animation: controller, // changes the _currentLength
|
|
builder: (BuildContext context, Widget? child) {
|
|
return Semantics(
|
|
maxValueLength: semanticsMaxValueLength,
|
|
currentValueLength: _currentLength,
|
|
onTap: widget.readOnly ? null : () {
|
|
if (!_effectiveController.selection.isValid) {
|
|
_effectiveController.selection = TextSelection.collapsed(offset: _effectiveController.text.length);
|
|
}
|
|
_requestKeyboard();
|
|
},
|
|
onDidGainAccessibilityFocus: handleDidGainAccessibilityFocus,
|
|
onDidLoseAccessibilityFocus: handleDidLoseAccessibilityFocus,
|
|
child: child,
|
|
);
|
|
},
|
|
child: _selectionGestureDetectorBuilder.buildGestureDetector(
|
|
behavior: HitTestBehavior.translucent,
|
|
child: child,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
TextStyle? _m2StateInputStyle(BuildContext context) => MaterialStateTextStyle.resolveWith((Set<MaterialState> states) {
|
|
final ThemeData theme = Theme.of(context);
|
|
if (states.contains(MaterialState.disabled)) {
|
|
return TextStyle(color: theme.disabledColor);
|
|
}
|
|
return TextStyle(color: theme.textTheme.titleMedium?.color);
|
|
});
|
|
|
|
TextStyle _m2CounterErrorStyle(BuildContext context) =>
|
|
Theme.of(context).textTheme.bodySmall!.copyWith(color: Theme.of(context).colorScheme.error);
|
|
|
|
// BEGIN GENERATED TOKEN PROPERTIES - TextField
|
|
|
|
// Do not edit by hand. The code between the "BEGIN GENERATED" and
|
|
// "END GENERATED" comments are generated from data in the Material
|
|
// Design token database by the script:
|
|
// dev/tools/gen_defaults/bin/gen_defaults.dart.
|
|
|
|
TextStyle? _m3StateInputStyle(BuildContext context) => MaterialStateTextStyle.resolveWith((Set<MaterialState> states) {
|
|
if (states.contains(MaterialState.disabled)) {
|
|
return TextStyle(color: Theme.of(context).textTheme.bodyLarge!.color?.withOpacity(0.38));
|
|
}
|
|
return TextStyle(color: Theme.of(context).textTheme.bodyLarge!.color);
|
|
});
|
|
|
|
TextStyle _m3InputStyle(BuildContext context) => Theme.of(context).textTheme.bodyLarge!;
|
|
|
|
TextStyle _m3CounterErrorStyle(BuildContext context) =>
|
|
Theme.of(context).textTheme.bodySmall!.copyWith(color: Theme.of(context).colorScheme.error);
|
|
|
|
// END GENERATED TOKEN PROPERTIES - TextField
|