Skip to content

Commit

Permalink
[macOS] Clear IME mark text on clear input client (flutter#31849)
Browse files Browse the repository at this point in the history
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: flutter/flutter#92060
  • Loading branch information
cbracken authored Mar 7, 2022
1 parent 46dc713 commit 1d581ab
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,5 @@
@interface FlutterTextInputPlugin (TestMethods)
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result;
- (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(NSRangePointer)actualRange;
- (NSDictionary*)editingState;
@end
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,13 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
_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;
Expand Down Expand Up @@ -360,22 +367,28 @@ - (void)setEditingState:(NSDictionary*)state {
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];

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,
Expand All @@ -384,7 +397,14 @@ - (void)updateEditState {
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];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ - (void)updateString:(NSString*)string withSelection:(NSRange)selection {

@interface FlutterInputPluginTestObjc : NSObject
- (bool)testEmptyCompositionRange;
- (bool)testClearClientDuringComposing;
@end

@implementation FlutterInputPluginTestObjc
Expand Down Expand Up @@ -99,6 +100,60 @@ - (bool)testEmptyCompositionRange {
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));
Expand Down Expand Up @@ -368,6 +423,10 @@ - (bool)testOperationsThatTriggerDelta {
ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testEmptyCompositionRange]);
}

TEST(FlutterTextInputPluginTest, TestClearClientDuringComposing) {
ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testClearClientDuringComposing]);
}

TEST(FlutterTextInputPluginTest, TestFirstRectForCharacterRange) {
ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testFirstRectForCharacterRange]);
}
Expand Down

0 comments on commit 1d581ab

Please sign in to comment.