added files via upload

This commit is contained in:
Balackburn
2023-06-27 09:54:41 +02:00
commit 2ff6aac218
1420 changed files with 88898 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
//
// FLEXBookmarkManager.h
// FLEX
//
// Created by Tanner on 2/6/20.
// Copyright © 2020 FLEX Team. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface FLEXBookmarkManager : NSObject
@property (nonatomic, readonly, class) NSMutableArray *bookmarks;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,25 @@
//
// FLEXBookmarkManager.m
// FLEX
//
// Created by Tanner on 2/6/20.
// Copyright © 2020 FLEX Team. All rights reserved.
//
#import "FLEXBookmarkManager.h"
static NSMutableArray *kFLEXBookmarkManagerBookmarks = nil;
@implementation FLEXBookmarkManager
+ (void)initialize {
if (self == [FLEXBookmarkManager class]) {
kFLEXBookmarkManagerBookmarks = [NSMutableArray new];
}
}
+ (NSMutableArray *)bookmarks {
return kFLEXBookmarkManagerBookmarks;
}
@end

View File

@@ -0,0 +1,17 @@
//
// FLEXBookmarksViewController.h
// FLEX
//
// Created by Tanner on 2/6/20.
// Copyright © 2020 FLEX Team. All rights reserved.
//
#import "FLEXTableViewController.h"
NS_ASSUME_NONNULL_BEGIN
@interface FLEXBookmarksViewController : FLEXTableViewController
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,235 @@
//
// FLEXBookmarksViewController.m
// FLEX
//
// Created by Tanner on 2/6/20.
// Copyright © 2020 FLEX Team. All rights reserved.
//
#import "FLEXBookmarksViewController.h"
#import "FLEXExplorerViewController.h"
#import "FLEXNavigationController.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXBookmarkManager.h"
#import "UIBarButtonItem+FLEX.h"
#import "FLEXColor.h"
#import "FLEXUtility.h"
#import "FLEXRuntimeUtility.h"
#import "FLEXTableView.h"
@interface FLEXBookmarksViewController ()
@property (nonatomic, copy) NSArray *bookmarks;
@property (nonatomic, readonly) FLEXExplorerViewController *corePresenter;
@end
@implementation FLEXBookmarksViewController
#pragma mark - Initialization
- (id)init {
return [self initWithStyle:UITableViewStylePlain];
}
- (void)viewDidLoad {
[super viewDidLoad];
self.navigationController.hidesBarsOnSwipe = NO;
self.tableView.allowsMultipleSelectionDuringEditing = YES;
[self reloadData];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self setupDefaultBarItems];
}
#pragma mark - Private
- (void)reloadData {
// We assume the bookmarks aren't going to change out from under us, since
// presenting any other tool via keyboard shortcuts should dismiss us first
self.bookmarks = FLEXBookmarkManager.bookmarks;
self.title = [NSString stringWithFormat:@"Bookmarks (%@)", @(self.bookmarks.count)];
}
- (void)setupDefaultBarItems {
self.navigationItem.rightBarButtonItem = FLEXBarButtonItemSystem(Done, self, @selector(dismissAnimated));
self.toolbarItems = @[
UIBarButtonItem.flex_flexibleSpace,
FLEXBarButtonItemSystem(Edit, self, @selector(toggleEditing)),
];
// Disable editing if no bookmarks available
self.toolbarItems.lastObject.enabled = self.bookmarks.count > 0;
}
- (void)setupEditingBarItems {
self.navigationItem.rightBarButtonItem = nil;
self.toolbarItems = @[
[UIBarButtonItem flex_itemWithTitle:@"Close All" target:self action:@selector(closeAllButtonPressed:)],
UIBarButtonItem.flex_flexibleSpace,
// We use a non-system done item because we change its title dynamically
[UIBarButtonItem flex_doneStyleitemWithTitle:@"Done" target:self action:@selector(toggleEditing)]
];
self.toolbarItems.firstObject.tintColor = FLEXColor.destructiveColor;
}
- (FLEXExplorerViewController *)corePresenter {
// We must be presented by a FLEXExplorerViewController, or presented
// by another view controller that was presented by FLEXExplorerViewController
FLEXExplorerViewController *presenter = (id)self.presentingViewController;
presenter = (id)presenter.presentingViewController ?: presenter;
presenter = (id)presenter.presentingViewController ?: presenter;
NSAssert(
[presenter isKindOfClass:[FLEXExplorerViewController class]],
@"The bookmarks view controller expects to be presented by the explorer controller"
);
return presenter;
}
#pragma mark Button Actions
- (void)dismissAnimated {
[self dismissAnimated:nil];
}
- (void)dismissAnimated:(id)selectedObject {
if (selectedObject) {
UIViewController *explorer = [FLEXObjectExplorerFactory
explorerViewControllerForObject:selectedObject
];
if ([self.presentingViewController isKindOfClass:[FLEXNavigationController class]]) {
// I am presented on an existing navigation stack, so
// dismiss myself and push the bookmark there
UINavigationController *presenter = (id)self.presentingViewController;
[presenter dismissViewControllerAnimated:YES completion:^{
[presenter pushViewController:explorer animated:YES];
}];
} else {
// Dismiss myself and present explorer
UIViewController *presenter = self.corePresenter;
[presenter dismissViewControllerAnimated:YES completion:^{
[presenter presentViewController:[FLEXNavigationController
withRootViewController:explorer
] animated:YES completion:nil];
}];
}
} else {
// Just dismiss myself
[self dismissViewControllerAnimated:YES completion:nil];
}
}
- (void)toggleEditing {
NSArray<NSIndexPath *> *selected = self.tableView.indexPathsForSelectedRows;
self.editing = !self.editing;
if (self.isEditing) {
[self setupEditingBarItems];
} else {
[self setupDefaultBarItems];
// Get index set of bookmarks to close
NSMutableIndexSet *indexes = [NSMutableIndexSet new];
for (NSIndexPath *ip in selected) {
[indexes addIndex:ip.row];
}
if (selected.count) {
// Close bookmarks and update data source
[FLEXBookmarkManager.bookmarks removeObjectsAtIndexes:indexes];
[self reloadData];
// Remove deleted rows
[self.tableView deleteRowsAtIndexPaths:selected withRowAnimation:UITableViewRowAnimationAutomatic];
}
}
}
- (void)closeAllButtonPressed:(UIBarButtonItem *)sender {
[FLEXAlert makeSheet:^(FLEXAlert *make) {
NSInteger count = self.bookmarks.count;
NSString *title = FLEXPluralFormatString(count, @"Remove %@ bookmarks", @"Remove %@ bookmark");
make.button(title).destructiveStyle().handler(^(NSArray<NSString *> *strings) {
[self closeAll];
[self toggleEditing];
});
make.button(@"Cancel").cancelStyle();
} showFrom:self source:sender];
}
- (void)closeAll {
NSInteger rowCount = self.bookmarks.count;
// Close bookmarks and update data source
[FLEXBookmarkManager.bookmarks removeAllObjects];
[self reloadData];
// Delete rows from table view
NSArray<NSIndexPath *> *allRows = [NSArray flex_forEachUpTo:rowCount map:^id(NSUInteger row) {
return [NSIndexPath indexPathForRow:row inSection:0];
}];
[self.tableView deleteRowsAtIndexPaths:allRows withRowAnimation:UITableViewRowAnimationAutomatic];
}
#pragma mark - Table View Data Source
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.bookmarks.count;
}
- (UITableViewCell *)tableView:(FLEXTableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kFLEXDetailCell forIndexPath:indexPath];
id object = self.bookmarks[indexPath.row];
cell.textLabel.text = [FLEXRuntimeUtility safeDescriptionForObject:object];
cell.detailTextLabel.text = [NSString stringWithFormat:@"%@ — %p", [object class], object];
return cell;
}
#pragma mark - Table View Delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if (self.editing) {
// Case: editing with multi-select
self.toolbarItems.lastObject.title = @"Remove Selected";
self.toolbarItems.lastObject.tintColor = FLEXColor.destructiveColor;
} else {
// Case: selected a bookmark
[self dismissAnimated:self.bookmarks[indexPath.row]];
}
}
- (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath {
NSParameterAssert(self.editing);
if (tableView.indexPathsForSelectedRows.count == 0) {
self.toolbarItems.lastObject.title = @"Done";
self.toolbarItems.lastObject.tintColor = self.view.tintColor;
}
}
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
return YES;
}
- (void)tableView:(UITableView *)table
commitEditingStyle:(UITableViewCellEditingStyle)edit
forRowAtIndexPath:(NSIndexPath *)indexPath {
NSParameterAssert(edit == UITableViewCellEditingStyleDelete);
// Remove bookmark and update data source
[FLEXBookmarkManager.bookmarks removeObjectAtIndex:indexPath.row];
[self reloadData];
// Delete row from table view
[table deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
}
@end

View File

@@ -0,0 +1,61 @@
//
// FLEXExplorerViewController.h
// Flipboard
//
// Created by Ryan Olson on 4/4/14.
// Copyright (c) 2020 FLEX Team. All rights reserved.
//
#import "FLEXExplorerToolbar.h"
@class FLEXWindow;
@protocol FLEXExplorerViewControllerDelegate;
/// A view controller that manages the FLEX toolbar.
@interface FLEXExplorerViewController : UIViewController
@property (nonatomic, weak) id <FLEXExplorerViewControllerDelegate> delegate;
@property (nonatomic, readonly) BOOL wantsWindowToBecomeKey;
@property (nonatomic, readonly) FLEXExplorerToolbar *explorerToolbar;
- (BOOL)shouldReceiveTouchAtWindowPoint:(CGPoint)pointInWindowCoordinates;
/// @brief Used to present (or dismiss) a modal view controller ("tool"),
/// typically triggered by pressing a button in the toolbar.
///
/// If a tool is already presented, this method simply dismisses it and calls the completion block.
/// If no tool is presented, @code future() @endcode is presented and the completion block is called.
- (void)toggleToolWithViewControllerProvider:(UINavigationController *(^)(void))future
completion:(void (^)(void))completion;
/// @brief Used to present (or dismiss) a modal view controller ("tool"),
/// typically triggered by pressing a button in the toolbar.
///
/// If a tool is already presented, this method dismisses it and presents the given tool.
/// The completion block is called once the tool has been presented.
- (void)presentTool:(UINavigationController *(^)(void))future
completion:(void (^)(void))completion;
// Keyboard shortcut helpers
- (void)toggleSelectTool;
- (void)toggleMoveTool;
- (void)toggleViewsTool;
- (void)toggleMenuTool;
/// @return YES if the explorer used the key press to perform an action, NO otherwise
- (BOOL)handleDownArrowKeyPressed;
/// @return YES if the explorer used the key press to perform an action, NO otherwise
- (BOOL)handleUpArrowKeyPressed;
/// @return YES if the explorer used the key press to perform an action, NO otherwise
- (BOOL)handleRightArrowKeyPressed;
/// @return YES if the explorer used the key press to perform an action, NO otherwise
- (BOOL)handleLeftArrowKeyPressed;
@end
#pragma mark -
@protocol FLEXExplorerViewControllerDelegate <NSObject>
- (void)explorerViewControllerDidFinish:(FLEXExplorerViewController *)explorerViewController;
@end

View File

@@ -0,0 +1,1050 @@
//
// FLEXExplorerViewController.m
// Flipboard
//
// Created by Ryan Olson on 4/4/14.
// Copyright (c) 2020 FLEX Team. All rights reserved.
//
#import "FLEXExplorerViewController.h"
#import "FLEXExplorerToolbarItem.h"
#import "FLEXUtility.h"
#import "FLEXWindow.h"
#import "FLEXTabList.h"
#import "FLEXNavigationController.h"
#import "FLEXHierarchyViewController.h"
#import "FLEXGlobalsViewController.h"
#import "FLEXObjectExplorerViewController.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXNetworkMITMViewController.h"
#import "FLEXTabsViewController.h"
#import "FLEXWindowManagerController.h"
#import "FLEXViewControllersViewController.h"
#import "NSUserDefaults+FLEX.h"
typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
FLEXExplorerModeDefault,
FLEXExplorerModeSelect,
FLEXExplorerModeMove
};
@interface FLEXExplorerViewController () <FLEXHierarchyDelegate, UIAdaptivePresentationControllerDelegate>
/// Tracks the currently active tool/mode
@property (nonatomic) FLEXExplorerMode currentMode;
/// Gesture recognizer for dragging a view in move mode
@property (nonatomic) UIPanGestureRecognizer *movePanGR;
/// Gesture recognizer for showing additional details on the selected view
@property (nonatomic) UITapGestureRecognizer *detailsTapGR;
/// Only valid while a move pan gesture is in progress.
@property (nonatomic) CGRect selectedViewFrameBeforeDragging;
/// Only valid while a toolbar drag pan gesture is in progress.
@property (nonatomic) CGRect toolbarFrameBeforeDragging;
/// Only valid while a selected view pan gesture is in progress.
@property (nonatomic) CGFloat selectedViewLastPanX;
/// Borders of all the visible views in the hierarchy at the selection point.
/// The keys are NSValues with the corresponding view (nonretained).
@property (nonatomic) NSDictionary<NSValue *, UIView *> *outlineViewsForVisibleViews;
/// The actual views at the selection point with the deepest view last.
@property (nonatomic) NSArray<UIView *> *viewsAtTapPoint;
/// The view that we're currently highlighting with an overlay and displaying details for.
@property (nonatomic) UIView *selectedView;
/// A colored transparent overlay to indicate that the view is selected.
@property (nonatomic) UIView *selectedViewOverlay;
/// Used to actuate changes in view selection on iOS 10+
@property (nonatomic, readonly) UISelectionFeedbackGenerator *selectionFBG API_AVAILABLE(ios(10.0));
/// self.view.window as a \c FLEXWindow
@property (nonatomic, readonly) FLEXWindow *window;
/// All views that we're KVOing. Used to help us clean up properly.
@property (nonatomic) NSMutableSet<UIView *> *observedViews;
/// Used to preserve the target app's UIMenuController items.
@property (nonatomic) NSArray<UIMenuItem *> *appMenuItems;
@end
@implementation FLEXExplorerViewController
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
self.observedViews = [NSMutableSet new];
}
return self;
}
- (void)dealloc {
for (UIView *view in _observedViews) {
[self stopObservingView:view];
}
}
- (void)viewDidLoad {
[super viewDidLoad];
// Toolbar
_explorerToolbar = [FLEXExplorerToolbar new];
// Start the toolbar off below any bars that may be at the top of the view.
CGFloat toolbarOriginY = NSUserDefaults.standardUserDefaults.flex_toolbarTopMargin;
CGRect safeArea = [self viewSafeArea];
CGSize toolbarSize = [self.explorerToolbar sizeThatFits:CGSizeMake(
CGRectGetWidth(self.view.bounds), CGRectGetHeight(safeArea)
)];
[self updateToolbarPositionWithUnconstrainedFrame:CGRectMake(
CGRectGetMinX(safeArea), toolbarOriginY, toolbarSize.width, toolbarSize.height
)];
self.explorerToolbar.autoresizingMask = UIViewAutoresizingFlexibleWidth |
UIViewAutoresizingFlexibleBottomMargin |
UIViewAutoresizingFlexibleTopMargin;
[self.view addSubview:self.explorerToolbar];
[self setupToolbarActions];
[self setupToolbarGestures];
// View selection
UITapGestureRecognizer *selectionTapGR = [[UITapGestureRecognizer alloc]
initWithTarget:self action:@selector(handleSelectionTap:)
];
[self.view addGestureRecognizer:selectionTapGR];
// View moving
self.movePanGR = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleMovePan:)];
self.movePanGR.enabled = self.currentMode == FLEXExplorerModeMove;
[self.view addGestureRecognizer:self.movePanGR];
// Feedback
if (@available(iOS 10.0, *)) {
_selectionFBG = [UISelectionFeedbackGenerator new];
}
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self updateButtonStates];
}
#pragma mark - Rotation
- (UIViewController *)viewControllerForRotationAndOrientation {
UIViewController *viewController = FLEXUtility.appKeyWindow.rootViewController;
// Obfuscating selector _viewControllerForSupportedInterfaceOrientations
NSString *viewControllerSelectorString = [@[
@"_vie", @"wContro", @"llerFor", @"Supported", @"Interface", @"Orientations"
] componentsJoinedByString:@""];
SEL viewControllerSelector = NSSelectorFromString(viewControllerSelectorString);
if ([viewController respondsToSelector:viewControllerSelector]) {
viewController = [viewController valueForKey:viewControllerSelectorString];
}
return viewController;
}
- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
// Commenting this out until I can figure out a better way to solve this
// if (self.window.isKeyWindow) {
// [self.window resignKeyWindow];
// }
UIViewController *viewControllerToAsk = [self viewControllerForRotationAndOrientation];
UIInterfaceOrientationMask supportedOrientations = FLEXUtility.infoPlistSupportedInterfaceOrientationsMask;
if (viewControllerToAsk && ![viewControllerToAsk isKindOfClass:[self class]]) {
supportedOrientations = [viewControllerToAsk supportedInterfaceOrientations];
}
// The UIViewController docs state that this method must not return zero.
// If we weren't able to get a valid value for the supported interface
// orientations, default to all supported.
if (supportedOrientations == 0) {
supportedOrientations = UIInterfaceOrientationMaskAll;
}
return supportedOrientations;
}
- (BOOL)shouldAutorotate {
UIViewController *viewControllerToAsk = [self viewControllerForRotationAndOrientation];
BOOL shouldAutorotate = YES;
if (viewControllerToAsk && viewControllerToAsk != self) {
shouldAutorotate = [viewControllerToAsk shouldAutorotate];
}
return shouldAutorotate;
}
- (void)viewWillTransitionToSize:(CGSize)size
withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
[coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
for (UIView *outlineView in self.outlineViewsForVisibleViews.allValues) {
outlineView.hidden = YES;
}
self.selectedViewOverlay.hidden = YES;
} completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
for (UIView *view in self.viewsAtTapPoint) {
NSValue *key = [NSValue valueWithNonretainedObject:view];
UIView *outlineView = self.outlineViewsForVisibleViews[key];
outlineView.frame = [self frameInLocalCoordinatesForView:view];
if (self.currentMode == FLEXExplorerModeSelect) {
outlineView.hidden = NO;
}
}
if (self.selectedView) {
self.selectedViewOverlay.frame = [self frameInLocalCoordinatesForView:self.selectedView];
self.selectedViewOverlay.hidden = NO;
}
}];
}
#pragma mark - Setter Overrides
- (void)setSelectedView:(UIView *)selectedView {
if (![_selectedView isEqual:selectedView]) {
if (![self.viewsAtTapPoint containsObject:_selectedView]) {
[self stopObservingView:_selectedView];
}
_selectedView = selectedView;
[self beginObservingView:selectedView];
// Update the toolbar and selected overlay
self.explorerToolbar.selectedViewDescription = [FLEXUtility
descriptionForView:selectedView includingFrame:YES
];
self.explorerToolbar.selectedViewOverlayColor = [FLEXUtility
consistentRandomColorForObject:selectedView
];
if (selectedView) {
if (!self.selectedViewOverlay) {
self.selectedViewOverlay = [UIView new];
[self.view addSubview:self.selectedViewOverlay];
self.selectedViewOverlay.layer.borderWidth = 1.0;
}
UIColor *outlineColor = [FLEXUtility consistentRandomColorForObject:selectedView];
self.selectedViewOverlay.backgroundColor = [outlineColor colorWithAlphaComponent:0.2];
self.selectedViewOverlay.layer.borderColor = outlineColor.CGColor;
self.selectedViewOverlay.frame = [self.view convertRect:selectedView.bounds fromView:selectedView];
// Make sure the selected overlay is in front of all the other subviews
// except the toolbar, which should always stay on top.
[self.view bringSubviewToFront:self.selectedViewOverlay];
[self.view bringSubviewToFront:self.explorerToolbar];
} else {
[self.selectedViewOverlay removeFromSuperview];
self.selectedViewOverlay = nil;
}
// Some of the button states depend on whether we have a selected view.
[self updateButtonStates];
}
}
- (void)setViewsAtTapPoint:(NSArray<UIView *> *)viewsAtTapPoint {
if (![_viewsAtTapPoint isEqual:viewsAtTapPoint]) {
for (UIView *view in _viewsAtTapPoint) {
if (view != self.selectedView) {
[self stopObservingView:view];
}
}
_viewsAtTapPoint = viewsAtTapPoint;
for (UIView *view in viewsAtTapPoint) {
[self beginObservingView:view];
}
}
}
- (void)setCurrentMode:(FLEXExplorerMode)currentMode {
if (_currentMode != currentMode) {
_currentMode = currentMode;
switch (currentMode) {
case FLEXExplorerModeDefault:
[self removeAndClearOutlineViews];
self.viewsAtTapPoint = nil;
self.selectedView = nil;
break;
case FLEXExplorerModeSelect:
// Make sure the outline views are unhidden in case we came from the move mode.
for (NSValue *key in self.outlineViewsForVisibleViews) {
UIView *outlineView = self.outlineViewsForVisibleViews[key];
outlineView.hidden = NO;
}
break;
case FLEXExplorerModeMove:
// Hide all the outline views to focus on the selected view,
// which is the only one that will move.
for (NSValue *key in self.outlineViewsForVisibleViews) {
UIView *outlineView = self.outlineViewsForVisibleViews[key];
outlineView.hidden = YES;
}
break;
}
self.movePanGR.enabled = currentMode == FLEXExplorerModeMove;
[self updateButtonStates];
}
}
#pragma mark - View Tracking
- (void)beginObservingView:(UIView *)view {
// Bail if we're already observing this view or if there's nothing to observe.
if (!view || [self.observedViews containsObject:view]) {
return;
}
for (NSString *keyPath in self.viewKeyPathsToTrack) {
[view addObserver:self forKeyPath:keyPath options:0 context:NULL];
}
[self.observedViews addObject:view];
}
- (void)stopObservingView:(UIView *)view {
if (!view) {
return;
}
for (NSString *keyPath in self.viewKeyPathsToTrack) {
[view removeObserver:self forKeyPath:keyPath];
}
[self.observedViews removeObject:view];
}
- (NSArray<NSString *> *)viewKeyPathsToTrack {
static NSArray<NSString *> *trackedViewKeyPaths = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSString *frameKeyPath = NSStringFromSelector(@selector(frame));
trackedViewKeyPaths = @[frameKeyPath];
});
return trackedViewKeyPaths;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary<NSString *, id> *)change
context:(void *)context {
[self updateOverlayAndDescriptionForObjectIfNeeded:object];
}
- (void)updateOverlayAndDescriptionForObjectIfNeeded:(id)object {
NSUInteger indexOfView = [self.viewsAtTapPoint indexOfObject:object];
if (indexOfView != NSNotFound) {
UIView *view = self.viewsAtTapPoint[indexOfView];
NSValue *key = [NSValue valueWithNonretainedObject:view];
UIView *outline = self.outlineViewsForVisibleViews[key];
if (outline) {
outline.frame = [self frameInLocalCoordinatesForView:view];
}
}
if (object == self.selectedView) {
// Update the selected view description since we show the frame value there.
self.explorerToolbar.selectedViewDescription = [FLEXUtility
descriptionForView:self.selectedView includingFrame:YES
];
CGRect selectedViewOutlineFrame = [self frameInLocalCoordinatesForView:self.selectedView];
self.selectedViewOverlay.frame = selectedViewOutlineFrame;
}
}
- (CGRect)frameInLocalCoordinatesForView:(UIView *)view {
// Convert to window coordinates since the view may be in a different window than our view
CGRect frameInWindow = [view convertRect:view.bounds toView:nil];
// Convert from the window to our view's coordinate space
return [self.view convertRect:frameInWindow fromView:nil];
}
#pragma mark - Toolbar Buttons
- (void)setupToolbarActions {
FLEXExplorerToolbar *toolbar = self.explorerToolbar;
NSDictionary<NSString *, FLEXExplorerToolbarItem *> *actionsToItems = @{
NSStringFromSelector(@selector(selectButtonTapped:)): toolbar.selectItem,
NSStringFromSelector(@selector(hierarchyButtonTapped:)): toolbar.hierarchyItem,
NSStringFromSelector(@selector(recentButtonTapped:)): toolbar.recentItem,
NSStringFromSelector(@selector(moveButtonTapped:)): toolbar.moveItem,
NSStringFromSelector(@selector(globalsButtonTapped:)): toolbar.globalsItem,
NSStringFromSelector(@selector(closeButtonTapped:)): toolbar.closeItem,
};
[actionsToItems enumerateKeysAndObjectsUsingBlock:^(NSString *sel, FLEXExplorerToolbarItem *item, BOOL *stop) {
[item addTarget:self action:NSSelectorFromString(sel) forControlEvents:UIControlEventTouchUpInside];
}];
}
- (void)selectButtonTapped:(FLEXExplorerToolbarItem *)sender {
[self toggleSelectTool];
}
- (void)hierarchyButtonTapped:(FLEXExplorerToolbarItem *)sender {
[self toggleViewsTool];
}
- (UIWindow *)statusWindow {
if (!@available(iOS 16, *)) {
NSString *statusBarString = [NSString stringWithFormat:@"%@arWindow", @"_statusB"];
return [UIApplication.sharedApplication valueForKey:statusBarString];
}
return nil;
}
- (void)recentButtonTapped:(FLEXExplorerToolbarItem *)sender {
NSAssert(FLEXTabList.sharedList.activeTab, @"Must have active tab");
[self presentViewController:FLEXTabList.sharedList.activeTab animated:YES completion:nil];
}
- (void)moveButtonTapped:(FLEXExplorerToolbarItem *)sender {
[self toggleMoveTool];
}
- (void)globalsButtonTapped:(FLEXExplorerToolbarItem *)sender {
[self toggleMenuTool];
}
- (void)closeButtonTapped:(FLEXExplorerToolbarItem *)sender {
self.currentMode = FLEXExplorerModeDefault;
[self.delegate explorerViewControllerDidFinish:self];
}
- (void)updateButtonStates {
FLEXExplorerToolbar *toolbar = self.explorerToolbar;
toolbar.selectItem.selected = self.currentMode == FLEXExplorerModeSelect;
// Move only enabled when an object is selected.
BOOL hasSelectedObject = self.selectedView != nil;
toolbar.moveItem.enabled = hasSelectedObject;
toolbar.moveItem.selected = self.currentMode == FLEXExplorerModeMove;
// Recent only enabled when we have a last active tab
toolbar.recentItem.enabled = FLEXTabList.sharedList.activeTab != nil;
}
#pragma mark - Toolbar Dragging
- (void)setupToolbarGestures {
FLEXExplorerToolbar *toolbar = self.explorerToolbar;
// Pan gesture for dragging.
[toolbar.dragHandle addGestureRecognizer:[[UIPanGestureRecognizer alloc]
initWithTarget:self action:@selector(handleToolbarPanGesture:)
]];
// Tap gesture for hinting.
[toolbar.dragHandle addGestureRecognizer:[[UITapGestureRecognizer alloc]
initWithTarget:self action:@selector(handleToolbarHintTapGesture:)
]];
// Tap gesture for showing additional details
self.detailsTapGR = [[UITapGestureRecognizer alloc]
initWithTarget:self action:@selector(handleToolbarDetailsTapGesture:)
];
[toolbar.selectedViewDescriptionContainer addGestureRecognizer:self.detailsTapGR];
// Swipe gestures for selecting deeper / higher views at a point
UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc]
initWithTarget:self action:@selector(handleChangeViewAtPointGesture:)
];
[toolbar.selectedViewDescriptionContainer addGestureRecognizer:panGesture];
// Long press gesture to present tabs manager
[toolbar.globalsItem addGestureRecognizer:[[UILongPressGestureRecognizer alloc]
initWithTarget:self action:@selector(handleToolbarShowTabsGesture:)
]];
// Long press gesture to present window manager
[toolbar.selectItem addGestureRecognizer:[[UILongPressGestureRecognizer alloc]
initWithTarget:self action:@selector(handleToolbarWindowManagerGesture:)
]];
// Long press gesture to present view controllers at tap
[toolbar.hierarchyItem addGestureRecognizer:[[UILongPressGestureRecognizer alloc]
initWithTarget:self action:@selector(handleToolbarShowViewControllersGesture:)
]];
}
- (void)handleToolbarPanGesture:(UIPanGestureRecognizer *)panGR {
switch (panGR.state) {
case UIGestureRecognizerStateBegan:
self.toolbarFrameBeforeDragging = self.explorerToolbar.frame;
[self updateToolbarPositionWithDragGesture:panGR];
break;
case UIGestureRecognizerStateChanged:
case UIGestureRecognizerStateEnded:
[self updateToolbarPositionWithDragGesture:panGR];
break;
default:
break;
}
}
- (void)updateToolbarPositionWithDragGesture:(UIPanGestureRecognizer *)panGR {
CGPoint translation = [panGR translationInView:self.view];
CGRect newToolbarFrame = self.toolbarFrameBeforeDragging;
newToolbarFrame.origin.y += translation.y;
[self updateToolbarPositionWithUnconstrainedFrame:newToolbarFrame];
}
- (void)updateToolbarPositionWithUnconstrainedFrame:(CGRect)unconstrainedFrame {
CGRect safeArea = [self viewSafeArea];
// We only constrain the Y-axis because we want the toolbar
// to handle the X-axis safeArea layout by itself
CGFloat minY = CGRectGetMinY(safeArea);
CGFloat maxY = CGRectGetMaxY(safeArea) - unconstrainedFrame.size.height;
if (unconstrainedFrame.origin.y < minY) {
unconstrainedFrame.origin.y = minY;
} else if (unconstrainedFrame.origin.y > maxY) {
unconstrainedFrame.origin.y = maxY;
}
self.explorerToolbar.frame = unconstrainedFrame;
NSUserDefaults.standardUserDefaults.flex_toolbarTopMargin = unconstrainedFrame.origin.y;
}
- (void)handleToolbarHintTapGesture:(UITapGestureRecognizer *)tapGR {
// Bounce the toolbar to indicate that it is draggable.
// TODO: make it bouncier.
if (tapGR.state == UIGestureRecognizerStateRecognized) {
CGRect originalToolbarFrame = self.explorerToolbar.frame;
const NSTimeInterval kHalfwayDuration = 0.2;
const CGFloat kVerticalOffset = 30.0;
[UIView animateWithDuration:kHalfwayDuration delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
CGRect newToolbarFrame = self.explorerToolbar.frame;
newToolbarFrame.origin.y += kVerticalOffset;
self.explorerToolbar.frame = newToolbarFrame;
} completion:^(BOOL finished) {
[UIView animateWithDuration:kHalfwayDuration delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{
self.explorerToolbar.frame = originalToolbarFrame;
} completion:nil];
}];
}
}
- (void)handleToolbarDetailsTapGesture:(UITapGestureRecognizer *)tapGR {
if (tapGR.state == UIGestureRecognizerStateRecognized && self.selectedView) {
UIViewController *topStackVC = [FLEXObjectExplorerFactory explorerViewControllerForObject:self.selectedView];
[self presentViewController:
[FLEXNavigationController withRootViewController:topStackVC]
animated:YES completion:nil];
}
}
- (void)handleToolbarShowTabsGesture:(UILongPressGestureRecognizer *)sender {
if (sender.state == UIGestureRecognizerStateBegan) {
// Back up the UIMenuController items since dismissViewController: will attempt to replace them
self.appMenuItems = UIMenuController.sharedMenuController.menuItems;
// Don't use FLEXNavigationController because the tab viewer itself is not a tab
[super presentViewController:[[UINavigationController alloc]
initWithRootViewController:[FLEXTabsViewController new]
] animated:YES completion:nil];
}
}
- (void)handleToolbarWindowManagerGesture:(UILongPressGestureRecognizer *)sender {
if (sender.state == UIGestureRecognizerStateBegan) {
// Back up the UIMenuController items since dismissViewController: will attempt to replace them
self.appMenuItems = UIMenuController.sharedMenuController.menuItems;
[super presentViewController:[FLEXNavigationController
withRootViewController:[FLEXWindowManagerController new]
] animated:YES completion:nil];
}
}
- (void)handleToolbarShowViewControllersGesture:(UILongPressGestureRecognizer *)sender {
if (sender.state == UIGestureRecognizerStateBegan && self.viewsAtTapPoint.count) {
// Back up the UIMenuController items since dismissViewController: will attempt to replace them
self.appMenuItems = UIMenuController.sharedMenuController.menuItems;
UIViewController *list = [FLEXViewControllersViewController
controllersForViews:self.viewsAtTapPoint
];
[self presentViewController:
[FLEXNavigationController withRootViewController:list
] animated:YES completion:nil];
}
}
#pragma mark - View Selection
- (void)handleSelectionTap:(UITapGestureRecognizer *)tapGR {
// Only if we're in selection mode
if (self.currentMode == FLEXExplorerModeSelect && tapGR.state == UIGestureRecognizerStateRecognized) {
// Note that [tapGR locationInView:nil] is broken in iOS 8,
// so we have to do a two step conversion to window coordinates.
// Thanks to @lascorbe for finding this: https://github.com/Flipboard/FLEX/pull/31
CGPoint tapPointInView = [tapGR locationInView:self.view];
CGPoint tapPointInWindow = [self.view convertPoint:tapPointInView toView:nil];
[self updateOutlineViewsForSelectionPoint:tapPointInWindow];
}
}
- (void)handleChangeViewAtPointGesture:(UIPanGestureRecognizer *)sender {
NSInteger max = self.viewsAtTapPoint.count - 1;
NSInteger currentIdx = [self.viewsAtTapPoint indexOfObject:self.selectedView];
CGFloat locationX = [sender locationInView:self.view].x;
// Track the pan gesture: every N points we move along the X axis,
// actuate some haptic feedback and move up or down the hierarchy.
// We only store the "last" location when we've met the threshold.
// We only change the view and actuate feedback if the view selection
// changes; that is, as long as we don't go outside or under the array.
switch (sender.state) {
case UIGestureRecognizerStateBegan: {
self.selectedViewLastPanX = locationX;
break;
}
case UIGestureRecognizerStateChanged: {
static CGFloat kNextLevelThreshold = 20.f;
CGFloat lastX = self.selectedViewLastPanX;
NSInteger newSelection = currentIdx;
// Left, go down the hierarchy
if (locationX < lastX && (lastX - locationX) >= kNextLevelThreshold) {
// Choose a new view index up to the max index
newSelection = MIN(max, currentIdx + 1);
self.selectedViewLastPanX = locationX;
}
// Right, go up the hierarchy
else if (lastX < locationX && (locationX - lastX) >= kNextLevelThreshold) {
// Choose a new view index down to the min index
newSelection = MAX(0, currentIdx - 1);
self.selectedViewLastPanX = locationX;
}
if (currentIdx != newSelection) {
self.selectedView = self.viewsAtTapPoint[newSelection];
[self actuateSelectionChangedFeedback];
}
break;
}
default: break;
}
}
- (void)actuateSelectionChangedFeedback {
if (@available(iOS 10.0, *)) {
[self.selectionFBG selectionChanged];
}
}
- (void)updateOutlineViewsForSelectionPoint:(CGPoint)selectionPointInWindow {
[self removeAndClearOutlineViews];
// Include hidden views in the "viewsAtTapPoint" array so we can show them in the hierarchy list.
self.viewsAtTapPoint = [self viewsAtPoint:selectionPointInWindow skipHiddenViews:NO];
// For outlined views and the selected view, only use visible views.
// Outlining hidden views adds clutter and makes the selection behavior confusing.
NSArray<UIView *> *visibleViewsAtTapPoint = [self viewsAtPoint:selectionPointInWindow skipHiddenViews:YES];
NSMutableDictionary<NSValue *, UIView *> *newOutlineViewsForVisibleViews = [NSMutableDictionary new];
for (UIView *view in visibleViewsAtTapPoint) {
UIView *outlineView = [self outlineViewForView:view];
[self.view addSubview:outlineView];
NSValue *key = [NSValue valueWithNonretainedObject:view];
[newOutlineViewsForVisibleViews setObject:outlineView forKey:key];
}
self.outlineViewsForVisibleViews = newOutlineViewsForVisibleViews;
self.selectedView = [self viewForSelectionAtPoint:selectionPointInWindow];
// Make sure the explorer toolbar doesn't end up behind the newly added outline views.
[self.view bringSubviewToFront:self.explorerToolbar];
[self updateButtonStates];
}
- (UIView *)outlineViewForView:(UIView *)view {
CGRect outlineFrame = [self frameInLocalCoordinatesForView:view];
UIView *outlineView = [[UIView alloc] initWithFrame:outlineFrame];
outlineView.backgroundColor = UIColor.clearColor;
outlineView.layer.borderColor = [FLEXUtility consistentRandomColorForObject:view].CGColor;
outlineView.layer.borderWidth = 1.0;
return outlineView;
}
- (void)removeAndClearOutlineViews {
for (NSValue *key in self.outlineViewsForVisibleViews) {
UIView *outlineView = self.outlineViewsForVisibleViews[key];
[outlineView removeFromSuperview];
}
self.outlineViewsForVisibleViews = nil;
}
- (NSArray<UIView *> *)viewsAtPoint:(CGPoint)tapPointInWindow skipHiddenViews:(BOOL)skipHidden {
NSMutableArray<UIView *> *views = [NSMutableArray new];
for (UIWindow *window in FLEXUtility.allWindows) {
// Don't include the explorer's own window or subviews.
if (window != self.view.window && [window pointInside:tapPointInWindow withEvent:nil]) {
[views addObject:window];
[views addObjectsFromArray:[self
recursiveSubviewsAtPoint:tapPointInWindow inView:window skipHiddenViews:skipHidden
]];
}
}
return views;
}
- (UIView *)viewForSelectionAtPoint:(CGPoint)tapPointInWindow {
// Select in the window that would handle the touch, but don't just use the result of
// hitTest:withEvent: so we can still select views with interaction disabled.
// Default to the the application's key window if none of the windows want the touch.
UIWindow *windowForSelection = UIApplication.sharedApplication.keyWindow;
for (UIWindow *window in FLEXUtility.allWindows.reverseObjectEnumerator) {
// Ignore the explorer's own window.
if (window != self.view.window) {
if ([window hitTest:tapPointInWindow withEvent:nil]) {
windowForSelection = window;
break;
}
}
}
// Select the deepest visible view at the tap point. This generally corresponds to what the user wants to select.
return [self recursiveSubviewsAtPoint:tapPointInWindow inView:windowForSelection skipHiddenViews:YES].lastObject;
}
- (NSArray<UIView *> *)recursiveSubviewsAtPoint:(CGPoint)pointInView
inView:(UIView *)view
skipHiddenViews:(BOOL)skipHidden {
NSMutableArray<UIView *> *subviewsAtPoint = [NSMutableArray new];
for (UIView *subview in view.subviews) {
BOOL isHidden = subview.hidden || subview.alpha < 0.01;
if (skipHidden && isHidden) {
continue;
}
BOOL subviewContainsPoint = CGRectContainsPoint(subview.frame, pointInView);
if (subviewContainsPoint) {
[subviewsAtPoint addObject:subview];
}
// If this view doesn't clip to its bounds, we need to check its subviews even if it
// doesn't contain the selection point. They may be visible and contain the selection point.
if (subviewContainsPoint || !subview.clipsToBounds) {
CGPoint pointInSubview = [view convertPoint:pointInView toView:subview];
[subviewsAtPoint addObjectsFromArray:[self
recursiveSubviewsAtPoint:pointInSubview inView:subview skipHiddenViews:skipHidden
]];
}
}
return subviewsAtPoint;
}
#pragma mark - Selected View Moving
- (void)handleMovePan:(UIPanGestureRecognizer *)movePanGR {
switch (movePanGR.state) {
case UIGestureRecognizerStateBegan:
self.selectedViewFrameBeforeDragging = self.selectedView.frame;
[self updateSelectedViewPositionWithDragGesture:movePanGR];
break;
case UIGestureRecognizerStateChanged:
case UIGestureRecognizerStateEnded:
[self updateSelectedViewPositionWithDragGesture:movePanGR];
break;
default:
break;
}
}
- (void)updateSelectedViewPositionWithDragGesture:(UIPanGestureRecognizer *)movePanGR {
CGPoint translation = [movePanGR translationInView:self.selectedView.superview];
CGRect newSelectedViewFrame = self.selectedViewFrameBeforeDragging;
newSelectedViewFrame.origin.x = FLEXFloor(newSelectedViewFrame.origin.x + translation.x);
newSelectedViewFrame.origin.y = FLEXFloor(newSelectedViewFrame.origin.y + translation.y);
self.selectedView.frame = newSelectedViewFrame;
}
#pragma mark - Safe Area Handling
- (CGRect)viewSafeArea {
CGRect safeArea = self.view.bounds;
if (@available(iOS 11.0, *)) {
safeArea = UIEdgeInsetsInsetRect(self.view.bounds, self.view.safeAreaInsets);
}
return safeArea;
}
- (void)viewSafeAreaInsetsDidChange {
if (@available(iOS 11.0, *)) {
[super viewSafeAreaInsetsDidChange];
CGRect safeArea = [self viewSafeArea];
CGSize toolbarSize = [self.explorerToolbar sizeThatFits:CGSizeMake(
CGRectGetWidth(self.view.bounds), CGRectGetHeight(safeArea)
)];
[self updateToolbarPositionWithUnconstrainedFrame:CGRectMake(
CGRectGetMinX(self.explorerToolbar.frame),
CGRectGetMinY(self.explorerToolbar.frame),
toolbarSize.width,
toolbarSize.height)
];
}
}
#pragma mark - Touch Handling
- (BOOL)shouldReceiveTouchAtWindowPoint:(CGPoint)pointInWindowCoordinates {
BOOL shouldReceiveTouch = NO;
CGPoint pointInLocalCoordinates = [self.view convertPoint:pointInWindowCoordinates fromView:nil];
// Always if it's on the toolbar
if (CGRectContainsPoint(self.explorerToolbar.frame, pointInLocalCoordinates)) {
shouldReceiveTouch = YES;
}
// Always if we're in selection mode
if (!shouldReceiveTouch && self.currentMode == FLEXExplorerModeSelect) {
shouldReceiveTouch = YES;
}
// Always in move mode too
if (!shouldReceiveTouch && self.currentMode == FLEXExplorerModeMove) {
shouldReceiveTouch = YES;
}
// Always if we have a modal presented
if (!shouldReceiveTouch && self.presentedViewController) {
shouldReceiveTouch = YES;
}
return shouldReceiveTouch;
}
#pragma mark - FLEXHierarchyDelegate
- (void)viewHierarchyDidDismiss:(UIView *)selectedView {
// Note that we need to wait until the view controller is dismissed to calculate the frame
// of the outline view, otherwise the coordinate conversion doesn't give the correct result.
[self toggleViewsToolWithCompletion:^{
// If the selected view is outside of the tap point array (selected from "Full Hierarchy"),
// then clear out the tap point array and remove all the outline views.
if (![self.viewsAtTapPoint containsObject:selectedView]) {
self.viewsAtTapPoint = nil;
[self removeAndClearOutlineViews];
}
// If we now have a selected view and we didn't have one previously, go to "select" mode.
if (self.currentMode == FLEXExplorerModeDefault && selectedView) {
self.currentMode = FLEXExplorerModeSelect;
}
// The selected view setter will also update the selected view overlay appropriately.
self.selectedView = selectedView;
}];
}
#pragma mark - Modal Presentation and Window Management
- (void)presentViewController:(UIViewController *)toPresent
animated:(BOOL)animated
completion:(void (^)(void))completion {
// Make our window key to correctly handle input.
[self.view.window makeKeyWindow];
// Move the status bar on top of FLEX so we can get scroll to top behavior for taps.
if (!@available(iOS 13, *)) {
[self statusWindow].windowLevel = self.view.window.windowLevel + 1.0;
}
// Back up and replace the UIMenuController items
// Edit: no longer replacing the items, but still backing them
// up in case we start replacing them again in the future
self.appMenuItems = UIMenuController.sharedMenuController.menuItems;
// Show the view controller
[super presentViewController:toPresent animated:animated completion:completion];
}
- (void)dismissViewControllerAnimated:(BOOL)animated completion:(void (^)(void))completion {
UIWindow *appWindow = self.window.previousKeyWindow;
[appWindow makeKeyWindow];
[appWindow.rootViewController setNeedsStatusBarAppearanceUpdate];
// Restore previous UIMenuController items
// Back up and replace the UIMenuController items
UIMenuController.sharedMenuController.menuItems = self.appMenuItems;
[UIMenuController.sharedMenuController update];
self.appMenuItems = nil;
// Restore the status bar window's normal window level.
// We want it above FLEX while a modal is presented for
// scroll to top, but below FLEX otherwise for exploration.
[self statusWindow].windowLevel = UIWindowLevelStatusBar;
[self updateButtonStates];
[super dismissViewControllerAnimated:animated completion:completion];
}
- (BOOL)wantsWindowToBecomeKey {
return self.window.previousKeyWindow != nil;
}
- (void)toggleToolWithViewControllerProvider:(UINavigationController *(^)(void))future
completion:(void (^)(void))completion {
if (self.presentedViewController) {
// We do NOT want to present the future; this is
// a convenience method for toggling the SAME TOOL
[self dismissViewControllerAnimated:YES completion:completion];
} else if (future) {
[self presentViewController:future() animated:YES completion:completion];
}
}
- (void)presentTool:(UINavigationController *(^)(void))future
completion:(void (^)(void))completion {
if (self.presentedViewController) {
// If a tool is already presented, dismiss it first
[self dismissViewControllerAnimated:YES completion:^{
[self presentViewController:future() animated:YES completion:completion];
}];
} else if (future) {
[self presentViewController:future() animated:YES completion:completion];
}
}
- (FLEXWindow *)window {
return (id)self.view.window;
}
#pragma mark - Keyboard Shortcut Helpers
- (void)toggleSelectTool {
if (self.currentMode == FLEXExplorerModeSelect) {
self.currentMode = FLEXExplorerModeDefault;
} else {
self.currentMode = FLEXExplorerModeSelect;
}
}
- (void)toggleMoveTool {
if (self.currentMode == FLEXExplorerModeMove) {
self.currentMode = FLEXExplorerModeSelect;
} else if (self.currentMode == FLEXExplorerModeSelect && self.selectedView) {
self.currentMode = FLEXExplorerModeMove;
}
}
- (void)toggleViewsTool {
[self toggleViewsToolWithCompletion:nil];
}
- (void)toggleViewsToolWithCompletion:(void(^)(void))completion {
[self toggleToolWithViewControllerProvider:^UINavigationController *{
if (self.selectedView) {
return [FLEXHierarchyViewController
delegate:self
viewsAtTap:self.viewsAtTapPoint
selectedView:self.selectedView
];
} else {
return [FLEXHierarchyViewController delegate:self];
}
} completion:completion];
}
- (void)toggleMenuTool {
[self toggleToolWithViewControllerProvider:^UINavigationController *{
return [FLEXNavigationController withRootViewController:[FLEXGlobalsViewController new]];
} completion:nil];
}
- (BOOL)handleDownArrowKeyPressed {
if (self.currentMode == FLEXExplorerModeMove) {
CGRect frame = self.selectedView.frame;
frame.origin.y += 1.0 / UIScreen.mainScreen.scale;
self.selectedView.frame = frame;
} else if (self.currentMode == FLEXExplorerModeSelect && self.viewsAtTapPoint.count > 0) {
NSInteger selectedViewIndex = [self.viewsAtTapPoint indexOfObject:self.selectedView];
if (selectedViewIndex > 0) {
self.selectedView = [self.viewsAtTapPoint objectAtIndex:selectedViewIndex - 1];
}
} else {
return NO;
}
return YES;
}
- (BOOL)handleUpArrowKeyPressed {
if (self.currentMode == FLEXExplorerModeMove) {
CGRect frame = self.selectedView.frame;
frame.origin.y -= 1.0 / UIScreen.mainScreen.scale;
self.selectedView.frame = frame;
} else if (self.currentMode == FLEXExplorerModeSelect && self.viewsAtTapPoint.count > 0) {
NSInteger selectedViewIndex = [self.viewsAtTapPoint indexOfObject:self.selectedView];
if (selectedViewIndex < self.viewsAtTapPoint.count - 1) {
self.selectedView = [self.viewsAtTapPoint objectAtIndex:selectedViewIndex + 1];
}
} else {
return NO;
}
return YES;
}
- (BOOL)handleRightArrowKeyPressed {
if (self.currentMode == FLEXExplorerModeMove) {
CGRect frame = self.selectedView.frame;
frame.origin.x += 1.0 / UIScreen.mainScreen.scale;
self.selectedView.frame = frame;
return YES;
}
return NO;
}
- (BOOL)handleLeftArrowKeyPressed {
if (self.currentMode == FLEXExplorerModeMove) {
CGRect frame = self.selectedView.frame;
frame.origin.x -= 1.0 / UIScreen.mainScreen.scale;
self.selectedView.frame = frame;
return YES;
}
return NO;
}
@end

