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