Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 1b3d273

Browse files
[Keyboard, iOS] Use special key mapping for logical keys (#34413)
* Impl * Add test * Change to string * Apply suggestions from code review Co-authored-by: Greg Spencer <gspencergoog@users.noreply.github.com> * Try alloc init * Fix character * Fix mapping * Fix compile * Try literal string Co-authored-by: Greg Spencer <gspencergoog@users.noreply.github.com>
1 parent e390849 commit 1b3d273

File tree

6 files changed

+135
-12
lines changed

6 files changed

+135
-12
lines changed

shell/platform/darwin/ios/framework/Source/FlutterEmbedderKeyResponder.mm

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,10 +146,17 @@ static uint64_t toLower(uint64_t n) {
146146
/**
147147
* Returns the logical key of a KeyUp or KeyDown event.
148148
*
149+
* The `maybeSpecialKey` is a nullable integer, and if not nil, indicates
150+
* that the event key is a special key as defined by `specialKeyMapping`,
151+
* and is the corresponding logical key.
152+
*
149153
* For modifier keys, use GetLogicalKeyForModifier.
150154
*/
151-
static uint64_t GetLogicalKeyForEvent(FlutterUIPressProxy* press, uint64_t physicalKey)
155+
static uint64_t GetLogicalKeyForEvent(FlutterUIPressProxy* press, NSNumber* maybeSpecialKey)
152156
API_AVAILABLE(ios(13.4)) {
157+
if (maybeSpecialKey != nil) {
158+
return [maybeSpecialKey unsignedLongLongValue];
159+
}
153160
// Look to see if the keyCode can be mapped from keycode.
154161
auto fromKeyCode = keyCodeToLogicalKey.find(press.key.keyCode);
155162
if (fromKeyCode != keyCodeToLogicalKey.end()) {
@@ -670,7 +677,11 @@ - (void)handlePressBegin:(nonnull FlutterUIPressProxy*)press
670677
return;
671678
}
672679
uint64_t physicalKey = GetPhysicalKeyForKeyCode(press.key.keyCode);
673-
uint64_t logicalKey = GetLogicalKeyForEvent(press, physicalKey);
680+
// Some unprintable keys on iOS have literal names on their key label, such as
681+
// @"UIKeyInputEscape". They are called the "special keys" and have predefined
682+
// logical keys and empty characters.
683+
NSNumber* specialKey = [specialKeyMapping objectForKey:press.key.charactersIgnoringModifiers];
684+
uint64_t logicalKey = GetLogicalKeyForEvent(press, specialKey);
674685
[self synchronizeModifiers:press];
675686

676687
NSNumber* pressedLogicalKey = nil;
@@ -697,7 +708,8 @@ - (void)handlePressBegin:(nonnull FlutterUIPressProxy*)press
697708
.type = kFlutterKeyEventTypeDown,
698709
.physical = physicalKey,
699710
.logical = pressedLogicalKey == nil ? logicalKey : [pressedLogicalKey unsignedLongLongValue],
700-
.character = getEventCharacters(press.key.characters, press.key.keyCode),
711+
.character =
712+
specialKey != nil ? nil : getEventCharacters(press.key.characters, press.key.keyCode),
701713
.synthesized = false,
702714
};
703715
[self sendPrimaryFlutterEvent:flutterEvent callback:callback];

shell/platform/darwin/ios/framework/Source/FlutterEmbedderKeyResponderTest.mm

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ - (void)dealloc {
7575
API_AVAILABLE(ios(13.4))
7676
constexpr UIKeyboardHIDUsage kKeyCodeKeyA = (UIKeyboardHIDUsage)0x04;
7777
API_AVAILABLE(ios(13.4))
78+
constexpr UIKeyboardHIDUsage kKeyCodePeriod = (UIKeyboardHIDUsage)0x37;
79+
API_AVAILABLE(ios(13.4))
7880
constexpr UIKeyboardHIDUsage kKeyCodeKeyW = (UIKeyboardHIDUsage)0x1a;
7981
API_AVAILABLE(ios(13.4))
8082
constexpr UIKeyboardHIDUsage kKeyCodeShiftLeft = (UIKeyboardHIDUsage)0xe1;
@@ -87,6 +89,8 @@ - (void)dealloc {
8789
API_AVAILABLE(ios(13.4))
8890
constexpr UIKeyboardHIDUsage kKeyCodeF1 = (UIKeyboardHIDUsage)0x3a;
8991
API_AVAILABLE(ios(13.4))
92+
constexpr UIKeyboardHIDUsage kKeyCodeCommandLeft = (UIKeyboardHIDUsage)0xe3;
93+
API_AVAILABLE(ios(13.4))
9094
constexpr UIKeyboardHIDUsage kKeyCodeAltRight = (UIKeyboardHIDUsage)0xe6;
9195
API_AVAILABLE(ios(13.4))
9296
constexpr UIKeyboardHIDUsage kKeyCodeEject = (UIKeyboardHIDUsage)0xb8;
@@ -941,4 +945,69 @@ - (void)testSynchronizeCapsLockStateOnNormalKey API_AVAILABLE(ios(13.4)) {
941945
[events removeAllObjects];
942946
}
943947

948+
// Press Cmd-. should correctly result in an Escape event.
949+
- (void)testCommandPeriodKey API_AVAILABLE(ios(13.4)) {
950+
__block NSMutableArray<TestKeyEvent*>* events = [[NSMutableArray<TestKeyEvent*> alloc] init];
951+
id keyEventCallback = ^(BOOL handled) {
952+
};
953+
FlutterKeyEvent* event;
954+
955+
FlutterEmbedderKeyResponder* responder = [[FlutterEmbedderKeyResponder alloc]
956+
initWithSendEvent:^(const FlutterKeyEvent& event, _Nullable FlutterKeyEventCallback callback,
957+
_Nullable _VoidPtr user_data) {
958+
[events addObject:[[TestKeyEvent alloc] initWithEvent:&event callback:nil userData:nil]];
959+
callback(true, user_data);
960+
}];
961+
962+
// MetaLeft down.
963+
[responder handlePress:keyDownEvent(kKeyCodeCommandLeft, kModifierFlagMetaAny, 123.0f, "", "")
964+
callback:keyEventCallback];
965+
XCTAssertEqual([events count], 1u);
966+
event = events[0].data;
967+
XCTAssertEqual(event->type, kFlutterKeyEventTypeDown);
968+
XCTAssertEqual(event->physical, kPhysicalMetaLeft);
969+
XCTAssertEqual(event->logical, kLogicalMetaLeft);
970+
XCTAssertEqual(event->character, nullptr);
971+
XCTAssertEqual(event->synthesized, false);
972+
[events removeAllObjects];
973+
974+
// Period down, which is logically Escape.
975+
[responder handlePress:keyDownEvent(kKeyCodePeriod, kModifierFlagMetaAny, 123.0f,
976+
"UIKeyInputEscape", "UIKeyInputEscape")
977+
callback:keyEventCallback];
978+
XCTAssertEqual([events count], 1u);
979+
event = events[0].data;
980+
XCTAssertEqual(event->type, kFlutterKeyEventTypeDown);
981+
XCTAssertEqual(event->physical, kPhysicalPeriod);
982+
XCTAssertEqual(event->logical, kLogicalEscape);
983+
XCTAssertEqual(event->character, nullptr);
984+
XCTAssertEqual(event->synthesized, false);
985+
[events removeAllObjects];
986+
987+
// Period up, which unconventionally has characters.
988+
[responder handlePress:keyUpEvent(kKeyCodePeriod, kModifierFlagMetaAny, 123.0f,
989+
"UIKeyInputEscape", "UIKeyInputEscape")
990+
callback:keyEventCallback];
991+
XCTAssertEqual([events count], 1u);
992+
event = events[0].data;
993+
XCTAssertEqual(event->type, kFlutterKeyEventTypeUp);
994+
XCTAssertEqual(event->physical, kPhysicalPeriod);
995+
XCTAssertEqual(event->logical, kLogicalEscape);
996+
XCTAssertEqual(event->character, nullptr);
997+
XCTAssertEqual(event->synthesized, false);
998+
[events removeAllObjects];
999+
1000+
// MetaLeft up.
1001+
[responder handlePress:keyUpEvent(kKeyCodeCommandLeft, kModifierFlagMetaAny, 123.0f, "", "")
1002+
callback:keyEventCallback];
1003+
XCTAssertEqual([events count], 1u);
1004+
event = events[0].data;
1005+
XCTAssertEqual(event->type, kFlutterKeyEventTypeUp);
1006+
XCTAssertEqual(event->physical, kPhysicalMetaLeft);
1007+
XCTAssertEqual(event->logical, kLogicalMetaLeft);
1008+
XCTAssertEqual(event->character, nullptr);
1009+
XCTAssertEqual(event->synthesized, false);
1010+
[events removeAllObjects];
1011+
}
1012+
9441013
@end

shell/platform/darwin/ios/framework/Source/FlutterFakeKeyEvents.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,10 @@ FlutterUIPressProxy* keyDownEvent(UIKeyboardHIDUsage keyCode,
5656

5757
FlutterUIPressProxy* keyUpEvent(UIKeyboardHIDUsage keyCode,
5858
UIKeyModifierFlags modifierFlags = 0x0,
59-
NSTimeInterval timestamp = 0.0f) API_AVAILABLE(ios(13.4));
59+
NSTimeInterval timestamp = 0.0f,
60+
const char* characters = "",
61+
const char* charactersIgnoringModifiers = "")
62+
API_AVAILABLE(ios(13.4));
6063

6164
FlutterUIPressProxy* keyEventWithPhase(UIPressPhase phase,
6265
UIKeyboardHIDUsage keyCode,

shell/platform/darwin/ios/framework/Source/FlutterFakeKeyEvents.mm

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,11 @@ - (NSString*)charactersIgnoringModifiers API_AVAILABLE(ios(13.4)) {
9494

9595
FlutterUIPressProxy* keyUpEvent(UIKeyboardHIDUsage keyCode,
9696
UIKeyModifierFlags modifierFlags,
97-
NSTimeInterval timestamp) API_AVAILABLE(ios(13.4)) {
98-
return keyEventWithPhase(UIPressPhaseEnded, keyCode, modifierFlags, timestamp);
97+
NSTimeInterval timestamp,
98+
const char* characters,
99+
const char* charactersIgnoringModifiers) API_AVAILABLE(ios(13.4)) {
100+
return keyEventWithPhase(UIPressPhaseEnded, keyCode, modifierFlags, timestamp, characters,
101+
charactersIgnoringModifiers);
99102
}
100103

101104
FlutterUIPressProxy* keyEventWithPhase(UIPressPhase phase,

shell/platform/darwin/ios/framework/Source/KeyCodeMap.g.mm

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,5 +328,30 @@
328328
0x00000073, // f24
329329
};
330330

331+
API_AVAILABLE(ios(13.4))
332+
NSDictionary<NSString*, NSNumber*>* specialKeyMapping = [[NSDictionary alloc] initWithDictionary:@{
333+
@"UIKeyInputEscape" : @(0x10000001b),
334+
@"UIKeyInputF1" : @(0x100000801),
335+
@"UIKeyInputF2" : @(0x100000802),
336+
@"UIKeyInputF3" : @(0x100000803),
337+
@"UIKeyInputF4" : @(0x100000804),
338+
@"UIKeyInputF5" : @(0x100000805),
339+
@"UIKeyInputF6" : @(0x100000806),
340+
@"UIKeyInputF7" : @(0x100000807),
341+
@"UIKeyInputF8" : @(0x100000808),
342+
@"UIKeyInputF9" : @(0x100000809),
343+
@"UIKeyInputF10" : @(0x10000080a),
344+
@"UIKeyInputF11" : @(0x10000080b),
345+
@"UIKeyInputF12" : @(0x10000080c),
346+
@"UIKeyInputUpArrow" : @(0x100000304),
347+
@"UIKeyInputDownArrow" : @(0x100000301),
348+
@"UIKeyInputLeftArrow" : @(0x100000302),
349+
@"UIKeyInputRightArrow" : @(0x100000303),
350+
@"UIKeyInputHome" : @(0x100000306),
351+
@"UIKeyInputEnd" : @(0x10000000d),
352+
@"UIKeyInputPageUp" : @(0x100000308),
353+
@"UIKeyInputPageDown" : @(0x100000307),
354+
}];
355+
331356
const uint64_t kCapsLockPhysicalKey = 0x00070039;
332357
const uint64_t kCapsLockLogicalKey = 0x100000104;

shell/platform/darwin/ios/framework/Source/KeyCodeMap_Internal.h

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,18 @@ extern const std::map<uint32_t, uint64_t> keyCodeToPhysicalKey;
2525
*/
2626
extern const std::map<uint32_t, uint64_t> keyCodeToLogicalKey;
2727

28+
/**
29+
* Maps iOS specific string values of nonvisible keys to logical keys.
30+
*
31+
* TODO(dkwingsmt): Change this getter function to a global variable. I tried to
32+
* do this but the unit test on CI threw errors saying "message sent to
33+
* deallocated instance" on the NSDictionary.
34+
*
35+
* See:
36+
* https://developer.apple.com/documentation/uikit/uikeycommand/input_strings_for_special_keys?language=objc
37+
*/
38+
extern NSDictionary<NSString*, NSNumber*>* specialKeyMapping;
39+
2840
// Several mask constants. See KeyCodeMap.g.mm for their descriptions.
2941

3042
extern const uint64_t kValueMask;
@@ -70,17 +82,16 @@ typedef enum {
7082
* not whether it is the left or right modifier.
7183
*/
7284
constexpr uint32_t kModifierFlagAnyMask =
73-
kModifierFlagShiftAny | kModifierFlagControlAny | kModifierFlagAltAny |
74-
kModifierFlagMetaAny;
85+
kModifierFlagShiftAny | kModifierFlagControlAny | kModifierFlagAltAny | kModifierFlagMetaAny;
7586

7687
/**
7788
* A mask of the modifier flags that represent only left or right modifier
7889
* keys, and not the generic "Any" mask.
7990
*/
80-
constexpr uint32_t kModifierFlagSidedMask =
81-
kModifierFlagControlLeft | kModifierFlagShiftLeft |
82-
kModifierFlagShiftRight | kModifierFlagMetaLeft | kModifierFlagMetaRight |
83-
kModifierFlagAltLeft | kModifierFlagAltRight | kModifierFlagControlRight;
91+
constexpr uint32_t kModifierFlagSidedMask = kModifierFlagControlLeft | kModifierFlagShiftLeft |
92+
kModifierFlagShiftRight | kModifierFlagMetaLeft |
93+
kModifierFlagMetaRight | kModifierFlagAltLeft |
94+
kModifierFlagAltRight | kModifierFlagControlRight;
8495

8596
/**
8697
* Map |UIKey.keyCode| to the matching sided modifier in UIEventModifierFlags.

0 commit comments

Comments
 (0)