mirror of
https://github.com/SoPat712/YTLitePlus.git
synced 2026-02-10 08:18:37 -05:00
added files via upload
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
61
Tweaks/FLEX/ExplorerInterface/FLEXExplorerViewController.h
Normal file
61
Tweaks/FLEX/ExplorerInterface/FLEXExplorerViewController.h
Normal 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
|
||||
1050
Tweaks/FLEX/ExplorerInterface/FLEXExplorerViewController.m
Normal file
1050
Tweaks/FLEX/ExplorerInterface/FLEXExplorerViewController.m
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
29
Tweaks/FLEX/ExplorerInterface/FLEXWindow.h
Normal file
29
Tweaks/FLEX/ExplorerInterface/FLEXWindow.h
Normal 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
|
||||
72
Tweaks/FLEX/ExplorerInterface/FLEXWindow.m
Normal file
72
Tweaks/FLEX/ExplorerInterface/FLEXWindow.m
Normal 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
|
||||
17
Tweaks/FLEX/ExplorerInterface/FLEXWindowManagerController.h
Normal file
17
Tweaks/FLEX/ExplorerInterface/FLEXWindowManagerController.h
Normal 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
|
||||
302
Tweaks/FLEX/ExplorerInterface/FLEXWindowManagerController.m
Normal file
302
Tweaks/FLEX/ExplorerInterface/FLEXWindowManagerController.m
Normal 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
|
||||
45
Tweaks/FLEX/ExplorerInterface/Tabs/FLEXTabList.h
Normal file
45
Tweaks/FLEX/ExplorerInterface/Tabs/FLEXTabList.h
Normal 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
|
||||
133
Tweaks/FLEX/ExplorerInterface/Tabs/FLEXTabList.m
Normal file
133
Tweaks/FLEX/ExplorerInterface/Tabs/FLEXTabList.m
Normal 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
|
||||
13
Tweaks/FLEX/ExplorerInterface/Tabs/FLEXTabsViewController.h
Normal file
13
Tweaks/FLEX/ExplorerInterface/Tabs/FLEXTabsViewController.h
Normal 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
|
||||
335
Tweaks/FLEX/ExplorerInterface/Tabs/FLEXTabsViewController.m
Normal file
335
Tweaks/FLEX/ExplorerInterface/Tabs/FLEXTabsViewController.m
Normal 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
|
||||
Reference in New Issue
Block a user