Revert "[text_input] introduce TextInputControl (#76072)" (#113724)

This reverts commit 2fdfa29e08.
This commit is contained in:
Zachary Anderson 2022-10-19 15:00:00 -07:00 committed by GitHub
parent 38ef9410b4
commit 9064bf674b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 88 additions and 907 deletions

View file

@ -77,7 +77,6 @@ Hidenori Matsubayashi <Hidenori.Matsubayashi@sony.com>
Perqin Xie <perqinxie@gmail.com>
Seongyun Kim <helloworld@cau.ac.kr>
Ludwik Trammer <ludwik@gmail.com>
J-P Nurmi <jpnurmi@gmail.com>
Marian Triebe <m.triebe@live.de>
Alexis Rouillard <contact@arouillard.fr>
Mirko Mucaria <skogsfrae@gmail.com>

View file

@ -1,167 +0,0 @@
// 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.
// Flutter code sample for TextInputControl
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: MyStatefulWidget(),
);
}
}
class MyStatefulWidget extends StatefulWidget {
const MyStatefulWidget({super.key});
@override
MyStatefulWidgetState createState() => MyStatefulWidgetState();
}
class MyStatefulWidgetState extends State<MyStatefulWidget> {
final TextEditingController _controller = TextEditingController();
final FocusNode _focusNode = FocusNode();
@override
void dispose() {
super.dispose();
_controller.dispose();
_focusNode.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: TextField(
autofocus: true,
controller: _controller,
focusNode: _focusNode,
decoration: InputDecoration(
suffix: IconButton(
icon: const Icon(Icons.clear),
tooltip: 'Clear and unfocus',
onPressed: () {
_controller.clear();
_focusNode.unfocus();
},
),
),
),
),
bottomSheet: const MyVirtualKeyboard(),
);
}
}
class MyVirtualKeyboard extends StatefulWidget {
const MyVirtualKeyboard({super.key});
@override
MyVirtualKeyboardState createState() => MyVirtualKeyboardState();
}
class MyVirtualKeyboardState extends State<MyVirtualKeyboard> {
final MyTextInputControl _inputControl = MyTextInputControl();
@override
void initState() {
super.initState();
_inputControl.register();
}
@override
void dispose() {
super.dispose();
_inputControl.unregister();
}
void _handleKeyPress(String key) {
_inputControl.processUserInput(key);
}
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<bool>(
valueListenable: _inputControl.visible,
builder: (_, bool visible, __) {
return Visibility(
visible: visible,
child: FocusScope(
canRequestFocus: false,
child: TextFieldTapRegion(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
for (final String key in <String>['A', 'B', 'C'])
ElevatedButton(
child: Text(key),
onPressed: () => _handleKeyPress(key),
),
],
),
),
),
);
},
);
}
}
class MyTextInputControl with TextInputControl {
TextEditingValue _editingState = TextEditingValue.empty;
final ValueNotifier<bool> _visible = ValueNotifier<bool>(false);
/// The input control's visibility state for updating the visual presentation.
ValueListenable<bool> get visible => _visible;
/// Register the input control.
void register() => TextInput.setInputControl(this);
/// Restore the original platform input control.
void unregister() => TextInput.restorePlatformInputControl();
@override
void show() => _visible.value = true;
@override
void hide() => _visible.value = false;
@override
void setEditingState(TextEditingValue value) => _editingState = value;
/// Process user input.
///
/// Updates the internal editing state by inserting the input text,
/// and by replacing the current selection if any.
void processUserInput(String input) {
_editingState = _editingState.copyWith(
text: _insertText(input),
selection: _replaceSelection(input),
);
// Request the attached client to update accordingly.
TextInput.updateEditingValue(_editingState);
}
String _insertText(String input) {
final String text = _editingState.text;
final TextSelection selection = _editingState.selection;
return text.replaceRange(selection.start, selection.end, input);
}
TextSelection _replaceSelection(String input) {
final TextSelection selection = _editingState.selection;
return TextSelection.collapsed(offset: selection.start + input.length);
}
}

View file

