Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
Front-End
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
abdullh.alsoleman
Front-End
Commits
7957c569
Unverified
Commit
7957c569
authored
Nov 05, 2019
by
Dan Field
Committed by
GitHub
Nov 05, 2019
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Respond to TextInputClient.reattach messages. (#43959)
parent
b94c1a41
Changes
4
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
297 additions
and
85 deletions
+297
-85
system_channels.dart
packages/flutter/lib/src/services/system_channels.dart
+7
-0
text_input.dart
packages/flutter/lib/src/services/text_input.dart
+150
-72
editable_text.dart
packages/flutter/lib/src/widgets/editable_text.dart
+13
-12
text_input_test.dart
packages/flutter/test/services/text_input_test.dart
+127
-1
No files found.
packages/flutter/lib/src/services/system_channels.dart
View file @
7957c569
...
...
@@ -159,6 +159,13 @@ class SystemChannels {
/// second argument is a [String] consisting of the stringification of one
/// of the values of the [TextInputAction] enum.
///
/// * `TextInputClient.requestExistingInputState`: The embedding may have
/// lost its internal state about the current editing client, if there is
/// one. The framework should call `TextInput.setClient` and
/// `TextInput.setEditingState` again with its most recent information. If
/// there is no existing state on the framework side, the call should
/// fizzle.
///
/// * `TextInputClient.onConnectionClosed`: The text input connection closed
/// on the platform side. For example the application is moved to
/// background or used closed the virtual keyboard. This method informs
...
...
packages/flutter/lib/src/services/text_input.dart
View file @
7957c569
...
...
@@ -17,6 +17,7 @@ import 'package:flutter/foundation.dart';
import
'package:vector_math/vector_math_64.dart'
show
Matrix4
;
import
'message_codec.dart'
;
import
'platform_channel.dart'
;
import
'system_channels.dart'
;
import
'system_chrome.dart'
;
import
'text_editing.dart'
;
...
...
@@ -673,24 +674,34 @@ class TextInputConnection {
static
int
_nextId
=
1
;
final
int
_id
;
/// Resets the internal ID counter for testing purposes.
///
/// This call has no effect when asserts are disabled. Calling it from
/// application code will likely break text input for the application.
@visibleForTesting
static
void
debugResetId
({
int
to
=
1
})
{
assert
(
to
!=
null
);
assert
(()
{
_nextId
=
to
;
return
true
;
}());
}
final
TextInputClient
_client
;
/// Whether this connection is currently interacting with the text input control.
bool
get
attached
=>
_clientHandler
.
_currentConnection
==
this
;
bool
get
attached
=>
TextInput
.
_instance
.
_currentConnection
==
this
;
/// Requests that the text input control become visible.
void
show
()
{
assert
(
attached
);
SystemChannels
.
textInput
.
invokeMethod
<
void
>(
'TextInput.show'
);
TextInput
.
_instance
.
_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
<
void
>(
'TextInput.setEditingState'
,
value
.
toJSON
(),
);
TextInput
.
_instance
.
_setEditingState
(
value
);
}
/// Send the size and transform of the editable text to engine.
...
...
@@ -706,8 +717,7 @@ class TextInputConnection {
if
(
editableBoxSize
!=
_cachedSize
||
transform
!=
_cachedTransform
)
{
_cachedSize
=
editableBoxSize
;
_cachedTransform
=
transform
;
SystemChannels
.
textInput
.
invokeMethod
<
void
>(
'TextInput.setEditableSizeAndTransform'
,
TextInput
.
_instance
.
_setEditableSizeAndTransform
(
<
String
,
dynamic
>{
'width'
:
editableBoxSize
.
width
,
'height'
:
editableBoxSize
.
height
,
...
...
@@ -731,8 +741,7 @@ class TextInputConnection {
})
{
assert
(
attached
);
SystemChannels
.
textInput
.
invokeMethod
<
void
>(
'TextInput.setStyle'
,
TextInput
.
_instance
.
_setStyle
(
<
String
,
dynamic
>{
'fontFamily'
:
fontFamily
,
'fontSize'
:
fontSize
,
...
...
@@ -749,10 +758,7 @@ class TextInputConnection {
/// other client attaches to it within this animation frame.
void
close
()
{
if
(
attached
)
{
SystemChannels
.
textInput
.
invokeMethod
<
void
>(
'TextInput.clearClient'
);
_clientHandler
..
_currentConnection
=
null
..
_scheduleHide
();
TextInput
.
_instance
.
_clearClient
();
}
assert
(!
attached
);
}
...
...
@@ -761,7 +767,7 @@ class TextInputConnection {
///
/// [TextInputConnection] should clean current client connection.
void
connectionClosedReceived
()
{
_clientHandler
.
_currentConnection
=
null
;
TextInput
.
_instance
.
_currentConnection
=
null
;
assert
(!
attached
);
}
}
...
...
@@ -818,63 +824,28 @@ RawFloatingCursorPoint _toTextPoint(FloatingCursorDragState state, Map<String, d
return
RawFloatingCursorPoint
(
offset:
offset
,
state:
state
);
}
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
(
TextEditingValue
.
fromJSON
(
args
[
1
]));
break
;
case
'TextInputClient.performAction'
:
_currentConnection
.
_client
.
performAction
(
_toTextInputAction
(
args
[
1
]));
break
;
case
'TextInputClient.updateFloatingCursor'
:
_currentConnection
.
_client
.
updateFloatingCursor
(
_toTextPoint
(
_toTextCursorAction
(
args
[
1
]),
args
[
2
]));
break
;
case
'TextInputClient.onConnectionClosed'
:
_currentConnection
.
_client
.
connectionClosed
();
break
;
default
:
throw
MissingPluginException
();
}
/// An interface to the system's text input control.
class
TextInput
{
TextInput
.
_
()
{
_channel
=
SystemChannels
.
textInput
;
_channel
.
setMethodCallHandler
(
_handleTextInputInvocation
);
}
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
<
void
>(
'TextInput.hide'
);
});
/// Set the [MethodChannel] used to communicate with the system's text input
/// control.
///
/// This is only meant for testing within the Flutter SDK. Changing this
/// will break the ability to input text. This has no effect if asserts are
/// disabled.
@visibleForTesting
static
void
setChannel
(
MethodChannel
newChannel
)
{
assert
(()
{
_instance
.
_channel
=
newChannel
..
setMethodCallHandler
(
_instance
.
_handleTextInputInvocation
);
return
true
;
}());
}
}
final
_TextInputClientHandler
_clientHandler
=
_TextInputClientHandler
();
/// An interface to the system's text input control.
class
TextInput
{
TextInput
.
_
();
static
final
TextInput
_instance
=
TextInput
.
_
();
static
const
List
<
TextInputAction
>
_androidSupportedInputActions
=
<
TextInputAction
>[
TextInputAction
.
none
,
...
...
@@ -915,14 +886,25 @@ class TextInput {
static
TextInputConnection
attach
(
TextInputClient
client
,
TextInputConfiguration
configuration
)
{
assert
(
client
!=
null
);
assert
(
configuration
!=
null
);
assert
(
_debugEnsureInputActionWorksOnPlatform
(
configuration
.
inputAction
));
final
TextInputConnection
connection
=
TextInputConnection
.
_
(
client
);
_clientHandler
.
_currentConnection
=
connection
;
SystemChannels
.
textInput
.
invokeMethod
<
void
>(
_instance
.
_attach
(
connection
,
configuration
);
return
connection
;
}
/// This method actually notifies the embedding of the client. It is utilized
/// by [attach] and by [_handleTextInputInvocation] for the
/// `TextInputClient.requestExistingInputState` method.
void
_attach
(
TextInputConnection
connection
,
TextInputConfiguration
configuration
)
{
assert
(
connection
!=
null
);
assert
(
connection
.
_client
!=
null
);
assert
(
configuration
!=
null
);
assert
(
_debugEnsureInputActionWorksOnPlatform
(
configuration
.
inputAction
));
_channel
.
invokeMethod
<
void
>(
'TextInput.setClient'
,
<
dynamic
>[
connection
.
_id
,
configuration
.
toJson
()
],
);
return
connection
;
_currentConnection
=
connection
;
_currentConfiguration
=
configuration
;
}
static
bool
_debugEnsureInputActionWorksOnPlatform
(
TextInputAction
inputAction
)
{
...
...
@@ -946,4 +928,100 @@ class TextInput {
}());
return
true
;
}
MethodChannel
_channel
;
TextInputConnection
_currentConnection
;
TextInputConfiguration
_currentConfiguration
;
TextEditingValue
_currentTextEditingValue
;
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
);
// This will be null if we've never had a call to [_setEditingState].
if
(
_currentTextEditingValue
!=
null
)
{
_setEditingState
(
_currentTextEditingValue
);
}
return
;
}
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
(
TextEditingValue
.
fromJSON
(
args
[
1
]));
break
;
case
'TextInputClient.performAction'
:
_currentConnection
.
_client
.
performAction
(
_toTextInputAction
(
args
[
1
]));
break
;
case
'TextInputClient.updateFloatingCursor'
:
_currentConnection
.
_client
.
updateFloatingCursor
(
_toTextPoint
(
_toTextCursorAction
(
args
[
1
]),
args
[
2
]));
break
;
case
'TextInputClient.onConnectionClosed'
:
_currentConnection
.
_client
.
connectionClosed
();
break
;
default
:
throw
MissingPluginException
();
}
}
bool
_hidePending
=
false
;
void
_scheduleHide
()
{
if
(
_hidePending
)
return
;
_hidePending
=
true
;
// Schedule a deferred task that hides the text input. If someone else
// shows the keyboard during this update cycle, then the task will do
// nothing.
scheduleMicrotask
(()
{
_hidePending
=
false
;
if
(
_currentConnection
==
null
)
_channel
.
invokeMethod
<
void
>(
'TextInput.hide'
);
});
}
void
_clearClient
()
{
_channel
.
invokeMethod
<
void
>(
'TextInput.clearClient'
);
_currentConnection
=
null
;
_scheduleHide
();
}
void
_setEditingState
(
TextEditingValue
value
)
{
assert
(
value
!=
null
);
_channel
.
invokeMethod
<
void
>(
'TextInput.setEditingState'
,
value
.
toJSON
(),
);
_currentTextEditingValue
=
value
;
}
void
_show
()
{
_channel
.
invokeMethod
<
void
>(
'TextInput.show'
);
}
void
_setEditableSizeAndTransform
(
Map
<
String
,
dynamic
>
args
)
{
_channel
.
invokeMethod
<
void
>(
'TextInput.setEditableSizeAndTransform'
,
args
,
);
}
void
_setStyle
(
Map
<
String
,
dynamic
>
args
)
{
_channel
.
invokeMethod
<
void
>(
'TextInput.setStyle'
,
args
,
);
}
}
packages/flutter/lib/src/widgets/editable_text.dart
View file @
7957c569
...
...
@@ -1395,19 +1395,20 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
if
(!
_hasInputConnection
)
{
final
TextEditingValue
localValue
=
_value
;
_lastKnownRemoteTextEditingValue
=
localValue
;
_textInputConnection
=
TextInput
.
attach
(
this
,
TextInputConfiguration
(
inputType:
widget
.
keyboardType
,
obscureText:
widget
.
obscureText
,
autocorrect:
widget
.
autocorrect
,
enableSuggestions:
widget
.
enableSuggestions
,
inputAction:
widget
.
textInputAction
??
(
widget
.
keyboardType
==
TextInputType
.
multiline
?
TextInputAction
.
newline
:
TextInputAction
.
done
),
textCapitalization:
widget
.
textCapitalization
,
keyboardAppearance:
widget
.
keyboardAppearance
,
_textInputConnection
=
TextInput
.
attach
(
this
,
TextInputConfiguration
(
inputType:
widget
.
keyboardType
,
obscureText:
widget
.
obscureText
,
autocorrect:
widget
.
autocorrect
,
enableSuggestions:
widget
.
enableSuggestions
,
inputAction:
widget
.
textInputAction
??
(
widget
.
keyboardType
==
TextInputType
.
multiline
?
TextInputAction
.
newline
:
TextInputAction
.
done
),
textCapitalization:
widget
.
textCapitalization
,
keyboardAppearance:
widget
.
keyboardAppearance
,
),
);
_textInputConnection
.
show
();
...
...
packages/flutter/test/services/text_input_test.dart
View file @
7957c569
...
...
@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'dart:convert'
show
utf8
;
import
'package:flutter/services.dart'
;
import
'package:flutter_test/flutter_test.dart'
show
TestWidgetsFlutterBinding
;
import
'../flutter_test_alternative.dart'
;
...
...
@@ -9,6 +11,66 @@ import '../flutter_test_alternative.dart';
void
main
(
)
{
TestWidgetsFlutterBinding
.
ensureInitialized
();
group
(
'TextInput message channels'
,
()
{
FakeTextChannel
fakeTextChannel
;
FakeTextInputClient
client
;
setUp
(()
{
fakeTextChannel
=
FakeTextChannel
((
MethodCall
call
)
async
{});
TextInput
.
setChannel
(
fakeTextChannel
);
client
=
FakeTextInputClient
();
});
tearDown
(()
{
TextInputConnection
.
debugResetId
();
TextInput
.
setChannel
(
SystemChannels
.
textInput
);
});
test
(
'text input client handler responds to reattach with setClient'
,
()
async
{
TextInput
.
attach
(
client
,
client
.
configuration
);
fakeTextChannel
.
validateOutgoingMethodCalls
(<
MethodCall
>[
MethodCall
(
'TextInput.setClient'
,
<
dynamic
>[
1
,
client
.
configuration
.
toJson
()]),
]);
fakeTextChannel
.
incoming
(
const
MethodCall
(
'TextInputClient.requestExistingInputState'
,
null
));
expect
(
fakeTextChannel
.
outgoingCalls
.
length
,
2
);
fakeTextChannel
.
validateOutgoingMethodCalls
(<
MethodCall
>[
// From original attach
MethodCall
(
'TextInput.setClient'
,
<
dynamic
>[
1
,
client
.
configuration
.
toJson
()]),
// From requestExistingInputState
MethodCall
(
'TextInput.setClient'
,
<
dynamic
>[
1
,
client
.
configuration
.
toJson
()]),
]);
});
test
(
'text input client handler responds to reattach with setClient and text state'
,
()
async
{
final
TextInputConnection
connection
=
TextInput
.
attach
(
client
,
client
.
configuration
);
fakeTextChannel
.
validateOutgoingMethodCalls
(<
MethodCall
>[
MethodCall
(
'TextInput.setClient'
,
<
dynamic
>[
1
,
client
.
configuration
.
toJson
()]),
]);
const
TextEditingValue
editingState
=
TextEditingValue
(
text:
'foo'
);
connection
.
setEditingState
(
editingState
);
fakeTextChannel
.
validateOutgoingMethodCalls
(<
MethodCall
>[
MethodCall
(
'TextInput.setClient'
,
<
dynamic
>[
1
,
client
.
configuration
.
toJson
()]),
MethodCall
(
'TextInput.setEditingState'
,
editingState
.
toJSON
()),
]);
fakeTextChannel
.
incoming
(
const
MethodCall
(
'TextInputClient.requestExistingInputState'
,
null
));
expect
(
fakeTextChannel
.
outgoingCalls
.
length
,
4
);
fakeTextChannel
.
validateOutgoingMethodCalls
(<
MethodCall
>[
// attach
MethodCall
(
'TextInput.setClient'
,
<
dynamic
>[
1
,
client
.
configuration
.
toJson
()]),
// set editing state 1
MethodCall
(
'TextInput.setEditingState'
,
editingState
.
toJSON
()),
// both from requestExistingInputState
MethodCall
(
'TextInput.setClient'
,
<
dynamic
>[
1
,
client
.
configuration
.
toJson
()]),
MethodCall
(
'TextInput.setEditingState'
,
editingState
.
toJSON
()),
]);
});
});
group
(
'TextInputConfiguration'
,
()
{
test
(
'sets expected defaults'
,
()
{
const
TextInputConfiguration
configuration
=
TextInputConfiguration
();
...
...
@@ -113,7 +175,7 @@ void main() {
});
}
class
FakeTextInputClient
extend
s
TextInputClient
{
class
FakeTextInputClient
implement
s
TextInputClient
{
String
latestMethodCall
=
''
;
@override
...
...
@@ -135,4 +197,68 @@ class FakeTextInputClient extends TextInputClient {
void
connectionClosed
()
{
latestMethodCall
=
'connectionClosed'
;
}
TextInputConfiguration
get
configuration
=>
const
TextInputConfiguration
();
}
class
FakeTextChannel
implements
MethodChannel
{
FakeTextChannel
(
this
.
outgoing
)
:
assert
(
outgoing
!=
null
);
Future
<
void
>
Function
(
MethodCall
)
outgoing
;
Future
<
void
>
Function
(
MethodCall
)
incoming
;
List
<
MethodCall
>
outgoingCalls
=
<
MethodCall
>[];
@override
BinaryMessenger
get
binaryMessenger
=>
throw
UnimplementedError
();
@override
MethodCodec
get
codec
=>
const
JSONMethodCodec
();
@override
Future
<
List
<
T
>>
invokeListMethod
<
T
>(
String
method
,
[
dynamic
arguments
])
=>
throw
UnimplementedError
();
@override
Future
<
Map
<
K
,
V
>>
invokeMapMethod
<
K
,
V
>(
String
method
,
[
dynamic
arguments
])
=>
throw
UnimplementedError
();
@override
Future
<
T
>
invokeMethod
<
T
>(
String
method
,
[
dynamic
arguments
])
{
final
MethodCall
call
=
MethodCall
(
method
,
arguments
);
outgoingCalls
.
add
(
call
);
return
outgoing
(
call
);
}
@override
String
get
name
=>
'flutter/textinput'
;
@override
void
setMethodCallHandler
(
Future
<
void
>
Function
(
MethodCall
call
)
handler
)
{
incoming
=
handler
;
}
@override
void
setMockMethodCallHandler
(
Future
<
void
>
Function
(
MethodCall
call
)
handler
)
=>
throw
UnimplementedError
();
void
validateOutgoingMethodCalls
(
List
<
MethodCall
>
calls
)
{
expect
(
outgoingCalls
.
length
,
calls
.
length
);
bool
hasError
=
false
;
for
(
int
i
=
0
;
i
<
calls
.
length
;
i
++)
{
final
ByteData
outgoingData
=
codec
.
encodeMethodCall
(
outgoingCalls
[
i
]);
final
ByteData
expectedData
=
codec
.
encodeMethodCall
(
calls
[
i
]);
final
String
outgoingString
=
utf8
.
decode
(
outgoingData
.
buffer
.
asUint8List
());
final
String
expectedString
=
utf8
.
decode
(
expectedData
.
buffer
.
asUint8List
());
if
(
outgoingString
!=
expectedString
)
{
print
(
'Index
$i
did not match:
\n
'
' actual:
${outgoingCalls[i]}
'
' expected:
${calls[i]}
'
);
hasError
=
true
;
}
}
if
(
hasError
)
{
fail
(
'Calls did not match.'
);
}
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment