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

Commit eb1e48e

Browse files
knoppharryterkelsen
authored andcommitted
[macOS] Synchronise modifiers from mouse events for RawKeyboard (#46230)
Fixes flutter/flutter#135349 This has been done for FlutterEmbedderKeyResponder in #37870, but has not been implemented for FlutterChannelKeyResponder, which results in RawKeyboard being out of sync with HardwareKeyboard. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide] and the [C++, Objective-C, Java style guides]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I added new tests to check the change I am making or feature I am adding, or the PR is [test-exempt]. See [testing the engine] for instructions on writing and running engine tests. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I signed the [CLA]. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/wiki/Tree-hygiene#overview [Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene [test-exempt]: https://github.com/flutter/flutter/wiki/Tree-hygiene#tests [Flutter Style Guide]: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style [testing the engine]: https://github.com/flutter/flutter/wiki/Testing-the-engine [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/wiki/Chat
1 parent 674ea73 commit eb1e48e

File tree

4 files changed

+125
-8
lines changed

4 files changed

+125
-8
lines changed

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

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,74 @@ - (nonnull instancetype)initWithChannel:(nonnull FlutterBasicMessageChannel*)cha
4040
return self;
4141
}
4242

43+
/// Checks single modifier flag from event flags and sends appropriate key event
44+
/// if it is different from the previous state.
45+
- (void)checkModifierFlag:(NSUInteger)targetMask
46+
forEventFlags:(NSEventModifierFlags)eventFlags
47+
keyCode:(NSUInteger)keyCode
48+
timestamp:(NSTimeInterval)timestamp {
49+
NSAssert((targetMask & (targetMask - 1)) == 0, @"targetMask must only have one bit set");
50+
if ((eventFlags & targetMask) != (_previouslyPressedFlags & targetMask)) {
51+
uint64_t newFlags = (_previouslyPressedFlags & ~targetMask) | (eventFlags & targetMask);
52+
53+
// Sets combined flag if either left or right modifier is pressed, unsets otherwise.
54+
auto updateCombinedFlag = [&](uint64_t side1, uint64_t side2, NSEventModifierFlags flag) {
55+
if (newFlags & (side1 | side2)) {
56+
newFlags |= flag;
57+
} else {
58+
newFlags &= ~flag;
59+
}
60+
};
61+
updateCombinedFlag(flutter::kModifierFlagShiftLeft, flutter::kModifierFlagShiftRight,
62+
NSEventModifierFlagShift);
63+
updateCombinedFlag(flutter::kModifierFlagControlLeft, flutter::kModifierFlagControlRight,
64+
NSEventModifierFlagControl);
65+
updateCombinedFlag(flutter::kModifierFlagAltLeft, flutter::kModifierFlagAltRight,
66+
NSEventModifierFlagOption);
67+
updateCombinedFlag(flutter::kModifierFlagMetaLeft, flutter::kModifierFlagMetaRight,
68+
NSEventModifierFlagCommand);
69+
70+
NSEvent* event = [NSEvent keyEventWithType:NSEventTypeFlagsChanged
71+
location:NSZeroPoint
72+
modifierFlags:newFlags
73+
timestamp:timestamp
74+
windowNumber:0
75+
context:nil
76+
characters:@""
77+
charactersIgnoringModifiers:@""
78+
isARepeat:NO
79+
keyCode:keyCode];
80+
[self handleEvent:event
81+
callback:^(BOOL){
82+
}];
83+
};
84+
}
85+
86+
- (void)syncModifiersIfNeeded:(NSEventModifierFlags)modifierFlags
87+
timestamp:(NSTimeInterval)timestamp {
88+
modifierFlags = modifierFlags & ~0x100;
89+
if (_previouslyPressedFlags == modifierFlags) {
90+
return;
91+
}
92+
93+
[flutter::modifierFlagToKeyCode
94+
enumerateKeysAndObjectsUsingBlock:^(NSNumber* flag, NSNumber* keyCode, BOOL* stop) {
95+
[self checkModifierFlag:[flag unsignedShortValue]
96+
forEventFlags:modifierFlags
97+
keyCode:[keyCode unsignedShortValue]
98+
timestamp:timestamp];
99+
}];
100+
101+
// Caps lock is not included in the modifierFlagToKeyCode map.
102+
[self checkModifierFlag:NSEventModifierFlagCapsLock
103+
forEventFlags:modifierFlags
104+
keyCode:0x00000039 // kVK_CapsLock
105+
timestamp:timestamp];
106+
107+
// At the end we should end up with the same modifier flags as the event.
108+
FML_DCHECK(_previouslyPressedFlags == modifierFlags);
109+
}
110+
43111
- (void)handleEvent:(NSEvent*)event callback:(FlutterAsyncKeyCallback)callback {
44112
// Remove the modifier bits that Flutter is not interested in.
45113
NSEventModifierFlags modifierFlags = event.modifierFlags & ~0x100;

shell/platform/darwin/macos/framework/Source/FlutterKeyPrimaryResponder.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@ typedef void (^FlutterAsyncKeyCallback)(BOOL handled);
2424
@required
2525
- (void)handleEvent:(nonnull NSEvent*)event callback:(nonnull FlutterAsyncKeyCallback)callback;
2626

27+
/**
28+
* Synchronize the modifier flags if necessary. The new modifier flag would usually come from mouse
29+
* event and may be out of sync with current keyboard state if the modifier flags have changed while
30+
* window was not key.
31+
*/
32+
@required
33+
- (void)syncModifiersIfNeeded:(NSEventModifierFlags)modifierFlags
34+
timestamp:(NSTimeInterval)timestamp;
35+
2736
/* A map from macOS key code to logical keyboard.
2837
*
2938
* The map is assigned on initialization, and updated when the user changes

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

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -337,10 +337,9 @@ - (void)buildLayout {
337337

338338
- (void)syncModifiersIfNeeded:(NSEventModifierFlags)modifierFlags
339339
timestamp:(NSTimeInterval)timestamp {
340-
// The embedder responder is the first element in _primaryResponders.
341-
FlutterEmbedderKeyResponder* embedderResponder =
342-
(FlutterEmbedderKeyResponder*)_primaryResponders[0];
343-
[embedderResponder syncModifiersIfNeeded:modifierFlags timestamp:timestamp];
340+
for (id<FlutterKeyPrimaryResponder> responder in _primaryResponders) {
341+
[responder syncModifiersIfNeeded:modifierFlags timestamp:timestamp];
342+
}
344343
}
345344

346345
/**

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

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -957,13 +957,17 @@ - (bool)testMouseDownUpEventsSentToNextResponder {
957957

958958
- (bool)testModifierKeysAreSynthesizedOnMouseMove {
959959
id engineMock = flutter::testing::CreateMockFlutterEngine(@"");
960+
id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
961+
OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
962+
[engineMock binaryMessenger])
963+
.andReturn(binaryMessengerMock);
964+
960965
// Need to return a real renderer to allow view controller to load.
961966
FlutterRenderer* renderer_ = [[FlutterRenderer alloc] initWithFlutterEngine:engineMock];
962967
OCMStub([engineMock renderer]).andReturn(renderer_);
963968

964969
// Capture calls to sendKeyEvent
965-
__block NSMutableArray<KeyEventWrapper*>* events =
966-
[[NSMutableArray<KeyEventWrapper*> alloc] init];
970+
__block NSMutableArray<KeyEventWrapper*>* events = [NSMutableArray array];
967971
OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:FlutterKeyEvent {}
968972
callback:nil
969973
userData:nil])
@@ -973,6 +977,17 @@ - (bool)testModifierKeysAreSynthesizedOnMouseMove {
973977
[events addObject:[[KeyEventWrapper alloc] initWithEvent:event]];
974978
}));
975979

980+
__block NSMutableArray<NSDictionary*>* channelEvents = [NSMutableArray array];
981+
OCMStub([binaryMessengerMock sendOnChannel:@"flutter/keyevent"
982+
message:[OCMArg any]
983+
binaryReply:[OCMArg any]])
984+
.andDo((^(NSInvocation* invocation) {
985+
NSData* data;
986+
[invocation getArgument:&data atIndex:3];
987+
id event = [[FlutterJSONMessageCodec sharedInstance] decode:data];
988+
[channelEvents addObject:event];
989+
}));
990+
976991
FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
977992
nibName:@""
978993
bundle:nil];
@@ -987,12 +1002,27 @@ - (bool)testModifierKeysAreSynthesizedOnMouseMove {
9871002
// For each modifier key, check that key events are synthesized.
9881003
for (NSNumber* keyCode in flutter::keyCodeToModifierFlag) {
9891004
FlutterKeyEvent* event;
1005+
NSDictionary* channelEvent;
9901006
NSNumber* logicalKey;
9911007
NSNumber* physicalKey;
992-
NSNumber* flag = flutter::keyCodeToModifierFlag[keyCode];
1008+
NSEventModifierFlags flag = [flutter::keyCodeToModifierFlag[keyCode] unsignedLongValue];
1009+
1010+
// Cocoa event always contain combined flags.
1011+
if (flag & (flutter::kModifierFlagShiftLeft | flutter::kModifierFlagShiftRight)) {
1012+
flag |= NSEventModifierFlagShift;
1013+
}
1014+
if (flag & (flutter::kModifierFlagControlLeft | flutter::kModifierFlagControlRight)) {
1015+
flag |= NSEventModifierFlagControl;
1016+
}
1017+
if (flag & (flutter::kModifierFlagAltLeft | flutter::kModifierFlagAltRight)) {
1018+
flag |= NSEventModifierFlagOption;
1019+
}
1020+
if (flag & (flutter::kModifierFlagMetaLeft | flutter::kModifierFlagMetaRight)) {
1021+
flag |= NSEventModifierFlagCommand;
1022+
}
9931023

9941024
// Should synthesize down event.
995-
NSEvent* mouseEvent = flutter::testing::CreateMouseEvent([flag unsignedLongValue]);
1025+
NSEvent* mouseEvent = flutter::testing::CreateMouseEvent(flag);
9961026
[viewController mouseMoved:mouseEvent];
9971027
EXPECT_EQ([events count], 1u);
9981028
event = events[0].data;
@@ -1003,6 +1033,11 @@ - (bool)testModifierKeysAreSynthesizedOnMouseMove {
10031033
EXPECT_EQ(event->physical, physicalKey.unsignedLongLongValue);
10041034
EXPECT_EQ(event->synthesized, true);
10051035

1036+
channelEvent = channelEvents[0];
1037+
EXPECT_TRUE([channelEvent[@"type"] isEqual:@"keydown"]);
1038+
EXPECT_TRUE([channelEvent[@"keyCode"] isEqual:keyCode]);
1039+
EXPECT_TRUE([channelEvent[@"modifiers"] isEqual:@(flag)]);
1040+
10061041
// Should synthesize up event.
10071042
mouseEvent = flutter::testing::CreateMouseEvent(0x00);
10081043
[viewController mouseMoved:mouseEvent];
@@ -1015,7 +1050,13 @@ - (bool)testModifierKeysAreSynthesizedOnMouseMove {
10151050
EXPECT_EQ(event->physical, physicalKey.unsignedLongLongValue);
10161051
EXPECT_EQ(event->synthesized, true);
10171052

1053+
channelEvent = channelEvents[1];
1054+
EXPECT_TRUE([channelEvent[@"type"] isEqual:@"keyup"]);
1055+
EXPECT_TRUE([channelEvent[@"keyCode"] isEqual:keyCode]);
1056+
EXPECT_TRUE([channelEvent[@"modifiers"] isEqual:@(0)]);
1057+
10181058
[events removeAllObjects];
1059+
[channelEvents removeAllObjects];
10191060
};
10201061

10211062
return true;

0 commit comments

Comments
 (0)