Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
Comment on lines +280 to +281
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the end result that the composing text (e.g. "nihao") is in the field or the first choice characters ("你好")? If I try this on non-Flutter web or Mac, it seems to keep the composing text.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep the end result is that we commit the current composing text and it remains in the field. Then we clear it from the IME's buffer.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool that matches what I see on native then 👍

}
[_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();
Comment on lines +370 to +371
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this let you resume composing after unfocusing the field and coming back? Or maybe it's just cleanup.

Copy link
Member Author

@cbracken cbracken Mar 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is technically unrelated cleanup. This just ensures that if the framework tells us we have composing text, but the engine wasn't already composing, we set it to composing mode. Adding the test uncovered this bug.

} 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