blob: ef488f3384a798200718ca5f1b3a0b1d2ceb8727 [file] [log] [blame]
// 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/ios/framework/Source/FlutterEmbedderKeyResponder.h"
#include <objc/NSObjCRuntime.h>
#import <objc/message.h>
#include <map>
#include "fml/memory/weak_ptr.h"
#import "KeyCodeMap_Internal.h"
#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterCodecs.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(UInt32 keyCode) {
auto physicalKey = keyCodeToPhysicalKey.find(keyCode);
if (physicalKey == keyCodeToPhysicalKey.end()) {
return KeyOfPlane(keyCode, kIosPlane);
}
return physicalKey->second;
}
/**
* Returns the logical key for a modifier physical key.
*/
static uint64_t GetLogicalKeyForModifier(UInt32 keyCode, uint64_t hidCode) {
auto fromKeyCode = keyCodeToLogicalKey.find(keyCode);
if (fromKeyCode != keyCodeToLogicalKey.end()) {
return fromKeyCode->second;
}
return KeyOfPlane(hidCode, kIosPlane);
}
/**
* Converts upper letters to lower letters in ASCII and extended ASCII, and
* returns as-is otherwise.
*
* Independent of locale.
*/
static uint64_t toLower(uint64_t n) {
constexpr uint64_t lower_a = 0x61;
constexpr uint64_t upper_a = 0x41;
constexpr uint64_t upper_z = 0x5a;
constexpr uint64_t lower_a_grave = 0xe0;
constexpr uint64_t upper_a_grave = 0xc0;
constexpr uint64_t upper_thorn = 0xde;
constexpr uint64_t division = 0xf7;
// ASCII range.
if (n >= upper_a && n <= upper_z) {
return n - upper_a + lower_a;
}
// EASCII range.
if (n >= upper_a_grave && n <= upper_thorn && n != division) {
return n - upper_a_grave + lower_a_grave;
}
return n;
}
/**
* Filters out some special cases in the characters field on UIKey.
*/
static const char* getEventCharacters(NSString* characters, UIKeyboardHIDUsage keyCode)
API_AVAILABLE(ios(13.4)) {
if (characters == nil) {
return nullptr;
}
if ([characters length] == 0) {
return nullptr;
}
if (@available(iOS 13.4, *)) {
// On iOS, function keys return the UTF8 string "\^P" (with a literal '/',
// '^' and a 'P', not escaped ctrl-P) as their "characters" field. This
// isn't a valid (single) UTF8 character. Looking at the only UTF16
// character for a function key yields a value of "16", which is a Unicode
// "SHIFT IN" character, which is just odd. UTF8 conversion of that string
// is what generates the three characters "\^P".
//
// Anyhow, we strip them all out and replace them with empty strings, since
// function keys shouldn't be printable.
if (functionKeyCodes.find(keyCode) != functionKeyCodes.end()) {
return nullptr;
}
}
return [characters UTF8String];
}
/**
* 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, 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()) {
return fromKeyCode->second;
}
const char* characters =
getEventCharacters(press.key.charactersIgnoringModifiers, press.key.keyCode);
NSString* keyLabel =
characters == nullptr ? nil : [[[NSString alloc] initWithUTF8String:characters] autorelease];
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);
}
// This is a non-printable key that is unrecognized, so a new code is minted
// with the autogenerated bit set.
return KeyOfPlane(press.key.keyCode, kIosPlane);
}
/**
* 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|.
*/
static NSUInteger computeModifierFlagOfInterestMask() {
NSUInteger modifierFlagOfInterestMask = kModifierFlagCapsLock | kModifierFlagShiftAny |
kModifierFlagControlAny | kModifierFlagAltAny |
kModifierFlagMetaAny;
for (std::pair<UInt32, ModifierFlag> entry : keyCodeToModifierFlag) {
modifierFlagOfInterestMask = modifierFlagOfInterestMask | entry.second;
}
return modifierFlagOfInterestMask;
}
static bool isKeyDown(FlutterUIPressProxy* press) API_AVAILABLE(ios(13.4)) {
switch (press.phase) {
case UIPressPhaseStationary:
case UIPressPhaseChanged:
// Not sure if this is the right thing to do for these two, but true seems
// more correct than false.
return true;
case UIPressPhaseBegan:
return true;
case UIPressPhaseCancelled:
case UIPressPhaseEnded:
return false;
}
return false;
}
/**
* The C-function sent to the engine's |sendKeyEvent|, wrapping
* |FlutterEmbedderKeyResponder.handleResponse|.
*
* For the reason of this wrap, see |FlutterKeyPendingResponse|.
*/
void HandleResponse(bool handled, void* user_data);
} // namespace
/**
* The invocation context for |HandleResponse|, wrapping
* |FlutterEmbedderKeyResponder.handleResponse|.
*
* The key responder's functions only accept C-functions as callbacks, as well
* as arbitrary 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|.
*/
@interface FlutterKeyPendingResponse : NSObject
@property(readonly) 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 the process of handling an event in |FlutterEmbedderKeyResponder|.
*
* A callback can either be handled with |pendTo:withId:|, or with |resolveTo:|.
* Either way, the callback cannot 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<NSNumber*, FlutterAsyncKeyCallback>*)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(readonly) NSString* debugHandleSource;
@end
@implementation FlutterKeyCallbackGuard {
// The callback is declared in the implementation block to avoid being
// accessed directly.
FlutterAsyncKeyCallback _callback;
}
- (nonnull instancetype)initWithCallback:(FlutterAsyncKeyCallback)callback {
self = [super init];
if (self != nil) {
_callback = [callback copy];
_handled = FALSE;
}
return self;
}
- (void)dealloc {
[_callback release];
[super dealloc];
}
- (void)pendTo:(nonnull NSMutableDictionary<NSNumber*, FlutterAsyncKeyCallback>*)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, readonly) FlutterSendKeyEvent sendEvent;
/**
* A map of pressed keys.
*
* The keys of the dictionary are physical keys, while the values are the logical keys
* of the key down event.
*/
@property(nonatomic, retain, readonly) NSMutableDictionary<NSNumber*, NSNumber*>* 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, retain, readonly)
NSMutableDictionary<NSNumber*, FlutterAsyncKeyCallback>* 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:(nonnull FlutterUIPressProxy*)press API_AVAILABLE(ios(13.4));
/**
* 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;
/**
* Synthesize a CapsLock down event, then a CapsLock up event.
*/
- (void)synthesizeCapsLockTapWithTimestamp:(NSTimeInterval)timestamp;
/**
* Send an event to the framework, expecting its response.
*/
- (void)sendPrimaryFlutterEvent:(const FlutterKeyEvent&)event
callback:(nonnull FlutterKeyCallbackGuard*)callback;
/**
* Send an empty key event.
*
* The event is never synthesized, and never expects an event result. An empty
* event is sent when no other events should be sent, such as upon back-to-back
* keydown events of the same key.
*/
- (void)sendEmptyEvent;
/**
* Send a key event for a modifier key.
*/
- (void)synthesizeModifierEventOfType:(BOOL)isDownEvent
timestamp:(NSTimeInterval)timestamp
keyCode:(UInt32)keyCode;
/**
* Processes a down event from the system.
*/
- (void)handlePressBegin:(nonnull FlutterUIPressProxy*)press
callback:(nonnull FlutterKeyCallbackGuard*)callback API_AVAILABLE(ios(13.4));
/**
* Processes an up event from the system.
*/
- (void)handlePressEnd:(nonnull FlutterUIPressProxy*)press
callback:(nonnull FlutterKeyCallbackGuard*)callback API_AVAILABLE(ios(13.4));
/**
* Processes the response from the framework.
*/
- (void)handleResponse:(BOOL)handled forId:(uint64_t)responseId;
/**
* Fix up the modifiers for a particular type of modifier key.
*/
- (UInt32)fixSidedFlags:(ModifierFlag)anyFlag
withLeftFlag:(ModifierFlag)leftSide
withRightFlag:(ModifierFlag)rightSide
withLeftKey:(UInt16)leftKeyCode
withRightKey:(UInt16)rightKeyCode
withKeyCode:(UInt16)keyCode
keyDown:(BOOL)isKeyDown
forFlags:(UInt32)modifiersPressed API_AVAILABLE(ios(13.4));
/**
* Because iOS differs from other platforms in that the modifier flags still
* contain the flag for the key that is being released on the keyup event, we
* adjust the modifiers when the released key is a matching modifier key.
*/
- (UInt32)adjustModifiers:(nonnull FlutterUIPressProxy*)press API_AVAILABLE(ios(13.4));
@end
@implementation FlutterEmbedderKeyResponder
- (nonnull instancetype)initWithSendEvent:(FlutterSendKeyEvent)sendEvent {
self = [super init];
if (self != nil) {
_sendEvent = [sendEvent copy];
_pressingRecords = [[NSMutableDictionary alloc] init];
_pendingResponses = [[NSMutableDictionary alloc] init];
_responseId = 1;
_lastModifierFlagsOfInterest = 0;
_modifierFlagOfInterestMask = computeModifierFlagOfInterestMask();
}
return self;
}
- (void)dealloc {
[_sendEvent release];
[_pressingRecords release];
[_pendingResponses release];
[super dealloc];
}
- (void)handlePress:(nonnull FlutterUIPressProxy*)press
callback:(FlutterAsyncKeyCallback)callback API_AVAILABLE(ios(13.4)) {
if (@available(iOS 13.4, *)) {
} else {
return;
}
// The conversion algorithm relies on a non-nil callback to properly compute
// `synthesized`.
NSAssert(callback != nil, @"The callback must not be nil.");
FlutterKeyCallbackGuard* guardedCallback = nil;
switch (press.phase) {
case UIPressPhaseBegan:
guardedCallback = [[[FlutterKeyCallbackGuard alloc] initWithCallback:callback] autorelease];
[self handlePressBegin:press callback:guardedCallback];
break;
case UIPressPhaseEnded:
guardedCallback = [[[FlutterKeyCallbackGuard alloc] initWithCallback:callback] autorelease];
[self handlePressEnd:press callback:guardedCallback];
break;
case UIPressPhaseChanged:
case UIPressPhaseCancelled:
// TODO(gspencergoog): Handle cancelled events as synthesized up events.
case UIPressPhaseStationary:
NSAssert(false, @"Unexpected press phase receieved in handlePress");
return;
}
NSAssert(guardedCallback.handled, @"The callback returned without being handled.");
NSAssert(
(_lastModifierFlagsOfInterest & ~kModifierFlagCapsLock) ==
([self adjustModifiers:press] & (_modifierFlagOfInterestMask & ~kModifierFlagCapsLock)),
@"The modifier flags are not properly updated: recorded 0x%lx, event with mask 0x%lx",
static_cast<unsigned long>(_lastModifierFlagsOfInterest & ~kModifierFlagCapsLock),
static_cast<unsigned long>([self adjustModifiers:press] &
(_modifierFlagOfInterestMask & ~kModifierFlagCapsLock)));
}
#pragma mark - Private
- (void)synchronizeModifiers:(nonnull FlutterUIPressProxy*)press API_AVAILABLE(ios(13.4)) {
if (@available(iOS 13.4, *)) {
} else {
return;
}
const UInt32 lastFlagsOfInterest = _lastModifierFlagsOfInterest & _modifierFlagOfInterestMask;
const UInt32 pressedModifiers = [self adjustModifiers:press];
const UInt32 currentFlagsOfInterest = pressedModifiers & _modifierFlagOfInterestMask;
UInt32 flagDifference = currentFlagsOfInterest ^ lastFlagsOfInterest;
if (flagDifference & kModifierFlagCapsLock) {
// If the caps lock changed, and we didn't expect that, then send a
// synthesized down and an up to simulate a toggle of the state.
if (press.key.keyCode != UIKeyboardHIDUsageKeyboardCapsLock) {
[self synthesizeCapsLockTapWithTimestamp:press.timestamp];
}
flagDifference &= ~kModifierFlagCapsLock;
}
while (true) {
const UInt32 currentFlag = lowestSetBit(flagDifference);
if (currentFlag == 0) {
break;
}
flagDifference &= ~currentFlag;
if (currentFlag & kModifierFlagAnyMask) {
// Skip synthesizing keys for the "any" flags, since their synthesis will
// be handled when we do the sided flags. We still want them in the flags
// of interest, though, so we can keep their state.
continue;
}
auto keyCode = modifierFlagToKeyCode.find(static_cast<ModifierFlag>(currentFlag));
NSAssert(keyCode != modifierFlagToKeyCode.end(), @"Invalid modifier flag of interest 0x%lx",
static_cast<unsigned long>(currentFlag));
if (keyCode == modifierFlagToKeyCode.end()) {
continue;
}
// If this press matches the modifier key in question, then don't synthesize
// it, because it's already a "real" keypress.
if (keyCode->second == static_cast<UInt32>(press.key.keyCode)) {
continue;
}
BOOL isDownEvent = currentFlagsOfInterest & currentFlag;
[self synthesizeModifierEventOfType:isDownEvent
timestamp:press.timestamp
keyCode:keyCode->second];
}
_lastModifierFlagsOfInterest =
(_lastModifierFlagsOfInterest & ~_modifierFlagOfInterestMask) | currentFlagsOfInterest;
}
- (void)synthesizeCapsLockTapWithTimestamp:(NSTimeInterval)timestamp {
// The assumption when the app starts is that caps lock is off, but if that
// turns out to be untrue (according to the modifier flags), then this is used
// to simulate a key down and a key up of the caps lock key, to simulate
// toggling of that state in the framework.
FlutterKeyEvent flutterEvent = {
.struct_size = sizeof(FlutterKeyEvent),
.timestamp = GetFlutterTimestampFrom(timestamp),
.type = kFlutterKeyEventTypeDown,
.physical = kCapsLockPhysicalKey,
.logical = kCapsLockLogicalKey,
.character = nil,
.synthesized = true,
};
_sendEvent(flutterEvent, nullptr, nullptr);
flutterEvent.type = kFlutterKeyEventTypeUp;
_sendEvent(flutterEvent, nullptr, nullptr);
}
- (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] autorelease];
[callback pendTo:_pendingResponses withId:responseId];
_sendEvent(event, HandleResponse, pending);
}
- (void)sendEmptyEvent {
FlutterKeyEvent event = {
.struct_size = sizeof(FlutterKeyEvent),
.timestamp = 0,
.type = kFlutterKeyEventTypeDown,
.physical = 0,
.logical = 0,
.character = nil,
.synthesized = false,
};
_sendEvent(event, nil, nil);
}
- (void)synthesizeModifierEventOfType:(BOOL)isDownEvent
timestamp:(NSTimeInterval)timestamp
keyCode:(UInt32)keyCode {
uint64_t physicalKey = GetPhysicalKeyForKeyCode(keyCode);
uint64_t logicalKey = GetLogicalKeyForModifier(keyCode, physicalKey);
if (physicalKey == 0 || logicalKey == 0) {
return;
}
FlutterKeyEvent flutterEvent = {
.struct_size = sizeof(FlutterKeyEvent),
.timestamp = GetFlutterTimestampFrom(timestamp),
.type = isDownEvent ? kFlutterKeyEventTypeDown : kFlutterKeyEventTypeUp,
.physical = physicalKey,
.logical = logicalKey,
.character = nil,
.synthesized = true,
};
[self updateKey:physicalKey asPressed:isDownEvent ? logicalKey : 0];
_sendEvent(flutterEvent, nullptr, nullptr);
}
- (void)handlePressBegin:(nonnull FlutterUIPressProxy*)press
callback:(nonnull FlutterKeyCallbackGuard*)callback API_AVAILABLE(ios(13.4)) {
if (@available(iOS 13.4, *)) {
} else {
return;
}
uint64_t physicalKey = GetPhysicalKeyForKeyCode(press.key.keyCode);
// 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;
if ([_pressingRecords count] > 0) {
pressedLogicalKey = _pressingRecords[@(physicalKey)];
if (pressedLogicalKey != nil) {
// Normally the key up events won't be missed since iOS always sends the
// key up event to the view 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];
[self sendEmptyEvent];
return;
}
}
if (pressedLogicalKey == nil) {
[self updateKey:physicalKey asPressed:logicalKey];
}
FlutterKeyEvent flutterEvent = {
.struct_size = sizeof(FlutterKeyEvent),
.timestamp = GetFlutterTimestampFrom(press.timestamp),
.type = kFlutterKeyEventTypeDown,
.physical = physicalKey,
.logical = pressedLogicalKey == nil ? logicalKey : [pressedLogicalKey unsignedLongLongValue],
.character =
specialKey != nil ? nil : getEventCharacters(press.key.characters, press.key.keyCode),
.synthesized = false,
};
[self sendPrimaryFlutterEvent:flutterEvent callback:callback];
}
- (void)handlePressEnd:(nonnull FlutterUIPressProxy*)press
callback:(nonnull FlutterKeyCallbackGuard*)callback API_AVAILABLE(ios(13.4)) {
if (@available(iOS 13.4, *)) {
} else {
return;
}
[self synchronizeModifiers:press];
uint64_t physicalKey = GetPhysicalKeyForKeyCode(press.key.keyCode);
NSNumber* pressedLogicalKey = _pressingRecords[@(physicalKey)];
if (pressedLogicalKey == nil) {
// Normally the key up events won't be missed since iOS always sends the
// key up event to the view 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];
[self sendEmptyEvent];
return;
}
[self updateKey:physicalKey asPressed:0];
FlutterKeyEvent flutterEvent = {
.struct_size = sizeof(FlutterKeyEvent),
.timestamp = GetFlutterTimestampFrom(press.timestamp),
.type = kFlutterKeyEventTypeUp,
.physical = physicalKey,
.logical = [pressedLogicalKey unsignedLongLongValue],
.character = nil,
.synthesized = false,
};
[self sendPrimaryFlutterEvent:flutterEvent callback:callback];
}
- (void)handleResponse:(BOOL)handled forId:(uint64_t)responseId {
FlutterAsyncKeyCallback callback = _pendingResponses[@(responseId)];
callback(handled);
[_pendingResponses removeObjectForKey:@(responseId)];
}
- (UInt32)fixSidedFlags:(ModifierFlag)anyFlag
withLeftFlag:(ModifierFlag)leftSide
withRightFlag:(ModifierFlag)rightSide
withLeftKey:(UInt16)leftKeyCode
withRightKey:(UInt16)rightKeyCode
withKeyCode:(UInt16)keyCode
keyDown:(BOOL)isKeyDown
forFlags:(UInt32)modifiersPressed API_AVAILABLE(ios(13.4)) {
UInt32 newModifiers = modifiersPressed;
if (isKeyDown) {
// Add in the modifier flags that correspond to this key code, if any.
if (keyCode == leftKeyCode) {
newModifiers |= leftSide | anyFlag;
} else if (keyCode == rightKeyCode) {
newModifiers |= rightSide | anyFlag;
}
} else {
// If this is a key up, then remove any modifier that matches the keycode in
// the event from the flags, and the anyFlag if the other side isn't also
// pressed.
if (keyCode == leftKeyCode) {
newModifiers &= ~leftSide;
if (!(newModifiers & rightSide)) {
newModifiers &= ~anyFlag;
}
} else if (keyCode == rightKeyCode) {
newModifiers &= ~rightSide;
if (!(newModifiers & leftSide)) {
newModifiers &= ~anyFlag;
}
}
}
if (!(newModifiers & anyFlag)) {
// Turn off any sided flags, since the "any" flag is gone.
newModifiers &= ~(leftSide | rightSide);
}
return newModifiers;
}
// This fixes a few cases where iOS provides modifier flags differently from how
// the framework would like to receive them.
//
// 1) iOS turns off the flag associated with a modifier key AFTER the modifier
// key up event, so when the key up event arrives, the flags must be modified
// before synchronizing so they do not include the modifier that arrived in
// the key up event.
// 2) Modifier flags can be set even when that modifier is not being pressed.
// One example of this is when a special character is produced with the Alt
// (Option) key, and the Alt key is released before the letter key: the
// letter key's key up event still contains the Alt key flag.
// 3) iOS doesn't provide information about which side modifier was pressed,
// except through the keycode of the pressed key, so we look at the pressed
// key code to decide which side to indicate in the flags. If we can't know
// (in the case of a non-modifier key event having an "any" modifier set, but
// we don't know already that the modifier is down), then we just pick the
// left one arbitrarily.
- (UInt32)adjustModifiers:(nonnull FlutterUIPressProxy*)press API_AVAILABLE(ios(13.4)) {
if (@available(iOS 13.4, *)) {
// no-op
} else {
return press.key.modifierFlags;
}
bool keyDown = isKeyDown(press);
// Start with the current modifier flags, along with any sided flags that we
// already know are down.
UInt32 pressedModifiers =
press.key.modifierFlags | (_lastModifierFlagsOfInterest & kModifierFlagSidedMask);
pressedModifiers = [self fixSidedFlags:kModifierFlagShiftAny
withLeftFlag:kModifierFlagShiftLeft
withRightFlag:kModifierFlagShiftRight
withLeftKey:UIKeyboardHIDUsageKeyboardLeftShift
withRightKey:UIKeyboardHIDUsageKeyboardRightShift
withKeyCode:press.key.keyCode
keyDown:keyDown
forFlags:pressedModifiers];
pressedModifiers = [self fixSidedFlags:kModifierFlagControlAny
withLeftFlag:kModifierFlagControlLeft
withRightFlag:kModifierFlagControlRight
withLeftKey:UIKeyboardHIDUsageKeyboardLeftControl
withRightKey:UIKeyboardHIDUsageKeyboardRightControl
withKeyCode:press.key.keyCode
keyDown:keyDown
forFlags:pressedModifiers];
pressedModifiers = [self fixSidedFlags:kModifierFlagAltAny
withLeftFlag:kModifierFlagAltLeft
withRightFlag:kModifierFlagAltRight
withLeftKey:UIKeyboardHIDUsageKeyboardLeftAlt
withRightKey:UIKeyboardHIDUsageKeyboardRightAlt
withKeyCode:press.key.keyCode
keyDown:keyDown
forFlags:pressedModifiers];
pressedModifiers = [self fixSidedFlags:kModifierFlagMetaAny
withLeftFlag:kModifierFlagMetaLeft
withRightFlag:kModifierFlagMetaRight
withLeftKey:UIKeyboardHIDUsageKeyboardLeftGUI
withRightKey:UIKeyboardHIDUsageKeyboardRightGUI
withKeyCode:press.key.keyCode
keyDown:keyDown
forFlags:pressedModifiers];
if (press.key.keyCode == UIKeyboardHIDUsageKeyboardCapsLock) {
// The caps lock modifier needs to be unset only if it was already on
// and this is a key up. This is because it indicates the lock state, and
// not the key press state. The caps lock state should be on between the
// first down, and the second up (i.e. while the lock in effect), and
// this code turns it off at the second up event. The OS leaves it on still
// because of iOS's weird late processing of modifier states. Synthesis of
// the appropriate synthesized key events happens in synchronizeModifiers.
if (!keyDown && _lastModifierFlagsOfInterest & kModifierFlagCapsLock) {
pressedModifiers &= ~kModifierFlagCapsLock;
}
}
return pressedModifiers;
}
@end
namespace {
void HandleResponse(bool handled, void* user_data) {
FlutterKeyPendingResponse* pending = reinterpret_cast<FlutterKeyPendingResponse*>(user_data);
[pending.responder handleResponse:handled forId:pending.responseId];
}
} // namespace