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