Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 2a3a248

Browse files
knoppharryterkelsen
authored andcommitted
Reland: [macOS] performKeyEquivalent cleanup (#46377)
Fixes the issue in original PR where `FlutterViewWrapper` does not pass the key equivalent to subviews thus prevents `TextInputPlugin` from receiving it. Also adds a regression test for this scenario. Note that key equivalent flow does not respect the regular responder chain. It is passed from root view to down to subviews and if unhandled will be forwarded to menus. Original message: #40706 added a duplicate `NSEvent (KeyEquivalentMarker)` category. This PR removes it. It also removes the call to `markAsKeyEquivalent` from `FlutterViewController`. That call is internal to `FlutterTextInputPlugin` and classes outside should not be calling it. *If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].* [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
1 parent 962489e commit 2a3a248

File tree

2 files changed

+71
-45
lines changed

2 files changed

+71
-45
lines changed

shell/platform/darwin/macos/framework/Source/FlutterViewController.mm

Lines changed: 8 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -164,8 +164,6 @@ @interface FlutterViewWrapper : NSView
164164

165165
- (void)setBackgroundColor:(NSColor*)color;
166166

167-
- (BOOL)performKeyEquivalent:(NSEvent*)event;
168-
169167
@end
170168

171169
/**
@@ -242,37 +240,6 @@ - (void)onKeyboardLayoutChanged;
242240

243241
@end
244242

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-
276243
#pragma mark - Private dependant functions
277244

278245
namespace {
@@ -312,19 +279,15 @@ - (void)setBackgroundColor:(NSColor*)color {
312279
}
313280

314281
- (BOOL)performKeyEquivalent:(NSEvent*)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:].
325-
return NO;
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]) {
289+
return [super performKeyEquivalent:event];
326290
}
327-
[event markAsKeyEquivalent];
328291
[_flutterView keyDown:event];
329292
return YES;
330293
}

shell/platform/darwin/macos/framework/Source/FlutterViewControllerTest.mm

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ - (bool)testKeyEventsAreSentToFramework;
6060
- (bool)testKeyEventsArePropagatedIfNotHandled;
6161
- (bool)testKeyEventsAreNotPropagatedIfHandled;
6262
- (bool)testCtrlTabKeyEventIsPropagated;
63+
- (bool)testKeyEquivalentIsPassedToTextInputPlugin;
6364
- (bool)testFlagsChangedEventsArePropagatedIfNotHandled;
6465
- (bool)testKeyboardIsRestartedOnEngineRestart;
6566
- (bool)testTrackpadGesturesAreSentToFramework;
@@ -215,6 +216,10 @@ id MockGestureEvent(NSEventType type, NSEventPhase phase, double magnification,
215216
ASSERT_TRUE([[FlutterViewControllerTestObjC alloc] testCtrlTabKeyEventIsPropagated]);
216217
}
217218

219+
TEST(FlutterViewControllerTest, TestKeyEquivalentIsPassedToTextInputPlugin) {
220+
ASSERT_TRUE([[FlutterViewControllerTestObjC alloc] testKeyEquivalentIsPassedToTextInputPlugin]);
221+
}
222+
218223
TEST(FlutterViewControllerTest, TestFlagsChangedEventsArePropagatedIfNotHandled) {
219224
ASSERT_TRUE(
220225
[[FlutterViewControllerTestObjC alloc] testFlagsChangedEventsArePropagatedIfNotHandled]);
@@ -331,6 +336,64 @@ - (bool)testCtrlTabKeyEventIsPropagated {
331336
const uint64_t kPhysicalKeyTab = 0x7002b;
332337

333338
[viewController viewWillAppear]; // Initializes the event channel.
339+
// Creates a NSWindow so that FlutterView view can be first responder.
340+
NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 800, 600)
341+
styleMask:NSBorderlessWindowMask
342+
backing:NSBackingStoreBuffered
343+
defer:NO];
344+
window.contentView = viewController.view;
345+
[window makeFirstResponder:viewController.flutterView];
346+
[viewController.view performKeyEquivalent:event];
347+
348+
EXPECT_TRUE(called);
349+
EXPECT_EQ(last_event.type, kFlutterKeyEventTypeDown);
350+
EXPECT_EQ(last_event.physical, kPhysicalKeyTab);
351+
return true;
352+
}
353+
354+
- (bool)testKeyEquivalentIsPassedToTextInputPlugin {
355+
id engineMock = flutter::testing::CreateMockFlutterEngine(@"");
356+
__block bool called = false;
357+
__block FlutterKeyEvent last_event;
358+
OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:FlutterKeyEvent {}
359+
callback:nil
360+
userData:nil])
361+
.andDo((^(NSInvocation* invocation) {
362+
FlutterKeyEvent* event;
363+
[invocation getArgument:&event atIndex:2];
364+
called = true;
365+
last_event = *event;
366+
}));
367+
FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
368+
nibName:@""
369+
bundle:nil];
370+
// Ctrl+tab
371+
NSEvent* event = [NSEvent keyEventWithType:NSEventTypeKeyDown
372+
location:NSZeroPoint
373+
modifierFlags:0x40101
374+
timestamp:0
375+
windowNumber:0
376+
context:nil
377+
characters:@""
378+
charactersIgnoringModifiers:@""
379+
isARepeat:NO
380+
keyCode:48];
381+
const uint64_t kPhysicalKeyTab = 0x7002b;
382+
383+
[viewController viewWillAppear]; // Initializes the event channel.
384+
385+
NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 800, 600)
386+
styleMask:NSBorderlessWindowMask
387+
backing:NSBackingStoreBuffered
388+
defer:NO];
389+
window.contentView = viewController.view;
390+
391+
[viewController.view addSubview:viewController.textInputPlugin];
392+
393+
// Make the textInputPlugin first responder. This should still result in
394+
// view controller reporting the key event.
395+
[window makeFirstResponder:viewController.textInputPlugin];
396+
334397
[viewController.view performKeyEquivalent:event];
335398

336399
EXPECT_TRUE(called);

0 commit comments

Comments
 (0)