From 67cfb7d0678e69fb10b6a8a5c71ad615d9447738 Mon Sep 17 00:00:00 2001 From: Matej Knopp Date: Sun, 5 Jun 2022 20:25:29 +0200 Subject: [PATCH 01/20] Remove the isComposing hack --- .../macos/framework/Source/FlutterKeyboardManager.mm | 5 ----- .../framework/Source/FlutterKeyboardViewDelegate.h | 10 ---------- .../macos/framework/Source/FlutterTextInputPlugin.h | 9 --------- .../macos/framework/Source/FlutterTextInputPlugin.mm | 4 ---- .../macos/framework/Source/FlutterViewController.mm | 4 ---- 5 files changed, 32 deletions(-) diff --git a/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.mm b/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.mm index 67dd257cc9c7e..488e5a01f87d7 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.mm @@ -196,11 +196,6 @@ - (void)processNextEvent { } - (void)performProcessEvent:(NSEvent*)event onFinish:(VoidBlock)onFinish { - if (_viewDelegate.isComposing) { - [self dispatchTextEvent:event]; - onFinish(); - return; - } // Having no primary responders require extra logic, but Flutter hard-codes // all primary responders, so this is a situation that Flutter will never diff --git a/shell/platform/darwin/macos/framework/Source/FlutterKeyboardViewDelegate.h b/shell/platform/darwin/macos/framework/Source/FlutterKeyboardViewDelegate.h index 0f8c48843e272..cc559fe3f063e 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterKeyboardViewDelegate.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterKeyboardViewDelegate.h @@ -73,16 +73,6 @@ typedef struct { */ - (BOOL)onTextInputKeyEvent:(nonnull NSEvent*)event; -/** - * Whether this FlutterKeyboardViewDelegate is actively taking provisional user text input. - * - * This is typically true when a Flutter text field is focused, and the user is entering composing - * text into the text field. - */ -// TODO (LongCatIsLooong): remove this method and implement a long-term fix for -// https://github.com/flutter/flutter/issues/85328. -- (BOOL)isComposing; - /** * Add a listener that is called whenever the user changes keyboard layout. * diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.h b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.h index af6dd540ccc47..57a5aec0e532b 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.h @@ -45,15 +45,6 @@ */ - (BOOL)isFirstResponder; -/** - * Whether this plugin has composing text. - * - * This is only true when the text input plugin is actively taking user input with composing text. - */ -// TODO (LongCatIsLooong): remove this method and implement a long-term fix for -// https://github.com/flutter/flutter/issues/85328. -- (BOOL)isComposing; - /** * Handles key down events received from the view controller, responding YES if * the event was handled. diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm index dda99268bc7d0..97146852ec334 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm @@ -504,10 +504,6 @@ - (NSString*)textAffinityString { : kTextAffinityDownstream; } -- (BOOL)isComposing { - return _activeModel && !_activeModel->composing_range().collapsed(); -} - - (BOOL)handleKeyEvent:(NSEvent*)event { if (event.type == NSEventTypeKeyUp || (event.type == NSEventTypeFlagsChanged && event.modifierFlags < _previouslyPressedFlags)) { diff --git a/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm b/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm index 040d22649e4ff..a4c37b179607f 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm @@ -688,10 +688,6 @@ - (void)viewDidReshape:(NSView*)view { #pragma mark - FlutterKeyboardViewDelegate -- (BOOL)isComposing { - return [_textInputPlugin isComposing]; -} - - (void)sendKeyEvent:(const FlutterKeyEvent&)event callback:(nullable FlutterKeyEventCallback)callback userData:(nullable void*)userData { From d7d60a77d680f5d0be73e61de3e0ba613f952f33 Mon Sep 17 00:00:00 2001 From: Matej Knopp Date: Sun, 5 Jun 2022 20:25:51 +0200 Subject: [PATCH 02/20] Forward editing intents to framework --- .../macos/framework/Source/FlutterTextInputPlugin.mm | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm index 97146852ec334..9fb38554c7dae 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm @@ -32,6 +32,7 @@ static NSString* const kUpdateEditStateWithDeltasResponseMethod = @"TextInputClient.updateEditingStateWithDeltas"; static NSString* const kPerformAction = @"TextInputClient.performAction"; +static NSString* const kPerformIntent = @"TextInputClient.performIntent"; static NSString* const kMultilineInputType = @"TextInputType.multiline"; static NSString* const kTextAffinityDownstream = @"TextAffinity.downstream"; @@ -666,6 +667,16 @@ - (void)doCommandBySelector:(SEL)selector { void (*func)(id, SEL, id) = reinterpret_cast(imp); func(self, selector, nil); } + // All NSStandardKeyBindingResponding method have single trailing space. + // For now forward all selectors to framework + NSString* name = NSStringFromSelector(selector); + + if ([name hasSuffix:@":"]) { + name = [name substringToIndex:name.length - 1]; + if (![name containsString:@":"]) { + [_channel invokeMethod:kPerformIntent arguments:@[ self.clientID, name ]]; + } + } } - (void)insertNewline:(id)sender { From 891adf6a0f00812ed7a582fdd002d4d146c6f9f0 Mon Sep 17 00:00:00 2001 From: Matej Knopp Date: Sun, 5 Jun 2022 20:26:04 +0200 Subject: [PATCH 03/20] Respect suggested composing range --- .../macos/framework/Source/FlutterTextInputPlugin.mm | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm index 9fb38554c7dae..1848373da3876 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm @@ -703,6 +703,16 @@ - (void)setMarkedText:(id)string if (!_activeModel->composing()) { _activeModel->BeginComposing(); } + + if (replacementRange.location != NSNotFound) { + _activeModel->SetComposingRange( + flutter::TextRange(replacementRange.location, + replacementRange.location + replacementRange.length), + 0); + _activeModel->SetSelection( + flutter::TextRange(selectedRange.location, selectedRange.location + selectedRange.length)); + } + flutter::TextRange composingBeforeChange = _activeModel->composing_range(); flutter::TextRange selectionBeforeChange = _activeModel->selection(); From d2c4e44532a3d8b952a9288072d30df22b57c337 Mon Sep 17 00:00:00 2001 From: Matej Knopp Date: Sun, 5 Jun 2022 20:32:58 +0200 Subject: [PATCH 04/20] Reformat --- .../darwin/macos/framework/Source/FlutterKeyboardManager.mm | 1 - 1 file changed, 1 deletion(-) diff --git a/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.mm b/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.mm index 488e5a01f87d7..6f6706305223f 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.mm @@ -196,7 +196,6 @@ - (void)processNextEvent { } - (void)performProcessEvent:(NSEvent*)event onFinish:(VoidBlock)onFinish { - // Having no primary responders require extra logic, but Flutter hard-codes // all primary responders, so this is a situation that Flutter will never // encounter. From c6bf312d05d4f8358604761b6a7e4cf50abbd523 Mon Sep 17 00:00:00 2001 From: Matej Knopp Date: Tue, 7 Jun 2022 21:46:48 +0200 Subject: [PATCH 05/20] Use performSelector call and forward all unmodified selectors --- .../framework/Source/FlutterTextInputPlugin.mm | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm index 1848373da3876..d18d126fbce71 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm @@ -32,7 +32,7 @@ static NSString* const kUpdateEditStateWithDeltasResponseMethod = @"TextInputClient.updateEditingStateWithDeltas"; static NSString* const kPerformAction = @"TextInputClient.performAction"; -static NSString* const kPerformIntent = @"TextInputClient.performIntent"; +static NSString* const kPerformSelector = @"TextInputClient.performSelector"; static NSString* const kMultilineInputType = @"TextInputType.multiline"; static NSString* const kTextAffinityDownstream = @"TextAffinity.downstream"; @@ -667,16 +667,9 @@ - (void)doCommandBySelector:(SEL)selector { void (*func)(id, SEL, id) = reinterpret_cast(imp); func(self, selector, nil); } - // All NSStandardKeyBindingResponding method have single trailing space. - // For now forward all selectors to framework + // Forward all selectors to framework NSString* name = NSStringFromSelector(selector); - - if ([name hasSuffix:@":"]) { - name = [name substringToIndex:name.length - 1]; - if (![name containsString:@":"]) { - [_channel invokeMethod:kPerformIntent arguments:@[ self.clientID, name ]]; - } - } + [_channel invokeMethod:kPerformSelector arguments:@[ self.clientID, name ]]; } - (void)insertNewline:(id)sender { From 43bc30d1653c114a1f8eb90055e4b0281664c53f Mon Sep 17 00:00:00 2001 From: Matej Knopp Date: Wed, 8 Jun 2022 14:44:06 +0200 Subject: [PATCH 06/20] Remove forwardKeyEventsToSystemWhenComposing test --- .../Source/FlutterKeyboardManagerUnittests.mm | 33 ------------------- 1 file changed, 33 deletions(-) diff --git a/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManagerUnittests.mm b/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManagerUnittests.mm index f091f535cf62b..e257e94a9a031 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManagerUnittests.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManagerUnittests.mm @@ -252,7 +252,6 @@ - (void)recordCallTypesTo:(nonnull NSMutableArray*)typeStorage @property(nonatomic) FlutterKeyboardManager* manager; @property(nonatomic) NSResponder* nextResponder; -@property(nonatomic, assign) BOOL isComposing; #pragma mark - Private @@ -289,7 +288,6 @@ - (nonnull instancetype)init { [self respondChannelCallsWith:FALSE]; [self respondEmbedderCallsWith:FALSE]; [self respondTextInputWith:FALSE]; - _isComposing = NO; _currentLayout = &kUsLayout; @@ -304,7 +302,6 @@ - (nonnull instancetype)init { OCMStub([viewDelegateMock onTextInputKeyEvent:[OCMArg any]]) .andCall(self, @selector(handleTextInputKeyEvent:)); OCMStub([viewDelegateMock getBinaryMessenger]).andReturn(messengerMock); - OCMStub([viewDelegateMock isComposing]).andCall(self, @selector(isComposing)); OCMStub([viewDelegateMock sendKeyEvent:FlutterKeyEvent {} callback:nil userData:nil]) .ignoringNonObjectArgs() .andCall(self, @selector(handleEmbedderEvent:callback:userData:)); @@ -430,7 +427,6 @@ @interface FlutterKeyboardManagerUnittestsObjC : NSObject - (bool)singlePrimaryResponder; - (bool)doublePrimaryResponder; - (bool)textInputPlugin; -- (bool)forwardKeyEventsToSystemWhenComposing; - (bool)emptyNextResponder; - (bool)racingConditionBetweenKeyAndText; - (bool)correctLogicalKeyForLayouts; @@ -449,10 +445,6 @@ - (bool)correctLogicalKeyForLayouts; ASSERT_TRUE([[FlutterKeyboardManagerUnittestsObjC alloc] textInputPlugin]); } -TEST(FlutterKeyboardManagerUnittests, handlingComposingText) { - ASSERT_TRUE([[FlutterKeyboardManagerUnittestsObjC alloc] forwardKeyEventsToSystemWhenComposing]); -} - TEST(FlutterKeyboardManagerUnittests, EmptyNextResponder) { ASSERT_TRUE([[FlutterKeyboardManagerUnittestsObjC alloc] emptyNextResponder]); } @@ -597,31 +589,6 @@ - (bool)textInputPlugin { return true; } -- (bool)forwardKeyEventsToSystemWhenComposing { - KeyboardTester* tester = OCMPartialMock([[KeyboardTester alloc] init]); - - NSMutableArray* channelCallbacks = - [NSMutableArray array]; - NSMutableArray* embedderCallbacks = - [NSMutableArray array]; - [tester recordEmbedderCallsTo:embedderCallbacks]; - [tester recordChannelCallsTo:channelCallbacks]; - // The event shouldn't propagate further even if TextInputPlugin does not - // claim the event. - [tester respondTextInputWith:NO]; - - tester.isComposing = YES; - // Send a down event with composing == YES. - [tester.manager handleEvent:keyUpEvent(0x50)]; - - // Nobody gets the event except for the text input plugin. - EXPECT_EQ([channelCallbacks count], 0u); - EXPECT_EQ([embedderCallbacks count], 0u); - OCMVerify(times(1), [tester handleTextInputKeyEvent:checkKeyDownEvent(0x50)]); - - return true; -} - - (bool)emptyNextResponder { KeyboardTester* tester = [[KeyboardTester alloc] init]; tester.nextResponder = nil; From 1fe79ed887d1630f4a613055950cf07abdddc990 Mon Sep 17 00:00:00 2001 From: Matej Knopp Date: Wed, 8 Jun 2022 14:44:24 +0200 Subject: [PATCH 07/20] Add testSelectorsAreForwardedToFramework --- .../Source/FlutterTextInputPluginTest.mm | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm index 2b35107f89288..6431b8d76a37f 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm @@ -1057,6 +1057,49 @@ - (bool)testLocalTextAndSelectionUpdateAfterDelta { return localTextAndSelectionUpdated; } +- (bool)testSelectorsAreForwardedToFramework { + 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]; + + [plugin handleMethodCall:[FlutterMethodCall + methodCallWithMethodName:@"TextInput.setClient" + arguments:@[ + @(1), @{ + @"inputAction" : @"action", + @"enableDeltaModel" : @"true", + @"inputType" : @{@"name" : @"inputName"}, + } + ]] + result:^(id){ + }]; + + [plugin doCommandBySelector:@selector(moveRightAndModifySelection:)]; + + NSData* performSelectorCall = [[FlutterJSONMethodCodec sharedInstance] + encodeMethodCall:[FlutterMethodCall + methodCallWithMethodName:@"TextInputClient.performSelector" + arguments:@[ @(1), @"moveRightAndModifySelection:" ]]]; + + @try { + OCMVerify( // NOLINT(google-objc-avoid-throwing-exception) + [binaryMessengerMock sendOnChannel:@"flutter/textinput" message:performSelectorCall]); + } @catch (...) { + return false; + } + + return true; +} + @end namespace flutter::testing { @@ -1120,6 +1163,10 @@ - (bool)testLocalTextAndSelectionUpdateAfterDelta { ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] unhandledKeyEquivalent]); } +TEST(FlutterTextInputPluginTest, TestSelectorsAreForwardedToFramework) { + ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testSelectorsAreForwardedToFramework]); +} + TEST(FlutterTextInputPluginTest, CanWorkWithFlutterTextField) { FlutterEngine* engine = CreateTestEngine(); NSString* fixtures = @(testing::GetFixturesPath()); From 737cc8c0139f25f68917dd5374be55587eb490ca Mon Sep 17 00:00:00 2001 From: Matej Knopp Date: Sat, 11 Jun 2022 20:16:31 +0200 Subject: [PATCH 08/20] Add TestSetMarkedTextWithReplacementRange --- .../Source/FlutterTextInputPluginTest.mm | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm index 6431b8d76a37f..67015812dc809 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm @@ -173,6 +173,74 @@ - (bool)testSetMarkedTextWithSelectionChange { return true; } +- (bool)testSetMarkedTextWithReplacementRange { + 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]; + + [plugin handleMethodCall:[FlutterMethodCall + methodCallWithMethodName:@"TextInput.setClient" + arguments:@[ + @(1), @{ + @"inputAction" : @"action", + @"inputType" : @{@"name" : @"inputName"}, + } + ]] + result:^(id){ + }]; + + FlutterMethodCall* call = [FlutterMethodCall methodCallWithMethodName:@"TextInput.setEditingState" + arguments:@{ + @"text" : @"1234", + @"selectionBase" : @(3), + @"selectionExtent" : @(3), + @"composingBase" : @(-1), + @"composingExtent" : @(-1), + }]; + [plugin handleMethodCall:call + result:^(id){ + }]; + + [plugin setMarkedText:@"marked" + selectedRange:NSMakeRange(1, 0) + replacementRange:NSMakeRange(1, 2)]; + + NSDictionary* expectedState = @{ + @"selectionBase" : @(2), + @"selectionExtent" : @(2), + @"selectionAffinity" : @"TextAffinity.upstream", + @"selectionIsDirectional" : @(NO), + @"composingBase" : @(1), + @"composingExtent" : @(7), + @"text" : @"1marked4", + }; + + NSData* updateCall = [[FlutterJSONMethodCodec sharedInstance] + encodeMethodCall:[FlutterMethodCall + methodCallWithMethodName:@"TextInputClient.updateEditingState" + arguments:@[ @(1), expectedState ]]]; + + OCMExpect( // NOLINT(google-objc-avoid-throwing-exception) + [binaryMessengerMock sendOnChannel:@"flutter/textinput" message:updateCall]); + + @try { + OCMVerify( // NOLINT(google-objc-avoid-throwing-exception) + [binaryMessengerMock sendOnChannel:@"flutter/textinput" message:updateCall]); + } @catch (...) { + return false; + } + return true; +} + - (bool)testComposingRegionRemovedByFramework { id engineMock = OCMClassMock([FlutterEngine class]); id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); @@ -1123,6 +1191,10 @@ - (bool)testSelectorsAreForwardedToFramework { ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testSetMarkedTextWithSelectionChange]); } +TEST(FlutterTextInputPluginTest, TestSetMarkedTextWithReplacementRange) { + ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testSetMarkedTextWithReplacementRange]); +} + TEST(FlutterTextInputPluginTest, TestComposingRegionRemovedByFramework) { ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testComposingRegionRemovedByFramework]); } From 794d35dc96c7a73899dcd11ed36d38169df6d843 Mon Sep 17 00:00:00 2001 From: Matej Knopp Date: Sun, 12 Jun 2022 16:13:23 +0200 Subject: [PATCH 09/20] Implement insertTab: --- .../darwin/macos/framework/Source/FlutterTextInputPlugin.mm | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm index d18d126fbce71..018ed45d7e431 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm @@ -612,6 +612,11 @@ - (NSTextInputContext*)inputContext { #pragma mark - #pragma mark NSTextInputClient +- (void)insertTab:(id)sender { + // Implementing insertTab: makes AppKit send tab as command, instead of + // insertText with '\t'. +} + - (void)insertText:(id)string replacementRange:(NSRange)range { if (_activeModel == nullptr) { return; From 178229c8551fee1792a95d4153b9f0c93aa61e3b Mon Sep 17 00:00:00 2001 From: Matej Knopp Date: Mon, 13 Jun 2022 12:45:10 +0200 Subject: [PATCH 10/20] Group selectors received in single runloop turn together --- .../Source/FlutterTextInputPlugin.mm | 27 ++++++++++++++++--- .../Source/FlutterTextInputPluginTest.mm | 18 +++++++++++-- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm index 018ed45d7e431..68d993340584a 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm @@ -32,7 +32,7 @@ static NSString* const kUpdateEditStateWithDeltasResponseMethod = @"TextInputClient.updateEditingStateWithDeltas"; static NSString* const kPerformAction = @"TextInputClient.performAction"; -static NSString* const kPerformSelector = @"TextInputClient.performSelector"; +static NSString* const kPerformSelector = @"TextInputClient.performSelectors"; static NSString* const kMultilineInputType = @"TextInputType.multiline"; static NSString* const kTextAffinityDownstream = @"TextAffinity.downstream"; @@ -175,6 +175,13 @@ @interface FlutterTextInputPlugin () */ @property(nonatomic) BOOL enableDeltaModel; +/** + * Used to gather multiple selectors performed in one run loop turn. These + * will be all send in one platform channel call so that framework can process + * them in single microtask. + */ +@property(nonatomic) NSMutableArray* pendingSelectors; + /** * Handles a Flutter system message on the text input channel. */ @@ -672,9 +679,23 @@ - (void)doCommandBySelector:(SEL)selector { void (*func)(id, SEL, id) = reinterpret_cast(imp); func(self, selector, nil); } - // Forward all selectors to framework + + // Group multiple selector within single run loop turn so that framework can + // process them in single microtask NSString* name = NSStringFromSelector(selector); - [_channel invokeMethod:kPerformSelector arguments:@[ self.clientID, name ]]; + if (_pendingSelectors == nil) { + _pendingSelectors = [NSMutableArray array]; + } + [_pendingSelectors addObject:name]; + __weak NSMutableArray* selectors = _pendingSelectors; + __weak FlutterMethodChannel* channel = _channel; + + dispatch_async(dispatch_get_main_queue(), ^{ + if (selectors.count > 0) { + [channel invokeMethod:kPerformSelector arguments:@[ self.clientID, selectors ]]; + [selectors removeAllObjects]; + } + }); } - (void)insertNewline:(id)sender { diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm index 67015812dc809..b4378f1519ce9 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm @@ -1151,12 +1151,26 @@ - (bool)testSelectorsAreForwardedToFramework { result:^(id){ }]; + // Ensure both selectors are grouped in one platform channel call + [plugin doCommandBySelector:@selector(moveUp:)]; [plugin doCommandBySelector:@selector(moveRightAndModifySelection:)]; + __block bool done = false; + dispatch_async(dispatch_get_main_queue(), ^{ + done = true; + }); + + while (!done) { + // Each invocation will handle one source + CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0, true); + } + NSData* performSelectorCall = [[FlutterJSONMethodCodec sharedInstance] encodeMethodCall:[FlutterMethodCall - methodCallWithMethodName:@"TextInputClient.performSelector" - arguments:@[ @(1), @"moveRightAndModifySelection:" ]]]; + methodCallWithMethodName:@"TextInputClient.performSelectors" + arguments:@[ + @(1), @[ @"moveUp:", @"moveRightAndModifySelection:" ] + ]]]; @try { OCMVerify( // NOLINT(google-objc-avoid-throwing-exception) From a098e6d527aea98bfe07e5dd9c96efe6dedd95f3 Mon Sep 17 00:00:00 2001 From: Matej Knopp Date: Mon, 13 Jun 2022 15:17:39 +0200 Subject: [PATCH 11/20] Fix test crash --- .../framework/Source/FlutterTextInputPlugin.h | 1 + .../framework/Source/FlutterTextInputPlugin.mm | 14 ++++++++++++-- .../framework/Source/FlutterTextInputPluginTest.mm | 9 +++++++-- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.h b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.h index 57a5aec0e532b..dedfc661884f6 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.h @@ -64,4 +64,5 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result; - (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(NSRangePointer)actualRange; - (NSDictionary*)editingState; +@property(readwrite, nonatomic) NSString* customRunLoopMode; @end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm index 68d993340584a..a394e38df9385 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm @@ -220,6 +220,11 @@ - (void)updateTextAndSelection; */ - (NSString*)textAffinityString; +/** + * Allow overriding run loop mode for test. + */ +@property(readwrite, nonatomic) NSString* customRunLoopMode; + @end @implementation FlutterTextInputPlugin { @@ -689,10 +694,15 @@ - (void)doCommandBySelector:(SEL)selector { [_pendingSelectors addObject:name]; __weak NSMutableArray* selectors = _pendingSelectors; __weak FlutterMethodChannel* channel = _channel; + __weak NSNumber* clientID = self.clientID; + + CFStringRef runLoopMode = self.customRunLoopMode != nil + ? (__bridge CFStringRef)self.customRunLoopMode + : kCFRunLoopCommonModes; - dispatch_async(dispatch_get_main_queue(), ^{ + CFRunLoopPerformBlock(CFRunLoopGetMain(), runLoopMode, ^{ if (selectors.count > 0) { - [channel invokeMethod:kPerformSelector arguments:@[ self.clientID, selectors ]]; + [channel invokeMethod:kPerformSelector arguments:@[ clientID, selectors ]]; [selectors removeAllObjects]; } }); diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm index b4378f1519ce9..be4602c7d86f2 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm @@ -1151,18 +1151,23 @@ - (bool)testSelectorsAreForwardedToFramework { result:^(id){ }]; + // Can't run CFRunLoop in default mode because it causes crashes from scheduled + // sources from other tests. + NSString* runLoopMode = @"FlutterTestRunLoopMode"; + plugin.customRunLoopMode = runLoopMode; + // Ensure both selectors are grouped in one platform channel call [plugin doCommandBySelector:@selector(moveUp:)]; [plugin doCommandBySelector:@selector(moveRightAndModifySelection:)]; __block bool done = false; - dispatch_async(dispatch_get_main_queue(), ^{ + CFRunLoopPerformBlock(CFRunLoopGetMain(), (__bridge CFStringRef)runLoopMode, ^{ done = true; }); while (!done) { // Each invocation will handle one source - CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0, true); + CFRunLoopRunInMode((__bridge CFStringRef)runLoopMode, 0, true); } NSData* performSelectorCall = [[FlutterJSONMethodCodec sharedInstance] From 82c653f25ba21180e4c56d1ef401ee7b8b410396 Mon Sep 17 00:00:00 2001 From: Matej Knopp Date: Mon, 13 Jun 2022 15:22:30 +0200 Subject: [PATCH 12/20] Update comment --- .../darwin/macos/framework/Source/FlutterTextInputPlugin.mm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm index a394e38df9385..2fe374ad525f7 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm @@ -685,8 +685,8 @@ - (void)doCommandBySelector:(SEL)selector { func(self, selector, nil); } - // Group multiple selector within single run loop turn so that framework can - // process them in single microtask + // Group multiple selectors received within a single run loop turn so that + // framework can process them in single microtask. NSString* name = NSStringFromSelector(selector); if (_pendingSelectors == nil) { _pendingSelectors = [NSMutableArray array]; From 8efd6fb805ec165f425a302feb4b1066b0d3dca8 Mon Sep 17 00:00:00 2001 From: Matej Knopp Date: Mon, 13 Jun 2022 15:23:15 +0200 Subject: [PATCH 13/20] Comment periods --- .../macos/framework/Source/FlutterTextInputPluginTest.mm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm index be4602c7d86f2..83f50a3403215 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm @@ -1156,7 +1156,7 @@ - (bool)testSelectorsAreForwardedToFramework { NSString* runLoopMode = @"FlutterTestRunLoopMode"; plugin.customRunLoopMode = runLoopMode; - // Ensure both selectors are grouped in one platform channel call + // Ensure both selectors are grouped in one platform channel call. [plugin doCommandBySelector:@selector(moveUp:)]; [plugin doCommandBySelector:@selector(moveRightAndModifySelection:)]; @@ -1166,7 +1166,7 @@ - (bool)testSelectorsAreForwardedToFramework { }); while (!done) { - // Each invocation will handle one source + // Each invocation will handle one source. CFRunLoopRunInMode((__bridge CFStringRef)runLoopMode, 0, true); } From 465a875df41ac19c46d9266a36e9844a69589167 Mon Sep 17 00:00:00 2001 From: Matej Knopp Date: Mon, 13 Jun 2022 15:23:58 +0200 Subject: [PATCH 14/20] Typo --- .../darwin/macos/framework/Source/FlutterTextInputPlugin.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm index 2fe374ad525f7..d49a7bf008ae6 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm @@ -177,7 +177,7 @@ @interface FlutterTextInputPlugin () /** * Used to gather multiple selectors performed in one run loop turn. These - * will be all send in one platform channel call so that framework can process + * will be all sent in one platform channel call so that framework can process * them in single microtask. */ @property(nonatomic) NSMutableArray* pendingSelectors; From 0b9a5c456de508b04a58ba35d31fff989c129c87 Mon Sep 17 00:00:00 2001 From: Matej Knopp Date: Tue, 14 Jun 2022 22:23:57 +0200 Subject: [PATCH 15/20] Plural --- .../darwin/macos/framework/Source/FlutterTextInputPlugin.mm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm index d49a7bf008ae6..6fbd11d3266fd 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm @@ -32,7 +32,7 @@ static NSString* const kUpdateEditStateWithDeltasResponseMethod = @"TextInputClient.updateEditingStateWithDeltas"; static NSString* const kPerformAction = @"TextInputClient.performAction"; -static NSString* const kPerformSelector = @"TextInputClient.performSelectors"; +static NSString* const kPerformSelectors = @"TextInputClient.performSelectors"; static NSString* const kMultilineInputType = @"TextInputType.multiline"; static NSString* const kTextAffinityDownstream = @"TextAffinity.downstream"; @@ -702,7 +702,7 @@ - (void)doCommandBySelector:(SEL)selector { CFRunLoopPerformBlock(CFRunLoopGetMain(), runLoopMode, ^{ if (selectors.count > 0) { - [channel invokeMethod:kPerformSelector arguments:@[ clientID, selectors ]]; + [channel invokeMethod:kPerformSelectors arguments:@[ clientID, selectors ]]; [selectors removeAllObjects]; } }); From 1e60b0ab78f944d2163c90e053c94869e09dc6ca Mon Sep 17 00:00:00 2001 From: Matej Knopp Date: Wed, 22 Jun 2022 09:48:17 +0200 Subject: [PATCH 16/20] Remove SetSelection call --- .../darwin/macos/framework/Source/FlutterTextInputPlugin.mm | 2 -- 1 file changed, 2 deletions(-) diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm index 6fbd11d3266fd..c2e2967eeaf97 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm @@ -738,8 +738,6 @@ - (void)setMarkedText:(id)string flutter::TextRange(replacementRange.location, replacementRange.location + replacementRange.length), 0); - _activeModel->SetSelection( - flutter::TextRange(selectedRange.location, selectedRange.location + selectedRange.length)); } flutter::TextRange composingBeforeChange = _activeModel->composing_range(); From 295e539ec02605fc7e80cc5bf297771bb19d8737 Mon Sep 17 00:00:00 2001 From: Matej Knopp Date: Wed, 22 Jun 2022 10:23:45 +0200 Subject: [PATCH 17/20] Only schedule performBlock if needed --- .../Source/FlutterTextInputPlugin.mm | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm index c2e2967eeaf97..7f9e1cf4f2a82 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm @@ -692,20 +692,23 @@ - (void)doCommandBySelector:(SEL)selector { _pendingSelectors = [NSMutableArray array]; } [_pendingSelectors addObject:name]; - __weak NSMutableArray* selectors = _pendingSelectors; - __weak FlutterMethodChannel* channel = _channel; - __weak NSNumber* clientID = self.clientID; - - CFStringRef runLoopMode = self.customRunLoopMode != nil - ? (__bridge CFStringRef)self.customRunLoopMode - : kCFRunLoopCommonModes; - - CFRunLoopPerformBlock(CFRunLoopGetMain(), runLoopMode, ^{ - if (selectors.count > 0) { - [channel invokeMethod:kPerformSelectors arguments:@[ clientID, selectors ]]; - [selectors removeAllObjects]; - } - }); + + if (_pendingSelectors.count == 1) { + __weak NSMutableArray* selectors = _pendingSelectors; + __weak FlutterMethodChannel* channel = _channel; + __weak NSNumber* clientID = self.clientID; + + CFStringRef runLoopMode = self.customRunLoopMode != nil + ? (__bridge CFStringRef)self.customRunLoopMode + : kCFRunLoopCommonModes; + + CFRunLoopPerformBlock(CFRunLoopGetMain(), runLoopMode, ^{ + if (selectors.count > 0) { + [channel invokeMethod:kPerformSelectors arguments:@[ clientID, selectors ]]; + [selectors removeAllObjects]; + } + }); + } } - (void)insertNewline:(id)sender { From a0e98d28a7a5d481d397120cac225d77e75cdce1 Mon Sep 17 00:00:00 2001 From: Matej Knopp Date: Wed, 22 Jun 2022 22:26:39 +0200 Subject: [PATCH 18/20] The framework --- .../darwin/macos/framework/Source/FlutterTextInputPlugin.mm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm index 7f9e1cf4f2a82..01b2acb73ad53 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm @@ -177,7 +177,7 @@ @interface FlutterTextInputPlugin () /** * Used to gather multiple selectors performed in one run loop turn. These - * will be all sent in one platform channel call so that framework can process + * will be all sent in one platform channel call so that the framework can process * them in single microtask. */ @property(nonatomic) NSMutableArray* pendingSelectors; @@ -686,7 +686,7 @@ - (void)doCommandBySelector:(SEL)selector { } // Group multiple selectors received within a single run loop turn so that - // framework can process them in single microtask. + // the framework can process them in single microtask. NSString* name = NSStringFromSelector(selector); if (_pendingSelectors == nil) { _pendingSelectors = [NSMutableArray array]; From 389dbb9ca88061a0a2b356a907e12ca0f87d8169 Mon Sep 17 00:00:00 2001 From: Matej Knopp Date: Tue, 5 Jul 2022 15:13:12 +0200 Subject: [PATCH 19/20] Document replacementRange documentation being wrong --- .../darwin/macos/framework/Source/FlutterTextInputPlugin.mm | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm index 01b2acb73ad53..904ea8aabb058 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm @@ -737,6 +737,11 @@ - (void)setMarkedText:(id)string } if (replacementRange.location != NSNotFound) { + // According to the NSTextInputClient documentation replacementRange is + // computed from the beginning of the marked text. That doesn't seem to be + // the case, because in situations where the replacementRange is actually + // specified (i.e. when switching between characters equivalent after long + // key press) the replacementRange is provided while there is no composition. _activeModel->SetComposingRange( flutter::TextRange(replacementRange.location, replacementRange.location + replacementRange.length), From 5f0b88a49c6b623c8b179a0e70f2e8657f2f7b74 Mon Sep 17 00:00:00 2001 From: Matej Knopp Date: Tue, 5 Jul 2022 15:16:41 +0200 Subject: [PATCH 20/20] do not fowrard insertNewline: to framework --- .../darwin/macos/framework/Source/FlutterTextInputPlugin.mm | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm index 904ea8aabb058..698d6fbbd03fe 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm @@ -685,6 +685,11 @@ - (void)doCommandBySelector:(SEL)selector { func(self, selector, nil); } + if (selector == @selector(insertNewline:)) { + // Already handled through text insertion (multiline) or action. + return; + } + // Group multiple selectors received within a single run loop turn so that // the framework can process them in single microtask. NSString* name = NSStringFromSelector(selector);