FLEXManager+Extensibility.m 9.8 KB

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