diff --git a/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.mm b/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.mm index a7c3eb8657739..c2406820af101 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.mm @@ -72,6 +72,12 @@ - (void)handleEvent:(nonnull NSEvent*)event { event.type != NSEventTypeFlagsChanged) { return; } + + if (_viewDelegate.isComposing) { + [self dispatchToSecondaryResponders:event]; + return; + } + // Having no primary responders require extra logic, but Flutter hard-codes // all primary responders, so this is a situation that Flutter will never // encounter. diff --git a/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManagerUnittests.mm b/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManagerUnittests.mm index 2430da1257eb5..9ee515d9f3dc3 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManagerUnittests.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManagerUnittests.mm @@ -75,6 +75,7 @@ - (void)recordChannelCallsTo:(nonnull NSMutableArray*)s @property(nonatomic) FlutterKeyboardManager* manager; @property(nonatomic) NSResponder* nextResponder; +@property(nonatomic, assign) BOOL isComposing; #pragma mark - Private @@ -105,6 +106,7 @@ - (nonnull instancetype)init { [self respondChannelCallsWith:FALSE]; [self respondEmbedderCallsWith:FALSE]; [self respondTextInputWith:FALSE]; + _isComposing = NO; id messengerMock = OCMStrictProtocolMock(@protocol(FlutterBinaryMessenger)); OCMStub([messengerMock sendOnChannel:@"flutter/keyevent" @@ -117,6 +119,7 @@ - (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:)); @@ -188,6 +191,7 @@ - (bool)nextResponderShouldThrowOnKeyUp; - (bool)singlePrimaryResponder; - (bool)doublePrimaryResponder; - (bool)textInputPlugin; +- (bool)forwardKeyEventsToSystemWhenComposing; - (bool)emptyNextResponder; @end @@ -208,6 +212,10 @@ - (bool)emptyNextResponder; ASSERT_TRUE([[FlutterKeyboardManagerUnittestsObjC alloc] textInputPlugin]); } +TEST(FlutterKeyboardManagerUnittests, handlingComposingText) { + ASSERT_TRUE([[FlutterKeyboardManagerUnittestsObjC alloc] forwardKeyEventsToSystemWhenComposing]); +} + TEST(FlutterKeyboardManagerUnittests, EmptyNextResponder) { ASSERT_TRUE([[FlutterKeyboardManagerUnittestsObjC alloc] emptyNextResponder]); } @@ -356,6 +364,31 @@ - (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; diff --git a/shell/platform/darwin/macos/framework/Source/FlutterKeyboardViewDelegate.h b/shell/platform/darwin/macos/framework/Source/FlutterKeyboardViewDelegate.h index 580fe045b419a..970d75fedd815 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterKeyboardViewDelegate.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterKeyboardViewDelegate.h @@ -50,4 +50,14 @@ */ - (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; + @end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.h b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.h index 57a5aec0e532b..af6dd540ccc47 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.h @@ -45,6 +45,15 @@ */ - (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 b961828f3d123..d2509fcfa480e 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm @@ -457,6 +457,10 @@ - (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 b7caab0758c2e..4baa5b2f3347a 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm @@ -661,6 +661,10 @@ - (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 {