diff --git a/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm b/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm index 7e7e0c9cc684c..5c22fe6c01d71 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm @@ -164,6 +164,8 @@ @interface FlutterViewWrapper : NSView - (void)setBackgroundColor:(NSColor*)color; +- (BOOL)performKeyEquivalent:(NSEvent*)event; + @end /** @@ -240,6 +242,37 @@ - (void)onKeyboardLayoutChanged; @end +#pragma mark - NSEvent (KeyEquivalentMarker) protocol + +@interface NSEvent (KeyEquivalentMarker) + +// Internally marks that the event was received through performKeyEquivalent:. +// When text editing is active, keyboard events that have modifier keys pressed +// are received through performKeyEquivalent: instead of keyDown:. If such event +// is passed to TextInputContext but doesn't result in a text editing action it +// needs to be forwarded by FlutterKeyboardManager to the next responder. +- (void)markAsKeyEquivalent; + +// Returns YES if the event is marked as a key equivalent. +- (BOOL)isKeyEquivalent; + +@end + +@implementation NSEvent (KeyEquivalentMarker) + +// This field doesn't need a value because only its address is used as a unique identifier. +static char markerKey; + +- (void)markAsKeyEquivalent { + objc_setAssociatedObject(self, &markerKey, @true, OBJC_ASSOCIATION_RETAIN); +} + +- (BOOL)isKeyEquivalent { + return [objc_getAssociatedObject(self, &markerKey) boolValue] == YES; +} + +@end + #pragma mark - Private dependant functions namespace { @@ -279,15 +312,19 @@ - (void)setBackgroundColor:(NSColor*)color { } - (BOOL)performKeyEquivalent:(NSEvent*)event { - // Do not intercept the event if flutterView is not first responder, otherwise this would - // interfere with TextInputPlugin, which also handles key equivalents. - // - // Also do not intercept the event if key equivalent is a product of an event being - // redispatched by the TextInputPlugin, in which case it needs to bubble up so that menus - // can handle key equivalents. - if (self.window.firstResponder != _flutterView || [_controller isDispatchingKeyEvent:event]) { + if ([_controller isDispatchingKeyEvent:event]) { + // When NSWindow is nextResponder, keyboard manager will send to it + // unhandled events (through [NSWindow keyDown:]). If event has both + // control and cmd modifiers set (i.e. cmd+control+space - emoji picker) + // NSWindow will then send this event as performKeyEquivalent: to first + // responder, which might be FlutterTextInputPlugin. If that's the case, the + // plugin must not handle the event, otherwise the emoji picker would not + // work (due to first responder returning YES from performKeyEquivalent:) + // and there would be an infinite loop, because FlutterViewController will + // send the event back to [keyboardManager handleEvent:]. return NO; } + [event markAsKeyEquivalent]; [_flutterView keyDown:event]; return YES; } diff --git a/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTest.mm index 7c0a96a1f8920..7894e9258b9ef 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTest.mm @@ -331,13 +331,6 @@ - (bool)testCtrlTabKeyEventIsPropagated { const uint64_t kPhysicalKeyTab = 0x7002b; [viewController viewWillAppear]; // Initializes the event channel. - // Creates a NSWindow so that FlutterView view can be first responder. - NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 800, 600) - styleMask:NSBorderlessWindowMask - backing:NSBackingStoreBuffered - defer:NO]; - window.contentView = viewController.view; - [window makeFirstResponder:viewController.flutterView]; [viewController.view performKeyEquivalent:event]; EXPECT_TRUE(called);