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

Commit bc78b7b

Browse files
committed
Fix Ctrl+Tab is broken on MacOS
1 parent eb3e7d5 commit bc78b7b

File tree

2 files changed

+102
-2
lines changed

2 files changed

+102
-2
lines changed

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

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h"
77

88
#include <Carbon/Carbon.h>
9+
#import <objc/message.h>
910

1011
#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterChannels.h"
1112
#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterCodecs.h"
@@ -163,6 +164,8 @@ @interface FlutterViewWrapper : NSView
163164

164165
- (void)setBackgroundColor:(NSColor*)color;
165166

167+
- (BOOL)performKeyEquivalent:(NSEvent*)event;
168+
166169
@end
167170

168171
/**
@@ -239,6 +242,37 @@ - (void)onKeyboardLayoutChanged;
239242

240243
@end
241244

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+
242276
#pragma mark - Private dependant functions
243277

244278
namespace {
@@ -258,12 +292,15 @@ void OnKeyboardLayoutChanged(CFNotificationCenterRef center,
258292

259293
@implementation FlutterViewWrapper {
260294
FlutterView* _flutterView;
295+
FlutterViewController* _controller;
261296
}
262297

263-
- (instancetype)initWithFlutterView:(FlutterView*)view {
298+
- (instancetype)initWithFlutterView:(FlutterView*)view
299+
controller:(FlutterViewController*)controller {
264300
self = [super initWithFrame:NSZeroRect];
265301
if (self) {
266302
_flutterView = view;
303+
_controller = controller;
267304
view.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
268305
[self addSubview:view];
269306
}
@@ -274,6 +311,24 @@ - (void)setBackgroundColor:(NSColor*)color {
274311
[_flutterView setBackgroundColor:color];
275312
}
276313

314+
- (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 endless loop, because FlutterViewController will
324+
// send the event back to [keyboardManager handleEvent:].
325+
return NO;
326+
}
327+
[event markAsKeyEquivalent];
328+
[_flutterView keyDown:event];
329+
return YES;
330+
}
331+
277332
- (NSArray*)accessibilityChildren {
278333
return @[ _flutterView ];
279334
}
@@ -405,7 +460,8 @@ - (void)loadView {
405460
if (_backgroundColor != nil) {
406461
[flutterView setBackgroundColor:_backgroundColor];
407462
}
408-
FlutterViewWrapper* wrapperView = [[FlutterViewWrapper alloc] initWithFlutterView:flutterView];
463+
FlutterViewWrapper* wrapperView = [[FlutterViewWrapper alloc] initWithFlutterView:flutterView
464+
controller:self];
409465
self.view = wrapperView;
410466
_flutterView = flutterView;
411467
}

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ @interface FlutterViewControllerTestObjC : NSObject
5959
- (bool)testKeyEventsAreSentToFramework;
6060
- (bool)testKeyEventsArePropagatedIfNotHandled;
6161
- (bool)testKeyEventsAreNotPropagatedIfHandled;
62+
- (bool)testCtrlTabKeyEventIsPropagated;
6263
- (bool)testFlagsChangedEventsArePropagatedIfNotHandled;
6364
- (bool)testKeyboardIsRestartedOnEngineRestart;
6465
- (bool)testTrackpadGesturesAreSentToFramework;
@@ -207,6 +208,10 @@ id MockGestureEvent(NSEventType type, NSEventPhase phase, double magnification,
207208
ASSERT_TRUE([[FlutterViewControllerTestObjC alloc] testKeyEventsAreNotPropagatedIfHandled]);
208209
}
209210

211+
TEST(FlutterViewControllerTest, TestCtrlTabKeyEventIsPropagated) {
212+
ASSERT_TRUE([[FlutterViewControllerTestObjC alloc] testCtrlTabKeyEventIsPropagated]);
213+
}
214+
210215
TEST(FlutterViewControllerTest, TestFlagsChangedEventsArePropagatedIfNotHandled) {
211216
ASSERT_TRUE(
212217
[[FlutterViewControllerTestObjC alloc] testFlagsChangedEventsArePropagatedIfNotHandled]);
@@ -280,6 +285,45 @@ - (bool)testKeyEventsAreSentToFramework {
280285
return true;
281286
}
282287

288+
// Regression test for https://github.com/flutter/flutter/issues/122084.
289+
- (bool)testCtrlTabKeyEventIsPropagated {
290+
id engineMock = flutter::testing::CreateMockFlutterEngine(@"");
291+
__block bool called = false;
292+
__block FlutterKeyEvent last_event;
293+
OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:FlutterKeyEvent {}
294+
callback:nil
295+
userData:nil])
296+
.andDo((^(NSInvocation* invocation) {
297+
FlutterKeyEvent* event;
298+
[invocation getArgument:&event atIndex:2];
299+
called = true;
300+
last_event = *event;
301+
}));
302+
FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
303+
nibName:@""
304+
bundle:nil];
305+
// Ctrl+tab
306+
NSEvent* event = [NSEvent keyEventWithType:NSEventTypeKeyDown
307+
location:NSZeroPoint
308+
modifierFlags:0x40101
309+
timestamp:0
310+
windowNumber:0
311+
context:nil
312+
characters:@""
313+
charactersIgnoringModifiers:@""
314+
isARepeat:NO
315+
keyCode:48];
316+
const uint64_t kPhysicalKeyTab = 0x7002b;
317+
318+
[viewController viewWillAppear]; // Initializes the event channel.
319+
[viewController.view performKeyEquivalent:event];
320+
321+
EXPECT_TRUE(called);
322+
EXPECT_EQ(last_event.type, kFlutterKeyEventTypeDown);
323+
EXPECT_EQ(last_event.physical, kPhysicalKeyTab);
324+
return true;
325+
}
326+
283327
- (bool)testKeyEventsArePropagatedIfNotHandled {
284328
id engineMock = flutter::testing::CreateMockFlutterEngine(@"");
285329
id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));

0 commit comments

Comments
 (0)