Unverified Commit b73722b1 authored by J-P Nurmi's avatar J-P Nurmi Committed by GitHub

[text_input] prepare for custom text input sources (#72803)

parent 58301211
...@@ -860,21 +860,22 @@ abstract class TextInputClient { ...@@ -860,21 +860,22 @@ abstract class TextInputClient {
/// An interface for interacting with a text input control. /// An interface for interacting with a text input control.
/// ///
/// [TextInputConnection] communicates with the platform text input plugin
/// over the [SystemChannels.textInput] method channel. See [SystemChannels.textInput]
/// for more details about the method channel messages.
///
/// See also: /// See also:
/// ///
/// * [TextInput.attach], a method used to establish a [TextInputConnection] /// * [TextInput.attach], a method used to establish a [TextInputConnection]
/// between the system's text input and a [TextInputClient]. /// between the system's text input and a [TextInputClient].
/// * [EditableText], a [TextInputClient] that connects to and interacts with /// * [EditableText], a [TextInputClient] that connects to and interacts with
/// the system's text input using a [TextInputConnection]. /// the system's text input using a [TextInputConnection].
class TextInputConnection { abstract class TextInputConnection {
TextInputConnection._(this._client) /// Creates a connection for a [TextInputClient].
TextInputConnection(this._client)
: assert(_client != null), : assert(_client != null),
_id = _nextId++; _id = _nextId++;
Size? _cachedSize;
Matrix4? _cachedTransform;
Rect? _cachedRect;
static int _nextId = 1; static int _nextId = 1;
final int _id; final int _id;
...@@ -897,10 +898,18 @@ class TextInputConnection { ...@@ -897,10 +898,18 @@ class TextInputConnection {
bool get attached => TextInput._instance._currentConnection == this; bool get attached => TextInput._instance._currentConnection == this;
/// Requests that the text input control become visible. /// Requests that the text input control become visible.
void show() { void show();
assert(attached);
TextInput._instance._show(); /// Requests that the text input control is hidden.
} ///
/// This method is called by the framework when the text input control should
/// hide.
///
/// See also:
///
/// * [TextInput.detach], a method to stop interacting with the text
/// input control.
void hide();
/// Requests the system autofill UI to appear. /// Requests the system autofill UI to appear.
/// ///
...@@ -910,24 +919,23 @@ class TextInputConnection { ...@@ -910,24 +919,23 @@ class TextInputConnection {
/// See also: /// See also:
/// ///
/// * [EditableText], a [TextInputClient] that calls this method when focused. /// * [EditableText], a [TextInputClient] that calls this method when focused.
void requestAutofill() { void requestAutofill();
assert(attached);
TextInput._instance._requestAutofill(); /// This method actually notifies the embedding of the client. It is utilized
} /// by [TextInput.attach] and for the `TextInputClient.requestExistingInputState`
/// method.
void setClient(TextInputConfiguration configuration);
/// Clears the embedding of the client.
void clearClient();
/// Requests that the text input control update itself according to the new /// Requests that the text input control update itself according to the new
/// [TextInputConfiguration]. /// [TextInputConfiguration].
void updateConfig(TextInputConfiguration configuration) { void updateConfig(TextInputConfiguration configuration);
assert(attached);
TextInput._instance._updateConfig(configuration);
}
/// Requests that the text input control change its internal state to match /// Requests that the text input control change its internal state to match
/// the given state. /// the given state.
void setEditingState(TextEditingValue value) { void setEditingState(TextEditingValue value);
assert(attached);
TextInput._instance._setEditingState(value);
}
/// Send the size and transform of the editable text to engine. /// Send the size and transform of the editable text to engine.
/// ///
...@@ -938,11 +946,126 @@ class TextInputConnection { ...@@ -938,11 +946,126 @@ class TextInputConnection {
/// ///
/// 2. [transform]: a matrix that maps the local paint coordinate system /// 2. [transform]: a matrix that maps the local paint coordinate system
/// to the [PipelineOwner.rootNode]. /// to the [PipelineOwner.rootNode].
void setEditableSizeAndTransform(Size editableBoxSize, Matrix4 transform);
/// 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);
/// 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,
});
/// 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.
@Deprecated(
'Use TextInput.detach instead. '
'This feature was deprecated after v1.26.0-1.0.pre.'
)
void close() {
TextInput.detach(_client);
assert(!attached);
}
/// Platform sent a notification informing the connection is closed.
///
/// [TextInputConnection] should clean current client connection.
@Deprecated(
'Use TextInput.reset instead. '
'This feature was deprecated after v1.26.0-1.0.pre.'
)
void connectionClosedReceived() {
TextInput.reset();
assert(!attached);
}
}
// A MethodChannel-based TextInputConnection implementation.
class _TextInputChannelConnection extends TextInputConnection {
_TextInputChannelConnection(TextInputClient client, this._channel)
: super(client);
Size? _cachedSize;
Matrix4? _cachedTransform;
Rect? _cachedRect;
final MethodChannel _channel;
@override
void show() {
assert(attached);
_channel.invokeMethod<void>('TextInput.show');
}
@override
void hide() {
_channel.invokeMethod<void>('TextInput.hide');
}
@override
void requestAutofill() {
assert(attached);
_channel.invokeMethod<void>('TextInput.requestAutofill');
}
@override
void setClient(TextInputConfiguration configuration) {
assert(_client != null);
assert(configuration != null);
_channel.invokeMethod<void>(
'TextInput.setClient',
<dynamic>[_id, configuration.toJson()],
);
}
@override
void clearClient() {
_channel.invokeMethod<void>('TextInput.clearClient');
}
@override
void updateConfig(TextInputConfiguration configuration) {
assert(attached);
assert(configuration != null);
_channel.invokeMethod<void>(
'TextInput.updateConfig',
configuration.toJson(),
);
}
@override
void setEditingState(TextEditingValue value) {
assert(attached);
assert(value != null);
_channel.invokeMethod<void>(
'TextInput.setEditingState',
value.toJSON(),
);
}
@override
void setEditableSizeAndTransform(Size editableBoxSize, Matrix4 transform) { void setEditableSizeAndTransform(Size editableBoxSize, Matrix4 transform) {
if (editableBoxSize != _cachedSize || transform != _cachedTransform) { if (editableBoxSize != _cachedSize || transform != _cachedTransform) {
_cachedSize = editableBoxSize; _cachedSize = editableBoxSize;
_cachedTransform = transform; _cachedTransform = transform;
TextInput._instance._setEditableSizeAndTransform( _channel.invokeMethod<void>(
'TextInput.setEditableSizeAndTransform',
<String, dynamic>{ <String, dynamic>{
'width': editableBoxSize.width, 'width': editableBoxSize.width,
'height': editableBoxSize.height, 'height': editableBoxSize.height,
...@@ -952,20 +1075,15 @@ class TextInputConnection { ...@@ -952,20 +1075,15 @@ class TextInputConnection {
} }
} }
/// Send the smallest rect that covers the text in the client that's currently @override
/// 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) { void setComposingRect(Rect rect) {
assert(rect != null); assert(rect != null);
if (rect == _cachedRect) if (rect == _cachedRect)
return; return;
_cachedRect = rect; _cachedRect = rect;
final Rect validRect = rect.isFinite ? rect : Offset.zero & const Size(-1, -1); final Rect validRect = rect.isFinite ? rect : Offset.zero & const Size(-1, -1);
TextInput._instance._setComposingTextRect( _channel.invokeMethod<void>(
'TextInput.setMarkedTextRect',
<String, dynamic>{ <String, dynamic>{
'width': validRect.width, 'width': validRect.width,
'height': validRect.height, 'height': validRect.height,
...@@ -975,11 +1093,7 @@ class TextInputConnection { ...@@ -975,11 +1093,7 @@ class TextInputConnection {
); );
} }
/// Send text styling information. @override
///
/// 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({ void setStyle({
required String? fontFamily, required String? fontFamily,
required double? fontSize, required double? fontSize,
...@@ -988,8 +1102,8 @@ class TextInputConnection { ...@@ -988,8 +1102,8 @@ class TextInputConnection {
required TextAlign textAlign, required TextAlign textAlign,
}) { }) {
assert(attached); assert(attached);
_channel.invokeMethod<void>(
TextInput._instance._setStyle( 'TextInput.setStyle',
<String, dynamic>{ <String, dynamic>{
'fontFamily': fontFamily, 'fontFamily': fontFamily,
'fontSize': fontSize, 'fontSize': fontSize,
...@@ -1000,23 +1114,68 @@ class TextInputConnection { ...@@ -1000,23 +1114,68 @@ class TextInputConnection {
); );
} }
/// Stop interacting with the text input control. Future<dynamic> _handleTextInputInvocation(MethodCall methodCall) async {
/// final String method = methodCall.method;
/// After calling this method, the text input control might disappear if no
/// other client attaches to it within this animation frame. // The requestExistingInputState request needs to be handled regardless of
void close() { // the client ID, as long as we have a _currentConnection.
if (attached) { if (method == 'TextInputClient.requestExistingInputState') {
TextInput._instance._clearClient(); assert(_client != null);
setClient(TextInput._instance._currentConfiguration);
final TextEditingValue? editingValue = _client.currentTextEditingValue;
if (editingValue != null) {
setEditingState(editingValue);
}
return;
} }
assert(!attached);
}
/// Platform sent a notification informing the connection is closed. final List<dynamic> args = methodCall.arguments as List<dynamic>;
///
/// [TextInputConnection] should clean current client connection. if (method == 'TextInputClient.updateEditingStateWithTag') {
void connectionClosedReceived() { assert(_client != null);
TextInput._instance._currentConnection = null; final AutofillScope? scope = _client.currentAutofillScope;
assert(!attached); 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 clientId = args[0] as int;
// The incoming message was for a different client.
if (clientId != _id)
return;
switch (method) {
case 'TextInputClient.updateEditingState':
_client.updateEditingValue(TextEditingValue.fromJSON(args[1] as Map<String, dynamic>));
break;
case 'TextInputClient.performAction':
_client.performAction(_toTextInputAction(args[1] as String));
break;
case 'TextInputClient.performPrivateCommand':
_client.performPrivateCommand(args[1]['action'] as String,
args[1]['data'] as Map<String, dynamic>);
break;
case 'TextInputClient.updateFloatingCursor':
_client.updateFloatingCursor(_toTextPoint(
_toTextCursorAction(args[1] as String),
args[2] as Map<String, dynamic>,
));
break;
case 'TextInputClient.onConnectionClosed':
_client.connectionClosed();
TextInput.reset();
break;
case 'TextInputClient.showAutocorrectionPromptRect':
_client.showAutocorrectionPromptRect(args[1] as int, args[2] as int);
break;
default:
throw MissingPluginException();
}
} }
} }
...@@ -1129,8 +1288,7 @@ RawFloatingCursorPoint _toTextPoint(FloatingCursorDragState state, Map<String, d ...@@ -1129,8 +1288,7 @@ RawFloatingCursorPoint _toTextPoint(FloatingCursorDragState state, Map<String, d
/// wants to take user input from the keyboard. /// wants to take user input from the keyboard.
class TextInput { class TextInput {
TextInput._() { TextInput._() {
_channel = SystemChannels.textInput; _currentSource.init();
_channel.setMethodCallHandler(_handleTextInputInvocation);
} }
/// Set the [MethodChannel] used to communicate with the system's text input /// Set the [MethodChannel] used to communicate with the system's text input
...@@ -1142,7 +1300,7 @@ class TextInput { ...@@ -1142,7 +1300,7 @@ class TextInput {
@visibleForTesting @visibleForTesting
static void setChannel(MethodChannel newChannel) { static void setChannel(MethodChannel newChannel) {
assert(() { assert(() {
_instance._channel = newChannel..setMethodCallHandler(_instance._handleTextInputInvocation); _TextInputSource.setChannel(newChannel);
return true; return true;
}()); }());
} }
...@@ -1183,28 +1341,23 @@ class TextInput { ...@@ -1183,28 +1341,23 @@ class TextInput {
/// the text input control. /// the text input control.
/// ///
/// A client that no longer wishes to interact 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 /// should call [TextInput.detach].
/// [TextInputConnection].
static TextInputConnection attach(TextInputClient client, TextInputConfiguration configuration) { static TextInputConnection attach(TextInputClient client, TextInputConfiguration configuration) {
assert(client != null); assert(client != null);
assert(configuration != null); assert(configuration != null);
final TextInputConnection connection = TextInputConnection._(client); _instance._detach();
final TextInputConnection connection = _instance._currentSource.attach(client);
_instance._attach(connection, configuration); _instance._attach(connection, configuration);
return connection; return connection;
} }
/// This method actually notifies the embedding of the client. It is utilized // This method actually notifies the embedding of the client.
/// by [attach] and by [_handleTextInputInvocation] for the
/// `TextInputClient.requestExistingInputState` method.
void _attach(TextInputConnection connection, TextInputConfiguration configuration) { void _attach(TextInputConnection connection, TextInputConfiguration configuration) {
assert(connection != null); assert(connection != null);
assert(connection._client != null); assert(connection._client != null);
assert(configuration != null); assert(configuration != null);
assert(_debugEnsureInputActionWorksOnPlatform(configuration.inputAction)); assert(_debugEnsureInputActionWorksOnPlatform(configuration.inputAction));
_channel.invokeMethod<void>( connection.setClient(configuration);
'TextInput.setClient',
<dynamic>[ connection._id, configuration.toJson() ],
);
_currentConnection = connection; _currentConnection = connection;
_currentConfiguration = configuration; _currentConfiguration = configuration;
} }
...@@ -1231,80 +1384,13 @@ class TextInput { ...@@ -1231,80 +1384,13 @@ class TextInput {
return true; return true;
} }
late MethodChannel _channel; final _TextInputSource _currentSource = _TextInputSource();
TextInputConnection? _currentConnection; TextInputConnection? _currentConnection;
late TextInputConfiguration _currentConfiguration; 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; bool _hidePending = false;
void _scheduleHide() { void _scheduleHide(TextInputConnection connection) {
if (_hidePending) if (_hidePending)
return; return;
_hidePending = true; _hidePending = true;
...@@ -1315,59 +1401,50 @@ class TextInput { ...@@ -1315,59 +1401,50 @@ class TextInput {
scheduleMicrotask(() { scheduleMicrotask(() {
_hidePending = false; _hidePending = false;
if (_currentConnection == null) if (_currentConnection == null)
_channel.invokeMethod<void>('TextInput.hide'); connection.hide();
}); });
} }
void _clearClient() { /// Stop interacting with the text input control.
_channel.invokeMethod<void>('TextInput.clearClient'); ///
_currentConnection = null; /// A client that no longer wishes to interact with the text input control
_scheduleHide(); /// should call this method.
} ///
/// After calling this method, the text input control might be requested to
void _updateConfig(TextInputConfiguration configuration) { /// hide if no other client attaches to it within this animation frame.
assert(configuration != null); ///
_channel.invokeMethod<void>( /// See also:
'TextInput.updateConfig', ///
configuration.toJson(), /// * [TextInputConnection.hide], a method called when the text input control
); /// actually should hide.
} static void detach(TextInputClient client) {
assert(client != null);
void _setEditingState(TextEditingValue value) { if (client != _instance._currentConnection?._client)
assert(value != null); return;
_channel.invokeMethod<void>( _instance._detach();
'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) { void _detach() {
_channel.invokeMethod<void>( if (_currentConnection == null)
'TextInput.setMarkedTextRect', return;
args, _currentConnection!.clearClient();
); _scheduleHide(_currentConnection!);
_currentSource.detach(_currentConnection!._client);
_currentConnection = null;
} }
void _setStyle(Map<String, dynamic> args) { /// Resets the current text input connection.
_channel.invokeMethod<void>( ///
'TextInput.setStyle', /// This function should be called to reset the current text input connection
args, /// in case the platform sent a notification informing the connection is
); /// closed.
///
/// See also:
///
/// * [TextInputClient.connectionClosed], a method called to notify the
/// current text input client when the connection is closed.
static void reset() {
_instance._currentConnection = null;
} }
/// Finishes the current autofill context, and potentially saves the user /// Finishes the current autofill context, and potentially saves the user
...@@ -1420,9 +1497,42 @@ class TextInput { ...@@ -1420,9 +1497,42 @@ class TextInput {
/// topmost [AutofillGroup] is getting disposed. /// topmost [AutofillGroup] is getting disposed.
static void finishAutofillContext({ bool shouldSave = true }) { static void finishAutofillContext({ bool shouldSave = true }) {
assert(shouldSave != null); assert(shouldSave != null);
TextInput._instance._channel.invokeMethod<void>( _instance._currentSource.finishAutofillContext(shouldSave: shouldSave);
}
}
class _TextInputSource {
static MethodChannel? _channel;
static void setChannel(MethodChannel newChannel) {
_channel = newChannel..setMethodCallHandler(_handleTextInputInvocation);
}
void init() {
_channel ??= SystemChannels.textInput;
_channel!.setMethodCallHandler(_handleTextInputInvocation);
}
void cleanup() {
_channel!.setMethodCallHandler((MethodCall methodCall) async {});
}
TextInputConnection attach(TextInputClient client) {
return _TextInputChannelConnection(client, _channel!);
}
void detach(TextInputClient client) {}
void finishAutofillContext({bool shouldSave = true}) {
_channel!.invokeMethod<void>(
'TextInput.finishAutofillContext', 'TextInput.finishAutofillContext',
shouldSave , shouldSave,
); );
} }
static Future<dynamic> _handleTextInputInvocation(MethodCall methodCall) async {
final TextInputConnection? connection = TextInput._instance._currentConnection;
if (connection is _TextInputChannelConnection)
return connection._handleTextInputInvocation(methodCall);
}
} }
...@@ -2038,7 +2038,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2038,7 +2038,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
void _closeInputConnectionIfNeeded() { void _closeInputConnectionIfNeeded() {
if (_hasInputConnection) { if (_hasInputConnection) {
_textInputConnection!.close(); TextInput.detach(this);
_textInputConnection = null; _textInputConnection = null;
_lastKnownRemoteTextEditingValue = null; _lastKnownRemoteTextEditingValue = null;
} }
...@@ -2056,7 +2056,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2056,7 +2056,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
@override @override
void connectionClosed() { void connectionClosed() {
if (_hasInputConnection) { if (_hasInputConnection) {
_textInputConnection!.connectionClosedReceived();
_textInputConnection = null; _textInputConnection = null;
_lastKnownRemoteTextEditingValue = null; _lastKnownRemoteTextEditingValue = null;
_finalizeEditing(TextInputAction.done, shouldUnfocus: true); _finalizeEditing(TextInputAction.done, shouldUnfocus: true);
......
...@@ -22,6 +22,7 @@ void main() { ...@@ -22,6 +22,7 @@ void main() {
}); });
tearDown(() { tearDown(() {
TextInput.reset();
TextInputConnection.debugResetId(); TextInputConnection.debugResetId();
TextInput.setChannel(SystemChannels.textInput); TextInput.setChannel(SystemChannels.textInput);
}); });
...@@ -74,6 +75,65 @@ void main() { ...@@ -74,6 +75,65 @@ void main() {
}), }),
]); ]);
}); });
test('text input client is requested to hide on detach', () async {
final FakeTextInputClient client = FakeTextInputClient(TextEditingValue.empty);
TextInput.attach(client, client.configuration);
fakeTextChannel.validateOutgoingMethodCalls(<MethodCall>[
MethodCall('TextInput.setClient', <dynamic>[1, client.configuration.toJson()]),
]);
TextInput.detach(client);
fakeTextChannel.validateOutgoingMethodCalls(<MethodCall>[
// From original attach
MethodCall('TextInput.setClient', <dynamic>[1, client.configuration.toJson()]),
// From detach
const MethodCall('TextInput.clearClient'),
]);
final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized() as TestWidgetsFlutterBinding;
await binding.runAsync(() async {});
await expectLater(fakeTextChannel.outgoingCalls.length, 3);
fakeTextChannel.validateOutgoingMethodCalls(<MethodCall>[
// From original attach
MethodCall('TextInput.setClient', <dynamic>[1, client.configuration.toJson()]),
// From detach
const MethodCall('TextInput.clearClient'),
// From hide
const MethodCall('TextInput.hide'),
]);
});
test('old client is detached when a new client is attached',() {
final FakeTextInputClient client1 = FakeTextInputClient(const TextEditingValue(text: '1'));
final TextInputConnection connection1 = TextInput.attach(client1, client1.configuration);
expect(connection1.attached, isTrue);
fakeTextChannel.validateOutgoingMethodCalls(<MethodCall>[
MethodCall('TextInput.setClient', <dynamic>[1, client1.configuration.toJson()]),
]);
final FakeTextInputClient client2 = FakeTextInputClient(const TextEditingValue(text: '1'));
final TextInputConnection connection2 = TextInput.attach(client2, client2.configuration);
expect(connection2.attached, isTrue);
expect(connection1.attached, isFalse);
fakeTextChannel.validateOutgoingMethodCalls(<MethodCall>[
// From original attach
MethodCall('TextInput.setClient', <dynamic>[1, client1.configuration.toJson()]),
// From internal detach
const MethodCall('TextInput.clearClient'),
// From second attach
MethodCall('TextInput.setClient', <dynamic>[2, client1.configuration.toJson()]),
]);
});
test('text input connection is reset', () async {
final FakeTextInputClient client = FakeTextInputClient(TextEditingValue.empty);
final TextInputConnection connection = TextInput.attach(client, client.configuration);
expect(connection.attached, isTrue);
TextInput.reset();
expect(connection.attached, isFalse);
});
}); });
group('TextInputConfiguration', () { group('TextInputConfiguration', () {
......
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