[macOS] Synchronise modifiers from mouse events for RawKeyboard (flutter/engine#46230)

Fixes https://github.com/flutter/flutter/issues/135349

This has been done for FlutterEmbedderKeyResponder in
https://github.com/flutter/engine/pull/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
This commit is contained in:
Matej Knopp
2023-09-27 17:09:49 +02:00
committed by GitHub
parent 168a4156df
commit 066459ce65
4 changed files with 125 additions and 8 deletions

View File

@@ -40,6 +40,74 @@
return self;
}
/// Checks single modifier flag from event flags and sends appropriate key event
/// if it is different from the previous state.
- (void)checkModifierFlag:(NSUInteger)targetMask
forEventFlags:(NSEventModifierFlags)eventFlags
keyCode:(NSUInteger)keyCode
timestamp:(NSTimeInterval)timestamp {
NSAssert((targetMask & (targetMask - 1)) == 0, @"targetMask must only have one bit set");
if ((eventFlags & targetMask) != (_previouslyPressedFlags & targetMask)) {
uint64_t newFlags = (_previouslyPressedFlags & ~targetMask) | (eventFlags & targetMask);
// Sets combined flag if either left or right modifier is pressed, unsets otherwise.
auto updateCombinedFlag = [&](uint64_t side1, uint64_t side2, NSEventModifierFlags flag) {
if (newFlags & (side1 | side2)) {
newFlags |= flag;
} else {
newFlags &= ~flag;
}
};
updateCombinedFlag(flutter::kModifierFlagShiftLeft, flutter::kModifierFlagShiftRight,
NSEventModifierFlagShift);
updateCombinedFlag(flutter::kModifierFlagControlLeft, flutter::kModifierFlagControlRight,
NSEventModifierFlagControl);
updateCombinedFlag(flutter::kModifierFlagAltLeft, flutter::kModifierFlagAltRight,
NSEventModifierFlagOption);
updateCombinedFlag(flutter::kModifierFlagMetaLeft, flutter::kModifierFlagMetaRight,
NSEventModifierFlagCommand);
NSEvent* event = [NSEvent keyEventWithType:NSEventTypeFlagsChanged
location:NSZeroPoint
modifierFlags:newFlags
timestamp:timestamp
windowNumber:0
context:nil
characters:@""
charactersIgnoringModifiers:@""
isARepeat:NO
keyCode:keyCode];
[self handleEvent:event
callback:^(BOOL){
}];
};
}
- (void)syncModifiersIfNeeded:(NSEventModifierFlags)modifierFlags
timestamp:(NSTimeInterval)timestamp {
modifierFlags = modifierFlags & ~0x100;
if (_previouslyPressedFlags == modifierFlags) {
return;
}
[flutter::modifierFlagToKeyCode
enumerateKeysAndObjectsUsingBlock:^(NSNumber* flag, NSNumber* keyCode, BOOL* stop) {
[self checkModifierFlag:[flag unsignedShortValue]
forEventFlags:modifierFlags
keyCode:[keyCode unsignedShortValue]
timestamp:timestamp];
}];
// Caps lock is not included in the modifierFlagToKeyCode map.
[self checkModifierFlag:NSEventModifierFlagCapsLock
forEventFlags:modifierFlags
keyCode:0x00000039 // kVK_CapsLock
timestamp:timestamp];
// At the end we should end up with the same modifier flags as the event.
FML_DCHECK(_previouslyPressedFlags == modifierFlags);
}
- (void)handleEvent:(NSEvent*)event callback:(FlutterAsyncKeyCallback)callback {
// Remove the modifier bits that Flutter is not interested in.
NSEventModifierFlags modifierFlags = event.modifierFlags & ~0x100;

View File

@@ -24,6 +24,15 @@ typedef void (^FlutterAsyncKeyCallback)(BOOL handled);
@required
- (void)handleEvent:(nonnull NSEvent*)event callback:(nonnull FlutterAsyncKeyCallback)callback;
/**
* Synchronize the modifier flags if necessary. The new modifier flag would usually come from mouse
* event and may be out of sync with current keyboard state if the modifier flags have changed while
* window was not key.
*/
@required
- (void)syncModifiersIfNeeded:(NSEventModifierFlags)modifierFlags
timestamp:(NSTimeInterval)timestamp;
/* A map from macOS key code to logical keyboard.
*
* The map is assigned on initialization, and updated when the user changes

View File

@@ -337,10 +337,9 @@ typedef _Nullable _NSResponderPtr (^NextResponderProvider)();
- (void)syncModifiersIfNeeded:(NSEventModifierFlags)modifierFlags
timestamp:(NSTimeInterval)timestamp {
// The embedder responder is the first element in _primaryResponders.
FlutterEmbedderKeyResponder* embedderResponder =
(FlutterEmbedderKeyResponder*)_primaryResponders[0];
[embedderResponder syncModifiersIfNeeded:modifierFlags timestamp:timestamp];
for (id<FlutterKeyPrimaryResponder> responder in _primaryResponders) {
[responder syncModifiersIfNeeded:modifierFlags timestamp:timestamp];
}
}
/**

View File

@@ -957,13 +957,17 @@ static void SwizzledNoop(id self, SEL _cmd) {}
- (bool)testModifierKeysAreSynthesizedOnMouseMove {
id engineMock = flutter::testing::CreateMockFlutterEngine(@"");
id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
[engineMock binaryMessenger])
.andReturn(binaryMessengerMock);
// Need to return a real renderer to allow view controller to load.
FlutterRenderer* renderer_ = [[FlutterRenderer alloc] initWithFlutterEngine:engineMock];
OCMStub([engineMock renderer]).andReturn(renderer_);
// Capture calls to sendKeyEvent
__block NSMutableArray<KeyEventWrapper*>* events =
[[NSMutableArray<KeyEventWrapper*> alloc] init];
__block NSMutableArray<KeyEventWrapper*>* events = [NSMutableArray array];
OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:FlutterKeyEvent {}
callback:nil
userData:nil])
@@ -973,6 +977,17 @@ static void SwizzledNoop(id self, SEL _cmd) {}
[events addObject:[[KeyEventWrapper alloc] initWithEvent:event]];
}));
__block NSMutableArray<NSDictionary*>* channelEvents = [NSMutableArray array];
OCMStub([binaryMessengerMock sendOnChannel:@"flutter/keyevent"
message:[OCMArg any]
binaryReply:[OCMArg any]])
.andDo((^(NSInvocation* invocation) {
NSData* data;
[invocation getArgument:&data atIndex:3];
id event = [[FlutterJSONMessageCodec sharedInstance] decode:data];
[channelEvents addObject:event];
}));
FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
nibName:@""
bundle:nil];
@@ -987,12 +1002,27 @@ static void SwizzledNoop(id self, SEL _cmd) {}
// For each modifier key, check that key events are synthesized.
for (NSNumber* keyCode in flutter::keyCodeToModifierFlag) {
FlutterKeyEvent* event;
NSDictionary* channelEvent;
NSNumber* logicalKey;
NSNumber* physicalKey;
NSNumber* flag = flutter::keyCodeToModifierFlag[keyCode];
NSEventModifierFlags flag = [flutter::keyCodeToModifierFlag[keyCode] unsignedLongValue];
// Cocoa event always contain combined flags.
if (flag & (flutter::kModifierFlagShiftLeft | flutter::kModifierFlagShiftRight)) {
flag |= NSEventModifierFlagShift;
}
if (flag & (flutter::kModifierFlagControlLeft | flutter::kModifierFlagControlRight)) {
flag |= NSEventModifierFlagControl;
}
if (flag & (flutter::kModifierFlagAltLeft | flutter::kModifierFlagAltRight)) {
flag |= NSEventModifierFlagOption;
}
if (flag & (flutter::kModifierFlagMetaLeft | flutter::kModifierFlagMetaRight)) {
flag |= NSEventModifierFlagCommand;
}
// Should synthesize down event.
NSEvent* mouseEvent = flutter::testing::CreateMouseEvent([flag unsignedLongValue]);
NSEvent* mouseEvent = flutter::testing::CreateMouseEvent(flag);
[viewController mouseMoved:mouseEvent];
EXPECT_EQ([events count], 1u);
event = events[0].data;
@@ -1003,6 +1033,11 @@ static void SwizzledNoop(id self, SEL _cmd) {}
EXPECT_EQ(event->physical, physicalKey.unsignedLongLongValue);
EXPECT_EQ(event->synthesized, true);
channelEvent = channelEvents[0];
EXPECT_TRUE([channelEvent[@"type"] isEqual:@"keydown"]);
EXPECT_TRUE([channelEvent[@"keyCode"] isEqual:keyCode]);
EXPECT_TRUE([channelEvent[@"modifiers"] isEqual:@(flag)]);
// Should synthesize up event.
mouseEvent = flutter::testing::CreateMouseEvent(0x00);
[viewController mouseMoved:mouseEvent];
@@ -1015,7 +1050,13 @@ static void SwizzledNoop(id self, SEL _cmd) {}
EXPECT_EQ(event->physical, physicalKey.unsignedLongLongValue);
EXPECT_EQ(event->synthesized, true);
channelEvent = channelEvents[1];
EXPECT_TRUE([channelEvent[@"type"] isEqual:@"keyup"]);
EXPECT_TRUE([channelEvent[@"keyCode"] isEqual:keyCode]);
EXPECT_TRUE([channelEvent[@"modifiers"] isEqual:@(0)]);
[events removeAllObjects];
[channelEvents removeAllObjects];
};
return true;