1// Copyright 2015-present 650 Industries. All rights reserved. 2 3#import "EXKernelDevKeyCommands.h" 4#import "EXKernel.h" 5#import "EXKernelAppRegistry.h" 6#import "EXReactAppManager.h" 7#import "EXShellManager.h" 8 9#import <React/RCTDefines.h> 10#import <React/RCTUtils.h> 11 12#import <UIKit/UIKit.h> 13 14@interface EXKeyCommand : NSObject <NSCopying> 15 16@property (nonatomic, strong) UIKeyCommand *keyCommand; 17@property (nonatomic, copy) void (^block)(UIKeyCommand *); 18 19@end 20 21@implementation EXKeyCommand 22 23- (instancetype)initWithKeyCommand:(UIKeyCommand *)keyCommand 24 block:(void (^)(UIKeyCommand *))block 25{ 26 if ((self = [super init])) { 27 _keyCommand = keyCommand; 28 _block = block; 29 } 30 return self; 31} 32 33RCT_NOT_IMPLEMENTED(- (instancetype)init) 34 35- (id)copyWithZone:(__unused NSZone *)zone 36{ 37 return self; 38} 39 40- (NSUInteger)hash 41{ 42 return _keyCommand.input.hash ^ _keyCommand.modifierFlags; 43} 44 45- (BOOL)isEqual:(EXKeyCommand *)object 46{ 47 if (![object isKindOfClass:[EXKeyCommand class]]) { 48 return NO; 49 } 50 return [self matchesInput:object.keyCommand.input 51 flags:object.keyCommand.modifierFlags]; 52} 53 54- (BOOL)matchesInput:(NSString *)input flags:(UIKeyModifierFlags)flags 55{ 56 return [_keyCommand.input isEqual:input] && _keyCommand.modifierFlags == flags; 57} 58 59- (NSString *)description 60{ 61 return [NSString stringWithFormat:@"<%@:%p input=\"%@\" flags=%zd hasBlock=%@>", 62 [self class], self, _keyCommand.input, _keyCommand.modifierFlags, 63 _block ? @"YES" : @"NO"]; 64} 65 66@end 67 68@interface EXKernelDevKeyCommands () 69 70@property (nonatomic, strong) NSMutableSet<EXKeyCommand *> *commands; 71 72@end 73 74@implementation UIResponder (EXKeyCommands) 75 76- (NSArray<UIKeyCommand *> *)EX_keyCommands 77{ 78 NSSet<EXKeyCommand *> *commands = [EXKernelDevKeyCommands sharedInstance].commands; 79 return [[commands valueForKeyPath:@"keyCommand"] allObjects]; 80} 81 82- (void)EX_handleKeyCommand:(UIKeyCommand *)key 83{ 84 // NOTE: throttle the key handler because on iOS 9 the handleKeyCommand: 85 // method gets called repeatedly if the command key is held down. 86 static NSTimeInterval lastCommand = 0; 87 if (CACurrentMediaTime() - lastCommand > 0.5) { 88 for (EXKeyCommand *command in [EXKernelDevKeyCommands sharedInstance].commands) { 89 if ([command.keyCommand.input isEqualToString:key.input] && 90 command.keyCommand.modifierFlags == key.modifierFlags) { 91 if (command.block) { 92 command.block(key); 93 lastCommand = CACurrentMediaTime(); 94 } 95 } 96 } 97 } 98} 99 100@end 101 102@implementation EXKernelDevKeyCommands 103 104+ (instancetype)sharedInstance 105{ 106 static EXKernelDevKeyCommands *instance; 107 static dispatch_once_t once; 108 dispatch_once(&once, ^{ 109 if (!instance) { 110 instance = [[EXKernelDevKeyCommands alloc] init]; 111 } 112 }); 113 return instance; 114} 115 116+ (void)initialize 117{ 118 // capture keycommands across all bridges. 119 // this is the same approach taken by RCTKeyCommands, 120 // but that class is disabled in the expo react native fork 121 // since there may be many instances of it. 122 RCTSwapInstanceMethods([UIResponder class], 123 @selector(keyCommands), 124 @selector(EX_keyCommands)); 125} 126 127- (instancetype)init 128{ 129 if ((self = [super init])) { 130 _commands = [NSMutableSet set]; 131 _isLegacyMenuBehaviorEnabled = NO; 132 [self _addDevCommands]; 133 } 134 return self; 135} 136 137#pragma mark - expo dev commands 138 139- (void)_addDevCommands 140{ 141 __weak typeof(self) weakSelf = self; 142 [self registerKeyCommandWithInput:@"d" 143 modifierFlags:UIKeyModifierCommand 144 action:^(__unused UIKeyCommand *_) { 145 [weakSelf _handleMenuCommand]; 146 }]; 147 [self registerKeyCommandWithInput:@"r" 148 modifierFlags:UIKeyModifierCommand 149 action:^(__unused UIKeyCommand *_) { 150 [weakSelf _handleRefreshCommand]; 151 }]; 152 [self registerKeyCommandWithInput:@"n" 153 modifierFlags:UIKeyModifierCommand 154 action:^(__unused UIKeyCommand *_) { 155 [weakSelf _handleDisableDebuggingCommand]; 156 }]; 157 [self registerKeyCommandWithInput:@"i" 158 modifierFlags:UIKeyModifierCommand 159 action:^(__unused UIKeyCommand *_) { 160 [weakSelf _handleToggleInspectorCommand]; 161 }]; 162 [self registerKeyCommandWithInput:@"k" 163 modifierFlags:UIKeyModifierCommand | UIKeyModifierControl 164 action:^(__unused UIKeyCommand *_) { 165 [weakSelf _handleKernelMenuCommand]; 166 }]; 167} 168 169- (void)_handleMenuCommand 170{ 171 if ([EXShellManager sharedInstance].isDetached || _isLegacyMenuBehaviorEnabled) { 172 [[EXKernel sharedInstance].visibleApp.appManager showDevMenu]; 173 } else { 174 [[EXKernel sharedInstance] switchTasks]; 175 } 176} 177 178- (void)_handleRefreshCommand 179{ 180 [[EXKernel sharedInstance].visibleApp.appManager reloadBridge]; 181} 182 183- (void)_handleDisableDebuggingCommand 184{ 185 [[EXKernel sharedInstance].visibleApp.appManager disableRemoteDebugging]; 186} 187 188- (void)_handleToggleInspectorCommand 189{ 190 [[EXKernel sharedInstance].visibleApp.appManager toggleElementInspector]; 191} 192 193- (void)_handleKernelMenuCommand 194{ 195 if ([EXKernel sharedInstance].visibleApp == [EXKernel sharedInstance].appRegistry.homeAppRecord) { 196 [[EXKernel sharedInstance].appRegistry.homeAppRecord.appManager showDevMenu]; 197 } 198} 199 200#pragma mark - managing list of commands 201 202- (void)registerKeyCommandWithInput:(NSString *)input 203 modifierFlags:(UIKeyModifierFlags)flags 204 action:(void (^)(UIKeyCommand *))block 205{ 206 RCTAssertMainQueue(); 207 208 UIKeyCommand *command = [UIKeyCommand keyCommandWithInput:input 209 modifierFlags:flags 210 action:@selector(EX_handleKeyCommand:)]; 211 212 EXKeyCommand *keyCommand = [[EXKeyCommand alloc] initWithKeyCommand:command block:block]; 213 [_commands removeObject:keyCommand]; 214 [_commands addObject:keyCommand]; 215} 216 217- (void)unregisterKeyCommandWithInput:(NSString *)input 218 modifierFlags:(UIKeyModifierFlags)flags 219{ 220 RCTAssertMainQueue(); 221 222 for (EXKeyCommand *command in _commands.allObjects) { 223 if ([command matchesInput:input flags:flags]) { 224 [_commands removeObject:command]; 225 break; 226 } 227 } 228} 229 230- (BOOL)isKeyCommandRegisteredForInput:(NSString *)input 231 modifierFlags:(UIKeyModifierFlags)flags 232{ 233 RCTAssertMainQueue(); 234 235 for (EXKeyCommand *command in _commands) { 236 if ([command matchesInput:input flags:flags]) { 237 return YES; 238 } 239 } 240 return NO; 241} 242 243@end 244 245