View File

@@ -0,0 +1,19 @@
//
// FLEXViewControllersViewController.h
// FLEX
//
// Created by Tanner Bennett on 2/13/20.
// Copyright © 2020 FLEX Team. All rights reserved.
//
#import "FLEXFilteringTableViewController.h"
NS_ASSUME_NONNULL_BEGIN
@interface FLEXViewControllersViewController : FLEXFilteringTableViewController
+ (instancetype)controllersForViews:(NSArray<UIView *> *)views;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,79 @@
//
// FLEXViewControllersViewController.m
// FLEX
//
// Created by Tanner Bennett on 2/13/20.
// Copyright © 2020 FLEX Team. All rights reserved.
//
#import "FLEXViewControllersViewController.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXMutableListSection.h"
#import "FLEXUtility.h"
@interface FLEXViewControllersViewController ()
@property (nonatomic, readonly) FLEXMutableListSection *section;
@property (nonatomic, readonly) NSArray<UIViewController *> *controllers;
@end
@implementation FLEXViewControllersViewController
@dynamic sections, allSections;
#pragma mark - Initialization
+ (instancetype)controllersForViews:(NSArray<UIView *> *)views {
return [[self alloc] initWithViews:views];
}
- (id)initWithViews:(NSArray<UIView *> *)views {
NSParameterAssert(views.count);
self = [self initWithStyle:UITableViewStylePlain];
if (self) {
_controllers = [views flex_mapped:^id(UIView *view, NSUInteger idx) {
return [FLEXUtility viewControllerForView:view];
}];
}
return self;
}
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"View Controllers at Tap";
self.showsSearchBar = YES;
[self disableToolbar];
}
- (NSArray<FLEXTableViewSection *> *)makeSections {
_section = [FLEXMutableListSection list:self.controllers
cellConfiguration:^(UITableViewCell *cell, UIViewController *controller, NSInteger row) {
cell.textLabel.text = [NSString
stringWithFormat:@"%@ — %p", NSStringFromClass(controller.class), controller
];
cell.detailTextLabel.text = controller.view.description;
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
cell.textLabel.lineBreakMode = NSLineBreakByTruncatingTail;
} filterMatcher:^BOOL(NSString *filterText, UIViewController *controller) {
return [NSStringFromClass(controller.class) localizedCaseInsensitiveContainsString:filterText];
}];
self.section.selectionHandler = ^(UIViewController *host, UIViewController *controller) {
[host.navigationController pushViewController:
[FLEXObjectExplorerFactory explorerViewControllerForObject:controller]
animated:YES];
};
self.section.customTitle = @"View Controllers";
return @[self.section];
}
#pragma mark - Private
- (void)dismissAnimated {
[self dismissViewControllerAnimated:YES completion:nil];
}
@end

