diff --git a/shell/platform/darwin/ios/framework/Source/FlutterEmbedderKeyResponder.mm b/shell/platform/darwin/ios/framework/Source/FlutterEmbedderKeyResponder.mm index 2bb10c182d84f..2ba01e2d5917a 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterEmbedderKeyResponder.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterEmbedderKeyResponder.mm @@ -146,10 +146,17 @@ static uint64_t toLower(uint64_t n) { /** * Returns the logical key of a KeyUp or KeyDown event. * + * The `maybeSpecialKey` is a nullable integer, and if not nil, indicates + * that the event key is a special key as defined by `specialKeyMapping`, + * and is the corresponding logical key. + * * For modifier keys, use GetLogicalKeyForModifier. */ -static uint64_t GetLogicalKeyForEvent(FlutterUIPressProxy* press, uint64_t physicalKey) +static uint64_t GetLogicalKeyForEvent(FlutterUIPressProxy* press, NSNumber* maybeSpecialKey) API_AVAILABLE(ios(13.4)) { + if (maybeSpecialKey != nil) { + return [maybeSpecialKey unsignedLongLongValue]; + } // Look to see if the keyCode can be mapped from keycode. auto fromKeyCode = keyCodeToLogicalKey.find(press.key.keyCode); if (fromKeyCode != keyCodeToLogicalKey.end()) { @@ -670,7 +677,11 @@ - (void)handlePressBegin:(nonnull FlutterUIPressProxy*)press return; } uint64_t physicalKey = GetPhysicalKeyForKeyCode(press.key.keyCode); - uint64_t logicalKey = GetLogicalKeyForEvent(press, physicalKey); + // Some unprintable keys on iOS have literal names on their key label, such as + // @"UIKeyInputEscape". They are called the "special keys" and have predefined + // logical keys and empty characters. + NSNumber* specialKey = [specialKeyMapping objectForKey:press.key.charactersIgnoringModifiers]; + uint64_t logicalKey = GetLogicalKeyForEvent(press, specialKey); [self synchronizeModifiers:press]; NSNumber* pressedLogicalKey = nil; @@ -697,7 +708,8 @@ - (void)handlePressBegin:(nonnull FlutterUIPressProxy*)press .type = kFlutterKeyEventTypeDown, .physical = physicalKey, .logical = pressedLogicalKey == nil ? logicalKey : [pressedLogicalKey unsignedLongLongValue], - .character = getEventCharacters(press.key.characters, press.key.keyCode), + .character = + specialKey != nil ? nil : getEventCharacters(press.key.characters, press.key.keyCode), .synthesized = false, }; [self sendPrimaryFlutterEvent:flutterEvent callback:callback]; diff --git a/shell/platform/darwin/ios/framework/Source/FlutterEmbedderKeyResponderTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterEmbedderKeyResponderTest.mm index a1b096b8096f1..f29d3b2d13664 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterEmbedderKeyResponderTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterEmbedderKeyResponderTest.mm @@ -75,6 +75,8 @@ - (void)dealloc { API_AVAILABLE(ios(13.4)) constexpr UIKeyboardHIDUsage kKeyCodeKeyA = (UIKeyboardHIDUsage)0x04; API_AVAILABLE(ios(13.4)) +constexpr UIKeyboardHIDUsage kKeyCodePeriod = (UIKeyboardHIDUsage)0x37; +API_AVAILABLE(ios(13.4)) constexpr UIKeyboardHIDUsage kKeyCodeKeyW = (UIKeyboardHIDUsage)0x1a; API_AVAILABLE(ios(13.4)) constexpr UIKeyboardHIDUsage kKeyCodeShiftLeft = (UIKeyboardHIDUsage)0xe1; @@ -87,6 +89,8 @@ - (void)dealloc { API_AVAILABLE(ios(13.4)) constexpr UIKeyboardHIDUsage kKeyCodeF1 = (UIKeyboardHIDUsage)0x3a; API_AVAILABLE(ios(13.4)) +constexpr UIKeyboardHIDUsage kKeyCodeCommandLeft = (UIKeyboardHIDUsage)0xe3; +API_AVAILABLE(ios(13.4)) constexpr UIKeyboardHIDUsage kKeyCodeAltRight = (UIKeyboardHIDUsage)0xe6; API_AVAILABLE(ios(13.4)) constexpr UIKeyboardHIDUsage kKeyCodeEject = (UIKeyboardHIDUsage)0xb8; @@ -941,4 +945,69 @@ - (void)testSynchronizeCapsLockStateOnNormalKey API_AVAILABLE(ios(13.4)) { [events removeAllObjects]; } +// Press Cmd-. should correctly result in an Escape event. +- (void)testCommandPeriodKey API_AVAILABLE(ios(13.4)) { + __block NSMutableArray* events = [[NSMutableArray alloc] init]; + id keyEventCallback = ^(BOOL 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:nil userData:nil]]; + callback(true, user_data); + }]; + + // MetaLeft down. + [responder handlePress:keyDownEvent(kKeyCodeCommandLeft, kModifierFlagMetaAny, 123.0f, "", "") + callback:keyEventCallback]; + XCTAssertEqual([events count], 1u); + event = events[0].data; + XCTAssertEqual(event->type, kFlutterKeyEventTypeDown); + XCTAssertEqual(event->physical, kPhysicalMetaLeft); + XCTAssertEqual(event->logical, kLogicalMetaLeft); + XCTAssertEqual(event->character, nullptr); + XCTAssertEqual(event->synthesized, false); + [events removeAllObjects]; + + // Period down, which is logically Escape. + [responder handlePress:keyDownEvent(kKeyCodePeriod, kModifierFlagMetaAny, 123.0f, + "UIKeyInputEscape", "UIKeyInputEscape") + callback:keyEventCallback]; + XCTAssertEqual([events count], 1u); + event = events[0].data; + XCTAssertEqual(event->type, kFlutterKeyEventTypeDown); + XCTAssertEqual(event->physical, kPhysicalPeriod); + XCTAssertEqual(event->logical, kLogicalEscape); + XCTAssertEqual(event->character, nullptr); + XCTAssertEqual(event->synthesized, false); + [events removeAllObjects]; + + // Period up, which unconventionally has characters. + [responder handlePress:keyUpEvent(kKeyCodePeriod, kModifierFlagMetaAny, 123.0f, + "UIKeyInputEscape", "UIKeyInputEscape") + callback:keyEventCallback]; + XCTAssertEqual([events count], 1u); + event = events[0].data; + XCTAssertEqual(event->type, kFlutterKeyEventTypeUp); + XCTAssertEqual(event->physical, kPhysicalPeriod); + XCTAssertEqual(event->logical, kLogicalEscape); + XCTAssertEqual(event->character, nullptr); + XCTAssertEqual(event->synthesized, false); + [events removeAllObjects]; + + // MetaLeft up. + [responder handlePress:keyUpEvent(kKeyCodeCommandLeft, kModifierFlagMetaAny, 123.0f, "", "") + callback:keyEventCallback]; + XCTAssertEqual([events count], 1u); + event = events[0].data; + XCTAssertEqual(event->type, kFlutterKeyEventTypeUp); + XCTAssertEqual(event->physical, kPhysicalMetaLeft); + XCTAssertEqual(event->logical, kLogicalMetaLeft); + XCTAssertEqual(event->character, nullptr); + XCTAssertEqual(event->synthesized, false); + [events removeAllObjects]; +} + @end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterFakeKeyEvents.h b/shell/platform/darwin/ios/framework/Source/FlutterFakeKeyEvents.h index ddf5facb81085..387ec5a7bb6af 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterFakeKeyEvents.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterFakeKeyEvents.h @@ -56,7 +56,10 @@ FlutterUIPressProxy* keyDownEvent(UIKeyboardHIDUsage keyCode, FlutterUIPressProxy* keyUpEvent(UIKeyboardHIDUsage keyCode, UIKeyModifierFlags modifierFlags = 0x0, - NSTimeInterval timestamp = 0.0f) API_AVAILABLE(ios(13.4)); + NSTimeInterval timestamp = 0.0f, + const char* characters = "", + const char* charactersIgnoringModifiers = "") + API_AVAILABLE(ios(13.4)); FlutterUIPressProxy* keyEventWithPhase(UIPressPhase phase, UIKeyboardHIDUsage keyCode, diff --git a/shell/platform/darwin/ios/framework/Source/FlutterFakeKeyEvents.mm b/shell/platform/darwin/ios/framework/Source/FlutterFakeKeyEvents.mm index 6cd75497e6d0c..461c2c4662ac5 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterFakeKeyEvents.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterFakeKeyEvents.mm @@ -94,8 +94,11 @@ - (NSString*)charactersIgnoringModifiers API_AVAILABLE(ios(13.4)) { FlutterUIPressProxy* keyUpEvent(UIKeyboardHIDUsage keyCode, UIKeyModifierFlags modifierFlags, - NSTimeInterval timestamp) API_AVAILABLE(ios(13.4)) { - return keyEventWithPhase(UIPressPhaseEnded, keyCode, modifierFlags, timestamp); + NSTimeInterval timestamp, + const char* characters, + const char* charactersIgnoringModifiers) API_AVAILABLE(ios(13.4)) { + return keyEventWithPhase(UIPressPhaseEnded, keyCode, modifierFlags, timestamp, characters, + charactersIgnoringModifiers); } FlutterUIPressProxy* keyEventWithPhase(UIPressPhase phase, diff --git a/shell/platform/darwin/ios/framework/Source/KeyCodeMap.g.mm b/shell/platform/darwin/ios/framework/Source/KeyCodeMap.g.mm index 3ad9b290df6fd..ef3491377232d 100644 --- a/shell/platform/darwin/ios/framework/Source/KeyCodeMap.g.mm +++ b/shell/platform/darwin/ios/framework/Source/KeyCodeMap.g.mm @@ -328,5 +328,30 @@ 0x00000073, // f24 }; +API_AVAILABLE(ios(13.4)) +NSDictionary* specialKeyMapping = [[NSDictionary alloc] initWithDictionary:@{ + @"UIKeyInputEscape" : @(0x10000001b), + @"UIKeyInputF1" : @(0x100000801), + @"UIKeyInputF2" : @(0x100000802), + @"UIKeyInputF3" : @(0x100000803), + @"UIKeyInputF4" : @(0x100000804), + @"UIKeyInputF5" : @(0x100000805), + @"UIKeyInputF6" : @(0x100000806), + @"UIKeyInputF7" : @(0x100000807), + @"UIKeyInputF8" : @(0x100000808), + @"UIKeyInputF9" : @(0x100000809), + @"UIKeyInputF10" : @(0x10000080a), + @"UIKeyInputF11" : @(0x10000080b), + @"UIKeyInputF12" : @(0x10000080c), + @"UIKeyInputUpArrow" : @(0x100000304), + @"UIKeyInputDownArrow" : @(0x100000301), + @"UIKeyInputLeftArrow" : @(0x100000302), + @"UIKeyInputRightArrow" : @(0x100000303), + @"UIKeyInputHome" : @(0x100000306), + @"UIKeyInputEnd" : @(0x10000000d), + @"UIKeyInputPageUp" : @(0x100000308), + @"UIKeyInputPageDown" : @(0x100000307), +}]; + const uint64_t kCapsLockPhysicalKey = 0x00070039; const uint64_t kCapsLockLogicalKey = 0x100000104; diff --git a/shell/platform/darwin/ios/framework/Source/KeyCodeMap_Internal.h b/shell/platform/darwin/ios/framework/Source/KeyCodeMap_Internal.h index 8478e576861d1..f6e98dd4d84f2 100644 --- a/shell/platform/darwin/ios/framework/Source/KeyCodeMap_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/KeyCodeMap_Internal.h @@ -25,6 +25,18 @@ extern const std::map keyCodeToPhysicalKey; */ extern const std::map keyCodeToLogicalKey; +/** + * Maps iOS specific string values of nonvisible keys to logical keys. + * + * TODO(dkwingsmt): Change this getter function to a global variable. I tried to + * do this but the unit test on CI threw errors saying "message sent to + * deallocated instance" on the NSDictionary. + * + * See: + * https://developer.apple.com/documentation/uikit/uikeycommand/input_strings_for_special_keys?language=objc + */ +extern NSDictionary* specialKeyMapping; + // Several mask constants. See KeyCodeMap.g.mm for their descriptions. extern const uint64_t kValueMask; @@ -70,17 +82,16 @@ typedef enum { * not whether it is the left or right modifier. */ constexpr uint32_t kModifierFlagAnyMask = - kModifierFlagShiftAny | kModifierFlagControlAny | kModifierFlagAltAny | - kModifierFlagMetaAny; + kModifierFlagShiftAny | kModifierFlagControlAny | kModifierFlagAltAny | kModifierFlagMetaAny; /** * A mask of the modifier flags that represent only left or right modifier * keys, and not the generic "Any" mask. */ -constexpr uint32_t kModifierFlagSidedMask = - kModifierFlagControlLeft | kModifierFlagShiftLeft | - kModifierFlagShiftRight | kModifierFlagMetaLeft | kModifierFlagMetaRight | - kModifierFlagAltLeft | kModifierFlagAltRight | kModifierFlagControlRight; +constexpr uint32_t kModifierFlagSidedMask = kModifierFlagControlLeft | kModifierFlagShiftLeft | + kModifierFlagShiftRight | kModifierFlagMetaLeft | + kModifierFlagMetaRight | kModifierFlagAltLeft | + kModifierFlagAltRight | kModifierFlagControlRight; /** * Map |UIKey.keyCode| to the matching sided modifier in UIEventModifierFlags.