[macos] Synthesize modifier keys events on pointer events (#37870)
* [macos] Synthesize modifier keys events on pointer events
* Move test to FlutterViewControllerTest
* Simplify by using 'for in'
Co-authored-by: Bruno Leroux <bruno.leroux@gmail.com>
diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponder.h b/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponder.h
index ab2b1c1..7a3b9ee 100644
--- a/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponder.h
+++ b/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponder.h
@@ -30,4 +30,13 @@
*/
- (nonnull instancetype)initWithSendEvent:(_Nonnull FlutterSendEmbedderKeyEvent)sendEvent;
+/**
+ * Synthesize modifier keys events.
+ *
+ * If needed, synthesize modifier keys up and down events by comparing their
+ * current pressing states with the given modifier flags.
+ */
+- (void)syncModifiersIfNeeded:(NSEventModifierFlags)modifierFlags
+ timestamp:(NSTimeInterval)timestamp;
+
@end
diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponder.mm b/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponder.mm
index 5854bb0..4a5d14f 100644
--- a/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponder.mm
+++ b/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponder.mm
@@ -780,6 +780,19 @@
[_pendingResponses removeObjectForKey:@(responseId)];
}
+- (void)syncModifiersIfNeeded:(NSEventModifierFlags)modifierFlags
+ timestamp:(NSTimeInterval)timestamp {
+ FlutterAsyncKeyCallback replyCallback = ^(BOOL handled) {
+ // Do nothing.
+ };
+ FlutterKeyCallbackGuard* guardedCallback =
+ [[FlutterKeyCallbackGuard alloc] initWithCallback:replyCallback];
+ [self synchronizeModifiers:modifierFlags
+ ignoringFlags:0
+ timestamp:timestamp
+ guard:guardedCallback];
+}
+
@end
namespace {
diff --git a/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.h b/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.h
index c8ba505..64ca1a3 100644
--- a/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.h
+++ b/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.h
@@ -48,4 +48,13 @@
*/
- (BOOL)isDispatchingKeyEvent:(nonnull NSEvent*)event;
+/**
+ * Synthesize modifier keys events.
+ *
+ * If needed, synthesize modifier keys up and down events by comparing their
+ * current pressing states with the given modifier flags.
+ */
+- (void)syncModifiersIfNeeded:(NSEventModifierFlags)modifierFlags
+ timestamp:(NSTimeInterval)timestamp;
+
@end
diff --git a/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.mm b/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.mm
index 116c945..78c18dd 100644
--- a/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.mm
+++ b/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.mm
@@ -320,4 +320,12 @@
}
}
+- (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];
+}
+
@end
diff --git a/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm b/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm
index 9034e8d..2c9e76e 100644
--- a/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm
+++ b/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm
@@ -661,6 +661,8 @@
flutterEvent.scroll_delta_y = scaledDeltaY;
}
}
+
+ [_keyboardManager syncModifiersIfNeeded:event.modifierFlags timestamp:event.timestamp];
[_engine sendPointerEvent:flutterEvent];
// Update tracking of state as reported to Flutter.
diff --git a/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTest.mm
index 9030657..dbc9887 100644
--- a/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTest.mm
+++ b/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTest.mm
@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+#import "KeyCodeMap_Internal.h"
#import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterViewController.h"
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h"
@@ -13,8 +14,27 @@
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h"
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterRenderer.h"
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTestUtils.h"
+#include "flutter/shell/platform/embedder/test_utils/key_codes.g.h"
#import "flutter/testing/testing.h"
+// A wrap to convert FlutterKeyEvent to a ObjC class.
+@interface KeyEventWrapper : NSObject
+@property(nonatomic) FlutterKeyEvent* data;
+- (nonnull instancetype)initWithEvent:(const FlutterKeyEvent*)event;
+@end
+
+@implementation KeyEventWrapper
+- (instancetype)initWithEvent:(const FlutterKeyEvent*)event {
+ self = [super init];
+ _data = new FlutterKeyEvent(*event);
+ return self;
+}
+
+- (void)dealloc {
+ delete _data;
+}
+@end
+
@interface FlutterViewControllerTestObjC : NSObject
- (bool)testKeyEventsAreSentToFramework;
- (bool)testKeyEventsArePropagatedIfNotHandled;
@@ -22,6 +42,7 @@
- (bool)testFlagsChangedEventsArePropagatedIfNotHandled;
- (bool)testKeyboardIsRestartedOnEngineRestart;
- (bool)testTrackpadGesturesAreSentToFramework;
+- (bool)testModifierKeysAreSynthesizedOnMouseMove;
- (bool)testViewWillAppearCalledMultipleTimes;
- (bool)testFlutterViewIsConfigured;
@@ -30,6 +51,8 @@
userData:(nullable void*)userData;
@end
+using namespace ::flutter::testing::keycodes;
+
namespace flutter::testing {
namespace {
@@ -69,6 +92,19 @@
OCMStub([mock flagsChanged:[OCMArg any]]).andDo(nil);
return mock;
}
+
+NSEvent* CreateMouseEvent(NSEventModifierFlags modifierFlags) {
+ return [NSEvent mouseEventWithType:NSEventTypeMouseMoved
+ location:NSZeroPoint
+ modifierFlags:modifierFlags
+ timestamp:0
+ windowNumber:0
+ context:nil
+ eventNumber:0
+ clickCount:1
+ pressure:1.0];
+}
+
} // namespace
TEST(FlutterViewController, HasViewThatHidesOtherViewsInAccessibility) {
@@ -161,6 +197,10 @@
ASSERT_TRUE([[FlutterViewControllerTestObjC alloc] testTrackpadGesturesAreSentToFramework]);
}
+TEST(FlutterViewControllerTest, TestModifierKeysAreSynthesizedOnMouseMove) {
+ ASSERT_TRUE([[FlutterViewControllerTestObjC alloc] testModifierKeysAreSynthesizedOnMouseMove]);
+}
+
TEST(FlutterViewControllerTest, testViewWillAppearCalledMultipleTimes) {
ASSERT_TRUE([[FlutterViewControllerTestObjC alloc] testViewWillAppearCalledMultipleTimes]);
}
@@ -763,4 +803,71 @@
return true;
}
+- (bool)testModifierKeysAreSynthesizedOnMouseMove {
+ id engineMock = OCMClassMock([FlutterEngine class]);
+ // 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];
+ OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:FlutterKeyEvent {}
+ callback:nil
+ userData:nil])
+ .andDo((^(NSInvocation* invocation) {
+ FlutterKeyEvent* event;
+ [invocation getArgument:&event atIndex:2];
+ [events addObject:[[KeyEventWrapper alloc] initWithEvent:event]];
+ }));
+
+ FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
+ nibName:@""
+ bundle:nil];
+ [viewController loadView];
+ [engineMock setViewController:viewController];
+ [viewController viewWillAppear];
+
+ // Zeroed modifier flag should not synthesize events.
+ NSEvent* mouseEvent = flutter::testing::CreateMouseEvent(0x00);
+ [viewController mouseMoved:mouseEvent];
+ EXPECT_EQ([events count], 0u);
+
+ // For each modifier key, check that key events are synthesized.
+ for (NSNumber* keyCode in flutter::keyCodeToModifierFlag) {
+ FlutterKeyEvent* event;
+ NSNumber* logicalKey;
+ NSNumber* physicalKey;
+ NSNumber* flag = flutter::keyCodeToModifierFlag[keyCode];
+
+ // Should synthesize down event.
+ NSEvent* mouseEvent = flutter::testing::CreateMouseEvent([flag unsignedLongValue]);
+ [viewController mouseMoved:mouseEvent];
+ EXPECT_EQ([events count], 1u);
+ event = events[0].data;
+ logicalKey = [flutter::keyCodeToLogicalKey objectForKey:keyCode];
+ physicalKey = [flutter::keyCodeToPhysicalKey objectForKey:keyCode];
+ EXPECT_EQ(event->type, kFlutterKeyEventTypeDown);
+ EXPECT_EQ(event->logical, logicalKey.unsignedLongLongValue);
+ EXPECT_EQ(event->physical, physicalKey.unsignedLongLongValue);
+ EXPECT_EQ(event->synthesized, true);
+
+ // Should synthesize up event.
+ mouseEvent = flutter::testing::CreateMouseEvent(0x00);
+ [viewController mouseMoved:mouseEvent];
+ EXPECT_EQ([events count], 2u);
+ event = events[1].data;
+ logicalKey = [flutter::keyCodeToLogicalKey objectForKey:keyCode];
+ physicalKey = [flutter::keyCodeToPhysicalKey objectForKey:keyCode];
+ EXPECT_EQ(event->type, kFlutterKeyEventTypeUp);
+ EXPECT_EQ(event->logical, logicalKey.unsignedLongLongValue);
+ EXPECT_EQ(event->physical, physicalKey.unsignedLongLongValue);
+ EXPECT_EQ(event->synthesized, true);
+
+ [events removeAllObjects];
+ };
+
+ return true;
+}
+
@end