10fbf6a33SBen Roth// Copyright 2015-present 650 Industries. All rights reserved. 20fbf6a33SBen Roth 300fcd3c6SBen Roth#import "EXEnvironment.h" 40fbf6a33SBen Roth#import "EXKernelDevKeyCommands.h" 50fbf6a33SBen Roth#import "EXKernel.h" 689c6c39cSEric Samelson#import "EXKernelAppRegistry.h" 77f6367d0SBen Roth#import "EXReactAppManager.h" 80fbf6a33SBen Roth 9bd9e856bSJames Ide#import <React/RCTDefines.h> 10bd9e856bSJames Ide#import <React/RCTUtils.h> 110fbf6a33SBen Roth 120fbf6a33SBen Roth#import <UIKit/UIKit.h> 130fbf6a33SBen Roth 140fbf6a33SBen Roth@interface EXKeyCommand : NSObject <NSCopying> 150fbf6a33SBen Roth 160fbf6a33SBen Roth@property (nonatomic, strong) UIKeyCommand *keyCommand; 170fbf6a33SBen Roth@property (nonatomic, copy) void (^block)(UIKeyCommand *); 180fbf6a33SBen Roth 190fbf6a33SBen Roth@end 200fbf6a33SBen Roth 210fbf6a33SBen Roth@implementation EXKeyCommand 220fbf6a33SBen Roth 230fbf6a33SBen Roth- (instancetype)initWithKeyCommand:(UIKeyCommand *)keyCommand 240fbf6a33SBen Roth block:(void (^)(UIKeyCommand *))block 250fbf6a33SBen Roth{ 260fbf6a33SBen Roth if ((self = [super init])) { 270fbf6a33SBen Roth _keyCommand = keyCommand; 280fbf6a33SBen Roth _block = block; 290fbf6a33SBen Roth } 300fbf6a33SBen Roth return self; 310fbf6a33SBen Roth} 320fbf6a33SBen Roth 330fbf6a33SBen RothRCT_NOT_IMPLEMENTED(- (instancetype)init) 340fbf6a33SBen Roth 350fbf6a33SBen Roth- (id)copyWithZone:(__unused NSZone *)zone 360fbf6a33SBen Roth{ 370fbf6a33SBen Roth return self; 380fbf6a33SBen Roth} 390fbf6a33SBen Roth 400fbf6a33SBen Roth- (NSUInteger)hash 410fbf6a33SBen Roth{ 420fbf6a33SBen Roth return _keyCommand.input.hash ^ _keyCommand.modifierFlags; 430fbf6a33SBen Roth} 440fbf6a33SBen Roth 450fbf6a33SBen Roth- (BOOL)isEqual:(EXKeyCommand *)object 460fbf6a33SBen Roth{ 470fbf6a33SBen Roth if (![object isKindOfClass:[EXKeyCommand class]]) { 480fbf6a33SBen Roth return NO; 490fbf6a33SBen Roth } 500fbf6a33SBen Roth return [self matchesInput:object.keyCommand.input 510fbf6a33SBen Roth flags:object.keyCommand.modifierFlags]; 520fbf6a33SBen Roth} 530fbf6a33SBen Roth 540fbf6a33SBen Roth- (BOOL)matchesInput:(NSString *)input flags:(UIKeyModifierFlags)flags 550fbf6a33SBen Roth{ 560fbf6a33SBen Roth return [_keyCommand.input isEqual:input] && _keyCommand.modifierFlags == flags; 570fbf6a33SBen Roth} 580fbf6a33SBen Roth 590fbf6a33SBen Roth- (NSString *)description 600fbf6a33SBen Roth{ 610fbf6a33SBen Roth return [NSString stringWithFormat:@"<%@:%p input=\"%@\" flags=%zd hasBlock=%@>", 620fbf6a33SBen Roth [self class], self, _keyCommand.input, _keyCommand.modifierFlags, 630fbf6a33SBen Roth _block ? @"YES" : @"NO"]; 640fbf6a33SBen Roth} 650fbf6a33SBen Roth 660fbf6a33SBen Roth@end 670fbf6a33SBen Roth 680fbf6a33SBen Roth@interface EXKernelDevKeyCommands () 690fbf6a33SBen Roth 700fbf6a33SBen Roth@property (nonatomic, strong) NSMutableSet<EXKeyCommand *> *commands; 7105a98924Sandy+ (void)handleKeyboardEvent:(UIEvent *)event; 720fbf6a33SBen Roth 730fbf6a33SBen Roth@end 740fbf6a33SBen Roth 7505a98924Sandy#if TARGET_IPHONE_SIMULATOR 7605a98924Sandy@interface UIEvent (UIPhysicalKeyboardEvent) 7705a98924Sandy 7805a98924Sandy@property (nonatomic) NSString *_modifiedInput; 7905a98924Sandy@property (nonatomic) NSString *_unmodifiedInput; 8005a98924Sandy@property (nonatomic) UIKeyModifierFlags _modifierFlags; 8105a98924Sandy@property (nonatomic) BOOL _isKeyDown; 8205a98924Sandy@property (nonatomic) long _keyCode; 8305a98924Sandy 8405a98924Sandy@end 8505a98924Sandy 8605a98924Sandy@implementation UIApplication (EXKeyCommands) 8705a98924Sandy 8805a98924Sandy- (void)EX_handleKeyUIEventSwizzle:(UIEvent *)event 8905a98924Sandy{ 9005a98924Sandy BOOL interactionEnabled = !UIApplication.sharedApplication.isIgnoringInteractionEvents; 9105a98924Sandy BOOL hasFirstResponder = NO; 9205a98924Sandy 9305a98924Sandy if (interactionEnabled) { 9405a98924Sandy UIResponder *firstResponder = nil; 9505a98924Sandy for (UIWindow *window in [self windows]) { 9605a98924Sandy firstResponder = [window valueForKey:@"firstResponder"]; 9705a98924Sandy if (firstResponder) { 9805a98924Sandy hasFirstResponder = YES; 9905a98924Sandy break; 10005a98924Sandy } 10105a98924Sandy } 10205a98924Sandy 10305a98924Sandy 10405a98924Sandy // Call the original swizzled method 10505a98924Sandy [self EX_handleKeyUIEventSwizzle:event]; 10605a98924Sandy 10705a98924Sandy if (firstResponder) { 10805a98924Sandy BOOL isTextField = [firstResponder isKindOfClass: [UITextField class]] || [firstResponder isKindOfClass: [UITextView class]]; 10905a98924Sandy 11072551c71Sandy // this is a runtime header that is not publicly exported from the Webkit.framework 11172551c71Sandy // obfuscating selector WKContentView 11272551c71Sandy NSArray<NSString *> *webViewClass = @[ @"WKCo", @"ntentVi", @"ew"]; 11372551c71Sandy Class WKContentView = NSClassFromString([webViewClass componentsJoinedByString:@""]); 11472551c71Sandy 11572551c71Sandy BOOL isWebView = [firstResponder isKindOfClass:[WKContentView class]]; 11672551c71Sandy 11772551c71Sandy if (!isTextField && !isWebView) { 11805a98924Sandy [EXKernelDevKeyCommands handleKeyboardEvent:event]; 11905a98924Sandy } 12005a98924Sandy } 12105a98924Sandy } 12205a98924Sandy}; 12305a98924Sandy 12405a98924Sandy@end 12505a98924Sandy#endif 12605a98924Sandy 1270fbf6a33SBen Roth@implementation UIResponder (EXKeyCommands) 1280fbf6a33SBen Roth 1290fbf6a33SBen Roth- (NSArray<UIKeyCommand *> *)EX_keyCommands 1300fbf6a33SBen Roth{ 131*6c55e685SŁukasz Kosmaty if ([self isKindOfClass:[UITextView class]] || [self isKindOfClass:[UITextField class]]) { 132*6c55e685SŁukasz Kosmaty return @[]; 133*6c55e685SŁukasz Kosmaty } 1340fbf6a33SBen Roth NSSet<EXKeyCommand *> *commands = [EXKernelDevKeyCommands sharedInstance].commands; 1350fbf6a33SBen Roth return [[commands valueForKeyPath:@"keyCommand"] allObjects]; 1360fbf6a33SBen Roth} 1370fbf6a33SBen Roth 1380fbf6a33SBen Roth- (void)EX_handleKeyCommand:(UIKeyCommand *)key 1390fbf6a33SBen Roth{ 1400fbf6a33SBen Roth // NOTE: throttle the key handler because on iOS 9 the handleKeyCommand: 1410fbf6a33SBen Roth // method gets called repeatedly if the command key is held down. 1420fbf6a33SBen Roth static NSTimeInterval lastCommand = 0; 1430fbf6a33SBen Roth if (CACurrentMediaTime() - lastCommand > 0.5) { 1440fbf6a33SBen Roth for (EXKeyCommand *command in [EXKernelDevKeyCommands sharedInstance].commands) { 1450fbf6a33SBen Roth if ([command.keyCommand.input isEqualToString:key.input] && 1460fbf6a33SBen Roth command.keyCommand.modifierFlags == key.modifierFlags) { 1470fbf6a33SBen Roth if (command.block) { 1480fbf6a33SBen Roth command.block(key); 1490fbf6a33SBen Roth lastCommand = CACurrentMediaTime(); 1500fbf6a33SBen Roth } 1510fbf6a33SBen Roth } 1520fbf6a33SBen Roth } 1530fbf6a33SBen Roth } 1540fbf6a33SBen Roth} 1550fbf6a33SBen Roth 1560fbf6a33SBen Roth@end 1570fbf6a33SBen Roth 1580fbf6a33SBen Roth@implementation EXKernelDevKeyCommands 1590fbf6a33SBen Roth 1600fbf6a33SBen Roth+ (instancetype)sharedInstance 1610fbf6a33SBen Roth{ 1620fbf6a33SBen Roth static EXKernelDevKeyCommands *instance; 1630fbf6a33SBen Roth static dispatch_once_t once; 1640fbf6a33SBen Roth dispatch_once(&once, ^{ 1650fbf6a33SBen Roth if (!instance) { 1660fbf6a33SBen Roth instance = [[EXKernelDevKeyCommands alloc] init]; 1670fbf6a33SBen Roth } 1680fbf6a33SBen Roth }); 1690fbf6a33SBen Roth return instance; 1700fbf6a33SBen Roth} 1710fbf6a33SBen Roth 1720fbf6a33SBen Roth+ (void)initialize 1730fbf6a33SBen Roth{ 1740fbf6a33SBen Roth // capture keycommands across all bridges. 1750fbf6a33SBen Roth // this is the same approach taken by RCTKeyCommands, 1760fbf6a33SBen Roth // but that class is disabled in the expo react native fork 1770fbf6a33SBen Roth // since there may be many instances of it. 1780fbf6a33SBen Roth RCTSwapInstanceMethods([UIResponder class], 1790fbf6a33SBen Roth @selector(keyCommands), 1800fbf6a33SBen Roth @selector(EX_keyCommands)); 18105a98924Sandy 18205a98924Sandy#if TARGET_IPHONE_SIMULATOR 18305a98924Sandy SEL originalKeyboardSelector = NSSelectorFromString(@"handleKeyUIEvent:"); 18405a98924Sandy RCTSwapInstanceMethods([UIApplication class], 18505a98924Sandy originalKeyboardSelector, 18605a98924Sandy @selector(EX_handleKeyUIEventSwizzle:)); 18705a98924Sandy#endif 1880fbf6a33SBen Roth} 1890fbf6a33SBen Roth 19005a98924Sandy#if TARGET_IPHONE_SIMULATOR 19105a98924Sandy+(void)handleKeyboardEvent:(UIEvent *)event 19205a98924Sandy{ 19305a98924Sandy static NSTimeInterval lastCommand = 0; 19405a98924Sandy 19505a98924Sandy if (event._isKeyDown) { 19605a98924Sandy if (CACurrentMediaTime() - lastCommand > 0.5) { 19705a98924Sandy NSString *input = event._modifiedInput; 19805a98924Sandy if ([input isEqualToString: @"r"]) { 19905a98924Sandy [[EXKernel sharedInstance] reloadVisibleApp]; 20005a98924Sandy } 20105a98924Sandy 20205a98924Sandy lastCommand = CACurrentMediaTime(); 20305a98924Sandy } 20405a98924Sandy } 20505a98924Sandy} 20605a98924Sandy#endif 20705a98924Sandy 2080fbf6a33SBen Roth- (instancetype)init 2090fbf6a33SBen Roth{ 2100fbf6a33SBen Roth if ((self = [super init])) { 2110fbf6a33SBen Roth _commands = [NSMutableSet set]; 2120fbf6a33SBen Roth } 2130fbf6a33SBen Roth return self; 2140fbf6a33SBen Roth} 2150fbf6a33SBen Roth 2160fbf6a33SBen Roth#pragma mark - expo dev commands 2170fbf6a33SBen Roth 21824a0cefbSTomasz Sapeta- (void)registerDevCommands 2190fbf6a33SBen Roth{ 2200fbf6a33SBen Roth __weak typeof(self) weakSelf = self; 2210fbf6a33SBen Roth [self registerKeyCommandWithInput:@"d" 2220fbf6a33SBen Roth modifierFlags:UIKeyModifierCommand 2230fbf6a33SBen Roth action:^(__unused UIKeyCommand *_) { 2240fbf6a33SBen Roth [weakSelf _handleMenuCommand]; 2250fbf6a33SBen Roth }]; 2260fbf6a33SBen Roth [self registerKeyCommandWithInput:@"r" 2270fbf6a33SBen Roth modifierFlags:UIKeyModifierCommand 2280fbf6a33SBen Roth action:^(__unused UIKeyCommand *_) { 2290fbf6a33SBen Roth [weakSelf _handleRefreshCommand]; 2300fbf6a33SBen Roth }]; 2310fbf6a33SBen Roth [self registerKeyCommandWithInput:@"n" 2320fbf6a33SBen Roth modifierFlags:UIKeyModifierCommand 2330fbf6a33SBen Roth action:^(__unused UIKeyCommand *_) { 2340fbf6a33SBen Roth [weakSelf _handleDisableDebuggingCommand]; 2350fbf6a33SBen Roth }]; 2360fbf6a33SBen Roth [self registerKeyCommandWithInput:@"i" 2370fbf6a33SBen Roth modifierFlags:UIKeyModifierCommand 2380fbf6a33SBen Roth action:^(__unused UIKeyCommand *_) { 2390fbf6a33SBen Roth [weakSelf _handleToggleInspectorCommand]; 2400fbf6a33SBen Roth }]; 2410fbf6a33SBen Roth [self registerKeyCommandWithInput:@"k" 2420fbf6a33SBen Roth modifierFlags:UIKeyModifierCommand | UIKeyModifierControl 2430fbf6a33SBen Roth action:^(__unused UIKeyCommand *_) { 2440fbf6a33SBen Roth [weakSelf _handleKernelMenuCommand]; 2450fbf6a33SBen Roth }]; 24669f88450SEvan Bacon 2470fbf6a33SBen Roth} 2480fbf6a33SBen Roth 2490fbf6a33SBen Roth- (void)_handleMenuCommand 2500fbf6a33SBen Roth{ 25124a0cefbSTomasz Sapeta if ([EXEnvironment sharedEnvironment].isDetached) { 2527f6367d0SBen Roth [[EXKernel sharedInstance].visibleApp.appManager showDevMenu]; 2530fbf6a33SBen Roth } else { 2547f6367d0SBen Roth [[EXKernel sharedInstance] switchTasks]; 2550fbf6a33SBen Roth } 2560fbf6a33SBen Roth} 2570fbf6a33SBen Roth 2580fbf6a33SBen Roth- (void)_handleRefreshCommand 2590fbf6a33SBen Roth{ 260b333b418SBrent Vatne // This reloads only JS 261b333b418SBrent Vatne // [[EXKernel sharedInstance].visibleApp.appManager reloadBridge]; 262b333b418SBrent Vatne 263b333b418SBrent Vatne // This reloads manifest and JS 264b333b418SBrent Vatne [[EXKernel sharedInstance] reloadVisibleApp]; 2650fbf6a33SBen Roth} 2660fbf6a33SBen Roth 2670fbf6a33SBen Roth- (void)_handleDisableDebuggingCommand 2680fbf6a33SBen Roth{ 2697f6367d0SBen Roth [[EXKernel sharedInstance].visibleApp.appManager disableRemoteDebugging]; 2700fbf6a33SBen Roth} 2710fbf6a33SBen Roth 27269f88450SEvan Bacon- (void)_handleToggleRemoteDebuggingCommand 27369f88450SEvan Bacon{ 27469f88450SEvan Bacon [[EXKernel sharedInstance].visibleApp.appManager toggleRemoteDebugging]; 27569f88450SEvan Bacon // This reloads manifest and JS 27669f88450SEvan Bacon [[EXKernel sharedInstance] reloadVisibleApp]; 27769f88450SEvan Bacon} 27869f88450SEvan Bacon 27969f88450SEvan Bacon- (void)_handleTogglePerformanceMonitorCommand 28069f88450SEvan Bacon{ 28169f88450SEvan Bacon [[EXKernel sharedInstance].visibleApp.appManager togglePerformanceMonitor]; 28269f88450SEvan Bacon} 28369f88450SEvan Bacon 2840fbf6a33SBen Roth- (void)_handleToggleInspectorCommand 2850fbf6a33SBen Roth{ 2867f6367d0SBen Roth [[EXKernel sharedInstance].visibleApp.appManager toggleElementInspector]; 2870fbf6a33SBen Roth} 2880fbf6a33SBen Roth 2890fbf6a33SBen Roth- (void)_handleKernelMenuCommand 2900fbf6a33SBen Roth{ 2917f6367d0SBen Roth if ([EXKernel sharedInstance].visibleApp == [EXKernel sharedInstance].appRegistry.homeAppRecord) { 2927f6367d0SBen Roth [[EXKernel sharedInstance].appRegistry.homeAppRecord.appManager showDevMenu]; 2930fbf6a33SBen Roth } 2940fbf6a33SBen Roth} 2950fbf6a33SBen Roth 2960fbf6a33SBen Roth#pragma mark - managing list of commands 2970fbf6a33SBen Roth 2980fbf6a33SBen Roth- (void)registerKeyCommandWithInput:(NSString *)input 2990fbf6a33SBen Roth modifierFlags:(UIKeyModifierFlags)flags 3000fbf6a33SBen Roth action:(void (^)(UIKeyCommand *))block 3010fbf6a33SBen Roth{ 3020fbf6a33SBen Roth RCTAssertMainQueue(); 3030fbf6a33SBen Roth 3040fbf6a33SBen Roth UIKeyCommand *command = [UIKeyCommand keyCommandWithInput:input 3050fbf6a33SBen Roth modifierFlags:flags 3060fbf6a33SBen Roth action:@selector(EX_handleKeyCommand:)]; 3070fbf6a33SBen Roth 3080fbf6a33SBen Roth EXKeyCommand *keyCommand = [[EXKeyCommand alloc] initWithKeyCommand:command block:block]; 3090fbf6a33SBen Roth [_commands removeObject:keyCommand]; 3100fbf6a33SBen Roth [_commands addObject:keyCommand]; 3110fbf6a33SBen Roth} 3120fbf6a33SBen Roth 3130fbf6a33SBen Roth- (void)unregisterKeyCommandWithInput:(NSString *)input 3140fbf6a33SBen Roth modifierFlags:(UIKeyModifierFlags)flags 3150fbf6a33SBen Roth{ 3160fbf6a33SBen Roth RCTAssertMainQueue(); 3170fbf6a33SBen Roth 3180fbf6a33SBen Roth for (EXKeyCommand *command in _commands.allObjects) { 3190fbf6a33SBen Roth if ([command matchesInput:input flags:flags]) { 3200fbf6a33SBen Roth [_commands removeObject:command]; 3210fbf6a33SBen Roth break; 3220fbf6a33SBen Roth } 3230fbf6a33SBen Roth } 3240fbf6a33SBen Roth} 3250fbf6a33SBen Roth 3260fbf6a33SBen Roth- (BOOL)isKeyCommandRegisteredForInput:(NSString *)input 3270fbf6a33SBen Roth modifierFlags:(UIKeyModifierFlags)flags 3280fbf6a33SBen Roth{ 3290fbf6a33SBen Roth RCTAssertMainQueue(); 3300fbf6a33SBen Roth 3310fbf6a33SBen Roth for (EXKeyCommand *command in _commands) { 3320fbf6a33SBen Roth if ([command matchesInput:input flags:flags]) { 3330fbf6a33SBen Roth return YES; 3340fbf6a33SBen Roth } 3350fbf6a33SBen Roth } 3360fbf6a33SBen Roth return NO; 3370fbf6a33SBen Roth} 3380fbf6a33SBen Roth 3390fbf6a33SBen Roth@end 3400fbf6a33SBen Roth 341