Просмотр исходного кода

Add basic support for bookmarking objects

Tanner Bennett лет назад: 6
Родитель
Сommit
02409d8051

+ 5 - 0
Classes/Core/Controllers/FLEXTableViewController.h

@@ -7,6 +7,7 @@
 //
 //
 
 
 #import <UIKit/UIKit.h>
 #import <UIKit/UIKit.h>
+#import "FLEXTableView.h"
 @class FLEXScopeCarousel, FLEXWindow;
 @class FLEXScopeCarousel, FLEXWindow;
 
 
 typedef CGFloat FLEXDebounceInterval;
 typedef CGFloat FLEXDebounceInterval;
@@ -112,4 +113,8 @@ extern CGFloat const kFLEXDebounceForExpensiveIO;
 /// Default implementation does nothign. Subclasses may override.
 /// Default implementation does nothign. Subclasses may override.
 - (void)shareButtonPressed;
 - (void)shareButtonPressed;
 
 
+/// Subclasses may call this to opt-out of all toolbar related behavior.
+/// This is necessary if you want to disable the gesture which reveals the toolbar.
+- (void)disableToolbar;
+
 @end
 @end

+ 11 - 1
Classes/Core/Controllers/FLEXTableViewController.m

@@ -8,6 +8,7 @@
 
 
 #import "FLEXTableViewController.h"
 #import "FLEXTableViewController.h"
 #import "FLEXExplorerViewController.h"
 #import "FLEXExplorerViewController.h"
+#import "FLEXBookmarksViewController.h"
 #import "FLEXTabsViewController.h"
 #import "FLEXTabsViewController.h"
 #import "FLEXScopeCarousel.h"
 #import "FLEXScopeCarousel.h"
 #import "FLEXTableView.h"
 #import "FLEXTableView.h"
@@ -191,6 +192,12 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
     }
     }
 }
 }
 
 
+- (void)disableToolbar {
+    self.navigationController.toolbarHidden = YES;
+    self.navigationController.hidesBarsOnSwipe = NO;
+    self.toolbarItems = nil;
+}
+
 
 
 #pragma mark - View Controller Lifecycle
 #pragma mark - View Controller Lifecycle
 
 
