1// Copyright 2015-present 650 Industries. All rights reserved. 2 3#import "EXEnvironment.h" 4#import "EXKernelDevKeyCommands.h" 5#import "EXKernel.h" 6#import "EXKernelAppRegistry.h" 7#import "EXReactAppManager.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 } 132 return self; 133} 134 135#pragma mark - expo dev commands 136 137- (void)registerDevCommands 138{ 139 __weak typeof(self) weakSelf = self; 140 [self registerKeyCommandWithInput:@"d" 141 modifierFlags:UIKeyModifierCommand 142 action:^(__unused UIKeyCommand *_) { 143 [weakSelf _handleMenuCommand]; 144 }]; 145 [self registerKeyCommandWithInput:@"r" 146 modifierFlags:UIKeyModifierCommand 147 action:^(__unused UIKeyCommand *_) { 148 [weakSelf _handleRefreshCommand]; 149 }]; 150 [self registerKeyCommandWithInput:@"n" 151 modifierFlags:UIKeyModifierCommand 152 action:^(__unused UIKeyCommand *_) { 153 [weakSelf _handleDisableDebuggingCommand]; 154 }]; 155 [self registerKeyCommandWithInput:@"i" 156 modifierFlags:UIKeyModifierCommand 157 action:^(__unused UIKeyCommand *_) { 158 [weakSelf _handleToggleInspectorCommand]; 159 }]; 160 [self registerKeyCommandWithInput:@"k" 161 modifierFlags:UIKeyModifierCommand | UIKeyModifierControl 162 action:^(__unused UIKeyCommand *_) { 163 [weakSelf _handleKernelMenuCommand]; 164 }]; 165 166} 167 168- (void)_handleMenuCommand 169{ 170 if ([EXEnvironment sharedEnvironment].isDetached) { 171 [[EXKernel sharedInstance].visibleApp.appManager showDevMenu]; 172 } else { 173 [[EXKernel sharedInstance] switchTasks]; 174 } 175} 176 177- (void)_handleRefreshCommand 178{ 179 // This reloads only JS 180 // [[EXKernel sharedInstance].visibleApp.appManager reloadBridge]; 181 182 // This reloads manifest and JS 183 [[EXKernel sharedInstance] reloadVisibleApp]; 184} 185 186- (void)_handleDisableDebuggingCommand 187{ 188 [[EXKernel sharedInstance].visibleApp.appManager disableRemoteDebugging]; 189} 190 191- (void)_handleToggleRemoteDebuggingCommand 192{ 193 [[EXKernel sharedInstance].visibleApp.appManager toggleRemoteDebugging]; 194 // This reloads manifest and JS 195 [[EXKernel sharedInstance] reloadVisibleApp]; 196} 197 198- (void)_handleTogglePerformanceMonitorCommand 199{ 200 [[EXKernel sharedInstance].visibleApp.appManager togglePerformanceMonitor]; 201} 202 203- (void)_handleToggleInspectorCommand 204{ 205 [[EXKernel sharedInstance].visibleApp.appManager toggleElementInspector]; 206} 207 208- (void)_handleKernelMenuCommand 209{ 210 if ([EXKernel sharedInstance].visibleApp == [EXKernel sharedInstance].appRegistry.homeAppRecord) { 211 [[EXKernel sharedInstance].appRegistry.homeAppRecord.appManager showDevMenu]; 212 } 213} 214 215#pragma mark - managing list of commands 216 217- (void)registerKeyCommandWithInput:(NSString *)input 218 modifierFlags:(UIKeyModifierFlags)flags 219 action:(void (^)(UIKeyCommand *))block 220{ 221 RCTAssertMainQueue(); 222 223 UIKeyCommand *command = [UIKeyCommand keyCommandWithInput:input 224 modifierFlags:flags 225 action:@selector(EX_handleKeyCommand:)]; 226 227 EXKeyCommand *keyCommand = [[EXKeyCommand alloc] initWithKeyCommand:command block:block]; 228 [_commands removeObject:keyCommand]; 229 [_commands addObject:keyCommand]; 230} 231 232- (void)unregisterKeyCommandWithInput:(NSString *)input 233 modifierFlags:(UIKeyModifierFlags)flags 234{ 235 RCTAssertMainQueue(); 236 237 for (EXKeyCommand *command in _commands.allObjects) { 238 if ([command matchesInput:input flags:flags]) { 239 [_commands removeObject:command]; 240 break; 241 } 242 } 243} 244 245- (BOOL)isKeyCommandRegisteredForInput:(NSString *)input 246 modifierFlags:(UIKeyModifierFlags)flags 247{ 248 RCTAssertMainQueue(); 249 250 for (EXKeyCommand *command in _commands) { 251 if ([command matchesInput:input flags:flags]) { 252 return YES; 253 } 254 } 255 return NO; 256} 257 258@end 259 260