diff --git a/React/Base/RCTKeyCommands.m b/React/Base/RCTKeyCommands.m index 7e4ccb2a6a153b..0e59a147e06f5f 100644 --- a/React/Base/RCTKeyCommands.m +++ b/React/Base/RCTKeyCommands.m @@ -9,38 +9,24 @@ #import -#import -#import #import "RCTDefines.h" #import "RCTUtils.h" #if RCT_DEV -@interface UIEvent (UIPhysicalKeyboardEvent) - -@property (nonatomic) NSString *_modifiedInput; -@property (nonatomic) NSString *_unmodifiedInput; -@property (nonatomic) UIKeyModifierFlags _modifierFlags; -@property (nonatomic) BOOL _isKeyDown; -@property (nonatomic) long _keyCode; - -@end - @interface RCTKeyCommand : NSObject -@property (nonatomic, copy, readonly) NSString *key; -@property (nonatomic, readonly) UIKeyModifierFlags flags; +@property (nonatomic, strong) UIKeyCommand *keyCommand; @property (nonatomic, copy) void (^block)(UIKeyCommand *); @end @implementation RCTKeyCommand -- (instancetype)init:(NSString *)key flags:(UIKeyModifierFlags)flags block:(void (^)(UIKeyCommand *))block +- (instancetype)initWithKeyCommand:(UIKeyCommand *)keyCommand block:(void (^)(UIKeyCommand *))block { if ((self = [super init])) { - _key = key; - _flags = flags; + _keyCommand = keyCommand; _block = block; } return self; @@ -55,7 +41,7 @@ - (id)copyWithZone:(__unused NSZone *)zone - (NSUInteger)hash { - return _key.hash ^ _flags; + return _keyCommand.input.hash ^ _keyCommand.modifierFlags; } - (BOOL)isEqual:(RCTKeyCommand *)object @@ -63,15 +49,12 @@ - (BOOL)isEqual:(RCTKeyCommand *)object if (![object isKindOfClass:[RCTKeyCommand class]]) { return NO; } - return [self matchesInput:object.key flags:object.flags]; + return [self matchesInput:object.keyCommand.input flags:object.keyCommand.modifierFlags]; } - (BOOL)matchesInput:(NSString *)input flags:(UIKeyModifierFlags)flags { - // We consider the key command a match if the modifier flags match - // exactly or is there are no modifier flags. This means that for - // `cmd + r`, we will match both `cmd + r` and `r` but not `opt + r`. - return [_key isEqual:input] && (_flags == flags || flags == 0); + return [_keyCommand.input isEqual:input] && _keyCommand.modifierFlags == flags; } - (NSString *)description @@ -79,8 +62,8 @@ - (NSString *)description return [NSString stringWithFormat:@"<%@:%p input=\"%@\" flags=%lld hasBlock=%@>", [self class], self, - _key, - (long long)_flags, + _keyCommand.input, + (long long)_keyCommand.modifierFlags, _block ? @"YES" : @"NO"]; } @@ -92,96 +75,63 @@ @interface RCTKeyCommands () @end -@implementation RCTKeyCommands +@implementation UIResponder (RCTKeyCommands) -+ (void)initialize ++ (UIResponder *)RCT_getFirstResponder:(UIResponder *)view { - SEL originalKeyEventSelector = NSSelectorFromString(@"handleKeyUIEvent:"); - SEL swizzledKeyEventSelector = NSSelectorFromString( - [NSString stringWithFormat:@"_rct_swizzle_%x_%@", arc4random(), NSStringFromSelector(originalKeyEventSelector)]); - - void (^handleKeyUIEventSwizzleBlock)(UIApplication *, UIEvent *) = ^(UIApplication *slf, UIEvent *event) { - [[[self class] sharedInstance] handleKeyUIEventSwizzle:event]; - - ((void (*)(id, SEL, id))objc_msgSend)(slf, swizzledKeyEventSelector, event); - }; - - RCTSwapInstanceMethodWithBlock( - [UIApplication class], originalKeyEventSelector, handleKeyUIEventSwizzleBlock, swizzledKeyEventSelector); -} - -- (void)handleKeyUIEventSwizzle:(UIEvent *)event -{ - NSString *modifiedInput = nil; - UIKeyModifierFlags modifierFlags = 0; - BOOL isKeyDown = NO; - - if ([event respondsToSelector:@selector(_modifiedInput)]) { - modifiedInput = [event _modifiedInput]; - } - - if ([event respondsToSelector:@selector(_modifierFlags)]) { - modifierFlags = [event _modifierFlags]; - } + UIResponder *firstResponder = nil; - if ([event respondsToSelector:@selector(_isKeyDown)]) { - isKeyDown = [event _isKeyDown]; - } - - BOOL interactionEnabled = !UIApplication.sharedApplication.isIgnoringInteractionEvents; - BOOL hasFirstResponder = NO; - if (isKeyDown && modifiedInput.length > 0 && interactionEnabled) { - UIResponder *firstResponder = nil; - for (UIWindow *window in [self allWindows]) { - firstResponder = [window valueForKey:@"firstResponder"]; + if (view.isFirstResponder) { + return view; + } else if ([view isKindOfClass:[UIViewController class]]) { + if ([(UIViewController *)view parentViewController]) { + firstResponder = [UIResponder RCT_getFirstResponder:[(UIViewController *)view parentViewController]]; + } + return firstResponder ? firstResponder : [UIResponder RCT_getFirstResponder:[(UIViewController *)view view]]; + } else if ([view isKindOfClass:[UIView class]]) { + for (UIView *subview in [(UIView *)view subviews]) { + firstResponder = [UIResponder RCT_getFirstResponder:subview]; if (firstResponder) { - hasFirstResponder = YES; - break; + return firstResponder; } } - - // Ignore key commands (except escape) when there's an active responder - if (!firstResponder) { - [self RCT_handleKeyCommand:modifiedInput flags:modifierFlags]; - } } -}; -- (NSArray *)allWindows -{ - BOOL includeInternalWindows = YES; - BOOL onlyVisibleWindows = NO; - - // Obfuscating selector allWindowsIncludingInternalWindows:onlyVisibleWindows: - NSArray *allWindowsComponents = - @[ @"al", @"lWindo", @"wsIncl", @"udingInt", @"ernalWin", @"dows:o", @"nlyVisi", @"bleWin", @"dows:" ]; - SEL allWindowsSelector = NSSelectorFromString([allWindowsComponents componentsJoinedByString:@""]); - - NSMethodSignature *methodSignature = [[UIWindow class] methodSignatureForSelector:allWindowsSelector]; - NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature]; - - invocation.target = [UIWindow class]; - invocation.selector = allWindowsSelector; - [invocation setArgument:&includeInternalWindows atIndex:2]; - [invocation setArgument:&onlyVisibleWindows atIndex:3]; - [invocation invoke]; - - __unsafe_unretained NSArray *windows = nil; - [invocation getReturnValue:&windows]; - return windows; + return firstResponder; } -- (void)RCT_handleKeyCommand:(NSString *)input flags:(UIKeyModifierFlags)modifierFlags +- (NSArray *)RCT_keyCommands { - for (RCTKeyCommand *command in [RCTKeyCommands sharedInstance].commands) { - if ([command matchesInput:input flags:modifierFlags]) { - if (command.block) { - command.block(nil); + NSSet *commands = [RCTKeyCommands sharedInstance].commands; + return [[commands valueForKeyPath:@"keyCommand"] allObjects]; +} + +/** + * Single Press Key Command Response + * Command + KeyEvent (Command + R/D, etc.) + */ +- (void)RCT_handleKeyCommand:(UIKeyCommand *)key +{ + // NOTE: throttle the key handler because on iOS 9 the handleKeyCommand: + // method gets called repeatedly if the command key is held down. + static NSTimeInterval lastCommand = 0; + if (CACurrentMediaTime() - lastCommand > 0.5) { + for (RCTKeyCommand *command in [RCTKeyCommands sharedInstance].commands) { + if ([command.keyCommand.input isEqualToString:key.input] && + command.keyCommand.modifierFlags == key.modifierFlags) { + if (command.block) { + command.block(key); + lastCommand = CACurrentMediaTime(); + } } } } } +@end + +@implementation RCTKeyCommands + + (instancetype)sharedInstance { static RCTKeyCommands *sharedInstance; @@ -207,7 +157,11 @@ - (void)registerKeyCommandWithInput:(NSString *)input { RCTAssertMainQueue(); - RCTKeyCommand *keyCommand = [[RCTKeyCommand alloc] init:input flags:flags block:block]; + UIKeyCommand *command = [UIKeyCommand keyCommandWithInput:input + modifierFlags:flags + action:@selector(RCT_handleKeyCommand:)]; + + RCTKeyCommand *keyCommand = [[RCTKeyCommand alloc] initWithKeyCommand:command block:block]; [_commands removeObject:keyCommand]; [_commands addObject:keyCommand]; }