Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[macos] Synthesize modifier keys events on pointer events #37870

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -780,6 +780,19 @@ - (void)handleResponse:(BOOL)handled forId:(uint64_t)responseId {
[_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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -320,4 +320,12 @@ - (void)buildLayout {
}
}

- (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
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,8 @@ - (void)dispatchMouseEvent:(NSEvent*)event phase:(FlutterPointerPhase)phase {
flutterEvent.scroll_delta_y = scaledDeltaY;
}
}

[_keyboardManager syncModifiersIfNeeded:event.modifierFlags timestamp:event.timestamp];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to test FlutterViewController as a whole so that we can make sure mouse events trigger flag synchronization? Now you only test that the method of the responder works, which is, in fact, not that useful (since we might change this internal method in the future and we'll have to change the test.)

[_engine sendPointerEvent:flutterEvent];

// Update tracking of state as reported to Flutter.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -13,15 +14,35 @@
#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;
- (bool)testKeyEventsAreNotPropagatedIfHandled;
- (bool)testFlagsChangedEventsArePropagatedIfNotHandled;
- (bool)testKeyboardIsRestartedOnEngineRestart;
- (bool)testTrackpadGesturesAreSentToFramework;
- (bool)testModifierKeysAreSynthesizedOnMouseMove;
- (bool)testViewWillAppearCalledMultipleTimes;
- (bool)testFlutterViewIsConfigured;

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

using namespace ::flutter::testing::keycodes;

namespace flutter::testing {

namespace {
Expand Down Expand Up @@ -69,6 +92,19 @@ id MockGestureEvent(NSEventType type, NSEventPhase phase, double magnification,
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) {
Expand Down Expand Up @@ -161,6 +197,10 @@ id MockGestureEvent(NSEventType type, NSEventPhase phase, double magnification,
ASSERT_TRUE([[FlutterViewControllerTestObjC alloc] testTrackpadGesturesAreSentToFramework]);
}

TEST(FlutterViewControllerTest, TestModifierKeysAreSynthesizedOnMouseMove) {
ASSERT_TRUE([[FlutterViewControllerTestObjC alloc] testModifierKeysAreSynthesizedOnMouseMove]);
}

TEST(FlutterViewControllerTest, testViewWillAppearCalledMultipleTimes) {
ASSERT_TRUE([[FlutterViewControllerTestObjC alloc] testViewWillAppearCalledMultipleTimes]);
}
Expand Down Expand Up @@ -763,4 +803,71 @@ - (bool)testViewWillAppearCalledMultipleTimes {
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