Justin McCandless c12d9b9c19
IME private command docs improvement (#68630)
Just a docs-only improvement in EditableText and text input.
2020-10-27 09:22:47 -07:00

1424 lines
52 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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,
TextAffinity,
TextAlign,
TextDirection,
hashValues;
import 'package:flutter/foundation.dart';
import 'package:vector_math/vector_math_64.dart' show Matrix4;
import 'autofill.dart';
import 'message_codec.dart';
import 'platform_channel.dart';
import 'system_channels.dart';
import 'system_chrome.dart';
import 'text_editing.dart';
export 'dart:ui' show TextAffinity;
/// 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 [new 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 persons 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);
/// All possible enum values.
static const List<TextInputType> values = <TextInputType>[
text, multiline, number, phone, datetime, emailAddress, url, visiblePassword, name, streetAddress,
];
// Corresponding string name for each of the [values].
static const List<String> _names = <String>[
'text', 'multiline', 'number', 'phone', 'datetime', 'emailAddress', 'url', 'visiblePassword', 'name', 'address',
];
// 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 => hashValues(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. 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.
///
/// 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.
///
/// 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.actionLabel,
this.inputAction = TextInputAction.done,
this.keyboardAppearance = Brightness.light,
this.textCapitalization = TextCapitalization.none,
this.autofillConfiguration,
}) : 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);
/// 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.textInput.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.textInput.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.textInput.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;
/// 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;
/// Returns a representation of this object as a JSON object.
Map<String, dynamic> toJson() {
return <String, dynamic>{
'inputType': inputType.toJson(),
'readOnly': readOnly,
'obscureText': obscureText,
'autocorrect': autocorrect,
'smartDashesType': smartDashesType.index.toString(),
'smartQuotesType': smartQuotesType.index.toString(),
'enableSuggestions': enableSuggestions,
'actionLabel': actionLabel,
'inputAction': inputAction.toString(),
'textCapitalization': textCapitalization.toString(),
'keyboardAppearance': keyboardAppearance.toString(),
if (autofillConfiguration != null) 'autofill': autofillConfiguration!.toJson(),
};
}
}
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.
///
/// The [text], [selection], and [composing] arguments must not be null but
/// each have default values.
const TextEditingValue({
this.text = '',
this.selection = const TextSelection.collapsed(offset: -1),
this.composing = TextRange.empty,
}) : assert(text != null),
assert(selection != null),
assert(composing != null);
/// Creates an instance of this class from a JSON object.
factory TextEditingValue.fromJSON(Map<String, dynamic> encoded) {
return TextEditingValue(
text: encoded['text'] as String,
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,
),
composing: TextRange(
start: encoded['composingBase'] as int? ?? -1,
end: encoded['composingExtent'] as int? ?? -1,
),
);
}
/// Returns a representation of this object as a JSON object.
Map<String, dynamic> toJSON() {
return <String, dynamic>{
'text': text,
'selectionBase': selection.baseOffset,
'selectionExtent': selection.extentOffset,
'selectionAffinity': selection.affinity.toString(),
'selectionIsDirectional': selection.isDirectional,
'composingBase': composing.start,
'composingExtent': composing.end,
};
}
/// The current text being edited.
final String text;
/// The range of text that is currently selected.
final TextSelection selection;
/// The range of text that is still 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;
@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 => hashValues(
text.hashCode,
selection.hashCode,
composing.hashCode,
);
}
/// An interface for manipulating the selection, to be used by the implementor
/// of the toolbar widget.
abstract class 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.textEditingValue]: an implementation that applies
/// additional pre-processing to the specified [value], before updating the
/// text editing state.
set textEditingValue(TextEditingValue value);
/// Hides the text selection toolbar.
void hideToolbar();
/// 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;
}
/// An interface to receive information from [TextInput].
///
/// See also:
///
/// * [TextInput.attach]
/// * [EditableText], a [TextInputClient] implementation.
abstract class TextInputClient {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const 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:
/// * [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.
/// * [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();
}
/// 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;
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;
/// 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.
///
/// The information is currently only used on iOS, for positioning the IME bar.
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,
},
);
}
/// 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.continue_action':
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)
: const Offset(0, 0);
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(_handleTextInputInvocation);
}
/// 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._handleTextInputInvocation);
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,
];
/// 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',
<dynamic>[ 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;
Future<dynamic> _handleTextInputInvocation(MethodCall methodCall) async {
if (_currentConnection == null)
return;
final String method = methodCall.method;
// 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>;
if (method == 'TextInputClient.updateEditingStateWithTag') {
final TextInputClient client = _currentConnection!._client;
assert(client != null);
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>,
);
scope?.getAutofillClient(tag)?.updateEditingValue(textEditingValue);
}
return;
}
final int client = args[0] as int;
// The incoming message was for a different client.
if (client != _currentConnection!._id)
return;
switch (method) {
case 'TextInputClient.updateEditingState':
_currentConnection!._client.updateEditingValue(TextEditingValue.fromJSON(args[1] as Map<String, dynamic>));
break;
case 'TextInputClient.performAction':
_currentConnection!._client.performAction(_toTextInputAction(args[1] as String));
break;
case 'TextInputClient.performPrivateCommand':
_currentConnection!._client.performPrivateCommand(
args[1]['action'] as String, args[1]['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;
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 _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.autofill.autofillContext}
/// 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:
///
/// * [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 ,
);
}
}