View File

@@ -0,0 +1,29 @@
//
// FLEXWindow.h
// Flipboard
//
// Created by Ryan Olson on 4/13/14.
// Copyright (c) 2020 FLEX Team. All rights reserved.
//
#import <UIKit/UIKit.h>
@protocol FLEXWindowEventDelegate <NSObject>
- (BOOL)shouldHandleTouchAtPoint:(CGPoint)pointInWindow;
- (BOOL)canBecomeKeyWindow;
@end
#pragma mark -
@interface FLEXWindow : UIWindow
@property (nonatomic, weak) id <FLEXWindowEventDelegate> eventDelegate;
/// Tracked so we can restore the key window after dismissing a modal.
/// We need to become key after modal presentation so we can correctly capture input.
/// If we're just showing the toolbar, we want the main app's window to remain key
/// so that we don't interfere with input, status bar, etc.
@property (nonatomic, readonly) UIWindow *previousKeyWindow;
@end

View File

@@ -0,0 +1,72 @@
//
// FLEXWindow.m
// Flipboard
//
// Created by Ryan Olson on 4/13/14.
// Copyright (c) 2020 FLEX Team. All rights reserved.
//
#import "FLEXWindow.h"
#import "FLEXUtility.h"
#import <objc/runtime.h>
@implementation FLEXWindow
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
// Some apps have windows at UIWindowLevelStatusBar + n.
// If we make the window level too high, we block out UIAlertViews.
// There's a balance between staying above the app's windows and staying below alerts.
// UIWindowLevelStatusBar + 100 seems to hit that balance.
self.windowLevel = UIWindowLevelStatusBar + 100.0;
}
return self;
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
BOOL pointInside = NO;
if ([self.eventDelegate shouldHandleTouchAtPoint:point]) {
pointInside = [super pointInside:point withEvent:event];
}
return pointInside;
}
- (BOOL)shouldAffectStatusBarAppearance {
return [self isKeyWindow];
}
- (BOOL)canBecomeKeyWindow {
return [self.eventDelegate canBecomeKeyWindow];
}
- (void)makeKeyWindow {
_previousKeyWindow = FLEXUtility.appKeyWindow;
[super makeKeyWindow];
}
- (void)resignKeyWindow {
[super resignKeyWindow];
_previousKeyWindow = nil;
}
+ (void)initialize {
// This adds a method (superclass override) at runtime which gives us the status bar behavior we want.
// The FLEX window is intended to be an overlay that generally doesn't affect the app underneath.
// Most of the time, we want the app's main window(s) to be in control of status bar behavior.
// Done at runtime with an obfuscated selector because it is private API. But you shouldn't ship this to the App Store anyways...
NSString *canAffectSelectorString = [@[@"_can", @"Affect", @"Status", @"Bar", @"Appearance"] componentsJoinedByString:@""];
SEL canAffectSelector = NSSelectorFromString(canAffectSelectorString);
Method shouldAffectMethod = class_getInstanceMethod(self, @selector(shouldAffectStatusBarAppearance));
IMP canAffectImplementation = method_getImplementation(shouldAffectMethod);
class_addMethod(self, canAffectSelector, canAffectImplementation, method_getTypeEncoding(shouldAffectMethod));
// One more...
NSString *canBecomeKeySelectorString = [NSString stringWithFormat:@"_%@", NSStringFromSelector(@selector(canBecomeKeyWindow))];
SEL canBecomeKeySelector = NSSelectorFromString(canBecomeKeySelectorString);
Method canBecomeKeyMethod = class_getInstanceMethod(self, @selector(canBecomeKeyWindow));
IMP canBecomeKeyImplementation = method_getImplementation(canBecomeKeyMethod);
class_addMethod(self, canBecomeKeySelector, canBecomeKeyImplementation, method_getTypeEncoding(canBecomeKeyMethod));
}
@end

