FLEXManager+Extensibility.m 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. //
  2. // FLEXManager+Extensibility.m
  3. // FLEX
  4. //
  5. // Created by Tanner on 2/2/20.
  6. // Copyright © 2020 Flipboard. 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 "FLEXFileBrowserTableViewController.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];
  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. - (void)registerDefaultSimulatorShortcuts {
  69. [self registerSimulatorShortcutWithKey:@"f" modifiers:0 action:^{
  70. [self toggleExplorer];
  71. } description:@"Toggle FLEX toolbar"];
  72. [self registerSimulatorShortcutWithKey:@"g" modifiers:0 action:^{
  73. [self showExplorerIfNeeded];
  74. [self.explorerViewController toggleMenuTool];
  75. } description:@"Toggle FLEX globals menu"];
  76. [self registerSimulatorShortcutWithKey:@"v" modifiers:0 action:^{
  77. [self showExplorerIfNeeded];
  78. [self.explorerViewController toggleViewsTool];
  79. } description:@"Toggle view hierarchy menu"];
  80. [self registerSimulatorShortcutWithKey:@"s" modifiers:0 action:^{
  81. [self showExplorerIfNeeded];
  82. [self.explorerViewController toggleSelectTool];
  83. } description:@"Toggle select tool"];
  84. [self registerSimulatorShortcutWithKey:@"m" modifiers:0 action:^{
  85. [self showExplorerIfNeeded];
  86. [self.explorerViewController toggleMoveTool];
  87. } description:@"Toggle move tool"];
  88. [self registerSimulatorShortcutWithKey:@"n" modifiers:0 action:^{
  89. [self toggleTopViewControllerOfClass:[FLEXNetworkMITMViewController class]];
  90. } description:@"Toggle network history view"];
  91. // 't' is for testing: quickly present an object explorer for debugging
  92. [self registerSimulatorShortcutWithKey:@"t" modifiers:0 action:^{
  93. [self showExplorerIfNeeded];
  94. [self.explorerViewController toggleToolWithViewControllerProvider:^UINavigationController *{
  95. return [FLEXNavigationController withRootViewController:[FLEXObjectExplorerFactory
  96. explorerViewControllerForObject:NSBundle.mainBundle
  97. ]];
  98. } completion:nil];
  99. } description:@"Present an object explorer for debugging"];
  100. [self registerSimulatorShortcutWithKey:UIKeyInputDownArrow modifiers:0 action:^{
  101. if (self.isHidden || ![self.explorerViewController handleDownArrowKeyPressed]) {
  102. [self tryScrollDown];
  103. }
  104. } description:@"Cycle view selection\n\t\tMove view down\n\t\tScroll down"];
  105. [self registerSimulatorShortcutWithKey:UIKeyInputUpArrow modifiers:0 action:^{
  106. if (self.isHidden || ![self.explorerViewController handleUpArrowKeyPressed]) {
  107. [self tryScrollUp];
  108. }
  109. } description:@"Cycle view selection\n\t\tMove view up\n\t\tScroll up"];
  110. [self registerSimulatorShortcutWithKey:UIKeyInputRightArrow modifiers:0 action:^{
  111. if (!self.isHidden) {
  112. [self.explorerViewController handleRightArrowKeyPressed];
  113. }
  114. } description:@"Move selected view right"];
  115. [self registerSimulatorShortcutWithKey:UIKeyInputLeftArrow modifiers:0 action:^{
  116. if (self.isHidden) {
  117. [self tryGoBack];
  118. } else {
  119. [self.explorerViewController handleLeftArrowKeyPressed];
  120. }
  121. } description:@"Move selected view left"];
  122. [self registerSimulatorShortcutWithKey:@"?" modifiers:0 action:^{
  123. [self toggleTopViewControllerOfClass:[FLEXKeyboardHelpViewController class]];
  124. } description:@"Toggle (this) help menu"];
  125. [self registerSimulatorShortcutWithKey:UIKeyInputEscape modifiers:0 action:^{
  126. [[self.topViewController presentingViewController] dismissViewControllerAnimated:YES completion:nil];
  127. } description:@"End editing text\n\t\tDismiss top view controller"];
  128. [self registerSimulatorShortcutWithKey:@"o" modifiers:UIKeyModifierCommand|UIKeyModifierShift action:^{
  129. [self toggleTopViewControllerOfClass:[FLEXFileBrowserTableViewController class]];
  130. } description:@"Toggle file browser menu"];
  131. }
  132. + (void)load {
  133. dispatch_async(dispatch_get_main_queue(), ^{
  134. [self.sharedManager registerDefaultSimulatorShortcuts];
  135. });
  136. }
  137. #pragma mark - Private
  138. - (UIEdgeInsets)contentInsetsOfScrollView:(UIScrollView *)scrollView {
  139. if (@available(iOS 11, *)) {
  140. return scrollView.adjustedContentInset;
  141. }
  142. return scrollView.contentInset;
  143. }
  144. - (void)tryScrollDown {
  145. UIScrollView *scrollview = [self firstScrollView];
  146. UIEdgeInsets insets = [self contentInsetsOfScrollView:scrollview];
  147. CGPoint contentOffset = scrollview.contentOffset;
  148. CGFloat maxYOffset = scrollview.contentSize.height - scrollview.bounds.size.height + insets.bottom;
  149. contentOffset.y = MIN(contentOffset.y + 200, maxYOffset);
  150. [scrollview setContentOffset:contentOffset animated:YES];
  151. }
  152. - (void)tryScrollUp {
  153. UIScrollView *scrollview = [self firstScrollView];
  154. UIEdgeInsets insets = [self contentInsetsOfScrollView:scrollview];
  155. CGPoint contentOffset = scrollview.contentOffset;
  156. contentOffset.y = MAX(contentOffset.y - 200, -insets.top);
  157. [scrollview setContentOffset:contentOffset animated:YES];
  158. }
  159. - (UIScrollView *)firstScrollView {
  160. NSMutableArray<UIView *> *views = FLEXUtility.appKeyWindow.subviews.mutableCopy;
  161. UIScrollView *scrollView = nil;
  162. while (views.count > 0) {
  163. UIView *view = views.firstObject;
  164. [views removeObjectAtIndex:0];
  165. if ([view isKindOfClass:[UIScrollView class]]) {
  166. scrollView = (UIScrollView *)view;
  167. break;
  168. } else {
  169. [views addObjectsFromArray:view.subviews];
  170. }
  171. }
  172. return scrollView;
  173. }
  174. - (void)tryGoBack {
  175. UINavigationController *navigationController = nil;
  176. UIViewController *topViewController = self.topViewController;
  177. if ([topViewController isKindOfClass:[UINavigationController class]]) {
  178. navigationController = (UINavigationController *)topViewController;
  179. } else {
  180. navigationController = topViewController.navigationController;
  181. }
  182. [navigationController popViewControllerAnimated:YES];
  183. }
  184. - (UIViewController *)topViewController {
  185. return [FLEXUtility topViewControllerInWindow:UIApplication.sharedApplication.keyWindow];
  186. }
  187. - (void)toggleTopViewControllerOfClass:(Class)class {
  188. UINavigationController *topViewController = (id)self.topViewController;
  189. if ([topViewController isKindOfClass:[FLEXNavigationController class]]) {
  190. if ([topViewController.topViewController isKindOfClass:[class class]]) {
  191. if (topViewController.viewControllers.count == 1) {
  192. // Dismiss since we are already presenting it
  193. [topViewController.presentingViewController dismissViewControllerAnimated:YES completion:nil];
  194. } else {
  195. // Pop since we are viewing it but it's not the only thing on the stack
  196. [topViewController popViewControllerAnimated:YES];
  197. }
  198. } else {
  199. // Push it on the existing navigation stack
  200. [topViewController pushViewController:[class new] animated:YES];
  201. }
  202. } else {
  203. // Present it in an entirely new navigation controller
  204. [self.explorerViewController presentViewController:
  205. [FLEXNavigationController withRootViewController:[class new]]
  206. animated:YES completion:nil];
  207. }
  208. }
  209. - (void)showExplorerIfNeeded {
  210. if (self.isHidden) {
  211. [self showExplorer];
  212. }
  213. }
  214. @end