@ -1,40 +0,0 @@
// 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 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_api_samples/services/text_input/text_input_control.0.dart'
as example;
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Enter text using the VKB', (WidgetTester tester) async {
await tester.pumpWidget(const example.MyApp());
await tester.pumpAndSettle();
await tester.tap(find.descendant(
of: find.byType(example.MyVirtualKeyboard),
matching: find.widgetWithText(ElevatedButton, 'A'),
));
await tester.pumpAndSettle();
expect(find.widgetWithText(TextField, 'A'), findsOneWidget);
await tester.tap(find.descendant(
of: find.byType(example.MyVirtualKeyboard),
matching: find.widgetWithText(ElevatedButton, 'B'),
));
await tester.pumpAndSettle();
expect(find.widgetWithText(TextField, 'AB'), findsOneWidget);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
await tester.pumpAndSettle();
await tester.tap(find.descendant(
of: find.byType(example.MyVirtualKeyboard),
matching: find.widgetWithText(ElevatedButton, 'C'),
));
await tester.pumpAndSettle();
expect(find.widgetWithText(TextField, 'ACB'), findsOneWidget);
});
}

View file

@ -1150,18 +1150,6 @@ mixin TextInputClient {
/// [TextInputClient] should cleanup its connection and finalize editing.
void connectionClosed();
/// The framework calls this method to notify that the text input control has
/// been changed.
///
/// The [TextInputClient] may switch to the new text input control by hiding
/// the old and showing the new input control.
///
/// See also:
///
/// * [TextInputControl.hide], a method to hide the old input control.
/// * [TextInputControl.show], a method to show the new input control.
void didChangeInputControl(TextInputControl? oldControl, TextInputControl? newControl) {}
/// Requests that the client show the editing toolbar, for example when the
/// platform changes the selection through a non-flutter method such as
/// scribble.
@ -1374,7 +1362,13 @@ class TextInputConnection {
if (editableBoxSize != _cachedSize || transform != _cachedTransform) {
_cachedSize = editableBoxSize;
_cachedTransform = transform;
TextInput._instance._setEditableSizeAndTransform(editableBoxSize, transform);
TextInput._instance._setEditableSizeAndTransform(
<String, dynamic>{
'width': editableBoxSize.width,
'height': editableBoxSize.height,
'transform': transform.storage,
},
);
}
}
@ -1393,7 +1387,14 @@ class TextInputConnection {
}
_cachedRect = rect;
final Rect validRect = rect.isFinite ? rect : Offset.zero & const Size(-1, -1);
TextInput._instance._setComposingTextRect(validRect);
TextInput._instance._setComposingTextRect(
<String, dynamic>{
'width': validRect.width,
'height': validRect.height,
'x': validRect.left,
'y': validRect.top,
},
);
}
/// Sends the coordinates of caret rect. This is used on macOS for positioning
@ -1405,7 +1406,14 @@ class TextInputConnection {
}
_cachedCaretRect = rect;
final Rect validRect = rect.isFinite ? rect : Offset.zero & const Size(-1, -1);
TextInput._instance._setCaretRect(validRect);
TextInput._instance._setCaretRect(
<String, dynamic>{
'width': validRect.width,
'height': validRect.height,
'x': validRect.left,
'y': validRect.top,
},
);
}
/// Send the bounding boxes of the current selected glyphs in the client to
@ -1415,7 +1423,9 @@ class TextInputConnection {
void setSelectionRects(List<SelectionRect> selectionRects) {
if (!listEquals(_cachedSelectionRects, selectionRects)) {
_cachedSelectionRects = selectionRects;
TextInput._instance._setSelectionRects(selectionRects);
TextInput._instance._setSelectionRects(selectionRects.map((SelectionRect rect) {
return <num>[rect.bounds.left, rect.bounds.top, rect.bounds.width, rect.bounds.height, rect.position];
}).toList());
}
}
@ -1434,11 +1444,13 @@ class TextInputConnection {
assert(attached);
TextInput._instance._setStyle(
fontFamily: fontFamily,
fontSize: fontSize,
fontWeight: fontWeight,
textDirection: textDirection,
textAlign: textAlign,
<String, dynamic>{
'fontFamily': fontFamily,
'fontSize': fontSize,
'fontWeightIndex': fontWeight?.index,
'textAlignIndex': textAlign.index,
'textDirectionIndex': textDirection.index,
},
);
}
@ -1591,63 +1603,6 @@ class TextInput {
static final TextInput _instance = TextInput._();
static void _addInputControl(TextInputControl control) {
if (control != _PlatformTextInputControl.instance) {
_instance._inputControls.add(control);
}
}
static void _removeInputControl(TextInputControl control) {
if (control != _PlatformTextInputControl.instance) {
_instance._inputControls.remove(control);
}
}
/// Sets the current text input control.
///
/// The current text input control receives text input state changes and visual
/// text input control requests, such as showing and hiding the input control,
/// from the framework.
///
/// Setting the current text input control as `null` removes the visual text
/// input control.
///
/// See also:
///
/// * [TextInputControl], an interface for implementing text input controls.
/// * [TextInput.restorePlatformInputControl], a method to restore the default
/// platform text input control.
static void setInputControl(TextInputControl? newControl) {
final TextInputControl? oldControl = _instance._currentControl;
if (newControl == oldControl) {
return;
}
if (newControl != null) {
_addInputControl(newControl);
}
if (oldControl != null) {
_removeInputControl(oldControl);
}
_instance._currentControl = newControl;
final TextInputClient? client = _instance._currentConnection?._client;
client?.didChangeInputControl(oldControl, newControl);
}
/// Restores the default platform text input control.
///
/// See also:
///
/// * [TextInput.setInputControl], a method to set a custom input
/// control, or to remove the visual input control.
static void restorePlatformInputControl() {
setInputControl(_PlatformTextInputControl.instance);
}
TextInputControl? _currentControl = _PlatformTextInputControl.instance;
final Set<TextInputControl> _inputControls = <TextInputControl>{
_PlatformTextInputControl.instance,
};
static const List<TextInputAction> _androidSupportedInputActions = <TextInputAction>[
TextInputAction.none,
TextInputAction.unspecified,
@ -1706,9 +1661,15 @@ class TextInput {
assert(connection._client != null);
assert(configuration != null);
assert(_debugEnsureInputActionWorksOnPlatform(configuration.inputAction));
_channel.invokeMethod<void>(
'TextInput.setClient',
<Object>[
connection._id,
configuration.toJson(),
],
);
_currentConnection = connection;
_currentConfiguration = configuration;
_setClient(connection._client, configuration);
}
static bool _debugEnsureInputActionWorksOnPlatform(TextInputAction inputAction) {
@ -1850,8 +1811,7 @@ class TextInput {
switch (method) {
case 'TextInputClient.updateEditingState':
final TextEditingValue value = TextEditingValue.fromJSON(args[1] as Map<String, dynamic>);
TextInput._instance._updateEditingValue(value, exclude: _PlatformTextInputControl.instance);
_currentConnection!._client.updateEditingValue(TextEditingValue.fromJSON(args[1] as Map<String, dynamic>));
break;
case 'TextInputClient.updateEditingStateWithDeltas':
assert(_currentConnection!._client is DeltaTextInputClient, 'You must be using a DeltaTextInputClient if TextInputConfiguration.enableDeltaModel is set to true');
@ -1922,119 +1882,74 @@ class TextInput {
scheduleMicrotask(() {
_hidePending = false;
if (_currentConnection == null) {
_hide();
_channel.invokeMethod<void>('TextInput.hide');
}
});
}
void _setClient(TextInputClient client, TextInputConfiguration configuration) {
for (final TextInputControl control in _inputControls) {
control.attach(client, configuration);
}
}
void _clearClient() {
final TextInputClient client = _currentConnection!._client;
for (final TextInputControl control in _inputControls) {
control.detach(client);
}
_channel.invokeMethod<void>('TextInput.clearClient');
_currentConnection = null;
_scheduleHide();
}
void _updateConfig(TextInputConfiguration configuration) {
assert(configuration != null);
for (final TextInputControl control in _inputControls) {
control.updateConfig(configuration);
}
_channel.invokeMethod<void>(
'TextInput.updateConfig',
configuration.toJson(),
);
}
void _setEditingState(TextEditingValue value) {
assert(value != null);
for (final TextInputControl control in _inputControls) {
control.setEditingState(value);
}
_channel.invokeMethod<void>(
'TextInput.setEditingState',
value.toJSON(),
);
}
void _show() {
for (final TextInputControl control in _inputControls) {
control.show();
}
}
void _hide() {
for (final TextInputControl control in _inputControls) {
control.hide();
}
}
void _setEditableSizeAndTransform(Size editableBoxSize, Matrix4 transform) {
for (final TextInputControl control in _inputControls) {
control.setEditableSizeAndTransform(editableBoxSize, transform);
}
}
void _setComposingTextRect(Rect rect) {
for (final TextInputControl control in _inputControls) {
control.setComposingRect(rect);
}
}
void _setCaretRect(Rect rect) {
for (final TextInputControl control in _inputControls) {
control.setCaretRect(rect);
}
}
void _setSelectionRects(List<SelectionRect> selectionRects) {
for (final TextInputControl control in _inputControls) {
control.setSelectionRects(selectionRects);
}
}
void _setStyle({
required String? fontFamily,
required double? fontSize,
required FontWeight? fontWeight,
required TextDirection textDirection,
required TextAlign textAlign,
}) {
for (final TextInputControl control in _inputControls) {
control.setStyle(
fontFamily: fontFamily,
fontSize: fontSize,
fontWeight: fontWeight,
textDirection: textDirection,
textAlign: textAlign,
);
}
_channel.invokeMethod<void>('TextInput.show');
}
void _requestAutofill() {
for (final TextInputControl control in _inputControls) {
control.requestAutofill();
}
_channel.invokeMethod<void>('TextInput.requestAutofill');
}
void _updateEditingValue(TextEditingValue value, {TextInputControl? exclude}) {
if (_currentConnection == null) {
return;
}
for (final TextInputControl control in _instance._inputControls) {
if (control != exclude) {
control.setEditingState(value);
}
}
_instance._currentConnection!._client.updateEditingValue(value);
void _setEditableSizeAndTransform(Map<String, dynamic> args) {
_channel.invokeMethod<void>(
'TextInput.setEditableSizeAndTransform',
args,
);
}
/// Updates the editing value of the attached input client.
///
/// This method should be called by the text input control implementation to
/// send editing value updates to the attached input client.
static void updateEditingValue(TextEditingValue value) {
_instance._updateEditingValue(value, exclude: _instance._currentControl);
void _setComposingTextRect(Map<String, dynamic> args) {
_channel.invokeMethod<void>(
'TextInput.setMarkedTextRect',
args,
);
}
void _setCaretRect(Map<String, dynamic> args) {
_channel.invokeMethod<void>(
'TextInput.setCaretRect',
args,
);
}
void _setSelectionRects(List<List<num>> args) {
_channel.invokeMethod<void>(
'TextInput.setSelectionRects',
args,
);
}
void _setStyle(Map<String, dynamic> args) {
_channel.invokeMethod<void>(
'TextInput.setStyle',
args,
);
}
/// Finishes the current autofill context, and potentially saves the user
@ -2087,9 +2002,10 @@ class TextInput {
/// topmost [AutofillGroup] is getting disposed.
static void finishAutofillContext({ bool shouldSave = true }) {
assert(shouldSave != null);
for (final TextInputControl control in TextInput._instance._inputControls) {
control.finishAutofillContext(shouldSave: shouldSave);
}
TextInput._instance._channel.invokeMethod<void>(
'TextInput.finishAutofillContext',
shouldSave,
);
}
/// Registers a [ScribbleClient] with [elementIdentifier] that can be focused
@ -2106,257 +2022,3 @@ class TextInput {
TextInput._instance._scribbleClients.remove(elementIdentifier);
}
}
/// An interface for implementing text input controls that receive text editing
/// state changes and visual input control requests.
///
/// Editing state changes and input control requests are sent by the framework
/// when the editing state of the attached text input client changes, or it
/// requests the input control to be shown or hidden, for example.
///
/// The input control can be installed with [TextInput.setInputControl], and the
/// default platform text input control can be restored with
/// [TextInput.restorePlatformInputControl].
///
/// The [TextInputControl] class must be extended. [TextInputControl]
/// implementations should call [TextInput.updateEditingValue] to send user
/// input to the attached input client.
///
/// {@tool dartpad}
/// This example illustrates a basic [TextInputControl] implementation.
///
/// ** See code in examples/api/lib/services/text_input/text_input_control.0.dart **
/// {@end-tool}
///
/// See also:
///
/// * [TextInput.setInputControl], a method to install a custom text input control.
/// * [TextInput.restorePlatformInputControl], a method to restore the default
/// platform text input control.
/// * [TextInput.updateEditingValue], a method to send user input to
/// the framework.
mixin TextInputControl {
/// Requests the text input control to attach to the given input client.
///
/// This method is called when a text input client is attached. The input
/// control should update its configuration to match the client's configuration.
void attach(TextInputClient client, TextInputConfiguration configuration) {}
/// Requests the text input control to detach from the given input client.
///
/// This method is called when a text input client is detached. The input
/// control should release any resources allocated for the client.
void detach(TextInputClient client) {}
/// Requests that the text input control is shown.
///
/// This method is called when the input control should become visible.
void show() {}
/// Requests that the text input control is hidden.
///
/// This method is called when the input control should hide.
void hide() {}
/// Informs the text input control about input configuration changes.
///
/// This method is called when the configuration of the attached input client
/// has changed.
void updateConfig(TextInputConfiguration configuration) {}
/// Informs the text input control about editing state changes.
///
/// This method is called when the editing state of the attached input client
/// has changed.
void setEditingState(TextEditingValue value) {}
/// Informs the text input control about client position changes.
///
/// This method is called on when the input control should position itself in
/// relation to the attached input client.
void setEditableSizeAndTransform(Size editableBoxSize, Matrix4 transform) {}
/// Informs the text input control about composing area changes.
///
/// This method is called when the attached input client's composing area
/// changes.
void setComposingRect(Rect rect) {}
/// Informs the text input control about caret area changes.
///
/// This method is called when the attached input client's caret area
/// changes.
void setCaretRect(Rect rect) {}
/// Informs the text input control about selection area changes.
///
/// This method is called when the attached input client's selection area
/// changes.
void setSelectionRects(List<SelectionRect> selectionRects) {}
/// Informs the text input control about text style changes.
///
/// This method is called on the when the attached input client's text style
/// changes.
void setStyle({
required String? fontFamily,
required double? fontSize,
required FontWeight? fontWeight,
required TextDirection textDirection,
required TextAlign textAlign,
}) {}
/// Requests autofill from the text input control.
///
/// This method is called when the autofill UI should appear.
void requestAutofill() {}
/// Requests that the autofill context is finalized.
///
/// See also:
///
/// * [TextInput.finishAutofillContext]
void finishAutofillContext({bool shouldSave = true}) {}
}
/// Provides access to the platform text input control.
class _PlatformTextInputControl with TextInputControl {
_PlatformTextInputControl._();
/// The shared instance of [_PlatformTextInputControl].
static final _PlatformTextInputControl instance = _PlatformTextInputControl._();
MethodChannel get _channel => TextInput._instance._channel;
Map<String, dynamic> _configurationToJson(TextInputConfiguration configuration) {
final Map<String, dynamic> json = configuration.toJson();
if (TextInput._instance._currentControl != _PlatformTextInputControl.instance) {
json['inputType'] = TextInputType.none.toJson();
}
return json;
}
@override
void attach(TextInputClient client, TextInputConfiguration configuration) {
_channel.invokeMethod<void>(
'TextInput.setClient',
<Object>[
TextInput._instance._currentConnection!._id,
_configurationToJson(configuration),
],
);
}
@override
void detach(TextInputClient client) {
_channel.invokeMethod<void>('TextInput.clearClient');
}
@override
void updateConfig(TextInputConfiguration configuration) {
_channel.invokeMethod<void>(
'TextInput.updateConfig',
_configurationToJson(configuration),
);
}
@override
void setEditingState(TextEditingValue value) {
_channel.invokeMethod<void>(
'TextInput.setEditingState',
value.toJSON(),
);
}
@override
void show() {
_channel.invokeMethod<void>('TextInput.show');
}
@override
void hide() {
_channel.invokeMethod<void>('TextInput.hide');
}
@override
void setEditableSizeAndTransform(Size editableBoxSize, Matrix4 transform) {
_channel.invokeMethod<void>(
'TextInput.setEditableSizeAndTransform',
<String, dynamic>{
'width': editableBoxSize.width,
'height': editableBoxSize.height,
'transform': transform.storage,
},
);
}
@override
void setComposingRect(Rect rect) {
_channel.invokeMethod<void>(
'TextInput.setMarkedTextRect',
<String, dynamic>{
'width': rect.width,
'height': rect.height,
'x': rect.left,
'y': rect.top,
},
);
}
@override
void setCaretRect(Rect rect) {
_channel.invokeMethod<void>(
'TextInput.setCaretRect',
<String, dynamic>{
'width': rect.width,
'height': rect.height,
'x': rect.left,
'y': rect.top,
},
);
}
@override
void setSelectionRects(List<SelectionRect> selectionRects) {
_channel.invokeMethod<void>(
'TextInput.setSelectionRects',
selectionRects.map((SelectionRect rect) {
return <num>[rect.bounds.left, rect.bounds.top, rect.bounds.width, rect.bounds.height, rect.position];
}).toList(),
);
}
@override
void setStyle({
required String? fontFamily,
required double? fontSize,
required FontWeight? fontWeight,
required TextDirection textDirection,
required TextAlign textAlign,
}) {
_channel.invokeMethod<void>(
'TextInput.setStyle',
<String, dynamic>{
'fontFamily': fontFamily,
'fontSize': fontSize,
'fontWeightIndex': fontWeight?.index,
'textAlignIndex': textAlign.index,
'textDirectionIndex': textDirection.index,
},
);
}
@override
void requestAutofill() {
_channel.invokeMethod<void>('TextInput.requestAutofill');
}
@override
void finishAutofillContext({bool shouldSave = true}) {
_channel.invokeMethod<void>(
'TextInput.finishAutofillContext',
shouldSave,
);
}
}

View file

@ -2675,14 +2675,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
}
@override
void didChangeInputControl(TextInputControl? oldControl, TextInputControl? newControl) {
if (_hasFocus && _hasInputConnection) {
oldControl?.hide();
newControl?.show();
}
}
@override
void connectionClosed() {
if (_hasInputConnection) {

View file

@ -139,11 +139,6 @@ class FakeAutofillClient implements TextInputClient, AutofillClient {
latestMethodCall = 'showAutocorrectionPromptRect';
}
@override
void didChangeInputControl(TextInputControl? oldControl, TextInputControl? newControl) {
latestMethodCall = 'didChangeInputControl';
}
@override
void autofill(TextEditingValue newEditingValue) => updateEditingValue(newEditingValue);

View file

@ -292,9 +292,4 @@ class FakeDeltaTextInputClient implements DeltaTextInputClient {
}
TextInputConfiguration get configuration => const TextInputConfiguration(enableDeltaModel: true);
@override
void didChangeInputControl(TextInputControl? oldControl, TextInputControl? newControl) {
latestMethodCall = 'didChangeInputControl';
}
}

View file

@ -780,178 +780,6 @@ void main() {
isTrue,
);
});
group('TextInputControl', () {
late FakeTextChannel fakeTextChannel;
setUp(() {
fakeTextChannel = FakeTextChannel((MethodCall call) async {});
TextInput.setChannel(fakeTextChannel);
});
tearDown(() {
TextInput.restorePlatformInputControl();
TextInputConnection.debugResetId();
TextInput.setChannel(SystemChannels.textInput);
});
test('gets attached and detached', () {
final FakeTextInputControl control = FakeTextInputControl();
TextInput.setInputControl(control);
final FakeTextInputClient client = FakeTextInputClient(TextEditingValue.empty);
final TextInputConnection connection = TextInput.attach(client, const TextInputConfiguration());
final List<String> expectedMethodCalls = <String>['attach'];
expect(control.methodCalls, expectedMethodCalls);
connection.close();
expectedMethodCalls.add('detach');
expect(control.methodCalls, expectedMethodCalls);
});
test('receives text input state changes', () {
final FakeTextInputControl control = FakeTextInputControl();
TextInput.setInputControl(control);
final FakeTextInputClient client = FakeTextInputClient(TextEditingValue.empty);
final TextInputConnection connection = TextInput.attach(client, const TextInputConfiguration());
control.methodCalls.clear();
final List<String> expectedMethodCalls = <String>[];
connection.updateConfig(const TextInputConfiguration());
expectedMethodCalls.add('updateConfig');
expect(control.methodCalls, expectedMethodCalls);
connection.setEditingState(TextEditingValue.empty);
expectedMethodCalls.add('setEditingState');
expect(control.methodCalls, expectedMethodCalls);
connection.close();
expectedMethodCalls.add('detach');
expect(control.methodCalls, expectedMethodCalls);
});
test('does not interfere with platform text input', () {
final FakeTextInputControl control = FakeTextInputControl();
TextInput.setInputControl(control);
final FakeTextInputClient client = FakeTextInputClient(TextEditingValue.empty);
TextInput.attach(client, const TextInputConfiguration());
fakeTextChannel.outgoingCalls.clear();
fakeTextChannel.incoming!(MethodCall('TextInputClient.updateEditingState', <dynamic>[1, TextEditingValue.empty.toJSON()]));
expect(client.latestMethodCall, 'updateEditingValue');
expect(control.methodCalls, <String>['attach', 'setEditingState']);
expect(fakeTextChannel.outgoingCalls, isEmpty);
});
test('both input controls receive requests', () async {
final FakeTextInputControl control = FakeTextInputControl();
TextInput.setInputControl(control);
const TextInputConfiguration textConfig = TextInputConfiguration();
const TextInputConfiguration numberConfig = TextInputConfiguration(inputType: TextInputType.number);
const TextInputConfiguration noneConfig = TextInputConfiguration(inputType: TextInputType.none);
final FakeTextInputClient client = FakeTextInputClient(TextEditingValue.empty);
final TextInputConnection connection = TextInput.attach(client, textConfig);
final List<String> expectedMethodCalls = <String>['attach'];
expect(control.methodCalls, expectedMethodCalls);
expect(control.inputType, TextInputType.text);
fakeTextChannel.validateOutgoingMethodCalls(<MethodCall>[
// When there's a custom text input control installed, the platform text
// input control receives TextInputType.none
MethodCall('TextInput.setClient', <dynamic>[1, noneConfig.toJson()]),
]);
connection.show();
expectedMethodCalls.add('show');
expect(control.methodCalls, expectedMethodCalls);
expect(fakeTextChannel.outgoingCalls.length, 2);
expect(fakeTextChannel.outgoingCalls.last.method, 'TextInput.show');
connection.updateConfig(numberConfig);
expectedMethodCalls.add('updateConfig');
expect(control.methodCalls, expectedMethodCalls);
expect(control.inputType, TextInputType.number);
expect(fakeTextChannel.outgoingCalls.length, 3);
fakeTextChannel.validateOutgoingMethodCalls(<MethodCall>[
// When there's a custom text input control installed, the platform text
// input control receives TextInputType.none
MethodCall('TextInput.setClient', <dynamic>[1, noneConfig.toJson()]),
const MethodCall('TextInput.show'),
MethodCall('TextInput.updateConfig', noneConfig.toJson()),
]);
connection.setComposingRect(Rect.zero);
expectedMethodCalls.add('setComposingRect');
expect(control.methodCalls, expectedMethodCalls);
expect(fakeTextChannel.outgoingCalls.length, 4);
expect(fakeTextChannel.outgoingCalls.last.method, 'TextInput.setMarkedTextRect');
connection.setCaretRect(Rect.zero);
expectedMethodCalls.add('setCaretRect');
expect(control.methodCalls, expectedMethodCalls);
expect(fakeTextChannel.outgoingCalls.length, 5);
expect(fakeTextChannel.outgoingCalls.last.method, 'TextInput.setCaretRect');
connection.setEditableSizeAndTransform(Size.zero, Matrix4.identity());
expectedMethodCalls.add('setEditableSizeAndTransform');
expect(control.methodCalls, expectedMethodCalls);
expect(fakeTextChannel.outgoingCalls.length, 6);
expect(fakeTextChannel.outgoingCalls.last.method, 'TextInput.setEditableSizeAndTransform');
connection.setSelectionRects(const <SelectionRect>[SelectionRect(position: 0, bounds: Rect.zero)]);
expectedMethodCalls.add('setSelectionRects');
expect(control.methodCalls, expectedMethodCalls);
expect(fakeTextChannel.outgoingCalls.length, 7);
expect(fakeTextChannel.outgoingCalls.last.method, 'TextInput.setSelectionRects');
connection.setStyle(
fontFamily: null,
fontSize: null,
fontWeight: null,
textDirection: TextDirection.ltr,
textAlign: TextAlign.left,
);
expectedMethodCalls.add('setStyle');
expect(control.methodCalls, expectedMethodCalls);
expect(fakeTextChannel.outgoingCalls.length, 8);
expect(fakeTextChannel.outgoingCalls.last.method, 'TextInput.setStyle');
connection.close();
expectedMethodCalls.add('detach');
expect(control.methodCalls, expectedMethodCalls);
expect(fakeTextChannel.outgoingCalls.length, 9);
expect(fakeTextChannel.outgoingCalls.last.method, 'TextInput.clearClient');
expectedMethodCalls.add('hide');
final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized();
await binding.runAsync(() async {});
await expectLater(control.methodCalls, expectedMethodCalls);
expect(fakeTextChannel.outgoingCalls.length, 10);
expect(fakeTextChannel.outgoingCalls.last.method, 'TextInput.hide');
});
test('notifies changes to the attached client', () async {
final FakeTextInputControl control = FakeTextInputControl();
TextInput.setInputControl(control);
final FakeTextInputClient client = FakeTextInputClient(TextEditingValue.empty);
final TextInputConnection connection = TextInput.attach(client, const TextInputConfiguration());
TextInput.setInputControl(null);
expect(client.latestMethodCall, 'didChangeInputControl');
connection.show();
expect(client.latestMethodCall, 'didChangeInputControl');
});
});
}
class FakeTextInputClient with TextInputClient {
@ -1005,11 +833,6 @@ class FakeTextInputClient with TextInputClient {
TextInputConfiguration get configuration => const TextInputConfiguration();
@override
void didChangeInputControl(TextInputControl? oldControl, TextInputControl? newControl) {
latestMethodCall = 'didChangeInputControl';
}
@override
void insertTextPlaceholder(Size size) {
latestMethodCall = 'insertTextPlaceholder';
@ -1026,81 +849,3 @@ class FakeTextInputClient with TextInputClient {
performedSelectors.add(selectorName);
}
}
class FakeTextInputControl with TextInputControl {
final List<String> methodCalls = <String>[];
late TextInputType inputType;
@override
void attach(TextInputClient client, TextInputConfiguration configuration) {
methodCalls.add('attach');
inputType = configuration.inputType;
}
@override
void detach(TextInputClient client) {
methodCalls.add('detach');
}
@override
void setEditingState(TextEditingValue value) {
methodCalls.add('setEditingState');
}
@override
void updateConfig(TextInputConfiguration configuration) {
methodCalls.add('updateConfig');
inputType = configuration.inputType;
}
@override
void show() {
methodCalls.add('show');
}
@override
void hide() {
methodCalls.add('hide');
}
@override
void setComposingRect(Rect rect) {
methodCalls.add('setComposingRect');
}
@override
void setCaretRect(Rect rect) {
methodCalls.add('setCaretRect');
}
@override
void setEditableSizeAndTransform(Size editableBoxSize, Matrix4 transform) {
methodCalls.add('setEditableSizeAndTransform');
}
@override
void setSelectionRects(List<SelectionRect> selectionRects) {
methodCalls.add('setSelectionRects');
}
@override
void setStyle({
required String? fontFamily,
required double? fontSize,
required FontWeight? fontWeight,
required TextDirection textDirection,
required TextAlign textAlign,
}) {
methodCalls.add('setStyle');
}
@override
void finishAutofillContext({bool shouldSave = true}) {
methodCalls.add('finishAutofillContext');
}
@override
void requestAutofill() {
methodCalls.add('requestAutofill');
}
}