View File

@@ -0,0 +1,17 @@
//
// FLEXWindowManagerController.h
// FLEX
//
// Created by Tanner on 2/6/20.
// Copyright © 2020 FLEX Team. All rights reserved.
//
#import "FLEXTableViewController.h"
NS_ASSUME_NONNULL_BEGIN
@interface FLEXWindowManagerController : FLEXTableViewController
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,302 @@
//
// FLEXWindowManagerController.m
// FLEX
//
// Created by Tanner on 2/6/20.
// Copyright © 2020 FLEX Team. All rights reserved.
//
#import "FLEXWindowManagerController.h"
#import "FLEXManager+Private.h"
#import "FLEXUtility.h"
#import "FLEXObjectExplorerFactory.h"
@interface FLEXWindowManagerController ()
@property (nonatomic) UIWindow *keyWindow;
@property (nonatomic, copy) NSString *keyWindowSubtitle;
@property (nonatomic, copy) NSArray<UIWindow *> *windows;
@property (nonatomic, copy) NSArray<NSString *> *windowSubtitles;
@property (nonatomic, copy) NSArray<UIScene *> *scenes API_AVAILABLE(ios(13));
@property (nonatomic, copy) NSArray<NSString *> *sceneSubtitles;
@property (nonatomic, copy) NSArray<NSArray *> *sections;
@end
@implementation FLEXWindowManagerController
#pragma mark - Initialization
- (id)init {
return [self initWithStyle:UITableViewStylePlain];
}
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"Windows";
if (@available(iOS 13, *)) {
self.title = @"Windows and Scenes";
}
[self disableToolbar];
[self reloadData];
}
#pragma mark - Private
- (void)reloadData {
self.keyWindow = UIApplication.sharedApplication.keyWindow;
self.windows = UIApplication.sharedApplication.windows;
self.keyWindowSubtitle = self.windowSubtitles[[self.windows indexOfObject:self.keyWindow]];
self.windowSubtitles = [self.windows flex_mapped:^id(UIWindow *window, NSUInteger idx) {
return [NSString stringWithFormat:@"Level: %@ — Root: %@",
@(window.windowLevel), window.rootViewController
];
}];
if (@available(iOS 13, *)) {
self.scenes = UIApplication.sharedApplication.connectedScenes.allObjects;
self.sceneSubtitles = [self.scenes flex_mapped:^id(UIScene *scene, NSUInteger idx) {
return [self sceneDescription:scene];
}];
self.sections = @[@[self.keyWindow], self.windows, self.scenes];
} else {
self.sections = @[@[self.keyWindow], self.windows];
}
[self.tableView reloadData];
}
- (void)dismissAnimated {
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void)showRevertOrDismissAlert:(void(^)(void))revertBlock {
[self.tableView deselectRowAtIndexPath:self.tableView.indexPathForSelectedRow animated:YES];
[self reloadData];
[self.tableView reloadData];
UIWindow *highestWindow = UIApplication.sharedApplication.keyWindow;
UIWindowLevel maxLevel = 0;
for (UIWindow *window in UIApplication.sharedApplication.windows) {
if (window.windowLevel > maxLevel) {
maxLevel = window.windowLevel;
highestWindow = window;
}
}
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(@"Keep Changes?");
make.message(@"If you do not wish to keep these settings, choose 'Revert Changes' below.");
make.button(@"Keep Changes").destructiveStyle();
make.button(@"Keep Changes and Dismiss").destructiveStyle().handler(^(NSArray<NSString *> *strings) {
[self dismissAnimated];
});
make.button(@"Revert Changes").cancelStyle().handler(^(NSArray<NSString *> *strings) {
revertBlock();
[self reloadData];
[self.tableView reloadData];
});
} showFrom:[FLEXUtility topViewControllerInWindow:highestWindow]];
}
- (NSString *)sceneDescription:(UIScene *)scene API_AVAILABLE(ios(13)) {
NSString *state = [self stringFromSceneState:scene.activationState];
NSString *title = scene.title.length ? scene.title : nil;
NSString *suffix = nil;
if ([scene isKindOfClass:[UIWindowScene class]]) {
UIWindowScene *windowScene = (id)scene;
suffix = FLEXPluralString(windowScene.windows.count, @"windows", @"window");
}
NSMutableString *description = state.mutableCopy;
if (title) {
[description appendFormat:@" — %@", title];
}
if (suffix) {
[description appendFormat:@" — %@", suffix];
}
return description.copy;
}
- (NSString *)stringFromSceneState:(UISceneActivationState)state API_AVAILABLE(ios(13)) {
switch (state) {
case UISceneActivationStateUnattached:
return @"Unattached";
case UISceneActivationStateForegroundActive:
return @"Active";
case UISceneActivationStateForegroundInactive:
return @"Inactive";
case UISceneActivationStateBackground:
return @"Backgrounded";
}
return [NSString stringWithFormat:@"Unknown state: %@", @(state)];
}
#pragma mark - Table View Data Source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return self.sections.count;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.sections[section].count;
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
switch (section) {
case 0: return @"Key Window";
case 1: return @"Windows";
case 2: return @"Connected Scenes";
}
return nil;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kFLEXDetailCell forIndexPath:indexPath];
cell.accessoryType = UITableViewCellAccessoryDetailButton;
cell.textLabel.lineBreakMode = NSLineBreakByTruncatingTail;
UIWindow *window = nil;
NSString *subtitle = nil;
switch (indexPath.section) {
case 0:
window = self.keyWindow;
subtitle = self.keyWindowSubtitle;
break;
case 1:
window = self.windows[indexPath.row];
subtitle = self.windowSubtitles[indexPath.row];
break;
case 2:
if (@available(iOS 13, *)) {
UIScene *scene = self.scenes[indexPath.row];
cell.textLabel.text = scene.description;
cell.detailTextLabel.text = self.sceneSubtitles[indexPath.row];
return cell;
}
}
cell.textLabel.text = window.description;
cell.detailTextLabel.text = [NSString
stringWithFormat:@"Level: %@ — Root: %@",
@((NSInteger)window.windowLevel), window.rootViewController.class
];
return cell;
}
#pragma mark - Table View Delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
UIWindow *window = nil;
NSString *subtitle = nil;
FLEXWindow *flex = FLEXManager.sharedManager.explorerWindow;
id cancelHandler = ^{
[self.tableView deselectRowAtIndexPath:self.tableView.indexPathForSelectedRow animated:YES];
};
switch (indexPath.section) {
case 0:
window = self.keyWindow;
subtitle = self.keyWindowSubtitle;
break;
case 1:
window = self.windows[indexPath.row];
subtitle = self.windowSubtitles[indexPath.row];
break;
case 2:
if (@available(iOS 13, *)) {
UIScene *scene = self.scenes[indexPath.row];
UIWindowScene *oldScene = flex.windowScene;
BOOL isWindowScene = [scene isKindOfClass:[UIWindowScene class]];
BOOL isFLEXScene = isWindowScene ? flex.windowScene == scene : NO;
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(NSStringFromClass(scene.class));
if (isWindowScene) {
if (isFLEXScene) {
make.message(@"Already the FLEX window scene");
}
make.button(@"Set as FLEX Window Scene")
.handler(^(NSArray<NSString *> *strings) {
flex.windowScene = (id)scene;
[self showRevertOrDismissAlert:^{
flex.windowScene = oldScene;
}];
}).enabled(!isFLEXScene);
make.button(@"Cancel").cancelStyle();
} else {
make.message(@"Not a UIWindowScene");
make.button(@"Dismiss").cancelStyle().handler(cancelHandler);
}
} showFrom:self];
}
}
__block UIWindow *targetWindow = nil, *oldKeyWindow = nil;
__block UIWindowLevel oldLevel;
__block BOOL wasVisible;
subtitle = [subtitle stringByAppendingString:
@"\n\n1) Adjust the FLEX window level relative to this window,\n"
"2) adjust this window's level relative to the FLEX window,\n"
"3) set this window's level to a specific value, or\n"
"4) make this window the key window if it isn't already."
];
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(NSStringFromClass(window.class)).message(subtitle);
make.button(@"Adjust FLEX Window Level").handler(^(NSArray<NSString *> *strings) {
targetWindow = flex; oldLevel = flex.windowLevel;
flex.windowLevel = window.windowLevel + strings.firstObject.integerValue;
[self showRevertOrDismissAlert:^{ targetWindow.windowLevel = oldLevel; }];
});
make.button(@"Adjust This Window's Level").handler(^(NSArray<NSString *> *strings) {
targetWindow = window; oldLevel = window.windowLevel;
window.windowLevel = flex.windowLevel + strings.firstObject.integerValue;
[self showRevertOrDismissAlert:^{ targetWindow.windowLevel = oldLevel; }];
});
make.button(@"Set This Window's Level").handler(^(NSArray<NSString *> *strings) {
targetWindow = window; oldLevel = window.windowLevel;
window.windowLevel = strings.firstObject.integerValue;
[self showRevertOrDismissAlert:^{ targetWindow.windowLevel = oldLevel; }];
});
make.button(@"Make Key And Visible").handler(^(NSArray<NSString *> *strings) {
oldKeyWindow = UIApplication.sharedApplication.keyWindow;
wasVisible = window.hidden;
[window makeKeyAndVisible];
[self showRevertOrDismissAlert:^{
window.hidden = wasVisible;
[oldKeyWindow makeKeyWindow];
}];
}).enabled(!window.isKeyWindow && !window.hidden);
make.button(@"Cancel").cancelStyle().handler(cancelHandler);
make.textField(@"+/- window level, i.e. 5 or -10");
} showFrom:self];
}
- (void)tableView:(UITableView *)tableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)ip {
[self.navigationController pushViewController:
[FLEXObjectExplorerFactory explorerViewControllerForObject:self.sections[ip.section][ip.row]]
animated:YES];
}
@end

