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 [[EXKernel sharedInstance].visibleApp.appManager reloadBridge]; 202} 203 204- (void)_handleDisableDebuggingCommand 205{ 206 [[EXKernel sharedInstance].visibleApp.appManager disableRemoteDebugging]; 207} 208 209- (void)_handleToggleInspectorCommand 210{ 211 [[EXKernel sharedInstance].visibleApp.appManager toggleElementInspector]; 212} 213 214- (void)_handleKernelMenuCommand 215{ 216 if ([EXKernel sharedInstance].visibleApp == [EXKernel sharedInstance].appRegistry.homeAppRecord) { 217 [[EXKernel sharedInstance].appRegistry.homeAppRecord.appManager showDevMenu]; 218 } 219} 220 221#pragma mark - managing list of commands 222 223- (void)registerKeyCommandWithInput:(NSString *)input 224 modifierFlags:(UIKeyModifierFlags)flags 225 action:(void (^)(UIKeyCommand *))block 226{ 227 RCTAssertMainQueue(); 228 229 UIKeyCommand *command = [UIKeyCommand keyCommandWithInput:input 230 modifierFlags:flags 231 action:@selector(EX_handleKeyCommand:)]; 232 233 EXKeyCommand *keyCommand = [[EXKeyCommand alloc] initWithKeyCommand:command block:block]; 234 [_commands removeObject:keyCommand]; 235 [_commands addObject:keyCommand]; 236} 237 238- (void)unregisterKeyCommandWithInput:(NSString *)input 239 modifierFlags:(UIKeyModifierFlags)flags 240{ 241 RCTAssertMainQueue(); 242 243 for (EXKeyCommand *command in _commands.allObjects) { 244 if ([command matchesInput:input flags:flags]) { 245 [_commands removeObject:command]; 246 break; 247 } 248 } 249} 250 251- (BOOL)isKeyCommandRegisteredForInput:(NSString *)input 252 modifierFlags:(UIKeyModifierFlags)flags 253{ 254 RCTAssertMainQueue(); 255 256 for (EXKeyCommand *command in _commands) { 257 if ([command matchesInput:input flags:flags]) { 258 return YES; 259 } 260 } 261 return NO; 262} 263 264@end 265 266