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

Commit ccccee5

Browse files
authored
[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>
1 parent 66e177a commit ccccee5

File tree

6 files changed

+148
-0
lines changed

6 files changed

+148
-0
lines changed

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,13 @@ typedef void (^FlutterSendEmbedderKeyEvent)(const FlutterKeyEvent& /* event */,
3030
*/
3131
- (nonnull instancetype)initWithSendEvent:(_Nonnull FlutterSendEmbedderKeyEvent)sendEvent;
3232

33+
/**
34+
* Synthesize modifier keys events.
35+
*
36+
* If needed, synthesize modifier keys up and down events by comparing their
37+
* current pressing states with the given modifier flags.
38+
*/
39+
- (void)syncModifiersIfNeeded:(NSEventModifierFlags)modifierFlags
40+
timestamp:(NSTimeInterval)timestamp;
41+
3342
@end

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -780,6 +780,19 @@ - (void)handleResponse:(BOOL)handled forId:(uint64_t)responseId {
780780
[_pendingResponses removeObjectForKey:@(responseId)];
781781
}
782782

783+
- (void)syncModifiersIfNeeded:(NSEventModifierFlags)modifierFlags
784+
timestamp:(NSTimeInterval)timestamp {
785+
FlutterAsyncKeyCallback replyCallback = ^(BOOL handled) {
786+
// Do nothing.
787+
};
788+
FlutterKeyCallbackGuard* guardedCallback =
789+
[[FlutterKeyCallbackGuard alloc] initWithCallback:replyCallback];
790+
[self synchronizeModifiers:modifierFlags
791+
ignoringFlags:0
792+
timestamp:timestamp
793+
guard:guardedCallback];
794+
}
795+
783796
@end
784797

785798
namespace {

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,13 @@
4848
*/
4949
- (BOOL)isDispatchingKeyEvent:(nonnull NSEvent*)event;
5050

51+
/**
52+
* Synthesize modifier keys events.
53+
*
54+
* If needed, synthesize modifier keys up and down events by comparing their
55+
* current pressing states with the given modifier flags.
56+
*/
57+
- (void)syncModifiersIfNeeded:(NSEventModifierFlags)modifierFlags
58+
timestamp:(NSTimeInterval)timestamp;
59+
5160
@end

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,4 +320,12 @@ - (void)buildLayout {
320320
}
321321
}
322322

323+
- (void)syncModifiersIfNeeded:(NSEventModifierFlags)modifierFlags
324+
timestamp:(NSTimeInterval)timestamp {
325+
// The embedder responder is the first element in _primaryResponders.
326+
FlutterEmbedderKeyResponder* embedderResponder =
327+
(FlutterEmbedderKeyResponder*)_primaryResponders[0];
328+
[embedderResponder syncModifiersIfNeeded:modifierFlags timestamp:timestamp];
329+
}
330+
323331
@end

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,8 @@ - (void)dispatchMouseEvent:(NSEvent*)event phase:(FlutterPointerPhase)phase {
661661
flutterEvent.scroll_delta_y = scaledDeltaY;
662662
}
663663
}
664+
665+
[_keyboardManager syncModifiersIfNeeded:event.modifierFlags timestamp:event.timestamp];
664666
[_engine sendPointerEvent:flutterEvent];
665667

666668
// Update tracking of state as reported to Flutter.

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

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
#import "KeyCodeMap_Internal.h"
56
#import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterViewController.h"
67
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h"
78

@@ -13,15 +14,35 @@
1314
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h"
1415
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterRenderer.h"
1516
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTestUtils.h"
17+
#include "flutter/shell/platform/embedder/test_utils/key_codes.g.h"
1618
#import "flutter/testing/testing.h"
1719

20+
// A wrap to convert FlutterKeyEvent to a ObjC class.
21+
@interface KeyEventWrapper : NSObject
22+
@property(nonatomic) FlutterKeyEvent* data;
23+
- (nonnull instancetype)initWithEvent:(const FlutterKeyEvent*)event;
24+
@end
25+
26+
@implementation KeyEventWrapper
27+
- (instancetype)initWithEvent:(const FlutterKeyEvent*)event {
28+
self = [super init];
29+
_data = new FlutterKeyEvent(*event);
30+
return self;
31+
}
32+
33+
- (void)dealloc {
34+
delete _data;
35+
}
36+
@end
37+
1838
@interface FlutterViewControllerTestObjC : NSObject
1939
- (bool)testKeyEventsAreSentToFramework;
2040
- (bool)testKeyEventsArePropagatedIfNotHandled;
2141
- (bool)testKeyEventsAreNotPropagatedIfHandled;
2242
- (bool)testFlagsChangedEventsArePropagatedIfNotHandled;
2343
- (bool)testKeyboardIsRestartedOnEngineRestart;
2444
- (bool)testTrackpadGesturesAreSentToFramework;
45+
- (bool)testModifierKeysAreSynthesizedOnMouseMove;
2546
- (bool)testViewWillAppearCalledMultipleTimes;
2647
- (bool)testFlutterViewIsConfigured;
2748

@@ -30,6 +51,8 @@ + (void)respondFalseForSendEvent:(const FlutterKeyEvent&)event
3051
userData:(nullable void*)userData;
3152
@end
3253

54+
using namespace ::flutter::testing::keycodes;
55+
3356
namespace flutter::testing {
3457

3558
namespace {
@@ -69,6 +92,19 @@ id MockGestureEvent(NSEventType type, NSEventPhase phase, double magnification,
6992
OCMStub([mock flagsChanged:[OCMArg any]]).andDo(nil);
7093
return mock;
7194
}
95+
96+
NSEvent* CreateMouseEvent(NSEventModifierFlags modifierFlags) {
97+
return [NSEvent mouseEventWithType:NSEventTypeMouseMoved
98+
location:NSZeroPoint
99+
modifierFlags:modifierFlags
100+
timestamp:0
101+
windowNumber:0
102+
context:nil
103+
eventNumber:0
104+
clickCount:1
105+
pressure:1.0];
106+
}
107+
72108
} // namespace
73109

74110
TEST(FlutterViewController, HasViewThatHidesOtherViewsInAccessibility) {
@@ -161,6 +197,10 @@ id MockGestureEvent(NSEventType type, NSEventPhase phase, double magnification,
161197
ASSERT_TRUE([[FlutterViewControllerTestObjC alloc] testTrackpadGesturesAreSentToFramework]);
162198
}
163199

200+
TEST(FlutterViewControllerTest, TestModifierKeysAreSynthesizedOnMouseMove) {
201+
ASSERT_TRUE([[FlutterViewControllerTestObjC alloc] testModifierKeysAreSynthesizedOnMouseMove]);
202+
}
203+
164204
TEST(FlutterViewControllerTest, testViewWillAppearCalledMultipleTimes) {
165205
ASSERT_TRUE([[FlutterViewControllerTestObjC alloc] testViewWillAppearCalledMultipleTimes]);
166206
}
@@ -763,4 +803,71 @@ - (bool)testViewWillAppearCalledMultipleTimes {
763803
return true;
764804
}
765805

806+
- (bool)testModifierKeysAreSynthesizedOnMouseMove {
807+
id engineMock = OCMClassMock([FlutterEngine class]);
808+
// Need to return a real renderer to allow view controller to load.
809+
FlutterRenderer* renderer_ = [[FlutterRenderer alloc] initWithFlutterEngine:engineMock];
810+
OCMStub([engineMock renderer]).andReturn(renderer_);
811+
812+
// Capture calls to sendKeyEvent
813+
__block NSMutableArray<KeyEventWrapper*>* events =
814+
[[NSMutableArray<KeyEventWrapper*> alloc] init];
815+
OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:FlutterKeyEvent {}
816+
callback:nil
817+
userData:nil])
818+
.andDo((^(NSInvocation* invocation) {
819+
FlutterKeyEvent* event;
820+
[invocation getArgument:&event atIndex:2];
821+
[events addObject:[[KeyEventWrapper alloc] initWithEvent:event]];
822+
}));
823+
824+
FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
825+
nibName:@""
826+
bundle:nil];
827+
[viewController loadView];
828+
[engineMock setViewController:viewController];
829+
[viewController viewWillAppear];
830+
831+
// Zeroed modifier flag should not synthesize events.
832+
NSEvent* mouseEvent = flutter::testing::CreateMouseEvent(0x00);
833+
[viewController mouseMoved:mouseEvent];
834+
EXPECT_EQ([events count], 0u);
835+
836+
// For each modifier key, check that key events are synthesized.
837+
for (NSNumber* keyCode in flutter::keyCodeToModifierFlag) {
838+
FlutterKeyEvent* event;
839+
NSNumber* logicalKey;
840+
NSNumber* physicalKey;
841+
NSNumber* flag = flutter::keyCodeToModifierFlag[keyCode];
842+
843+
// Should synthesize down event.
844+
NSEvent* mouseEvent = flutter::testing::CreateMouseEvent([flag unsignedLongValue]);
845+
[viewController mouseMoved:mouseEvent];
846+
EXPECT_EQ([events count], 1u);
847+
event = events[0].data;
848+
logicalKey = [flutter::keyCodeToLogicalKey objectForKey:keyCode];
849+
physicalKey = [flutter::keyCodeToPhysicalKey objectForKey:keyCode];
850+
EXPECT_EQ(event->type, kFlutterKeyEventTypeDown);
851+
EXPECT_EQ(event->logical, logicalKey.unsignedLongLongValue);
852+
EXPECT_EQ(event->physical, physicalKey.unsignedLongLongValue);
853+
EXPECT_EQ(event->synthesized, true);
854+
855+
// Should synthesize up event.
856+
mouseEvent = flutter::testing::CreateMouseEvent(0x00);
857+
[viewController mouseMoved:mouseEvent];
858+
EXPECT_EQ([events count], 2u);
859+
event = events[1].data;
860+
logicalKey = [flutter::keyCodeToLogicalKey objectForKey:keyCode];
861+
physicalKey = [flutter::keyCodeToPhysicalKey objectForKey:keyCode];
862+
EXPECT_EQ(event->type, kFlutterKeyEventTypeUp);
863+
EXPECT_EQ(event->logical, logicalKey.unsignedLongLongValue);
864+
EXPECT_EQ(event->physical, physicalKey.unsignedLongLongValue);
865+
EXPECT_EQ(event->synthesized, true);
866+
867+
[events removeAllObjects];
868+
};
869+
870+
return true;
871+
}
872+
766873
@end

0 commit comments

Comments
 (0)