[macos] Synthesize modifier keys events on pointer events (flutter/engine#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>
This commit is contained in:
Bruno Leroux
2023-01-20 11:43:16 +01:00
committed by GitHub
parent ed344b85d5
commit 258cde6cd2
6 changed files with 148 additions and 0 deletions

View File

@@ -30,4 +30,13 @@ typedef void (^FlutterSendEmbedderKeyEvent)(const FlutterKeyEvent& /* event */,
*/
- (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

View File

@@ -780,6 +780,19 @@ struct FlutterKeyPendingResponse {
[_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 {

View File

@@ -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

View File

@@ -320,4 +320,12 @@ 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];
}
@end

View File

@@ -661,6 +661,8 @@ static void CommonInit(FlutterViewController* controller) {
flutterEvent.scroll_delta_y = scaledDeltaY;
}
}
[_keyboardManager syncModifiersIfNeeded:event.modifierFlags timestamp:event.timestamp];
[_engine sendPointerEvent:flutterEvent];
// Update tracking of state as reported to Flutter.

View File

@@ -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 @@ NSResponder* mockResponder() {
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 @@ TEST(FlutterViewControllerTest, TestTrackpadGesturesAreSentToFramework) {
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 @@ TEST(FlutterViewControllerTest, testFlutterViewIsConfigured) {
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