mirror of
https://github.com/SoPat712/YTLitePlus.git
synced 2025-08-25 11:55:28 -04:00
added files via upload
This commit is contained in:
@@ -0,0 +1,417 @@
|
||||
//
|
||||
// FLEXKeyPathSearchController.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 3/23/17.
|
||||
// Copyright © 2017 Tanner Bennett. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXKeyPathSearchController.h"
|
||||
#import "FLEXRuntimeKeyPathTokenizer.h"
|
||||
#import "FLEXRuntimeController.h"
|
||||
#import "NSString+FLEX.h"
|
||||
#import "NSArray+FLEX.h"
|
||||
#import "UITextField+Range.h"
|
||||
#import "NSTimer+FLEX.h"
|
||||
#import "FLEXTableView.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
|
||||
@interface FLEXKeyPathSearchController ()
|
||||
@property (nonatomic, readonly, weak) id<FLEXKeyPathSearchControllerDelegate> delegate;
|
||||
@property (nonatomic) NSTimer *timer;
|
||||
/// If \c keyPath is \c nil or if it only has a \c bundleKey, this is
|
||||
/// a list of bundle key path components like \c UICatalog or \c UIKit\.framework
|
||||
/// If \c keyPath has more than a \c bundleKey then it is a list of class names.
|
||||
@property (nonatomic) NSArray<NSString *> *bundlesOrClasses;
|
||||
/// nil when search bar is empty
|
||||
@property (nonatomic) FLEXRuntimeKeyPath *keyPath;
|
||||
|
||||
@property (nonatomic, readonly) NSString *emptySuggestion;
|
||||
|
||||
/// Used to track which methods go with which classes. This is used in
|
||||
/// two scenarios: (1) when the target class is absolute and has classes,
|
||||
/// (this list will include the "leaf" class as well as parent classes in this case)
|
||||
/// or (2) when the class key is a wildcard and we're searching methods in many
|
||||
/// classes at once. Each list in \c classesToMethods correspnds to a class here.
|
||||
@property (nonatomic) NSArray<NSString *> *classes;
|
||||
/// A filtered version of \c classes used when searching for a specific attribute.
|
||||
/// Classes with no matching ivars/properties/methods are not shown.
|
||||
@property (nonatomic) NSArray<NSString *> *filteredClasses;
|
||||
// We use this regardless of whether the target class is absolute, just as above
|
||||
@property (nonatomic) NSArray<NSArray<FLEXMethod *> *> *classesToMethods;
|
||||
@end
|
||||
|
||||
@implementation FLEXKeyPathSearchController
|
||||
|
||||
+ (instancetype)delegate:(id<FLEXKeyPathSearchControllerDelegate>)delegate {
|
||||
FLEXKeyPathSearchController *controller = [self new];
|
||||
controller->_bundlesOrClasses = [FLEXRuntimeController allBundleNames];
|
||||
controller->_delegate = delegate;
|
||||
controller->_emptySuggestion = NSBundle.mainBundle.executablePath.lastPathComponent;
|
||||
|
||||
NSParameterAssert(delegate.tableView);
|
||||
NSParameterAssert(delegate.searchController);
|
||||
|
||||
delegate.tableView.delegate = controller;
|
||||
delegate.tableView.dataSource = controller;
|
||||
|
||||
UISearchBar *searchBar = delegate.searchController.searchBar;
|
||||
searchBar.delegate = controller;
|
||||
searchBar.keyboardType = UIKeyboardTypeWebSearch;
|
||||
searchBar.autocorrectionType = UITextAutocorrectionTypeNo;
|
||||
if (@available(iOS 11, *)) {
|
||||
searchBar.smartQuotesType = UITextSmartQuotesTypeNo;
|
||||
searchBar.smartInsertDeleteType = UITextSmartInsertDeleteTypeNo;
|
||||
}
|
||||
|
||||
return controller;
|
||||
}
|
||||
|
||||
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
|
||||
if (scrollView.isTracking || scrollView.isDragging || scrollView.isDecelerating) {
|
||||
[self.delegate.searchController.searchBar resignFirstResponder];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setToolbar:(FLEXRuntimeBrowserToolbar *)toolbar {
|
||||
_toolbar = toolbar;
|
||||
self.delegate.searchController.searchBar.inputAccessoryView = toolbar;
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)classesOf:(NSString *)className {
|
||||
Class baseClass = NSClassFromString(className);
|
||||
if (!baseClass) {
|
||||
return @[];
|
||||
}
|
||||
|
||||
// Find classes
|
||||
NSMutableArray<NSString*> *classes = [NSMutableArray arrayWithObject:className];
|
||||
while ([baseClass superclass]) {
|
||||
[classes addObject:NSStringFromClass([baseClass superclass])];
|
||||
baseClass = [baseClass superclass];
|
||||
}
|
||||
|
||||
return classes;
|
||||
}
|
||||
|
||||
#pragma mark Key path stuff
|
||||
|
||||
- (void)didSelectKeyPathOption:(NSString *)text {
|
||||
[_timer invalidate]; // Still might be waiting to refresh when method is selected
|
||||
|
||||
// Change "Bundle.fooba" to "Bundle.foobar."
|
||||
NSString *orig = self.delegate.searchController.searchBar.text;
|
||||
NSString *keyPath = [orig flex_stringByReplacingLastKeyPathComponent:text];
|
||||
self.delegate.searchController.searchBar.text = keyPath;
|
||||
|
||||
self.keyPath = [FLEXRuntimeKeyPathTokenizer tokenizeString:keyPath];
|
||||
|
||||
// Get classes if class was selected
|
||||
if (self.keyPath.classKey.isAbsolute && self.keyPath.methodKey.isAny) {
|
||||
[self didSelectAbsoluteClass:text];
|
||||
} else {
|
||||
self.classes = nil;
|
||||
self.filteredClasses = nil;
|
||||
}
|
||||
|
||||
[self updateTable];
|
||||
}
|
||||
|
||||
- (void)didSelectAbsoluteClass:(NSString *)name {
|
||||
self.classes = [self classesOf:name];
|
||||
self.filteredClasses = self.classes;
|
||||
self.bundlesOrClasses = nil;
|
||||
self.classesToMethods = nil;
|
||||
}
|
||||
|
||||
- (void)didPressButton:(NSString *)text insertInto:(UISearchBar *)searchBar {
|
||||
[self.toolbar setKeyPath:self.keyPath suggestions:nil];
|
||||
|
||||
// Available since at least iOS 9, still present in iOS 13
|
||||
UITextField *field = [searchBar valueForKey:@"_searchBarTextField"];
|
||||
|
||||
if ([self searchBar:searchBar shouldChangeTextInRange:field.flex_selectedRange replacementText:text]) {
|
||||
[field replaceRange:field.selectedTextRange withText:text];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)suggestions {
|
||||
if (self.bundlesOrClasses) {
|
||||
if (self.classes) {
|
||||
if (self.classesToMethods) {
|
||||
// We have selected a class and are searching metadata
|
||||
return nil;
|
||||
}
|
||||
|
||||
// We are currently searching classes
|
||||
return [self.filteredClasses flex_subArrayUpto:10];
|
||||
}
|
||||
|
||||
if (!self.keyPath) {
|
||||
// Search bar is empty
|
||||
return @[self.emptySuggestion];
|
||||
}
|
||||
|
||||
// We are currently searching bundles
|
||||
return [self.bundlesOrClasses flex_subArrayUpto:10];
|
||||
}
|
||||
|
||||
// We have nothing at all to even search
|
||||
return nil;
|
||||
}
|
||||
|
||||
#pragma mark - Filtering + UISearchBarDelegate
|
||||
|
||||
- (void)updateTable {
|
||||
// Compute the method, class, or bundle lists on a background thread
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
|
||||
if (self.classes) {
|
||||
// Here, our class key is 'absolute'; .classes is a list of superclasses
|
||||
// and we want to show the methods for those classes specifically
|
||||
// TODO: add caching to this somehow
|
||||
NSMutableArray *methods = [FLEXRuntimeController
|
||||
methodsForToken:self.keyPath.methodKey
|
||||
instance:self.keyPath.instanceMethods
|
||||
inClasses:self.classes
|
||||
].mutableCopy;
|
||||
|
||||
// Remove classes without results if we're searching for a method
|
||||
//
|
||||
// Note: this will remove classes without any methods or overrides
|
||||
// even if the query doesn't specify a method, like `*.*.`
|
||||
if (self.keyPath.methodKey) {
|
||||
[self setNonEmptyMethodLists:methods withClasses:self.classes.mutableCopy];
|
||||
} else {
|
||||
self.filteredClasses = self.classes;
|
||||
}
|
||||
}
|
||||
else {
|
||||
FLEXRuntimeKeyPath *keyPath = self.keyPath;
|
||||
NSArray *models = [FLEXRuntimeController dataForKeyPath:keyPath];
|
||||
if (keyPath.methodKey) { // We're looking at methods
|
||||
self.bundlesOrClasses = nil;
|
||||
|
||||
NSMutableArray *methods = models.mutableCopy;
|
||||
NSMutableArray<NSString *> *classes = [
|
||||
FLEXRuntimeController classesForKeyPath:keyPath
|
||||
];
|
||||
self.classes = classes;
|
||||
[self setNonEmptyMethodLists:methods withClasses:classes];
|
||||
} else { // We're looking at bundles or classes
|
||||
self.bundlesOrClasses = models;
|
||||
self.classesToMethods = nil;
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, reload the table on the main thread
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self updateToolbarButtons];
|
||||
[self.delegate.tableView reloadData];
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
- (void)updateToolbarButtons {
|
||||
// Update toolbar buttons
|
||||
[self.toolbar setKeyPath:self.keyPath suggestions:self.suggestions];
|
||||
}
|
||||
|
||||
/// Assign assign .filteredClasses and .classesToMethods after removing empty sections
|
||||
- (void)setNonEmptyMethodLists:(NSMutableArray<NSArray<FLEXMethod *> *> *)methods
|
||||
withClasses:(NSMutableArray<NSString *> *)classes {
|
||||
// Remove sections with no methods
|
||||
NSIndexSet *allEmpty = [methods indexesOfObjectsPassingTest:^BOOL(NSArray *list, NSUInteger idx, BOOL *stop) {
|
||||
return list.count == 0;
|
||||
}];
|
||||
[methods removeObjectsAtIndexes:allEmpty];
|
||||
[classes removeObjectsAtIndexes:allEmpty];
|
||||
|
||||
self.filteredClasses = classes;
|
||||
self.classesToMethods = methods;
|
||||
}
|
||||
|
||||
- (BOOL)searchBar:(UISearchBar *)searchBar shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
|
||||
// Check if character is even legal
|
||||
if (![FLEXRuntimeKeyPathTokenizer allowedInKeyPath:text]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
BOOL terminatedToken = NO;
|
||||
BOOL isAppending = range.length == 0 && range.location == searchBar.text.length;
|
||||
if (isAppending && [text isEqualToString:@"."]) {
|
||||
terminatedToken = YES;
|
||||
}
|
||||
|
||||
// Actually parse input
|
||||
@try {
|
||||
text = [searchBar.text stringByReplacingCharactersInRange:range withString:text] ?: text;
|
||||
self.keyPath = [FLEXRuntimeKeyPathTokenizer tokenizeString:text];
|
||||
if (self.keyPath.classKey.isAbsolute && terminatedToken) {
|
||||
[self didSelectAbsoluteClass:self.keyPath.classKey.string];
|
||||
}
|
||||
} @catch (id e) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText {
|
||||
[_timer invalidate];
|
||||
|
||||
// Schedule update timer
|
||||
if (searchText.length) {
|
||||
if (!self.keyPath.methodKey) {
|
||||
self.classes = nil;
|
||||
self.filteredClasses = nil;
|
||||
}
|
||||
|
||||
self.timer = [NSTimer flex_fireSecondsFromNow:0.15 block:^{
|
||||
[self updateTable];
|
||||
}];
|
||||
}
|
||||
// ... or remove all rows
|
||||
else {
|
||||
_bundlesOrClasses = [FLEXRuntimeController allBundleNames];
|
||||
_classesToMethods = nil;
|
||||
_classes = nil;
|
||||
_keyPath = nil;
|
||||
[self updateToolbarButtons];
|
||||
[self.delegate.tableView reloadData];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar {
|
||||
self.keyPath = FLEXRuntimeKeyPath.empty;
|
||||
[self updateTable];
|
||||
}
|
||||
|
||||
/// Restore key path when going "back" and activating search bar again
|
||||
- (void)searchBarTextDidBeginEditing:(UISearchBar *)searchBar {
|
||||
searchBar.text = self.keyPath.description;
|
||||
}
|
||||
|
||||
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar {
|
||||
[_timer invalidate];
|
||||
[searchBar resignFirstResponder];
|
||||
[self updateTable];
|
||||
}
|
||||
|
||||
#pragma mark UITableViewDataSource
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
||||
return self.filteredClasses.count ?: self.bundlesOrClasses.count;
|
||||
}
|
||||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
UITableViewCell *cell = [tableView
|
||||
dequeueReusableCellWithIdentifier:kFLEXMultilineDetailCell
|
||||
forIndexPath:indexPath
|
||||
];
|
||||
|
||||
if (self.bundlesOrClasses.count) {
|
||||
cell.accessoryType = UITableViewCellAccessoryDetailButton;
|
||||
cell.textLabel.text = self.bundlesOrClasses[indexPath.row];
|
||||
cell.detailTextLabel.text = nil;
|
||||
if (self.keyPath.classKey) {
|
||||
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
|
||||
}
|
||||
}
|
||||
// One row per section
|
||||
else if (self.filteredClasses.count) {
|
||||
NSArray<FLEXMethod *> *methods = self.classesToMethods[indexPath.row];
|
||||
NSMutableString *summary = [NSMutableString new];
|
||||
[methods enumerateObjectsUsingBlock:^(FLEXMethod *method, NSUInteger idx, BOOL *stop) {
|
||||
NSString *format = nil;
|
||||
if (idx == methods.count-1) {
|
||||
format = @"%@%@";
|
||||
*stop = YES;
|
||||
} else if (idx < 3) {
|
||||
format = @"%@%@\n";
|
||||
} else {
|
||||
format = @"%@%@\n…";
|
||||
*stop = YES;
|
||||
}
|
||||
|
||||
[summary appendFormat:format, method.isInstanceMethod ? @"-" : @"+", method.selectorString];
|
||||
}];
|
||||
|
||||
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
|
||||
cell.textLabel.text = self.filteredClasses[indexPath.row];
|
||||
if (@available(iOS 10, *)) {
|
||||
cell.detailTextLabel.text = summary.length ? summary : nil;
|
||||
}
|
||||
|
||||
}
|
||||
else {
|
||||
@throw NSInternalInconsistencyException;
|
||||
}
|
||||
|
||||
return cell;
|
||||
}
|
||||
|
||||
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
|
||||
if (self.filteredClasses || self.keyPath.methodKey) {
|
||||
return @" ";
|
||||
} else if (self.bundlesOrClasses) {
|
||||
NSInteger count = self.bundlesOrClasses.count;
|
||||
if (self.keyPath.classKey) {
|
||||
return FLEXPluralString(count, @"classes", @"class");
|
||||
} else {
|
||||
return FLEXPluralString(count, @"bundles", @"bundle");
|
||||
}
|
||||
}
|
||||
|
||||
return [self.delegate tableView:tableView titleForHeaderInSection:section];
|
||||
}
|
||||
|
||||
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
|
||||
if (self.filteredClasses || self.keyPath.methodKey) {
|
||||
if (section == 0) {
|
||||
return 55;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 55;
|
||||
}
|
||||
|
||||
#pragma mark UITableViewDelegate
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
if (self.bundlesOrClasses) {
|
||||
NSString *bundleSuffixOrClass = self.bundlesOrClasses[indexPath.row];
|
||||
if (self.keyPath.classKey) {
|
||||
NSParameterAssert(NSClassFromString(bundleSuffixOrClass));
|
||||
[self.delegate didSelectClass:NSClassFromString(bundleSuffixOrClass)];
|
||||
} else {
|
||||
// Selected a bundle
|
||||
[self didSelectKeyPathOption:bundleSuffixOrClass];
|
||||
}
|
||||
} else {
|
||||
if (self.filteredClasses.count) {
|
||||
Class cls = NSClassFromString(self.filteredClasses[indexPath.row]);
|
||||
NSParameterAssert(cls);
|
||||
[self.delegate didSelectClass:cls];
|
||||
} else {
|
||||
@throw NSInternalInconsistencyException;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)tableView:(UITableView *)tableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath {
|
||||
NSString *bundleSuffixOrClass = self.bundlesOrClasses[indexPath.row];
|
||||
NSString *imagePath = [FLEXRuntimeController imagePathWithShortName:bundleSuffixOrClass];
|
||||
NSBundle *bundle = [NSBundle bundleWithPath:imagePath.stringByDeletingLastPathComponent];
|
||||
|
||||
if (bundle) {
|
||||
[self.delegate didSelectBundle:bundle];
|
||||
} else {
|
||||
[self.delegate didSelectImagePath:imagePath shortName:bundleSuffixOrClass];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
Reference in New Issue
Block a user