FLEXManager+Extensibility.m 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. //
  2. // FLEXManager+Extensibility.m
  3. // FLEX
  4. //
  5. // Created by Tanner on 2/2/20.
  6. // Copyright © 2020 FLEX Team. All rights reserved.
  7. //
  8. #import "FLEXManager+Extensibility.h"
  9. #import "FLEXManager+Private.h"
  10. #import "FLEXNavigationController.h"
  11. #import "FLEXGlobalsEntry.h"
  12. #import "FLEXObjectExplorerFactory.h"
  13. #import "FLEXKeyboardShortcutManager.h"
  14. #import "FLEXExplorerViewController.h"
  15. #import "FLEXNetworkMITMViewController.h"
  16. #import "FLEXKeyboardHelpViewController.h"
  17. #import "FLEXFileBrowserController.h"
  18. #import "FLEXUtility.h"
  19. @interface FLEXManager (ExtensibilityPrivate)
  20. @property (nonatomic, readonly) UIViewController *topViewController;
  21. @end
  22. @implementation FLEXManager (Extensibility)
  23. #pragma mark - Globals Screen Entries
  24. - (void)registerGlobalEntryWithName:(NSString *)entryName objectFutureBlock:(id (^)(void))objectFutureBlock {
  25. NSParameterAssert(entryName);
  26. NSParameterAssert(objectFutureBlock);
  27. NSAssert(NSThread.isMainThread, @"This method must be called from the main thread.");
  28. entryName = entryName.copy;
  29. FLEXGlobalsEntry *entry = [FLEXGlobalsEntry entryWithNameFuture:^NSString *{
  30. return entryName;
  31. } viewControllerFuture:^UIViewController *{
  32. return [FLEXObjectExplorerFactory explorerViewControllerForObject:objectFutureBlock()];
  33. }];
  34. [self.userGlobalEntries addObject:entry];
  35. }
  36. - (void)registerGlobalEntryWithName:(NSString *)entryName viewControllerFutureBlock:(UIViewController * (^)(void))viewControllerFutureBlock {
  37. NSParameterAssert(entryName);
  38. NSParameterAssert(viewControllerFutureBlock);
  39. NSAssert(NSThread.isMainThread, @"This method must be called from the main thread.");
  40. entryName = entryName.copy;
  41. FLEXGlobalsEntry *entry = [FLEXGlobalsEntry entryWithNameFuture:^NSString *{
  42. return entryName;
  43. } viewControllerFuture:^UIViewController *{
  44. UIViewController *viewController = viewControllerFutureBlock();
  45. NSCAssert(viewController, @"'%@' entry returned nil viewController. viewControllerFutureBlock should never return nil.", entryName);
  46. return viewController;
  47. }];
  48. [self.userGlobalEntries addObject:entry];
  49. }
  50. #pragma mark - Simulator Shortcuts
  51. - (void)registerSimulatorShortcutWithKey:(NSString *)key modifiers:(UIKeyModifierFlags)modifiers action:(dispatch_block_t)action description:(NSString *)description {
  52. #if TARGET_OS_SIMULATOR
  53. [FLEXKeyboardShortcutManager.sharedManager registerSimulatorShortcutWithKey:key modifiers:modifiers action:action description:description allowOverride:YES];
  54. #endif
  55. }
  56. - (void)setSimulatorShortcutsEnabled:(BOOL)simulatorShortcutsEnabled {
  57. #if TARGET_OS_SIMULATOR
  58. [FLEXKeyboardShortcutManager.sharedManager setEnabled:simulatorShortcutsEnabled];
  59. #endif
  60. }
  61. - (BOOL)simulatorShortcutsEnabled {
  62. #if TARGET_OS_SIMULATOR
  63. return FLEXKeyboardShortcutManager.sharedManager.isEnabled;
  64. #else
  65. return NO;
  66. #endif
  67. }
  68. #pragma mark - Shortcuts Defaults
  69. - (void)registerDefaultSimulatorShortcutWithKey:(NSString *)key modifiers:(UIKeyModifierFlags)modifiers action:(dispatch_block_t)action description:(NSString *)description {
  70. #if TARGET_OS_SIMULATOR
  71. // Don't allow override to avoid changing keys registered by the app
  72. [FLEXKeyboardShortcutManager.sharedManager registerSimulatorShortcutWithKey:key modifiers:modifiers action:action description:description allowOverride:NO];
  73. #endif
  74. }
  75. - (void)registerDefaultSimulatorShortcuts {
  76. [self registerDefaultSimulatorShortcutWithKey:@"f" modifiers:0 action:^{
  77. [self toggleExplorer];
  78. } description:@"Toggle FLEX toolbar"];
  79. [self registerDefaultSimulatorShortcutWithKey:@"g" modifiers:0 action:^{
  80. [self showExplorerIfNeeded];
  81. [self.explorerViewController toggleMenuTool];
  82. } description:@"Toggle FLEX globals menu"];
  83. [self registerDefaultSimulatorShortcutWithKey:@"v" modifiers:0 action:^{
  84. [self showExplorerIfNeeded];
  85. [self.explorerViewController toggleViewsTool];
  86. } description:@"Toggle view hierarchy menu"];
  87. [self registerDefaultSimulatorShortcutWithKey:@"s" modifiers:0 action:^{
  88. [self showExplorerIfNeeded];
  89. [self.explorerViewController toggleSelectTool];
  90. } description:@"Toggle select tool"];
  91. [self registerDefaultSimulatorShortcutWithKey:@"m" modifiers:0 action:^{
  92. [self showExplorerIfNeeded];
  93. [self.explorerViewController toggleMoveTool];
  94. } description:@"Toggle move tool"];
  95. [self registerDefaultSimulatorShortcutWithKey:@"n" modifiers:0 action:^{
  96. [self toggleTopViewControllerOfClass:[FLEXNetworkMITMViewController class]];
  97. } description:@"Toggle network history view"];
  98. // 't' is for testing: quickly present an object explorer for debugging
  99. [self registerDefaultSimulatorShortcutWithKey:@"t" modifiers:0 action:^{
  100. [self showExplorerIfNeeded];
  101. [self.explorerViewController toggleToolWithViewControllerProvider:^UINavigationController *{
  102. return [FLEXNavigationController withRootViewController:[FLEXObjectExplorerFactory
  103. explorerViewControllerForObject:NSBundle.mainBundle
  104. ]];
  105. } completion:nil];
  106. } description:@"Present an object explorer for debugging"];
  107. [self registerDefaultSimulatorShortcutWithKey:UIKeyInputDownArrow modifiers:0 action:^{
  108. if (self.isHidden || ![self.explorerViewController handleDownArrowKeyPressed]) {
  109. [self tryScrollDown];
  110. }
  111. } description:@"Cycle view selection\n\t\tMove view down\n\t\tScroll down"];
  112. [self registerDefaultSimulatorShortcutWithKey:UIKeyInputUpArrow modifiers:0 action:^{
  113. if (self.isHidden || ![self.explorerViewController handleUpArrowKeyPressed]) {
  114. [self tryScrollUp];
  115. }
  116. } description:@"Cycle view selection\n\t\tMove view up\n\t\tScroll up"];
  117. [self registerDefaultSimulatorShortcutWithKey:UIKeyInputRightArrow modifiers:0 action:^{
  118. if (!self.isHidden) {
  119. [self.explorerViewController handleRightArrowKeyPressed];
  120. }
  121. } description:@"Move selected view right"];
  122. [self registerDefaultSimulatorShortcutWithKey:UIKeyInputLeftArrow modifiers:0 action:^{
  123. if (self.isHidden) {
  124. [self tryGoBack];
  125. } else {
  126. [self.explorerViewController handleLeftArrowKeyPressed];
  127. }
  128. } description:@"Move selected view left"];
  129. [self registerDefaultSimulatorShortcutWithKey:@"?" modifiers:0 action:^{
  130. [self toggleTopViewControllerOfClass:[FLEXKeyboardHelpViewController class]];
  131. } description:@"Toggle (this) help menu"];
  132. [self registerDefaultSimulatorShortcutWithKey:UIKeyInputEscape modifiers:0 action:^{
  133. [[self.topViewController presentingViewController] dismissViewControllerAnimated:YES completion:nil];
  134. } description:@"End editing text\n\t\tDismiss top view controller"];
  135. [self registerDefaultSimulatorShortcutWithKey:@"o" modifiers:UIKeyModifierCommand|UIKeyModifierShift action:^{
  136. [self toggleTopViewControllerOfClass:[FLEXFileBrowserController class]];
  137. } description:@"Toggle file browser menu"];
  138. }
  139. + (void)load {
  140. dispatch_async(dispatch_get_main_queue(), ^{
  141. [self.sharedManager registerDefaultSimulatorShortcuts];
  142. });
  143. }
  144. #pragma mark - Private
  145. - (UIEdgeInsets)contentInsetsOfScrollView:(UIScrollView *)scrollView {
  146. if (@available(iOS 11, *)) {
  147. return scrollView.adjustedContentInset;
  148. }
  149. return scrollView.contentInset;
  150. }
  151. - (void)tryScrollDown {
  152. UIScrollView *scrollview = [self firstScrollView];
  153. UIEdgeInsets insets = [self contentInsetsOfScrollView:scrollview];
  154. CGPoint contentOffset = scrollview.contentOffset;
  155. CGFloat maxYOffset = scrollview.contentSize.height - scrollview.bounds.size.height + insets.bottom;
  156. contentOffset.y = MIN(contentOffset.y + 200, maxYOffset);
  157. [scrollview setContentOffset:contentOffset animated:YES];
  158. }
  159. - (void)tryScrollUp {
  160. UIScrollView *scrollview = [self firstScrollView];
  161. UIEdgeInsets insets = [self contentInsetsOfScrollView:scrollview];
  162. CGPoint contentOffset = scrollview.contentOffset;
  163. contentOffset.y = MAX(contentOffset.y - 200, -insets.top);
  164. [scrollview setContentOffset:contentOffset animated:YES];
  165. }
  166. - (UIScrollView *)firstScrollView {
  167. NSMutableArray<UIView *> *views = FLEXUtility.appKeyWindow.subviews.mutableCopy;
  168. UIScrollView *scrollView = nil;
  169. while (views.count > 0) {
  170. UIView *view = views.firstObject;
  171. [views removeObjectAtIndex:0];
  172. if ([view isKindOfClass:[UIScrollView class]]) {
  173. scrollView = (UIScrollView *)view;
  174. break;
  175. } else {
  176. [views addObjectsFromArray:view.subviews];
  177. }
  178. }
  179. return scrollView;
  180. }
  181. - (void)tryGoBack {
  182. UINavigationController *navigationController = nil;
  183. UIViewController *topViewController = self.topViewController;
  184. if ([topViewController isKindOfClass:[UINavigationController class]]) {
  185. navigationController = (UINavigationController *)topViewController;
  186. } else {
  187. navigationController = topViewController.navigationController;
  188. }
  189. [navigationController popViewControllerAnimated:YES];
  190. }
  191. - (UIViewController *)topViewController {
  192. return [FLEXUtility topViewControllerInWindow:UIApplication.sharedApplication.keyWindow];
  193. }
  194. - (void)toggleTopViewControllerOfClass:(Class)class {
  195. UINavigationController *topViewController = (id)self.topViewController;
  196. if ([topViewController isKindOfClass:[FLEXNavigationController class]]) {
  197. if ([topViewController.topViewController isKindOfClass:[class class]]) {
  198. if (topViewController.viewControllers.count == 1) {
  199. // Dismiss since we are already presenting it
  200. [topViewController.presentingViewController dismissViewControllerAnimated:YES completion:nil];
  201. } else {
  202. // Pop since we are viewing it but it's not the only thing on the stack
  203. [topViewController popViewControllerAnimated:YES];
  204. }
  205. } else {
  206. // Push it on the existing navigation stack
  207. [topViewController pushViewController:[class new] animated:YES];
  208. }
  209. } else {
  210. // Present it in an entirely new navigation controller
  211. [self.explorerViewController presentViewController:
  212. [FLEXNavigationController withRootViewController:[class new]]
  213. animated:YES completion:nil];
  214. }
  215. }
  216. - (void)showExplorerIfNeeded {
  217. if (self.isHidden) {
  218. [self showExplorer];
  219. }
  220. }
  221. @end