View File

@@ -0,0 +1,45 @@
//
// FLEXTabList.h
// FLEX
//
// Created by Tanner on 2/1/20.
// Copyright © 2020 FLEX Team. All rights reserved.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface FLEXTabList : NSObject
@property (nonatomic, readonly, class) FLEXTabList *sharedList;
@property (nonatomic, readonly, nullable) UINavigationController *activeTab;
@property (nonatomic, readonly) NSArray<UINavigationController *> *openTabs;
/// Snapshots of each tab when they were last active.
@property (nonatomic, readonly) NSArray<UIImage *> *openTabSnapshots;
/// \c NSNotFound if no tabs are present.
/// Setting this property changes the active tab to one of the already open tabs.
@property (nonatomic) NSInteger activeTabIndex;
/// Adds a new tab and sets the new tab as the active tab.
- (void)addTab:(UINavigationController *)newTab;
/// Closes the given tab. If this tab was the active tab,
/// the most recent tab before that becomes the active tab.
- (void)closeTab:(UINavigationController *)tab;
/// Closes a tab at the given index. If this tab was the active tab,
/// the most recent tab before that becomes the active tab.
- (void)closeTabAtIndex:(NSInteger)idx;
/// Closes all of the tabs at the given indexes. If the active tab
/// is included, the most recent still-open tab becomes the active tab.
- (void)closeTabsAtIndexes:(NSIndexSet *)indexes;
/// A shortcut to close the active tab.
- (void)closeActiveTab;
/// A shortcut to close \e every tab.
- (void)closeAllTabs;
- (void)updateSnapshotForActiveTab;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,133 @@
//
// FLEXTabList.m
// FLEX
//
// Created by Tanner on 2/1/20.
// Copyright © 2020 FLEX Team. All rights reserved.
//
#import "FLEXTabList.h"
#import "FLEXUtility.h"
@interface FLEXTabList () {
NSMutableArray *_openTabs;
NSMutableArray *_openTabSnapshots;
}
@end
#pragma mark -
@implementation FLEXTabList
#pragma mark Initialization
+ (FLEXTabList *)sharedList {
static FLEXTabList *sharedList = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedList = [self new];
});
return sharedList;
}
- (id)init {
self = [super init];
if (self) {
_openTabs = [NSMutableArray new];
_openTabSnapshots = [NSMutableArray new];
_activeTabIndex = NSNotFound;
}
return self;
}
#pragma mark Private
- (void)chooseNewActiveTab {
if (self.openTabs.count) {
self.activeTabIndex = self.openTabs.count - 1;
} else {
self.activeTabIndex = NSNotFound;
}
}
#pragma mark Public
- (void)setActiveTabIndex:(NSInteger)idx {
NSParameterAssert(idx < self.openTabs.count || idx == NSNotFound);
if (_activeTabIndex == idx) return;
_activeTabIndex = idx;
_activeTab = (idx == NSNotFound) ? nil : self.openTabs[idx];
}
- (void)addTab:(UINavigationController *)newTab {
NSParameterAssert(newTab);
// Update snapshot of the last active tab
if (self.activeTab) {
[self updateSnapshotForActiveTab];
}
// Add new tab and snapshot,
// update active tab and index
[_openTabs addObject:newTab];
[_openTabSnapshots addObject:[FLEXUtility previewImageForView:newTab.view]];
_activeTab = newTab;
_activeTabIndex = self.openTabs.count - 1;
}
- (void)closeTab:(UINavigationController *)tab {
NSParameterAssert(tab);
NSInteger idx = [self.openTabs indexOfObject:tab];
if (idx != NSNotFound) {
[self closeTabAtIndex:idx];
}
}
- (void)closeTabAtIndex:(NSInteger)idx {
NSParameterAssert(idx < self.openTabs.count);
// Remove old tab and snapshot
[_openTabs removeObjectAtIndex:idx];
[_openTabSnapshots removeObjectAtIndex:idx];
// Update active tab and index if needed
if (self.activeTabIndex == idx) {
[self chooseNewActiveTab];
}
}
- (void)closeTabsAtIndexes:(NSIndexSet *)indexes {
// Remove old tabs and snapshot
[_openTabs removeObjectsAtIndexes:indexes];
[_openTabSnapshots removeObjectsAtIndexes:indexes];
// Update active tab and index if needed
if ([indexes containsIndex:self.activeTabIndex]) {
[self chooseNewActiveTab];
}
}
- (void)closeActiveTab {
[self closeTab:self.activeTab];
}
- (void)closeAllTabs {
// Remove tabs and snapshots
[_openTabs removeAllObjects];
[_openTabSnapshots removeAllObjects];
// Update active tab index
self.activeTabIndex = NSNotFound;
}
- (void)updateSnapshotForActiveTab {
if (self.activeTabIndex != NSNotFound) {
UIImage *newSnapshot = [FLEXUtility previewImageForView:self.activeTab.view];
[_openTabSnapshots replaceObjectAtIndex:self.activeTabIndex withObject:newSnapshot];
}
}
@end

