@@ -164,6 +164,8 @@ @interface FlutterViewWrapper : NSView
164164
165165- (void )setBackgroundColor : (NSColor *)color ;
166166
167+ - (BOOL )performKeyEquivalent : (NSEvent *)event ;
168+
167169@end
168170
169171/* *
@@ -240,6 +242,37 @@ - (void)onKeyboardLayoutChanged;
240242
241243@end
242244
245+ #pragma mark - NSEvent (KeyEquivalentMarker) protocol
246+
247+ @interface NSEvent (KeyEquivalentMarker)
248+
249+ // Internally marks that the event was received through performKeyEquivalent:.
250+ // When text editing is active, keyboard events that have modifier keys pressed
251+ // are received through performKeyEquivalent: instead of keyDown:. If such event
252+ // is passed to TextInputContext but doesn't result in a text editing action it
253+ // needs to be forwarded by FlutterKeyboardManager to the next responder.
254+ - (void )markAsKeyEquivalent ;
255+
256+ // Returns YES if the event is marked as a key equivalent.
257+ - (BOOL )isKeyEquivalent ;
258+
259+ @end
260+
261+ @implementation NSEvent (KeyEquivalentMarker)
262+
263+ // This field doesn't need a value because only its address is used as a unique identifier.
264+ static char markerKey;
265+
266+ - (void )markAsKeyEquivalent {
267+ objc_setAssociatedObject (self, &markerKey, @true , OBJC_ASSOCIATION_RETAIN );
268+ }
269+
270+ - (BOOL )isKeyEquivalent {
271+ return [objc_getAssociatedObject (self , &markerKey) boolValue ] == YES ;
272+ }
273+
274+ @end
275+
243276#pragma mark - Private dependant functions
244277
245278namespace {
@@ -279,15 +312,19 @@ - (void)setBackgroundColor:(NSColor*)color {
279312}
280313
281314- (BOOL )performKeyEquivalent : (NSEvent *)event {
282- // Do not intercept the event if flutterView is not first responder, otherwise this would
283- // interfere with TextInputPlugin, which also handles key equivalents.
284- //
285- // Also do not intercept the event if key equivalent is a product of an event being
286- // redispatched by the TextInputPlugin, in which case it needs to bubble up so that menus
287- // can handle key equivalents.
288- if (self.window .firstResponder != _flutterView || [_controller isDispatchingKeyEvent: event]) {
315+ if ([_controller isDispatchingKeyEvent: event]) {
316+ // When NSWindow is nextResponder, keyboard manager will send to it
317+ // unhandled events (through [NSWindow keyDown:]). If event has both
318+ // control and cmd modifiers set (i.e. cmd+control+space - emoji picker)
319+ // NSWindow will then send this event as performKeyEquivalent: to first
320+ // responder, which might be FlutterTextInputPlugin. If that's the case, the
321+ // plugin must not handle the event, otherwise the emoji picker would not
322+ // work (due to first responder returning YES from performKeyEquivalent:)
323+ // and there would be an infinite loop, because FlutterViewController will
324+ // send the event back to [keyboardManager handleEvent:].
289325 return NO ;
290326 }
327+ [event markAsKeyEquivalent ];
291328 [_flutterView keyDown: event];
292329 return YES ;
293330}
0 commit comments