[macOS] Return keyboard pressed state (flutter/engine#42878)

## Description

This PR updates the macOS engine in order to answer to keyboard pressed state queries from the framework (as implemented in https://github.com/flutter/flutter/pull/122885).

## Related Issue

macOS engine implementation for https://github.com/flutter/flutter/issues/87391
Similar to:
- Linux: https://github.com/flutter/engine/pull/42346
- Android: https://github.com/flutter/engine/pull/42758

## Tests

Adds 2 tests.
This commit is contained in:
Bruno Leroux 2023-07-26 09:14:19 +02:00 committed by GitHub
parent dee698f80c
commit ae000cbf8c
7 changed files with 144 additions and 6 deletions

View File

@ -39,4 +39,12 @@ typedef void (^FlutterSendEmbedderKeyEvent)(const FlutterKeyEvent& /* event */,
- (void)syncModifiersIfNeeded:(NSEventModifierFlags)modifierFlags
timestamp:(NSTimeInterval)timestamp;
/**
* Returns the keyboard pressed state.
*
* Returns the keyboard pressed state. The dictionary contains one entry per
* pressed keys, mapping from the logical key to the physical key.
*/
- (nonnull NSDictionary*)getPressedState;
@end

View File

@ -793,6 +793,9 @@ struct FlutterKeyPendingResponse {
guard:guardedCallback];
}
- (nonnull NSDictionary*)getPressedState {
return [NSDictionary dictionaryWithDictionary:_pressingRecords];
}
@end
namespace {

View File

@ -57,4 +57,12 @@
- (void)syncModifiersIfNeeded:(NSEventModifierFlags)modifierFlags
timestamp:(NSTimeInterval)timestamp;
/**
* Returns the keyboard pressed state.
*
* Returns the keyboard pressed state. The dictionary contains one entry per
* pressed keys, mapping from the logical key to the physical key.
*/
- (nonnull NSDictionary*)getPressedState;
@end

View File

@ -119,6 +119,13 @@ typedef _Nullable _NSResponderPtr (^NextResponderProvider)();
_processingEvent = FALSE;
_viewDelegate = viewDelegate;
FlutterMethodChannel* keyboardChannel =
[FlutterMethodChannel methodChannelWithName:@"flutter/keyboard"
binaryMessenger:[_viewDelegate getBinaryMessenger]
codec:[FlutterStandardMethodCodec sharedInstance]];
[keyboardChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
[self handleKeyboardMethodCall:call result:result];
}];
_primaryResponders = [[NSMutableArray alloc] init];
[self addPrimaryResponder:[[FlutterEmbedderKeyResponder alloc]
initWithSendEvent:^(const FlutterKeyEvent& event,
@ -151,6 +158,14 @@ typedef _Nullable _NSResponderPtr (^NextResponderProvider)();
return self;
}
- (void)handleKeyboardMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
if ([[call method] isEqualToString:@"getKeyboardState"]) {
result([self getPressedState]);
} else {
result(FlutterMethodNotImplemented);
}
}
- (void)addPrimaryResponder:(nonnull id<FlutterKeyPrimaryResponder>)responder {
[_primaryResponders addObject:responder];
}
@ -328,4 +343,17 @@ typedef _Nullable _NSResponderPtr (^NextResponderProvider)();
[embedderResponder syncModifiersIfNeeded:modifierFlags timestamp:timestamp];
}
/**
* Returns the keyboard pressed state.
*
* Returns the keyboard pressed state. The dictionary contains one entry per
* pressed keys, mapping from the logical key to the physical key.
*/
- (nonnull NSDictionary*)getPressedState {
// The embedder responder is the first element in _primaryResponders.
FlutterEmbedderKeyResponder* embedderResponder =
(FlutterEmbedderKeyResponder*)_primaryResponders[0];
return [embedderResponder getPressedState];
}
@end

View File

@ -22,6 +22,7 @@ using flutter::testing::keycodes::kLogicalKeyA;
using flutter::testing::keycodes::kLogicalKeyM;
using flutter::testing::keycodes::kLogicalKeyQ;
using flutter::testing::keycodes::kLogicalKeyT;
using flutter::testing::keycodes::kPhysicalKeyA;
using flutter::LayoutClue;
@ -211,6 +212,10 @@ void clearEvents(std::vector<FlutterKeyEvent>& events) {
- (void)recordCallTypesTo:(nonnull NSMutableArray<NSNumber*>*)typeStorage
forTypes:(uint32_t)typeMask;
- (id)lastKeyboardChannelResult;
- (void)sendKeyboardChannelMessage:(NSData* _Nullable)message;
@property(readonly, nonatomic, strong) FlutterKeyboardManager* manager;
@property(nonatomic, nullable, strong) NSResponder* nextResponder;
@ -237,6 +242,10 @@ void clearEvents(std::vector<FlutterKeyEvent>& events) {
flutter::KeyboardLayoutNotifier _keyboardLayoutNotifier;
const MockLayoutData* _currentLayout;
id _keyboardChannelResult;
NSObject<FlutterBinaryMessenger>* _messengerMock;
FlutterBinaryMessageHandler _keyboardHandler;
}
- (nonnull instancetype)init {
@ -252,17 +261,21 @@ void clearEvents(std::vector<FlutterKeyEvent>& events) {
_currentLayout = &kUsLayout;
id messengerMock = OCMStrictProtocolMock(@protocol(FlutterBinaryMessenger));
OCMStub([messengerMock sendOnChannel:@"flutter/keyevent"
message:[OCMArg any]
binaryReply:[OCMArg any]])
_messengerMock = OCMStrictProtocolMock(@protocol(FlutterBinaryMessenger));
OCMStub([_messengerMock sendOnChannel:@"flutter/keyevent"
message:[OCMArg any]
binaryReply:[OCMArg any]])
.andCall(self, @selector(handleChannelMessage:message:binaryReply:));
OCMStub([_messengerMock setMessageHandlerOnChannel:@"flutter/keyboard"
binaryMessageHandler:[OCMArg any]])
.andCall(self, @selector(setKeyboardChannelHandler:handler:));
OCMStub([_messengerMock sendOnChannel:@"flutter/keyboard" message:[OCMArg any]])
.andCall(self, @selector(handleKeyboardChannelMessage:message:));
id viewDelegateMock = OCMStrictProtocolMock(@protocol(FlutterKeyboardViewDelegate));
OCMStub([viewDelegateMock nextResponder]).andReturn(_nextResponder);
OCMStub([viewDelegateMock onTextInputKeyEvent:[OCMArg any]])
.andCall(self, @selector(handleTextInputKeyEvent:));
OCMStub([viewDelegateMock getBinaryMessenger]).andReturn(messengerMock);
OCMStub([viewDelegateMock getBinaryMessenger]).andReturn(_messengerMock);
OCMStub([viewDelegateMock sendKeyEvent:FlutterKeyEvent {} callback:nil userData:nil])
.ignoringNonObjectArgs()
.andCall(self, @selector(handleEmbedderEvent:callback:userData:));
@ -276,6 +289,10 @@ void clearEvents(std::vector<FlutterKeyEvent>& events) {
return self;
}
- (id)lastKeyboardChannelResult {
return _keyboardChannelResult;
}
- (void)respondEmbedderCallsWith:(BOOL)response {
_embedderHandler = ^(const FlutterKeyEvent* event, FlutterAsyncKeyCallback callback) {
callback(response);
@ -327,6 +344,10 @@ void clearEvents(std::vector<FlutterKeyEvent>& events) {
_typeStorageMask = typeMask;
}
- (void)sendKeyboardChannelMessage:(NSData* _Nullable)message {
[_messengerMock sendOnChannel:@"flutter/keyboard" message:message];
}
- (void)setLayout:(const MockLayoutData&)layout {
_currentLayout = &layout;
if (_keyboardLayoutNotifier != nil) {
@ -364,6 +385,12 @@ void clearEvents(std::vector<FlutterKeyEvent>& events) {
});
}
- (void)handleKeyboardChannelMessage:(NSString*)channel message:(NSData* _Nullable)message {
_keyboardHandler(message, ^(id result) {
_keyboardChannelResult = result;
});
}
- (BOOL)handleTextInputKeyEvent:(NSEvent*)event {
if (_typeStorage != nil && (_typeStorageMask & kTextCall) != 0) {
[_typeStorage addObject:@(kTextCall)];
@ -382,6 +409,10 @@ void clearEvents(std::vector<FlutterKeyEvent>& events) {
return LayoutClue{cluePair & kCharMask, (cluePair & kDeadKeyMask) != 0};
}
- (void)setKeyboardChannelHandler:(NSString*)channel handler:(FlutterBinaryMessageHandler)handler {
_keyboardHandler = handler;
}
@end
@interface FlutterKeyboardManagerUnittestsObjC : NSObject
@ -389,6 +420,8 @@ void clearEvents(std::vector<FlutterKeyEvent>& events) {
- (bool)doublePrimaryResponder;
- (bool)textInputPlugin;
- (bool)emptyNextResponder;
- (bool)getPressedState;
- (bool)keyboardChannelGetPressedState;
- (bool)racingConditionBetweenKeyAndText;
- (bool)correctLogicalKeyForLayouts;
@end
@ -410,6 +443,14 @@ TEST(FlutterKeyboardManagerUnittests, EmptyNextResponder) {
ASSERT_TRUE([[FlutterKeyboardManagerUnittestsObjC alloc] emptyNextResponder]);
}
TEST(FlutterKeyboardManagerUnittests, GetPressedState) {
ASSERT_TRUE([[FlutterKeyboardManagerUnittestsObjC alloc] getPressedState]);
}
TEST(FlutterKeyboardManagerUnittests, KeyboardChannelGetPressedState) {
ASSERT_TRUE([[FlutterKeyboardManagerUnittestsObjC alloc] keyboardChannelGetPressedState]);
}
TEST(FlutterKeyboardManagerUnittests, RacingConditionBetweenKeyAndText) {
ASSERT_TRUE([[FlutterKeyboardManagerUnittestsObjC alloc] racingConditionBetweenKeyAndText]);
}
@ -563,6 +604,44 @@ TEST(FlutterKeyboardManagerUnittests, CorrectLogicalKeyForLayouts) {
return true;
}
- (bool)getPressedState {
KeyboardTester* tester = [[KeyboardTester alloc] init];
[tester respondEmbedderCallsWith:false];
[tester respondChannelCallsWith:false];
[tester respondTextInputWith:false];
[tester.manager handleEvent:keyDownEvent(kVK_ANSI_A)];
NSDictionary* pressingRecords = [tester.manager getPressedState];
EXPECT_EQ([pressingRecords count], 1u);
EXPECT_EQ(pressingRecords[@(kPhysicalKeyA)], @(kLogicalKeyA));
return true;
}
- (bool)keyboardChannelGetPressedState {
KeyboardTester* tester = [[KeyboardTester alloc] init];
[tester respondEmbedderCallsWith:false];
[tester respondChannelCallsWith:false];
[tester respondTextInputWith:false];
[tester.manager handleEvent:keyDownEvent(kVK_ANSI_A)];
FlutterMethodCall* getKeyboardStateMethodCall =
[FlutterMethodCall methodCallWithMethodName:@"getKeyboardState" arguments:nil];
NSData* getKeyboardStateMessage =
[[FlutterStandardMethodCodec sharedInstance] encodeMethodCall:getKeyboardStateMethodCall];
[tester sendKeyboardChannelMessage:getKeyboardStateMessage];
id encodedResult = [tester lastKeyboardChannelResult];
id decoded = [[FlutterStandardMethodCodec sharedInstance] decodeEnvelope:encodedResult];
EXPECT_EQ([decoded count], 1u);
EXPECT_EQ(decoded[@(kPhysicalKeyA)], @(kLogicalKeyA));
return true;
}
// Regression test for https://github.com/flutter/flutter/issues/82673.
- (bool)racingConditionBetweenKeyAndText {
KeyboardTester* tester = [[KeyboardTester alloc] init];

View File

@ -86,4 +86,12 @@ typedef struct {
*/
- (flutter::LayoutClue)lookUpLayoutForKeyCode:(uint16_t)keyCode shift:(BOOL)shift;
/**
* Returns the keyboard pressed state.
*
* Returns the keyboard pressed state. The dictionary contains one entry per
* pressed keys, mapping from the logical key to the physical key.
*/
- (nonnull NSDictionary*)getPressedState;
@end

View File

@ -955,6 +955,10 @@ static void CommonInit(FlutterViewController* controller, FlutterEngine* engine)
return LayoutClue{0, false};
}
- (nonnull NSDictionary*)getPressedState {
return [_keyboardManager getPressedState];
}
#pragma mark - NSResponder
- (BOOL)acceptsFirstResponder {