FLEXManager+Extensibility.m 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  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 "FLEXNetworkHistoryTableViewController.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:[FLEXNetworkHistoryTableViewController class]];
  89. } description:@"Toggle network history view"];
  90. [self registerSimulatorShortcutWithKey:UIKeyInputDownArrow modifiers:0 action:^{
  91. if (self.isHidden) {
  92. [self tryScrollDown];
  93. } else {
  94. [self.explorerViewController handleDownArrowKeyPressed];
  95. }
  96. } description:@"Cycle view selection\n\t\tMove view down\n\t\tScroll down"];
  97. [self registerSimulatorShortcutWithKey:UIKeyInputUpArrow modifiers:0 action:^{
  98. if (self.isHidden) {
  99. [self tryScrollUp];
  100. } else {
  101. [self.explorerViewController handleUpArrowKeyPressed];
  102. }
  103. } description:@"Cycle view selection\n\t\tMove view up\n\t\tScroll up"];
  104. [self registerSimulatorShortcutWithKey:UIKeyInputRightArrow modifiers:0 action:^{
  105. if (!self.isHidden) {
  106. [self.explorerViewController handleRightArrowKeyPressed];
  107. }
  108. } description:@"Move selected view right"];
  109. [self registerSimulatorShortcutWithKey:UIKeyInputLeftArrow modifiers:0 action:^{
  110. if (self.isHidden) {
  111. [self tryGoBack];
  112. } else {
  113. [self.explorerViewController handleLeftArrowKeyPressed];
  114. }
  115. } description:@"Move selected view left"];
  116. [self registerSimulatorShortcutWithKey:@"?" modifiers:0 action:^{
  117. [self toggleTopViewControllerOfClass:[FLEXKeyboardHelpViewController class]];
  118. } description:@"Toggle (this) help menu"];
  119. [self registerSimulatorShortcutWithKey:UIKeyInputEscape modifiers:0 action:^{
  120. [[self.topViewController presentingViewController] dismissViewControllerAnimated:YES completion:nil];
  121. } description:@"End editing text\n\t\tDismiss top view controller"];
  122. [self registerSimulatorShortcutWithKey:@"o" modifiers:UIKeyModifierCommand|UIKeyModifierShift action:^{
  123. [self toggleTopViewControllerOfClass:[FLEXFileBrowserTableViewController class]];
  124. } description:@"Toggle file browser menu"];
  125. }
  126. + (void)load {
  127. dispatch_async(dispatch_get_main_queue(), ^{
  128. [self.sharedManager registerDefaultSimulatorShortcuts];
  129. });
  130. }
  131. #pragma mark - Private
  132. - (void)tryScrollDown {
  133. UIScrollView *firstScrollView = [self firstScrollView];
  134. CGPoint contentOffset = firstScrollView.contentOffset;
  135. CGFloat distance = floor(firstScrollView.frame.size.height / 2.0);
  136. CGFloat maxContentOffsetY = firstScrollView.contentSize.height + firstScrollView.contentInset.bottom - firstScrollView.frame.size.height;
  137. distance = MIN(maxContentOffsetY - firstScrollView.contentOffset.y, distance);
  138. contentOffset.y += distance;
  139. [firstScrollView setContentOffset:contentOffset animated:YES];
  140. }
  141. - (void)tryScrollUp {
  142. UIScrollView *firstScrollView = [self firstScrollView];
  143. CGPoint contentOffset = firstScrollView.contentOffset;
  144. CGFloat distance = floor(firstScrollView.frame.size.height / 2.0);
  145. CGFloat minContentOffsetY = -firstScrollView.contentInset.top;
  146. distance = MIN(firstScrollView.contentOffset.y - minContentOffsetY, distance);
  147. contentOffset.y -= distance;
  148. [firstScrollView setContentOffset:contentOffset animated:YES];
  149. }
  150. - (UIScrollView *)firstScrollView {
  151. NSMutableArray<UIView *> *views = [UIApplication.sharedApplication.keyWindow.subviews mutableCopy];
  152. UIScrollView *scrollView = nil;
  153. while (views.count > 0) {
  154. UIView *view = views.firstObject;
  155. [views removeObjectAtIndex:0];
  156. if ([view isKindOfClass:[UIScrollView class]]) {
  157. scrollView = (UIScrollView *)view;
  158. break;
  159. } else {
  160. [views addObjectsFromArray:view.subviews];
  161. }
  162. }
  163. return scrollView;
  164. }
  165. - (void)tryGoBack {
  166. UINavigationController *navigationController = nil;
  167. UIViewController *topViewController = self.topViewController;
  168. if ([topViewController isKindOfClass:[UINavigationController class]]) {
  169. navigationController = (UINavigationController *)topViewController;
  170. } else {
  171. navigationController = topViewController.navigationController;
  172. }
  173. [navigationController popViewControllerAnimated:YES];
  174. }
  175. - (UIViewController *)topViewController {
  176. UIViewController *topViewController = UIApplication.sharedApplication.keyWindow.rootViewController;
  177. while (topViewController.presentedViewController) {
  178. topViewController = topViewController.presentedViewController;
  179. }
  180. return topViewController;
  181. }
  182. - (void)toggleTopViewControllerOfClass:(Class)class {
  183. UINavigationController *topViewController = (id)self.topViewController;
  184. if ([topViewController isKindOfClass:[UINavigationController class]] &&
  185. [topViewController.topViewController isKindOfClass:[class class]]) {
  186. [topViewController.presentingViewController dismissViewControllerAnimated:YES completion:nil];
  187. } else {
  188. id viewController = [class new];
  189. UINavigationController *navigationController = [[FLEXNavigationController alloc] initWithRootViewController:viewController];
  190. [topViewController presentViewController:navigationController animated:YES completion:nil];
  191. }
  192. }
  193. - (void)showExplorerIfNeeded {
  194. if (self.isHidden) {
  195. [self showExplorer];
  196. }
  197. }
  198. @end