blob: 4a07f4635a2f88ffcdf6f77c5ea578443f0565ed [file] [log] [blame]
// Copyright 2016 The Chromium 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:ui' show TextAffinity, hashValues;
import 'package:flutter/foundation.dart';
import 'message_codec.dart';
import 'system_channels.dart';
import 'text_editing.dart';
export 'dart:ui' show TextAffinity;
/// 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.
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 = const TextInputType._(0);
/// Optimize for multi-line textual information.
///
/// Requests the default platform keyboard, but accepts newlines when the
/// enter key is pressed. This is the input type used for all multi-line text
/// fields.
static const TextInputType multiline = const 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 = const TextInputType.numberWithOptions();
/// Optimize for telephone numbers.
///
/// Requests a keyboard with ready access to the number keys, "*", and "#".
static const TextInputType phone = const 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 = const TextInputType._(4);
/// Optimize for email addresses.
///
/// Requests a keyboard with ready access to the "@" and "." keys.
static const TextInputType emailAddress = const TextInputType._(5);
/// Optimize for URLs.
///
/// Requests a keyboard with ready access to the "/" and "." keys.
static const TextInputType url = const TextInputType._(6);
/// All possible enum values.
static const List<TextInputType> values = const <TextInputType>[
text, multiline, number, phone, datetime, emailAddress, url,
];
// Corresponding string name for each of the [values].
static const List<String> _names = const <String>[
'text', 'multiline', 'number', 'phone', 'datetime', 'emailAddress', 'url',
];
// 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 '$runtimeType('
'name: $_name, '
'signed: $signed, '
'decimal: $decimal)';
}
@override
bool operator ==(dynamic other) {
if (other is! TextInputType)
return false;
final TextInputType typedOther = other;
return typedOther.index == index
&& typedOther.signed == signed
&& typedOther.decimal == decimal;
}
@override
int get hashCode => hashValues(index, signed, decimal);
}
/// An action the user has requested the text input control to perform.
enum TextInputAction {
/// Complete the text input operation.
done,
/// The action to take when the enter button is pressed in a multi-line
/// text field (which is typically to do nothing).
newline,
}
/// Controls the visual appearance of the text input control.
///
/// See also:
///
/// * [TextInput.attach]
@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.obscureText = false,
this.autocorrect = true,
this.actionLabel,
this.inputAction = TextInputAction.done,
}) : assert(inputType != null),
assert(obscureText != null),
assert(autocorrect != null),
assert(inputAction != null);
/// The type of information for which to optimize the text input control.
final TextInputType inputType;
/// 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;
/// 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;
/// Returns a representation of this object as a JSON object.
Map<String, dynamic> toJSON() {
return <String, dynamic>{
'inputType': inputType.toJSON(),
'obscureText': obscureText,
'autocorrect': autocorrect,
'actionLabel': actionLabel,
'inputAction': inputAction.toString(),
};
}
}
TextAffinity _toTextAffinity(String affinity) {
switch (affinity) {
case 'TextAffinity.downstream':
return TextAffinity.downstream;
case 'TextAffinity.upstream':
return TextAffinity.upstream;
}
return null;
}
/// 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 new TextEditingValue(
text: encoded['text'],
selection: new TextSelection(
baseOffset: encoded['selectionBase'] ?? -1,
extentOffset: encoded['selectionExtent'] ?? -1,
affinity: _toTextAffinity(encoded['selectionAffinity']) ?? TextAffinity.downstream,
isDirectional: encoded['selectionIsDirectional'] ?? false,
),
composing: new TextRange(
start: encoded['composingBase'] ?? -1,
end: encoded['composingExtent'] ?? -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 = const 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 new TextEditingValue(
text: text ?? this.text,
selection: selection ?? this.selection,
composing: composing ?? this.composing
);
}
@override
String toString() => '$runtimeType(text: \u2524$text\u251C, selection: $selection, composing: $composing)';
@override
bool operator ==(dynamic other) {
if (identical(this, other))
return true;
if (other is! TextEditingValue)
return false;
final TextEditingValue typedOther = other;
return typedOther.text == text
&& typedOther.selection == selection
&& typedOther.composing == composing;
}
@override
int get hashCode => hashValues(
text.hashCode,
selection.hashCode,
composing.hashCode
);
}
/// An interface to receive information from [TextInput].
///
/// See also:
///
/// * [TextInput.attach]
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();
/// Requests that this client update its editing state to the given value.
void updateEditingValue(TextEditingValue value);
/// Requests that this client perform the given action.
void performAction(TextInputAction action);
}
/// An interface for interacting with a text input control.
///
/// See also:
///
/// * [TextInput.attach]
class TextInputConnection {
TextInputConnection._(this._client)
: assert(_client != null),
_id = _nextId++;
static int _nextId = 1;
final int _id;
final TextInputClient _client;
/// Whether this connection is currently interacting with the text input control.
bool get attached => _clientHandler._currentConnection == this;
/// Requests that the text input control become visible.
void show() {
assert(attached);
SystemChannels.textInput.invokeMethod('TextInput.show');
}
/// Requests that the text input control change its internal state to match the given state.
void setEditingState(TextEditingValue value) {
assert(attached);
SystemChannels.textInput.invokeMethod(
'TextInput.setEditingState',
value.toJSON(),
);
}
/// 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) {
SystemChannels.textInput.invokeMethod('TextInput.clearClient');
_clientHandler
.._currentConnection = null
.._scheduleHide();
}
assert(!attached);
}
}
TextInputAction _toTextInputAction(String action) {
switch (action) {
case 'TextInputAction.done':
return TextInputAction.done;
case 'TextInputAction.newline':
return TextInputAction.newline;
}
throw new FlutterError('Unknown text input action: $action');
}
class _TextInputClientHandler {
_TextInputClientHandler() {
SystemChannels.textInput.setMethodCallHandler(_handleTextInputInvocation);
}
TextInputConnection _currentConnection;
Future<dynamic> _handleTextInputInvocation(MethodCall methodCall) async {
if (_currentConnection == null)
return;
final String method = methodCall.method;
final List<dynamic> args = methodCall.arguments;
final int client = args[0];
// The incoming message was for a different client.
if (client != _currentConnection._id)
return;
switch (method) {
case 'TextInputClient.updateEditingState':
_currentConnection._client.updateEditingValue(new TextEditingValue.fromJSON(args[1]));
break;
case 'TextInputClient.performAction':
_currentConnection._client.performAction(_toTextInputAction(args[1]));
break;
default:
throw new 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)
SystemChannels.textInput.invokeMethod('TextInput.hide');
});
}
}
final _TextInputClientHandler _clientHandler = new _TextInputClientHandler();
/// An interface to the system's text input control.
class TextInput {
TextInput._();
/// 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 = new TextInputConnection._(client);
_clientHandler._currentConnection = connection;
SystemChannels.textInput.invokeMethod(
'TextInput.setClient',
<dynamic>[ connection._id, configuration.toJSON() ],
);
return connection;
}
}