@@ -410,7 +417,10 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
 }
 }
 
 
 - (void)showBookmarks {
 - (void)showBookmarks {
-    // TODO
+    UINavigationController *nav = [[UINavigationController alloc]
+        initWithRootViewController:[FLEXBookmarksViewController new]
+    ];
+    [self presentViewController:nav animated:YES completion:nil];
 }
 }
 
 
 - (void)showTabSwitcher {
 - (void)showTabSwitcher {

+ 19 - 0
Classes/ExplorerInterface/Bookmarks/FLEXBookmarkManager.h

@@ -0,0 +1,19 @@
+//
+//  FLEXBookmarkManager.h
+//  FLEX
+//
+//  Created by Tanner on 2/6/20.
+//  Copyright © 2020 Flipboard. 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

+ 25 - 0
Classes/ExplorerInterface/Bookmarks/FLEXBookmarkManager.m

@@ -0,0 +1,25 @@
+//
+//  FLEXBookmarkManager.m
+//  FLEX
+//
+//  Created by Tanner on 2/6/20.
+//  Copyright © 2020 Flipboard. 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

+ 17 - 0
Classes/ExplorerInterface/Bookmarks/FLEXBookmarksViewController.h

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

+ 235 - 0
Classes/ExplorerInterface/Bookmarks/FLEXBookmarksViewController.m

@@ -0,0 +1,235 @@
+//
+//  FLEXBookmarksViewController.m
+//  FLEX
+//
+//  Created by Tanner on 2/6/20.
+//  Copyright © 2020 Flipboard. 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 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 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 {
+    [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];
+}
+
+- (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

+ 31 - 7
Classes/ExplorerInterface/Tabs/FLEXTabsViewController.m

@@ -9,12 +9,14 @@
 #import "FLEXTabsViewController.h"
 #import "FLEXTabsViewController.h"
 #import "FLEXNavigationController.h"
 #import "FLEXNavigationController.h"
 #import "FLEXTabList.h"
 #import "FLEXTabList.h"
+#import "FLEXBookmarkManager.h"
 #import "FLEXTableView.h"
 #import "FLEXTableView.h"
 #import "FLEXUtility.h"
 #import "FLEXUtility.h"
 #import "FLEXColor.h"
 #import "FLEXColor.h"
 #import "UIBarButtonItem+FLEX.h"
 #import "UIBarButtonItem+FLEX.h"
 #import "FLEXExplorerViewController.h"
 #import "FLEXExplorerViewController.h"
 #import "FLEXGlobalsViewController.h"
 #import "FLEXGlobalsViewController.h"
+#import "FLEXBookmarksViewController.h"
 
 
 @interface FLEXTabsViewController ()
 @interface FLEXTabsViewController ()
 @property (nonatomic, copy) NSArray<UINavigationController *> *openTabs;
 @property (nonatomic, copy) NSArray<UINavigationController *> *openTabs;
@@ -141,10 +143,10 @@
             [presenter presentViewController:activeTab animated:YES completion:nil];
             [presenter presentViewController:activeTab animated:YES completion:nil];
         }];
         }];
     } else if (self.activeIndex == NSNotFound) {
     } else if (self.activeIndex == NSNotFound) {
-        // The only tab was closed
+        // The only tab was closed, so dismiss everything
         [self.corePresenter dismissViewControllerAnimated:YES completion:nil];
         [self.corePresenter dismissViewControllerAnimated:YES completion:nil];
     } else {
     } else {
-        // Simple dismiss with the same active tab
+        // Simple dismiss with the same active tab, only dismiss myself
         [self dismissViewControllerAnimated:YES completion:nil];
         [self dismissViewControllerAnimated:YES completion:nil];
     }
     }
 }
 }
@@ -179,11 +181,33 @@
 }
 }
 
 
 - (void)addTabButtonPressed {
 - (void)addTabButtonPressed {
+    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];
+    } else {
+        // No bookmarks, just open the main menu
+        [self addTabAndDismiss:[FLEXNavigationController
+            withRootViewController:[FLEXGlobalsViewController new]
+        ]];
+    }
+}
+
+- (void)addTabAndDismiss:(UINavigationController *)newTab {
     FLEXExplorerViewController *presenter = self.corePresenter;
     FLEXExplorerViewController *presenter = self.corePresenter;
     [presenter dismissViewControllerAnimated:YES completion:^{
     [presenter dismissViewControllerAnimated:YES completion:^{
-        [presenter presentViewController:[FLEXNavigationController
-            withRootViewController:[FLEXGlobalsViewController new]
-        ] animated:YES completion:nil];
+        [presenter presentViewController:newTab animated:YES completion:nil];
     }];
     }];
 }
 }
 
 
@@ -192,14 +216,14 @@
         NSInteger count = self.openTabs.count;
         NSInteger count = self.openTabs.count;
         NSString *title = FLEXPluralFormatString(count, @"Close %@ tabs", @"Close %@ tab");
         NSString *title = FLEXPluralFormatString(count, @"Close %@ tabs", @"Close %@ tab");
         make.button(title).destructiveStyle().handler(^(NSArray<NSString *> *strings) {
         make.button(title).destructiveStyle().handler(^(NSArray<NSString *> *strings) {
-            [self closeAllTabs];
+            [self closeAll];
             [self toggleEditing];
             [self toggleEditing];
         });
         });
         make.button(@"Cancel").cancelStyle();
         make.button(@"Cancel").cancelStyle();
     } showFrom:self];
     } showFrom:self];
 }
 }
 
 
