[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:
@@ -52,4 +52,5 @@
|
||||
@interface FlutterTextInputPlugin (TestMethods)
|
||||
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result;
|
||||
- (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(NSRangePointer)actualRange;
|
||||
- (NSDictionary*)editingState;
|
||||
@end
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user