diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m index dceaf4409..2ca2d7c06 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -172,6 +172,15 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) userInfo:nil]; }]; + + [commands registerDoublePressKeyCommandWithInput:@"r" + modifierFlags:0 + action:^(__unused UIKeyCommand *command) { + [[NSNotificationCenter defaultCenter] postNotificationName:RCTReloadNotification + object:nil + userInfo:nil]; + }]; + #endif } diff --git a/React/Base/RCTKeyCommands.h b/React/Base/RCTKeyCommands.h index 0a27ce493..d93485749 100644 --- a/React/Base/RCTKeyCommands.h +++ b/React/Base/RCTKeyCommands.h @@ -14,22 +14,41 @@ + (instancetype)sharedInstance; /** - * Register a keyboard command. + * Register a single-press keyboard command. */ - (void)registerKeyCommandWithInput:(NSString *)input modifierFlags:(UIKeyModifierFlags)flags action:(void (^)(UIKeyCommand *command))block; /** - * Unregister a keyboard command. + * Unregister a single-press keyboard command. */ - (void)unregisterKeyCommandWithInput:(NSString *)input modifierFlags:(UIKeyModifierFlags)flags; /** - * Check if a command is registered. + * Check if a single-press command is registered. */ - (BOOL)isKeyCommandRegisteredForInput:(NSString *)input modifierFlags:(UIKeyModifierFlags)flags; +/** + * Register a double-press keyboard command. + */ +- (void)registerDoublePressKeyCommandWithInput:(NSString *)input + modifierFlags:(UIKeyModifierFlags)flags + action:(void (^)(UIKeyCommand *command))block; + +/** + * Unregister a double-press keyboard command. + */ +- (void)unregisterDoublePressKeyCommandWithInput:(NSString *)input + modifierFlags:(UIKeyModifierFlags)flags; + +/** + * Check if a double-press command is registered. + */ +- (BOOL)isDoublePressKeyCommandRegisteredForInput:(NSString *)input + modifierFlags:(UIKeyModifierFlags)flags; + @end diff --git a/React/Base/RCTKeyCommands.m b/React/Base/RCTKeyCommands.m index 92747519f..77ba60545 100644 --- a/React/Base/RCTKeyCommands.m +++ b/React/Base/RCTKeyCommands.m @@ -83,17 +83,60 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) @implementation UIResponder (RCTKeyCommands) ++ (UIResponder *)RCT_getFirstResponder:(UIResponder *)view +{ + UIResponder *firstResponder = nil; + + 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) { + return firstResponder; + } + } + } + + return firstResponder; +} + - (NSArray *)RCT_keyCommands { + /** + * If current first responder is UITextView or UITextField, disable the shortcut key commands. + * For example, double-pressing a key should type two characters into the text view, + * not invoke a predefined keyboard shortcut. + */ + NSString *name = NSStringFromClass(self.class); + if ([name isEqualToString : @"AppDelegate"]) { + return nil; + } + + UIResponder *firstResponder = [UIResponder RCT_getFirstResponder: self]; + if ([firstResponder isKindOfClass:[UITextView class]] || + [firstResponder isKindOfClass:[UITextField class]] || + [firstResponder.class conformsToProtocol:@protocol(UITextViewDelegate)]) { + return 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 (RCTIsIOS8OrEarlier() || CACurrentMediaTime() - lastCommand > 0.5) { for (RCTKeyCommand *command in [RCTKeyCommands sharedInstance].commands) { @@ -108,6 +151,61 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) } } +/** + * Double Press Key Command Response + * Double KeyEvent (Double R, etc.) + */ +- (void)RCT_handleDoublePressKeyCommand:(UIKeyCommand *)key +{ + static BOOL firstPress = YES; + static NSTimeInterval lastCommand = 0; + static NSTimeInterval lastDoubleCommand = 0; + static NSString *lastInput = nil; + static UIKeyModifierFlags lastModifierFlags = nil; + + if (firstPress) { + for (RCTKeyCommand *command in [RCTKeyCommands sharedInstance].commands) { + if ([command.keyCommand.input isEqualToString:key.input] && + command.keyCommand.modifierFlags == key.modifierFlags && + command.block) { + + firstPress = NO; + lastCommand = CACurrentMediaTime(); + lastInput = key.input; + lastModifierFlags = key.modifierFlags; + return; + } + } + } else { + // Second keyevent within 0.2 second, + // with the same key as the first one. + if (CACurrentMediaTime() - lastCommand < 0.2 && + lastInput == key.input && + lastModifierFlags == key.modifierFlags) { + + for (RCTKeyCommand *command in [RCTKeyCommands sharedInstance].commands) { + if ([command.keyCommand.input isEqualToString:key.input] && + command.keyCommand.modifierFlags == key.modifierFlags && + command.block) { + + // NOTE: throttle the key handler because on iOS 9 the handleKeyCommand: + // method gets called repeatedly if the command key is held down. + if (RCTIsIOS8OrEarlier() || CACurrentMediaTime() - lastDoubleCommand > 0.5) { + command.block(key); + lastDoubleCommand = CACurrentMediaTime(); + } + firstPress = YES; + return; + } + } + } + + lastCommand = CACurrentMediaTime(); + lastInput = key.input; + lastModifierFlags = key.modifierFlags; + } +} + @end @implementation UIApplication (RCTKeyCommands) @@ -118,6 +216,9 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) if (action == @selector(RCT_handleKeyCommand:)) { [self RCT_handleKeyCommand:sender]; return YES; + } else if (action == @selector(RCT_handleDoublePressKeyCommand:)) { + [self RCT_handleDoublePressKeyCommand:sender]; + return YES; } return [self RCT_sendAction:action to:target from:sender forEvent:event]; } @@ -130,7 +231,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) { if (RCTIsIOS8OrEarlier()) { - //swizzle UIApplication + // swizzle UIApplication RCTSwapInstanceMethods([UIApplication class], @selector(keyCommands), @selector(RCT_keyCommands)); @@ -140,7 +241,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) @selector(RCT_sendAction:to:from:forEvent:)); } else { - //swizzle UIResponder + // swizzle UIResponder RCTSwapInstanceMethods([UIResponder class], @selector(keyCommands), @selector(RCT_keyCommands)); @@ -218,6 +319,58 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) return NO; } +- (void)registerDoublePressKeyCommandWithInput:(NSString *)input + modifierFlags:(UIKeyModifierFlags)flags + action:(void (^)(UIKeyCommand *))block +{ + RCTAssertMainThread(); + + if (input.length && flags && RCTIsIOS8OrEarlier()) { + + // Workaround around the first cmd not working: http://openradar.appspot.com/19613391 + // You can register just the cmd key and do nothing. This ensures that + // command-key modified commands will work first time. Fixed in iOS 9. + + [self registerDoublePressKeyCommandWithInput:@"" + modifierFlags:flags + action:nil]; + } + + UIKeyCommand *command = [UIKeyCommand keyCommandWithInput:input + modifierFlags:flags + action:@selector(RCT_handleDoublePressKeyCommand:)]; + + RCTKeyCommand *keyCommand = [[RCTKeyCommand alloc] initWithKeyCommand:command block:block]; + [_commands removeObject:keyCommand]; + [_commands addObject:keyCommand]; +} + +- (void)unregisterDoublePressKeyCommandWithInput:(NSString *)input + modifierFlags:(UIKeyModifierFlags)flags +{ + RCTAssertMainThread(); + + for (RCTKeyCommand *command in _commands.allObjects) { + if ([command matchesInput:input flags:flags]) { + [_commands removeObject:command]; + break; + } + } +} + +- (BOOL)isDoublePressKeyCommandRegisteredForInput:(NSString *)input + modifierFlags:(UIKeyModifierFlags)flags +{ + RCTAssertMainThread(); + + for (RCTKeyCommand *command in _commands) { + if ([command matchesInput:input flags:flags]) { + return YES; + } + } + return NO; +} + @end #else @@ -242,6 +395,19 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) return NO; } +- (void)registerDoublePressKeyCommandWithInput:(NSString *)input + modifierFlags:(UIKeyModifierFlags)flags + action:(void (^)(UIKeyCommand *))block {} + +- (void)unregisterDoublePressKeyCommandWithInput:(NSString *)input + modifierFlags:(UIKeyModifierFlags)flags {} + +- (BOOL)isDoublePressKeyCommandRegisteredForInput:(NSString *)input + modifierFlags:(UIKeyModifierFlags)flags +{ + return NO; +} + @end #endif