-- (void)closeAllTabs {
+- (void)closeAll {
     NSInteger rowCount = self.openTabs.count;
     NSInteger rowCount = self.openTabs.count;
     
     
     // Close tabs and update data source
     // Close tabs and update data source

+ 1 - 1
Classes/GlobalStateExplorers/Globals/FLEXGlobalsViewController.m

@@ -157,7 +157,7 @@
 - (void)viewWillAppear:(BOOL)animated {
 - (void)viewWillAppear:(BOOL)animated {
     [super viewWillAppear:animated];
     [super viewWillAppear:animated];
     
     
-    self.navigationController.toolbarHidden = YES;
+    [self disableToolbar];
 }
 }
 
 
 
 

+ 2 - 1
Classes/ObjectExplorers/FLEXObjectExplorerViewController.m

@@ -16,6 +16,7 @@
 #import "FLEXMethodCallingViewController.h"
 #import "FLEXMethodCallingViewController.h"
 #import "FLEXInstancesViewController.h"
 #import "FLEXInstancesViewController.h"
 #import "FLEXTabsViewController.h"
 #import "FLEXTabsViewController.h"
+#import "FLEXBookmarkManager.h"
 #import "FLEXTableView.h"
 #import "FLEXTableView.h"
 #import "FLEXTableViewCell.h"
 #import "FLEXTableViewCell.h"
 #import "FLEXScopeCarousel.h"
 #import "FLEXScopeCarousel.h"
@@ -232,7 +233,7 @@
 - (void)shareButtonPressed {
 - (void)shareButtonPressed {
     [FLEXAlert makeSheet:^(FLEXAlert *make) {
     [FLEXAlert makeSheet:^(FLEXAlert *make) {
         make.button(@"Add to Bookmarks").handler(^(NSArray<NSString *> *strings) {
         make.button(@"Add to Bookmarks").handler(^(NSArray<NSString *> *strings) {
-            // TODO
+            [FLEXBookmarkManager.bookmarks addObject:self.object];
         });
         });
         make.button(@"Copy Description").handler(^(NSArray<NSString *> *strings) {
         make.button(@"Copy Description").handler(^(NSArray<NSString *> *strings) {
             UIPasteboard.generalPasteboard.string = self.explorer.objectDescription;
             UIPasteboard.generalPasteboard.string = self.explorer.objectDescription;

+ 26 - 2
FLEX.xcodeproj/project.pbxproj

@@ -142,6 +142,10 @@
 		94AAF0391BAF2E1F00DE8760 /* FLEXKeyboardHelpViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 94AAF0371BAF2E1F00DE8760 /* FLEXKeyboardHelpViewController.m */; };
 		94AAF0391BAF2E1F00DE8760 /* FLEXKeyboardHelpViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 94AAF0371BAF2E1F00DE8760 /* FLEXKeyboardHelpViewController.m */; };
 		94AAF03A1BAF2F0300DE8760 /* FLEXKeyboardShortcutManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 942DCD821BAE0AD300DB5DC2 /* FLEXKeyboardShortcutManager.h */; settings = {ATTRIBUTES = (Private, ); }; };
 		94AAF03A1BAF2F0300DE8760 /* FLEXKeyboardShortcutManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 942DCD821BAE0AD300DB5DC2 /* FLEXKeyboardShortcutManager.h */; settings = {ATTRIBUTES = (Private, ); }; };
 		C309B82F223ED64400B228EC /* FLEXLogController.h in Headers */ = {isa = PBXBuildFile; fileRef = C309B82D223ED64400B228EC /* FLEXLogController.h */; };
 		C309B82F223ED64400B228EC /* FLEXLogController.h in Headers */ = {isa = PBXBuildFile; fileRef = C309B82D223ED64400B228EC /* FLEXLogController.h */; };
+		C312A13023ECB5D300E38049 /* FLEXBookmarkManager.h in Headers */ = {isa = PBXBuildFile; fileRef = C312A12E23ECB5D300E38049 /* FLEXBookmarkManager.h */; };
+		C312A13123ECB5D300E38049 /* FLEXBookmarkManager.m in Sources */ = {isa = PBXBuildFile; fileRef = C312A12F23ECB5D300E38049 /* FLEXBookmarkManager.m */; };
+		C312A13423ECBE5800E38049 /* FLEXBookmarksViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = C312A13223ECBE5800E38049 /* FLEXBookmarksViewController.h */; };
+		C312A13523ECBE5800E38049 /* FLEXBookmarksViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = C312A13323ECBE5800E38049 /* FLEXBookmarksViewController.m */; };
 		C31C4A6923342A2200C35F12 /* FLEXMetadataSection.h in Headers */ = {isa = PBXBuildFile; fileRef = C31C4A6723342A2200C35F12 /* FLEXMetadataSection.h */; };
 		C31C4A6923342A2200C35F12 /* FLEXMetadataSection.h in Headers */ = {isa = PBXBuildFile; fileRef = C31C4A6723342A2200C35F12 /* FLEXMetadataSection.h */; };
 		C31C4A6A23342A2200C35F12 /* FLEXMetadataSection.m in Sources */ = {isa = PBXBuildFile; fileRef = C31C4A6823342A2200C35F12 /* FLEXMetadataSection.m */; };
 		C31C4A6A23342A2200C35F12 /* FLEXMetadataSection.m in Sources */ = {isa = PBXBuildFile; fileRef = C31C4A6823342A2200C35F12 /* FLEXMetadataSection.m */; };
 		C31D93E423E38CBE005517BF /* FLEXBlockShortcuts.h in Headers */ = {isa = PBXBuildFile; fileRef = C31D93E223E38CBE005517BF /* FLEXBlockShortcuts.h */; };
 		C31D93E423E38CBE005517BF /* FLEXBlockShortcuts.h in Headers */ = {isa = PBXBuildFile; fileRef = C31D93E223E38CBE005517BF /* FLEXBlockShortcuts.h */; };
@@ -469,6 +473,10 @@
 		94AAF0361BAF2E1F00DE8760 /* FLEXKeyboardHelpViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXKeyboardHelpViewController.h; sourceTree = "<group>"; };
 		94AAF0361BAF2E1F00DE8760 /* FLEXKeyboardHelpViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXKeyboardHelpViewController.h; sourceTree = "<group>"; };
 		94AAF0371BAF2E1F00DE8760 /* FLEXKeyboardHelpViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXKeyboardHelpViewController.m; sourceTree = "<group>"; };
 		94AAF0371BAF2E1F00DE8760 /* FLEXKeyboardHelpViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXKeyboardHelpViewController.m; sourceTree = "<group>"; };
 		C309B82D223ED64400B228EC /* FLEXLogController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FLEXLogController.h; sourceTree = "<group>"; };
 		C309B82D223ED64400B228EC /* FLEXLogController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FLEXLogController.h; sourceTree = "<group>"; };
+		C312A12E23ECB5D300E38049 /* FLEXBookmarkManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FLEXBookmarkManager.h; sourceTree = "<group>"; };
+		C312A12F23ECB5D300E38049 /* FLEXBookmarkManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLEXBookmarkManager.m; sourceTree = "<group>"; };
+		C312A13223ECBE5800E38049 /* FLEXBookmarksViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FLEXBookmarksViewController.h; sourceTree = "<group>"; };
+		C312A13323ECBE5800E38049 /* FLEXBookmarksViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLEXBookmarksViewController.m; sourceTree = "<group>"; };
 		C31C4A6723342A2200C35F12 /* FLEXMetadataSection.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FLEXMetadataSection.h; sourceTree = "<group>"; };
 		C31C4A6723342A2200C35F12 /* FLEXMetadataSection.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FLEXMetadataSection.h; sourceTree = "<group>"; };
 		C31C4A6823342A2200C35F12 /* FLEXMetadataSection.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLEXMetadataSection.m; sourceTree = "<group>"; };
 		C31C4A6823342A2200C35F12 /* FLEXMetadataSection.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLEXMetadataSection.m; sourceTree = "<group>"; };
 		C31D93E223E38CBE005517BF /* FLEXBlockShortcuts.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FLEXBlockShortcuts.h; sourceTree = "<group>"; };
 		C31D93E223E38CBE005517BF /* FLEXBlockShortcuts.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FLEXBlockShortcuts.h; sourceTree = "<group>"; };
@@ -975,15 +983,27 @@
 		94A515121C4C9E7B0063292F /* ExplorerInterface */ = {
 		94A515121C4C9E7B0063292F /* ExplorerInterface */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
+				C312A12D23ECB55B00E38049 /* Bookmarks */,
 				C3531BA723E88FAC00A184AD /* Tabs */,
 				C3531BA723E88FAC00A184AD /* Tabs */,
-				94A515191C4CA1F10063292F /* FLEXExplorerViewController.h */,
-				94A5151A1C4CA1F10063292F /* FLEXExplorerViewController.m */,
 				94A5151B1C4CA1F10063292F /* FLEXWindow.h */,
 				94A5151B1C4CA1F10063292F /* FLEXWindow.h */,
 				94A5151C1C4CA1F10063292F /* FLEXWindow.m */,
 				94A5151C1C4CA1F10063292F /* FLEXWindow.m */,
+				94A515191C4CA1F10063292F /* FLEXExplorerViewController.h */,
+				94A5151A1C4CA1F10063292F /* FLEXExplorerViewController.m */,
 			);
 			);
 			path = ExplorerInterface;
 			path = ExplorerInterface;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
 		};
 		};
+		C312A12D23ECB55B00E38049 /* Bookmarks */ = {
+			isa = PBXGroup;
+			children = (
+				C312A12E23ECB5D300E38049 /* FLEXBookmarkManager.h */,
+				C312A12F23ECB5D300E38049 /* FLEXBookmarkManager.m */,
+				C312A13223ECBE5800E38049 /* FLEXBookmarksViewController.h */,
+				C312A13323ECBE5800E38049 /* FLEXBookmarksViewController.m */,
+			);
+			path = Bookmarks;
+			sourceTree = "<group>";
+		};
 		C33CF16B22D664E600F9C6C0 /* FileBrowser */ = {
 		C33CF16B22D664E600F9C6C0 /* FileBrowser */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
@@ -1292,6 +1312,7 @@
 				C383C3C523B6BB81007A321B /* FLEXCodeFontCell.h in Headers */,
 				C383C3C523B6BB81007A321B /* FLEXCodeFontCell.h in Headers */,
 				C3F527C12318670F009CBA07 /* FLEXImageShortcuts.h in Headers */,
 				C3F527C12318670F009CBA07 /* FLEXImageShortcuts.h in Headers */,
 				3A4C95051B5B21410088C3F2 /* FLEXArgumentInputViewFactory.h in Headers */,
 				3A4C95051B5B21410088C3F2 /* FLEXArgumentInputViewFactory.h in Headers */,
+				C312A13423ECBE5800E38049 /* FLEXBookmarksViewController.h in Headers */,
 				222C88221C7339DC007CA15F /* FLEXRealmDefines.h in Headers */,
 				222C88221C7339DC007CA15F /* FLEXRealmDefines.h in Headers */,
 				779B1ED21C0C4D7C001F5E49 /* FLEXTableColumnHeader.h in Headers */,
 				779B1ED21C0C4D7C001F5E49 /* FLEXTableColumnHeader.h in Headers */,
 				3A4C94FD1B5B21410088C3F2 /* FLEXArgumentInputStructView.h in Headers */,
 				3A4C94FD1B5B21410088C3F2 /* FLEXArgumentInputStructView.h in Headers */,
@@ -1331,6 +1352,7 @@
 				C3531BA123E796BD00A184AD /* FLEXManager+Extensibility.h in Headers */,
 				C3531BA123E796BD00A184AD /* FLEXManager+Extensibility.h in Headers */,
 				C36FBFCF230F3B98008D95D5 /* FLEXProtocol.h in Headers */,
 				C36FBFCF230F3B98008D95D5 /* FLEXProtocol.h in Headers */,
 				94A515271C4CA2080063292F /* FLEXToolbarItem.h in Headers */,
 				94A515271C4CA2080063292F /* FLEXToolbarItem.h in Headers */,
+				C312A13023ECB5D300E38049 /* FLEXBookmarkManager.h in Headers */,
 				3A4C95341B5B21410088C3F2 /* FLEXSystemLogTableViewController.h in Headers */,
 				3A4C95341B5B21410088C3F2 /* FLEXSystemLogTableViewController.h in Headers */,
 				C34C9BDD23A7F2740031CA3E /* FLEXRuntime+UIKitHelpers.h in Headers */,
 				C34C9BDD23A7F2740031CA3E /* FLEXRuntime+UIKitHelpers.h in Headers */,
 				C398624F23AD6C67007E6793 /* TBKeyPathTokenizer.h in Headers */,
 				C398624F23AD6C67007E6793 /* TBKeyPathTokenizer.h in Headers */,
@@ -1612,6 +1634,7 @@
 				3A4C952F1B5B21410088C3F2 /* FLEXWebViewController.m in Sources */,
 				3A4C952F1B5B21410088C3F2 /* FLEXWebViewController.m in Sources */,
 				3A4C95041B5B21410088C3F2 /* FLEXArgumentInputView.m in Sources */,
 				3A4C95041B5B21410088C3F2 /* FLEXArgumentInputView.m in Sources */,
 				C3F977842311B38F0032776D /* NSDictionary+ObjcRuntime.m in Sources */,
 				C3F977842311B38F0032776D /* NSDictionary+ObjcRuntime.m in Sources */,
+				C312A13523ECBE5800E38049 /* FLEXBookmarksViewController.m in Sources */,
 				3A4C95411B5B21410088C3F2 /* FLEXNetworkTransactionTableViewCell.m in Sources */,
 				3A4C95411B5B21410088C3F2 /* FLEXNetworkTransactionTableViewCell.m in Sources */,
 				C3F527BE2318603F009CBA07 /* FLEXShortcutsSection.m in Sources */,
 				C3F527BE2318603F009CBA07 /* FLEXShortcutsSection.m in Sources */,
 				3A4C94D61B5B21410088C3F2 /* FLEXObjectExplorerViewController.m in Sources */,
 				3A4C94D61B5B21410088C3F2 /* FLEXObjectExplorerViewController.m in Sources */,
@@ -1644,6 +1667,7 @@
 				779B1ED31C0C4D7C001F5E49 /* FLEXTableColumnHeader.m in Sources */,
 				779B1ED31C0C4D7C001F5E49 /* FLEXTableColumnHeader.m in Sources */,
 				C37A0C94218BAC9600848CA7 /* FLEXObjcInternal.mm in Sources */,
 				C37A0C94218BAC9600848CA7 /* FLEXObjcInternal.mm in Sources */,
 				C31D93E523E38CBE005517BF /* FLEXBlockShortcuts.m in Sources */,
 				C31D93E523E38CBE005517BF /* FLEXBlockShortcuts.m in Sources */,
+				C312A13123ECB5D300E38049 /* FLEXBookmarkManager.m in Sources */,
 				C34D4EB523A2AF2A00C1F903 /* FLEXColorPreviewSection.m in Sources */,
 				C34D4EB523A2AF2A00C1F903 /* FLEXColorPreviewSection.m in Sources */,
 				C36B097123E1EDCD008F5D47 /* FLEXTableViewSection.m in Sources */,
 				C36B097123E1EDCD008F5D47 /* FLEXTableViewSection.m in Sources */,
 				C3531BA623E88A2100A184AD /* FLEXNavigationController.m in Sources */,
 				C3531BA623E88A2100A184AD /* FLEXNavigationController.m in Sources */,