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