blob: 4fee13bb9b96e4200f6fdf604e031181f3eb48d3 [file] [log] [blame]
// 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:async';
import 'dart:io' show Platform;
import 'dart:ui' show
FontWeight,
Offset,
Size,
Rect,
TextAlign,
TextDirection;
import 'package:flutter/foundation.dart';
import 'package:vector_math/vector_math_64.dart' show Matrix4;
import 'autofill.dart';
import 'clipboard.dart' show Clipboard;
import 'message_codec.dart';
import 'platform_channel.dart';
import 'system_channels.dart';
import 'text_editing.dart';
import 'text_editing_delta.dart';
export 'dart:ui' show Brightness, FontWeight, Offset, Rect, Size, TextAlign, TextDirection, TextPosition, TextRange;
export 'package:vector_math/vector_math_64.dart' show Matrix4;
export 'autofill.dart' show AutofillConfiguration, AutofillScope;
export 'text_editing.dart' show TextSelection;
// TODO(a14n): the following export leads to Segmentation fault, see https://github.com/flutter/flutter/issues/106332
// export 'text_editing_delta.dart' show TextEditingDelta;
/// Indicates how to handle the intelligent replacement of dashes in text input.
///
/// See also:
///
/// * [TextField.smartDashesType]
/// * [CupertinoTextField.smartDashesType]
/// * [EditableText.smartDashesType]
/// * [SmartQuotesType]
/// * <https://developer.apple.com/documentation/uikit/uitextinputtraits>
enum SmartDashesType {
/// Smart dashes is disabled.
///
/// This corresponds to the
/// ["no" value of UITextSmartDashesType](https://developer.apple.com/documentation/uikit/uitextsmartdashestype/no).
disabled,
/// Smart dashes is enabled.
///
/// This corresponds to the
/// ["yes" value of UITextSmartDashesType](https://developer.apple.com/documentation/uikit/uitextsmartdashestype/yes).
enabled,
}
/// Indicates how to handle the intelligent replacement of quotes in text input.
///
/// See also:
///
/// * [TextField.smartQuotesType]
/// * [CupertinoTextField.smartQuotesType]
/// * [EditableText.smartQuotesType]
/// * <https://developer.apple.com/documentation/uikit/uitextinputtraits>
enum SmartQuotesType {
/// Smart quotes is disabled.
///
/// This corresponds to the
/// ["no" value of UITextSmartQuotesType](https://developer.apple.com/documentation/uikit/uitextsmartquotestype/no).
disabled,
/// Smart quotes is enabled.
///
/// This corresponds to the
/// ["yes" value of UITextSmartQuotesType](https://developer.apple.com/documentation/uikit/uitextsmartquotestype/yes).
enabled,
}
/// The type of information for which to optimize the text input control.
///
/// On Android, behavior may vary across device and keyboard provider.
///
/// This class stays as close to `Enum` interface as possible, and allows
/// for additional flags for some input types. For example, numeric input
/// can specify whether it supports decimal numbers and/or signed numbers.
@immutable
class TextInputType {
const TextInputType._(this.index)
: signed = null,
decimal = null;
/// Optimize for numerical information.
///
/// Requests a numeric keyboard with additional settings.
/// The [signed] and [decimal] parameters are optional.
const TextInputType.numberWithOptions({
this.signed = false,
this.decimal = false,
}) : index = 2;
/// Enum value index, corresponds to one of the [values].
final int index;
/// The number is signed, allowing a positive or negative sign at the start.
///
/// This flag is only used for the [number] input type, otherwise `null`.
/// Use `const TextInputType.numberWithOptions(signed: true)` to set this.
final bool? signed;
/// The number is decimal, allowing a decimal point to provide fractional.
///
/// This flag is only used for the [number] input type, otherwise `null`.
/// Use `const TextInputType.numberWithOptions(decimal: true)` to set this.
final bool? decimal;
/// Optimize for textual information.
///
/// Requests the default platform keyboard.
static const TextInputType text = TextInputType._(0);
/// Optimize for multiline textual information.
///
/// Requests the default platform keyboard, but accepts newlines when the
/// enter key is pressed. This is the input type used for all multiline text
/// fields.
static const TextInputType multiline = TextInputType._(1);
/// Optimize for unsigned numerical information without a decimal point.
///
/// Requests a default keyboard with ready access to the number keys.
/// Additional options, such as decimal point and/or positive/negative
/// signs, can be requested using [TextInputType.numberWithOptions].
static const TextInputType number = TextInputType.numberWithOptions();
/// Optimize for telephone numbers.
///
/// Requests a keyboard with ready access to the number keys, "*", and "#".
static const TextInputType phone = TextInputType._(3);
/// Optimize for date and time information.
///
/// On iOS, requests the default keyboard.
///
/// On Android, requests a keyboard with ready access to the number keys,
/// ":", and "-".
static const TextInputType datetime = TextInputType._(4);
/// Optimize for email addresses.
///
/// Requests a keyboard with ready access to the "@" and "." keys.
static const TextInputType emailAddress = TextInputType._(5);
/// Optimize for URLs.
///
/// Requests a keyboard with ready access to the "/" and "." keys.
static const TextInputType url = TextInputType._(6);
/// Optimize for passwords that are visible to the user.
///
/// Requests a keyboard with ready access to both letters and numbers.
static const TextInputType visiblePassword = TextInputType._(7);
/// Optimized for a person's name.
///
/// On iOS, requests the
/// [UIKeyboardType.namePhonePad](https://developer.apple.com/documentation/uikit/uikeyboardtype/namephonepad)
/// keyboard, a keyboard optimized for entering a person’s name or phone number.
/// Does not support auto-capitalization.
///
/// On Android, requests a keyboard optimized for
/// [TYPE_TEXT_VARIATION_PERSON_NAME](https://developer.android.com/reference/android/text/InputType#TYPE_TEXT_VARIATION_PERSON_NAME).
static const TextInputType name = TextInputType._(8);
/// Optimized for postal mailing addresses.
///
/// On iOS, requests the default keyboard.
///
/// On Android, requests a keyboard optimized for
/// [TYPE_TEXT_VARIATION_POSTAL_ADDRESS](https://developer.android.com/reference/android/text/InputType#TYPE_TEXT_VARIATION_POSTAL_ADDRESS).
static const TextInputType streetAddress = TextInputType._(9);
/// Prevent the OS from showing the on-screen virtual keyboard.
static const TextInputType none = TextInputType._(10);
/// All possible enum values.
static const List<TextInputType> values = <TextInputType>[
text, multiline, number, phone, datetime, emailAddress, url, visiblePassword, name, streetAddress, none,
];
// Corresponding string name for each of the [values].
static const List<String> _names = <String>[
'text', 'multiline', 'number', 'phone', 'datetime', 'emailAddress', 'url', 'visiblePassword', 'name', 'address', 'none',
];
// Enum value name, this is what enum.toString() would normally return.
String get _name => 'TextInputType.${_names[index]}';
/// Returns a representation of this object as a JSON object.
Map<String, dynamic> toJson() {
return <String, dynamic>{
'name': _name,
'signed': signed,
'decimal': decimal,
};
}
@override
String toString() {
return '${objectRuntimeType(this, 'TextInputType')}('
'name: $_name, '
'signed: $signed, '
'decimal: $decimal)';
}
@override
bool operator ==(Object other) {
return other is TextInputType
&& other.index == index
&& other.signed == signed
&& other.decimal == decimal;
}
@override
int get hashCode => Object.hash(index, signed, decimal);
}
/// An action the user has requested the text input control to perform.
///
/// Each action represents a logical meaning, and also configures the soft
/// keyboard to display a certain kind of action button. The visual appearance
/// of the action button might differ between versions of the same OS.
///
/// Despite the logical meaning of each action, choosing a particular
/// [TextInputAction] does not necessarily cause any specific behavior to
/// happen, other than changing the focus when appropriate. It is up to the
/// developer to ensure that the behavior that occurs when an action button is
/// pressed is appropriate for the action button chosen.
///
/// For example: If the user presses the keyboard action button on iOS when it
/// reads "Emergency Call", the result should not be a focus change to the next
/// TextField. This behavior is not logically appropriate for a button that says
/// "Emergency Call".
///
/// See [EditableText] for more information about customizing action button
/// behavior.
///
/// Most [TextInputAction]s are supported equally by both Android and iOS.
/// However, there is not a complete, direct mapping between Android's IME input
/// types and iOS's keyboard return types. Therefore, some [TextInputAction]s
/// are inappropriate for one of the platforms. If a developer chooses an
/// inappropriate [TextInputAction] when running in debug mode, an error will be
/// thrown. If the same thing is done in release mode, then instead of sending
/// the inappropriate value, Android will use "unspecified" on the platform
/// side and iOS will use "default" on the platform side.
///
/// See also:
///
/// * [TextInput], which configures the platform's keyboard setup.
/// * [EditableText], which invokes callbacks when the action button is pressed.
enum TextInputAction {
/// Logical meaning: There is no relevant input action for the current input
/// source, e.g., [TextField].
///
/// Android: Corresponds to Android's "IME_ACTION_NONE". The keyboard setup
/// is decided by the OS. The keyboard will likely show a return key.
///
/// iOS: iOS does not have a keyboard return type of "none." It is
/// inappropriate to choose this [TextInputAction] when running on iOS.
none,
/// Logical meaning: Let the OS decide which action is most appropriate.
///
/// Android: Corresponds to Android's "IME_ACTION_UNSPECIFIED". The OS chooses
/// which keyboard action to display. The decision will likely be a done
/// button or a return key.
///
/// iOS: Corresponds to iOS's "UIReturnKeyDefault". The title displayed in
/// the action button is "return".
unspecified,
/// Logical meaning: The user is done providing input to a group of inputs
/// (like a form). Some kind of finalization behavior should now take place.
///
/// Android: Corresponds to Android's "IME_ACTION_DONE". The OS displays a
/// button that represents completion, e.g., a checkmark button.
///
/// iOS: Corresponds to iOS's "UIReturnKeyDone". The title displayed in the
/// action button is "Done".
done,
/// Logical meaning: The user has entered some text that represents a
/// destination, e.g., a restaurant name. The "go" button is intended to take
/// the user to a part of the app that corresponds to this destination.
///
/// Android: Corresponds to Android's "IME_ACTION_GO". The OS displays a
/// button that represents taking "the user to the target of the text they
/// typed", e.g., a right-facing arrow button.
///
/// iOS: Corresponds to iOS's "UIReturnKeyGo". The title displayed in the
/// action button is "Go".
go,
/// Logical meaning: Execute a search query.
///
/// Android: Corresponds to Android's "IME_ACTION_SEARCH". The OS displays a
/// button that represents a search, e.g., a magnifying glass button.
///
/// iOS: Corresponds to iOS's "UIReturnKeySearch". The title displayed in the
/// action button is "Search".
search,
/// Logical meaning: Sends something that the user has composed, e.g., an
/// email or a text message.
///
/// Android: Corresponds to Android's "IME_ACTION_SEND". The OS displays a
/// button that represents sending something, e.g., a paper plane button.
///
/// iOS: Corresponds to iOS's "UIReturnKeySend". The title displayed in the
/// action button is "Send".
send,
/// Logical meaning: The user is done with the current input source and wants
/// to move to the next one.
///
/// Moves the focus to the next focusable item in the same [FocusScope].
///
/// Android: Corresponds to Android's "IME_ACTION_NEXT". The OS displays a
/// button that represents moving forward, e.g., a right-facing arrow button.
///
/// iOS: Corresponds to iOS's "UIReturnKeyNext". The title displayed in the
/// action button is "Next".
next,
/// Logical meaning: The user wishes to return to the previous input source
/// in the group, e.g., a form with multiple [TextField]s.
///
/// Moves the focus to the previous focusable item in the same [FocusScope].
///
/// Android: Corresponds to Android's "IME_ACTION_PREVIOUS". The OS displays a
/// button that represents moving backward, e.g., a left-facing arrow button.
///
/// iOS: iOS does not have a keyboard return type of "previous." It is
/// inappropriate to choose this [TextInputAction] when running on iOS.
previous,
/// Logical meaning: In iOS apps, it is common for a "Back" button and
/// "Continue" button to appear at the top of the screen. However, when the
/// keyboard is open, these buttons are often hidden off-screen. Therefore,
/// the purpose of the "Continue" return key on iOS is to make the "Continue"
/// button available when the user is entering text.
///
/// Historical context aside, [TextInputAction.continueAction] can be used any
/// time that the term "Continue" seems most appropriate for the given action.
///
/// Android: Android does not have an IME input type of "continue." It is
/// inappropriate to choose this [TextInputAction] when running on Android.
///
/// iOS: Corresponds to iOS's "UIReturnKeyContinue". The title displayed in the
/// action button is "Continue". This action is only available on iOS 9.0+.
///
/// The reason that this value has "Action" post-fixed to it is because
/// "continue" is a reserved word in Dart, as well as many other languages.
continueAction,
/// Logical meaning: The user wants to join something, e.g., a wireless
/// network.
///
/// Android: Android does not have an IME input type of "join." It is
/// inappropriate to choose this [TextInputAction] when running on Android.
///
/// iOS: Corresponds to iOS's "UIReturnKeyJoin". The title displayed in the
/// action button is "Join".
join,
/// Logical meaning: The user wants routing options, e.g., driving directions.
///
/// Android: Android does not have an IME input type of "route." It is
/// inappropriate to choose this [TextInputAction] when running on Android.
///
/// iOS: Corresponds to iOS's "UIReturnKeyRoute". The title displayed in the
/// action button is "Route".
route,
/// Logical meaning: Initiate a call to emergency services.
///
/// Android: Android does not have an IME input type of "emergencyCall." It is
/// inappropriate to choose this [TextInputAction] when running on Android.
///
/// iOS: Corresponds to iOS's "UIReturnKeyEmergencyCall". The title displayed
/// in the action button is "Emergency Call".
emergencyCall,
/// Logical meaning: Insert a newline character in the focused text input,
/// e.g., [TextField].
///
/// Android: Corresponds to Android's "IME_ACTION_NONE". The OS displays a
/// button that represents a new line, e.g., a carriage return button.
///
/// iOS: Corresponds to iOS's "UIReturnKeyDefault". The title displayed in the
/// action button is "return".
///
/// The term [TextInputAction.newline] exists in Flutter but not in Android
/// or iOS. The reason for introducing this term is so that developers can
/// achieve the common result of inserting new lines without needing to
/// understand the various IME actions on Android and return keys on iOS.
/// Thus, [TextInputAction.newline] is a convenience term that alleviates the
/// need to understand the underlying platforms to achieve this common behavior.
newline,
}
/// Configures how the platform keyboard will select an uppercase or
/// lowercase keyboard.
///
/// Only supports text keyboards, other keyboard types will ignore this
/// configuration. Capitalization is locale-aware.
enum TextCapitalization {
/// Defaults to an uppercase keyboard for the first letter of each word.
///
/// Corresponds to `InputType.TYPE_TEXT_FLAG_CAP_WORDS` on Android, and
/// `UITextAutocapitalizationTypeWords` on iOS.
words,
/// Defaults to an uppercase keyboard for the first letter of each sentence.
///
/// Corresponds to `InputType.TYPE_TEXT_FLAG_CAP_SENTENCES` on Android, and
/// `UITextAutocapitalizationTypeSentences` on iOS.
sentences,
/// Defaults to an uppercase keyboard for each character.
///
/// Corresponds to `InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS` on Android, and
/// `UITextAutocapitalizationTypeAllCharacters` on iOS.
characters,
/// Defaults to a lowercase keyboard.
none,
}
/// Controls the visual appearance of the text input control.
///
/// Many [TextInputAction]s are common between Android and iOS. However, if an
/// [inputAction] is provided that is not supported by the current
/// platform in debug mode, an error will be thrown when the corresponding
/// text input is attached. For example, providing iOS's "emergencyCall"
/// action when running on an Android device will result in an error when in
/// debug mode. In release mode, incompatible [TextInputAction]s are replaced
/// either with "unspecified" on Android, or "default" on iOS. Appropriate
/// [inputAction]s can be chosen by checking the current platform and then
/// selecting the appropriate action.
///
/// See also:
///
/// * [TextInput.attach]
/// * [TextInputAction]
@immutable
class TextInputConfiguration {
/// Creates configuration information for a text input control.
///
/// All arguments have default values, except [actionLabel]. Only
/// [actionLabel] may be null.
const TextInputConfiguration({
this.inputType = TextInputType.text,
this.readOnly = false,
this.obscureText = false,
this.autocorrect = true,
SmartDashesType? smartDashesType,
SmartQuotesType? smartQuotesType,
this.enableSuggestions = true,
this.enableInteractiveSelection = true,
this.actionLabel,
this.inputAction = TextInputAction.done,
this.keyboardAppearance = Brightness.light,
this.textCapitalization = TextCapitalization.none,
this.autofillConfiguration = AutofillConfiguration.disabled,
this.enableIMEPersonalizedLearning = true,
this.enableDeltaModel = false,
}) : assert(inputType != null),
assert(obscureText != null),
smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
assert(autocorrect != null),
assert(enableSuggestions != null),
assert(keyboardAppearance != null),
assert(inputAction != null),
assert(textCapitalization != null),
assert(enableIMEPersonalizedLearning != null),
assert(enableDeltaModel != null);
/// The type of information for which to optimize the text input control.
final TextInputType inputType;
/// Whether the text field can be edited or not.
///
/// Defaults to false.
final bool readOnly;
/// Whether to hide the text being edited (e.g., for passwords).
///
/// Defaults to false.
final bool obscureText;
/// Whether to enable autocorrection.
///
/// Defaults to true.
final bool autocorrect;
/// The configuration to use for autofill.
///
/// Defaults to null, in which case no autofill information will be provided
/// to the platform. This will prevent the corresponding input field from
/// participating in autofills triggered by other fields. Additionally, on
/// Android and web, setting [autofillConfiguration] to null disables autofill.
final AutofillConfiguration autofillConfiguration;
/// {@template flutter.services.TextInputConfiguration.smartDashesType}
/// Whether to allow the platform to automatically format dashes.
///
/// This flag only affects iOS versions 11 and above. It sets
/// [`UITextSmartDashesType`](https://developer.apple.com/documentation/uikit/uitextsmartdashestype?language=objc)
/// in the engine. When true, it passes
/// [`UITextSmartDashesTypeYes`](https://developer.apple.com/documentation/uikit/uitextsmartdashestype/uitextsmartdashestypeyes?language=objc),
/// and when false, it passes
/// [`UITextSmartDashesTypeNo`](https://developer.apple.com/documentation/uikit/uitextsmartdashestype/uitextsmartdashestypeno?language=objc).
///
/// As an example of what this does, two consecutive hyphen characters will be
/// automatically replaced with one en dash, and three consecutive hyphens
/// will become one em dash.
///
/// Defaults to true, unless [obscureText] is true, when it defaults to false.
/// This is to avoid the problem where password fields receive autoformatted
/// characters.
///
/// See also:
///
/// * [smartQuotesType]
/// * <https://developer.apple.com/documentation/uikit/uitextinputtraits>
/// {@endtemplate}
final SmartDashesType smartDashesType;
/// {@template flutter.services.TextInputConfiguration.smartQuotesType}
/// Whether to allow the platform to automatically format quotes.
///
/// This flag only affects iOS. It sets
/// [`UITextSmartQuotesType`](https://developer.apple.com/documentation/uikit/uitextsmartquotestype?language=objc)
/// in the engine. When true, it passes
/// [`UITextSmartQuotesTypeYes`](https://developer.apple.com/documentation/uikit/uitextsmartquotestype/uitextsmartquotestypeyes?language=objc),
/// and when false, it passes
/// [`UITextSmartQuotesTypeNo`](https://developer.apple.com/documentation/uikit/uitextsmartquotestype/uitextsmartquotestypeno?language=objc).
///
/// As an example of what this does, a standard vertical double quote
/// character will be automatically replaced by a left or right double quote
/// depending on its position in a word.
///
/// Defaults to true, unless [obscureText] is true, when it defaults to false.
/// This is to avoid the problem where password fields receive autoformatted
/// characters.
///
/// See also:
///
/// * [smartDashesType]
/// * <https://developer.apple.com/documentation/uikit/uitextinputtraits>
/// {@endtemplate}
final SmartQuotesType smartQuotesType;
/// {@template flutter.services.TextInputConfiguration.enableSuggestions}
/// Whether to show input suggestions as the user types.
///
/// This flag only affects Android. On iOS, suggestions are tied directly to
/// [autocorrect], so that suggestions are only shown when [autocorrect] is
/// true. On Android autocorrection and suggestion are controlled separately.
///
/// Defaults to true. Cannot be null.
///
/// See also:
///
/// * <https://developer.android.com/reference/android/text/InputType.html#TYPE_TEXT_FLAG_NO_SUGGESTIONS>
/// {@endtemplate}
final bool enableSuggestions;
/// Whether a user can change its selection.
///
/// This flag only affects iOS VoiceOver. On Android Talkback, the selection
/// change is sent through semantics actions and is directly disabled from
/// the widget side.
///
/// Defaults to true. Cannot be null.
final bool enableInteractiveSelection;
/// What text to display in the text input control's action button.
final String? actionLabel;
/// What kind of action to request for the action button on the IME.
final TextInputAction inputAction;
/// Specifies how platforms may automatically capitalize text entered by the
/// user.
///
/// Defaults to [TextCapitalization.none].
///
/// See also:
///
/// * [TextCapitalization], for a description of each capitalization behavior.
final TextCapitalization textCapitalization;
/// The appearance of the keyboard.
///
/// This setting is only honored on iOS devices.
///
/// Defaults to [Brightness.light].
final Brightness keyboardAppearance;
/// {@template flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning}
/// Whether to enable that the IME update personalized data such as typing
/// history and user dictionary data.
///
/// This flag only affects Android. On iOS, there is no equivalent flag.
///
/// Defaults to true. Cannot be null.
///
/// See also:
///
/// * <https://developer.android.com/reference/android/view/inputmethod/EditorInfo#IME_FLAG_NO_PERSONALIZED_LEARNING>
/// {@endtemplate}
final bool enableIMEPersonalizedLearning;
/// Creates a copy of this [TextInputConfiguration] with the given fields
/// replaced with new values.
TextInputConfiguration copyWith({
TextInputType? inputType,
bool? readOnly,
bool? obscureText,
bool? autocorrect,
SmartDashesType? smartDashesType,
SmartQuotesType? smartQuotesType,
bool? enableSuggestions,
bool? enableInteractiveSelection,
String? actionLabel,
TextInputAction? inputAction,
Brightness? keyboardAppearance,
TextCapitalization? textCapitalization,
bool? enableIMEPersonalizedLearning,
AutofillConfiguration? autofillConfiguration,
bool? enableDeltaModel,
}) {
return TextInputConfiguration(
inputType: inputType ?? this.inputType,
readOnly: readOnly ?? this.readOnly,
obscureText: obscureText ?? this.obscureText,
autocorrect: autocorrect ?? this.autocorrect,
smartDashesType: smartDashesType ?? this.smartDashesType,
smartQuotesType: smartQuotesType ?? this.smartQuotesType,
enableSuggestions: enableSuggestions ?? this.enableSuggestions,
enableInteractiveSelection: enableInteractiveSelection ?? this.enableInteractiveSelection,
inputAction: inputAction ?? this.inputAction,
textCapitalization: textCapitalization ?? this.textCapitalization,
keyboardAppearance: keyboardAppearance ?? this.keyboardAppearance,
enableIMEPersonalizedLearning: enableIMEPersonalizedLearning?? this.enableIMEPersonalizedLearning,
autofillConfiguration: autofillConfiguration ?? this.autofillConfiguration,
enableDeltaModel: enableDeltaModel ?? this.enableDeltaModel,
);
}
/// Whether to enable that the engine sends text input updates to the
/// framework as [TextEditingDelta]'s or as one [TextEditingValue].
///
/// Enabling this flag results in granular text updates being received from the
/// platform's text input control.
///
/// When this is enabled:
/// * You must implement [DeltaTextInputClient] and not [TextInputClient] to
/// receive granular updates from the platform's text input.
/// * Platform text input updates will come through
/// [DeltaTextInputClient.updateEditingValueWithDeltas].
/// * If [TextInputClient] is implemented with this property enabled then
/// you will experience unexpected behavior as [TextInputClient] does not implement
/// a delta channel.
///
/// When this is disabled:
/// * If [DeltaTextInputClient] is implemented then updates for the
/// editing state will continue to come through the
/// [DeltaTextInputClient.updateEditingValue] channel.
/// * If [TextInputClient] is implemented then updates for the editing
/// state will come through [TextInputClient.updateEditingValue].
///
/// Defaults to false. Cannot be null.
final bool enableDeltaModel;
/// Returns a representation of this object as a JSON object.
Map<String, dynamic> toJson() {
final Map<String, dynamic>? autofill = autofillConfiguration.toJson();
return <String, dynamic>{
'inputType': inputType.toJson(),
'readOnly': readOnly,
'obscureText': obscureText,
'autocorrect': autocorrect,
'smartDashesType': smartDashesType.index.toString(),
'smartQuotesType': smartQuotesType.index.toString(),
'enableSuggestions': enableSuggestions,
'enableInteractiveSelection': enableInteractiveSelection,
'actionLabel': actionLabel,
'inputAction': inputAction.toString(),
'textCapitalization': textCapitalization.toString(),
'keyboardAppearance': keyboardAppearance.toString(),
'enableIMEPersonalizedLearning': enableIMEPersonalizedLearning,
if (autofill != null) 'autofill': autofill,
'enableDeltaModel' : enableDeltaModel,
};
}
}
TextAffinity? _toTextAffinity(String? affinity) {
switch (affinity) {
case 'TextAffinity.downstream':
return TextAffinity.downstream;
case 'TextAffinity.upstream':
return TextAffinity.upstream;
}
return null;
}
/// A floating cursor state the user has induced by force pressing an iOS
/// keyboard.
enum FloatingCursorDragState {
/// A user has just activated a floating cursor.
Start,
/// A user is dragging a floating cursor.
Update,
/// A user has lifted their finger off the screen after using a floating
/// cursor.
End,
}
/// The current state and position of the floating cursor.
class RawFloatingCursorPoint {
/// Creates information for setting the position and state of a floating
/// cursor.
///
/// [state] must not be null and [offset] must not be null if the state is
/// [FloatingCursorDragState.Update].
RawFloatingCursorPoint({
this.offset,
required this.state,
}) : assert(state != null),
assert(state != FloatingCursorDragState.Update || offset != null);
/// The raw position of the floating cursor as determined by the iOS sdk.
final Offset? offset;
/// The state of the floating cursor.
final FloatingCursorDragState state;
}
/// The current text, selection, and composing state for editing a run of text.
@immutable
class TextEditingValue {
/// Creates information for editing a run of text.
///
/// The selection and composing range must be within the text. This is not
/// checked during construction, and must be guaranteed by the caller.
///
/// The [text], [selection], and [composing] arguments must not be null but
/// each have default values.
///
/// The default value of [selection] is `TextSelection.collapsed(offset: -1)`.
/// This indicates that there is no selection at all.
const TextEditingValue({
this.text = '',
this.selection = const TextSelection.collapsed(offset: -1),
this.composing = TextRange.empty,
}) : assert(text != null),
// The constructor does not verify that `selection` and `composing` are
// valid ranges within `text`, and is unable to do so due to limitation
// of const constructors. Some checks are performed by assertion in
// other occasions. See `_textRangeIsValid`.
assert(selection != null),
assert(composing != null);
/// Creates an instance of this class from a JSON object.
factory TextEditingValue.fromJSON(Map<String, dynamic> encoded) {
final String text = encoded['text'] as String;
final TextSelection selection = TextSelection(
baseOffset: encoded['selectionBase'] as int? ?? -1,
extentOffset: encoded['selectionExtent'] as int? ?? -1,
affinity: _toTextAffinity(encoded['selectionAffinity'] as String?) ?? TextAffinity.downstream,
isDirectional: encoded['selectionIsDirectional'] as bool? ?? false,
);
final TextRange composing = TextRange(
start: encoded['composingBase'] as int? ?? -1,
end: encoded['composingExtent'] as int? ?? -1,
);
assert(_textRangeIsValid(selection, text));
assert(_textRangeIsValid(composing, text));
return TextEditingValue(
text: text,
selection: selection,
composing: composing,
);
}
/// The current text being edited.
final String text;
/// The range of text that is currently selected.
///
/// When [selection] is a [TextSelection] that has the same non-negative
/// `baseOffset` and `extentOffset`, the [selection] property represents the
/// caret position.
///
/// If the current [selection] has a negative `baseOffset` or `extentOffset`,
/// then the text currently does not have a selection or a caret location, and
/// most text editing operations that rely on the current selection (for
/// instance, insert a character at the caret location) will do nothing.
final TextSelection selection;
/// The range of text that is still being composed.
///
/// Composing regions are created by input methods (IMEs) to indicate the text
/// within a certain range is provisional. For instance, the Android Gboard
/// app's English keyboard puts the current word under the caret into a
/// composing region to indicate the word is subject to autocorrect or
/// prediction changes.
///
/// Composing regions can also be used for performing multistage input, which
/// is typically used by IMEs designed for phoetic keyboard to enter
/// ideographic symbols. As an example, many CJK keyboards require the user to
/// enter a latin alphabet sequence and then convert it to CJK characters. On
/// iOS, the default software keyboards do not have a dedicated view to show
/// the unfinished latin sequence, so it's displayed directly in the text
/// field, inside of a composing region.
///
/// The composing region should typically only be changed by the IME, or the
/// user via interacting with the IME.
///
/// If the range represented by this property is [TextRange.empty], then the
/// text is not currently being composed.
final TextRange composing;
/// A value that corresponds to the empty string with no selection and no composing range.
static const TextEditingValue empty = TextEditingValue();
/// Creates a copy of this value but with the given fields replaced with the new values.
TextEditingValue copyWith({
String? text,
TextSelection? selection,
TextRange? composing,
}) {
return TextEditingValue(
text: text ?? this.text,
selection: selection ?? this.selection,
composing: composing ?? this.composing,
);
}
/// Whether the [composing] range is a valid range within [text].
///
/// Returns true if and only if the [composing] range is normalized, its start
/// is greater than or equal to 0, and its end is less than or equal to
/// [text]'s length.
///
/// If this property is false while the [composing] range's `isValid` is true,
/// it usually indicates the current [composing] range is invalid because of a
/// programming error.
bool get isComposingRangeValid => composing.isValid && composing.isNormalized && composing.end <= text.length;
/// Returns a new [TextEditingValue], which is this [TextEditingValue] with
/// its [text] partially replaced by the `replacementString`.
///
/// The `replacementRange` parameter specifies the range of the
/// [TextEditingValue.text] that needs to be replaced.
///
/// The `replacementString` parameter specifies the string to replace the
/// given range of text with.
///
/// This method also adjusts the selection range and the composing range of the
/// resulting [TextEditingValue], such that they point to the same substrings
/// as the correspoinding ranges in the original [TextEditingValue]. For
/// example, if the original [TextEditingValue] is "Hello world" with the word
/// "world" selected, replacing "Hello" with a different string using this
/// method will not change the selected word.
///
/// This method does nothing if the given `replacementRange` is not
/// [TextRange.isValid].
TextEditingValue replaced(TextRange replacementRange, String replacementString) {
if (!replacementRange.isValid) {
return this;
}
final String newText = text.replaceRange(replacementRange.start, replacementRange.end, replacementString);
if (replacementRange.end - replacementRange.start == replacementString.length) {
return copyWith(text: newText);
}
int adjustIndex(int originalIndex) {
// The length added by adding the replacementString.
final int replacedLength = originalIndex <= replacementRange.start && originalIndex < replacementRange.end ? 0 : replacementString.length;
// The length removed by removing the replacementRange.
final int removedLength = originalIndex.clamp(replacementRange.start, replacementRange.end) - replacementRange.start; // ignore_clamp_double_lint
return originalIndex + replacedLength - removedLength;
}
final TextSelection adjustedSelection = TextSelection(
baseOffset: adjustIndex(selection.baseOffset),
extentOffset: adjustIndex(selection.extentOffset),
);
final TextRange adjustedComposing = TextRange(
start: adjustIndex(composing.start),
end: adjustIndex(composing.end),
);
assert(_textRangeIsValid(adjustedSelection, newText));
assert(_textRangeIsValid(adjustedComposing, newText));
return TextEditingValue(
text: newText,
selection: adjustedSelection,
composing: adjustedComposing,
);
}
/// Returns a representation of this object as a JSON object.
Map<String, dynamic> toJSON() {
assert(_textRangeIsValid(selection, text));
assert(_textRangeIsValid(composing, text));
return <String, dynamic>{
'text': text,
'selectionBase': selection.baseOffset,
'selectionExtent': selection.extentOffset,
'selectionAffinity': selection.affinity.toString(),
'selectionIsDirectional': selection.isDirectional,
'composingBase': composing.start,
'composingExtent': composing.end,
};
}
@override
String toString() => '${objectRuntimeType(this, 'TextEditingValue')}(text: \u2524$text\u251C, selection: $selection, composing: $composing)';
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
return other is TextEditingValue
&& other.text == text
&& other.selection == selection
&& other.composing == composing;
}
@override
int get hashCode => Object.hash(
text.hashCode,
selection.hashCode,
composing.hashCode,
);
// Verify that the given range is within the text.
//
// The verification can't be perform during the constructor of
// [TextEditingValue], which are `const` and are allowed to retrieve
// properties of [TextRange]s. [TextEditingValue] should perform this
// wherever it is building other values (such as toJson) or is built in a
// non-const way (such as fromJson).
static bool _textRangeIsValid(TextRange range, String text) {
if (range.start == -1 && range.end == -1) {
return true;
}
assert(range.start >= 0 && range.start <= text.length,
'Range start ${range.start} is out of text of length ${text.length}');
assert(range.end >= 0 && range.end <= text.length,
'Range end ${range.end} is out of text of length ${text.length}');
return true;
}
}
/// Indicates what triggered the change in selected text (including changes to
/// the cursor location).
enum SelectionChangedCause {
/// The user tapped on the text and that caused the selection (or the location
/// of the cursor) to change.
tap,
/// The user tapped twice in quick succession on the text and that caused
/// the selection (or the location of the cursor) to change.
doubleTap,
/// The user long-pressed the text and that caused the selection (or the
/// location of the cursor) to change.
longPress,
/// The user force-pressed the text and that caused the selection (or the
/// location of the cursor) to change.
forcePress,
/// The user used the keyboard to change the selection or the location of the
/// cursor.
///
/// Keyboard-triggered selection changes may be caused by the IME as well as
/// by accessibility tools (e.g. TalkBack on Android).
keyboard,
/// The user used the selection toolbar to change the selection or the
/// location of the cursor.
///
/// An example is when the user taps on select all in the tool bar.
toolbar,
/// The user used the mouse to change the selection by dragging over a piece
/// of text.
drag,
/// The user used iPadOS 14+ Scribble to change the selection.
scribble,
}
/// A mixin for manipulating the selection, provided for toolbar or shortcut
/// keys.
mixin TextSelectionDelegate {
/// Gets the current text input.
TextEditingValue get textEditingValue;
/// Indicates that the user has requested the delegate to replace its current
/// text editing state with [value].
///
/// The new [value] is treated as user input and thus may subject to input
/// formatting.
///
/// See also:
///
/// * [EditableTextState.userUpdateTextEditingValue]: an implementation that
/// applies additional pre-processing to the specified [value], before
/// updating the text editing state.
void userUpdateTextEditingValue(TextEditingValue value, SelectionChangedCause cause);
/// Hides the text selection toolbar.
///
/// By default, hideHandles is true, and the toolbar is hidden along with its
/// handles. If hideHandles is set to false, then the toolbar will be hidden
/// but the handles will remain.
void hideToolbar([bool hideHandles = true]);
/// Brings the provided [TextPosition] into the visible area of the text
/// input.
void bringIntoView(TextPosition position);
/// Whether cut is enabled, must not be null.
bool get cutEnabled => true;
/// Whether copy is enabled, must not be null.
bool get copyEnabled => true;
/// Whether paste is enabled, must not be null.
bool get pasteEnabled => true;
/// Whether select all is enabled, must not be null.
bool get selectAllEnabled => true;
/// Cut current selection to [Clipboard].
///
/// If and only if [cause] is [SelectionChangedCause.toolbar], the toolbar
/// will be hidden and the current selection will be scrolled into view.
void cutSelection(SelectionChangedCause cause);
/// Paste text from [Clipboard].
///
/// If there is currently a selection, it will be replaced.
///
/// If and only if [cause] is [SelectionChangedCause.toolbar], the toolbar
/// will be hidden and the current selection will be scrolled into view.
Future<void> pasteText(SelectionChangedCause cause);
/// Set the current selection to contain the entire text value.
///
/// If and only if [cause] is [SelectionChangedCause.toolbar], the selection
/// will be scrolled into view.
void selectAll(SelectionChangedCause cause);
/// Copy current selection to [Clipboard].
///
/// If [cause] is [SelectionChangedCause.toolbar], the position of
/// [bringIntoView] to selection will be called and hide toolbar.
void copySelection(SelectionChangedCause cause);
}
/// An interface to receive information from [TextInput].
///
/// If [TextInputConfiguration.enableDeltaModel] is set to true,
/// [DeltaTextInputClient] must be implemented instead of this class.
///
/// See also:
///
/// * [TextInput.attach]
/// * [EditableText], a [TextInputClient] implementation.
/// * [DeltaTextInputClient], a [TextInputClient] extension that receives
/// granular information from the platform's text input.
mixin TextInputClient {
/// The current state of the [TextEditingValue] held by this client.
TextEditingValue? get currentTextEditingValue;
/// The [AutofillScope] this [TextInputClient] belongs to, if any.
///
/// It should return null if this [TextInputClient] does not need autofill
/// support. For a [TextInputClient] that supports autofill, returning null
/// causes it to participate in autofill alone.
///
/// See also:
///
/// * [AutofillGroup], a widget that creates an [AutofillScope] for its
/// descendent autofillable [TextInputClient]s.
AutofillScope? get currentAutofillScope;
/// Requests that this client update its editing state to the given value.
///
/// The new [value] is treated as user input and thus may subject to input
/// formatting.
void updateEditingValue(TextEditingValue value);
/// Requests that this client perform the given action.
void performAction(TextInputAction action);
/// Request from the input method that this client perform the given private
/// command.
///
/// This can be used to provide domain-specific features that are only known
/// between certain input methods and their clients.
///
/// See also:
/// * [performPrivateCommand](https://developer.android.com/reference/android/view/inputmethod/InputConnection#performPrivateCommand\(java.lang.String,%20android.os.Bundle\)),
/// which is the Android documentation for performPrivateCommand, used to
/// send a command from the input method.
/// * [sendAppPrivateCommand](https://developer.android.com/reference/android/view/inputmethod/InputMethodManager#sendAppPrivateCommand),
/// which is the Android documentation for sendAppPrivateCommand, used to
/// send a command to the input method.
void performPrivateCommand(String action, Map<String, dynamic> data);
/// Updates the floating cursor position and state.
void updateFloatingCursor(RawFloatingCursorPoint point);
/// Requests that this client display a prompt rectangle for the given text range,
/// to indicate the range of text that will be changed by a pending autocorrection.
///
/// This method will only be called on iOS.
void showAutocorrectionPromptRect(int start, int end);
/// Platform notified framework of closed connection.
///
/// [TextInputClient] should cleanup its connection and finalize editing.
void connectionClosed();
/// Requests that the client show the editing toolbar, for example when the
/// platform changes the selection through a non-flutter method such as
/// scribble.
void showToolbar() {}
/// Requests that the client add a text placeholder to reserve visual space
/// in the text.
///
/// For example, this is called when responding to UIKit requesting
/// a text placeholder be added at the current selection, such as when
/// requesting additional writing space with iPadOS14 Scribble.
void insertTextPlaceholder(Size size) {}
/// Requests that the client remove the text placeholder.
void removeTextPlaceholder() {}
}
/// An interface to receive focus from the engine.
///
/// This is currently only used to handle UIIndirectScribbleInteraction.
abstract class ScribbleClient {
/// A unique identifier for this element.
String get elementIdentifier;
/// Called by the engine when the [ScribbleClient] should receive focus.
///
/// For example, this method is called during a UIIndirectScribbleInteraction.
void onScribbleFocus(Offset offset);
/// Tests whether the [ScribbleClient] overlaps the given rectangle bounds.
bool isInScribbleRect(Rect rect);
/// The current bounds of the [ScribbleClient].
Rect get bounds;
}
/// Represents a selection rect for a character and it's position in the text.
///
/// This is used to report the current text selection rect and position data
/// to the engine for Scribble support on iPadOS 14.
@immutable
class SelectionRect {
/// Constructor for creating a [SelectionRect] from a text [position] and
/// [bounds].
const SelectionRect({required this.position, required this.bounds});
/// The position of this selection rect within the text String.
final int position;
/// The rectangle representing the bounds of this selection rect within the
/// currently focused [RenderEditable]'s coordinate space.
final Rect bounds;
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (runtimeType != other.runtimeType) {
return false;
}
return other is SelectionRect
&& other.position == position
&& other.bounds == bounds;
}
@override
int get hashCode => Object.hash(position, bounds);
@override
String toString() => 'SelectionRect($position, $bounds)';
}
/// An interface to receive granular information from [TextInput].
///
/// See also:
///
/// * [TextInput.attach]
/// * [TextInputConfiguration], to opt-in to receive [TextEditingDelta]'s from
/// the platforms [TextInput] you must set [TextInputConfiguration.enableDeltaModel]
/// to true.
mixin DeltaTextInputClient implements TextInputClient {
/// Requests that this client update its editing state by applying the deltas
/// received from the engine.
///
/// The list of [TextEditingDelta]'s are treated as changes that will be applied
/// to the client's editing state. A change is any mutation to the raw text
/// value, or any updates to the selection and/or composing region.
///
/// {@tool snippet}
/// This example shows what an implementation of this method could look like.
///
/// ```dart
/// TextEditingValue? _localValue;
/// @override
/// void updateEditingValueWithDeltas(List<TextEditingDelta> textEditingDeltas) {
/// if (_localValue == null) {
/// return;
/// }
/// TextEditingValue newValue = _localValue!;
/// for (final TextEditingDelta delta in textEditingDeltas) {
/// newValue = delta.apply(newValue);
/// }
/// _localValue = newValue;
/// }
/// ```
/// {@end-tool}
void updateEditingValueWithDeltas(List<TextEditingDelta> textEditingDeltas);
}
/// An interface for interacting with a text input control.
///
/// See also:
///
/// * [TextInput.attach], a method used to establish a [TextInputConnection]
/// between the system's text input and a [TextInputClient].
/// * [EditableText], a [TextInputClient] that connects to and interacts with
/// the system's text input using a [TextInputConnection].
class TextInputConnection {
TextInputConnection._(this._client)
: assert(_client != null),
_id = _nextId++;
Size? _cachedSize;
Matrix4? _cachedTransform;
Rect? _cachedRect;
Rect? _cachedCaretRect;
List<SelectionRect> _cachedSelectionRects = <SelectionRect>[];
static int _nextId = 1;
final int _id;
/// Resets the internal ID counter for testing purposes.
///
/// This call has no effect when asserts are disabled. Calling it from
/// application code will likely break text input for the application.
@visibleForTesting
static void debugResetId({int to = 1}) {
assert(to != null);
assert(() {
_nextId = to;
return true;
}());
}
final TextInputClient _client;
/// Whether this connection is currently interacting with the text input control.
bool get attached => TextInput._instance._currentConnection == this;
/// Whether there is currently a Scribble interaction in progress.
///
/// This is used to make sure selection handles are shown when UIKit changes
/// the selection during a Scribble interaction.
bool get scribbleInProgress => TextInput._instance.scribbleInProgress;
/// Requests that the text input control become visible.
void show() {
assert(attached);
TextInput._instance._show();
}
/// Requests the system autofill UI to appear.
///
/// Currently only works on Android. Other platforms do not respond to this
/// message.
///
/// See also:
///
/// * [EditableText], a [TextInputClient] that calls this method when focused.
void requestAutofill() {
assert(attached);
TextInput._instance._requestAutofill();
}
/// Requests that the text input control update itself according to the new
/// [TextInputConfiguration].
void updateConfig(TextInputConfiguration configuration) {
assert(attached);
TextInput._instance._updateConfig(configuration);
}
/// Requests that the text input control change its internal state to match
/// the given state.
void setEditingState(TextEditingValue value) {
assert(attached);
TextInput._instance._setEditingState(value);
}
/// Send the size and transform of the editable text to engine.
///
/// The values are sent as platform messages so they can be used on web for
/// example to correctly position and size the html input field.
///
/// 1. [editableBoxSize]: size of the render editable box.
///
/// 2. [transform]: a matrix that maps the local paint coordinate system
/// to the [PipelineOwner.rootNode].
void setEditableSizeAndTransform(Size editableBoxSize, Matrix4 transform) {
if (editableBoxSize != _cachedSize || transform != _cachedTransform) {
_cachedSize = editableBoxSize;
_cachedTransform = transform;
TextInput._instance._setEditableSizeAndTransform(
<String, dynamic>{
'width': editableBoxSize.width,
'height': editableBoxSize.height,
'transform': transform.storage,
},
);
}
}
/// Send the smallest rect that covers the text in the client that's currently
/// being composed.
///
/// The given `rect` can not be null. If any of the 4 coordinates of the given
/// [Rect] is not finite, a [Rect] of size (-1, -1) will be sent instead.
///
/// This information is used for positioning the IME candidates menu on each
/// platform.
void setComposingRect(Rect rect) {
assert(rect != null);
if (rect == _cachedRect) {
return;
}
_cachedRect = rect;
final Rect validRect = rect.isFinite ? rect : Offset.zero & const Size(-1, -1);
TextInput._instance._setComposingTextRect(
<String, dynamic>{
'width': validRect.width,
'height': validRect.height,
'x': validRect.left,
'y': validRect.top,
},
);
}
/// Sends the coordinates of caret rect. This is used on macOS for positioning
/// the accent selection menu.
void setCaretRect(Rect rect) {
assert(rect != null);
if (rect == _cachedCaretRect) {
return;
}
_cachedCaretRect = rect;
final Rect validRect = rect.isFinite ? rect : Offset.zero & const Size(-1, -1);
TextInput._instance._setCaretRect(
<String, dynamic>{
'width': validRect.width,
'height': validRect.height,
'x': validRect.left,
'y': validRect.top,
},
);
}
/// Send the bounding boxes of the current selected glyphs in the client to
/// the platform's text input plugin.
///
/// These are used by the engine during a UIDirectScribbleInteraction.
void setSelectionRects(List<SelectionRect> selectionRects) {
if (!listEquals(_cachedSelectionRects, selectionRects)) {
_cachedSelectionRects = selectionRects;
TextInput._instance._setSelectionRects(selectionRects.map((SelectionRect rect) {
return <num>[rect.bounds.left, rect.bounds.top, rect.bounds.width, rect.bounds.height, rect.position];
}).toList());
}
}
/// Send text styling information.
///
/// This information is used by the Flutter Web Engine to change the style
/// of the hidden native input's content. Hence, the content size will match
/// to the size of the editable widget's content.
void setStyle({
required String? fontFamily,
required double? fontSize,
required FontWeight? fontWeight,
required TextDirection textDirection,
required TextAlign textAlign,
}) {
assert(attached);
TextInput._instance._setStyle(
<String, dynamic>{
'fontFamily': fontFamily,
'fontSize': fontSize,
'fontWeightIndex': fontWeight?.index,
'textAlignIndex': textAlign.index,
'textDirectionIndex': textDirection.index,
},
);
}
/// Stop interacting with the text input control.
///
/// After calling this method, the text input control might disappear if no
/// other client attaches to it within this animation frame.
void close() {
if (attached) {
TextInput._instance._clearClient();
}
assert(!attached);
}
/// Platform sent a notification informing the connection is closed.
///
/// [TextInputConnection] should clean current client connection.
void connectionClosedReceived() {
TextInput._instance._currentConnection = null;
assert(!attached);
}
}
TextInputAction _toTextInputAction(String action) {
switch (action) {
case 'TextInputAction.none':
return TextInputAction.none;
case 'TextInputAction.unspecified':
return TextInputAction.unspecified;
case 'TextInputAction.go':
return TextInputAction.go;
case 'TextInputAction.search':
return TextInputAction.search;
case 'TextInputAction.send':
return TextInputAction.send;
case 'TextInputAction.next':
return TextInputAction.next;
case 'TextInputAction.previous':
return TextInputAction.previous;
case 'TextInputAction.continueAction':
return TextInputAction.continueAction;
case 'TextInputAction.join':
return TextInputAction.join;
case 'TextInputAction.route':
return TextInputAction.route;
case 'TextInputAction.emergencyCall':
return TextInputAction.emergencyCall;
case 'TextInputAction.done':
return TextInputAction.done;
case 'TextInputAction.newline':
return TextInputAction.newline;
}
throw FlutterError.fromParts(<DiagnosticsNode>[ErrorSummary('Unknown text input action: $action')]);
}
FloatingCursorDragState _toTextCursorAction(String state) {
switch (state) {
case 'FloatingCursorDragState.start':
return FloatingCursorDragState.Start;
case 'FloatingCursorDragState.update':
return FloatingCursorDragState.Update;
case 'FloatingCursorDragState.end':
return FloatingCursorDragState.End;
}
throw FlutterError.fromParts(<DiagnosticsNode>[ErrorSummary('Unknown text cursor action: $state')]);
}
RawFloatingCursorPoint _toTextPoint(FloatingCursorDragState state, Map<String, dynamic> encoded) {
assert(state != null, 'You must provide a state to set a new editing point.');
assert(encoded['X'] != null, 'You must provide a value for the horizontal location of the floating cursor.');
assert(encoded['Y'] != null, 'You must provide a value for the vertical location of the floating cursor.');
final Offset offset = state == FloatingCursorDragState.Update
? Offset(encoded['X'] as double, encoded['Y'] as double)
: Offset.zero;
return RawFloatingCursorPoint(offset: offset, state: state);
}
/// An low-level interface to the system's text input control.
///
/// To start interacting with the system's text input control, call [attach] to
/// establish a [TextInputConnection] between the system's text input control
/// and a [TextInputClient]. The majority of commands available for
/// interacting with the text input control reside in the returned
/// [TextInputConnection]. The communication between the system text input and
/// the [TextInputClient] is asynchronous.
///
/// The platform text input plugin (which represents the system's text input)
/// and the [TextInputClient] usually maintain their own text editing states
/// ([TextEditingValue]) separately. They must be kept in sync as long as the
/// [TextInputClient] is connected. The following methods can be used to send
/// [TextEditingValue] to update the other party, when either party's text
/// editing states change:
///
/// * The [TextInput.attach] method allows a [TextInputClient] to establish a
/// connection to the text input. An optional field in its `configuration`
/// parameter can be used to specify an initial value for the platform text
/// input plugin's [TextEditingValue].
///
/// * The [TextInputClient] sends its [TextEditingValue] to the platform text
/// input plugin using [TextInputConnection.setEditingState].
///
/// * The platform text input plugin sends its [TextEditingValue] to the
/// connected [TextInputClient] via a "TextInput.setEditingState" message.
///
/// * When autofill happens on a disconnected [TextInputClient], the platform
/// text input plugin sends the [TextEditingValue] to the connected
/// [TextInputClient]'s [AutofillScope], and the [AutofillScope] will further
/// relay the value to the correct [TextInputClient].
///
/// When synchronizing the [TextEditingValue]s, the communication may get stuck
/// in an infinite when both parties are trying to send their own update. To
/// mitigate the problem, only [TextInputClient]s are allowed to alter the
/// received [TextEditingValue]s while platform text input plugins are to accept
/// the received [TextEditingValue]s unmodified. More specifically:
///
/// * When a [TextInputClient] receives a new [TextEditingValue] from the
/// platform text input plugin, it's allowed to modify the value (for example,
/// apply [TextInputFormatter]s). If it decides to do so, it must send the
/// updated [TextEditingValue] back to the platform text input plugin to keep
/// the [TextEditingValue]s in sync.
///
/// * When the platform text input plugin receives a new value from the
/// connected [TextInputClient], it must accept the new value as-is, to avoid
/// sending back an updated value.
///
/// See also:
///
/// * [TextField], a widget in which the user may enter text.
/// * [EditableText], a [TextInputClient] that connects to [TextInput] when it
/// wants to take user input from the keyboard.
class TextInput {
TextInput._() {
_channel = SystemChannels.textInput;
_channel.setMethodCallHandler(_loudlyHandleTextInputInvocation);
}
/// Set the [MethodChannel] used to communicate with the system's text input
/// control.
///
/// This is only meant for testing within the Flutter SDK. Changing this
/// will break the ability to input text. This has no effect if asserts are
/// disabled.
@visibleForTesting
static void setChannel(MethodChannel newChannel) {
assert(() {
_instance._channel = newChannel..setMethodCallHandler(_instance._loudlyHandleTextInputInvocation);
return true;
}());
}
static final TextInput _instance = TextInput._();
static const List<TextInputAction> _androidSupportedInputActions = <TextInputAction>[
TextInputAction.none,
TextInputAction.unspecified,
TextInputAction.done,
TextInputAction.send,
TextInputAction.go,
TextInputAction.search,
TextInputAction.next,
TextInputAction.previous,
TextInputAction.newline,
];
static const List<TextInputAction> _iOSSupportedInputActions = <TextInputAction>[
TextInputAction.unspecified,
TextInputAction.done,
TextInputAction.send,
TextInputAction.go,
TextInputAction.search,
TextInputAction.next,
TextInputAction.newline,
TextInputAction.continueAction,
TextInputAction.join,
TextInputAction.route,
TextInputAction.emergencyCall,
];
/// Ensure that a [TextInput] instance has been set up so that the platform
/// can handle messages on the text input method channel.
static void ensureInitialized() {
_instance; // ignore: unnecessary_statements
}
/// Begin interacting with the text input control.
///
/// Calling this function helps multiple clients coordinate about which one is
/// currently interacting with the text input control. The returned
/// [TextInputConnection] provides an interface for actually interacting with
/// the text input control.
///
/// A client that no longer wishes to interact with the text input control
/// should call [TextInputConnection.close] on the returned
/// [TextInputConnection].
static TextInputConnection attach(TextInputClient client, TextInputConfiguration configuration) {
assert(client != null);
assert(configuration != null);
final TextInputConnection connection = TextInputConnection._(client);
_instance._attach(connection, configuration);
return connection;
}
// This method actually notifies the embedding of the client. It is utilized
// by [attach] and by [_handleTextInputInvocation] for the
// `TextInputClient.requestExistingInputState` method.
void _attach(TextInputConnection connection, TextInputConfiguration configuration) {
assert(connection != null);
assert(connection._client != null);
assert(configuration != null);
assert(_debugEnsureInputActionWorksOnPlatform(configuration.inputAction));
_channel.invokeMethod<void>(
'TextInput.setClient',
<Object>[
connection._id,
configuration.toJson(),
],
);
_currentConnection = connection;
_currentConfiguration = configuration;
}
static bool _debugEnsureInputActionWorksOnPlatform(TextInputAction inputAction) {
assert(() {
if (kIsWeb) {
// TODO(flutterweb): what makes sense here?
return true;
}
if (Platform.isIOS) {
assert(
_iOSSupportedInputActions.contains(inputAction),
'The requested TextInputAction "$inputAction" is not supported on iOS.',
);
} else if (Platform.isAndroid) {
assert(
_androidSupportedInputActions.contains(inputAction),
'The requested TextInputAction "$inputAction" is not supported on Android.',
);
}
return true;
}());
return true;
}
late MethodChannel _channel;
TextInputConnection? _currentConnection;
late TextInputConfiguration _currentConfiguration;
final Map<String, ScribbleClient> _scribbleClients = <String, ScribbleClient>{};
bool _scribbleInProgress = false;
/// Used for testing within the Flutter SDK to get the currently registered [ScribbleClient] list.
@visibleForTesting
static Map<String, ScribbleClient> get scribbleClients => TextInput._instance._scribbleClients;
/// Returns true if a scribble interaction is currently happening.
bool get scribbleInProgress => _scribbleInProgress;
Future<dynamic> _loudlyHandleTextInputInvocation(MethodCall call) async {
try {
return await _handleTextInputInvocation(call);
} catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'services library',
context: ErrorDescription('during method call ${call.method}'),
informationCollector: () => <DiagnosticsNode>[
DiagnosticsProperty<MethodCall>('call', call, style: DiagnosticsTreeStyle.errorProperty),
],
));
rethrow;
}
}
Future<dynamic> _handleTextInputInvocation(MethodCall methodCall) async {
final String method = methodCall.method;
if (method == 'TextInputClient.focusElement') {
final List<dynamic> args = methodCall.arguments as List<dynamic>;
_scribbleClients[args[0]]?.onScribbleFocus(Offset((args[1] as num).toDouble(), (args[2] as num).toDouble()));
return;
} else if (method == 'TextInputClient.requestElementsInRect') {
final List<double> args = (methodCall.arguments as List<dynamic>).cast<num>().map<double>((num value) => value.toDouble()).toList();
return _scribbleClients.keys.where((String elementIdentifier) {
final Rect rect = Rect.fromLTWH(args[0], args[1], args[2], args[3]);
if (!(_scribbleClients[elementIdentifier]?.isInScribbleRect(rect) ?? false)) {
return false;
}
final Rect bounds = _scribbleClients[elementIdentifier]?.bounds ?? Rect.zero;
return !(bounds == Rect.zero || bounds.hasNaN || bounds.isInfinite);
}).map((String elementIdentifier) {
final Rect bounds = _scribbleClients[elementIdentifier]!.bounds;
return <dynamic>[elementIdentifier, ...<dynamic>[bounds.left, bounds.top, bounds.width, bounds.height]];
}).toList();
} else if (method == 'TextInputClient.scribbleInteractionBegan') {
_scribbleInProgress = true;
return;
} else if (method == 'TextInputClient.scribbleInteractionFinished') {
_scribbleInProgress = false;
return;
}
if (_currentConnection == null) {
return;
}
// The requestExistingInputState request needs to be handled regardless of
// the client ID, as long as we have a _currentConnection.
if (method == 'TextInputClient.requestExistingInputState') {
assert(_currentConnection!._client != null);
_attach(_currentConnection!, _currentConfiguration);
final TextEditingValue? editingValue = _currentConnection!._client.currentTextEditingValue;
if (editingValue != null) {
_setEditingState(editingValue);
}
return;
}
final List<dynamic> args = methodCall.arguments as List<dynamic>;
// The updateEditingStateWithTag request (autofill) can come up even to a
// text field that doesn't have a connection.
if (method == 'TextInputClient.updateEditingStateWithTag') {
assert(_currentConnection!._client != null);
final TextInputClient client = _currentConnection!._client;
final AutofillScope? scope = client.currentAutofillScope;
final Map<String, dynamic> editingValue = args[1] as Map<String, dynamic>;
for (final String tag in editingValue.keys) {
final TextEditingValue textEditingValue = TextEditingValue.fromJSON(
editingValue[tag] as Map<String, dynamic>,
);
final AutofillClient? client = scope?.getAutofillClient(tag);
if (client != null && client.textInputConfiguration.autofillConfiguration.enabled) {
client.autofill(textEditingValue);
}
}
return;
}
final int client = args[0] as int;
if (client != _currentConnection!._id) {
// If the client IDs don't match, the incoming message was for a different
// client.
bool debugAllowAnyway = false;
assert(() {
// In debug builds we allow "-1" as a magical client ID that ignores
// this verification step so that tests can always get through, even
// when they are not mocking the engine side of text input.
if (client == -1) {
debugAllowAnyway = true;
}
return true;
}());
if (!debugAllowAnyway) {
return;
}
}
switch (method) {
case 'TextInputClient.updateEditingState':
_currentConnection!._client.updateEditingValue(TextEditingValue.fromJSON(args[1] as Map<String, dynamic>));
break;
case 'TextInputClient.updateEditingStateWithDeltas':
assert(_currentConnection!._client is DeltaTextInputClient, 'You must be using a DeltaTextInputClient if TextInputConfiguration.enableDeltaModel is set to true');
final List<TextEditingDelta> deltas = <TextEditingDelta>[];
final Map<String, dynamic> encoded = args[1] as Map<String, dynamic>;
for (final dynamic encodedDelta in encoded['deltas'] as List<dynamic>) {
final TextEditingDelta delta = TextEditingDelta.fromJSON(encodedDelta as Map<String, dynamic>);
deltas.add(delta);
}
(_currentConnection!._client as DeltaTextInputClient).updateEditingValueWithDeltas(deltas);
break;
case 'TextInputClient.performAction':
_currentConnection!._client.performAction(_toTextInputAction(args[1] as String));
break;
case 'TextInputClient.performPrivateCommand':
final Map<String, dynamic> firstArg = args[1] as Map<String, dynamic>;
_currentConnection!._client.performPrivateCommand(
firstArg['action'] as String,
firstArg['data'] as Map<String, dynamic>,
);
break;
case 'TextInputClient.updateFloatingCursor':
_currentConnection!._client.updateFloatingCursor(_toTextPoint(
_toTextCursorAction(args[1] as String),
args[2] as Map<String, dynamic>,
));
break;
case 'TextInputClient.onConnectionClosed':
_currentConnection!._client.connectionClosed();
break;
case 'TextInputClient.showAutocorrectionPromptRect':
_currentConnection!._client.showAutocorrectionPromptRect(args[1] as int, args[2] as int);
break;
case 'TextInputClient.showToolbar':
_currentConnection!._client.showToolbar();
break;
case 'TextInputClient.insertTextPlaceholder':
_currentConnection!._client.insertTextPlaceholder(Size((args[1] as num).toDouble(), (args[2] as num).toDouble()));
break;
case 'TextInputClient.removeTextPlaceholder':
_currentConnection!._client.removeTextPlaceholder();
break;
default:
throw MissingPluginException();
}
}
bool _hidePending = false;
void _scheduleHide() {
if (_hidePending) {
return;
}
_hidePending = true;
// Schedule a deferred task that hides the text input. If someone else
// shows the keyboard during this update cycle, then the task will do
// nothing.
scheduleMicrotask(() {
_hidePending = false;
if (_currentConnection == null) {
_channel.invokeMethod<void>('TextInput.hide');
}
});
}
void _clearClient() {
_channel.invokeMethod<void>('TextInput.clearClient');
_currentConnection = null;
_scheduleHide();
}
void _updateConfig(TextInputConfiguration configuration) {
assert(configuration != null);
_channel.invokeMethod<void>(
'TextInput.updateConfig',
configuration.toJson(),
);
}
void _setEditingState(TextEditingValue value) {
assert(value != null);
_channel.invokeMethod<void>(
'TextInput.setEditingState',
value.toJSON(),
);
}
void _show() {
_channel.invokeMethod<void>('TextInput.show');
}
void _requestAutofill() {
_channel.invokeMethod<void>('TextInput.requestAutofill');
}
void _setEditableSizeAndTransform(Map<String, dynamic> args) {
_channel.invokeMethod<void>(
'TextInput.setEditableSizeAndTransform',
args,
);
}
void _setComposingTextRect(Map<String, dynamic> args) {
_channel.invokeMethod<void>(
'TextInput.setMarkedTextRect',
args,
);
}
void _setCaretRect(Map<String, dynamic> args) {
_channel.invokeMethod<void>(
'TextInput.setCaretRect',
args,
);
}
void _setSelectionRects(List<List<num>> args) {
_channel.invokeMethod<void>(
'TextInput.setSelectionRects',
args,
);
}
void _setStyle(Map<String, dynamic> args) {
_channel.invokeMethod<void>(
'TextInput.setStyle',
args,
);
}
/// Finishes the current autofill context, and potentially saves the user
/// input for future use if `shouldSave` is true.
///
/// Typically, this method should be called when the user has finalized their
/// input. For example, in a [Form], it's typically done immediately before or
/// after its content is submitted.
///
/// The topmost [AutofillGroup]s also call [finishAutofillContext]
/// automatically when they are disposed. The default behavior can be
/// overridden in [AutofillGroup.onDisposeAction].
///
/// {@template flutter.services.TextInput.finishAutofillContext}
/// An autofill context is a collection of input fields that live in the
/// platform's text input plugin. The platform is encouraged to save the user
/// input stored in the current autofill context before the context is
/// destroyed, when [TextInput.finishAutofillContext] is called with
/// `shouldSave` set to true.
///
/// Currently, there can only be at most one autofill context at any given
/// time. When any input field in an [AutofillGroup] requests for autofill
/// (which is done automatically when an autofillable [EditableText] gains
/// focus), the current autofill context will merge the content of that
/// [AutofillGroup] into itself. When there isn't an existing autofill context,
/// one will be created to hold the newly added input fields from the group.
///
/// Once added to an autofill context, an input field will stay in the context
/// until the context is destroyed. To prevent leaks, call
/// [TextInput.finishAutofillContext] to signal the text input plugin that the
/// user has finalized their input in the current autofill context. The
/// platform text input plugin either encourages or discourages the platform
/// from saving the user input based on the value of the `shouldSave`
/// parameter. The platform usually shows a "Save for autofill?" prompt for
/// user confirmation.
/// {@endtemplate}
///
/// On many platforms, calling [finishAutofillContext] shows the save user
/// input dialog and disrupts the user's flow. Ideally the dialog should only
/// be shown no more than once for every screen. Consider removing premature
/// [finishAutofillContext] calls to prevent showing the save user input UI
/// too frequently. However, calling [finishAutofillContext] when there's no
/// existing autofill context usually does not bring up the save user input
/// UI.
///
/// See also:
///
/// * [EditableText.autofillHints] for autofill save troubleshooting tips.
/// * [AutofillGroup.onDisposeAction], a configurable action that runs when a
/// topmost [AutofillGroup] is getting disposed.
static void finishAutofillContext({ bool shouldSave = true }) {
assert(shouldSave != null);
TextInput._instance._channel.invokeMethod<void>(
'TextInput.finishAutofillContext',
shouldSave,
);
}
/// Registers a [ScribbleClient] with [elementIdentifier] that can be focused
/// by the engine.
///
/// For example, the registered [ScribbleClient] list is used to respond to
/// UIIndirectScribbleInteraction on an iPad.
static void registerScribbleElement(String elementIdentifier, ScribbleClient scribbleClient) {
TextInput._instance._scribbleClients[elementIdentifier] = scribbleClient;
}
/// Unregisters a [ScribbleClient] with [elementIdentifier].
static void unregisterScribbleElement(String elementIdentifier) {
TextInput._instance._scribbleClients.remove(elementIdentifier);
}
}