View File

@@ -0,0 +1,13 @@
//
// FLEXTabsViewController.h
// FLEX
//
// Created by Tanner on 2/4/20.
// Copyright © 2020 FLEX Team. All rights reserved.
//
#import "FLEXTableViewController.h"
@interface FLEXTabsViewController : FLEXTableViewController
@end

View File

@@ -0,0 +1,335 @@
//
// FLEXTabsViewController.m
// FLEX
//
// Created by Tanner on 2/4/20.
// Copyright © 2020 FLEX Team. All rights reserved.
//
#import "FLEXTabsViewController.h"
#import "FLEXNavigationController.h"
#import "FLEXTabList.h"
#import "FLEXBookmarkManager.h"
#import "FLEXTableView.h"
#import "FLEXUtility.h"
#import "FLEXColor.h"
#import "UIBarButtonItem+FLEX.h"
#import "FLEXExplorerViewController.h"
#import "FLEXGlobalsViewController.h"
#import "FLEXBookmarksViewController.h"
@interface FLEXTabsViewController ()
@property (nonatomic, copy) NSArray<UINavigationController *> *openTabs;
@property (nonatomic, copy) NSArray<UIImage *> *tabSnapshots;
@property (nonatomic) NSInteger activeIndex;
@property (nonatomic) BOOL presentNewActiveTabOnDismiss;
@property (nonatomic, readonly) FLEXExplorerViewController *corePresenter;
@end
@implementation FLEXTabsViewController
#pragma mark - Initialization
- (id)init {
return [self initWithStyle:UITableViewStylePlain];
}
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"Open Tabs";
self.navigationController.hidesBarsOnSwipe = NO;
self.tableView.allowsMultipleSelectionDuringEditing = YES;
[self reloadData:NO];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self setupDefaultBarItems];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
// Instead of updating the active snapshot before we present,
// we update it after we present to avoid pre-presenation latency
dispatch_async(dispatch_get_main_queue(), ^{
[FLEXTabList.sharedList updateSnapshotForActiveTab];
[self reloadData:NO];
[self.tableView reloadData];
});
}
#pragma mark - Private
/// @param trackActiveTabDelta whether to check if the active
/// tab changed and needs to be presented upon "Done" dismissal.
/// @return whether the active tab changed or not (if there are any tabs left)
- (BOOL)reloadData:(BOOL)trackActiveTabDelta {
BOOL activeTabDidChange = NO;
FLEXTabList *list = FLEXTabList.sharedList;
// Flag to enable check to determine whether
if (trackActiveTabDelta) {
NSInteger oldActiveIndex = self.activeIndex;
if (oldActiveIndex != list.activeTabIndex && list.activeTabIndex != NSNotFound) {
self.presentNewActiveTabOnDismiss = YES;
activeTabDidChange = YES;
} else if (self.presentNewActiveTabOnDismiss) {
// If we had something to present before, now we don't
// (i.e. activeTabIndex == NSNotFound)
self.presentNewActiveTabOnDismiss = NO;
}
}
// We assume the tabs aren't going to change out from under us, since
// presenting any other tool via keyboard shortcuts should dismiss us first
self.openTabs = list.openTabs;
self.tabSnapshots = list.openTabSnapshots;
self.activeIndex = list.activeTabIndex;
return activeTabDidChange;
}
- (void)reloadActiveTabRowIfChanged:(BOOL)activeTabChanged {
// Refresh the newly active tab row if needed
if (activeTabChanged) {
NSIndexPath *active = [NSIndexPath
indexPathForRow:self.activeIndex inSection:0
];
[self.tableView reloadRowsAtIndexPaths:@[active] withRowAnimation:UITableViewRowAnimationNone];
}
}
- (void)setupDefaultBarItems {
self.navigationItem.rightBarButtonItem = FLEXBarButtonItemSystem(Done, self, @selector(dismissAnimated));
self.toolbarItems = @[
UIBarButtonItem.flex_fixedSpace,
UIBarButtonItem.flex_flexibleSpace,
FLEXBarButtonItemSystem(Add, self, @selector(addTabButtonPressed:)),
UIBarButtonItem.flex_flexibleSpace,
FLEXBarButtonItemSystem(Edit, self, @selector(toggleEditing)),
];
// Disable editing if no tabs available
self.toolbarItems.lastObject.enabled = self.openTabs.count > 0;
}
- (void)setupEditingBarItems {
self.navigationItem.rightBarButtonItem = nil;
self.toolbarItems = @[
[UIBarButtonItem flex_itemWithTitle:@"Close All" target:self action:@selector(closeAllButtonPressed:)],
UIBarButtonItem.flex_flexibleSpace,
[UIBarButtonItem flex_disabledSystemItem:UIBarButtonSystemItemAdd],
UIBarButtonItem.flex_flexibleSpace,
// We use a non-system done item because we change its title dynamically
[UIBarButtonItem flex_doneStyleitemWithTitle:@"Done" target:self action:@selector(toggleEditing)]
];
self.toolbarItems.firstObject.tintColor = FLEXColor.destructiveColor;
}
- (FLEXExplorerViewController *)corePresenter {
// We must be presented by a FLEXExplorerViewController, or presented
// by another view controller that was presented by FLEXExplorerViewController
FLEXExplorerViewController *presenter = (id)self.presentingViewController;
presenter = (id)presenter.presentingViewController ?: presenter;
NSAssert(
[presenter isKindOfClass:[FLEXExplorerViewController class]],
@"The tabs view controller expects to be presented by the explorer controller"
);
return presenter;
}
#pragma mark Button Actions
- (void)dismissAnimated {
if (self.presentNewActiveTabOnDismiss) {
// The active tab was closed so we need to present the new one
UIViewController *activeTab = FLEXTabList.sharedList.activeTab;
FLEXExplorerViewController *presenter = self.corePresenter;
[presenter dismissViewControllerAnimated:YES completion:^{
[presenter presentViewController:activeTab animated:YES completion:nil];
}];
} else if (self.activeIndex == NSNotFound) {
// The only tab was closed, so dismiss everything
[self.corePresenter dismissViewControllerAnimated:YES completion:nil];
} else {
// Simple dismiss with the same active tab, only dismiss myself
[self dismissViewControllerAnimated:YES completion:nil];
}
}
- (void)toggleEditing {
NSArray<NSIndexPath *> *selected = self.tableView.indexPathsForSelectedRows;
self.editing = !self.editing;
if (self.isEditing) {
[self setupEditingBarItems];
} else {
[self setupDefaultBarItems];
// Get index set of tabs to close
NSMutableIndexSet *indexes = [NSMutableIndexSet new];
for (NSIndexPath *ip in selected) {
[indexes addIndex:ip.row];
}
if (selected.count) {
// Close tabs and update data source
[FLEXTabList.sharedList closeTabsAtIndexes:indexes];
BOOL activeTabChanged = [self reloadData:YES];
// Remove deleted rows
[self.tableView deleteRowsAtIndexPaths:selected withRowAnimation:UITableViewRowAnimationAutomatic];
// Refresh the newly active tab row if needed
[self reloadActiveTabRowIfChanged:activeTabChanged];
}
}
}
- (void)addTabButtonPressed:(UIBarButtonItem *)sender {
if (FLEXBookmarkManager.bookmarks.count) {
[FLEXAlert makeSheet:^(FLEXAlert *make) {
make.title(@"New Tab");
make.button(@"Main Menu").handler(^(NSArray<NSString *> *strings) {
[self addTabAndDismiss:[FLEXNavigationController
withRootViewController:[FLEXGlobalsViewController new]
]];
});
make.button(@"Choose from Bookmarks").handler(^(NSArray<NSString *> *strings) {
[self presentViewController:[FLEXNavigationController
withRootViewController:[FLEXBookmarksViewController new]
] animated:YES completion:nil];
});
make.button(@"Cancel").cancelStyle();
} showFrom:self source:sender];
} else {
// No bookmarks, just open the main menu
[self addTabAndDismiss:[FLEXNavigationController
withRootViewController:[FLEXGlobalsViewController new]
]];
}
}
- (void)addTabAndDismiss:(UINavigationController *)newTab {
FLEXExplorerViewController *presenter = self.corePresenter;
[presenter dismissViewControllerAnimated:YES completion:^{
[presenter presentViewController:newTab animated:YES completion:nil];
}];
}
- (void)closeAllButtonPressed:(UIBarButtonItem *)sender {
[FLEXAlert makeSheet:^(FLEXAlert *make) {
NSInteger count = self.openTabs.count;
NSString *title = FLEXPluralFormatString(count, @"Close %@ tabs", @"Close %@ tab");
make.button(title).destructiveStyle().handler(^(NSArray<NSString *> *strings) {
[self closeAll];
[self toggleEditing];
});
make.button(@"Cancel").cancelStyle();
} showFrom:self source:sender];
}
- (void)closeAll {
NSInteger rowCount = self.openTabs.count;
// Close tabs and update data source
[FLEXTabList.sharedList closeAllTabs];
[self reloadData:YES];
// Delete rows from table view
NSArray<NSIndexPath *> *allRows = [NSArray flex_forEachUpTo:rowCount map:^id(NSUInteger row) {
return [NSIndexPath indexPathForRow:row inSection:0];
}];
[self.tableView deleteRowsAtIndexPaths:allRows withRowAnimation:UITableViewRowAnimationAutomatic];
}
#pragma mark - Table View Data Source
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.openTabs.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kFLEXDetailCell forIndexPath:indexPath];
UINavigationController *tab = self.openTabs[indexPath.row];
cell.imageView.image = self.tabSnapshots[indexPath.row];
cell.textLabel.text = tab.topViewController.title;
cell.detailTextLabel.text = FLEXPluralString(tab.viewControllers.count, @"pages", @"page");
if (!cell.tag) {
cell.textLabel.lineBreakMode = NSLineBreakByTruncatingTail;
cell.textLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline];
cell.detailTextLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleSubheadline];
cell.tag = 1;
}
if (indexPath.row == self.activeIndex) {
cell.backgroundColor = FLEXColor.secondaryBackgroundColor;
} else {
cell.backgroundColor = FLEXColor.primaryBackgroundColor;
}
return cell;
}
#pragma mark - Table View Delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if (self.editing) {
// Case: editing with multi-select
self.toolbarItems.lastObject.title = @"Close Selected";
self.toolbarItems.lastObject.tintColor = FLEXColor.destructiveColor;
} else {
if (self.activeIndex == indexPath.row && self.corePresenter != self.presentingViewController) {
// Case: selected the already active tab
[self dismissAnimated];
} else {
// Case: selected a different tab,
// or selected a tab when presented from the FLEX toolbar
FLEXTabList.sharedList.activeTabIndex = indexPath.row;
self.presentNewActiveTabOnDismiss = YES;
[self dismissAnimated];
}
}
}
- (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath {
NSParameterAssert(self.editing);
if (tableView.indexPathsForSelectedRows.count == 0) {
self.toolbarItems.lastObject.title = @"Done";
self.toolbarItems.lastObject.tintColor = self.view.tintColor;
}
}
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
return YES;
}
- (void)tableView:(UITableView *)table
commitEditingStyle:(UITableViewCellEditingStyle)edit
forRowAtIndexPath:(NSIndexPath *)indexPath {
NSParameterAssert(edit == UITableViewCellEditingStyleDelete);
// Close tab and update data source
[FLEXTabList.sharedList closeTab:self.openTabs[indexPath.row]];
BOOL activeTabChanged = [self reloadData:YES];
// Delete row from table view
[table deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
// Refresh the newly active tab row if needed
[self reloadActiveTabRowIfChanged:activeTabChanged];
}
@end