[macOS] Clear IME mark text on clear input client (flutter/engine#31849)

When the embedder receives a TextInput.clearClient message from the
framework (typically when a text field loses focus), if the user is
currently inputting composing text using an IME, commit the composing
text, end composing, and clear the IME's composing state.

This also exposes a public `editingState` getter on
FlutterTextInputPlugin as part of the TestMethods informal protocol.
This allows us to get at the text editing state as a dictionary in
tests.

Issue: https://github.com/flutter/flutter/issues/92060
This commit is contained in:
Chris Bracken
2022-03-07 10:30:46 -08:00
committed by GitHub
parent 4f31c558bc
commit 76d4487cc9
3 changed files with 83 additions and 3 deletions

View File

@@ -52,4 +52,5 @@
@interface FlutterTextInputPlugin (TestMethods)
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result;
- (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(NSRangePointer)actualRange;
- (NSDictionary*)editingState;
@end

View File

@@ -275,6 +275,13 @@ static flutter::TextRange RangeFromBaseExtent(NSNumber* base,
_shown = FALSE;
[_textInputContext deactivate];
} else if ([method isEqualToString:kClearClientMethod]) {
// If there's an active mark region, commit it, end composing, and clear the IME's mark text.
if (_activeModel && _activeModel->composing()) {
_activeModel->CommitComposing();
_activeModel->EndComposing();
}
[_textInputContext discardMarkedText];
_clientID = nil;
_inputAction = nil;
_enableDeltaModel = NO;
@@ -360,14 +367,20 @@ static flutter::TextRange RangeFromBaseExtent(NSNumber* base,
flutter::TextRange composing_range = RangeFromBaseExtent(
state[kComposingBaseKey], state[kComposingExtentKey], _activeModel->composing_range());
size_t cursor_offset = selected_range.base() - composing_range.start();
if (!composing_range.collapsed() && !_activeModel->composing()) {
_activeModel->BeginComposing();
} else if (composing_range.collapsed() && _activeModel->composing()) {
_activeModel->EndComposing();
[_textInputContext discardMarkedText];
}
_activeModel->SetComposingRange(composing_range, cursor_offset);
[_client becomeFirstResponder];
[self updateTextAndSelection];
}
- (void)updateEditState {
- (NSDictionary*)editingState {
if (_activeModel == nullptr) {
return;
return nil;
}
NSString* const textAffinity = [self textAffinityString];
@@ -375,7 +388,7 @@ static flutter::TextRange RangeFromBaseExtent(NSNumber* base,
int composingBase = _activeModel->composing() ? _activeModel->composing_range().base() : -1;
int composingExtent = _activeModel->composing() ? _activeModel->composing_range().extent() : -1;
NSDictionary* state = @{
return @{
kSelectionBaseKey : @(_activeModel->selection().base()),
kSelectionExtentKey : @(_activeModel->selection().extent()),
kSelectionAffinityKey : textAffinity,
@@ -384,7 +397,14 @@ static flutter::TextRange RangeFromBaseExtent(NSNumber* base,
kComposingExtentKey : @(composingExtent),
kTextKey : [NSString stringWithUTF8String:_activeModel->GetText().c_str()]
};
}
- (void)updateEditState {
if (_activeModel == nullptr) {
return;
}
NSDictionary* state = [self editingState];
[_channel invokeMethod:kUpdateEditStateResponseMethod arguments:@[ self.clientID, state ]];
[self updateTextAndSelection];
}

View File

@@ -30,6 +30,7 @@
@interface FlutterInputPluginTestObjc : NSObject
- (bool)testEmptyCompositionRange;
- (bool)testClearClientDuringComposing;
@end
@implementation FlutterInputPluginTestObjc
@@ -99,6 +100,60 @@
return true;
}
- (bool)testClearClientDuringComposing {
// Set up FlutterTextInputPlugin.
id engineMock = OCMClassMock([FlutterEngine class]);
id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
[engineMock binaryMessenger])
.andReturn(binaryMessengerMock);
FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
nibName:@""
bundle:nil];
FlutterTextInputPlugin* plugin =
[[FlutterTextInputPlugin alloc] initWithViewController:viewController];
// Set input client 1.
[plugin handleMethodCall:[FlutterMethodCall
methodCallWithMethodName:@"TextInput.setClient"
arguments:@[
@(1), @{
@"inputAction" : @"action",
@"inputType" : @{@"name" : @"inputName"},
}
]]
result:^(id){
}];
// Set editing state with an active composing range.
[plugin handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.setEditingState"
arguments:@{
@"text" : @"Text",
@"selectionBase" : @(0),
@"selectionExtent" : @(0),
@"composingBase" : @(0),
@"composingExtent" : @(1),
}]
result:^(id){
}];
// Verify composing range is (0, 1).
NSDictionary* editingState = [plugin editingState];
EXPECT_EQ([editingState[@"composingBase"] intValue], 0);
EXPECT_EQ([editingState[@"composingExtent"] intValue], 1);
// Clear input client.
[plugin handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.clearClient"
arguments:@[]]
result:^(id){
}];
// Verify composing range is collapsed.
editingState = [plugin editingState];
EXPECT_EQ([editingState[@"composingBase"] intValue], [editingState[@"composingExtent"] intValue]);
return true;
}
- (bool)testFirstRectForCharacterRange {
id engineMock = OCMClassMock([FlutterEngine class]);
id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
@@ -368,6 +423,10 @@ TEST(FlutterTextInputPluginTest, TestEmptyCompositionRange) {
ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testEmptyCompositionRange]);
}
TEST(FlutterTextInputPluginTest, TestClearClientDuringComposing) {
ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testClearClientDuringComposing]);
}
TEST(FlutterTextInputPluginTest, TestFirstRectForCharacterRange) {
ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testFirstRectForCharacterRange]);
}