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+ (void)handleKeyboardEvent:(UIEvent *)event; 72 73@end 74 75@interface UIEvent (UIPhysicalKeyboardEvent) 76 77@property (nonatomic) NSString *_modifiedInput; 78@property (nonatomic) NSString *_unmodifiedInput; 79@property (nonatomic) UIKeyModifierFlags _modifierFlags; 80@property (nonatomic) BOOL _isKeyDown; 81@property (nonatomic) long _keyCode; 82 83@end 84 85 86@implementation UIApplication (EXKeyCommands) 87 88- (void)EX_handleKeyUIEventSwizzle:(UIEvent *)event 89{ 90 BOOL interactionEnabled = !UIApplication.sharedApplication.isIgnoringInteractionEvents; 91 BOOL hasFirstResponder = NO; 92 93 if (interactionEnabled) { 94 UIResponder *firstResponder = nil; 95 for (UIWindow *window in [self windows]) { 96 firstResponder = [window valueForKey:@"firstResponder"]; 97 if (firstResponder) { 98 hasFirstResponder = YES; 99 break; 100 } 101 } 102 103 104 // Call the original swizzled method 105 [self EX_handleKeyUIEventSwizzle:event]; 106 107 if (firstResponder) { 108 BOOL isTextField = [firstResponder isKindOfClass: [UITextField class]] || [firstResponder isKindOfClass: [UITextView class]]; 109 110 if (!isTextField) { 111 [EXKernelDevKeyCommands handleKeyboardEvent:event]; 112 } 113 } 114 } 115}; 116 117@end 118 119@implementation UIResponder (EXKeyCommands) 120 121- (NSArray<UIKeyCommand *> *)EX_keyCommands 122{ 123 NSSet<EXKeyCommand *> *commands = [EXKernelDevKeyCommands sharedInstance].commands; 124 return [[commands valueForKeyPath:@"keyCommand"] allObjects]; 125} 126 127- (void)EX_handleKeyCommand:(UIKeyCommand *)key 128{ 129 // NOTE: throttle the key handler because on iOS 9 the handleKeyCommand: 130 // method gets called repeatedly if the command key is held down. 131 static NSTimeInterval lastCommand = 0; 132 if (CACurrentMediaTime() - lastCommand > 0.5) { 133 for (EXKeyCommand *command in [EXKernelDevKeyCommands sharedInstance].commands) { 134 if ([command.keyCommand.input isEqualToString:key.input] && 135 command.keyCommand.modifierFlags == key.modifierFlags) { 136 if (command.block) { 137 command.block(key); 138 lastCommand = CACurrentMediaTime(); 139 } 140 } 141 } 142 } 143} 144 145@end 146 147@implementation EXKernelDevKeyCommands 148 149+ (instancetype)sharedInstance 150{ 151 static EXKernelDevKeyCommands *instance; 152 static dispatch_once_t once; 153 dispatch_once(&once, ^{ 154 if (!instance) { 155 instance = [[EXKernelDevKeyCommands alloc] init]; 156 } 157 }); 158 return instance; 159} 160 161+ (void)initialize 162{ 163 // capture keycommands across all bridges. 164 // this is the same approach taken by RCTKeyCommands, 165 // but that class is disabled in the expo react native fork 166 // since there may be many instances of it. 167 RCTSwapInstanceMethods([UIResponder class], 168 @selector(keyCommands), 169 @selector(EX_keyCommands)); 170 171 SEL originalKeyboardSelector = NSSelectorFromString(@"handleKeyUIEvent:"); 172 RCTSwapInstanceMethods([UIApplication class], 173 originalKeyboardSelector, 174 @selector(EX_handleKeyUIEventSwizzle:)); 175} 176 177+(void)handleKeyboardEvent:(UIEvent *)event 178{ 179 static NSTimeInterval lastCommand = 0; 180 181 if (event._isKeyDown) { 182 if (CACurrentMediaTime() - lastCommand > 0.5) { 183 NSString *input = event._modifiedInput; 184 if ([input isEqualToString: @"r"]) { 185 [[EXKernel sharedInstance] reloadVisibleApp]; 186 } 187 188 lastCommand = CACurrentMediaTime(); 189 } 190 } 191} 192 193- (instancetype)init 194{ 195 if ((self = [super init])) { 196 _commands = [NSMutableSet set]; 197 } 198 return self; 199} 200 201#pragma mark - expo dev commands 202 203- (void)registerDevCommands 204{ 205 __weak typeof(self) weakSelf = self; 206 [self registerKeyCommandWithInput:@"d" 207 modifierFlags:UIKeyModifierCommand 208 action:^(__unused UIKeyCommand *_) { 209 [weakSelf _handleMenuCommand]; 210 }]; 211 [self registerKeyCommandWithInput:@"r" 212 modifierFlags:UIKeyModifierCommand 213 action:^(__unused UIKeyCommand *_) { 214 [weakSelf _handleRefreshCommand]; 215 }]; 216 [self registerKeyCommandWithInput:@"n" 217 modifierFlags:UIKeyModifierCommand 218 action:^(__unused UIKeyCommand *_) { 219 [weakSelf _handleDisableDebuggingCommand]; 220 }]; 221 [self registerKeyCommandWithInput:@"i" 222 modifierFlags:UIKeyModifierCommand 223 action:^(__unused UIKeyCommand *_) { 224 [weakSelf _handleToggleInspectorCommand]; 225 }]; 226 [self registerKeyCommandWithInput:@"k" 227 modifierFlags:UIKeyModifierCommand | UIKeyModifierControl 228 action:^(__unused UIKeyCommand *_) { 229 [weakSelf _handleKernelMenuCommand]; 230 }]; 231 232} 233 234- (void)_handleMenuCommand 235{ 236 if ([EXEnvironment sharedEnvironment].isDetached) { 237 [[EXKernel sharedInstance].visibleApp.appManager showDevMenu]; 238 } else { 239 [[EXKernel sharedInstance] switchTasks]; 240 } 241} 242 243- (void)_handleRefreshCommand 244{ 245 // This reloads only JS 246 // [[EXKernel sharedInstance].visibleApp.appManager reloadBridge]; 247 248 // This reloads manifest and JS 249 [[EXKernel sharedInstance] reloadVisibleApp]; 250} 251 252- (void)_handleDisableDebuggingCommand 253{ 254 [[EXKernel sharedInstance].visibleApp.appManager disableRemoteDebugging]; 255} 256 257- (void)_handleToggleRemoteDebuggingCommand 258{ 259 [[EXKernel sharedInstance].visibleApp.appManager toggleRemoteDebugging]; 260 // This reloads manifest and JS 261 [[EXKernel sharedInstance] reloadVisibleApp]; 262} 263 264- (void)_handleTogglePerformanceMonitorCommand 265{ 266 [[EXKernel sharedInstance].visibleApp.appManager togglePerformanceMonitor]; 267} 268 269- (void)_handleToggleInspectorCommand 270{ 271 [[EXKernel sharedInstance].visibleApp.appManager toggleElementInspector]; 272} 273 274- (void)_handleKernelMenuCommand 275{ 276 if ([EXKernel sharedInstance].visibleApp == [EXKernel sharedInstance].appRegistry.homeAppRecord) { 277 [[EXKernel sharedInstance].appRegistry.homeAppRecord.appManager showDevMenu]; 278 } 279} 280 281#pragma mark - managing list of commands 282 283- (void)registerKeyCommandWithInput:(NSString *)input 284 modifierFlags:(UIKeyModifierFlags)flags 285 action:(void (^)(UIKeyCommand *))block 286{ 287 RCTAssertMainQueue(); 288 289 UIKeyCommand *command = [UIKeyCommand keyCommandWithInput:input 290 modifierFlags:flags 291 action:@selector(EX_handleKeyCommand:)]; 292 293 EXKeyCommand *keyCommand = [[EXKeyCommand alloc] initWithKeyCommand:command block:block]; 294 [_commands removeObject:keyCommand]; 295 [_commands addObject:keyCommand]; 296} 297 298- (void)unregisterKeyCommandWithInput:(NSString *)input 299 modifierFlags:(UIKeyModifierFlags)flags 300{ 301 RCTAssertMainQueue(); 302 303 for (EXKeyCommand *command in _commands.allObjects) { 304 if ([command matchesInput:input flags:flags]) { 305 [_commands removeObject:command]; 306 break; 307 } 308 } 309} 310 311- (BOOL)isKeyCommandRegisteredForInput:(NSString *)input 312 modifierFlags:(UIKeyModifierFlags)flags 313{ 314 RCTAssertMainQueue(); 315 316 for (EXKeyCommand *command in _commands) { 317 if ([command matchesInput:input flags:flags]) { 318 return YES; 319 } 320 } 321 return NO; 322} 323 324@end 325 326