mirror of
https://github.com/HChaZZY/PiliPlus.git
synced 2025-12-06 09:13:48 +08:00
1962 lines
73 KiB
Dart
1962 lines
73 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.
|
|
|
|
// ignore_for_file: uri_does_not_exist_in_doc_import
|
|
|
|
/// @docImport 'input_border.dart';
|
|
/// @docImport 'material.dart';
|
|
/// @docImport 'scaffold.dart';
|
|
/// @docImport 'text_form_field.dart';
|
|
/// @docImport 'text_theme.dart';
|
|
library;
|
|
|
|
import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle;
|
|
|
|
import 'package:PiliPlus/common/widgets/text_field/adaptive_text_selection_toolbar.dart';
|
|
import 'package:PiliPlus/common/widgets/text_field/controller.dart';
|
|
import 'package:PiliPlus/common/widgets/text_field/cupertino/cupertino_spell_check_suggestions_toolbar.dart';
|
|
import 'package:PiliPlus/common/widgets/text_field/cupertino/cupertino_text_field.dart';
|
|
import 'package:PiliPlus/common/widgets/text_field/editable_text.dart';
|
|
import 'package:PiliPlus/common/widgets/text_field/spell_check.dart';
|
|
import 'package:PiliPlus/common/widgets/text_field/spell_check_suggestions_toolbar.dart';
|
|
import 'package:PiliPlus/common/widgets/text_field/system_context_menu.dart';
|
|
import 'package:PiliPlus/common/widgets/text_field/text_selection.dart';
|
|
import 'package:flutter/cupertino.dart'
|
|
hide
|
|
EditableText,
|
|
EditableTextState,
|
|
CupertinoSpellCheckSuggestionsToolbar,
|
|
SystemContextMenu,
|
|
SpellCheckConfiguration,
|
|
EditableTextContextMenuBuilder,
|
|
buildTextSpanWithSpellCheckSuggestions,
|
|
CupertinoTextField,
|
|
TextSelectionGestureDetectorBuilderDelegate,
|
|
TextSelectionGestureDetectorBuilder,
|
|
TextSelectionOverlay;
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/gestures.dart';
|
|
import 'package:flutter/material.dart'
|
|
hide
|
|
EditableText,
|
|
EditableTextState,
|
|
SpellCheckSuggestionsToolbar,
|
|
AdaptiveTextSelectionToolbar,
|
|
SystemContextMenu,
|
|
SpellCheckConfiguration,
|
|
EditableTextContextMenuBuilder,
|
|
buildTextSpanWithSpellCheckSuggestions,
|
|
TextSelectionGestureDetectorBuilderDelegate,
|
|
TextSelectionGestureDetectorBuilder,
|
|
TextSelectionOverlay;
|
|
import 'package:flutter/rendering.dart';
|
|
import 'package:flutter/services.dart';
|
|
|
|
export 'package:flutter/services.dart'
|
|
show
|
|
SmartDashesType,
|
|
SmartQuotesType,
|
|
TextCapitalization,
|
|
TextInputAction,
|
|
TextInputType;
|
|
|
|
// Examples can assume:
|
|
// late BuildContext context;
|
|
// late FocusNode myFocusNode;
|
|
|
|
/// Signature for the [RichTextField.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 _RichTextFieldState state})
|
|
: _state = state,
|
|
super(delegate: state);
|
|
|
|
final _RichTextFieldState _state;
|
|
|
|
@override
|
|
bool get onUserTapAlwaysCalled => _state.widget.onTapAlwaysCalled;
|
|
|
|
@override
|
|
void onUserTap() {
|
|
_state.widget.onTap?.call();
|
|
}
|
|
}
|
|
|
|
/// 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 [RichTextField] 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 [RichTextEditingController.dispose] on the [RichTextEditingController]
|
|
/// when it is no longer needed. This will ensure we discard any resources used
|
|
/// by the object.
|
|
///
|
|
/// If this field is part of a scrolling container that lazily constructs its
|
|
/// children, like a [ListView] or a [CustomScrollView], then a [controller]
|
|
/// should be specified. The controller's lifetime should be managed by a
|
|
/// stateful widget ancestor of the scrolling container.
|
|
///
|
|
/// ## Obscured Input
|
|
///
|
|
/// {@tool dartpad}
|
|
/// This example shows how to create a [RichTextField] 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
|
|
/// [RichTextEditingController] using [RichTextEditingController.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 [RichTextField] 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
|
|
/// [RichTextField] to ensure proper scroll coordination for [RichTextField] 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
|
|
/// [RichTextField]. 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://docs.flutter.dev/cookbook/forms/text-input)
|
|
/// * Cookbook: [Handle changes to a text field](https://docs.flutter.dev/cookbook/forms/text-field-changes)
|
|
/// * Cookbook: [Retrieve the value of a text field](https://docs.flutter.dev/cookbook/forms/retrieve-input)
|
|
/// * Cookbook: [Focus and text fields](https://docs.flutter.dev/cookbook/forms/focus)
|
|
class RichTextField 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
|
|
/// [RichTextField.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 RichTextField({
|
|
super.key,
|
|
this.groupId = EditableText,
|
|
required 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.onTapUpOutside,
|
|
this.mouseCursor,
|
|
this.buildCounter,
|
|
this.scrollController,
|
|
this.scrollPhysics,
|
|
this.autofillHints = const <String>[],
|
|
this.contentInsertionConfiguration,
|
|
this.clipBehavior = Clip.hardEdge,
|
|
this.restorationId,
|
|
@Deprecated(
|
|
'Use `stylusHandwritingEnabled` instead. '
|
|
'This feature was deprecated after v3.27.0-0.2.pre.',
|
|
)
|
|
this.scribbleEnabled = true,
|
|
this.stylusHandwritingEnabled =
|
|
EditableText.defaultStylusHandwritingEnabled,
|
|
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 == RichTextField.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);
|
|
|
|
/// The configuration for the magnifier of this text field.
|
|
///
|
|
/// By default, builds a [CupertinoTextMagnifier] on iOS and [TextMagnifier]
|
|
/// on Android, and builds nothing on all other platforms. To suppress the
|
|
/// magnifier, consider passing [TextMagnifierConfiguration.disabled].
|
|
///
|
|
/// {@macro flutter.widgets.magnifier.intro}
|
|
///
|
|
/// {@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;
|
|
|
|
/// {@macro flutter.widgets.editableText.groupId}
|
|
final Object groupId;
|
|
|
|
/// Controls the text being edited.
|
|
///
|
|
/// If null, this widget will create its own [RichTextEditingController].
|
|
final RichTextEditingController 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
|
|
/// [WidgetState]s, including [WidgetState.disabled], [WidgetState.hovered],
|
|
/// [WidgetState.error], and [WidgetState.focused].
|
|
///
|
|
/// Classes based on this one can provide their own
|
|
/// [WidgetStatesController] to which they've added listeners.
|
|
/// They can also update the controller's [WidgetStatesController.value]
|
|
/// however, this may only be done when it's safe to call
|
|
/// [State.setState], like in an event handler.
|
|
///
|
|
/// The controller's [WidgetStatesController.value] represents the set of
|
|
/// states that a widget's visual properties, typically [WidgetStateProperty]
|
|
/// values, are resolved against. It is _not_ the intrinsic state of the widget.
|
|
/// The widget is responsible for ensuring that the controller's
|
|
/// [WidgetStatesController.value] tracks its intrinsic state. For example
|
|
/// one cannot request the keyboard focus for a widget by adding [WidgetState.focused]
|
|
/// to its controller. When the widget gains the or loses the focus it will
|
|
/// [WidgetStatesController.update] its controller's [WidgetStatesController.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 [RichTextField.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, [RichTextField.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 [RichTextField.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 [RichTextField]
|
|
/// 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;
|
|
|
|
/// {@macro flutter.widgets.editableText.onTapUpOutside}
|
|
final TapRegionUpCallback? onTapUpOutside;
|
|
|
|
/// The cursor for a mouse pointer when it enters or is hovering over the
|
|
/// widget.
|
|
///
|
|
/// If [mouseCursor] is a [WidgetStateMouseCursor],
|
|
/// [WidgetStateProperty.resolve] is used for the following [WidgetState]s:
|
|
///
|
|
/// * [WidgetState.error].
|
|
/// * [WidgetState.hovered].
|
|
/// * [WidgetState.focused].
|
|
/// * [WidgetState.disabled].
|
|
///
|
|
/// If this property is null, [WidgetStateMouseCursor.textable] will be used.
|
|
///
|
|
/// The [mouseCursor] is the only property of [RichTextField] 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}
|
|
@Deprecated(
|
|
'Use `stylusHandwritingEnabled` instead. '
|
|
'This feature was deprecated after v3.27.0-0.2.pre.',
|
|
)
|
|
final bool scribbleEnabled;
|
|
|
|
/// {@macro flutter.widgets.editableText.stylusHandwritingEnabled}
|
|
final bool stylusHandwritingEnabled;
|
|
|
|
/// {@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.
|
|
/// * [BrowserContextMenu], which allows the browser's context menu on web to
|
|
/// be disabled and Flutter-rendered context menus to appear.
|
|
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,
|
|
) {
|
|
if (defaultTargetPlatform == TargetPlatform.iOS &&
|
|
SystemContextMenu.isSupported(context)) {
|
|
return SystemContextMenu.editableText(
|
|
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.
|
|
/// * [CupertinoRichTextField.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 [RichTextField]'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
|
|
/// [RichTextField].
|
|
/// * [SpellCheckConfiguration.spellCheckSuggestionsToolbarBuilder], the
|
|
/// parameter for which this is the default value for [RichTextField].
|
|
/// * [CupertinoRichTextField.defaultSpellCheckSuggestionsToolbarBuilder], which
|
|
/// is like this but specifies the default for [CupertinoRichTextField].
|
|
@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 ??
|
|
RichTextField.materialMisspelledTextStyle,
|
|
spellCheckSuggestionsToolbarBuilder:
|
|
configuration.spellCheckSuggestionsToolbarBuilder ??
|
|
RichTextField.defaultSpellCheckSuggestionsToolbarBuilder,
|
|
);
|
|
}
|
|
|
|
@override
|
|
State<RichTextField> createState() => _RichTextFieldState();
|
|
|
|
@override
|
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
|
super.debugFillProperties(properties);
|
|
properties
|
|
..add(
|
|
DiagnosticsProperty<RichTextEditingController>('controller', controller,
|
|
defaultValue: null),
|
|
)
|
|
..add(DiagnosticsProperty<FocusNode>('focusNode', focusNode,
|
|
defaultValue: null))
|
|
..add(
|
|
DiagnosticsProperty<UndoHistoryController>(
|
|
'undoController',
|
|
undoController,
|
|
defaultValue: null,
|
|
),
|
|
)
|
|
..add(DiagnosticsProperty<bool>('enabled', enabled, defaultValue: null))
|
|
..add(
|
|
DiagnosticsProperty<InputDecoration>(
|
|
'decoration',
|
|
decoration,
|
|
defaultValue: const InputDecoration(),
|
|
),
|
|
)
|
|
..add(
|
|
DiagnosticsProperty<TextInputType>(
|
|
'keyboardType',
|
|
keyboardType,
|
|
defaultValue: TextInputType.text,
|
|
),
|
|
)
|
|
..add(DiagnosticsProperty<TextStyle>('style', style, defaultValue: null))
|
|
..add(DiagnosticsProperty<bool>('autofocus', autofocus,
|
|
defaultValue: false))
|
|
..add(
|
|
DiagnosticsProperty<String>('obscuringCharacter', obscuringCharacter,
|
|
defaultValue: '•'),
|
|
)
|
|
..add(DiagnosticsProperty<bool>('obscureText', obscureText,
|
|
defaultValue: false))
|
|
..add(DiagnosticsProperty<bool>('autocorrect', autocorrect,
|
|
defaultValue: true))
|
|
..add(
|
|
EnumProperty<SmartDashesType>(
|
|
'smartDashesType',
|
|
smartDashesType,
|
|
defaultValue:
|
|
obscureText ? SmartDashesType.disabled : SmartDashesType.enabled,
|
|
),
|
|
)
|
|
..add(
|
|
EnumProperty<SmartQuotesType>(
|
|
'smartQuotesType',
|
|
smartQuotesType,
|
|
defaultValue:
|
|
obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled,
|
|
),
|
|
)
|
|
..add(
|
|
DiagnosticsProperty<bool>('enableSuggestions', enableSuggestions,
|
|
defaultValue: true),
|
|
)
|
|
..add(IntProperty('maxLines', maxLines, defaultValue: 1))
|
|
..add(IntProperty('minLines', minLines, defaultValue: null))
|
|
..add(DiagnosticsProperty<bool>('expands', expands, defaultValue: false))
|
|
..add(IntProperty('maxLength', maxLength, defaultValue: null))
|
|
..add(
|
|
EnumProperty<MaxLengthEnforcement>(
|
|
'maxLengthEnforcement',
|
|
maxLengthEnforcement,
|
|
defaultValue: null,
|
|
),
|
|
)
|
|
..add(
|
|
EnumProperty<TextInputAction>('textInputAction', textInputAction,
|
|
defaultValue: null),
|
|
)
|
|
..add(
|
|
EnumProperty<TextCapitalization>(
|
|
'textCapitalization',
|
|
textCapitalization,
|
|
defaultValue: TextCapitalization.none,
|
|
),
|
|
)
|
|
..add(EnumProperty<TextAlign>('textAlign', textAlign,
|
|
defaultValue: TextAlign.start))
|
|
..add(
|
|
DiagnosticsProperty<TextAlignVertical>(
|
|
'textAlignVertical',
|
|
textAlignVertical,
|
|
defaultValue: null,
|
|
),
|
|
)
|
|
..add(EnumProperty<TextDirection>('textDirection', textDirection,
|
|
defaultValue: null))
|
|
..add(DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0))
|
|
..add(DoubleProperty('cursorHeight', cursorHeight, defaultValue: null))
|
|
..add(DiagnosticsProperty<Radius>('cursorRadius', cursorRadius,
|
|
defaultValue: null))
|
|
..add(
|
|
DiagnosticsProperty<bool>(
|
|
'cursorOpacityAnimates', cursorOpacityAnimates,
|
|
defaultValue: null),
|
|
)
|
|
..add(ColorProperty('cursorColor', cursorColor, defaultValue: null))
|
|
..add(ColorProperty('cursorErrorColor', cursorErrorColor,
|
|
defaultValue: null))
|
|
..add(
|
|
DiagnosticsProperty<Brightness>(
|
|
'keyboardAppearance', keyboardAppearance,
|
|
defaultValue: null),
|
|
)
|
|
..add(
|
|
DiagnosticsProperty<EdgeInsetsGeometry>(
|
|
'scrollPadding',
|
|
scrollPadding,
|
|
defaultValue: const EdgeInsets.all(20.0),
|
|
),
|
|
)
|
|
..add(
|
|
FlagProperty(
|
|
'selectionEnabled',
|
|
value: selectionEnabled,
|
|
defaultValue: true,
|
|
ifFalse: 'selection disabled',
|
|
),
|
|
)
|
|
..add(
|
|
DiagnosticsProperty<TextSelectionControls>(
|
|
'selectionControls',
|
|
selectionControls,
|
|
defaultValue: null,
|
|
),
|
|
)
|
|
..add(
|
|
DiagnosticsProperty<ScrollController>(
|
|
'scrollController',
|
|
scrollController,
|
|
defaultValue: null,
|
|
),
|
|
)
|
|
..add(
|
|
DiagnosticsProperty<ScrollPhysics>('scrollPhysics', scrollPhysics,
|
|
defaultValue: null),
|
|
)
|
|
..add(
|
|
DiagnosticsProperty<Clip>('clipBehavior', clipBehavior,
|
|
defaultValue: Clip.hardEdge),
|
|
)
|
|
..add(
|
|
DiagnosticsProperty<bool>('scribbleEnabled', scribbleEnabled,
|
|
defaultValue: true),
|
|
)
|
|
..add(
|
|
DiagnosticsProperty<bool>(
|
|
'stylusHandwritingEnabled',
|
|
stylusHandwritingEnabled,
|
|
defaultValue: EditableText.defaultStylusHandwritingEnabled,
|
|
),
|
|
)
|
|
..add(
|
|
DiagnosticsProperty<bool>(
|
|
'enableIMEPersonalizedLearning',
|
|
enableIMEPersonalizedLearning,
|
|
defaultValue: true,
|
|
),
|
|
)
|
|
..add(
|
|
DiagnosticsProperty<SpellCheckConfiguration>(
|
|
'spellCheckConfiguration',
|
|
spellCheckConfiguration,
|
|
defaultValue: null,
|
|
),
|
|
)
|
|
..add(
|
|
DiagnosticsProperty<List<String>>(
|
|
'contentCommitMimeTypes',
|
|
contentInsertionConfiguration?.allowedMimeTypes ?? const <String>[],
|
|
defaultValue: contentInsertionConfiguration == null
|
|
? const <String>[]
|
|
: kDefaultContentInsertionMimeTypes,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _RichTextFieldState extends State<RichTextField>
|
|
with RestorationMixin
|
|
implements TextSelectionGestureDetectorBuilderDelegate, AutofillClient {
|
|
// RestorableRichTextEditingController? _controller;
|
|
RichTextEditingController get _effectiveController => widget.controller;
|
|
// 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;
|
|
return switch (mode) {
|
|
NavigationMode.traditional => widget.canRequestFocus && _isEnabled,
|
|
NavigationMode.directional => true,
|
|
};
|
|
}
|
|
|
|
@override
|
|
void didChangeDependencies() {
|
|
super.didChangeDependencies();
|
|
_effectiveFocusNode.canRequestFocus = _canRequestFocus;
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(RichTextField 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)
|
|
..update(MaterialState.hovered, _isHovering)
|
|
..update(MaterialState.focused, _effectiveFocusNode.hasFocus)
|
|
..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
|
|
// ? RestorableRichTextEditingController()
|
|
// : RestorableRichTextEditingController.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.stylusHandwriting) {
|
|
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 RichTextEditingController 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 =
|
|
CupertinoRichTextField.inferIOSSpellCheckConfiguration(
|
|
widget.spellCheckConfiguration,
|
|
);
|
|
case TargetPlatform.android:
|
|
case TargetPlatform.fuchsia:
|
|
case TargetPlatform.linux:
|
|
case TargetPlatform.windows:
|
|
spellCheckConfiguration =
|
|
RichTextField.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,
|
|
groupId: widget.groupId,
|
|
onSelectionHandleTapped: _handleSelectionHandleTapped,
|
|
onTapOutside: widget.onTapOutside,
|
|
onTapUpOutside: widget.onTapUpOutside,
|
|
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,
|
|
stylusHandwritingEnabled: widget.stylusHandwritingEnabled,
|
|
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(
|
|
enabled: _isEnabled,
|
|
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,
|
|
onFocus: _isEnabled
|
|
? () {
|
|
assert(
|
|
_effectiveFocusNode.canRequestFocus,
|
|
'Received SemanticsAction.focus from the engine. However, the FocusNode '
|
|
'of this text field cannot gain focus. This likely indicates a bug. '
|
|
'If this text field cannot be focused (e.g. because it is not '
|
|
'enabled), then its corresponding semantics node must be configured '
|
|
'such that the assistive technology cannot request focus on it.',
|
|
);
|
|
|
|
if (_effectiveFocusNode.canRequestFocus &&
|
|
!_effectiveFocusNode.hasFocus) {
|
|
_effectiveFocusNode.requestFocus();
|
|
} else if (!widget.readOnly) {
|
|
// If the platform requested focus, that means that previously the
|
|
// platform believed that the text field did not have focus (even
|
|
// though Flutter's widget system believed otherwise). This likely
|
|
// means that the on-screen keyboard is hidden, or more generally,
|
|
// there is no current editing session in this field. To correct
|
|
// that, keyboard must be requested.
|
|
//
|
|
// A concrete scenario where this can happen is when the user
|
|
// dismisses the keyboard on the web. The editing session is
|
|
// closed by the engine, but the text field widget stays focused
|
|
// in the framework.
|
|
_requestKeyboard();
|
|
}
|
|
}
|
|
: null,
|
|
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.
|
|
|
|
// dart format off
|
|
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);
|
|
// dart format on
|
|
|
|
// END GENERATED TOKEN PROPERTIES - TextField
|