diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 2ced460cb08bf..8c8658b626594 100755 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -1105,9 +1105,15 @@ FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterBacki FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterBackingStore.mm FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterBackingStoreData.h FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterBackingStoreData.mm +FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterChannelKeyResponder.h +FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterChannelKeyResponder.mm +FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterChannelKeyResponderUnittests.mm FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterDartProject.mm FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterDartProject_Internal.h FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterEmbedderExternalTextureUnittests.mm +FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponder.h +FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponder.mm +FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponderUnittests.mm FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h @@ -1122,8 +1128,11 @@ FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterGLCom FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterGLCompositorUnittests.mm FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterIOSurfaceHolder.h FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterIOSurfaceHolder.mm -FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterIntermediateKeyResponder.h -FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterIntermediateKeyResponder.mm +FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterKeyPrimaryResponder.h +FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterKeySecondaryResponder.h +FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.h +FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.mm +FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManagerUnittests.mm FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterMacOSExternalTexture.h FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterMetalRenderer.h FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterMetalRenderer.mm @@ -1158,6 +1167,8 @@ FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterViewC FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTestUtils.h FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTestUtils.mm FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h +FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/KeyCodeMap.mm +FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/KeyCodeMap_internal.h FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/MacOSGLContextSwitch.h FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/MacOSGLContextSwitch.mm FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/fixtures/flutter_desktop_test.dart diff --git a/shell/platform/darwin/macos/BUILD.gn b/shell/platform/darwin/macos/BUILD.gn index e8e2223ce0286..dfb5d736faec9 100644 --- a/shell/platform/darwin/macos/BUILD.gn +++ b/shell/platform/darwin/macos/BUILD.gn @@ -59,8 +59,12 @@ source_set("flutter_framework_source") { "framework/Source/FlutterBackingStore.mm", "framework/Source/FlutterBackingStoreData.h", "framework/Source/FlutterBackingStoreData.mm", + "framework/Source/FlutterChannelKeyResponder.h", + "framework/Source/FlutterChannelKeyResponder.mm", "framework/Source/FlutterDartProject.mm", "framework/Source/FlutterDartProject_Internal.h", + "framework/Source/FlutterEmbedderKeyResponder.h", + "framework/Source/FlutterEmbedderKeyResponder.mm", "framework/Source/FlutterEngine.mm", "framework/Source/FlutterEngine_Internal.h", "framework/Source/FlutterExternalTextureGL.h", @@ -73,8 +77,10 @@ source_set("flutter_framework_source") { "framework/Source/FlutterGLCompositor.mm", "framework/Source/FlutterIOSurfaceHolder.h", "framework/Source/FlutterIOSurfaceHolder.mm", - "framework/Source/FlutterIntermediateKeyResponder.h", - "framework/Source/FlutterIntermediateKeyResponder.mm", + "framework/Source/FlutterKeyPrimaryResponder.h", + "framework/Source/FlutterKeySecondaryResponder.h", + "framework/Source/FlutterKeyboardManager.h", + "framework/Source/FlutterKeyboardManager.mm", "framework/Source/FlutterMacOSExternalTexture.h", "framework/Source/FlutterMacOSExternalTexture.h", "framework/Source/FlutterMetalRenderer.h", @@ -102,6 +108,7 @@ source_set("flutter_framework_source") { "framework/Source/FlutterView.mm", "framework/Source/FlutterViewController.mm", "framework/Source/FlutterViewController_Internal.h", + "framework/Source/KeyCodeMap.mm", "framework/Source/MacOSGLContextSwitch.h", "framework/Source/MacOSGLContextSwitch.mm", ] @@ -158,9 +165,12 @@ executable("flutter_desktop_darwin_unittests") { testonly = true sources = [ + "framework/Source/FlutterChannelKeyResponderUnittests.mm", "framework/Source/FlutterEmbedderExternalTextureUnittests.mm", + "framework/Source/FlutterEmbedderKeyResponderUnittests.mm", "framework/Source/FlutterEngineTest.mm", "framework/Source/FlutterGLCompositorUnittests.mm", + "framework/Source/FlutterKeyboardManagerUnittests.mm", "framework/Source/FlutterMetalRendererTest.mm", "framework/Source/FlutterMetalSurfaceManagerTest.mm", "framework/Source/FlutterOpenGLRendererTest.mm", diff --git a/shell/platform/darwin/macos/framework/Source/FlutterChannelKeyResponder.h b/shell/platform/darwin/macos/framework/Source/FlutterChannelKeyResponder.h new file mode 100644 index 0000000000000..b40492a3550fb --- /dev/null +++ b/shell/platform/darwin/macos/framework/Source/FlutterChannelKeyResponder.h @@ -0,0 +1,24 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterKeyPrimaryResponder.h" + +#import + +#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterChannels.h" + +/** + * A primary responder of |FlutterKeyboardManager| that handles events by + * sending the raw information through the method channel. + * + * This class corresponds to the RawKeyboard API in the framework. + */ +@interface FlutterChannelKeyResponder : NSObject + +/** + * Create an instance by specifying the method channel to use. + */ +- (nonnull instancetype)initWithChannel:(nonnull FlutterBasicMessageChannel*)channel; + +@end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterChannelKeyResponder.mm b/shell/platform/darwin/macos/framework/Source/FlutterChannelKeyResponder.mm new file mode 100644 index 0000000000000..392986e0d01af --- /dev/null +++ b/shell/platform/darwin/macos/framework/Source/FlutterChannelKeyResponder.mm @@ -0,0 +1,86 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import + +#import "FlutterChannelKeyResponder.h" +#import "KeyCodeMap_internal.h" +#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterCodecs.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h" +#import "flutter/shell/platform/embedder/embedder.h" + +@interface FlutterChannelKeyResponder () + +/** + * The channel used to communicate with Flutter. + */ +@property(nonatomic) FlutterBasicMessageChannel* channel; + +/** + * The |NSEvent.modifierFlags| of the last event received. + * + * Used to determine whether a FlagsChanged event should count as a keydown or + * a keyup event. + */ +@property(nonatomic) uint64_t previouslyPressedFlags; + +@end + +@implementation FlutterChannelKeyResponder + +- (nonnull instancetype)initWithChannel:(nonnull FlutterBasicMessageChannel*)channel { + self = [super init]; + if (self != nil) { + _channel = channel; + _previouslyPressedFlags = 0; + } + return self; +} + +- (void)handleEvent:(NSEvent*)event callback:(FlutterAsyncKeyCallback)callback { + NSString* type; + switch (event.type) { + case NSEventTypeKeyDown: + type = @"keydown"; + break; + case NSEventTypeKeyUp: + type = @"keyup"; + break; + case NSEventTypeFlagsChanged: + if (event.modifierFlags < _previouslyPressedFlags) { + type = @"keyup"; + } else { + type = @"keydown"; + } + break; + default: + NSAssert(false, @"Unexpected key event type (got %lu).", event.type); + } + _previouslyPressedFlags = event.modifierFlags; + NSMutableDictionary* keyMessage = [@{ + @"keymap" : @"macos", + @"type" : type, + @"keyCode" : @(event.keyCode), + @"modifiers" : @(event.modifierFlags), + } mutableCopy]; + // Calling these methods on any other type of event + // (e.g NSEventTypeFlagsChanged) will raise an exception. + if (event.type == NSEventTypeKeyDown || event.type == NSEventTypeKeyUp) { + keyMessage[@"characters"] = event.characters; + keyMessage[@"charactersIgnoringModifiers"] = event.charactersIgnoringModifiers; + } + [_channel sendMessage:keyMessage + reply:^(id reply) { + if (!reply) { + return callback(true); + } + // Only propagate the event to other responders if the framework didn't handle + // it. + callback([[reply valueForKey:@"handled"] boolValue]); + }]; +} + +#pragma mark - Private + +@end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterChannelKeyResponderUnittests.mm b/shell/platform/darwin/macos/framework/Source/FlutterChannelKeyResponderUnittests.mm new file mode 100644 index 0000000000000..0ee7445efecc3 --- /dev/null +++ b/shell/platform/darwin/macos/framework/Source/FlutterChannelKeyResponderUnittests.mm @@ -0,0 +1,211 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import + +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterChannelKeyResponder.h" +#import "flutter/testing/testing.h" +#include "third_party/googletest/googletest/include/gtest/gtest.h" + +namespace flutter::testing { + +namespace { +NSEvent* keyEvent(NSEventType type, + NSEventModifierFlags modifierFlags, + NSString* characters, + NSString* charactersIgnoringModifiers, + BOOL isARepeat, + unsigned short keyCode) { + return [NSEvent keyEventWithType:type + location:NSZeroPoint + modifierFlags:modifierFlags + timestamp:0 + windowNumber:0 + context:nil + characters:characters + charactersIgnoringModifiers:charactersIgnoringModifiers + isARepeat:isARepeat + keyCode:keyCode]; +} +} // namespace + +TEST(FlutterChannelKeyResponderUnittests, BasicKeyEvent) { + __block NSMutableArray* messages = [[NSMutableArray alloc] init]; + __block BOOL next_response = TRUE; + __block NSMutableArray* responses = [[NSMutableArray alloc] init]; + + id mockKeyEventChannel = OCMStrictClassMock([FlutterBasicMessageChannel class]); + OCMStub([mockKeyEventChannel sendMessage:[OCMArg any] reply:[OCMArg any]]) + .andDo((^(NSInvocation* invocation) { + [invocation retainArguments]; + NSDictionary* message; + [invocation getArgument:&message atIndex:2]; + [messages addObject:message]; + + FlutterReply callback; + [invocation getArgument:&callback atIndex:3]; + NSDictionary* keyMessage = @{ + @"handled" : @(next_response), + }; + callback(keyMessage); + })); + + // Key down + FlutterChannelKeyResponder* responder = + [[FlutterChannelKeyResponder alloc] initWithChannel:mockKeyEventChannel]; + [responder handleEvent:keyEvent(NSEventTypeKeyDown, 0x100, @"a", @"a", FALSE, 0) + callback:^(BOOL handled) { + [responses addObject:@(handled)]; + }]; + + EXPECT_EQ([messages count], 1u); + EXPECT_STREQ([[messages lastObject][@"keymap"] UTF8String], "macos"); + EXPECT_STREQ([[messages lastObject][@"type"] UTF8String], "keydown"); + EXPECT_EQ([[messages lastObject][@"keyCode"] intValue], 0); + EXPECT_EQ([[messages lastObject][@"modifiers"] intValue], 0x100); + EXPECT_EQ([[messages lastObject][@"characters"] UTF8String], "a"); + EXPECT_EQ([[messages lastObject][@"charactersIgnoringModifiers"] UTF8String], "a"); + + EXPECT_EQ([responses count], 1u); + EXPECT_EQ([[responses lastObject] boolValue], TRUE); + + [messages removeAllObjects]; + [responses removeAllObjects]; + + // Key up + next_response = FALSE; + [responder handleEvent:keyEvent(NSEventTypeKeyUp, 0x100, @"a", @"a", FALSE, 0) + callback:^(BOOL handled) { + [responses addObject:@(handled)]; + }]; + + EXPECT_EQ([messages count], 1u); + EXPECT_STREQ([[messages lastObject][@"keymap"] UTF8String], "macos"); + EXPECT_STREQ([[messages lastObject][@"type"] UTF8String], "keyup"); + EXPECT_EQ([[messages lastObject][@"keyCode"] intValue], 0); + EXPECT_EQ([[messages lastObject][@"modifiers"] intValue], 0x100); + EXPECT_EQ([[messages lastObject][@"characters"] UTF8String], "a"); + EXPECT_EQ([[messages lastObject][@"charactersIgnoringModifiers"] UTF8String], "a"); + + EXPECT_EQ([responses count], 1u); + EXPECT_EQ([[responses lastObject] boolValue], FALSE); + + [messages removeAllObjects]; + [responses removeAllObjects]; + + // LShift down + next_response = TRUE; + [responder handleEvent:keyEvent(NSEventTypeFlagsChanged, 0x20102, @"", @"", FALSE, 56) + callback:^(BOOL handled) { + [responses addObject:@(handled)]; + }]; + + EXPECT_EQ([messages count], 1u); + EXPECT_STREQ([[messages lastObject][@"keymap"] UTF8String], "macos"); + EXPECT_STREQ([[messages lastObject][@"type"] UTF8String], "keydown"); + EXPECT_EQ([[messages lastObject][@"keyCode"] intValue], 56); + EXPECT_EQ([[messages lastObject][@"modifiers"] intValue], 0x20102); + + EXPECT_EQ([responses count], 1u); + EXPECT_EQ([[responses lastObject] boolValue], TRUE); + + [messages removeAllObjects]; + [responses removeAllObjects]; + + // RShift down + next_response = false; + [responder handleEvent:keyEvent(NSEventTypeFlagsChanged, 0x20106, @"", @"", FALSE, 60) + callback:^(BOOL handled) { + [responses addObject:@(handled)]; + }]; + + EXPECT_EQ([messages count], 1u); + EXPECT_STREQ([[messages lastObject][@"keymap"] UTF8String], "macos"); + EXPECT_STREQ([[messages lastObject][@"type"] UTF8String], "keydown"); + EXPECT_EQ([[messages lastObject][@"keyCode"] intValue], 60); + EXPECT_EQ([[messages lastObject][@"modifiers"] intValue], 0x20106); + + EXPECT_EQ([responses count], 1u); + EXPECT_EQ([[responses lastObject] boolValue], FALSE); + + [messages removeAllObjects]; + [responses removeAllObjects]; + + // LShift up + next_response = false; + [responder handleEvent:keyEvent(NSEventTypeFlagsChanged, 0x20104, @"", @"", FALSE, 56) + callback:^(BOOL handled) { + [responses addObject:@(handled)]; + }]; + + EXPECT_EQ([messages count], 1u); + EXPECT_STREQ([[messages lastObject][@"keymap"] UTF8String], "macos"); + EXPECT_STREQ([[messages lastObject][@"type"] UTF8String], "keyup"); + EXPECT_EQ([[messages lastObject][@"keyCode"] intValue], 56); + EXPECT_EQ([[messages lastObject][@"modifiers"] intValue], 0x20104); + + EXPECT_EQ([responses count], 1u); + EXPECT_EQ([[responses lastObject] boolValue], FALSE); + + [messages removeAllObjects]; + [responses removeAllObjects]; + + // RShift up + next_response = false; + [responder handleEvent:keyEvent(NSEventTypeFlagsChanged, 0x100, @"", @"", FALSE, 60) + callback:^(BOOL handled) { + [responses addObject:@(handled)]; + }]; + + EXPECT_EQ([messages count], 1u); + EXPECT_STREQ([[messages lastObject][@"keymap"] UTF8String], "macos"); + EXPECT_STREQ([[messages lastObject][@"type"] UTF8String], "keyup"); + EXPECT_EQ([[messages lastObject][@"keyCode"] intValue], 60); + EXPECT_EQ([[messages lastObject][@"modifiers"] intValue], 0x100); + + EXPECT_EQ([responses count], 1u); + EXPECT_EQ([[responses lastObject] boolValue], FALSE); + + [messages removeAllObjects]; + [responses removeAllObjects]; +} + +TEST(FlutterChannelKeyResponderUnittests, EmptyResponseIsTakenAsHandled) { + __block NSMutableArray* messages = [[NSMutableArray alloc] init]; + __block NSMutableArray* responses = [[NSMutableArray alloc] init]; + + id mockKeyEventChannel = OCMStrictClassMock([FlutterBasicMessageChannel class]); + OCMStub([mockKeyEventChannel sendMessage:[OCMArg any] reply:[OCMArg any]]) + .andDo((^(NSInvocation* invocation) { + [invocation retainArguments]; + NSDictionary* message; + [invocation getArgument:&message atIndex:2]; + [messages addObject:message]; + + FlutterReply callback; + [invocation getArgument:&callback atIndex:3]; + callback(nullptr); + })); + + FlutterChannelKeyResponder* responder = + [[FlutterChannelKeyResponder alloc] initWithChannel:mockKeyEventChannel]; + [responder handleEvent:keyEvent(NSEventTypeKeyDown, 0x100, @"a", @"a", FALSE, 0) + callback:^(BOOL handled) { + [responses addObject:@(handled)]; + }]; + + EXPECT_EQ([messages count], 1u); + EXPECT_STREQ([[messages lastObject][@"keymap"] UTF8String], "macos"); + EXPECT_STREQ([[messages lastObject][@"type"] UTF8String], "keydown"); + EXPECT_EQ([[messages lastObject][@"keyCode"] intValue], 0); + EXPECT_EQ([[messages lastObject][@"modifiers"] intValue], 0x100); + EXPECT_EQ([[messages lastObject][@"characters"] UTF8String], "a"); + EXPECT_EQ([[messages lastObject][@"charactersIgnoringModifiers"] UTF8String], "a"); + + EXPECT_EQ([responses count], 1u); + EXPECT_EQ([[responses lastObject] boolValue], TRUE); +} + +} // namespace flutter::testing diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponder.h b/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponder.h new file mode 100644 index 0000000000000..5cd68954d0df9 --- /dev/null +++ b/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponder.h @@ -0,0 +1,33 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import + +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterKeyPrimaryResponder.h" +#include "flutter/shell/platform/embedder/embedder.h" + +namespace { +typedef void* _VoidPtr; +} + +typedef void (^FlutterSendEmbedderKeyEvent)(const FlutterKeyEvent& /* event */, + _Nullable FlutterKeyEventCallback /* callback */, + _Nullable _VoidPtr /* user_data */); + +/** + * A primary responder of |FlutterKeyboardManager| that handles events by + * sending the converted events through the embedder API. + * + * This class corresponds to the HardwareKeyboard API in the framework. + */ +@interface FlutterEmbedderKeyResponder : NSObject + +/** + * Create an instance by specifying the function to send converted events to. + * + * The |sendEvent| is typically |FlutterEngine|'s |sendKeyEvent|. + */ +- (nonnull instancetype)initWithSendEvent:(_Nonnull FlutterSendEmbedderKeyEvent)sendEvent; + +@end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponder.mm b/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponder.mm new file mode 100644 index 0000000000000..5ae7e6fc5e853 --- /dev/null +++ b/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponder.mm @@ -0,0 +1,721 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import + +#import "FlutterEmbedderKeyResponder.h" +#import "KeyCodeMap_internal.h" +#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterCodecs.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h" +#import "flutter/shell/platform/embedder/embedder.h" + +namespace { + +/** + * Isolate the least significant 1-bit. + * + * For example, + * + * * lowestSetBit(0x1010) returns 0x10. + * * lowestSetBit(0) returns 0. + */ +static NSUInteger lowestSetBit(NSUInteger bitmask) { + // This utilizes property of two's complement (negation), which propagates a + // carry bit from LSB to the lowest set bit. + return bitmask & -bitmask; +} + +/** + * Whether a string represents a control character. + */ +static bool IsControlCharacter(NSUInteger length, NSString* label) { + if (length > 1) { + return false; + } + unichar codeUnit = [label characterAtIndex:0]; + return (codeUnit <= 0x1f && codeUnit >= 0x00) || (codeUnit >= 0x7f && codeUnit <= 0x9f); +} + +/** + * Whether a string represents an unprintable key. + */ +static bool IsUnprintableKey(NSUInteger length, NSString* label) { + if (length > 1) { + return false; + } + unichar codeUnit = [label characterAtIndex:0]; + return codeUnit >= 0xF700 && codeUnit <= 0xF8FF; +} + +/** + * Returns a key code composed with a base key and a plane. + * + * Examples of unprintable keys are "NSUpArrowFunctionKey = 0xF700" or + * "NSHomeFunctionKey = 0xF729". + * + * See + * https://developer.apple.com/documentation/appkit/1535851-function-key_unicodes?language=objc + * for more information. + */ +static uint64_t KeyOfPlane(uint64_t baseKey, uint64_t plane) { + return plane | (baseKey & kValueMask); +} + +/** + * Returns the physical key for a key code. + */ +static uint64_t GetPhysicalKeyForKeyCode(unsigned short keyCode) { + NSNumber* physicalKey = [keyCodeToPhysicalKey objectForKey:@(keyCode)]; + if (physicalKey == nil) + return 0; + return physicalKey.unsignedLongLongValue; +} + +/** + * Returns the logical key for a modifier physical key. + */ +static uint64_t GetLogicalKeyForModifier(unsigned short keyCode, uint64_t hidCode) { + NSNumber* fromKeyCode = [keyCodeToLogicalKey objectForKey:@(keyCode)]; + if (fromKeyCode != nil) + return fromKeyCode.unsignedLongLongValue; + return KeyOfPlane(hidCode, kHidPlane); +} + +/** + * Converts upper letters to lower letters in ASCII, and returns as-is + * otherwise. + * + * Independent of locale. + */ +static uint64_t toLower(uint64_t ch) { + const uint64_t lowerA = 0x61; + const uint64_t upperA = 0x41; + const uint64_t upperZ = 0x5a; + if (ch >= upperA && ch <= upperZ) + return ch + (lowerA - upperA); + return ch; +} + +/** + * Returns the logical key of a KeyUp or KeyDown event. + * + * For FlagsChanged event, use GetLogicalKeyForModifier. + */ +static uint64_t GetLogicalKeyForEvent(NSEvent* event, uint64_t physicalKey) { + // Look to see if the keyCode can be mapped from keycode. + NSNumber* fromKeyCode = [keyCodeToLogicalKey objectForKey:@(event.keyCode)]; + if (fromKeyCode != nil) + return fromKeyCode.unsignedLongLongValue; + + NSString* keyLabel = event.charactersIgnoringModifiers; + NSUInteger keyLabelLength = [keyLabel length]; + // If this key is printable, generate the logical key from its Unicode + // value. Control keys such as ESC, CTRL, and SHIFT are not printable. HOME, + // DEL, arrow keys, and function keys are considered modifier function keys, + // which generate invalid Unicode scalar values. + if (keyLabelLength != 0 && !IsControlCharacter(keyLabelLength, keyLabel) && + !IsUnprintableKey(keyLabelLength, keyLabel)) { + // Given that charactersIgnoringModifiers can contain a string of arbitrary + // length, limit to a maximum of two Unicode scalar values. It is unlikely + // that a keyboard would produce a code point bigger than 32 bits, but it is + // still worth defending against this case. + NSCAssert((keyLabelLength < 2), @"Unexpected long key label: |%@|.", keyLabel); + + uint64_t codeUnit = (uint64_t)[keyLabel characterAtIndex:0]; + if (keyLabelLength == 2) { + uint64_t secondCode = (uint64_t)[keyLabel characterAtIndex:1]; + codeUnit = (codeUnit << 16) | secondCode; + } + return KeyOfPlane(toLower(codeUnit), kUnicodePlane); + } + + // Control keys like "backspace" and movement keys like arrow keys don't have + // a printable representation, but are present on the physical keyboard. Since + // there is no logical keycode map for macOS (macOS uses the keycode to + // reference physical keys), a LogicalKeyboardKey is created with the physical + // key's HID usage and debugName. This avoids duplicating the physical key + // map. + if (physicalKey != 0) { + return KeyOfPlane(physicalKey, kHidPlane); + } + + // This is a non-printable key that is unrecognized, so a new code is minted + // with the autogenerated bit set. + return KeyOfPlane(event.keyCode, kMacosPlane | kAutogeneratedMask); +} + +/** + * Converts NSEvent.timestamp to the timestamp for Flutter. + */ +static double GetFlutterTimestampFrom(NSTimeInterval timestamp) { + // Timestamp in microseconds. The event.timestamp is in seconds with sub-ms precision. + return timestamp * 1000000.0; +} + +/** + * Compute |modifierFlagOfInterestMask| out of |keyCodeToModifierFlag|. + * + * This is equal to the bitwise-or of all values of |keyCodeToModifierFlag| as + * well as NSEventModifierFlagCapsLock. + */ +static NSUInteger computeModifierFlagOfInterestMask() { + __block NSUInteger modifierFlagOfInterestMask = NSEventModifierFlagCapsLock; + [keyCodeToModifierFlag + enumerateKeysAndObjectsUsingBlock:^(NSNumber* keyCode, NSNumber* flag, BOOL* stop) { + modifierFlagOfInterestMask = modifierFlagOfInterestMask | [flag unsignedLongValue]; + }]; + return modifierFlagOfInterestMask; +} + +/** + * The C-function sent to the embedder's |SendKeyEvent|, wrapping + * |FlutterEmbedderKeyResponder.handleResponse|. + * + * For the reason of this wrap, see |FlutterKeyPendingResponse|. + */ +void HandleResponse(bool handled, void* user_data); + +/** + * Converts NSEvent.characters to a C-string for FlutterKeyEvent. + */ +const char* getEventString(NSString* characters) { + if ([characters length] == 0) { + return nullptr; + } + unichar utf16Code = [characters characterAtIndex:0]; + if (utf16Code >= 0xf700 && utf16Code <= 0xf7ff) { + // Some function keys are assigned characters with codepoints from the + // private use area. These characters are filtered out since they're + // unprintable. + // + // The official documentation reserves 0xF700-0xF8FF as private use area + // (https://developer.apple.com/documentation/appkit/1535851-function-key_unicodes?language=objc). + // But macOS seems to only use a reduced range of it. The official doc + // defines a few constants, all of which are within 0xF700-0xF747. + // (https://developer.apple.com/documentation/appkit/1535851-function-key_unicodes?language=objc). + // This mostly aligns with the experimentation result, except for 0xF8FF, + // which is used for the "Apple logo" character (Option-Shift-K on a US + // keyboard.) + // + // We hereby assume that non-printable function keys are defined from + // 0xF700 upwards, and printable private keys are defined from 0xF8FF + // downwards. We want to keep the printable private keys, therefore we only + // filter out 0xF700-0xF7FF. + return nullptr; + } + return [characters UTF8String]; +} +} // namespace + +/** + * The invocation context for |HandleResponse|, wrapping + * |FlutterEmbedderKeyResponder.handleResponse|. + * + * The embedder functions only accept C-functions as callbacks, as well as an + * arbitray user_data. In order to send an instance method of + * |FlutterEmbedderKeyResponder.handleResponse| to the engine's |SendKeyEvent|, + * we wrap the invocation into a C-function |HandleResponse| and invocation + * context |FlutterKeyPendingResponse|. + * + * When this object is sent to the engine's |SendKeyEvent| as |user_data|, it + * must be attached with |__bridge_retained|. When this object is parsed + * in |HandleResponse| from |user_data|, it will be attached with + * |__bridge_transfer|. + */ +@interface FlutterKeyPendingResponse : NSObject + +@property(nonatomic) FlutterEmbedderKeyResponder* responder; + +@property(nonatomic) uint64_t responseId; + +- (nonnull instancetype)initWithHandler:(nonnull FlutterEmbedderKeyResponder*)responder + responseId:(uint64_t)responseId; + +@end + +@implementation FlutterKeyPendingResponse +- (instancetype)initWithHandler:(FlutterEmbedderKeyResponder*)responder + responseId:(uint64_t)responseId { + self = [super init]; + if (self != nil) { + _responder = responder; + _responseId = responseId; + } + return self; +} +@end + +/** + * Guards a |FlutterAsyncKeyCallback| to make sure it's handled exactly once + * throughout |FlutterEmbedderKeyResponder.handleEvent|. + * + * A callback can either be handled with |pendTo:withId:|, or with |resolveTo:|. + * Either way, the callback can not be handled again, or an assertion will be + * thrown. + */ +@interface FlutterKeyCallbackGuard : NSObject +- (nonnull instancetype)initWithCallback:(FlutterAsyncKeyCallback)callback; + +/** + * Handle the callback by storing it to pending responses. + */ +- (void)pendTo:(nonnull NSMutableDictionary*)pendingResponses + withId:(uint64_t)responseId; + +/** + * Handle the callback by calling it with a result. + */ +- (void)resolveTo:(BOOL)handled; + +@property(nonatomic) BOOL handled; +/** + * A string indicating how the callback is handled. + * + * Only set in debug mode. Nil in release mode, or if the callback has not been + * handled. + */ +@property(nonatomic) NSString* debugHandleSource; +@end + +@implementation FlutterKeyCallbackGuard { + // The callback is declared in the implemnetation block to avoid being + // accessed directly. + FlutterAsyncKeyCallback _callback; +} +- (nonnull instancetype)initWithCallback:(FlutterAsyncKeyCallback)callback { + self = [super init]; + if (self != nil) { + _callback = callback; + _handled = FALSE; + } + return self; +} + +- (void)pendTo:(nonnull NSMutableDictionary*)pendingResponses + withId:(uint64_t)responseId { + NSAssert(!_handled, @"This callback has been handled by %@.", _debugHandleSource); + if (_handled) { + return; + } + pendingResponses[@(responseId)] = _callback; + _handled = TRUE; + NSAssert( + ((_debugHandleSource = [NSString stringWithFormat:@"pending event %llu", responseId]), TRUE), + @""); +} + +- (void)resolveTo:(BOOL)handled { + NSAssert(!_handled, @"This callback has been handled by %@.", _debugHandleSource); + if (_handled) { + return; + } + _callback(handled); + _handled = TRUE; + NSAssert(((_debugHandleSource = [NSString stringWithFormat:@"resolved with %d", _handled]), TRUE), + @""); +} +@end + +@interface FlutterEmbedderKeyResponder () + +/** + * The function to send converted events to. + * + * Set by the initializer. + */ +@property(nonatomic, copy) FlutterSendEmbedderKeyEvent sendEvent; + +/** + * A map of presessd keys. + * + * The keys of the dictionary are physical keys, while the values are the logical keys + * of the key down event. + */ +@property(nonatomic) NSMutableDictionary* pressingRecords; + +/** + * A constant mask for NSEvent.modifierFlags that Flutter synchronizes with. + * + * Flutter keeps track of the last |modifierFlags| and compares it with the + * incoming one. Any bit within |modifierFlagOfInterestMask| that is different + * (except for the one that corresponds to the event key) indicates that an + * event for this modifier was missed, and Flutter synthesizes an event to make + * up for the state difference. + * + * It is computed by computeModifierFlagOfInterestMask. + */ +@property(nonatomic) NSUInteger modifierFlagOfInterestMask; + +/** + * The modifier flags of the last received key event, excluding uninterested + * bits. + * + * This should be kept synchronized with the last |NSEvent.modifierFlags| + * after masking with |modifierFlagOfInterestMask|. This should also be kept + * synchronized with the corresponding keys of |pressingRecords|. + * + * This is used by |synchronizeModifiers| to quickly find + * out modifier keys that are desynchronized. + */ +@property(nonatomic) NSUInteger lastModifierFlagsOfInterest; + +/** + * A self-incrementing ID used to label key events sent to the framework. + */ +@property(nonatomic) uint64_t responseId; + +/** + * A map of unresponded key events sent to the framework. + * + * Its values are |responseId|s, and keys are the callback that was received + * along with the event. + */ +@property(nonatomic) NSMutableDictionary* pendingResponses; + +/** + * Compare the last modifier flags and the current, and dispatch synthesized + * key events for each different modifier flag bit. + * + * The flags compared are all flags after masking with + * |modifierFlagOfInterestMask| and excluding |ignoringFlags|. + */ +- (void)synchronizeModifiers:(NSUInteger)currentFlags + ignoringFlags:(NSUInteger)ignoringFlags + timestamp:(NSTimeInterval)timestamp; + +/** + * Update the pressing state. + * + * If `logicalKey` is not 0, `physicalKey` is pressed as `logicalKey`. + * Otherwise, `physicalKey` is released. + */ +- (void)updateKey:(uint64_t)physicalKey asPressed:(uint64_t)logicalKey; + +/** + * Send an event to the framework, expecting its response. + */ +- (void)sendPrimaryFlutterEvent:(const FlutterKeyEvent&)event + callback:(nonnull FlutterKeyCallbackGuard*)callback; + +/** + * Send a CapsLock down event, then a CapsLock up event. + * + * If downCallback is nil, then both events will be synthesized. Otherwise, the + * downCallback will be used as the callback for the down event, which is not + * synthesized. + */ +- (void)sendCapsLockTapWithTimestamp:(NSTimeInterval)timestamp + callback:(nullable FlutterKeyCallbackGuard*)downCallback; + +/** + * Send a key event for a modifier key. + * + * If callback is nil, then the event is synthesized. + */ +- (void)sendModifierEventOfType:(BOOL)isDownEvent + timestamp:(NSTimeInterval)timestamp + keyCode:(unsigned short)keyCode + callback:(nullable FlutterKeyCallbackGuard*)callback; + +/** + * Processes a down event from the system. + */ +- (void)handleDownEvent:(nonnull NSEvent*)event callback:(nonnull FlutterKeyCallbackGuard*)callback; + +/** + * Processes an up event from the system. + */ +- (void)handleUpEvent:(nonnull NSEvent*)event callback:(nonnull FlutterKeyCallbackGuard*)callback; + +/** + * Processes an event from the system for the CapsLock key. + */ +- (void)handleCapsLockEvent:(nonnull NSEvent*)event + callback:(nonnull FlutterKeyCallbackGuard*)callback; + +/** + * Processes a flags changed event from the system, where modifier keys are pressed or released. + */ +- (void)handleFlagEvent:(nonnull NSEvent*)event callback:(nonnull FlutterKeyCallbackGuard*)callback; + +/** + * Processes the response from the framework. + */ +- (void)handleResponse:(BOOL)handled forId:(uint64_t)responseId; + +@end + +@implementation FlutterEmbedderKeyResponder + +- (nonnull instancetype)initWithSendEvent:(FlutterSendEmbedderKeyEvent)sendEvent { + self = [super init]; + if (self != nil) { + _sendEvent = sendEvent; + _pressingRecords = [NSMutableDictionary dictionary]; + _pendingResponses = [NSMutableDictionary dictionary]; + _responseId = 1; + _lastModifierFlagsOfInterest = 0; + _modifierFlagOfInterestMask = computeModifierFlagOfInterestMask(); + } + return self; +} + +- (void)handleEvent:(NSEvent*)event callback:(FlutterAsyncKeyCallback)callback { + // The conversion algorithm relies on a non-nil callback to properly compute + // `synthesized`. + NSAssert(callback != nil, @"The callback must not be nil."); + FlutterKeyCallbackGuard* guardedCallback = + [[FlutterKeyCallbackGuard alloc] initWithCallback:callback]; + switch (event.type) { + case NSEventTypeKeyDown: + [self handleDownEvent:event callback:guardedCallback]; + break; + case NSEventTypeKeyUp: + [self handleUpEvent:event callback:guardedCallback]; + break; + case NSEventTypeFlagsChanged: + [self handleFlagEvent:event callback:guardedCallback]; + break; + default: + NSAssert(false, @"Unexpected key event type: |%@|.", @(event.type)); + } + NSAssert(guardedCallback.handled, @"The callback is returned without being handled."); + NSAssert(_lastModifierFlagsOfInterest == (event.modifierFlags & _modifierFlagOfInterestMask), + @"The modifier flags are not properly updated: recorded 0x%lx, event with mask 0x%lx", + _lastModifierFlagsOfInterest, event.modifierFlags & _modifierFlagOfInterestMask); +} + +#pragma mark - Private + +- (void)synchronizeModifiers:(NSUInteger)currentFlags + ignoringFlags:(NSUInteger)ignoringFlags + timestamp:(NSTimeInterval)timestamp { + const NSUInteger updatingMask = _modifierFlagOfInterestMask & ~ignoringFlags; + const NSUInteger currentFlagsOfInterest = currentFlags & updatingMask; + const NSUInteger lastFlagsOfInterest = _lastModifierFlagsOfInterest & updatingMask; + NSUInteger flagDifference = currentFlagsOfInterest ^ lastFlagsOfInterest; + if (flagDifference & NSEventModifierFlagCapsLock) { + [self sendCapsLockTapWithTimestamp:timestamp callback:nil]; + flagDifference = flagDifference & ~NSEventModifierFlagCapsLock; + } + while (true) { + const NSUInteger currentFlag = lowestSetBit(flagDifference); + if (currentFlag == 0) { + break; + } + flagDifference = flagDifference & ~currentFlag; + NSNumber* keyCode = [modifierFlagToKeyCode objectForKey:@(currentFlag)]; + NSAssert(keyCode != nil, @"Invalid modifier flag 0x%lx", currentFlag); + if (keyCode == nil) { + continue; + } + BOOL isDownEvent = (currentFlagsOfInterest & currentFlag) != 0; + [self sendModifierEventOfType:isDownEvent + timestamp:timestamp + keyCode:[keyCode unsignedShortValue] + callback:nil]; + } + _lastModifierFlagsOfInterest = + (_lastModifierFlagsOfInterest & ~updatingMask) | currentFlagsOfInterest; +} + +- (void)updateKey:(uint64_t)physicalKey asPressed:(uint64_t)logicalKey { + if (logicalKey == 0) { + [_pressingRecords removeObjectForKey:@(physicalKey)]; + } else { + _pressingRecords[@(physicalKey)] = @(logicalKey); + } +} + +- (void)sendPrimaryFlutterEvent:(const FlutterKeyEvent&)event + callback:(FlutterKeyCallbackGuard*)callback { + _responseId += 1; + uint64_t responseId = _responseId; + FlutterKeyPendingResponse* pending = + [[FlutterKeyPendingResponse alloc] initWithHandler:self responseId:responseId]; + [callback pendTo:_pendingResponses withId:responseId]; + // The `__bridge_retained` here is matched by `__bridge_transfer` in HandleResponse. + _sendEvent(event, HandleResponse, (__bridge_retained void*)pending); +} + +- (void)sendCapsLockTapWithTimestamp:(NSTimeInterval)timestamp + callback:(FlutterKeyCallbackGuard*)downCallback { + // MacOS sends a down *or* an up when CapsLock is tapped, alternatively on + // even taps and odd taps. A CapsLock down or CapsLock up should always be + // converted to a down *and* an up, and the up should always be a synthesized + // event, since we will never know when the button is released. + FlutterKeyEvent flutterEvent = { + .struct_size = sizeof(FlutterKeyEvent), + .timestamp = GetFlutterTimestampFrom(timestamp), + .type = kFlutterKeyEventTypeDown, + .physical = kCapsLockPhysicalKey, + .logical = kCapsLockLogicalKey, + .character = nil, + .synthesized = downCallback == nil, + }; + if (downCallback != nil) { + [self sendPrimaryFlutterEvent:flutterEvent callback:downCallback]; + } else { + _sendEvent(flutterEvent, nullptr, nullptr); + } + + flutterEvent.type = kFlutterKeyEventTypeUp; + flutterEvent.synthesized = true; + _sendEvent(flutterEvent, nullptr, nullptr); +} + +- (void)sendModifierEventOfType:(BOOL)isDownEvent + timestamp:(NSTimeInterval)timestamp + keyCode:(unsigned short)keyCode + callback:(FlutterKeyCallbackGuard*)callback { + uint64_t physicalKey = GetPhysicalKeyForKeyCode(keyCode); + uint64_t logicalKey = GetLogicalKeyForModifier(keyCode, physicalKey); + if (physicalKey == 0 || logicalKey == 0) { + NSLog(@"Unrecognized modifier key: keyCode 0x%hx, physical key 0x%llx", keyCode, physicalKey); + [callback resolveTo:TRUE]; + return; + } + FlutterKeyEvent flutterEvent = { + .struct_size = sizeof(FlutterKeyEvent), + .timestamp = GetFlutterTimestampFrom(timestamp), + .type = isDownEvent ? kFlutterKeyEventTypeDown : kFlutterKeyEventTypeUp, + .physical = physicalKey, + .logical = logicalKey, + .character = nil, + .synthesized = callback == nil, + }; + [self updateKey:physicalKey asPressed:isDownEvent ? logicalKey : 0]; + if (callback != nil) { + [self sendPrimaryFlutterEvent:flutterEvent callback:callback]; + } else { + _sendEvent(flutterEvent, nullptr, nullptr); + } +} + +- (void)handleDownEvent:(NSEvent*)event callback:(FlutterKeyCallbackGuard*)callback { + uint64_t physicalKey = GetPhysicalKeyForKeyCode(event.keyCode); + uint64_t logicalKey = GetLogicalKeyForEvent(event, physicalKey); + [self synchronizeModifiers:event.modifierFlags ignoringFlags:0 timestamp:event.timestamp]; + + bool isARepeat = event.isARepeat; + NSNumber* pressedLogicalKey = _pressingRecords[@(physicalKey)]; + if (pressedLogicalKey != nil && !isARepeat) { + // Normally the key up events won't be missed since macOS always sends the + // key up event to the window where the corresponding key down occurred. + // However this might happen in add-to-app scenarios if the focus is changed + // from the native view to the Flutter view amid the key tap. + [callback resolveTo:TRUE]; + return; + } + bool isSynthesized = false; + + if (pressedLogicalKey == nil) { + [self updateKey:physicalKey asPressed:logicalKey]; + } + + FlutterKeyEvent flutterEvent = { + .struct_size = sizeof(FlutterKeyEvent), + .timestamp = GetFlutterTimestampFrom(event.timestamp), + .type = isARepeat ? kFlutterKeyEventTypeRepeat : kFlutterKeyEventTypeDown, + .physical = physicalKey, + .logical = pressedLogicalKey == nil ? logicalKey : [pressedLogicalKey unsignedLongLongValue], + .character = getEventString(event.characters), + .synthesized = isSynthesized, + }; + [self sendPrimaryFlutterEvent:flutterEvent callback:callback]; +} + +- (void)handleUpEvent:(NSEvent*)event callback:(FlutterKeyCallbackGuard*)callback { + NSAssert(!event.isARepeat, @"Unexpected repeated Up event: keyCode %d, char %@, charIM %@", + event.keyCode, event.characters, event.charactersIgnoringModifiers); + [self synchronizeModifiers:event.modifierFlags ignoringFlags:0 timestamp:event.timestamp]; + + uint64_t physicalKey = GetPhysicalKeyForKeyCode(event.keyCode); + NSNumber* pressedLogicalKey = _pressingRecords[@(physicalKey)]; + if (pressedLogicalKey == nil) { + // Normally the key up events won't be missed since macOS always sends the + // key up event to the window where the corresponding key down occurred. + // However this might happen in add-to-app scenarios if the focus is changed + // from the native view to the Flutter view amid the key tap. + [callback resolveTo:TRUE]; + return; + } + [self updateKey:physicalKey asPressed:0]; + + FlutterKeyEvent flutterEvent = { + .struct_size = sizeof(FlutterKeyEvent), + .timestamp = GetFlutterTimestampFrom(event.timestamp), + .type = kFlutterKeyEventTypeUp, + .physical = physicalKey, + .logical = [pressedLogicalKey unsignedLongLongValue], + .character = nil, + .synthesized = false, + }; + [self sendPrimaryFlutterEvent:flutterEvent callback:callback]; +} + +- (void)handleCapsLockEvent:(NSEvent*)event callback:(FlutterKeyCallbackGuard*)callback { + [self synchronizeModifiers:event.modifierFlags + ignoringFlags:NSEventModifierFlagCapsLock + timestamp:event.timestamp]; + if ((_lastModifierFlagsOfInterest & NSEventModifierFlagCapsLock) != + (event.modifierFlags & NSEventModifierFlagCapsLock)) { + [self sendCapsLockTapWithTimestamp:event.timestamp callback:callback]; + _lastModifierFlagsOfInterest = _lastModifierFlagsOfInterest ^ NSEventModifierFlagCapsLock; + } else { + [callback resolveTo:TRUE]; + } +} + +- (void)handleFlagEvent:(NSEvent*)event callback:(FlutterKeyCallbackGuard*)callback { + NSNumber* targetModifierFlagObj = keyCodeToModifierFlag[@(event.keyCode)]; + NSUInteger targetModifierFlag = + targetModifierFlagObj == nil ? 0 : [targetModifierFlagObj unsignedLongValue]; + uint64_t targetKey = GetPhysicalKeyForKeyCode(event.keyCode); + if (targetKey == kCapsLockPhysicalKey) { + return [self handleCapsLockEvent:event callback:callback]; + } + + [self synchronizeModifiers:event.modifierFlags + ignoringFlags:targetModifierFlag + timestamp:event.timestamp]; + + NSNumber* pressedLogicalKey = [_pressingRecords objectForKey:@(targetKey)]; + BOOL lastTargetPressed = pressedLogicalKey != nil; + NSAssert(targetModifierFlagObj == nil || + (_lastModifierFlagsOfInterest & targetModifierFlag) != 0 == lastTargetPressed, + @"Desynchronized state between lastModifierFlagsOfInterest (0x%lx) on bit 0x%lx " + @"for keyCode 0x%hx, whose pressing state is %@.", + _lastModifierFlagsOfInterest, targetModifierFlag, event.keyCode, + lastTargetPressed + ? [NSString stringWithFormat:@"0x%llx", [pressedLogicalKey unsignedLongLongValue]] + : @"empty"); + + BOOL shouldBePressed = (event.modifierFlags & targetModifierFlag) != 0; + if (lastTargetPressed == shouldBePressed) { + [callback resolveTo:TRUE]; + return; + } + _lastModifierFlagsOfInterest = _lastModifierFlagsOfInterest ^ targetModifierFlag; + [self sendModifierEventOfType:shouldBePressed + timestamp:event.timestamp + keyCode:event.keyCode + callback:callback]; +} + +- (void)handleResponse:(BOOL)handled forId:(uint64_t)responseId { + FlutterAsyncKeyCallback callback = _pendingResponses[@(responseId)]; + callback(handled); + [_pendingResponses removeObjectForKey:@(responseId)]; +} + +@end + +namespace { +void HandleResponse(bool handled, void* user_data) { + // The `__bridge_transfer` here is matched by `__bridge_retained` in sendPrimaryFlutterEvent. + FlutterKeyPendingResponse* pending = (__bridge_transfer FlutterKeyPendingResponse*)user_data; + [pending.responder handleResponse:handled forId:pending.responseId]; +} +} // namespace diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponderUnittests.mm b/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponderUnittests.mm new file mode 100644 index 0000000000000..67974abcb6952 --- /dev/null +++ b/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponderUnittests.mm @@ -0,0 +1,1137 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import + +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponder.h" +#import "flutter/testing/testing.h" +#include "third_party/googletest/googletest/include/gtest/gtest.h" + +// A wrap to convert FlutterKeyEvent to a ObjC class. +@interface TestKeyEvent : NSObject +@property(nonatomic) FlutterKeyEvent* data; +@property(nonatomic) FlutterKeyEventCallback callback; +@property(nonatomic) _VoidPtr userData; +- (nonnull instancetype)initWithEvent:(const FlutterKeyEvent*)event + callback:(nullable FlutterKeyEventCallback)callback + userData:(nullable _VoidPtr)userData; +- (BOOL)hasCallback; +- (void)respond:(BOOL)handled; +@end + +@implementation TestKeyEvent +- (instancetype)initWithEvent:(const FlutterKeyEvent*)event + callback:(nullable FlutterKeyEventCallback)callback + userData:(nullable _VoidPtr)userData { + self = [super init]; + _data = new FlutterKeyEvent(*event); + if (event->character != nullptr) { + size_t len = strlen(event->character); + char* character = new char[len + 1]; + strcpy(character, event->character); + _data->character = character; + } + _callback = callback; + _userData = userData; + return self; +} + +- (BOOL)hasCallback { + return _callback != nil; +} + +- (void)respond:(BOOL)handled { + NSAssert( + _callback != nil, + @"Improper call to `respond` that does not have a callback."); // Caller's responsibility + _callback(handled, _userData); +} + +- (void)dealloc { + if (_data->character != nullptr) + delete[] _data->character; + delete _data; +} +@end + +namespace flutter::testing { + +namespace { +constexpr uint64_t kKeyCodeKeyA = 0x00; +constexpr uint64_t kKeyCodeKeyW = 0x0d; +constexpr uint64_t kKeyCodeShiftLeft = 0x38; +constexpr uint64_t kKeyCodeShiftRight = 0x3c; +constexpr uint64_t kKeyCodeCapsLock = 0x39; +constexpr uint64_t kKeyCodeNumpad1 = 0x53; +constexpr uint64_t kKeyCodeF1 = 0x7a; +constexpr uint64_t kKeyCodeAltRight = 0x3d; + +constexpr uint64_t kPhysicalKeyA = 0x00070004; +constexpr uint64_t kPhysicalKeyW = 0x0007001a; +constexpr uint64_t kPhysicalShiftLeft = 0x000700e1; +constexpr uint64_t kPhysicalShiftRight = 0x000700e5; +constexpr uint64_t kPhysicalCapsLock = 0x00070039; +constexpr uint64_t kPhysicalNumpad1 = 0x00070059; +constexpr uint64_t kPhysicalF1 = 0x0007003a; +constexpr uint64_t kPhysicalAltRight = 0x000700e6; + +constexpr uint64_t kLogicalKeyA = 0x00000061; +constexpr uint64_t kLogicalKeyW = 0x00000077; +constexpr uint64_t kLogicalShiftLeft = 0x0030000010d; +constexpr uint64_t kLogicalShiftRight = 0x0040000010d; +constexpr uint64_t kLogicalCapsLock = 0x00000000104; +constexpr uint64_t kLogicalNumpad1 = 0x00200000031; +constexpr uint64_t kLogicalF1 = 0x00000000801; +constexpr uint64_t kLogicalAltRight = 0x00400000102; + +typedef void (^ResponseCallback)(bool handled); + +NSEvent* keyEvent(NSEventType type, + NSEventModifierFlags modifierFlags, + NSString* characters, + NSString* charactersIgnoringModifiers, + BOOL isARepeat, + unsigned short keyCode) { + return [NSEvent keyEventWithType:type + location:NSZeroPoint + modifierFlags:modifierFlags + timestamp:0 + windowNumber:0 + context:nil + characters:characters + charactersIgnoringModifiers:charactersIgnoringModifiers + isARepeat:isARepeat + keyCode:keyCode]; +} + +NSEvent* keyEvent(NSEventType type, + NSTimeInterval timestamp, + NSEventModifierFlags modifierFlags, + NSString* characters, + NSString* charactersIgnoringModifiers, + BOOL isARepeat, + unsigned short keyCode) { + return [NSEvent keyEventWithType:type + location:NSZeroPoint + modifierFlags:modifierFlags + timestamp:timestamp + windowNumber:0 + context:nil + characters:characters + charactersIgnoringModifiers:charactersIgnoringModifiers + isARepeat:isARepeat + keyCode:keyCode]; +} + +} // namespace + +// Test the most basic key events. +// +// Press, hold, and release key A on an US keyboard. +TEST(FlutterEmbedderKeyResponderUnittests, BasicKeyEvent) { + __block NSMutableArray* events = [[NSMutableArray alloc] init]; + __block BOOL last_handled = TRUE; + FlutterKeyEvent* event; + + FlutterEmbedderKeyResponder* responder = [[FlutterEmbedderKeyResponder alloc] + initWithSendEvent:^(const FlutterKeyEvent& event, _Nullable FlutterKeyEventCallback callback, + _Nullable _VoidPtr user_data) { + [events addObject:[[TestKeyEvent alloc] initWithEvent:&event + callback:callback + userData:user_data]]; + }]; + + last_handled = FALSE; + [responder handleEvent:keyEvent(NSEventTypeKeyDown, 123.0f, 0x100, @"a", @"a", FALSE, 0) + callback:^(BOOL handled) { + last_handled = handled; + }]; + + EXPECT_EQ([events count], 1u); + event = [events lastObject].data; + EXPECT_EQ(event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(event->timestamp, 123000000.0f); + EXPECT_EQ(event->physical, kPhysicalKeyA); + EXPECT_EQ(event->logical, kLogicalKeyA); + EXPECT_STREQ(event->character, "a"); + EXPECT_EQ(event->synthesized, false); + + EXPECT_EQ(last_handled, FALSE); + EXPECT_TRUE([[events lastObject] hasCallback]); + [[events lastObject] respond:TRUE]; + EXPECT_EQ(last_handled, TRUE); + + [events removeAllObjects]; + + last_handled = FALSE; + [responder handleEvent:keyEvent(NSEventTypeKeyDown, 0x100, @"a", @"a", TRUE, kKeyCodeKeyA) + callback:^(BOOL handled) { + last_handled = handled; + }]; + + EXPECT_EQ([events count], 1u); + event = [events lastObject].data; + EXPECT_EQ(event->type, kFlutterKeyEventTypeRepeat); + EXPECT_EQ(event->physical, kPhysicalKeyA); + EXPECT_EQ(event->logical, kLogicalKeyA); + EXPECT_STREQ(event->character, "a"); + EXPECT_EQ(event->synthesized, false); + + EXPECT_EQ(last_handled, FALSE); + EXPECT_TRUE([[events lastObject] hasCallback]); + [[events lastObject] respond:TRUE]; + EXPECT_EQ(last_handled, TRUE); + + [events removeAllObjects]; + + last_handled = TRUE; + [responder handleEvent:keyEvent(NSEventTypeKeyUp, 124.0f, 0x100, @"a", @"a", FALSE, kKeyCodeKeyA) + callback:^(BOOL handled) { + last_handled = handled; + }]; + + EXPECT_EQ([events count], 1u); + event = [events lastObject].data; + EXPECT_EQ(event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(event->timestamp, 124000000.0f); + EXPECT_EQ(event->physical, kPhysicalKeyA); + EXPECT_EQ(event->logical, kLogicalKeyA); + EXPECT_EQ(event->character, nullptr); + EXPECT_EQ(event->synthesized, false); + + EXPECT_EQ(last_handled, TRUE); + EXPECT_TRUE([[events lastObject] hasCallback]); + [[events lastObject] respond:FALSE]; // Check if responding FALSE works + EXPECT_EQ(last_handled, FALSE); + + [events removeAllObjects]; +} + +TEST(FlutterEmbedderKeyResponderUnittests, NonAsciiCharacters) { + __block NSMutableArray* events = [[NSMutableArray alloc] init]; + FlutterKeyEvent* event; + + FlutterEmbedderKeyResponder* responder = [[FlutterEmbedderKeyResponder alloc] + initWithSendEvent:^(const FlutterKeyEvent& event, _Nullable FlutterKeyEventCallback callback, + _Nullable _VoidPtr user_data) { + [events addObject:[[TestKeyEvent alloc] initWithEvent:&event + callback:callback + userData:user_data]]; + }]; + + [responder + handleEvent:keyEvent(NSEventTypeFlagsChanged, 0x80140, @"", @"", FALSE, kKeyCodeAltRight) + callback:^(BOOL handled){ + }]; + + EXPECT_EQ([events count], 1u); + event = [events lastObject].data; + EXPECT_EQ(event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(event->physical, kPhysicalAltRight); + EXPECT_EQ(event->logical, kLogicalAltRight); + EXPECT_STREQ(event->character, nullptr); + EXPECT_EQ(event->synthesized, false); + + [events removeAllObjects]; + + [responder handleEvent:keyEvent(NSEventTypeKeyDown, 0x80140, @"∑", @"w", FALSE, kKeyCodeKeyW) + callback:^(BOOL handled){ + }]; + + EXPECT_EQ([events count], 1u); + event = [events lastObject].data; + EXPECT_EQ(event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(event->physical, kPhysicalKeyW); + EXPECT_EQ(event->logical, kLogicalKeyW); + EXPECT_STREQ(event->character, "∑"); + EXPECT_EQ(event->synthesized, false); + + [events removeAllObjects]; + + [responder handleEvent:keyEvent(NSEventTypeFlagsChanged, 0x100, @"", @"", FALSE, kKeyCodeAltRight) + callback:^(BOOL handled){ + }]; + + EXPECT_EQ([events count], 1u); + event = [events lastObject].data; + EXPECT_EQ(event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(event->physical, kPhysicalAltRight); + EXPECT_EQ(event->logical, kLogicalAltRight); + EXPECT_STREQ(event->character, nullptr); + EXPECT_EQ(event->synthesized, false); + + [events removeAllObjects]; + + [responder handleEvent:keyEvent(NSEventTypeKeyUp, 0x100, @"w", @"w", FALSE, kKeyCodeKeyW) + callback:^(BOOL handled){ + }]; + + EXPECT_EQ([events count], 1u); + event = [events lastObject].data; + EXPECT_EQ(event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(event->physical, kPhysicalKeyW); + EXPECT_EQ(event->logical, kLogicalKeyW); + EXPECT_STREQ(event->character, nullptr); + EXPECT_EQ(event->synthesized, false); + + [events removeAllObjects]; +} + +TEST(FlutterEmbedderKeyResponderUnittests, IgnoreDuplicateDownEvent) { + __block NSMutableArray* events = [[NSMutableArray alloc] init]; + __block BOOL last_handled = TRUE; + FlutterKeyEvent* event; + + FlutterEmbedderKeyResponder* responder = [[FlutterEmbedderKeyResponder alloc] + initWithSendEvent:^(const FlutterKeyEvent& event, _Nullable FlutterKeyEventCallback callback, + _Nullable _VoidPtr user_data) { + [events addObject:[[TestKeyEvent alloc] initWithEvent:&event + callback:callback + userData:user_data]]; + }]; + + last_handled = FALSE; + [responder handleEvent:keyEvent(NSEventTypeKeyDown, 0x100, @"a", @"a", FALSE, kKeyCodeKeyA) + callback:^(BOOL handled) { + last_handled = handled; + }]; + + EXPECT_EQ([events count], 1u); + event = [events lastObject].data; + EXPECT_EQ(event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(event->physical, kPhysicalKeyA); + EXPECT_EQ(event->logical, kLogicalKeyA); + EXPECT_STREQ(event->character, "a"); + EXPECT_EQ(event->synthesized, false); + EXPECT_EQ(last_handled, FALSE); + [[events lastObject] respond:TRUE]; + EXPECT_EQ(last_handled, TRUE); + + [events removeAllObjects]; + + last_handled = FALSE; + [responder handleEvent:keyEvent(NSEventTypeKeyDown, 0x100, @"a", @"a", FALSE, kKeyCodeKeyA) + callback:^(BOOL handled) { + last_handled = handled; + }]; + + EXPECT_EQ([events count], 0u); + EXPECT_EQ(last_handled, TRUE); + + last_handled = FALSE; + [responder handleEvent:keyEvent(NSEventTypeKeyUp, 0x100, @"a", @"a", FALSE, kKeyCodeKeyA) + callback:^(BOOL handled) { + last_handled = handled; + }]; + + EXPECT_EQ([events count], 1u); + event = [events lastObject].data; + EXPECT_EQ(event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(event->physical, kPhysicalKeyA); + EXPECT_EQ(event->logical, kLogicalKeyA); + EXPECT_STREQ(event->character, nullptr); + EXPECT_EQ(event->synthesized, false); + EXPECT_EQ(last_handled, FALSE); + [[events lastObject] respond:TRUE]; + EXPECT_EQ(last_handled, TRUE); + + [events removeAllObjects]; +} + +TEST(FlutterEmbedderKeyResponderUnittests, IgnoreDuplicateUpEvent) { + __block NSMutableArray* events = [[NSMutableArray alloc] init]; + __block BOOL last_handled = TRUE; + + FlutterEmbedderKeyResponder* responder = [[FlutterEmbedderKeyResponder alloc] + initWithSendEvent:^(const FlutterKeyEvent& event, _Nullable FlutterKeyEventCallback callback, + _Nullable _VoidPtr user_data) { + [events addObject:[[TestKeyEvent alloc] initWithEvent:&event + callback:callback + userData:user_data]]; + }]; + + last_handled = FALSE; + [responder handleEvent:keyEvent(NSEventTypeKeyUp, 0x100, @"a", @"a", FALSE, kKeyCodeKeyA) + callback:^(BOOL handled) { + last_handled = handled; + }]; + + EXPECT_EQ([events count], 0u); + EXPECT_EQ(last_handled, TRUE); +} + +// Press L shift, A, then release L shift then A, on an US keyboard. +// +// This is special because the characters for the A key will change in this +// process. +TEST(FlutterEmbedderKeyResponderUnittests, ToggleModifiersDuringKeyTap) { + __block NSMutableArray* events = [[NSMutableArray alloc] init]; + FlutterKeyEvent* event; + + FlutterEmbedderKeyResponder* responder = [[FlutterEmbedderKeyResponder alloc] + initWithSendEvent:^(const FlutterKeyEvent& event, _Nullable FlutterKeyEventCallback callback, + _Nullable _VoidPtr user_data) { + [events addObject:[[TestKeyEvent alloc] initWithEvent:&event + callback:callback + userData:user_data]]; + }]; + + [responder handleEvent:keyEvent(NSEventTypeFlagsChanged, 123.0f, 0x20104, @"", @"", FALSE, + kKeyCodeShiftRight) + callback:^(BOOL handled){ + }]; + + EXPECT_EQ([events count], 1u); + event = [events lastObject].data; + EXPECT_EQ(event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(event->timestamp, 123000000.0f); + EXPECT_EQ(event->physical, kPhysicalShiftRight); + EXPECT_EQ(event->logical, kLogicalShiftRight); + EXPECT_STREQ(event->character, nullptr); + EXPECT_EQ(event->synthesized, false); + [[events lastObject] respond:TRUE]; + + [events removeAllObjects]; + + [responder handleEvent:keyEvent(NSEventTypeKeyDown, 0x20104, @"A", @"A", FALSE, kKeyCodeKeyA) + callback:^(BOOL handled){ + }]; + + EXPECT_EQ([events count], 1u); + event = [events lastObject].data; + EXPECT_EQ(event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(event->physical, kPhysicalKeyA); + EXPECT_EQ(event->logical, kLogicalKeyA); + EXPECT_STREQ(event->character, "A"); + EXPECT_EQ(event->synthesized, false); + [[events lastObject] respond:TRUE]; + + [events removeAllObjects]; + + [responder handleEvent:keyEvent(NSEventTypeKeyDown, 0x20104, @"A", @"A", TRUE, kKeyCodeKeyA) + callback:^(BOOL handled){ + }]; + + EXPECT_EQ([events count], 1u); + event = [events lastObject].data; + EXPECT_EQ(event->type, kFlutterKeyEventTypeRepeat); + EXPECT_EQ(event->physical, kPhysicalKeyA); + EXPECT_EQ(event->logical, kLogicalKeyA); + EXPECT_STREQ(event->character, "A"); + EXPECT_EQ(event->synthesized, false); + [[events lastObject] respond:TRUE]; + + [events removeAllObjects]; + + [responder + handleEvent:keyEvent(NSEventTypeFlagsChanged, 0x100, @"", @"", FALSE, kKeyCodeShiftRight) + callback:^(BOOL handled){ + }]; + + EXPECT_EQ([events count], 1u); + event = [events lastObject].data; + EXPECT_EQ(event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(event->physical, kPhysicalShiftRight); + EXPECT_EQ(event->logical, kLogicalShiftRight); + EXPECT_STREQ(event->character, nullptr); + EXPECT_EQ(event->synthesized, false); + [[events lastObject] respond:TRUE]; + + [events removeAllObjects]; + + [responder handleEvent:keyEvent(NSEventTypeKeyDown, 0x100, @"a", @"a", TRUE, kKeyCodeKeyA) + callback:^(BOOL handled){ + }]; + + EXPECT_EQ([events count], 1u); + event = [events lastObject].data; + EXPECT_EQ(event->type, kFlutterKeyEventTypeRepeat); + EXPECT_EQ(event->physical, kPhysicalKeyA); + EXPECT_EQ(event->logical, kLogicalKeyA); + EXPECT_STREQ(event->character, "a"); + EXPECT_EQ(event->synthesized, false); + + [events removeAllObjects]; + + [responder handleEvent:keyEvent(NSEventTypeKeyUp, 0x100, @"a", @"a", FALSE, kKeyCodeKeyA) + callback:^(BOOL handled){ + }]; + + EXPECT_EQ([events count], 1u); + event = [events lastObject].data; + EXPECT_EQ(event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(event->physical, kPhysicalKeyA); + EXPECT_EQ(event->logical, kLogicalKeyA); + EXPECT_STREQ(event->character, nullptr); + EXPECT_EQ(event->synthesized, false); + [[events lastObject] respond:TRUE]; + + [events removeAllObjects]; +} + +// Special modifier flags. +// +// Some keys in modifierFlags are not to indicate modifier state, but to mark +// the key area that the key belongs to, such as numpad keys or function keys. +// Ensure these flags do not obstruct other keys. +TEST(FlutterEmbedderKeyResponderUnittests, SpecialModiferFlags) { + __block NSMutableArray* events = [[NSMutableArray alloc] init]; + FlutterKeyEvent* event; + + FlutterEmbedderKeyResponder* responder = [[FlutterEmbedderKeyResponder alloc] + initWithSendEvent:^(const FlutterKeyEvent& event, _Nullable FlutterKeyEventCallback callback, + _Nullable _VoidPtr user_data) { + [events addObject:[[TestKeyEvent alloc] initWithEvent:&event + callback:callback + userData:user_data]]; + }]; + + // Keydown: Numpad1, F1, KeyA, ShiftLeft + // Then KeyUp: Numpad1, F1, KeyA, ShiftLeft + + // Numpad 1 + [responder handleEvent:keyEvent(NSEventTypeKeyDown, 0x200100, @"1", @"1", FALSE, kKeyCodeNumpad1) + callback:^(BOOL handled){ + }]; + + EXPECT_EQ([events count], 1u); + event = [events lastObject].data; + EXPECT_EQ(event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(event->physical, kPhysicalNumpad1); + EXPECT_EQ(event->logical, kLogicalNumpad1); + EXPECT_STREQ(event->character, "1"); + EXPECT_EQ(event->synthesized, false); + [[events lastObject] respond:TRUE]; + + [events removeAllObjects]; + + // F1 + [responder + handleEvent:keyEvent(NSEventTypeKeyDown, 0x800100, @"\uf704", @"\uf704", FALSE, kKeyCodeF1) + callback:^(BOOL handled){ + }]; + + EXPECT_EQ([events count], 1u); + event = [events lastObject].data; + EXPECT_EQ(event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(event->physical, kPhysicalF1); + EXPECT_EQ(event->logical, kLogicalF1); + EXPECT_STREQ(event->character, nullptr); + EXPECT_EQ(event->synthesized, false); + [[events lastObject] respond:TRUE]; + + [events removeAllObjects]; + + // KeyA + [responder handleEvent:keyEvent(NSEventTypeKeyDown, 0x100, @"a", @"a", FALSE, kKeyCodeKeyA) + callback:^(BOOL handled){ + }]; + + EXPECT_EQ([events count], 1u); + event = [events lastObject].data; + EXPECT_EQ(event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(event->physical, kPhysicalKeyA); + EXPECT_EQ(event->logical, kLogicalKeyA); + EXPECT_STREQ(event->character, "a"); + EXPECT_EQ(event->synthesized, false); + [[events lastObject] respond:TRUE]; + + [events removeAllObjects]; + + // ShiftLeft + [responder + handleEvent:keyEvent(NSEventTypeFlagsChanged, 0x20102, @"", @"", FALSE, kKeyCodeShiftLeft) + callback:^(BOOL handled){ + }]; + + EXPECT_EQ([events count], 1u); + event = [events lastObject].data; + EXPECT_EQ(event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(event->physical, kPhysicalShiftLeft); + EXPECT_EQ(event->logical, kLogicalShiftLeft); + EXPECT_STREQ(event->character, nullptr); + EXPECT_EQ(event->synthesized, false); + [[events lastObject] respond:TRUE]; + + [events removeAllObjects]; + + // Numpad 1 + [responder handleEvent:keyEvent(NSEventTypeKeyUp, 0x220102, @"1", @"1", FALSE, kKeyCodeNumpad1) + callback:^(BOOL handled){ + }]; + + EXPECT_EQ([events count], 1u); + event = [events lastObject].data; + EXPECT_EQ(event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(event->physical, kPhysicalNumpad1); + EXPECT_EQ(event->logical, kLogicalNumpad1); + EXPECT_STREQ(event->character, nullptr); + EXPECT_EQ(event->synthesized, false); + [[events lastObject] respond:TRUE]; + + [events removeAllObjects]; + + // F1 + [responder + handleEvent:keyEvent(NSEventTypeKeyUp, 0x820102, @"\uF704", @"\uF704", FALSE, kKeyCodeF1) + callback:^(BOOL handled){ + }]; + + EXPECT_EQ([events count], 1u); + event = [events lastObject].data; + EXPECT_EQ(event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(event->physical, kPhysicalF1); + EXPECT_EQ(event->logical, kLogicalF1); + EXPECT_STREQ(event->character, nullptr); + EXPECT_EQ(event->synthesized, false); + [[events lastObject] respond:TRUE]; + + [events removeAllObjects]; + + // KeyA + [responder handleEvent:keyEvent(NSEventTypeKeyUp, 0x20102, @"a", @"a", FALSE, kKeyCodeKeyA) + callback:^(BOOL handled){ + }]; + + EXPECT_EQ([events count], 1u); + event = [events lastObject].data; + EXPECT_EQ(event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(event->physical, kPhysicalKeyA); + EXPECT_EQ(event->logical, kLogicalKeyA); + EXPECT_STREQ(event->character, nullptr); + EXPECT_EQ(event->synthesized, false); + [[events lastObject] respond:TRUE]; + + [events removeAllObjects]; + + // ShiftLeft + [responder + handleEvent:keyEvent(NSEventTypeFlagsChanged, 0x100, @"", @"", FALSE, kKeyCodeShiftLeft) + callback:^(BOOL handled){ + }]; + + EXPECT_EQ([events count], 1u); + event = [events lastObject].data; + EXPECT_EQ(event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(event->physical, kPhysicalShiftLeft); + EXPECT_EQ(event->logical, kLogicalShiftLeft); + EXPECT_STREQ(event->character, nullptr); + EXPECT_EQ(event->synthesized, false); + [[events lastObject] respond:TRUE]; + + [events removeAllObjects]; +} + +TEST(FlutterEmbedderKeyResponderUnittests, IdentifyLeftAndRightModifiers) { + __block NSMutableArray* events = [[NSMutableArray alloc] init]; + FlutterKeyEvent* event; + + FlutterEmbedderKeyResponder* responder = [[FlutterEmbedderKeyResponder alloc] + initWithSendEvent:^(const FlutterKeyEvent& event, _Nullable FlutterKeyEventCallback callback, + _Nullable _VoidPtr user_data) { + [events addObject:[[TestKeyEvent alloc] initWithEvent:&event + callback:callback + userData:user_data]]; + }]; + + [responder + handleEvent:keyEvent(NSEventTypeFlagsChanged, 0x20102, @"", @"", FALSE, kKeyCodeShiftLeft) + callback:^(BOOL handled){ + }]; + + EXPECT_EQ([events count], 1u); + event = [events lastObject].data; + EXPECT_EQ(event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(event->physical, kPhysicalShiftLeft); + EXPECT_EQ(event->logical, kLogicalShiftLeft); + EXPECT_STREQ(event->character, nullptr); + EXPECT_EQ(event->synthesized, false); + [[events lastObject] respond:TRUE]; + + [events removeAllObjects]; + + [responder + handleEvent:keyEvent(NSEventTypeFlagsChanged, 0x20106, @"", @"", FALSE, kKeyCodeShiftRight) + callback:^(BOOL handled){ + }]; + + EXPECT_EQ([events count], 1u); + event = [events lastObject].data; + EXPECT_EQ(event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(event->physical, kPhysicalShiftRight); + EXPECT_EQ(event->logical, kLogicalShiftRight); + EXPECT_STREQ(event->character, nullptr); + EXPECT_EQ(event->synthesized, false); + [[events lastObject] respond:TRUE]; + + [events removeAllObjects]; + + [responder + handleEvent:keyEvent(NSEventTypeFlagsChanged, 0x20104, @"", @"", FALSE, kKeyCodeShiftLeft) + callback:^(BOOL handled){ + }]; + + EXPECT_EQ([events count], 1u); + event = [events lastObject].data; + EXPECT_EQ(event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(event->physical, kPhysicalShiftLeft); + EXPECT_EQ(event->logical, kLogicalShiftLeft); + EXPECT_STREQ(event->character, nullptr); + EXPECT_EQ(event->synthesized, false); + [[events lastObject] respond:TRUE]; + + [events removeAllObjects]; + + [responder + handleEvent:keyEvent(NSEventTypeFlagsChanged, 0x100, @"", @"", FALSE, kKeyCodeShiftRight) + callback:^(BOOL handled){ + }]; + + EXPECT_EQ([events count], 1u); + event = [events lastObject].data; + EXPECT_EQ(event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(event->physical, kPhysicalShiftRight); + EXPECT_EQ(event->logical, kLogicalShiftRight); + EXPECT_STREQ(event->character, nullptr); + EXPECT_EQ(event->synthesized, false); + [[events lastObject] respond:TRUE]; + + [events removeAllObjects]; +} + +// Process various cases where pair modifier key events are missed, and the +// responder has to "guess" how to synchronize states. +// +// In the following comments, parentheses indicate missed events, while +// asterisks indicate synthesized events. +TEST(FlutterEmbedderKeyResponderUnittests, SynthesizeMissedModifierEvents) { + __block NSMutableArray* events = [[NSMutableArray alloc] init]; + __block BOOL last_handled = TRUE; + id keyEventCallback = ^(BOOL handled) { + last_handled = handled; + }; + FlutterKeyEvent* event; + + FlutterEmbedderKeyResponder* responder = [[FlutterEmbedderKeyResponder alloc] + initWithSendEvent:^(const FlutterKeyEvent& event, _Nullable FlutterKeyEventCallback callback, + _Nullable _VoidPtr user_data) { + [events addObject:[[TestKeyEvent alloc] initWithEvent:&event + callback:callback + userData:user_data]]; + }]; + + // Case 1: + // In: L down, (L up), L down, L up + // Out: L down, L up + last_handled = FALSE; + [responder + handleEvent:keyEvent(NSEventTypeFlagsChanged, 0x20102, @"", @"", FALSE, kKeyCodeShiftLeft) + callback:keyEventCallback]; + + EXPECT_EQ([events count], 1u); + event = [events lastObject].data; + EXPECT_EQ(event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(event->physical, kPhysicalShiftLeft); + EXPECT_EQ(event->logical, kLogicalShiftLeft); + EXPECT_STREQ(event->character, nullptr); + EXPECT_EQ(event->synthesized, false); + + EXPECT_EQ(last_handled, FALSE); + EXPECT_TRUE([[events lastObject] hasCallback]); + [[events lastObject] respond:TRUE]; + EXPECT_EQ(last_handled, TRUE); + + [events removeAllObjects]; + + last_handled = FALSE; + [responder + handleEvent:keyEvent(NSEventTypeFlagsChanged, 0x20102, @"", @"", FALSE, kKeyCodeShiftLeft) + callback:keyEventCallback]; + + EXPECT_EQ([events count], 0u); + EXPECT_EQ(last_handled, TRUE); + + last_handled = FALSE; + [responder + handleEvent:keyEvent(NSEventTypeFlagsChanged, 0x100, @"", @"", FALSE, kKeyCodeShiftLeft) + callback:keyEventCallback]; + + EXPECT_EQ([events count], 1u); + event = [events lastObject].data; + EXPECT_EQ(event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(event->physical, kPhysicalShiftLeft); + EXPECT_EQ(event->logical, kLogicalShiftLeft); + EXPECT_STREQ(event->character, nullptr); + EXPECT_EQ(event->synthesized, false); + + EXPECT_EQ(last_handled, FALSE); + EXPECT_TRUE([[events lastObject] hasCallback]); + [[events lastObject] respond:TRUE]; + EXPECT_EQ(last_handled, TRUE); + + [events removeAllObjects]; + + // Case 2: + // In: (L down), L up + // Out: + + last_handled = FALSE; + [responder + handleEvent:keyEvent(NSEventTypeFlagsChanged, 0x100, @"", @"", FALSE, kKeyCodeShiftLeft) + callback:keyEventCallback]; + + EXPECT_EQ([events count], 0u); + EXPECT_EQ(last_handled, TRUE); + + // Case 3: + // In: L down, (L up), (R down), R up + // Out: L down, *L up + + last_handled = FALSE; + [responder + handleEvent:keyEvent(NSEventTypeFlagsChanged, 0x20102, @"", @"", FALSE, kKeyCodeShiftLeft) + callback:keyEventCallback]; + + EXPECT_EQ([events count], 1u); + event = [events lastObject].data; + EXPECT_EQ(event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(event->physical, kPhysicalShiftLeft); + EXPECT_EQ(event->logical, kLogicalShiftLeft); + EXPECT_STREQ(event->character, nullptr); + EXPECT_EQ(event->synthesized, false); + + EXPECT_EQ(last_handled, FALSE); + EXPECT_TRUE([[events lastObject] hasCallback]); + [[events lastObject] respond:TRUE]; + EXPECT_EQ(last_handled, TRUE); + + [events removeAllObjects]; + + last_handled = FALSE; + [responder + handleEvent:keyEvent(NSEventTypeFlagsChanged, 0x100, @"", @"", FALSE, kKeyCodeShiftRight) + callback:keyEventCallback]; + + EXPECT_EQ([events count], 1u); + event = [events lastObject].data; + EXPECT_EQ(event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(event->physical, kPhysicalShiftLeft); + EXPECT_EQ(event->logical, kLogicalShiftLeft); + EXPECT_STREQ(event->character, nullptr); + EXPECT_EQ(event->synthesized, true); + + // The primary event is automatically replied with TRUE, unrelated to the received event. + EXPECT_EQ(last_handled, TRUE); + EXPECT_FALSE([[events lastObject] hasCallback]); + + [events removeAllObjects]; + + // Case 4: + // In: L down, R down, (L up), R up + // Out: L down, R down *L up & R up + + last_handled = FALSE; + [responder + handleEvent:keyEvent(NSEventTypeFlagsChanged, 0x20102, @"", @"", FALSE, kKeyCodeShiftLeft) + callback:keyEventCallback]; + + EXPECT_EQ([events count], 1u); + event = [events lastObject].data; + EXPECT_EQ(event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(event->physical, kPhysicalShiftLeft); + EXPECT_EQ(event->logical, kLogicalShiftLeft); + EXPECT_STREQ(event->character, nullptr); + EXPECT_EQ(event->synthesized, false); + + EXPECT_EQ(last_handled, FALSE); + EXPECT_TRUE([[events lastObject] hasCallback]); + [[events lastObject] respond:TRUE]; + EXPECT_EQ(last_handled, TRUE); + + [events removeAllObjects]; + + last_handled = FALSE; + [responder + handleEvent:keyEvent(NSEventTypeFlagsChanged, 0x20106, @"", @"", FALSE, kKeyCodeShiftRight) + callback:keyEventCallback]; + + EXPECT_EQ([events count], 1u); + event = [events lastObject].data; + EXPECT_EQ(event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(event->physical, kPhysicalShiftRight); + EXPECT_EQ(event->logical, kLogicalShiftRight); + EXPECT_STREQ(event->character, nullptr); + EXPECT_EQ(event->synthesized, false); + + EXPECT_EQ(last_handled, FALSE); + EXPECT_TRUE([[events lastObject] hasCallback]); + [[events lastObject] respond:TRUE]; + EXPECT_EQ(last_handled, TRUE); + + [events removeAllObjects]; + + last_handled = FALSE; + [responder + handleEvent:keyEvent(NSEventTypeFlagsChanged, 0x100, @"", @"", FALSE, kKeyCodeShiftRight) + callback:keyEventCallback]; + + EXPECT_EQ([events count], 2u); + event = [events firstObject].data; + EXPECT_EQ(event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(event->physical, kPhysicalShiftLeft); + EXPECT_EQ(event->logical, kLogicalShiftLeft); + EXPECT_STREQ(event->character, nullptr); + EXPECT_EQ(event->synthesized, true); + + EXPECT_FALSE([[events firstObject] hasCallback]); + + event = [events lastObject].data; + EXPECT_EQ(event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(event->physical, kPhysicalShiftRight); + EXPECT_EQ(event->logical, kLogicalShiftRight); + EXPECT_STREQ(event->character, nullptr); + EXPECT_EQ(event->synthesized, false); + + EXPECT_EQ(last_handled, FALSE); + EXPECT_TRUE([[events lastObject] hasCallback]); + [[events lastObject] respond:TRUE]; + EXPECT_EQ(last_handled, TRUE); + + [events removeAllObjects]; +} + +TEST(FlutterEmbedderKeyResponderUnittests, SynthesizeMissedModifierEventsInNormalEvents) { + __block NSMutableArray* events = [[NSMutableArray alloc] init]; + __block BOOL last_handled = TRUE; + id keyEventCallback = ^(BOOL handled) { + last_handled = handled; + }; + FlutterKeyEvent* event; + + FlutterEmbedderKeyResponder* responder = [[FlutterEmbedderKeyResponder alloc] + initWithSendEvent:^(const FlutterKeyEvent& event, _Nullable FlutterKeyEventCallback callback, + _Nullable _VoidPtr user_data) { + [events addObject:[[TestKeyEvent alloc] initWithEvent:&event + callback:callback + userData:user_data]]; + }]; + + // In: (LShift down), A down, (LShift up), A up + // Out: *LS down & A down, *LS up & A up + + last_handled = FALSE; + [responder handleEvent:keyEvent(NSEventTypeKeyDown, 0x20102, @"A", @"A", FALSE, kKeyCodeKeyA) + callback:keyEventCallback]; + + EXPECT_EQ([events count], 2u); + event = [events firstObject].data; + EXPECT_EQ(event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(event->physical, kPhysicalShiftLeft); + EXPECT_EQ(event->logical, kLogicalShiftLeft); + EXPECT_STREQ(event->character, nullptr); + EXPECT_EQ(event->synthesized, true); + EXPECT_FALSE([[events firstObject] hasCallback]); + + event = [events lastObject].data; + EXPECT_EQ(event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(event->physical, kPhysicalKeyA); + EXPECT_EQ(event->logical, kLogicalKeyA); + EXPECT_STREQ(event->character, "A"); + EXPECT_EQ(event->synthesized, false); + EXPECT_TRUE([[events lastObject] hasCallback]); + + EXPECT_EQ(last_handled, FALSE); + [[events lastObject] respond:TRUE]; + EXPECT_EQ(last_handled, TRUE); + + [events removeAllObjects]; + + last_handled = FALSE; + [responder handleEvent:keyEvent(NSEventTypeKeyUp, 0x100, @"a", @"a", FALSE, kKeyCodeKeyA) + callback:keyEventCallback]; + + EXPECT_EQ([events count], 2u); + event = [events firstObject].data; + EXPECT_EQ(event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(event->physical, kPhysicalShiftLeft); + EXPECT_EQ(event->logical, kLogicalShiftLeft); + EXPECT_STREQ(event->character, nullptr); + EXPECT_EQ(event->synthesized, true); + EXPECT_FALSE([[events firstObject] hasCallback]); + + event = [events lastObject].data; + EXPECT_EQ(event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(event->physical, kPhysicalKeyA); + EXPECT_EQ(event->logical, kLogicalKeyA); + EXPECT_STREQ(event->character, nullptr); + EXPECT_EQ(event->synthesized, false); + EXPECT_TRUE([[events lastObject] hasCallback]); + + EXPECT_EQ(last_handled, FALSE); + [[events lastObject] respond:TRUE]; + EXPECT_EQ(last_handled, TRUE); + + [events removeAllObjects]; +} + +TEST(FlutterEmbedderKeyResponderUnittests, ConvertCapsLockEvents) { + __block NSMutableArray* events = [[NSMutableArray alloc] init]; + __block BOOL last_handled = TRUE; + id keyEventCallback = ^(BOOL handled) { + last_handled = handled; + }; + FlutterKeyEvent* event; + + FlutterEmbedderKeyResponder* responder = [[FlutterEmbedderKeyResponder alloc] + initWithSendEvent:^(const FlutterKeyEvent& event, _Nullable FlutterKeyEventCallback callback, + _Nullable _VoidPtr user_data) { + [events addObject:[[TestKeyEvent alloc] initWithEvent:&event + callback:callback + userData:user_data]]; + }]; + + // In: CapsLock down + // Out: CapsLock down & *CapsLock Up + last_handled = FALSE; + [responder + handleEvent:keyEvent(NSEventTypeFlagsChanged, 0x10100, @"", @"", FALSE, kKeyCodeCapsLock) + callback:keyEventCallback]; + + EXPECT_EQ([events count], 2u); + + event = [events firstObject].data; + EXPECT_EQ(event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(event->physical, kPhysicalCapsLock); + EXPECT_EQ(event->logical, kLogicalCapsLock); + EXPECT_STREQ(event->character, nullptr); + EXPECT_EQ(event->synthesized, false); + EXPECT_TRUE([[events firstObject] hasCallback]); + + event = [events lastObject].data; + EXPECT_EQ(event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(event->physical, kPhysicalCapsLock); + EXPECT_EQ(event->logical, kLogicalCapsLock); + EXPECT_STREQ(event->character, nullptr); + EXPECT_EQ(event->synthesized, true); + EXPECT_FALSE([[events lastObject] hasCallback]); + + EXPECT_EQ(last_handled, FALSE); + [[events firstObject] respond:TRUE]; + EXPECT_EQ(last_handled, TRUE); + + [events removeAllObjects]; + + // In: CapsLock up + // Out: CapsLock down & *CapsLock Up + last_handled = FALSE; + [responder handleEvent:keyEvent(NSEventTypeFlagsChanged, 0x100, @"", @"", FALSE, kKeyCodeCapsLock) + callback:keyEventCallback]; + + EXPECT_EQ([events count], 2u); + + event = [events firstObject].data; + EXPECT_EQ(event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(event->physical, kPhysicalCapsLock); + EXPECT_EQ(event->logical, kLogicalCapsLock); + EXPECT_STREQ(event->character, nullptr); + EXPECT_EQ(event->synthesized, false); + EXPECT_TRUE([[events firstObject] hasCallback]); + + event = [events lastObject].data; + EXPECT_EQ(event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(event->physical, kPhysicalCapsLock); + EXPECT_EQ(event->logical, kLogicalCapsLock); + EXPECT_STREQ(event->character, nullptr); + EXPECT_EQ(event->synthesized, true); + EXPECT_FALSE([[events lastObject] hasCallback]); + + EXPECT_EQ(last_handled, FALSE); + [[events firstObject] respond:TRUE]; + EXPECT_EQ(last_handled, TRUE); + + [events removeAllObjects]; +} + +// Press the CapsLock key when CapsLock state is desynchronized +TEST(FlutterEmbedderKeyResponderUnittests, SynchronizeCapsLockStateOnCapsLock) { + __block NSMutableArray* events = [[NSMutableArray alloc] init]; + __block BOOL last_handled = TRUE; + id keyEventCallback = ^(BOOL handled) { + last_handled = handled; + }; + + FlutterEmbedderKeyResponder* responder = [[FlutterEmbedderKeyResponder alloc] + initWithSendEvent:^(const FlutterKeyEvent& event, _Nullable FlutterKeyEventCallback callback, + _Nullable _VoidPtr user_data) { + [events addObject:[[TestKeyEvent alloc] initWithEvent:&event + callback:callback + userData:user_data]]; + }]; + + // In: CapsLock down + // Out: + last_handled = FALSE; + [responder handleEvent:keyEvent(NSEventTypeFlagsChanged, 0x100, @"", @"", FALSE, kKeyCodeCapsLock) + callback:keyEventCallback]; + + EXPECT_EQ([events count], 0u); + EXPECT_EQ(last_handled, TRUE); +} + +// Press the CapsLock key when CapsLock state is desynchronized +TEST(FlutterEmbedderKeyResponderUnittests, SynchronizeCapsLockStateOnNormalKey) { + __block NSMutableArray* events = [[NSMutableArray alloc] init]; + __block BOOL last_handled = TRUE; + id keyEventCallback = ^(BOOL handled) { + last_handled = handled; + }; + FlutterKeyEvent* event; + + FlutterEmbedderKeyResponder* responder = [[FlutterEmbedderKeyResponder alloc] + initWithSendEvent:^(const FlutterKeyEvent& event, _Nullable FlutterKeyEventCallback callback, + _Nullable _VoidPtr user_data) { + [events addObject:[[TestKeyEvent alloc] initWithEvent:&event + callback:callback + userData:user_data]]; + }]; + + last_handled = FALSE; + [responder handleEvent:keyEvent(NSEventTypeKeyDown, 0x10100, @"A", @"a", FALSE, kKeyCodeKeyA) + callback:keyEventCallback]; + + EXPECT_EQ([events count], 3u); + + event = events[0].data; + EXPECT_EQ(event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(event->physical, kPhysicalCapsLock); + EXPECT_EQ(event->logical, kLogicalCapsLock); + EXPECT_STREQ(event->character, nullptr); + EXPECT_EQ(event->synthesized, true); + EXPECT_FALSE([events[0] hasCallback]); + + event = events[1].data; + EXPECT_EQ(event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(event->physical, kPhysicalCapsLock); + EXPECT_EQ(event->logical, kLogicalCapsLock); + EXPECT_STREQ(event->character, nullptr); + EXPECT_EQ(event->synthesized, true); + EXPECT_FALSE([events[1] hasCallback]); + + event = events[2].data; + EXPECT_EQ(event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(event->physical, kPhysicalKeyA); + EXPECT_EQ(event->logical, kLogicalKeyA); + EXPECT_STREQ(event->character, "A"); + EXPECT_EQ(event->synthesized, false); + EXPECT_TRUE([events[2] hasCallback]); + + EXPECT_EQ(last_handled, FALSE); + [[events lastObject] respond:TRUE]; + EXPECT_EQ(last_handled, TRUE); + + [events removeAllObjects]; +} + +} // namespace flutter::testing diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm b/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm index 68a710a3aa31b..517f6e30a3d36 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm @@ -440,6 +440,12 @@ - (void)sendPointerEvent:(const FlutterPointerEvent&)event { _embedderAPI.SendPointerEvent(_engine, &event, 1); } +- (void)sendKeyEvent:(const FlutterKeyEvent&)event + callback:(FlutterKeyEventCallback)callback + userData:(void*)userData { + _embedderAPI.SendKeyEvent(_engine, &event, callback, userData); +} + - (void)setSemanticsEnabled:(BOOL)enabled { if (_semanticsEnabled == enabled) { return; diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h b/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h index f13ea02e45c64..5d2176ea59098 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h @@ -47,6 +47,13 @@ */ - (void)sendPointerEvent:(const FlutterPointerEvent&)event; +/** + * Dispatches the given pointer event data to engine. + */ +- (void)sendKeyEvent:(const FlutterKeyEvent&)event + callback:(nullable FlutterKeyEventCallback)callback + userData:(nullable void*)userData; + /** * Registers an external texture with the given id. Returns YES on success. */ diff --git a/shell/platform/darwin/macos/framework/Source/FlutterIntermediateKeyResponder.h b/shell/platform/darwin/macos/framework/Source/FlutterIntermediateKeyResponder.h deleted file mode 100644 index 31113ec8dfef8..0000000000000 --- a/shell/platform/darwin/macos/framework/Source/FlutterIntermediateKeyResponder.h +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import - -/* - * An interface for a key responder that can declare itself as the final - * responder of the event, terminating the event propagation. - * - * It differs from an NSResponder in that it returns a boolean from the - * handleKeyUp and handleKeyDown calls, where true means it has handled the - * given event. - */ -@interface FlutterIntermediateKeyResponder : NSObject -/* - * Informs the receiver that the user has released a key. - * - * Default implementation returns NO. - */ -- (BOOL)handleKeyUp:(nonnull NSEvent*)event; -/* - * Informs the receiver that the user has pressed a key. - * - * Default implementation returns NO. - */ -- (BOOL)handleKeyDown:(nonnull NSEvent*)event; -@end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterIntermediateKeyResponder.mm b/shell/platform/darwin/macos/framework/Source/FlutterIntermediateKeyResponder.mm deleted file mode 100644 index 71dd8c87cd7e4..0000000000000 --- a/shell/platform/darwin/macos/framework/Source/FlutterIntermediateKeyResponder.mm +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterIntermediateKeyResponder.h" - -@implementation FlutterIntermediateKeyResponder { -} - -#pragma mark - Default key handling methods - -- (BOOL)handleKeyUp:(NSEvent*)event { - return NO; -} - -- (BOOL)handleKeyDown:(NSEvent*)event { - return NO; -} -@end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterKeyPrimaryResponder.h b/shell/platform/darwin/macos/framework/Source/FlutterKeyPrimaryResponder.h new file mode 100644 index 0000000000000..28cd4ddd72cd5 --- /dev/null +++ b/shell/platform/darwin/macos/framework/Source/FlutterKeyPrimaryResponder.h @@ -0,0 +1,27 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import + +typedef void (^FlutterAsyncKeyCallback)(BOOL handled); + +/** + * An interface for a responder that can process a key event and decides whether + * to handle an event asynchronously. + * + * To use this class, add it to a |FlutterKeyboardManager| with |addPrimaryResponder|. + */ +@protocol FlutterKeyPrimaryResponder + +/** + * Process the event. + * + * The |callback| should be called with a value that indicates whether the + * responder has handled the given event. The |callback| must be called exactly + * once, and can be called before the return of this method, or after. + */ +@required +- (void)handleEvent:(nonnull NSEvent*)event callback:(nonnull FlutterAsyncKeyCallback)callback; + +@end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterKeySecondaryResponder.h b/shell/platform/darwin/macos/framework/Source/FlutterKeySecondaryResponder.h new file mode 100644 index 0000000000000..e0d33522d2342 --- /dev/null +++ b/shell/platform/darwin/macos/framework/Source/FlutterKeySecondaryResponder.h @@ -0,0 +1,24 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import + +/** + * An interface for a responder that can process a key event and decides whether + * to handle an event synchronously. + * + * To use this class, add it to a |FlutterKeyboardManager| with + * |addSecondaryResponder|. + */ +@protocol FlutterKeySecondaryResponder +/** + * Informs the receiver that the user has interacted with a key. + * + * The return value indicates whether it has handled the given event. + * + * Default implementation returns NO. + */ +@required +- (BOOL)handleKeyEvent:(nonnull NSEvent*)event; +@end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.h b/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.h new file mode 100644 index 0000000000000..922f1ad0721f4 --- /dev/null +++ b/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.h @@ -0,0 +1,70 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterKeyPrimaryResponder.h" + +#import + +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterKeyPrimaryResponder.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterKeySecondaryResponder.h" + +/** + * A hub that manages how key events are dispatched to various Flutter key + * responders, and whether the event is propagated to the next NSResponder. + * + * This class manage one or more primary responders, as well as zero or more + * secondary responders. + * + * An event that is received by |handleEvent| is first dispatched to *all* + * primary resopnders. Each primary responder responds *ascynchronously* with a + * boolean, indicating whether it handles the event. + * + * An event that is not handled by any primary responders is then passed to to + * the first secondary responder (in the chronological order of addition), + * which responds *synchronously* with a boolean, indicating whether it handles + * the event. If not, the event is passed to the next secondary responder, and + * so on. + * + * If no responders handle the event, the event is then handed over to the + * owner's |nextResponder| if not nil, dispatching to method |keyDown|, + * |keyUp|, or |flagsChanged| depending on the event's type. If the + * |nextResponder| is nil, then the event will be propagated no further. + * + * Preventing primary responders from receiving events is not supported, + * because in reality this class will only support 2 hardcoded ones (channel + * and embedder), where the only purpose of supporting two is to support the + * legacy API (channel) during the deprecation window, after which the channel + * resopnder should be removed. + */ +@interface FlutterKeyboardManager : NSObject + +/** + * Create a manager by specifying the owner. + * + * The owner should be an object that handles the lifecycle of this instance. + * The |owner.nextResponder| can be nil, but if it isn't, it will be where the + * key events are propagated to if no responders handle the event. The owner + * is typically a |FlutterViewController|. + */ +- (nonnull instancetype)initWithOwner:(nonnull NSResponder*)weakOwner; + +/** + * Add a primary resopnder, which asynchronously decides whether to handle an + * event. + */ +- (void)addPrimaryResponder:(nonnull id)responder; + +/** + * Add a secondary responder, which synchronously decides whether to handle an + * event in order if no earlier responders handle. + */ +- (void)addSecondaryResponder:(nonnull id)responder; + +/** + * Dispatch a key event to all responders, and possibly the next |NSResponder| + * afterwards. + */ +- (void)handleEvent:(nonnull NSEvent*)event; + +@end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.mm b/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.mm new file mode 100644 index 0000000000000..cdb9704f96b0b --- /dev/null +++ b/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.mm @@ -0,0 +1,106 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.h" + +@interface FlutterKeyboardManager () + +/** + * The owner set by initWithOwner. + */ +@property(nonatomic, weak) NSResponder* owner; + +/** + * The primary responders added by addPrimaryResponder. + */ +@property(nonatomic) NSMutableArray>* primaryResponders; + +/** + * The secondary responders added by addSecondaryResponder. + */ +@property(nonatomic) NSMutableArray>* secondaryResponders; + +- (void)dispatchToSecondaryResponders:(NSEvent*)event; + +@end + +@implementation FlutterKeyboardManager + +- (nonnull instancetype)initWithOwner:(NSResponder*)weakOwner { + self = [super init]; + if (self != nil) { + _owner = weakOwner; + _primaryResponders = [[NSMutableArray alloc] init]; + _secondaryResponders = [[NSMutableArray alloc] init]; + } + return self; +} + +- (void)addPrimaryResponder:(nonnull id)responder { + [_primaryResponders addObject:responder]; +} + +- (void)addSecondaryResponder:(nonnull id)responder { + [_secondaryResponders addObject:responder]; +} + +- (void)handleEvent:(nonnull NSEvent*)event { + // Be sure to add a handling method in propagateKeyEvent if you allow more + // event types here. + if (event.type != NSEventTypeKeyDown && event.type != NSEventTypeKeyUp && + event.type != NSEventTypeFlagsChanged) { + return; + } + // Having no primary responders require extra logic, but since Flutter adds + // all primary responders in hard-code, this is a situation that Flutter will + // never meet. + NSAssert([_primaryResponders count] >= 0, @"At least one primary responder must be added."); + + __weak __typeof__(self) weakSelf = self; + __block int unreplied = [_primaryResponders count]; + __block BOOL anyHandled = false; + FlutterAsyncKeyCallback replyCallback = ^(BOOL handled) { + unreplied -= 1; + NSAssert(unreplied >= 0, @"More primary responders replied than possible."); + anyHandled = anyHandled || handled; + if (unreplied == 0 && !anyHandled) { + [weakSelf dispatchToSecondaryResponders:event]; + } + }; + + for (id responder in _primaryResponders) { + [responder handleEvent:event callback:replyCallback]; + } +} + +#pragma mark - Private + +- (void)dispatchToSecondaryResponders:(NSEvent*)event { + for (id responder in _secondaryResponders) { + if ([responder handleKeyEvent:event]) { + return; + } + } + switch (event.type) { + case NSEventTypeKeyDown: + if ([_owner.nextResponder respondsToSelector:@selector(keyDown:)]) { + [_owner.nextResponder keyDown:event]; + } + break; + case NSEventTypeKeyUp: + if ([_owner.nextResponder respondsToSelector:@selector(keyUp:)]) { + [_owner.nextResponder keyUp:event]; + } + break; + case NSEventTypeFlagsChanged: + if ([_owner.nextResponder respondsToSelector:@selector(flagsChanged:)]) { + [_owner.nextResponder flagsChanged:event]; + } + break; + default: + NSAssert(false, @"Unexpected key event type (got %lu).", event.type); + } +} + +@end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManagerUnittests.mm b/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManagerUnittests.mm new file mode 100644 index 0000000000000..3d0589dd1c4ff --- /dev/null +++ b/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManagerUnittests.mm @@ -0,0 +1,277 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import + +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.h" +#import "flutter/testing/testing.h" +#include "third_party/googletest/googletest/include/gtest/gtest.h" + +@interface FlutterKeyboardManagerUnittestsObjC : NSObject +- (bool)nextResponderShouldThrowOnKeyUp; +- (bool)singlePrimaryResponder; +- (bool)doublePrimaryResponder; +- (bool)singleSecondaryResponder; +- (bool)emptyNextResponder; +@end + +namespace flutter::testing { + +namespace { + +NSEvent* keyDownEvent(unsigned short keyCode) { + return [NSEvent keyEventWithType:NSEventTypeKeyDown + location:NSZeroPoint + modifierFlags:0x100 + timestamp:0 + windowNumber:0 + context:nil + characters:@"" + charactersIgnoringModifiers:@"" + isARepeat:NO + keyCode:keyCode]; +} + +NSEvent* keyUpEvent(unsigned short keyCode) { + return [NSEvent keyEventWithType:NSEventTypeKeyUp + location:NSZeroPoint + modifierFlags:0x100 + timestamp:0 + windowNumber:0 + context:nil + characters:@"" + charactersIgnoringModifiers:@"" + isARepeat:NO + keyCode:keyCode]; +} + +id checkKeyDownEvent(unsigned short keyCode) { + return [OCMArg checkWithBlock:^BOOL(id value) { + if (![value isKindOfClass:[NSEvent class]]) { + return NO; + } + NSEvent* event = value; + return event.keyCode == keyCode; + }]; +} + +NSResponder* mockOwnerWithDownOnlyNext() { + NSResponder* nextResponder = OCMStrictClassMock([NSResponder class]); + OCMStub([nextResponder keyDown:[OCMArg any]]).andDo(nil); + // The nextResponder is a strict mock and hasn't stubbed keyUp. + // An error will be thrown on keyUp. + + NSResponder* owner = OCMStrictClassMock([NSResponder class]); + OCMStub([owner nextResponder]).andReturn(nextResponder); + return owner; +} + +typedef void (^KeyCallbackSetter)(FlutterAsyncKeyCallback callback); +typedef BOOL (^BoolGetter)(); + +id mockPrimaryResponder(KeyCallbackSetter callbackSetter) { + id mock = + OCMStrictProtocolMock(@protocol(FlutterKeyPrimaryResponder)); + OCMStub([mock handleEvent:[OCMArg any] callback:[OCMArg any]]) + .andDo((^(NSInvocation* invocation) { + FlutterAsyncKeyCallback callback; + [invocation getArgument:&callback atIndex:3]; + callbackSetter(callback); + })); + return mock; +} + +id mockSecondaryResponder(BoolGetter resultGetter) { + id mock = + OCMStrictProtocolMock(@protocol(FlutterKeySecondaryResponder)); + OCMStub([mock handleKeyEvent:[OCMArg any]]).andDo((^(NSInvocation* invocation) { + BOOL result = resultGetter(); + [invocation setReturnValue:&result]; + })); + return mock; +} + +} // namespace + +TEST(FlutterKeyboardManagerUnittests, NextResponderShouldThrowOnKeyUp) { + ASSERT_TRUE([[FlutterKeyboardManagerUnittestsObjC alloc] nextResponderShouldThrowOnKeyUp]); +} + +TEST(FlutterKeyboardManagerUnittests, SinglePrimaryResponder) { + ASSERT_TRUE([[FlutterKeyboardManagerUnittestsObjC alloc] singlePrimaryResponder]); +} + +TEST(FlutterKeyboardManagerUnittests, DoublePrimaryResponder) { + ASSERT_TRUE([[FlutterKeyboardManagerUnittestsObjC alloc] doublePrimaryResponder]); +} + +TEST(FlutterKeyboardManagerUnittests, SingleFinalResponder) { + ASSERT_TRUE([[FlutterKeyboardManagerUnittestsObjC alloc] singleSecondaryResponder]); +} + +TEST(FlutterKeyboardManagerUnittests, EmptyNextResponder) { + ASSERT_TRUE([[FlutterKeyboardManagerUnittestsObjC alloc] emptyNextResponder]); +} + +} // namespace flutter::testing + +@implementation FlutterKeyboardManagerUnittestsObjC + +// Verify that the nextResponder returned from mockOwnerWithDownOnlyNext() +// throws exception when keyUp is called. +- (bool)nextResponderShouldThrowOnKeyUp { + NSResponder* owner = flutter::testing::mockOwnerWithDownOnlyNext(); + @try { + [owner.nextResponder keyUp:flutter::testing::keyUpEvent(0x50)]; + return false; + } @catch (...) { + return true; + } +} + +- (bool)singlePrimaryResponder { + NSResponder* owner = flutter::testing::mockOwnerWithDownOnlyNext(); + FlutterKeyboardManager* manager = [[FlutterKeyboardManager alloc] initWithOwner:owner]; + + __block NSMutableArray* callbacks = + [NSMutableArray array]; + [manager addPrimaryResponder:flutter::testing::mockPrimaryResponder( + ^(FlutterAsyncKeyCallback callback) { + [callbacks addObject:callback]; + })]; + + // Case: The responder reports FALSE + [manager handleEvent:flutter::testing::keyDownEvent(0x50)]; + EXPECT_EQ([callbacks count], 1u); + callbacks[0](FALSE); + OCMVerify([owner.nextResponder keyDown:flutter::testing::checkKeyDownEvent(0x50)]); + [callbacks removeAllObjects]; + + // Case: The responder reports TRUE + [manager handleEvent:flutter::testing::keyUpEvent(0x50)]; + EXPECT_EQ([callbacks count], 1u); + callbacks[0](TRUE); + // [owner.nextResponder keyUp:] should not be called, otherwise an error will be thrown. + + return true; +} + +- (bool)doublePrimaryResponder { + NSResponder* owner = flutter::testing::mockOwnerWithDownOnlyNext(); + FlutterKeyboardManager* manager = [[FlutterKeyboardManager alloc] initWithOwner:owner]; + + __block NSMutableArray* callbacks1 = + [NSMutableArray array]; + [manager addPrimaryResponder:flutter::testing::mockPrimaryResponder( + ^(FlutterAsyncKeyCallback callback) { + [callbacks1 addObject:callback]; + })]; + + __block NSMutableArray* callbacks2 = + [NSMutableArray array]; + [manager addPrimaryResponder:flutter::testing::mockPrimaryResponder( + ^(FlutterAsyncKeyCallback callback) { + [callbacks2 addObject:callback]; + })]; + + // Case: Both responder report TRUE. + [manager handleEvent:flutter::testing::keyUpEvent(0x50)]; + EXPECT_EQ([callbacks1 count], 1u); + EXPECT_EQ([callbacks2 count], 1u); + callbacks1[0](TRUE); + callbacks2[0](TRUE); + EXPECT_EQ([callbacks1 count], 1u); + EXPECT_EQ([callbacks2 count], 1u); + // [owner.nextResponder keyUp:] should not be called, otherwise an error will be thrown. + [callbacks1 removeAllObjects]; + [callbacks2 removeAllObjects]; + + // Case: One responder reports TRUE. + [manager handleEvent:flutter::testing::keyUpEvent(0x50)]; + EXPECT_EQ([callbacks1 count], 1u); + EXPECT_EQ([callbacks2 count], 1u); + callbacks1[0](FALSE); + callbacks2[0](TRUE); + EXPECT_EQ([callbacks1 count], 1u); + EXPECT_EQ([callbacks2 count], 1u); + // [owner.nextResponder keyUp:] should not be called, otherwise an error will be thrown. + [callbacks1 removeAllObjects]; + [callbacks2 removeAllObjects]; + + // Case: Both responders report FALSE. + [manager handleEvent:flutter::testing::keyDownEvent(0x50)]; + EXPECT_EQ([callbacks1 count], 1u); + EXPECT_EQ([callbacks2 count], 1u); + callbacks1[0](FALSE); + callbacks2[0](FALSE); + EXPECT_EQ([callbacks1 count], 1u); + EXPECT_EQ([callbacks2 count], 1u); + OCMVerify([owner.nextResponder keyDown:flutter::testing::checkKeyDownEvent(0x50)]); + [callbacks1 removeAllObjects]; + [callbacks2 removeAllObjects]; + + return true; +} + +- (bool)singleSecondaryResponder { + NSResponder* owner = flutter::testing::mockOwnerWithDownOnlyNext(); + FlutterKeyboardManager* manager = [[FlutterKeyboardManager alloc] initWithOwner:owner]; + + __block NSMutableArray* callbacks = + [NSMutableArray array]; + [manager addPrimaryResponder:flutter::testing::mockPrimaryResponder( + ^(FlutterAsyncKeyCallback callback) { + [callbacks addObject:callback]; + })]; + + __block BOOL nextResponse; + [manager addSecondaryResponder:flutter::testing::mockSecondaryResponder(^() { + return nextResponse; + })]; + + // Case: Primary responder responds TRUE. The event shouldn't be handled by + // the secondary responder. + nextResponse = FALSE; + [manager handleEvent:flutter::testing::keyUpEvent(0x50)]; + EXPECT_EQ([callbacks count], 1u); + callbacks[0](TRUE); + // [owner.nextResponder keyUp:] should not be called, otherwise an error will be thrown. + [callbacks removeAllObjects]; + + // Case: Primary responder responds FALSE. The secondary responder returns + // TRUE. + nextResponse = TRUE; + [manager handleEvent:flutter::testing::keyUpEvent(0x50)]; + EXPECT_EQ([callbacks count], 1u); + callbacks[0](FALSE); + // [owner.nextResponder keyUp:] should not be called, otherwise an error will be thrown. + [callbacks removeAllObjects]; + + // Case: Primary responder responds FALSE. The secondary responder returns FALSE. + nextResponse = FALSE; + [manager handleEvent:flutter::testing::keyDownEvent(0x50)]; + EXPECT_EQ([callbacks count], 1u); + callbacks[0](FALSE); + OCMVerify([owner.nextResponder keyDown:flutter::testing::checkKeyDownEvent(0x50)]); + [callbacks removeAllObjects]; + + return true; +} + +- (bool)emptyNextResponder { + NSResponder* owner = OCMStrictClassMock([NSResponder class]); + OCMStub([owner nextResponder]).andReturn(nil); + + FlutterKeyboardManager* manager = [[FlutterKeyboardManager alloc] initWithOwner:owner]; + + [manager addPrimaryResponder:flutter::testing::mockPrimaryResponder( + ^(FlutterAsyncKeyCallback callback) { + callback(FALSE); + })]; + // Passes if no error is thrown. + return true; +} + +@end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.h b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.h index f82be5568a283..7ce919d22e0b8 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.h @@ -6,7 +6,7 @@ #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterBinaryMessenger.h" #import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterViewController.h" -#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterIntermediateKeyResponder.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterKeySecondaryResponder.h" /** * A plugin to handle text input. @@ -17,7 +17,7 @@ * This is not an FlutterPlugin since it needs access to FlutterViewController internals, so needs * to be managed differently. */ -@interface FlutterTextInputPlugin : FlutterIntermediateKeyResponder +@interface FlutterTextInputPlugin : NSObject /** * Initializes a text input plugin that coordinates key event handling with |viewController|. diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm index 4161c6b2d137e..169269112ec37 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm @@ -85,6 +85,18 @@ @interface FlutterTextInputPlugin () */ @property(nonatomic, weak) FlutterViewController* flutterViewController; +/** + * Whether the text input is shown in the view. + * + * Defaults to TRUE on startup. + */ +@property(nonatomic) BOOL shown; + +/** + * The current state of the keyboard and pressed keys. + */ +@property(nonatomic) uint64_t previouslyPressedFlags; + /** * The affinity for the current cursor position. */ @@ -130,15 +142,16 @@ @implementation FlutterTextInputPlugin { - (instancetype)initWithViewController:(FlutterViewController*)viewController { self = [super init]; if (self != nil) { - _flutterViewController = viewController; _channel = [FlutterMethodChannel methodChannelWithName:kTextInputChannel binaryMessenger:viewController.engine.binaryMessenger codec:[FlutterJSONMethodCodec sharedInstance]]; + _shown = FALSE; __weak FlutterTextInputPlugin* weakSelf = self; [_channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) { [weakSelf handleMethodCall:call result:result]; }]; _textInputContext = [[NSTextInputContext alloc] initWithClient:self]; + _previouslyPressedFlags = 0; } return self; } @@ -169,10 +182,10 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { _activeModel = std::make_unique(); } } else if ([method isEqualToString:kShowMethod]) { - [self.flutterViewController addKeyResponder:self]; + _shown = TRUE; [_textInputContext activate]; } else if ([method isEqualToString:kHideMethod]) { - [self.flutterViewController removeKeyResponder:self]; + _shown = FALSE; [_textInputContext deactivate]; } else if ([method isEqualToString:kClearClientMethod]) { _clientID = nil; @@ -245,7 +258,7 @@ - (void)updateEditState { } #pragma mark - -#pragma mark FlutterIntermediateKeyResponder +#pragma mark FlutterKeySecondaryResponder /** * Handles key down events received from the view controller, responding TRUE if @@ -257,7 +270,15 @@ - (void)updateEditState { * mouse events. Additionally, processing both keyUp and keyDown results in duplicate * processing of the same keys. So for now, limit processing to just handleKeyDown. */ -- (BOOL)handleKeyDown:(NSEvent*)event { +- (BOOL)handleKeyEvent:(NSEvent*)event { + if (event.type == NSEventTypeKeyUp || + (event.type == NSEventTypeFlagsChanged && event.modifierFlags < _previouslyPressedFlags)) { + return NO; + } + _previouslyPressedFlags = event.modifierFlags; + if (!_shown) { + return NO; + } return [_textInputContext handleEvent:event]; } diff --git a/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm b/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm index 1381241623c93..64f86fee2aab8 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm @@ -8,7 +8,11 @@ #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterChannels.h" #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterCodecs.h" #import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterEngine.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterChannelKeyResponder.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponder.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterKeyPrimaryResponder.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterMetalRenderer.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterMouseCursorPlugin.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterOpenGLRenderer.h" @@ -69,17 +73,6 @@ void Reset() { } }; -/** - * State tracking for keyboard events, to adapt between the events coming from the system and the - * events that the embedding API expects. - */ -struct KeyboardState { - /** - * The last known pressed modifier flag keys. - */ - uint64_t previously_pressed_flags = 0; -}; - } // namespace #pragma mark - Private interface declaration. @@ -89,16 +82,6 @@ void Reset() { */ @interface FlutterViewController () -/** - * A list of additional responders to keyboard events. - * - * Keyboard events received by FlutterViewController are first dispatched to - * each additional responder in order. If any of them handle the event (by - * returning true), the event is not dispatched to later additional responders - * or to the nextResponder. - */ -@property(nonatomic) NSMutableOrderedSet* additionalKeyResponders; - /** * The tracking area used to generate hover events, if enabled. */ @@ -110,14 +93,14 @@ @interface FlutterViewController () @property(nonatomic) MouseState mouseState; /** - * The current state of the keyboard and pressed keys. + * Event monitor for keyUp events. */ -@property(nonatomic) KeyboardState keyboardState; +@property(nonatomic) id keyUpMonitor; /** - * Event monitor for keyUp events. + * TODO */ -@property(nonatomic) id keyUpMonitor; +@property(nonatomic) FlutterKeyboardManager* keyboardManager; /** * Starts running |engine|, including any initial setup. @@ -147,19 +130,6 @@ - (void)dispatchMouseEvent:(nonnull NSEvent*)event; */ - (void)dispatchMouseEvent:(nonnull NSEvent*)event phase:(FlutterPointerPhase)phase; -/** - * Sends |event| to all responders in additionalKeyResponders and then to the - * nextResponder if none of the additional responders handled the event. - */ -- (void)propagateKeyEvent:(NSEvent*)event ofType:(NSString*)type; - -/** - * Converts |event| to a key event channel message, and sends it to the engine to - * hand to the framework. Once the framework responds, if the event was not handled, - * propagates the event to any additional key responders. - */ -- (void)dispatchKeyEvent:(NSEvent*)event ofType:(NSString*)type; - /** * Initializes the KVO for user settings and passes the initial user settings to the engine. */ @@ -211,14 +181,6 @@ @implementation FlutterViewController { // The project to run in this controller's engine. FlutterDartProject* _project; - // The plugin used to handle text input. This is not an FlutterPlugin, so must be owned - // separately. - FlutterTextInputPlugin* _textInputPlugin; - - // A message channel for passing key events to the Flutter engine. This should be replaced with - // an embedding API; see Issue #47. - FlutterBasicMessageChannel* _keyEventChannel; - // A message channel for sending user settings to the flutter engine. FlutterBasicMessageChannel* _settingsChannel; @@ -237,7 +199,6 @@ static void CommonInit(FlutterViewController* controller) { project:controller->_project allowHeadlessExecution:NO]; } - controller->_additionalKeyResponders = [[NSMutableOrderedSet alloc] init]; controller->_mouseTrackingMode = FlutterMouseTrackingModeInKeyWindow; NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; @@ -359,14 +320,6 @@ - (FlutterView*)flutterView { return static_cast(self.view); } -- (void)addKeyResponder:(FlutterIntermediateKeyResponder*)responder { - [self.additionalKeyResponders addObject:responder]; -} - -- (void)removeKeyResponder:(FlutterIntermediateKeyResponder*)responder { - [self.additionalKeyResponders removeObject:responder]; -} - #pragma mark - Private methods - (BOOL)launchEngine { @@ -435,12 +388,26 @@ - (void)configureTrackingArea { } - (void)addInternalPlugins { + __weak FlutterViewController* weakSelf = self; [FlutterMouseCursorPlugin registerWithRegistrar:[self registrarForPlugin:@"mousecursor"]]; - _textInputPlugin = [[FlutterTextInputPlugin alloc] initWithViewController:self]; - _keyEventChannel = - [FlutterBasicMessageChannel messageChannelWithName:@"flutter/keyevent" - binaryMessenger:_engine.binaryMessenger - codec:[FlutterJSONMessageCodec sharedInstance]]; + _keyboardManager = [[FlutterKeyboardManager alloc] initWithOwner:weakSelf]; + [_keyboardManager addPrimaryResponder:[[FlutterEmbedderKeyResponder alloc] + initWithSendEvent:^(const FlutterKeyEvent& event, + FlutterKeyEventCallback callback, + void* userData) { + [weakSelf.engine sendKeyEvent:event + callback:callback + userData:userData]; + }]]; + [_keyboardManager + addPrimaryResponder:[[FlutterChannelKeyResponder alloc] + initWithChannel:[FlutterBasicMessageChannel + messageChannelWithName:@"flutter/keyevent" + binaryMessenger:_engine.binaryMessenger + codec:[FlutterJSONMessageCodec + sharedInstance]]]]; + [_keyboardManager + addSecondaryResponder:[[FlutterTextInputPlugin alloc] initWithViewController:self]]; _settingsChannel = [FlutterBasicMessageChannel messageChannelWithName:@"flutter/settings" binaryMessenger:_engine.binaryMessenger @@ -449,7 +416,6 @@ - (void)addInternalPlugins { [FlutterMethodChannel methodChannelWithName:@"flutter/platform" binaryMessenger:_engine.binaryMessenger codec:[FlutterJSONMethodCodec sharedInstance]]; - __weak FlutterViewController* weakSelf = self; [_platformChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) { [weakSelf handleMethodCall:call result:result]; }]; @@ -532,66 +498,6 @@ - (void)dispatchMouseEvent:(NSEvent*)event phase:(FlutterPointerPhase)phase { } } -- (void)propagateKeyEvent:(NSEvent*)event ofType:(NSString*)type { - if ([type isEqual:@"keydown"]) { - for (FlutterIntermediateKeyResponder* responder in self.additionalKeyResponders) { - if ([responder handleKeyDown:event]) { - return; - } - } - if ([self.nextResponder respondsToSelector:@selector(keyDown:)] && - event.type == NSEventTypeKeyDown) { - [self.nextResponder keyDown:event]; - } - } else if ([type isEqual:@"keyup"]) { - for (FlutterIntermediateKeyResponder* responder in self.additionalKeyResponders) { - if ([responder handleKeyUp:event]) { - return; - } - } - if ([self.nextResponder respondsToSelector:@selector(keyUp:)] && - event.type == NSEventTypeKeyUp) { - [self.nextResponder keyUp:event]; - } - } - if ([self.nextResponder respondsToSelector:@selector(flagsChanged:)] && - event.type == NSEventTypeFlagsChanged) { - [self.nextResponder flagsChanged:event]; - } -} - -- (void)dispatchKeyEvent:(NSEvent*)event ofType:(NSString*)type { - // Be sure to add a handler in propagateKeyEvent if you allow more event - // types here. - if (event.type != NSEventTypeKeyDown && event.type != NSEventTypeKeyUp && - event.type != NSEventTypeFlagsChanged) { - return; - } - NSMutableDictionary* keyMessage = [@{ - @"keymap" : @"macos", - @"type" : type, - @"keyCode" : @(event.keyCode), - @"modifiers" : @(event.modifierFlags), - } mutableCopy]; - // Calling these methods on any other type of event - // (e.g NSEventTypeFlagsChanged) will raise an exception. - if (event.type == NSEventTypeKeyDown || event.type == NSEventTypeKeyUp) { - keyMessage[@"characters"] = event.characters; - keyMessage[@"charactersIgnoringModifiers"] = event.charactersIgnoringModifiers; - } - __weak __typeof__(self) weakSelf = self; - FlutterReply replyHandler = ^(id _Nullable reply) { - if (!reply) { - return; - } - // Only re-dispatch the event to other responders if the framework didn't handle it. - if (![[reply valueForKey:@"handled"] boolValue]) { - [weakSelf propagateKeyEvent:event ofType:type]; - } - }; - [_keyEventChannel sendMessage:keyMessage reply:replyHandler]; -} - - (void)onAccessibilityStatusChanged:(NSNotification*)notification { if (!_engine) { return; @@ -694,20 +600,15 @@ - (BOOL)acceptsFirstResponder { } - (void)keyDown:(NSEvent*)event { - [self dispatchKeyEvent:event ofType:@"keydown"]; + [_keyboardManager handleEvent:event]; } - (void)keyUp:(NSEvent*)event { - [self dispatchKeyEvent:event ofType:@"keyup"]; + [_keyboardManager handleEvent:event]; } - (void)flagsChanged:(NSEvent*)event { - if (event.modifierFlags < _keyboardState.previously_pressed_flags) { - [self keyUp:event]; - } else { - [self keyDown:event]; - } - _keyboardState.previously_pressed_flags = event.modifierFlags; + [_keyboardManager handleEvent:event]; } - (void)mouseEntered:(NSEvent*)event { diff --git a/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTest.mm index f078c2a5a3937..fbf01cfbe7dd5 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTest.mm @@ -19,34 +19,24 @@ - (bool)testKeyEventsAreSentToFramework; - (bool)testKeyEventsArePropagatedIfNotHandled; - (bool)testKeyEventsAreNotPropagatedIfHandled; - (bool)testFlagsChangedEventsArePropagatedIfNotHandled; + ++ (void)respondFalseForSendEvent:(const FlutterKeyEvent&)event + callback:(nullable FlutterKeyEventCallback)callback + userData:(nullable void*)userData; @end namespace flutter::testing { -// Returns a mock FlutterViewController that is able to work in environments -// without a real pasteboard. -id mockViewController(NSString* pasteboardString) { - NSString* fixtures = @(testing::GetFixturesPath()); - FlutterDartProject* project = [[FlutterDartProject alloc] - initWithAssetsPath:fixtures - ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]]; - FlutterViewController* viewController = [[FlutterViewController alloc] initWithProject:project]; +namespace { - // Mock pasteboard so that this test will work in environments without a - // real pasteboard. - id pasteboardMock = OCMClassMock([NSPasteboard class]); - OCMExpect( // NOLINT(google-objc-avoid-throwing-exception) - [pasteboardMock stringForType:[OCMArg any]]) - .andDo(^(NSInvocation* invocation) { - NSString* returnValue = pasteboardString.length > 0 ? pasteboardString : nil; - [invocation setReturnValue:&returnValue]; - }); - id viewControllerMock = OCMPartialMock(viewController); - OCMStub( // NOLINT(google-objc-avoid-throwing-exception) - [viewControllerMock pasteboard]) - .andReturn(pasteboardMock); - return viewControllerMock; +NSResponder* mockResponder() { + NSResponder* mock = OCMStrictClassMock([NSResponder class]); + OCMStub([mock keyDown:[OCMArg any]]).andDo(nil); + OCMStub([mock keyUp:[OCMArg any]]).andDo(nil); + OCMStub([mock flagsChanged:[OCMArg any]]).andDo(nil); + return mock; } +} // namespace TEST(FlutterViewController, HasStringsWhenPasteboardEmpty) { // Mock FlutterViewController so that it behaves like the pasteboard is empty. @@ -114,6 +104,11 @@ - (bool)testKeyEventsAreSentToFramework { OCMStub( // NOLINT(google-objc-avoid-throwing-exception) [engineMock binaryMessenger]) .andReturn(binaryMessengerMock); + OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:FlutterKeyEvent {} + callback:nil + userData:nil]) + .andCall([FlutterViewControllerTestObjC class], + @selector(respondFalseForSendEvent:callback:userData:)); FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock nibName:@"" bundle:nil]; @@ -147,11 +142,16 @@ - (bool)testKeyEventsArePropagatedIfNotHandled { OCMStub( // NOLINT(google-objc-avoid-throwing-exception) [engineMock binaryMessenger]) .andReturn(binaryMessengerMock); + OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:FlutterKeyEvent {} + callback:nil + userData:nil]) + .andCall([FlutterViewControllerTestObjC class], + @selector(respondFalseForSendEvent:callback:userData:)); FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock nibName:@"" bundle:nil]; - id responderMock = OCMClassMock([FlutterIntermediateKeyResponder class]); - [viewController addKeyResponder:responderMock]; + id responderMock = flutter::testing::mockResponder(); + viewController.nextResponder = responderMock; NSDictionary* expectedEvent = @{ @"keymap" : @"macos", @"type" : @"keydown", @@ -180,7 +180,7 @@ - (bool)testKeyEventsArePropagatedIfNotHandled { [viewController keyDown:event]; @try { OCMVerify( // NOLINT(google-objc-avoid-throwing-exception) - [responderMock handleKeyDown:[OCMArg any]]); + [responderMock keyDown:[OCMArg any]]); OCMVerify( // NOLINT(google-objc-avoid-throwing-exception) [binaryMessengerMock sendOnChannel:@"flutter/keyevent" message:encodedKeyEvent @@ -197,11 +197,16 @@ - (bool)testFlagsChangedEventsArePropagatedIfNotHandled { OCMStub( // NOLINT(google-objc-avoid-throwing-exception) [engineMock binaryMessenger]) .andReturn(binaryMessengerMock); + OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:FlutterKeyEvent {} + callback:nil + userData:nil]) + .andCall([FlutterViewControllerTestObjC class], + @selector(respondFalseForSendEvent:callback:userData:)); FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock nibName:@"" bundle:nil]; - id responderMock = OCMClassMock([FlutterIntermediateKeyResponder class]); - [viewController addKeyResponder:responderMock]; + id responderMock = flutter::testing::mockResponder(); + viewController.nextResponder = responderMock; NSDictionary* expectedEvent = @{ @"keymap" : @"macos", @"type" : @"keydown", @@ -232,7 +237,8 @@ - (bool)testFlagsChangedEventsArePropagatedIfNotHandled { [binaryMessengerMock sendOnChannel:@"flutter/keyevent" message:encodedKeyEvent binaryReply:[OCMArg any]]); - } @catch (...) { + } @catch (NSException* e) { + NSLog(@"%@", e.reason); return false; } return true; @@ -244,11 +250,16 @@ - (bool)testKeyEventsAreNotPropagatedIfHandled { OCMStub( // NOLINT(google-objc-avoid-throwing-exception) [engineMock binaryMessenger]) .andReturn(binaryMessengerMock); + OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:FlutterKeyEvent {} + callback:nil + userData:nil]) + .andCall([FlutterViewControllerTestObjC class], + @selector(respondFalseForSendEvent:callback:userData:)); FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock nibName:@"" bundle:nil]; - id responderMock = OCMClassMock([FlutterIntermediateKeyResponder class]); - [viewController addKeyResponder:responderMock]; + id responderMock = flutter::testing::mockResponder(); + viewController.nextResponder = responderMock; NSDictionary* expectedEvent = @{ @"keymap" : @"macos", @"type" : @"keydown", @@ -277,7 +288,7 @@ - (bool)testKeyEventsAreNotPropagatedIfHandled { [viewController keyDown:event]; @try { OCMVerify( // NOLINT(google-objc-avoid-throwing-exception) - never(), [responderMock handleKeyDown:[OCMArg any]]); + never(), [responderMock keyDown:[OCMArg any]]); OCMVerify( // NOLINT(google-objc-avoid-throwing-exception) [binaryMessengerMock sendOnChannel:@"flutter/keyevent" message:encodedKeyEvent @@ -288,4 +299,11 @@ - (bool)testKeyEventsAreNotPropagatedIfHandled { return true; } ++ (void)respondFalseForSendEvent:(const FlutterKeyEvent&)event + callback:(nullable FlutterKeyEventCallback)callback + userData:(nullable void*)userData { + if (callback != nullptr) + callback(false, userData); +} + @end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h b/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h index acef9f23bae5e..a32f2f4f96764 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h @@ -4,7 +4,7 @@ #import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterViewController.h" -#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterIntermediateKeyResponder.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterKeySecondaryResponder.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterView.h" @interface FlutterViewController () @@ -17,17 +17,6 @@ */ @property(nonatomic, readonly, nonnull) NSPasteboard* pasteboard; -/** - * Adds an intermediate responder for keyboard events. Key up and key down events are forwarded to - * all added responders, and they either handle the keys or not. - */ -- (void)addKeyResponder:(nonnull FlutterIntermediateKeyResponder*)responder; - -/** - * Removes an intermediate responder for keyboard events. - */ -- (void)removeKeyResponder:(nonnull FlutterIntermediateKeyResponder*)responder; - /** * Initializes this FlutterViewController with the specified `FlutterEngine`. * @@ -40,4 +29,6 @@ - (nonnull instancetype)initWithEngine:(nonnull FlutterEngine*)engine nibName:(nullable NSString*)nibName bundle:(nullable NSBundle*)nibBundle NS_DESIGNATED_INITIALIZER; + +#pragma mark - Private interface declaration. @end diff --git a/shell/platform/darwin/macos/framework/Source/KeyCodeMap.mm b/shell/platform/darwin/macos/framework/Source/KeyCodeMap.mm new file mode 100644 index 0000000000000..4c8a0ac4fc3e7 --- /dev/null +++ b/shell/platform/darwin/macos/framework/Source/KeyCodeMap.mm @@ -0,0 +1,285 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import +#include "./KeyCodeMap_internal.h" + +// DO NOT EDIT -- DO NOT EDIT -- DO NOT EDIT +// This file is generated by flutter/flutter@dev/tools/gen_keycodes/bin/gen_keycodes.dart and +// should not be edited directly. +// +// Edit the template dev/tools/gen_keycodes/data/keyboard_map_macos_cc.tmpl instead. +// See dev/tools/gen_keycodes/README.md for more information. + +/** + * Mask for the 32-bit value portion of the key code. + * + * This is used by platform-specific code to generate Flutter key codes. + */ +const uint64_t kValueMask = 0x000FFFFFFFF; + +/** + * Mask for the platform prefix portion of the key code. + * + * This is used by platform-specific code to generate Flutter key codes. + */ +const uint64_t kPlatformMask = 0x0FF00000000; + +/** + * The code prefix for keys which have a Unicode representation. + * + * This is used by platform-specific code to generate Flutter key codes. + */ +const uint64_t kUnicodePlane = 0x00000000000; + +/** + * Mask for the auto-generated bit portion of the key code. + * + * This is used by platform-specific code to generate new Flutter key codes for + * keys which are not recognized. + */ +const uint64_t kAutogeneratedMask = 0x10000000000; + +/** + * Mask for the synonym pseudo-keys generated for keys which appear in more than + * one place on the keyboard. + * + * IDs in this range are used to represent keys which appear in multiple places + * on the keyboard, such as the SHIFT, ALT, CTRL, and numeric keypad keys. These + * key codes will never be generated by the key event system, but may be used in + * key maps to represent the union of all the keys of each type in order to + * match them. + * + * To look up the synonyms that are defined, look in the [synonyms] map. + */ +const uint64_t kSynonymMask = 0x20000000000; + +/** + * The code prefix for keys which do not have a Unicode representation. + * + * This is used by platform-specific code to generate Flutter key codes using + * HID Usage codes. + */ +const uint64_t kHidPlane = 0x00100000000; + +const NSDictionary* keyCodeToPhysicalKey = @{ + @0x00000000 : @0x00070004, // keyA + @0x0000000b : @0x00070005, // keyB + @0x00000008 : @0x00070006, // keyC + @0x00000002 : @0x00070007, // keyD + @0x0000000e : @0x00070008, // keyE + @0x00000003 : @0x00070009, // keyF + @0x00000005 : @0x0007000a, // keyG + @0x00000004 : @0x0007000b, // keyH + @0x00000022 : @0x0007000c, // keyI + @0x00000026 : @0x0007000d, // keyJ + @0x00000028 : @0x0007000e, // keyK + @0x00000025 : @0x0007000f, // keyL + @0x0000002e : @0x00070010, // keyM + @0x0000002d : @0x00070011, // keyN + @0x0000001f : @0x00070012, // keyO + @0x00000023 : @0x00070013, // keyP + @0x0000000c : @0x00070014, // keyQ + @0x0000000f : @0x00070015, // keyR + @0x00000001 : @0x00070016, // keyS + @0x00000011 : @0x00070017, // keyT + @0x00000020 : @0x00070018, // keyU + @0x00000009 : @0x00070019, // keyV + @0x0000000d : @0x0007001a, // keyW + @0x00000007 : @0x0007001b, // keyX + @0x00000010 : @0x0007001c, // keyY + @0x00000006 : @0x0007001d, // keyZ + @0x00000012 : @0x0007001e, // digit1 + @0x00000013 : @0x0007001f, // digit2 + @0x00000014 : @0x00070020, // digit3 + @0x00000015 : @0x00070021, // digit4 + @0x00000017 : @0x00070022, // digit5 + @0x00000016 : @0x00070023, // digit6 + @0x0000001a : @0x00070024, // digit7 + @0x0000001c : @0x00070025, // digit8 + @0x00000019 : @0x00070026, // digit9 + @0x0000001d : @0x00070027, // digit0 + @0x00000024 : @0x00070028, // enter + @0x00000035 : @0x00070029, // escape + @0x00000033 : @0x0007002a, // backspace + @0x00000030 : @0x0007002b, // tab + @0x00000031 : @0x0007002c, // space + @0x0000001b : @0x0007002d, // minus + @0x00000018 : @0x0007002e, // equal + @0x00000021 : @0x0007002f, // bracketLeft + @0x0000001e : @0x00070030, // bracketRight + @0x0000002a : @0x00070031, // backslash + @0x00000029 : @0x00070033, // semicolon + @0x00000027 : @0x00070034, // quote + @0x00000032 : @0x00070035, // backquote + @0x0000002b : @0x00070036, // comma + @0x0000002f : @0x00070037, // period + @0x0000002c : @0x00070038, // slash + @0x00000039 : @0x00070039, // capsLock + @0x0000007a : @0x0007003a, // f1 + @0x00000078 : @0x0007003b, // f2 + @0x00000063 : @0x0007003c, // f3 + @0x00000076 : @0x0007003d, // f4 + @0x00000060 : @0x0007003e, // f5 + @0x00000061 : @0x0007003f, // f6 + @0x00000062 : @0x00070040, // f7 + @0x00000064 : @0x00070041, // f8 + @0x00000065 : @0x00070042, // f9 + @0x0000006d : @0x00070043, // f10 + @0x00000067 : @0x00070044, // f11 + @0x0000006f : @0x00070045, // f12 + @0x00000072 : @0x00070049, // insert + @0x00000073 : @0x0007004a, // home + @0x00000074 : @0x0007004b, // pageUp + @0x00000075 : @0x0007004c, // delete + @0x00000077 : @0x0007004d, // end + @0x00000079 : @0x0007004e, // pageDown + @0x0000007c : @0x0007004f, // arrowRight + @0x0000007b : @0x00070050, // arrowLeft + @0x0000007d : @0x00070051, // arrowDown + @0x0000007e : @0x00070052, // arrowUp + @0x00000047 : @0x00070053, // numLock + @0x0000004b : @0x00070054, // numpadDivide + @0x00000043 : @0x00070055, // numpadMultiply + @0x0000004e : @0x00070056, // numpadSubtract + @0x00000045 : @0x00070057, // numpadAdd + @0x0000004c : @0x00070058, // numpadEnter + @0x00000053 : @0x00070059, // numpad1 + @0x00000054 : @0x0007005a, // numpad2 + @0x00000055 : @0x0007005b, // numpad3 + @0x00000056 : @0x0007005c, // numpad4 + @0x00000057 : @0x0007005d, // numpad5 + @0x00000058 : @0x0007005e, // numpad6 + @0x00000059 : @0x0007005f, // numpad7 + @0x0000005b : @0x00070060, // numpad8 + @0x0000005c : @0x00070061, // numpad9 + @0x00000052 : @0x00070062, // numpad0 + @0x00000041 : @0x00070063, // numpadDecimal + @0x0000000a : @0x00070064, // intlBackslash + @0x0000006e : @0x00070065, // contextMenu + @0x00000051 : @0x00070067, // numpadEqual + @0x00000069 : @0x00070068, // f13 + @0x0000006b : @0x00070069, // f14 + @0x00000071 : @0x0007006a, // f15 + @0x0000006a : @0x0007006b, // f16 + @0x00000040 : @0x0007006c, // f17 + @0x0000004f : @0x0007006d, // f18 + @0x00000050 : @0x0007006e, // f19 + @0x0000005a : @0x0007006f, // f20 + @0x0000004a : @0x0007007f, // audioVolumeMute + @0x00000048 : @0x00070080, // audioVolumeUp + @0x00000049 : @0x00070081, // audioVolumeDown + @0x0000005f : @0x00070085, // numpadComma + @0x0000005e : @0x00070087, // intlRo + @0x0000005d : @0x00070089, // intlYen + @0x00000068 : @0x00070090, // lang1 + @0x00000066 : @0x00070091, // lang2 + @0x0000003b : @0x000700e0, // controlLeft + @0x00000038 : @0x000700e1, // shiftLeft + @0x0000003a : @0x000700e2, // altLeft + @0x00000037 : @0x000700e3, // metaLeft + @0x0000003e : @0x000700e4, // controlRight + @0x0000003c : @0x000700e5, // shiftRight + @0x0000003d : @0x000700e6, // altRight + @0x00000036 : @0x000700e7, // metaRight + @0x0000003f : @0x00000012, // fn +}; + +const NSDictionary* keyCodeToLogicalKey = @{ + @0x00000033 : @0x0000000008, // Backspace + @0x00000035 : @0x000000001b, // Escape + @0x00000075 : @0x000000007f, // Delete + @0x00000039 : @0x0000000104, // CapsLock + @0x0000003f : @0x0000000106, // Fn + @0x00000047 : @0x000000010a, // NumLock + @0x0000007d : @0x0000000301, // ArrowDown + @0x0000007b : @0x0000000302, // ArrowLeft + @0x0000007c : @0x0000000303, // ArrowRight + @0x0000007e : @0x0000000304, // ArrowUp + @0x00000077 : @0x0000000305, // End + @0x00000073 : @0x0000000306, // Home + @0x00000079 : @0x0000000307, // PageDown + @0x00000074 : @0x0000000308, // PageUp + @0x00000072 : @0x0000000407, // Insert + @0x0000006e : @0x0000000505, // ContextMenu + @0x0000007a : @0x0000000801, // F1 + @0x00000078 : @0x0000000802, // F2 + @0x00000063 : @0x0000000803, // F3 + @0x00000076 : @0x0000000804, // F4 + @0x00000060 : @0x0000000805, // F5 + @0x00000061 : @0x0000000806, // F6 + @0x00000062 : @0x0000000807, // F7 + @0x00000064 : @0x0000000808, // F8 + @0x00000065 : @0x0000000809, // F9 + @0x0000006d : @0x000000080a, // F10 + @0x00000067 : @0x000000080b, // F11 + @0x0000006f : @0x000000080c, // F12 + @0x00000069 : @0x000000080d, // F13 + @0x0000006b : @0x000000080e, // F14 + @0x00000071 : @0x000000080f, // F15 + @0x0000006a : @0x0000000810, // F16 + @0x00000040 : @0x0000000811, // F17 + @0x0000004f : @0x0000000812, // F18 + @0x00000050 : @0x0000000813, // F19 + @0x0000005a : @0x0000000814, // F20 + @0x00000049 : @0x0000000a0f, // AudioVolumeDown + @0x00000048 : @0x0000000a10, // AudioVolumeUp + @0x0000004a : @0x0000000a11, // AudioVolumeMute + @0x0000005e : @0x0100070087, // IntlRo + @0x0000005d : @0x0100070089, // IntlYen + @0x00000068 : @0x0100070090, // Lang1 + @0x00000066 : @0x0100070091, // Lang2 + @0x0000004c : @0x020000000d, // NumpadEnter + @0x00000043 : @0x020000002a, // NumpadMultiply + @0x00000045 : @0x020000002b, // NumpadAdd + @0x0000005f : @0x020000002c, // NumpadComma + @0x0000004e : @0x020000002d, // NumpadSubtract + @0x00000041 : @0x020000002e, // NumpadDecimal + @0x0000004b : @0x020000002f, // NumpadDivide + @0x00000052 : @0x0200000030, // Numpad0 + @0x00000053 : @0x0200000031, // Numpad1 + @0x00000054 : @0x0200000032, // Numpad2 + @0x00000055 : @0x0200000033, // Numpad3 + @0x00000056 : @0x0200000034, // Numpad4 + @0x00000057 : @0x0200000035, // Numpad5 + @0x00000058 : @0x0200000036, // Numpad6 + @0x00000059 : @0x0200000037, // Numpad7 + @0x0000005b : @0x0200000038, // Numpad8 + @0x0000005c : @0x0200000039, // Numpad9 + @0x00000051 : @0x020000003d, // NumpadEqual + @0x0000003a : @0x0300000102, // AltLeft + @0x0000003b : @0x0300000105, // ControlLeft + @0x00000037 : @0x0300000109, // MetaLeft + @0x00000038 : @0x030000010d, // ShiftLeft + @0x0000003d : @0x0400000102, // AltRight + @0x0000003e : @0x0400000105, // ControlRight + @0x00000036 : @0x0400000109, // MetaRight + @0x0000003c : @0x040000010d, // ShiftRight +}; + +const NSDictionary* keyCodeToModifierFlag = @{ + @0x00000038 : @(kModifierFlagShiftLeft), + @0x0000003c : @(kModifierFlagShiftRight), + @0x0000003b : @(kModifierFlagControlLeft), + @0x0000003e : @(kModifierFlagControlRight), + @0x0000003a : @(kModifierFlagAltLeft), + @0x0000003d : @(kModifierFlagAltRight), + @0x00000037 : @(kModifierFlagMetaLeft), + @0x00000036 : @(kModifierFlagMetaRight), +}; + +const NSDictionary* modifierFlagToKeyCode = @{ + @(kModifierFlagShiftLeft) : @0x00000038, + @(kModifierFlagShiftRight) : @0x0000003c, + @(kModifierFlagControlLeft) : @0x0000003b, + @(kModifierFlagControlRight) : @0x0000003e, + @(kModifierFlagAltLeft) : @0x0000003a, + @(kModifierFlagAltRight) : @0x0000003d, + @(kModifierFlagMetaLeft) : @0x00000037, + @(kModifierFlagMetaRight) : @0x00000036, +}; + +const uint64_t kCapsLockPhysicalKey = 0x00070039; +const uint64_t kCapsLockLogicalKey = 0x00000104; diff --git a/shell/platform/darwin/macos/framework/Source/KeyCodeMap_internal.h b/shell/platform/darwin/macos/framework/Source/KeyCodeMap_internal.h new file mode 100644 index 0000000000000..0e523155559b0 --- /dev/null +++ b/shell/platform/darwin/macos/framework/Source/KeyCodeMap_internal.h @@ -0,0 +1,77 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * Maps macOS-specific key code values representing |PhysicalKeyboardKey|. + * + * MacOS doesn't provide a scan code, but a virtual keycode to represent a physical key. + */ +extern const NSDictionary* keyCodeToPhysicalKey; + +/** + * A map from macOS key codes to Flutter's logical key values. + * + * This is used to derive logical keys that can't or shouldn't be derived from + * |charactersIgnoringModifiers|. + */ +extern const NSDictionary* keyCodeToLogicalKey; + +// Several mask constants. See KeyCodeMap.mm for their descriptions. + +extern const uint64_t kValueMask; +extern const uint64_t kPlatformMask; +extern const uint64_t kUnicodePlane; +extern const uint64_t kHidPlane; +extern const uint64_t kAutogeneratedMask; +extern const uint64_t kSynonymMask; + +/** + * The code prefix for keys from macOS which do not have a Unicode + * representation. + */ +static const uint64_t kMacosPlane = 0x00500000000; + +/** + * Map |NSEvent.keyCode| to its corresponding bitmask of NSEventModifierFlags. + * + * This does not include CapsLock, for it is handled specially. + */ +extern const NSDictionary* keyCodeToModifierFlag; + +/** + * Map a bit of bitmask of NSEventModifierFlags to its corresponding + * |NSEvent.keyCode|. + * + * This does not include CapsLock, for it is handled specially. + */ +extern const NSDictionary* modifierFlagToKeyCode; + +/** + * The physical key for CapsLock, which needs special handling. + */ +extern const uint64_t kCapsLockPhysicalKey; + +/** + * The logical key for CapsLock, which needs special handling. + */ +extern const uint64_t kCapsLockLogicalKey; + +/** + * Bits in |NSEvent.modifierFlags| indicating whether a modifier key is pressed. + * + * These constants are not written in the official documentation, but derived + * from experiments. This is currently the only way to know whether a one-side + * modifier key (such as ShiftLeft) is pressed, instead of the general combined + * modifier state (such as Shift). + */ +typedef enum { + kModifierFlagControlLeft = 0x1, + kModifierFlagShiftLeft = 0x2, + kModifierFlagShiftRight = 0x4, + kModifierFlagMetaLeft = 0x8, + kModifierFlagMetaRight = 0x10, + kModifierFlagAltLeft = 0x20, + kModifierFlagAltRight = 0x40, + kModifierFlagControlRight = 0x200, +} ModifierFlag;