FLEXManager.m 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. //
  2. // FLEXManager.m
  3. // Flipboard
  4. //
  5. // Created by Ryan Olson on 4/4/14.
  6. // Copyright (c) 2014 Flipboard. All rights reserved.
  7. //
  8. #import "FLEXManager.h"
  9. #import "FLEXUtility.h"
  10. #import "FLEXExplorerViewController.h"
  11. #import "FLEXWindow.h"
  12. #import "FLEXGlobalsEntry.h"
  13. #import "FLEXObjectExplorerFactory.h"
  14. #import "FLEXObjectExplorerViewController.h"
  15. #import "FLEXNetworkObserver.h"
  16. #import "FLEXNetworkRecorder.h"
  17. #import "FLEXKeyboardShortcutManager.h"
  18. #import "FLEXFileBrowserTableViewController.h"
  19. #import "FLEXNetworkHistoryTableViewController.h"
  20. #import "FLEXKeyboardHelpViewController.h"
  21. @interface FLEXManager () <FLEXWindowEventDelegate, FLEXExplorerViewControllerDelegate>
  22. @property (nonatomic) FLEXWindow *explorerWindow;
  23. @property (nonatomic) FLEXExplorerViewController *explorerViewController;
  24. @property (nonatomic, readonly) NSMutableArray<FLEXGlobalsEntry *> *userGlobalEntries;
  25. @property (nonatomic, readonly) NSMutableDictionary<NSString *, FLEXCustomContentViewerFuture> *customContentTypeViewers;
  26. @end
  27. @implementation FLEXManager
  28. + (instancetype)sharedManager
  29. {
  30. static FLEXManager *sharedManager = nil;
  31. static dispatch_once_t onceToken;
  32. dispatch_once(&onceToken, ^{
  33. sharedManager = [self new];
  34. });
  35. return sharedManager;
  36. }
  37. - (instancetype)init
  38. {
  39. self = [super init];
  40. if (self) {
  41. _userGlobalEntries = [NSMutableArray array];
  42. _customContentTypeViewers = [NSMutableDictionary dictionary];
  43. }
  44. return self;
  45. }
  46. - (FLEXWindow *)explorerWindow
  47. {
  48. NSAssert([NSThread isMainThread], @"You must use %@ from the main thread only.", NSStringFromClass([self class]));
  49. if (!_explorerWindow) {
  50. _explorerWindow = [[FLEXWindow alloc] initWithFrame:UIScreen.mainScreen.bounds];
  51. _explorerWindow.eventDelegate = self;
  52. _explorerWindow.rootViewController = self.explorerViewController;
  53. }
  54. return _explorerWindow;
  55. }
  56. - (FLEXExplorerViewController *)explorerViewController
  57. {
  58. if (!_explorerViewController) {
  59. _explorerViewController = [FLEXExplorerViewController new];
  60. _explorerViewController.delegate = self;
  61. }
  62. return _explorerViewController;
  63. }
  64. - (void)showExplorer
  65. {
  66. self.explorerWindow.hidden = NO;
  67. #if FLEX_AT_LEAST_IOS13_SDK
  68. if (@available(iOS 13.0, *)) {
  69. // Only look for a new scene if we don't have one, or the one we have
  70. // isn't the active scene
  71. if (!self.explorerWindow.windowScene ||
  72. self.explorerWindow.windowScene.activationState != UISceneActivationStateForegroundActive) {
  73. for (UIScene *scene in UIApplication.sharedApplication.connectedScenes) {
  74. // Look for an active UIWindowScene
  75. if (scene.activationState == UISceneActivationStateForegroundActive &&
  76. [scene isKindOfClass:[UIWindowScene class]]) {
  77. self.explorerWindow.windowScene = (UIWindowScene *)scene;
  78. break;
  79. }
  80. }
  81. }
  82. }
  83. #endif
  84. }
  85. - (void)hideExplorer
  86. {
  87. self.explorerWindow.hidden = YES;
  88. }
  89. - (void)toggleExplorer {
  90. if (self.explorerWindow.isHidden) {
  91. [self showExplorer];
  92. } else {
  93. [self hideExplorer];
  94. }
  95. }
  96. - (void)showExplorerFromScene:(UIWindowScene *)scene
  97. {
  98. #if FLEX_AT_LEAST_IOS13_SDK
  99. if (@available(iOS 13.0, *)) {
  100. self.explorerWindow.windowScene = scene;
  101. }
  102. #endif
  103. self.explorerWindow.hidden = NO;
  104. }
  105. - (BOOL)isHidden
  106. {
  107. return self.explorerWindow.isHidden;
  108. }
  109. - (BOOL)isNetworkDebuggingEnabled
  110. {
  111. return [FLEXNetworkObserver isEnabled];
  112. }
  113. - (void)setNetworkDebuggingEnabled:(BOOL)networkDebuggingEnabled
  114. {
  115. [FLEXNetworkObserver setEnabled:networkDebuggingEnabled];
  116. }
  117. - (NSUInteger)networkResponseCacheByteLimit
  118. {
  119. return [[FLEXNetworkRecorder defaultRecorder] responseCacheByteLimit];
  120. }
  121. - (void)setNetworkResponseCacheByteLimit:(NSUInteger)networkResponseCacheByteLimit
  122. {
  123. [[FLEXNetworkRecorder defaultRecorder] setResponseCacheByteLimit:networkResponseCacheByteLimit];
  124. }
  125. - (void)setNetworkRequestHostBlacklist:(NSArray<NSString *> *)networkRequestHostBlacklist
  126. {
  127. [FLEXNetworkRecorder defaultRecorder].hostBlacklist = networkRequestHostBlacklist;
  128. }
  129. - (NSArray<NSString *> *)hostBlacklist
  130. {
  131. return [FLEXNetworkRecorder defaultRecorder].hostBlacklist;
  132. }
  133. #pragma mark - FLEXWindowEventDelegate
  134. - (BOOL)shouldHandleTouchAtPoint:(CGPoint)pointInWindow
  135. {
  136. // Ask the explorer view controller
  137. return [self.explorerViewController shouldReceiveTouchAtWindowPoint:pointInWindow];
  138. }
  139. - (BOOL)canBecomeKeyWindow
  140. {
  141. // Only when the explorer view controller wants it because it needs to accept key input & affect the status bar.
  142. return [self.explorerViewController wantsWindowToBecomeKey];
  143. }
  144. #pragma mark - FLEXExplorerViewControllerDelegate
  145. - (void)explorerViewControllerDidFinish:(FLEXExplorerViewController *)explorerViewController
  146. {
  147. [self hideExplorer];
  148. }
  149. #pragma mark - Simulator Shortcuts
  150. - (void)registerSimulatorShortcutWithKey:(NSString *)key modifiers:(UIKeyModifierFlags)modifiers action:(dispatch_block_t)action description:(NSString *)description
  151. {
  152. # if TARGET_OS_SIMULATOR
  153. [[FLEXKeyboardShortcutManager sharedManager] registerSimulatorShortcutWithKey:key modifiers:modifiers action:action description:description];
  154. #endif
  155. }
  156. - (void)setSimulatorShortcutsEnabled:(BOOL)simulatorShortcutsEnabled
  157. {
  158. # if TARGET_OS_SIMULATOR
  159. [[FLEXKeyboardShortcutManager sharedManager] setEnabled:simulatorShortcutsEnabled];
  160. #endif
  161. }
  162. - (BOOL)simulatorShortcutsEnabled
  163. {
  164. # if TARGET_OS_SIMULATOR
  165. return [[FLEXKeyboardShortcutManager sharedManager] isEnabled];
  166. #else
  167. return NO;
  168. #endif
  169. }
  170. - (void)registerDefaultSimulatorShortcuts
  171. {
  172. [self registerSimulatorShortcutWithKey:@"f" modifiers:0 action:^{
  173. [self toggleExplorer];
  174. } description:@"Toggle FLEX toolbar"];
  175. [self registerSimulatorShortcutWithKey:@"g" modifiers:0 action:^{
  176. [self showExplorerIfNeeded];
  177. [self.explorerViewController toggleMenuTool];
  178. } description:@"Toggle FLEX globals menu"];
  179. [self registerSimulatorShortcutWithKey:@"v" modifiers:0 action:^{
  180. [self showExplorerIfNeeded];
  181. [self.explorerViewController toggleViewsTool];
  182. } description:@"Toggle view hierarchy menu"];
  183. [self registerSimulatorShortcutWithKey:@"s" modifiers:0 action:^{
  184. [self showExplorerIfNeeded];
  185. [self.explorerViewController toggleSelectTool];
  186. } description:@"Toggle select tool"];
  187. [self registerSimulatorShortcutWithKey:@"m" modifiers:0 action:^{
  188. [self showExplorerIfNeeded];
  189. [self.explorerViewController toggleMoveTool];
  190. } description:@"Toggle move tool"];
  191. [self registerSimulatorShortcutWithKey:@"n" modifiers:0 action:^{
  192. [self toggleTopViewControllerOfClass:[FLEXNetworkHistoryTableViewController class]];
  193. } description:@"Toggle network history view"];
  194. [self registerSimulatorShortcutWithKey:UIKeyInputDownArrow modifiers:0 action:^{
  195. if ([self isHidden]) {
  196. [self tryScrollDown];
  197. } else {
  198. [self.explorerViewController handleDownArrowKeyPressed];
  199. }
  200. } description:@"Cycle view selection\n\t\tMove view down\n\t\tScroll down"];
  201. [self registerSimulatorShortcutWithKey:UIKeyInputUpArrow modifiers:0 action:^{
  202. if ([self isHidden]) {
  203. [self tryScrollUp];
  204. } else {
  205. [self.explorerViewController handleUpArrowKeyPressed];
  206. }
  207. } description:@"Cycle view selection\n\t\tMove view up\n\t\tScroll up"];
  208. [self registerSimulatorShortcutWithKey:UIKeyInputRightArrow modifiers:0 action:^{
  209. if (![self isHidden]) {
  210. [self.explorerViewController handleRightArrowKeyPressed];
  211. }
  212. } description:@"Move selected view right"];
  213. [self registerSimulatorShortcutWithKey:UIKeyInputLeftArrow modifiers:0 action:^{
  214. if ([self isHidden]) {
  215. [self tryGoBack];
  216. } else {
  217. [self.explorerViewController handleLeftArrowKeyPressed];
  218. }
  219. } description:@"Move selected view left"];
  220. [self registerSimulatorShortcutWithKey:@"?" modifiers:0 action:^{
  221. [self toggleTopViewControllerOfClass:[FLEXKeyboardHelpViewController class]];
  222. } description:@"Toggle (this) help menu"];
  223. [self registerSimulatorShortcutWithKey:UIKeyInputEscape modifiers:0 action:^{
  224. [[[self topViewController] presentingViewController] dismissViewControllerAnimated:YES completion:nil];
  225. } description:@"End editing text\n\t\tDismiss top view controller"];
  226. [self registerSimulatorShortcutWithKey:@"o" modifiers:UIKeyModifierCommand|UIKeyModifierShift action:^{
  227. [self toggleTopViewControllerOfClass:[FLEXFileBrowserTableViewController class]];
  228. } description:@"Toggle file browser menu"];
  229. }
  230. + (void)load
  231. {
  232. dispatch_async(dispatch_get_main_queue(), ^{
  233. [[[self class] sharedManager] registerDefaultSimulatorShortcuts];
  234. });
  235. }
  236. #pragma mark - Extensions
  237. - (void)registerGlobalEntryWithName:(NSString *)entryName objectFutureBlock:(id (^)(void))objectFutureBlock
  238. {
  239. NSParameterAssert(entryName);
  240. NSParameterAssert(objectFutureBlock);
  241. NSAssert([NSThread isMainThread], @"This method must be called from the main thread.");
  242. entryName = entryName.copy;
  243. FLEXGlobalsEntry *entry = [FLEXGlobalsEntry entryWithNameFuture:^NSString *{
  244. return entryName;
  245. } viewControllerFuture:^UIViewController *{
  246. return [FLEXObjectExplorerFactory explorerViewControllerForObject:objectFutureBlock()];
  247. }];
  248. [self.userGlobalEntries addObject:entry];
  249. }
  250. - (void)registerGlobalEntryWithName:(NSString *)entryName viewControllerFutureBlock:(UIViewController * (^)(void))viewControllerFutureBlock
  251. {
  252. NSParameterAssert(entryName);
  253. NSParameterAssert(viewControllerFutureBlock);
  254. NSAssert([NSThread isMainThread], @"This method must be called from the main thread.");
  255. entryName = entryName.copy;
  256. FLEXGlobalsEntry *entry = [FLEXGlobalsEntry entryWithNameFuture:^NSString *{
  257. return entryName;
  258. } viewControllerFuture:^UIViewController *{
  259. UIViewController *viewController = viewControllerFutureBlock();
  260. NSCAssert(viewController, @"'%@' entry returned nil viewController. viewControllerFutureBlock should never return nil.", entryName);
  261. return viewController;
  262. }];
  263. [self.userGlobalEntries addObject:entry];
  264. }
  265. - (void)setCustomViewerForContentType:(NSString *)contentType viewControllerFutureBlock:(FLEXCustomContentViewerFuture)viewControllerFutureBlock
  266. {
  267. NSParameterAssert(contentType.length);
  268. NSParameterAssert(viewControllerFutureBlock);
  269. NSAssert([NSThread isMainThread], @"This method must be called from the main thread.");
  270. self.customContentTypeViewers[contentType.lowercaseString] = viewControllerFutureBlock;
  271. }
  272. - (void)tryScrollDown
  273. {
  274. UIScrollView *firstScrollView = [self firstScrollView];
  275. CGPoint contentOffset = firstScrollView.contentOffset;
  276. CGFloat distance = floor(firstScrollView.frame.size.height / 2.0);
  277. CGFloat maxContentOffsetY = firstScrollView.contentSize.height + firstScrollView.contentInset.bottom - firstScrollView.frame.size.height;
  278. distance = MIN(maxContentOffsetY - firstScrollView.contentOffset.y, distance);
  279. contentOffset.y += distance;
  280. [firstScrollView setContentOffset:contentOffset animated:YES];
  281. }
  282. - (void)tryScrollUp
  283. {
  284. UIScrollView *firstScrollView = [self firstScrollView];
  285. CGPoint contentOffset = firstScrollView.contentOffset;
  286. CGFloat distance = floor(firstScrollView.frame.size.height / 2.0);
  287. CGFloat minContentOffsetY = -firstScrollView.contentInset.top;
  288. distance = MIN(firstScrollView.contentOffset.y - minContentOffsetY, distance);
  289. contentOffset.y -= distance;
  290. [firstScrollView setContentOffset:contentOffset animated:YES];
  291. }
  292. - (UIScrollView *)firstScrollView
  293. {
  294. NSMutableArray<UIView *> *views = [UIApplication.sharedApplication.keyWindow.subviews mutableCopy];
  295. UIScrollView *scrollView = nil;
  296. while (views.count > 0) {
  297. UIView *view = views.firstObject;
  298. [views removeObjectAtIndex:0];
  299. if ([view isKindOfClass:[UIScrollView class]]) {
  300. scrollView = (UIScrollView *)view;
  301. break;
  302. } else {
  303. [views addObjectsFromArray:view.subviews];
  304. }
  305. }
  306. return scrollView;
  307. }
  308. - (void)tryGoBack
  309. {
  310. UINavigationController *navigationController = nil;
  311. UIViewController *topViewController = [self topViewController];
  312. if ([topViewController isKindOfClass:[UINavigationController class]]) {
  313. navigationController = (UINavigationController *)topViewController;
  314. } else {
  315. navigationController = topViewController.navigationController;
  316. }
  317. [navigationController popViewControllerAnimated:YES];
  318. }
  319. - (UIViewController *)topViewController
  320. {
  321. UIViewController *topViewController = [[UIApplication.sharedApplication keyWindow] rootViewController];
  322. while ([topViewController presentedViewController]) {
  323. topViewController = [topViewController presentedViewController];
  324. }
  325. return topViewController;
  326. }
  327. - (void)toggleTopViewControllerOfClass:(Class)class
  328. {
  329. UIViewController *topViewController = [self topViewController];
  330. if ([topViewController isKindOfClass:[UINavigationController class]] &&
  331. [[(UINavigationController *)topViewController viewControllers].firstObject isKindOfClass:[class class]])
  332. {
  333. [[topViewController presentingViewController] dismissViewControllerAnimated:YES completion:nil];
  334. } else {
  335. id viewController = [class new];
  336. UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:viewController];
  337. [topViewController presentViewController:navigationController animated:YES completion:nil];
  338. }
  339. }
  340. - (void)showExplorerIfNeeded
  341. {
  342. if ([self isHidden]) {
  343. [self showExplorer];
  344. }
  345. }
  346. @end