Commit 2cfc0405 authored by Adam Barth's avatar Adam Barth Committed by GitHub

Add TextInput class for interacting with the IME (#6386)

This class will eventually replace the Keyboard class we currently use. As part
of this migration, we'll switch from using mojom to interact with the IME to
using platform messages.
parent cfddacbb
......@@ -32,4 +32,5 @@ export 'src/services/shell.dart';
export 'src/services/system_chrome.dart';
export 'src/services/system_navigator.dart';
export 'src/services/system_sound.dart';
export 'src/services/text_input.dart';
export 'src/services/url_launcher.dart';
......@@ -138,7 +138,7 @@ class TextSelection extends TextRange {
/// Might be larger than, smaller than, or equal to base.
final int extentOffset;
/// If the the text range is collpased and has more than one visual location
/// If the the text range is collapsed and has more than one visual location
/// (e.g., occurs at a line break), which of the two locations to use when
/// painting the caret.
final TextAffinity affinity;
......
......@@ -18,6 +18,8 @@ class ClipboardData {
final String text;
}
const String _kChannelName = 'flutter/platform';
/// An interface to the system's clipboard.
class Clipboard {
/// Constants for common [getData] [format] types.
......@@ -27,23 +29,21 @@ class Clipboard {
/// Stores the given clipboard data on the clipboard.
static Future<Null> setData(ClipboardData data) async {
await PlatformMessages.sendJSON('flutter/platform', <String, dynamic>{
'method': 'Clipboard.setData',
'args': <Map<String, dynamic>>[<String, dynamic>{
await PlatformMessages.invokeMethod(
_kChannelName,
'Clipboard.setData',
<Map<String, dynamic>>[<String, dynamic>{
'text': data.text,
}],
});
);
}
/// Retrieves data from the clipboard that matches the given format.
///
/// * `format` is a media type, such as `text/plain`.
static Future<ClipboardData> getData(String format) async {
Map<String, dynamic> result =
await PlatformMessages.sendJSON('flutter/platform', <String, dynamic>{
'method': 'Clipboard.getData',
'args': <String>[format],
});
Map<String, dynamic> result = await PlatformMessages.invokeMethod(
_kChannelName, 'Clipboard.getData', <String>[format]);
if (result == null)
return null;
return new ClipboardData(text: result['text']);
......
......@@ -22,9 +22,6 @@ class HapticFeedback {
/// * _Android_: Uses the platform haptic feedback API that simulates a short
/// a short tap on a virtual keyboard.
static Future<Null> vibrate() async {
await PlatformMessages.sendJSON('flutter/platform', <String, dynamic>{
'method': 'HapticFeedback.vibrate',
'args': const <Null>[],
});
await PlatformMessages.invokeMethod('flutter/platform', 'HapticFeedback.vibrate');
}
}
......@@ -7,6 +7,8 @@ import 'dart:io';
import 'platform_messages.dart';
const String _kChannelName = 'flutter/platform';
/// Returns commonly used locations on the filesystem.
class PathProvider {
PathProvider._();
......@@ -22,11 +24,8 @@ class PathProvider {
/// * _iOS_: `NSTemporaryDirectory()`
/// * _Android_: `getCacheDir()` on the context.
static Future<Directory> getTemporaryDirectory() async {
Map<String, dynamic> result =
await PlatformMessages.sendJSON('flutter/platform', <String, dynamic>{
'method': 'PathProvider.getTemporaryDirectory',
'args': const <Null>[],
});
Map<String, dynamic> result = await PlatformMessages.invokeMethod(
_kChannelName, 'PathProvider.getTemporaryDirectory');
if (result == null)
return null;
return new Directory(result['path']);
......@@ -41,11 +40,8 @@ class PathProvider {
/// * _iOS_: `NSDocumentsDirectory`
/// * _Android_: The AppData directory.
static Future<Directory> getApplicationDocumentsDirectory() async {
Map<String, dynamic> result =
await PlatformMessages.sendJSON('flutter/platform', <String, dynamic>{
'method': 'PathProvider.getApplicationDocumentsDirectory',
'args': const <Null>[],
});
Map<String, dynamic> result = await PlatformMessages.invokeMethod(
_kChannelName, 'PathProvider.getApplicationDocumentsDirectory');
if (result == null)
return null;
return new Directory(result['path']);
......
......@@ -40,9 +40,9 @@ class PlatformMessages {
static final Map<String, _PlatformMessageHandler> _mockHandlers =
<String, _PlatformMessageHandler>{};
static Future<ByteData> _sendPlatformMessage(String name, ByteData message) {
static Future<ByteData> _sendPlatformMessage(String channel, ByteData message) {
final Completer<ByteData> completer = new Completer<ByteData>();
ui.window.sendPlatformMessage(name, message, (ByteData reply) {
ui.window.sendPlatformMessage(channel, message, (ByteData reply) {
try {
completer.complete(reply);
} catch (exception, stack) {
......@@ -57,18 +57,18 @@ class PlatformMessages {
return completer.future;
}
/// Calls the handler registered for the given name.
/// Calls the handler registered for the given channel.
///
/// Typically called by [ServicesBinding] to handle platform messages received
/// from [ui.window.onPlatformMessage].
///
/// To register a handler for a given message name, see
/// To register a handler for a given message channel, see
/// [setStringMessageHandler] and [setJSONMessageHandler].
static Future<Null> handlePlatformMessage(
String name, ByteData data, ui.PlatformMessageResponseCallback callback) async {
String channel, ByteData data, ui.PlatformMessageResponseCallback callback) async {
ByteData response;
try {
_PlatformMessageHandler handler = _handlers[name];
_PlatformMessageHandler handler = _handlers[channel];
if (handler != null)
response = await handler(data);
} catch (exception, stack) {
......@@ -84,35 +84,42 @@ class PlatformMessages {
}
/// Send a binary message to the host application.
static Future<ByteData> sendBinary(String name, ByteData message) {
final _PlatformMessageHandler handler = _mockHandlers[name];
static Future<ByteData> sendBinary(String channel, ByteData message) {
final _PlatformMessageHandler handler = _mockHandlers[channel];
if (handler != null)
return handler(message);
return _sendPlatformMessage(name, message);
return _sendPlatformMessage(channel, message);
}
/// Send a string message to the host application.
static Future<String> sendString(String name, String message) async {
return _decodeUTF8(await sendBinary(name, _encodeUTF8(message)));
static Future<String> sendString(String channel, String message) async {
return _decodeUTF8(await sendBinary(channel, _encodeUTF8(message)));
}
/// Sends a JSON-encoded message to the host application and JSON-decodes the response.
static Future<dynamic> sendJSON(String name, dynamic json) async {
return _decodeJSON(await sendString(name, _encodeJSON(json)));
static Future<dynamic> sendJSON(String channel, dynamic json) async {
return _decodeJSON(await sendString(channel, _encodeJSON(json)));
}
static Future<dynamic> invokeMethod(String channel, String method, [ List<dynamic> args = const <Null>[] ]) {
return sendJSON(channel, <String, dynamic>{
'method': method,
'args': args,
});
}
/// Set a callback for receiving binary messages from the platform.
///
/// The given callback will replace the currently registered callback (if any).
static void setBinaryMessageHandler(String name, Future<ByteData> handler(ByteData message)) {
_handlers[name] = handler;
static void setBinaryMessageHandler(String channel, Future<ByteData> handler(ByteData message)) {
_handlers[channel] = handler;
}
/// Set a callback for receiving string messages from the platform.
///
/// The given callback will replace the currently registered callback (if any).
static void setStringMessageHandler(String name, Future<String> handler(String message)) {
setBinaryMessageHandler(name, (ByteData message) async {
static void setStringMessageHandler(String channel, Future<String> handler(String message)) {
setBinaryMessageHandler(channel, (ByteData message) async {
return _encodeUTF8(await handler(_decodeUTF8(message)));
});
}
......@@ -124,8 +131,8 @@ class PlatformMessages {
/// returned as the response to the message.
///
/// The given callback will replace the currently registered callback (if any).
static void setJSONMessageHandler(String name, Future<dynamic> handler(dynamic message)) {
setStringMessageHandler(name, (String message) async {
static void setJSONMessageHandler(String channel, Future<dynamic> handler(dynamic message)) {
setStringMessageHandler(channel, (String message) async {
return _encodeJSON(await handler(_decodeJSON(message)));
});
}
......@@ -134,19 +141,19 @@ class PlatformMessages {
///
/// The given callback will replace the currently registered callback (if any).
/// To remove the mock handler, pass `null` as the `handler` argument.
static void setMockBinaryMessageHandler(String name, Future<ByteData> handler(ByteData message)) {
static void setMockBinaryMessageHandler(String channel, Future<ByteData> handler(ByteData message)) {
if (handler == null)
_mockHandlers.remove(handler);
else
_mockHandlers[name] = handler;
_mockHandlers[channel] = handler;
}
/// Sets a message handler that intercepts outgoing messages in string form.
///
/// The given callback will replace the currently registered callback (if any).
/// To remove the mock handler, pass `null` as the `handler` argument.
static void setMockStringMessageHandler(String name, Future<String> handler(String message)) {
setMockBinaryMessageHandler(name, (ByteData message) async {
static void setMockStringMessageHandler(String channel, Future<String> handler(String message)) {
setMockBinaryMessageHandler(channel, (ByteData message) async {
return _encodeUTF8(await handler(_decodeUTF8(message)));
});
}
......@@ -155,8 +162,8 @@ class PlatformMessages {
///
/// The given callback will replace the currently registered callback (if any).
/// To remove the mock handler, pass `null` as the `handler` argument.
static void setMockJSONMessageHandler(String name, Future<dynamic> handler(dynamic message)) {
setMockStringMessageHandler(name, (String message) async {
static void setMockJSONMessageHandler(String channel, Future<dynamic> handler(dynamic message)) {
setMockStringMessageHandler(channel, (String message) async {
return _encodeJSON(await handler(_decodeJSON(message)));
});
}
......
......@@ -75,6 +75,8 @@ enum SystemUiOverlayStyle {
dark,
}
const String _kChannelName = 'flutter/platform';
List<String> _stringify(List<dynamic> list) {
List<String> result = <String>[];
for (dynamic item in list)
......@@ -94,10 +96,11 @@ class SystemChrome {
/// * [orientation]: A list of [DeviceOrientation] enum values. The empty
/// list is synonymous with having all options enabled.
static Future<Null> setPreferredOrientations(List<DeviceOrientation> orientations) async {
await PlatformMessages.sendJSON('flutter/platform', <String, dynamic>{
'method': 'SystemChrome.setPreferredOrientations',
'args': <List<String>>[ _stringify(orientations) ],
});
await PlatformMessages.invokeMethod(
_kChannelName,
'SystemChrome.setPreferredOrientations',
<List<String>>[ _stringify(orientations) ],
);
}
/// Specifies the description of the current state of the application as it
......@@ -112,13 +115,14 @@ class SystemChrome {
/// If application-specified metadata is unsupported on the platform,
/// specifying it is a no-op and always return true.
static Future<Null> setApplicationSwitcherDescription(ApplicationSwitcherDescription description) async {
await PlatformMessages.sendJSON('flutter/platform', <String, dynamic>{
'method': 'SystemChrome.setApplicationSwitcherDescription',
'args': <Map<String, dynamic>>[<String, dynamic>{
await PlatformMessages.invokeMethod(
_kChannelName,
'SystemChrome.setApplicationSwitcherDescription',
<Map<String, dynamic>>[<String, dynamic>{
'label': description.label,
'primaryColor': description.primaryColor,
}],
});
);
}
/// Specifies the set of overlays visible on the embedder when the
......@@ -135,10 +139,11 @@ class SystemChrome {
/// If the overlay is unsupported on the platform, enabling or disabling
/// that overlay is a no-op and always return true.
static Future<Null> setEnabledSystemUIOverlays(List<SystemUiOverlay> overlays) async {
await PlatformMessages.sendJSON('flutter/platform', <String, dynamic>{
'method': 'SystemChrome.setEnabledSystemUIOverlays',
'args': <List<String>>[ _stringify(overlays) ],
});
await PlatformMessages.invokeMethod(
_kChannelName,
'SystemChrome.setEnabledSystemUIOverlays',
<List<String>>[ _stringify(overlays) ],
);
}
/// Specifies the style of the system overlays that are visible on the
......@@ -166,10 +171,11 @@ class SystemChrome {
scheduleMicrotask(() {
assert(_pendingStyle != null);
if (_pendingStyle != _latestStyle) {
PlatformMessages.sendJSON('flutter/platform', <String, dynamic>{
'method': 'SystemChrome.setSystemUIOverlayStyle',
'args': <String>[ _pendingStyle.toString() ],
});
PlatformMessages.invokeMethod(
_kChannelName,
'SystemChrome.setSystemUIOverlayStyle',
<String>[ _pendingStyle.toString() ],
);
_latestStyle = _pendingStyle;
}
_pendingStyle = null;
......
......@@ -18,9 +18,6 @@ class SystemNavigator {
/// On iOS, this is a no-op because Apple's human interface guidelines state
/// that applications should not exit themselves.
static Future<Null> pop() async {
await PlatformMessages.sendJSON('flutter/platform', <String, dynamic>{
'method': 'SystemNavigator.pop',
'args': const <Null>[],
});
await PlatformMessages.invokeMethod('flutter/platform', 'SystemNavigator.pop');
}
}
......@@ -20,9 +20,10 @@ class SystemSound {
/// Play the specified system sound. If that sound is not present on the
/// system, this method is a no-op.
static Future<Null> play(SystemSoundType type) async {
await PlatformMessages.sendJSON('flutter/platform', <String, dynamic>{
'method': 'SystemSound.play',
'args': <String>[ type.toString() ],
});
await PlatformMessages.invokeMethod(
'flutter/platform',
'SystemSound.play',
<String>[ type.toString() ],
);
}
}
// 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;
import 'package:flutter/foundation.dart';
import 'platform_messages.dart';
export 'dart:ui' show TextAffinity;
/// For which type of information to optimize the text input control.
enum TextInputType {
/// Optimize for textual information.
text,
/// Optimize for numerical information.
number,
/// Optimize for telephone numbers.
phone,
/// Optimize for date and time information.
datetime,
}
/// A action the user has requested the text input control to perform.
enum TextInputAction {
/// Complete the text input operation.
done,
}
/// Controls the visual appearance of the text input control.
///
/// See also:
///
/// * [TextInput.attach]
class TextInputConfiguration {
/// Creates configuration information for a text input control.
///
/// The [inputType] argument must not be null.
const TextInputConfiguration({
this.inputType: TextInputType.text,
this.actionLabel,
});
/// For which type of information to optimize the text input control.
final TextInputType inputType;
/// What text to display in the text input control's action button.
final String actionLabel;
/// Returns a representation of this object as a JSON object.
Map<String, dynamic> toJSON() {
return <String, dynamic>{
'inputType': inputType.toString(),
'actionLabel': actionLabel,
};
}
}
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.
class TextEditingState {
/// Creates state for text editing.
///
/// The [selectionBase], [selectionExtent], [selectionAffinity],
/// [selectionIsDirectional], [selectionIsDirectional], [composingBase], and
/// [composingExtent] arguments must not be null.
const TextEditingState({
this.text,
this.selectionBase: -1,
this.selectionExtent: -1,
this.selectionAffinity: TextAffinity.downstream,
this.selectionIsDirectional: false,
this.composingBase: -1,
this.composingExtent: -1,
});
/// The text that is currently being edited.
final String text;
/// The offset in [text] at which the selection originates.
///
/// Might be larger than, smaller than, or equal to [selectionExtent].
final int selectionBase;
/// The offset in [text] at which the selection terminates.
///
/// When the user uses the arrow keys to adjust the selection, this is the
/// value that changes. Similarly, if the current theme paints a caret on one
/// side of the selection, this is the location at which to paint the caret.
///
/// Might be larger than, smaller than, or equal to [selectionBase].
final int selectionExtent;
/// If the the text range is collapsed and has more than one visual location
/// (e.g., occurs at a line break), which of the two locations to use when
/// painting the caret.
final TextAffinity selectionAffinity;
/// Whether this selection has disambiguated its base and extent.
///
/// On some platforms, the base and extent are not disambiguated until the
/// first time the user adjusts the selection. At that point, either the start
/// or the end of the selection becomes the base and the other one becomes the
/// extent and is adjusted.
final bool selectionIsDirectional;
/// The offset in [text] at which the composing region originates.
///
/// Always smaller than, or equal to, [composingExtent].
final int composingBase;
/// The offset in [text] at which the selection terminates.
///
/// Always larger than, or equal to, [composingBase].
final int composingExtent;
/// Creates an instance of this class from a JSON object.
factory TextEditingState.fromJSON(Map<String, dynamic> encoded) {
return new TextEditingState(
text: encoded['text'],
selectionBase: encoded['selectionBase'] ?? -1,
selectionExtent: encoded['selectionExtent'] ?? -1,
selectionIsDirectional: encoded['selectionIsDirectional'] ?? false,
selectionAffinity: _toTextAffinity(encoded['selectionAffinity']) ?? TextAffinity.downstream,
composingBase: encoded['composingBase'] ?? -1,
composingExtent: encoded['composingExtent'] ?? -1,
);
}
/// Returns a representation of this object as a JSON object.
Map<String, dynamic> toJSON() {
return <String, dynamic>{
'text': text,
'selectionBase': selectionBase,
'selectionExtent': selectionExtent,
'selectionAffinity': selectionAffinity.toString(),
'selectionIsDirectional': selectionIsDirectional,
'composingBase': composingBase,
'composingExtent': composingExtent,
};
}
}
/// 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 updateEditingState(TextEditingState state);
/// Requests that this client perform the given action.
void performAction(TextInputAction action);
}
const String _kChannelName = 'flutter/textinput';
/// A interface for interacting with a text input control.
///
/// See also:
///
/// * [TextInput.attach]
class TextInputConnection {
TextInputConnection._(this._client) : _id = _nextId++ {
assert(_client != null);
}
static int _nextId = 1;
final int _id;
final TextInputClient _client;
/// Whether this connection is currently interacting with the text input control.
bool get attached => _attached;
bool _attached = true;
bool get _isAttachedAndCurrent {
return _attached && _clientHandler._currentConnection == this;
}
/// Requests that the text input control become visible.
void show() {
assert(_isAttachedAndCurrent);
PlatformMessages.invokeMethod(_kChannelName, 'TextInput.show');
}
/// Requests that the text input control change its internal state to match the given state.
void setEditingState(TextEditingState state) {
assert(_isAttachedAndCurrent);
PlatformMessages.invokeMethod(
_kChannelName,
'TextInput.setEditingState',
<dynamic>[ state.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) {
assert(_clientHandler._currentConnection == this);
_attached = false;
PlatformMessages.invokeMethod(_kChannelName, 'TextInput.clearClient');
_clientHandler
.._currentConnection = null
.._scheduleHide();
}
assert(_clientHandler._currentConnection != this);
}
}
TextInputAction _toTextInputAction(String action) {
switch (action) {
case 'TextInputAction.done':
return TextInputAction.done;
}
throw new FlutterError('Unknow text input action: $action');
}
class _TextInputClientHandler {
_TextInputClientHandler() {
PlatformMessages.setJSONMessageHandler('flutter/textinputclient', _handleMessage);
}
TextInputConnection _currentConnection;
Future<Null> _handleMessage(dynamic message) async {
if (_currentConnection == null)
return;
final String method = message['method'];
final List<dynamic> args = message['args'];
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.updateEditingState(new TextEditingState.fromJSON(args[1]));
break;
case 'TextInputClient.performAction':
_currentConnection._client.performAction(_toTextInputAction(args[1]));
break;
}
}
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)
PlatformMessages.invokeMethod(_kChannelName, '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;
PlatformMessages.invokeMethod(
_kChannelName,
'TextInput.setClient',
<dynamic>[ connection._id, configuration.toJSON() ],
);
return connection;
}
}
......@@ -19,9 +19,10 @@ class UrlLauncher {
/// * [urlString]: The URL string to be parsed by the underlying platform and
/// before it attempts to launch the same.
static Future<Null> launch(String urlString) async {
await PlatformMessages.sendJSON('flutter/platform', <String, dynamic>{
'method': 'UrlLauncher.launch',
'args': <String>[ urlString ],
});
await PlatformMessages.invokeMethod(
'flutter/platform',
'UrlLauncher.launch',
<String>[ urlString ],
);
}
}
......@@ -36,9 +36,9 @@ class MockClipboard {
'text': null
};
Future<dynamic> handleJSONMessage(dynamic json) async {
final String method = json['method'];
final List<dynamic> args= json['args'];
Future<dynamic> handleJSONMessage(dynamic message) async {
final String method = message['method'];
final List<dynamic> args= message['args'];
switch (method) {
case 'Clipboard.getData':
return _clipboardData;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment