| // Copyright 2017 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "ios/chrome/browser/ui/bookmarks/bookmark_home_view_controller.h" |
| |
| #include "base/mac/bind_objc_block.h" |
| #include "base/mac/foundation_util.h" |
| #include "base/metrics/user_metrics.h" |
| #include "base/strings/sys_string_conversions.h" |
| #include "components/bookmarks/browser/bookmark_model.h" |
| #include "components/favicon/core/fallback_url_util.h" |
| #include "components/favicon/core/favicon_server_fetcher_params.h" |
| #include "components/favicon/core/large_icon_service.h" |
| #include "components/favicon_base/fallback_icon_style.h" |
| #include "components/strings/grit/components_strings.h" |
| #include "ios/chrome/browser/bookmarks/bookmark_model_factory.h" |
| #include "ios/chrome/browser/bookmarks/bookmarks_utils.h" |
| #include "ios/chrome/browser/browser_state/chrome_browser_state.h" |
| #include "ios/chrome/browser/favicon/ios_chrome_large_icon_service_factory.h" |
| #import "ios/chrome/browser/metrics/new_tab_page_uma.h" |
| #import "ios/chrome/browser/ui/authentication/signin_promo_view_configurator.h" |
| #import "ios/chrome/browser/ui/bookmarks/bars/bookmark_context_bar.h" |
| #import "ios/chrome/browser/ui/bookmarks/bookmark_edit_view_controller.h" |
| #import "ios/chrome/browser/ui/bookmarks/bookmark_folder_editor_view_controller.h" |
| #import "ios/chrome/browser/ui/bookmarks/bookmark_folder_view_controller.h" |
| #import "ios/chrome/browser/ui/bookmarks/bookmark_home_consumer.h" |
| #import "ios/chrome/browser/ui/bookmarks/bookmark_home_mediator.h" |
| #import "ios/chrome/browser/ui/bookmarks/bookmark_home_shared_state.h" |
| #import "ios/chrome/browser/ui/bookmarks/bookmark_home_waiting_view.h" |
| #include "ios/chrome/browser/ui/bookmarks/bookmark_model_bridge_observer.h" |
| #import "ios/chrome/browser/ui/bookmarks/bookmark_navigation_controller.h" |
| #import "ios/chrome/browser/ui/bookmarks/bookmark_path_cache.h" |
| #import "ios/chrome/browser/ui/bookmarks/bookmark_table_view.h" |
| #import "ios/chrome/browser/ui/bookmarks/bookmark_utils_ios.h" |
| #import "ios/chrome/browser/ui/bookmarks/cells/bookmark_home_node_item.h" |
| #import "ios/chrome/browser/ui/bookmarks/cells/bookmark_table_cell.h" |
| #import "ios/chrome/browser/ui/bookmarks/cells/bookmark_table_signin_promo_cell.h" |
| #import "ios/chrome/browser/ui/commands/application_commands.h" |
| #import "ios/chrome/browser/ui/icons/chrome_icon.h" |
| #import "ios/chrome/browser/ui/keyboard/UIKeyCommand+Chrome.h" |
| #import "ios/chrome/browser/ui/material_components/utils.h" |
| #import "ios/chrome/browser/ui/rtl_geometry.h" |
| #import "ios/chrome/browser/ui/table_view/chrome_table_view_styler.h" |
| #import "ios/chrome/browser/ui/table_view/table_view_model.h" |
| #import "ios/chrome/browser/ui/ui_util.h" |
| #import "ios/chrome/browser/ui/uikit_ui_util.h" |
| #import "ios/chrome/browser/ui/url_loader.h" |
| #import "ios/chrome/browser/ui/util/constraints_ui_util.h" |
| #include "ios/chrome/grit/ios_strings.h" |
| #import "ios/third_party/material_components_ios/src/components/AppBar/src/MaterialAppBar.h" |
| #import "ios/third_party/material_components_ios/src/components/Typography/src/MaterialTypography.h" |
| #include "ios/web/public/referrer.h" |
| #include "skia/ext/skia_utils_ios.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/l10n/l10n_util_mac.h" |
| |
| using bookmarks::BookmarkNode; |
| |
| // Used to store a pair of NSIntegers when storing a NSIndexPath in C++ |
| // collections. |
| using IntegerPair = std::pair<NSInteger, NSInteger>; |
| |
| namespace { |
| typedef NS_ENUM(NSInteger, BookmarksContextBarState) { |
| BookmarksContextBarNone, // No state. |
| BookmarksContextBarDefault, // No selection is possible in this state. |
| BookmarksContextBarBeginSelection, // This is the clean start state, |
| // selection is possible, but nothing is |
| // selected yet. |
| BookmarksContextBarSingleURLSelection, // Single URL selected state. |
| BookmarksContextBarMultipleURLSelection, // Multiple URLs selected state. |
| BookmarksContextBarSingleFolderSelection, // Single folder selected. |
| BookmarksContextBarMultipleFolderSelection, // Multiple folders selected. |
| BookmarksContextBarMixedSelection, // Multiple URL / Folders selected. |
| }; |
| |
| // NetworkTrafficAnnotationTag for fetching favicon from a Google server. |
| const net::NetworkTrafficAnnotationTag kTrafficAnnotation = |
| net::DefineNetworkTrafficAnnotation("bookmarks_get_large_icon", R"( |
| semantics { |
| sender: "Bookmarks" |
| description: |
| "Sends a request to a Google server to retrieve the favicon bitmap " |
| "for a bookmark." |
| trigger: |
| "A request can be sent if Chrome does not have a favicon for a " |
| "bookmark." |
| data: "Page URL and desired icon size." |
| destination: GOOGLE_OWNED_SERVICE |
| } |
| policy { |
| cookies_allowed: NO |
| setting: "This feature cannot be disabled by settings." |
| policy_exception_justification: "Not implemented." |
| } |
| )"); |
| |
| // Returns a vector of all URLs in |nodes|. |
| std::vector<GURL> GetUrlsToOpen(const std::vector<const BookmarkNode*>& nodes) { |
| std::vector<GURL> urls; |
| for (const BookmarkNode* node : nodes) { |
| if (node->is_url()) { |
| urls.push_back(node->url()); |
| } |
| } |
| return urls; |
| } |
| } // namespace |
| |
| @interface BookmarkHomeViewController ()< |
| BookmarkEditViewControllerDelegate, |
| BookmarkFolderEditorViewControllerDelegate, |
| BookmarkFolderViewControllerDelegate, |
| BookmarkHomeConsumer, |
| BookmarkHomeSharedStateObserver, |
| BookmarkModelBridgeObserver, |
| BookmarkTableCellTitleEditDelegate, |
| BookmarkTableViewDelegate, |
| ContextBarDelegate, |
| UIGestureRecognizerDelegate, |
| UITableViewDataSource, |
| UITableViewDelegate> { |
| // Bridge to register for bookmark changes. |
| std::unique_ptr<bookmarks::BookmarkModelBridge> _bridge; |
| |
| // The root node, whose child nodes are shown in the bookmark table view. |
| const bookmarks::BookmarkNode* _rootNode; |
| |
| // YES if NSLayoutConstraits were added. |
| BOOL _addedConstraints; |
| |
| // Map of favicon load tasks for each index path. Used to keep track of |
| // pending favicon load operations so that they can be cancelled upon cell |
| // reuse. Keys are (section, item) pairs of cell index paths. |
| std::map<IntegerPair, base::CancelableTaskTracker::TaskId> _faviconLoadTasks; |
| // Task tracker used for async favicon loads. |
| base::CancelableTaskTracker _faviconTaskTracker; |
| } |
| |
| // Shared state between BookmarkHome classes. Used as a temporary refactoring |
| // aid. |
| @property(nonatomic, strong) BookmarkHomeSharedState* sharedState; |
| |
| // The bookmark model used. |
| @property(nonatomic, assign) bookmarks::BookmarkModel* bookmarks; |
| |
| // The user's browser state model used. |
| @property(nonatomic, assign) ios::ChromeBrowserState* browserState; |
| |
| // The mediator that provides data for this view controller. |
| @property(nonatomic, strong) BookmarkHomeMediator* mediator; |
| |
| // The main view showing all the bookmarks. |
| @property(nonatomic, strong) BookmarkTableView* bookmarksTableView; |
| |
| // The table view's styler. |
| @property(nonatomic, strong) ChromeTableViewStyler* tableViewStyler; |
| |
| // The view controller used to pick a folder in which to move the selected |
| // bookmarks. |
| @property(nonatomic, strong) BookmarkFolderViewController* folderSelector; |
| |
| // Object to load URLs. |
| @property(nonatomic, weak) id<UrlLoader> loader; |
| |
| // The app bar for the bookmarks. |
| @property(nonatomic, strong) MDCAppBar* appBar; |
| |
| // The context bar at the bottom of the bookmarks. |
| @property(nonatomic, strong) BookmarkContextBar* contextBar; |
| |
| // This view is created and used if the model is not fully loaded yet by the |
| // time this controller starts. |
| @property(nonatomic, strong) BookmarkHomeWaitingView* waitForModelView; |
| |
| // The view controller used to view and edit a single bookmark. |
| @property(nonatomic, strong) BookmarkEditViewController* editViewController; |
| |
| // The view controller to present when editing the current folder. |
| @property(nonatomic, strong) BookmarkFolderEditorViewController* folderEditor; |
| |
| // The current state of the context bar UI. |
| @property(nonatomic, assign) BookmarksContextBarState contextBarState; |
| |
| // When the view is first shown on the screen, this property represents the |
| // cached value of the y of the content offset of the table view. This |
| // property is set to nil after it is used. |
| @property(nonatomic, strong) NSNumber* cachedContentPosition; |
| |
| // Dispatcher for sending commands. |
| @property(nonatomic, readonly, weak) id<ApplicationCommands> dispatcher; |
| @end |
| |
| @implementation BookmarkHomeViewController |
| |
| @synthesize appBar = _appBar; |
| @synthesize bookmarks = _bookmarks; |
| @synthesize browserState = _browserState; |
| @synthesize editViewController = _editViewController; |
| @synthesize folderEditor = _folderEditor; |
| @synthesize folderSelector = _folderSelector; |
| @synthesize loader = _loader; |
| @synthesize waitForModelView = _waitForModelView; |
| @synthesize homeDelegate = _homeDelegate; |
| @synthesize bookmarksTableView = _bookmarksTableView; |
| @synthesize contextBar = _contextBar; |
| @synthesize contextBarState = _contextBarState; |
| @synthesize dispatcher = _dispatcher; |
| @synthesize cachedContentPosition = _cachedContentPosition; |
| @synthesize isReconstructingFromCache = _isReconstructingFromCache; |
| @synthesize sharedState = _sharedState; |
| @synthesize mediator = _mediator; |
| @synthesize tableViewStyler = _tableViewStyler; |
| |
| #if !defined(__has_feature) || !__has_feature(objc_arc) |
| #error "This file requires ARC support." |
| #endif |
| |
| #pragma mark - Initializer |
| |
| - (instancetype)initWithLoader:(id<UrlLoader>)loader |
| browserState:(ios::ChromeBrowserState*)browserState |
| dispatcher:(id<ApplicationCommands>)dispatcher { |
| DCHECK(browserState); |
| self = [super initWithNibName:nil bundle:nil]; |
| if (self) { |
| _browserState = browserState->GetOriginalChromeBrowserState(); |
| _loader = loader; |
| _dispatcher = dispatcher; |
| |
| _bookmarks = ios::BookmarkModelFactory::GetForBrowserState(browserState); |
| |
| _bridge.reset(new bookmarks::BookmarkModelBridge(self, _bookmarks)); |
| } |
| return self; |
| } |
| |
| - (void)dealloc { |
| [self.mediator disconnect]; |
| [self removeKeyboardObservers]; |
| _faviconTaskTracker.TryCancelAll(); |
| _sharedState.tableView.dataSource = nil; |
| _sharedState.tableView.delegate = nil; |
| } |
| |
| - (void)setRootNode:(const bookmarks::BookmarkNode*)rootNode { |
| _rootNode = rootNode; |
| } |
| |
| #pragma mark - UIViewController |
| |
| - (void)viewDidLoad { |
| [super viewDidLoad]; |
| |
| if (self.bookmarks->loaded()) { |
| [self loadBookmarkViews]; |
| } else { |
| [self loadWaitingView]; |
| } |
| } |
| |
| - (void)viewWillAppear:(BOOL)animated { |
| [super viewWillAppear:animated]; |
| // Set the delegate here to make sure it is working when navigating in the |
| // ViewController hierarchy (as each view controller is setting itself as |
| // delegate). |
| self.navigationController.interactivePopGestureRecognizer.delegate = self; |
| } |
| |
| - (void)viewDidLayoutSubviews { |
| [super viewDidLayoutSubviews]; |
| // Set the content position after views are laid out, to ensure the right |
| // window of rows is shown. Once used, reset self.cachedContentPosition. |
| if (self.cachedContentPosition) { |
| [self setContentPosition:self.cachedContentPosition.floatValue]; |
| self.cachedContentPosition = nil; |
| } |
| // The height of contextBar might change due to word wrapping of buttons |
| // after titleLabel or orientation changed. |
| [self.contextBar updateHeight]; |
| } |
| |
| - (BOOL)prefersStatusBarHidden { |
| return NO; |
| } |
| |
| - (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection { |
| [super traitCollectionDidChange:previousTraitCollection]; |
| // Stop edit of current bookmark folder name, if any. |
| [self.sharedState.editingFolderCell stopEdit]; |
| } |
| |
| - (NSArray*)keyCommands { |
| __weak BookmarkHomeViewController* weakSelf = self; |
| return @[ [UIKeyCommand cr_keyCommandWithInput:UIKeyInputEscape |
| modifierFlags:Cr_UIKeyModifierNone |
| title:nil |
| action:^{ |
| [weakSelf navigationBarCancel:nil]; |
| }] ]; |
| } |
| |
| - (UIStatusBarStyle)preferredStatusBarStyle { |
| return UIStatusBarStyleDefault; |
| } |
| |
| #pragma mark - Protected |
| |
| - (void)loadBookmarkViews { |
| DCHECK(_rootNode); |
| self.sharedState = |
| [[BookmarkHomeSharedState alloc] initWithBookmarkModel:_bookmarks |
| displayedRootNode:_rootNode]; |
| self.sharedState.observer = self; |
| |
| self.automaticallyAdjustsScrollViewInsets = NO; |
| self.bookmarksTableView = |
| [[BookmarkTableView alloc] initWithSharedState:self.sharedState |
| browserState:self.browserState |
| delegate:self |
| frame:self.view.bounds]; |
| [self.bookmarksTableView |
| setAutoresizingMask:UIViewAutoresizingFlexibleWidth | |
| UIViewAutoresizingFlexibleHeight]; |
| [self.bookmarksTableView setTranslatesAutoresizingMaskIntoConstraints:NO]; |
| [self.view addSubview:self.bookmarksTableView]; |
| |
| // Configure the table view. |
| self.tableViewStyler = [[ChromeTableViewStyler alloc] init]; |
| self.sharedState.tableView.accessibilityIdentifier = @"bookmarksTableView"; |
| if (@available(iOS 11.0, *)) { |
| self.sharedState.tableView.contentInsetAdjustmentBehavior = |
| UIScrollViewContentInsetAdjustmentNever; |
| } |
| self.sharedState.tableView.estimatedRowHeight = |
| [BookmarkHomeSharedState cellHeightPt]; |
| self.sharedState.tableView.separatorStyle = UITableViewCellSeparatorStyleNone; |
| self.sharedState.tableView.allowsMultipleSelectionDuringEditing = YES; |
| |
| UILongPressGestureRecognizer* longPressRecognizer = |
| [[UILongPressGestureRecognizer alloc] |
| initWithTarget:self |
| action:@selector(handleLongPress:)]; |
| longPressRecognizer.numberOfTouchesRequired = 1; |
| longPressRecognizer.delegate = self; |
| [self.sharedState.tableView addGestureRecognizer:longPressRecognizer]; |
| |
| // Create the mediator and hook up the table view. |
| self.mediator = |
| [[BookmarkHomeMediator alloc] initWithSharedState:self.sharedState |
| browserState:self.browserState]; |
| self.mediator.consumer = self; |
| [self.mediator startMediating]; |
| self.sharedState.tableView.dataSource = self; |
| self.sharedState.tableView.delegate = self; |
| |
| [self registerForKeyboardNotifications]; |
| |
| // After the table view has been added. |
| [self setupNavigationBar]; |
| |
| if (_rootNode != self.bookmarks->root_node()) { |
| [self setupContextBar]; |
| } |
| if (self.isReconstructingFromCache) { |
| [self setupUIStackCacheIfApplicable]; |
| } |
| DCHECK(self.bookmarks->loaded()); |
| DCHECK([self isViewLoaded]); |
| } |
| |
| - (void)loadWaitingView { |
| DCHECK(!self.waitForModelView); |
| DCHECK(self.view); |
| |
| // Present a waiting view. |
| BookmarkHomeWaitingView* waitingView = |
| [[BookmarkHomeWaitingView alloc] initWithFrame:self.view.bounds]; |
| self.waitForModelView = waitingView; |
| [self.view addSubview:self.waitForModelView]; |
| [self.waitForModelView startWaiting]; |
| } |
| |
| - (void)cachePosition { |
| // Cache position for BookmarkTableView. |
| [BookmarkPathCache |
| cacheBookmarkUIPositionWithPrefService:self.browserState->GetPrefs() |
| folderId:_rootNode->id() |
| scrollPosition:static_cast<double>( |
| self.contentPosition)]; |
| } |
| |
| #pragma mark - BookmarkHomeConsumer |
| |
| - (void)reconfigureCellsForItems:(NSArray*)items { |
| for (TableViewItem* item in items) { |
| NSIndexPath* indexPath = |
| [self.sharedState.tableViewModel indexPathForItem:item]; |
| UITableViewCell* cell = |
| [self.sharedState.tableView cellForRowAtIndexPath:indexPath]; |
| |
| // |cell| may be nil if the row is not currently on screen. |
| if (cell) { |
| [item configureCell:cell withStyler:self.tableViewStyler]; |
| } |
| } |
| } |
| |
| - (void)refreshContents { |
| [self.mediator computeBookmarkTableViewData]; |
| [self cancelAllFaviconLoads]; |
| [self bookmarkTableViewRefreshContextBar:self.bookmarksTableView]; |
| [self.sharedState.editingFolderCell stopEdit]; |
| [self.sharedState.tableView reloadData]; |
| if (self.sharedState.currentlyInEditMode && |
| !self.sharedState.editNodes.empty()) { |
| [self restoreRowSelection]; |
| } |
| } |
| |
| // Asynchronously loads favicon for given index path. The loads are cancelled |
| // upon cell reuse automatically. When the favicon is not found in cache, try |
| // loading it from a Google server if |continueToGoogleServer| is YES, |
| // otherwise, use the fall back icon style. |
| - (void)loadFaviconAtIndexPath:(NSIndexPath*)indexPath |
| continueToGoogleServer:(BOOL)continueToGoogleServer { |
| const bookmarks::BookmarkNode* node = [self nodeAtIndexPath:indexPath]; |
| if (node->is_folder()) { |
| return; |
| } |
| |
| CGFloat scale = [UIScreen mainScreen].scale; |
| CGFloat desiredFaviconSizeInPixel = |
| scale * [BookmarkHomeSharedState desiredFaviconSizePt]; |
| CGFloat minFaviconSizeInPixel = |
| scale * [BookmarkHomeSharedState minFaviconSizePt]; |
| |
| // Start loading a favicon. |
| __weak BookmarkHomeViewController* weakSelf = self; |
| GURL blockURL(node->url()); |
| NSString* fallbackText = |
| base::SysUTF16ToNSString(favicon::GetFallbackIconText(blockURL)); |
| void (^faviconLoadedFromCacheBlock)(const favicon_base::LargeIconResult&) = ^( |
| const favicon_base::LargeIconResult& result) { |
| BookmarkHomeViewController* strongSelf = weakSelf; |
| if (!strongSelf) { |
| return; |
| } |
| // TODO(crbug.com/697329) When fetching icon from server to replace existing |
| // cache is allowed, fetch icon from server here when cached icon is smaller |
| // than the desired size. |
| if (!result.bitmap.is_valid() && continueToGoogleServer && |
| strongSelf.sharedState.faviconDownloadCount < |
| [BookmarkHomeSharedState maxDownloadFaviconCount]) { |
| void (^faviconLoadedFromServerBlock)( |
| favicon_base::GoogleFaviconServerRequestStatus status) = |
| ^(const favicon_base::GoogleFaviconServerRequestStatus status) { |
| if (status == |
| favicon_base::GoogleFaviconServerRequestStatus::SUCCESS) { |
| BookmarkHomeViewController* strongSelf = weakSelf; |
| // GetLargeIconOrFallbackStyleFromGoogleServerSkippingLocalCache |
| // is not cancellable. So need to check if node has been changed |
| // before proceeding to favicon update. |
| if (!strongSelf || |
| [strongSelf nodeAtIndexPath:indexPath] != node) { |
| return; |
| } |
| // Favicon should be ready in cache now. Fetch it again. |
| [strongSelf loadFaviconAtIndexPath:indexPath |
| continueToGoogleServer:NO]; |
| } |
| }; // faviconLoadedFromServerBlock |
| |
| strongSelf.sharedState.faviconDownloadCount++; |
| IOSChromeLargeIconServiceFactory::GetForBrowserState(self.browserState) |
| ->GetLargeIconOrFallbackStyleFromGoogleServerSkippingLocalCache( |
| favicon::FaviconServerFetcherParams::CreateForMobile( |
| node->url(), minFaviconSizeInPixel, |
| desiredFaviconSizeInPixel), |
| /*may_page_url_be_private=*/true, kTrafficAnnotation, |
| base::BindBlockArc(faviconLoadedFromServerBlock)); |
| } |
| [strongSelf updateCellAtIndexPath:indexPath |
| withLargeIconResult:result |
| fallbackText:fallbackText]; |
| }; // faviconLoadedFromCacheBlock |
| |
| base::CancelableTaskTracker::TaskId taskId = |
| IOSChromeLargeIconServiceFactory::GetForBrowserState(self.browserState) |
| ->GetLargeIconOrFallbackStyle( |
| node->url(), minFaviconSizeInPixel, desiredFaviconSizeInPixel, |
| base::BindBlockArc(faviconLoadedFromCacheBlock), |
| &_faviconTaskTracker); |
| _faviconLoadTasks[IntegerPair(indexPath.section, indexPath.item)] = taskId; |
| } |
| |
| - (void)updateTableViewBackgroundStyle:(BookmarkHomeBackgroundStyle)style { |
| if (style == BookmarkHomeBackgroundStyleDefault) { |
| [self.bookmarksTableView hideLoadingSpinnerBackground]; |
| [self.bookmarksTableView hideEmptyBackground]; |
| } else if (style == BookmarkHomeBackgroundStyleLoading) { |
| [self.bookmarksTableView hideEmptyBackground]; |
| [self.bookmarksTableView showLoadingSpinnerBackground]; |
| } else if (style == BookmarkHomeBackgroundStyleEmpty) { |
| [self.bookmarksTableView hideLoadingSpinnerBackground]; |
| [self.bookmarksTableView showEmptyBackground]; |
| } |
| } |
| |
| - (void)showSignin:(ShowSigninCommand*)command { |
| [self.dispatcher showSignin:command baseViewController:self]; |
| } |
| |
| - (void)configureSigninPromoWithConfigurator: |
| (SigninPromoViewConfigurator*)configurator |
| atIndexPath:(NSIndexPath*)indexPath |
| forceReloadCell:(BOOL)forceReloadCell { |
| BookmarkTableSigninPromoCell* signinPromoCell = |
| base::mac::ObjCCast<BookmarkTableSigninPromoCell>( |
| [self.sharedState.tableView cellForRowAtIndexPath:indexPath]); |
| if (!signinPromoCell) { |
| return; |
| } |
| // Should always reconfigure the cell size even if it has to be reloaded, |
| // to make sure it has the right size to compute the cell size. |
| [configurator configureSigninPromoView:signinPromoCell.signinPromoView]; |
| if (forceReloadCell) { |
| // The section should be reload to update the cell height. |
| NSIndexSet* indexSet = [NSIndexSet indexSetWithIndex:indexPath.section]; |
| [self.sharedState.tableView reloadSections:indexSet |
| withRowAnimation:UITableViewRowAnimationNone]; |
| } |
| } |
| |
| #pragma mark - Action sheet callbacks |
| |
| // Opens the folder move editor for the given node. |
| - (void)moveNodes:(const std::set<const BookmarkNode*>&)nodes { |
| DCHECK(!self.folderSelector); |
| DCHECK(nodes.size() > 0); |
| const BookmarkNode* editedNode = *(nodes.begin()); |
| const BookmarkNode* selectedFolder = editedNode->parent(); |
| self.folderSelector = [[BookmarkFolderViewController alloc] |
| initWithBookmarkModel:self.bookmarks |
| allowsNewFolders:YES |
| editedNodes:nodes |
| allowsCancel:YES |
| selectedFolder:selectedFolder]; |
| self.folderSelector.delegate = self; |
| UINavigationController* navController = [[BookmarkNavigationController alloc] |
| initWithRootViewController:self.folderSelector]; |
| [navController setModalPresentationStyle:UIModalPresentationFormSheet]; |
| [self presentViewController:navController animated:YES completion:NULL]; |
| } |
| |
| // Deletes the current node. |
| - (void)deleteNodes:(const std::set<const BookmarkNode*>&)nodes { |
| DCHECK_GE(nodes.size(), 1u); |
| bookmark_utils_ios::DeleteBookmarksWithUndoToast(nodes, self.bookmarks, |
| self.browserState); |
| [self setTableViewEditing:NO]; |
| } |
| |
| // Opens the editor on the given node. |
| - (void)editNode:(const BookmarkNode*)node { |
| DCHECK(!self.editViewController); |
| DCHECK(!self.folderEditor); |
| UIViewController* editorController = nil; |
| if (node->is_folder()) { |
| BookmarkFolderEditorViewController* folderEditor = |
| [BookmarkFolderEditorViewController |
| folderEditorWithBookmarkModel:self.bookmarks |
| folder:node |
| browserState:self.browserState]; |
| folderEditor.delegate = self; |
| self.folderEditor = folderEditor; |
| editorController = folderEditor; |
| } else { |
| BookmarkEditViewController* controller = |
| [[BookmarkEditViewController alloc] initWithBookmark:node |
| browserState:self.browserState]; |
| self.editViewController = controller; |
| self.editViewController.delegate = self; |
| editorController = self.editViewController; |
| } |
| DCHECK(editorController); |
| UINavigationController* navController = [[BookmarkNavigationController alloc] |
| initWithRootViewController:editorController]; |
| navController.modalPresentationStyle = UIModalPresentationFormSheet; |
| [self presentViewController:navController animated:YES completion:NULL]; |
| } |
| |
| - (void)openAllNodes:(const std::vector<const bookmarks::BookmarkNode*>&)nodes |
| inIncognito:(BOOL)inIncognito |
| newTab:(BOOL)newTab { |
| [self cachePosition]; |
| std::vector<GURL> urls = GetUrlsToOpen(nodes); |
| [self.homeDelegate bookmarkHomeViewControllerWantsDismissal:self |
| navigationToUrls:urls |
| inIncognito:inIncognito |
| newTab:newTab]; |
| } |
| |
| #pragma mark - Navigation Bar Callbacks |
| |
| - (void)navigationBarCancel:(id)sender { |
| [self navigateAway]; |
| [self dismissWithURL:GURL()]; |
| } |
| |
| #pragma mark - BookmarkTableViewDelegate |
| |
| - (void)bookmarkTableView:(BookmarkTableView*)view |
| selectedUrlForNavigation:(const GURL&)url { |
| [self dismissWithURL:url]; |
| } |
| |
| - (void)bookmarkTableView:(BookmarkTableView*)view |
| selectedFolderForNavigation:(const bookmarks::BookmarkNode*)folder { |
| BookmarkHomeViewController* controller = |
| [self createControllerWithRootFolder:folder]; |
| [self.navigationController pushViewController:controller animated:YES]; |
| } |
| |
| - (void)bookmarkTableView:(BookmarkTableView*)view |
| selectedNodesForDeletion: |
| (const std::set<const bookmarks::BookmarkNode*>&)nodes { |
| [self deleteNodes:nodes]; |
| } |
| |
| - (void)bookmarkTableView:(BookmarkTableView*)view |
| selectedEditNodes: |
| (const std::set<const bookmarks::BookmarkNode*>&)nodes { |
| // Early return if bookmarks table is not in edit mode. |
| if (!self.sharedState.currentlyInEditMode) { |
| return; |
| } |
| |
| if (nodes.size() == 0) { |
| // if nothing to select, exit edit mode. |
| if (![self hasBookmarksOrFolders]) { |
| [self setTableViewEditing:NO]; |
| return; |
| } |
| [self setContextBarState:BookmarksContextBarBeginSelection]; |
| return; |
| } |
| if (nodes.size() == 1) { |
| const bookmarks::BookmarkNode* node = *nodes.begin(); |
| if (node->is_url()) { |
| [self setContextBarState:BookmarksContextBarSingleURLSelection]; |
| } else if (node->is_folder()) { |
| [self setContextBarState:BookmarksContextBarSingleFolderSelection]; |
| } |
| return; |
| } |
| |
| BOOL foundURL = NO; |
| BOOL foundFolder = NO; |
| for (const BookmarkNode* node : nodes) { |
| if (!foundURL && node->is_url()) { |
| foundURL = YES; |
| } else if (!foundFolder && node->is_folder()) { |
| foundFolder = YES; |
| } |
| // Break early, if we found both types of nodes. |
| if (foundURL && foundFolder) { |
| break; |
| } |
| } |
| |
| // Only URLs are selected. |
| if (foundURL && !foundFolder) { |
| [self setContextBarState:BookmarksContextBarMultipleURLSelection]; |
| return; |
| } |
| // Only Folders are selected. |
| if (!foundURL && foundFolder) { |
| [self setContextBarState:BookmarksContextBarMultipleFolderSelection]; |
| return; |
| } |
| // Mixed selection. |
| if (foundURL && foundFolder) { |
| [self setContextBarState:BookmarksContextBarMixedSelection]; |
| return; |
| } |
| |
| NOTREACHED(); |
| return; |
| } |
| |
| - (void)bookmarkTableView:(BookmarkTableView*)view |
| showContextMenuForNode:(const bookmarks::BookmarkNode*)node { |
| if (node->is_url()) { |
| [self presentViewController:[self contextMenuForSingleBookmarkURL:node] |
| animated:YES |
| completion:nil]; |
| return; |
| } |
| |
| if (node->is_folder()) { |
| [self presentViewController:[self contextMenuForSingleBookmarkFolder:node] |
| animated:YES |
| completion:nil]; |
| return; |
| } |
| NOTREACHED(); |
| } |
| |
| - (void)bookmarkTableView:(BookmarkTableView*)view |
| didMoveNode:(const bookmarks::BookmarkNode*)node |
| toPosition:(int)position { |
| bookmark_utils_ios::UpdateBookmarkPositionWithUndoToast( |
| node, _rootNode, position, self.bookmarks, self.browserState); |
| } |
| |
| - (void)bookmarkTableViewRefreshContextBar:(BookmarkTableView*)view { |
| // At default state, the enable state of context bar buttons could change |
| // during refresh. |
| if (self.contextBarState == BookmarksContextBarDefault) { |
| [self setBookmarksContextBarButtonsDefaultState]; |
| } |
| } |
| |
| - (BOOL)isAtTopOfNavigation:(BookmarkTableView*)view { |
| return (self.navigationController.topViewController == self); |
| } |
| |
| - (void)bookmarkTableViewRefreshContents:(BookmarkTableView*)view { |
| [self refreshContents]; |
| } |
| |
| #pragma mark - BookmarkTableCellTitleEditDelegate |
| |
| - (void)textDidChangeTo:(NSString*)newName { |
| DCHECK(self.sharedState.editingFolderNode); |
| self.sharedState.addingNewFolder = NO; |
| if (newName.length > 0) { |
| self.sharedState.bookmarkModel->SetTitle(self.sharedState.editingFolderNode, |
| base::SysNSStringToUTF16(newName)); |
| } |
| self.sharedState.editingFolderNode = nullptr; |
| self.sharedState.editingFolderCell = nil; |
| [self refreshContents]; |
| } |
| |
| #pragma mark - BookmarkFolderViewControllerDelegate |
| |
| - (void)folderPicker:(BookmarkFolderViewController*)folderPicker |
| didFinishWithFolder:(const BookmarkNode*)folder { |
| DCHECK(folder); |
| DCHECK(!folder->is_url()); |
| DCHECK_GE(folderPicker.editedNodes.size(), 1u); |
| |
| bookmark_utils_ios::MoveBookmarksWithUndoToast( |
| folderPicker.editedNodes, self.bookmarks, folder, self.browserState); |
| |
| [self setTableViewEditing:NO]; |
| [self dismissViewControllerAnimated:YES completion:NULL]; |
| self.folderSelector.delegate = nil; |
| self.folderSelector = nil; |
| } |
| |
| - (void)folderPickerDidCancel:(BookmarkFolderViewController*)folderPicker { |
| [self setTableViewEditing:NO]; |
| [self dismissViewControllerAnimated:YES completion:NULL]; |
| self.folderSelector.delegate = nil; |
| self.folderSelector = nil; |
| } |
| |
| #pragma mark - BookmarkFolderEditorViewControllerDelegate |
| |
| - (void)bookmarkFolderEditor:(BookmarkFolderEditorViewController*)folderEditor |
| didFinishEditingFolder:(const BookmarkNode*)folder { |
| DCHECK(folder); |
| [self dismissViewControllerAnimated:YES completion:nil]; |
| self.folderEditor.delegate = nil; |
| self.folderEditor = nil; |
| } |
| |
| - (void)bookmarkFolderEditorDidDeleteEditedFolder: |
| (BookmarkFolderEditorViewController*)folderEditor { |
| [self dismissViewControllerAnimated:YES completion:nil]; |
| self.folderEditor.delegate = nil; |
| self.folderEditor = nil; |
| } |
| |
| - (void)bookmarkFolderEditorDidCancel: |
| (BookmarkFolderEditorViewController*)folderEditor { |
| [self dismissViewControllerAnimated:YES completion:nil]; |
| self.folderEditor.delegate = nil; |
| self.folderEditor = nil; |
| } |
| |
| - (void)bookmarkFolderEditorWillCommitTitleChange: |
| (BookmarkFolderEditorViewController*)controller { |
| [self setTableViewEditing:NO]; |
| } |
| |
| #pragma mark - BookmarkEditViewControllerDelegate |
| |
| - (BOOL)bookmarkEditor:(BookmarkEditViewController*)controller |
| shoudDeleteAllOccurencesOfBookmark:(const BookmarkNode*)bookmark { |
| return NO; |
| } |
| |
| - (void)bookmarkEditorWantsDismissal:(BookmarkEditViewController*)controller { |
| self.editViewController.delegate = nil; |
| self.editViewController = nil; |
| [self dismissViewControllerAnimated:YES completion:NULL]; |
| } |
| |
| - (void)bookmarkEditorWillCommitTitleOrUrlChange: |
| (BookmarkEditViewController*)controller { |
| [self setTableViewEditing:NO]; |
| } |
| |
| #pragma mark - BookmarkModelBridgeObserver |
| |
| - (void)bookmarkModelLoaded { |
| if (![self isViewLoaded]) |
| return; |
| |
| DCHECK(!_rootNode); |
| [self setRootNode:self.bookmarks->root_node()]; |
| |
| int64_t unusedFolderId; |
| double unusedScrollPosition; |
| // Bookmark Model is loaded after presenting Bookmarks, we need to check |
| // again here if restoring of cache position is needed. It is to prevent |
| // crbug.com/765503. |
| if ([BookmarkPathCache |
| getBookmarkUIPositionCacheWithPrefService:self.browserState |
| ->GetPrefs() |
| model:self.bookmarks |
| folderId:&unusedFolderId |
| scrollPosition:&unusedScrollPosition]) { |
| self.isReconstructingFromCache = YES; |
| } |
| |
| DCHECK(self.waitForModelView); |
| __weak BookmarkHomeViewController* weakSelf = self; |
| [self.waitForModelView stopWaitingWithCompletion:^{ |
| BookmarkHomeViewController* strongSelf = weakSelf; |
| // Early return if the controller has been deallocated. |
| if (!strongSelf) |
| return; |
| [UIView animateWithDuration:0.2 |
| animations:^{ |
| strongSelf.waitForModelView.alpha = 0.0; |
| } |
| completion:^(BOOL finished) { |
| [strongSelf.waitForModelView removeFromSuperview]; |
| strongSelf.waitForModelView = nil; |
| }]; |
| [strongSelf loadBookmarkViews]; |
| }]; |
| } |
| |
| - (void)bookmarkNodeChanged:(const BookmarkNode*)node { |
| // No-op here. Bookmarks might be refreshed at bookmarkTableView. |
| } |
| |
| - (void)bookmarkNodeChildrenChanged:(const BookmarkNode*)bookmarkNode { |
| // No-op here. Bookmarks might be refreshed at bookmarkTableView. |
| } |
| |
| - (void)bookmarkNode:(const BookmarkNode*)bookmarkNode |
| movedFromParent:(const BookmarkNode*)oldParent |
| toParent:(const BookmarkNode*)newParent { |
| // No-op here. Bookmarks might be refreshed at bookmarkTableView. |
| } |
| |
| - (void)bookmarkNodeDeleted:(const BookmarkNode*)node |
| fromFolder:(const BookmarkNode*)folder { |
| if (_rootNode == node) { |
| [self setTableViewEditing:NO]; |
| } |
| } |
| |
| - (void)bookmarkModelRemovedAllNodes { |
| // No-op |
| } |
| |
| #pragma mark - Accessibility |
| |
| - (BOOL)accessibilityPerformEscape { |
| [self dismissWithURL:GURL()]; |
| return YES; |
| } |
| |
| #pragma mark - private |
| |
| - (void)setupUIStackCacheIfApplicable { |
| self.isReconstructingFromCache = NO; |
| |
| int64_t folderId; |
| double scrollPosition; |
| // If folderId is invalid or rootNode reached the cached folderId, stop |
| // stacking and return. |
| if (![BookmarkPathCache |
| getBookmarkUIPositionCacheWithPrefService:self.browserState |
| ->GetPrefs() |
| model:self.bookmarks |
| folderId:&folderId |
| scrollPosition:&scrollPosition] || |
| folderId == _rootNode->id()) { |
| return; |
| } |
| |
| // Otherwise drill down until we recreate the UI stack for the cached bookmark |
| // path. |
| NSMutableArray* mutablePath = [bookmark_utils_ios::CreateBookmarkPath( |
| self.bookmarks, folderId) mutableCopy]; |
| if (!mutablePath) { |
| return; |
| } |
| NSArray* thisBookmarkPath = |
| bookmark_utils_ios::CreateBookmarkPath(self.bookmarks, _rootNode->id()); |
| if (!thisBookmarkPath) { |
| return; |
| } |
| |
| [mutablePath removeObjectsInArray:thisBookmarkPath]; |
| const BookmarkNode* node = bookmark_utils_ios::FindFolderById( |
| self.bookmarks, [[mutablePath firstObject] longLongValue]); |
| DCHECK(node); |
| // if node is an empty permanent node, return. |
| if (node->empty() && IsPrimaryPermanentNode(node, self.bookmarks)) { |
| return; |
| } |
| |
| BookmarkHomeViewController* controller = |
| [self createControllerWithRootFolder:node]; |
| // Only scroll to the last viewing position for the leaf node. |
| if (mutablePath.count == 1 && scrollPosition) { |
| [controller |
| setCachedContentPosition:[NSNumber numberWithDouble:scrollPosition]]; |
| } |
| controller.isReconstructingFromCache = YES; |
| [self.navigationController pushViewController:controller animated:NO]; |
| } |
| |
| // Set up context bar for the new UI. |
| - (void)setupContextBar { |
| self.contextBar = [[BookmarkContextBar alloc] initWithFrame:CGRectZero]; |
| self.contextBar.delegate = self; |
| [self.contextBar setTranslatesAutoresizingMaskIntoConstraints:NO]; |
| |
| [self setContextBarState:BookmarksContextBarDefault]; |
| [self.view addSubview:self.contextBar]; |
| } |
| |
| // Set up navigation bar for the new UI. |
| - (void)setupNavigationBar { |
| DCHECK(self.sharedState.tableView); |
| self.navigationController.navigationBarHidden = YES; |
| self.appBar = [[MDCAppBar alloc] init]; |
| [self addChildViewController:self.appBar.headerViewController]; |
| ConfigureAppBarWithCardStyle(self.appBar); |
| // Set the header view's tracking scroll view. |
| self.appBar.headerViewController.headerView.trackingScrollView = |
| self.sharedState.tableView; |
| self.sharedState.headerView = self.appBar.headerViewController.headerView; |
| |
| [self.appBar addSubviewsToParent]; |
| // Prevent the touch events on appBar from being forwarded to the tableView. |
| // See https://crbug.com/773580 |
| [self.appBar.headerViewController.headerView |
| stopForwardingTouchEventsForView:self.appBar.navigationBar]; |
| |
| if (self.navigationController.viewControllers.count > 1) { |
| // Add custom back button. |
| UIBarButtonItem* backButton = |
| [ChromeIcon templateBarButtonItemWithImage:[ChromeIcon backIcon] |
| target:self |
| action:@selector(back)]; |
| self.navigationItem.leftBarButtonItem = backButton; |
| } |
| |
| // Add custom title. |
| self.title = bookmark_utils_ios::TitleForBookmarkNode(_rootNode); |
| |
| // Add custom done button. |
| self.navigationItem.rightBarButtonItem = [self customizedDoneButton]; |
| } |
| |
| // Back button callback for the new ui. |
| - (void)back { |
| [self navigateAway]; |
| [self.navigationController popViewControllerAnimated:YES]; |
| } |
| |
| - (UIBarButtonItem*)customizedDoneButton { |
| UIBarButtonItem* doneButton = [[UIBarButtonItem alloc] |
| initWithTitle:l10n_util::GetNSString(IDS_IOS_NAVIGATION_BAR_DONE_BUTTON) |
| style:UIBarButtonItemStyleDone |
| target:self |
| action:@selector(navigationBarCancel:)]; |
| doneButton.accessibilityLabel = |
| l10n_util::GetNSString(IDS_IOS_NAVIGATION_BAR_DONE_BUTTON); |
| return doneButton; |
| } |
| |
| // Saves the current position and asks the delegate to open the url, if delegate |
| // is set, otherwise opens the URL using loader. |
| - (void)dismissWithURL:(const GURL&)url { |
| [self cachePosition]; |
| if (self.homeDelegate) { |
| std::vector<GURL> urls; |
| if (url.is_valid()) |
| urls.push_back(url); |
| [self.homeDelegate bookmarkHomeViewControllerWantsDismissal:self |
| navigationToUrls:urls]; |
| } else { |
| // Before passing the URL to the block, make sure the block has a copy of |
| // the URL and not just a reference. |
| const GURL localUrl(url); |
| dispatch_async(dispatch_get_main_queue(), ^{ |
| [self loadURL:localUrl]; |
| }); |
| } |
| } |
| |
| - (void)loadURL:(const GURL&)url { |
| if (url.is_empty() || url.SchemeIs(url::kJavaScriptScheme)) |
| return; |
| |
| new_tab_page_uma::RecordAction(self.browserState, |
| new_tab_page_uma::ACTION_OPENED_BOOKMARK); |
| base::RecordAction( |
| base::UserMetricsAction("MobileBookmarkManagerEntryOpened")); |
| [self.loader loadURL:url |
| referrer:web::Referrer() |
| transition:ui::PAGE_TRANSITION_AUTO_BOOKMARK |
| rendererInitiated:NO]; |
| } |
| |
| - (void)updateViewConstraints { |
| if (!_addedConstraints) { |
| if (self.contextBar && self.bookmarksTableView) { |
| NSDictionary* views = @{ |
| @"tableView" : self.bookmarksTableView, |
| @"contextBar" : self.contextBar, |
| }; |
| NSArray* constraints = @[ |
| @"V:|[tableView][contextBar]|", |
| @"H:|[tableView]|", |
| @"H:|[contextBar]|", |
| ]; |
| ApplyVisualConstraints(constraints, views); |
| } else if (self.bookmarksTableView) { |
| NSDictionary* views = @{ |
| @"tableView" : self.bookmarksTableView, |
| }; |
| NSArray* constraints = @[ |
| @"V:|[tableView]|", |
| @"H:|[tableView]|", |
| ]; |
| ApplyVisualConstraints(constraints, views); |
| } |
| _addedConstraints = YES; |
| } |
| [super updateViewConstraints]; |
| } |
| |
| - (void)addNewFolder { |
| [self.sharedState.editingFolderCell stopEdit]; |
| if (!self.sharedState.tableViewDisplayedRootNode) { |
| return; |
| } |
| self.sharedState.addingNewFolder = YES; |
| base::string16 folderTitle = base::SysNSStringToUTF16( |
| l10n_util::GetNSString(IDS_IOS_BOOKMARK_NEW_GROUP_DEFAULT_NAME)); |
| self.sharedState.editingFolderNode = |
| self.sharedState.bookmarkModel->AddFolder( |
| self.sharedState.tableViewDisplayedRootNode, |
| self.sharedState.tableViewDisplayedRootNode->child_count(), |
| folderTitle); |
| |
| BookmarkHomeNodeItem* nodeItem = [[BookmarkHomeNodeItem alloc] |
| initWithType:BookmarkHomeItemTypeBookmark |
| bookmarkNode:self.sharedState.editingFolderNode]; |
| [self.sharedState.tableViewModel |
| addItem:nodeItem |
| toSectionWithIdentifier:BookmarkHomeSectionIdentifierBookmarks]; |
| |
| // Insert the new folder cell at the end of the table. |
| NSIndexPath* newRowIndexPath = |
| [self.sharedState.tableViewModel indexPathForItem:nodeItem]; |
| NSMutableArray* newRowIndexPaths = |
| [[NSMutableArray alloc] initWithObjects:newRowIndexPath, nil]; |
| [self.sharedState.tableView beginUpdates]; |
| [self.sharedState.tableView |
| insertRowsAtIndexPaths:newRowIndexPaths |
| withRowAnimation:UITableViewRowAnimationNone]; |
| [self.sharedState.tableView endUpdates]; |
| |
| // Scroll to the end of the table |
| [self.sharedState.tableView |
| scrollToRowAtIndexPath:newRowIndexPath |
| atScrollPosition:UITableViewScrollPositionBottom |
| animated:YES]; |
| } |
| |
| - (BookmarkHomeViewController*)createControllerWithRootFolder: |
| (const bookmarks::BookmarkNode*)folder { |
| BookmarkHomeViewController* controller = |
| [[BookmarkHomeViewController alloc] initWithLoader:_loader |
| browserState:self.browserState |
| dispatcher:self.dispatcher]; |
| [controller setRootNode:folder]; |
| controller.homeDelegate = self.homeDelegate; |
| return controller; |
| } |
| |
| // Sets the editing mode for tableView, update context bar state accordingly. |
| - (void)setTableViewEditing:(BOOL)editing { |
| self.sharedState.currentlyInEditMode = editing; |
| [self setContextBarState:editing ? BookmarksContextBarBeginSelection |
| : BookmarksContextBarDefault]; |
| } |
| |
| // Row selection of the tableView will be cleared after reloadData. This |
| // function is used to restore the row selection. It also updates editNodes in |
| // case some selected nodes are removed. |
| - (void)restoreRowSelection { |
| // Create a new editNodes set to check if some selected nodes are removed. |
| std::set<const bookmarks::BookmarkNode*> newEditNodes; |
| |
| // Add selected nodes to editNodes only if they are not removed (still exist |
| // in the table). |
| NSArray<TableViewItem*>* items = [self.sharedState.tableViewModel |
| itemsInSectionWithIdentifier:BookmarkHomeSectionIdentifierBookmarks]; |
| for (TableViewItem* item in items) { |
| BookmarkHomeNodeItem* nodeItem = |
| base::mac::ObjCCastStrict<BookmarkHomeNodeItem>(item); |
| const BookmarkNode* node = nodeItem.bookmarkNode; |
| if (self.sharedState.editNodes.find(node) != |
| self.sharedState.editNodes.end()) { |
| newEditNodes.insert(node); |
| // Reselect the row of this node. |
| NSIndexPath* itemPath = |
| [self.sharedState.tableViewModel indexPathForItem:nodeItem]; |
| [self.sharedState.tableView |
| selectRowAtIndexPath:itemPath |
| animated:NO |
| scrollPosition:UITableViewScrollPositionNone]; |
| } |
| } |
| |
| // if editNodes is changed, update it and tell BookmarkTableViewDelegate. |
| if (self.sharedState.editNodes.size() != newEditNodes.size()) { |
| self.sharedState.editNodes = newEditNodes; |
| [self bookmarkTableView:self.bookmarksTableView |
| selectedEditNodes:self.sharedState.editNodes]; |
| } |
| } |
| |
| - (BOOL)allowsNewFolder { |
| // When the current root node has been removed remotely (becomes NULL), |
| // creating new folder is forbidden. |
| return self.sharedState.tableViewDisplayedRootNode != NULL; |
| } |
| |
| - (CGFloat)contentPosition { |
| if (self.sharedState.tableViewDisplayedRootNode == |
| self.sharedState.bookmarkModel->root_node()) { |
| return 0; |
| } |
| // Divided the scroll position by cell height so that it will stay correct in |
| // case the cell height is changed in future. |
| return self.sharedState.tableView.contentOffset.y / |
| [BookmarkHomeSharedState cellHeightPt]; |
| } |
| |
| - (void)setContentPosition:(CGFloat)position { |
| // The scroll position was divided by the cell height when stored. |
| [self.sharedState.tableView |
| setContentOffset:CGPointMake( |
| 0, |
| position * [BookmarkHomeSharedState cellHeightPt])]; |
| } |
| |
| - (void)navigateAway { |
| [self.sharedState.editingFolderCell stopEdit]; |
| } |
| |
| // Returns YES if the given node is a url or folder node. |
| - (BOOL)isUrlOrFolder:(const BookmarkNode*)node { |
| return node->type() == BookmarkNode::URL || |
| node->type() == BookmarkNode::FOLDER; |
| } |
| |
| // Returns the bookmark node associated with |indexPath|. |
| - (const BookmarkNode*)nodeAtIndexPath:(NSIndexPath*)indexPath { |
| TableViewItem* item = |
| [self.sharedState.tableViewModel itemAtIndexPath:indexPath]; |
| |
| if (item.type == BookmarkHomeItemTypeBookmark) { |
| BookmarkHomeNodeItem* nodeItem = |
| base::mac::ObjCCastStrict<BookmarkHomeNodeItem>(item); |
| return nodeItem.bookmarkNode; |
| } |
| |
| NOTREACHED(); |
| return nullptr; |
| } |
| |
| - (BOOL)hasBookmarksOrFolders { |
| return self.sharedState.tableViewDisplayedRootNode && |
| !self.sharedState.tableViewDisplayedRootNode->empty(); |
| } |
| |
| - (std::vector<const bookmarks::BookmarkNode*>)getEditNodesInVector { |
| // Create a vector of edit nodes in the same order as the nodes in folder. |
| std::vector<const bookmarks::BookmarkNode*> nodes; |
| int childCount = self.sharedState.tableViewDisplayedRootNode->child_count(); |
| for (int i = 0; i < childCount; ++i) { |
| const BookmarkNode* node = |
| self.sharedState.tableViewDisplayedRootNode->GetChild(i); |
| if (self.sharedState.editNodes.find(node) != |
| self.sharedState.editNodes.end()) { |
| nodes.push_back(node); |
| } |
| } |
| return nodes; |
| } |
| |
| #pragma mark - ContextBarDelegate implementation |
| |
| // Called when the leading button is clicked. |
| - (void)leadingButtonClicked { |
| // Ignore the button tap if view controller presenting. |
| if ([self presentedViewController]) { |
| return; |
| } |
| const std::set<const bookmarks::BookmarkNode*> nodes = |
| self.sharedState.editNodes; |
| switch (self.contextBarState) { |
| case BookmarksContextBarDefault: |
| // New Folder clicked. |
| [self addNewFolder]; |
| break; |
| case BookmarksContextBarBeginSelection: |
| // This must never happen, as the leading button is disabled at this |
| // point. |
| NOTREACHED(); |
| break; |
| case BookmarksContextBarSingleURLSelection: |
| case BookmarksContextBarMultipleURLSelection: |
| case BookmarksContextBarSingleFolderSelection: |
| case BookmarksContextBarMultipleFolderSelection: |
| case BookmarksContextBarMixedSelection: |
| // Delete clicked. |
| [self deleteNodes:nodes]; |
| break; |
| case BookmarksContextBarNone: |
| default: |
| NOTREACHED(); |
| } |
| } |
| |
| // Called when the center button is clicked. |
| - (void)centerButtonClicked { |
| // Ignore the button tap if view controller presenting. |
| if ([self presentedViewController]) { |
| return; |
| } |
| const std::set<const bookmarks::BookmarkNode*> nodes = |
| self.sharedState.editNodes; |
| // Center button is shown and is clickable only when at least |
| // one node is selected. |
| DCHECK(nodes.size() > 0); |
| switch (self.contextBarState) { |
| case BookmarksContextBarDefault: |
| // Center button is disabled in default state. |
| NOTREACHED(); |
| break; |
| case BookmarksContextBarBeginSelection: |
| // Center button is disabled in start state. |
| NOTREACHED(); |
| break; |
| case BookmarksContextBarSingleURLSelection: |
| // More clicked, show action sheet with context menu. |
| [self presentViewController: |
| [self contextMenuForSingleBookmarkURL:*(nodes.begin())] |
| animated:YES |
| completion:nil]; |
| break; |
| case BookmarksContextBarMultipleURLSelection: |
| // More clicked, show action sheet with context menu. |
| [self |
| presentViewController:[self contextMenuForMultipleBookmarkURLs:nodes] |
| animated:YES |
| completion:nil]; |
| break; |
| case BookmarksContextBarSingleFolderSelection: |
| // More clicked, show action sheet with context menu. |
| [self presentViewController: |
| [self contextMenuForSingleBookmarkFolder:*(nodes.begin())] |
| animated:YES |
| completion:nil]; |
| break; |
| case BookmarksContextBarMultipleFolderSelection: |
| case BookmarksContextBarMixedSelection: |
| // More clicked, show action sheet with context menu. |
| [self presentViewController: |
| [self contextMenuForMixedAndMultiFolderSelection:nodes] |
| animated:YES |
| completion:nil]; |
| break; |
| case BookmarksContextBarNone: |
| default: |
| NOTREACHED(); |
| } |
| } |
| |
| // Called when the trailing button, "Select" or "Cancel" is clicked. |
| - (void)trailingButtonClicked { |
| // Ignore the button tap if view controller presenting. |
| if ([self presentedViewController]) { |
| return; |
| } |
| // Toggle edit mode. |
| [self setTableViewEditing:!self.sharedState.currentlyInEditMode]; |
| } |
| |
| #pragma mark - ContextBarStates |
| |
| // Customizes the context bar buttons based the |state| passed in. |
| - (void)setContextBarState:(BookmarksContextBarState)state { |
| _contextBarState = state; |
| switch (state) { |
| case BookmarksContextBarDefault: |
| [self setBookmarksContextBarButtonsDefaultState]; |
| break; |
| case BookmarksContextBarBeginSelection: |
| [self setBookmarksContextBarSelectionStartState]; |
| break; |
| case BookmarksContextBarSingleURLSelection: |
| case BookmarksContextBarMultipleURLSelection: |
| case BookmarksContextBarMultipleFolderSelection: |
| case BookmarksContextBarMixedSelection: |
| case BookmarksContextBarSingleFolderSelection: |
| // Reset to start state, and then override with customizations that apply. |
| [self setBookmarksContextBarSelectionStartState]; |
| [self.contextBar setButtonEnabled:YES forButton:ContextBarCenterButton]; |
| [self.contextBar setButtonEnabled:YES forButton:ContextBarLeadingButton]; |
| break; |
| case BookmarksContextBarNone: |
| default: |
| break; |
| } |
| } |
| |
| - (void)setBookmarksContextBarButtonsDefaultState { |
| // Set New Folder button |
| [self.contextBar setButtonTitle:l10n_util::GetNSString( |
| IDS_IOS_BOOKMARK_CONTEXT_BAR_NEW_FOLDER) |
| forButton:ContextBarLeadingButton]; |
| [self.contextBar setButtonVisibility:YES forButton:ContextBarLeadingButton]; |
| [self.contextBar setButtonEnabled:[self allowsNewFolder] |
| forButton:ContextBarLeadingButton]; |
| [self.contextBar setButtonStyle:ContextBarButtonStyleDefault |
| forButton:ContextBarLeadingButton]; |
| |
| // Set Center button to invisible. |
| [self.contextBar setButtonVisibility:NO forButton:ContextBarCenterButton]; |
| |
| // Set Select button. |
| [self.contextBar |
| setButtonTitle:l10n_util::GetNSString(IDS_IOS_BOOKMARK_CONTEXT_BAR_SELECT) |
| forButton:ContextBarTrailingButton]; |
| [self.contextBar setButtonVisibility:YES forButton:ContextBarTrailingButton]; |
| [self.contextBar setButtonEnabled:[self hasBookmarksOrFolders] |
| forButton:ContextBarTrailingButton]; |
| } |
| |
| - (void)setBookmarksContextBarSelectionStartState { |
| // Disabled Delete button. |
| [self.contextBar |
| setButtonTitle:l10n_util::GetNSString(IDS_IOS_BOOKMARK_CONTEXT_BAR_DELETE) |
| forButton:ContextBarLeadingButton]; |
| [self.contextBar setButtonVisibility:YES forButton:ContextBarLeadingButton]; |
| [self.contextBar setButtonEnabled:NO forButton:ContextBarLeadingButton]; |
| [self.contextBar setButtonStyle:ContextBarButtonStyleDelete |
| forButton:ContextBarLeadingButton]; |
| |
| // Disabled More button. |
| [self.contextBar |
| setButtonTitle:l10n_util::GetNSString(IDS_IOS_BOOKMARK_CONTEXT_BAR_MORE) |
| forButton:ContextBarCenterButton]; |
| [self.contextBar setButtonVisibility:YES forButton:ContextBarCenterButton]; |
| [self.contextBar setButtonEnabled:NO forButton:ContextBarCenterButton]; |
| |
| // Enabled Cancel button. |
| [self.contextBar setButtonTitle:l10n_util::GetNSString(IDS_CANCEL) |
| forButton:ContextBarTrailingButton]; |
| [self.contextBar setButtonVisibility:YES forButton:ContextBarTrailingButton]; |
| [self.contextBar setButtonEnabled:YES forButton:ContextBarTrailingButton]; |
| } |
| |
| #pragma mark - Context Menu |
| |
| - (UIAlertController*)contextMenuForMultipleBookmarkURLs: |
| (const std::set<const bookmarks::BookmarkNode*>)nodes { |
| __weak BookmarkHomeViewController* weakSelf = self; |
| UIAlertController* alert = [UIAlertController |
| alertControllerWithTitle:nil |
| message:nil |
| preferredStyle:UIAlertControllerStyleActionSheet]; |
| alert.view.accessibilityIdentifier = @"bookmark_context_menu"; |
| |
| UIAlertAction* cancelAction = |
| [UIAlertAction actionWithTitle:l10n_util::GetNSString(IDS_CANCEL) |
| style:UIAlertActionStyleCancel |
| handler:nil]; |
| |
| UIAlertAction* openAllAction = [UIAlertAction |
| actionWithTitle:l10n_util::GetNSString(IDS_IOS_BOOKMARK_CONTEXT_MENU_OPEN) |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* _Nonnull action) { |
| std::vector<const BookmarkNode*> nodes = |
| [weakSelf getEditNodesInVector]; |
| [weakSelf openAllNodes:nodes inIncognito:NO newTab:NO]; |
| }]; |
| |
| UIAlertAction* openInIncognitoAction = [UIAlertAction |
| actionWithTitle:l10n_util::GetNSString( |
| IDS_IOS_BOOKMARK_CONTEXT_MENU_OPEN_INCOGNITO) |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* _Nonnull action) { |
| std::vector<const BookmarkNode*> nodes = |
| [weakSelf getEditNodesInVector]; |
| [weakSelf openAllNodes:nodes inIncognito:YES newTab:NO]; |
| }]; |
| |
| UIAlertAction* moveAction = [UIAlertAction |
| actionWithTitle:l10n_util::GetNSString(IDS_IOS_BOOKMARK_CONTEXT_MENU_MOVE) |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* _Nonnull action) { |
| [weakSelf moveNodes:nodes]; |
| }]; |
| [alert addAction:openAllAction]; |
| [alert addAction:openInIncognitoAction]; |
| [alert addAction:moveAction]; |
| [alert addAction:cancelAction]; |
| return alert; |
| } |
| |
| - (UIAlertController*)contextMenuForSingleBookmarkURL: |
| (const BookmarkNode*)node { |
| __weak BookmarkHomeViewController* weakSelf = self; |
| UIAlertController* alert = [UIAlertController |
| alertControllerWithTitle:nil |
| message:nil |
| preferredStyle:UIAlertControllerStyleActionSheet]; |
| alert.view.accessibilityIdentifier = @"bookmark_context_menu"; |
| |
| UIAlertAction* cancelAction = |
| [UIAlertAction actionWithTitle:l10n_util::GetNSString(IDS_CANCEL) |
| style:UIAlertActionStyleCancel |
| handler:nil]; |
| |
| UIAlertAction* editAction = [UIAlertAction |
| actionWithTitle:l10n_util::GetNSString(IDS_IOS_BOOKMARK_CONTEXT_MENU_EDIT) |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* _Nonnull action) { |
| [weakSelf editNode:node]; |
| }]; |
| |
| void (^copyHandler)(UIAlertAction*) = ^(UIAlertAction*) { |
| UIPasteboard* pasteboard = [UIPasteboard generalPasteboard]; |
| std::string urlString = node->url().possibly_invalid_spec(); |
| pasteboard.string = base::SysUTF8ToNSString(urlString); |
| [self setTableViewEditing:NO]; |
| }; |
| UIAlertAction* copyAction = [UIAlertAction |
| actionWithTitle:l10n_util::GetNSString(IDS_IOS_CONTENT_CONTEXT_COPY) |
| style:UIAlertActionStyleDefault |
| handler:copyHandler]; |
| |
| UIAlertAction* openInNewTabAction = [UIAlertAction |
| actionWithTitle:l10n_util::GetNSString( |
| IDS_IOS_CONTENT_CONTEXT_OPENLINKNEWTAB) |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* _Nonnull action) { |
| std::vector<const BookmarkNode*> nodes = {node}; |
| [weakSelf openAllNodes:nodes inIncognito:NO newTab:YES]; |
| }]; |
| |
| UIAlertAction* openInIncognitoAction = [UIAlertAction |
| actionWithTitle:l10n_util::GetNSString( |
| IDS_IOS_CONTENT_CONTEXT_OPENLINKNEWINCOGNITOTAB) |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* _Nonnull action) { |
| std::vector<const BookmarkNode*> nodes = {node}; |
| [weakSelf openAllNodes:nodes inIncognito:YES newTab:YES]; |
| }]; |
| |
| [alert addAction:editAction]; |
| [alert addAction:openInNewTabAction]; |
| [alert addAction:openInIncognitoAction]; |
| [alert addAction:copyAction]; |
| [alert addAction:cancelAction]; |
| return alert; |
| } |
| |
| - (UIAlertController*)contextMenuForSingleBookmarkFolder: |
| (const BookmarkNode*)node { |
| __weak BookmarkHomeViewController* weakSelf = self; |
| UIAlertController* alert = [UIAlertController |
| alertControllerWithTitle:nil |
| message:nil |
| preferredStyle:UIAlertControllerStyleActionSheet]; |
| alert.view.accessibilityIdentifier = @"bookmark_context_menu"; |
| |
| UIAlertAction* cancelAction = |
| [UIAlertAction actionWithTitle:l10n_util::GetNSString(IDS_CANCEL) |
| style:UIAlertActionStyleCancel |
| handler:nil]; |
| |
| UIAlertAction* editAction = [UIAlertAction |
| actionWithTitle:l10n_util::GetNSString( |
| IDS_IOS_BOOKMARK_CONTEXT_MENU_EDIT_FOLDER) |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* _Nonnull action) { |
| [weakSelf editNode:node]; |
| }]; |
| |
| void (^moveHandler)(UIAlertAction*) = ^(UIAlertAction*) { |
| std::set<const BookmarkNode*> nodes; |
| nodes.insert(node); |
| [weakSelf moveNodes:nodes]; |
| }; |
| UIAlertAction* moveAction = [UIAlertAction |
| actionWithTitle:l10n_util::GetNSString(IDS_IOS_BOOKMARK_CONTEXT_MENU_MOVE) |
| style:UIAlertActionStyleDefault |
| handler:moveHandler]; |
| [alert addAction:editAction]; |
| [alert addAction:moveAction]; |
| [alert addAction:cancelAction]; |
| return alert; |
| } |
| |
| - (UIAlertController*)contextMenuForMixedAndMultiFolderSelection: |
| (const std::set<const bookmarks::BookmarkNode*>)nodes { |
| __weak BookmarkHomeViewController* weakSelf = self; |
| UIAlertController* alert = [UIAlertController |
| alertControllerWithTitle:nil |
| message:nil |
| preferredStyle:UIAlertControllerStyleActionSheet]; |
| alert.view.accessibilityIdentifier = @"bookmark_context_menu"; |
| |
| UIAlertAction* cancelAction = |
| [UIAlertAction actionWithTitle:l10n_util::GetNSString(IDS_CANCEL) |
| style:UIAlertActionStyleCancel |
| handler:nil]; |
| |
| UIAlertAction* moveAction = [UIAlertAction |
| actionWithTitle:l10n_util::GetNSString(IDS_IOS_BOOKMARK_CONTEXT_MENU_MOVE) |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* _Nonnull action) { |
| [weakSelf moveNodes:nodes]; |
| }]; |
| [alert addAction:moveAction]; |
| [alert addAction:cancelAction]; |
| return alert; |
| } |
| |
| #pragma mark - Favicon Handling |
| |
| - (void)updateCellAtIndexPath:(NSIndexPath*)indexPath |
| withImage:(UIImage*)image |
| backgroundColor:(UIColor*)backgroundColor |
| textColor:(UIColor*)textColor |
| fallbackText:(NSString*)fallbackText { |
| BookmarkTableCell* cell = |
| [self.sharedState.tableView cellForRowAtIndexPath:indexPath]; |
| if (!cell) { |
| return; |
| } |
| |
| if (image) { |
| [cell setImage:image]; |
| } else { |
| [cell setPlaceholderText:fallbackText |
| textColor:textColor |
| backgroundColor:backgroundColor]; |
| } |
| } |
| |
| - (void)updateCellAtIndexPath:(NSIndexPath*)indexPath |
| withLargeIconResult:(const favicon_base::LargeIconResult&)result |
| fallbackText:(NSString*)fallbackText { |
| UIImage* favIcon = nil; |
| UIColor* backgroundColor = nil; |
| UIColor* textColor = nil; |
| |
| if (result.bitmap.is_valid()) { |
| scoped_refptr<base::RefCountedMemory> data = result.bitmap.bitmap_data; |
| favIcon = [UIImage |
| imageWithData:[NSData dataWithBytes:data->front() length:data->size()]]; |
| fallbackText = nil; |
| // Update the time when the icon was last requested - postpone thus the |
| // automatic eviction of the favicon from the favicon database. |
| IOSChromeLargeIconServiceFactory::GetForBrowserState(self.browserState) |
| ->TouchIconFromGoogleServer(result.bitmap.icon_url); |
| } else if (result.fallback_icon_style) { |
| backgroundColor = |
| skia::UIColorFromSkColor(result.fallback_icon_style->background_color); |
| textColor = |
| skia::UIColorFromSkColor(result.fallback_icon_style->text_color); |
| } |
| |
| [self updateCellAtIndexPath:indexPath |
| withImage:favIcon |
| backgroundColor:backgroundColor |
| textColor:textColor |
| fallbackText:fallbackText]; |
| } |
| |
| // Cancels all async loads of favicons. Subclasses should call this method when |
| // the bookmark model is going through significant changes, then manually call |
| // loadFaviconAtIndexPath: for everything that needs to be loaded; or |
| // just reload relevant cells. |
| - (void)cancelAllFaviconLoads { |
| _faviconTaskTracker.TryCancelAll(); |
| } |
| |
| - (void)cancelLoadingFaviconAtIndexPath:(NSIndexPath*)indexPath { |
| _faviconTaskTracker.TryCancel( |
| _faviconLoadTasks[IntegerPair(indexPath.section, indexPath.item)]); |
| } |
| |
| #pragma mark - UIGestureRecognizerDelegate and gesture handling |
| |
| - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer*)gestureRecognizer { |
| if (gestureRecognizer == |
| self.navigationController.interactivePopGestureRecognizer) { |
| return self.navigationController.viewControllers.count > 1; |
| } |
| return YES; |
| } |
| |
| - (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer |
| shouldReceiveTouch:(UITouch*)touch { |
| // Ignore long press in edit mode. |
| if (self.sharedState.currentlyInEditMode) { |
| return NO; |
| } |
| return YES; |
| } |
| |
| - (void)handleLongPress:(UILongPressGestureRecognizer*)gestureRecognizer { |
| if (self.sharedState.currentlyInEditMode || |
| gestureRecognizer.state != UIGestureRecognizerStateBegan) { |
| return; |
| } |
| CGPoint touchPoint = |
| [gestureRecognizer locationInView:self.sharedState.tableView]; |
| NSIndexPath* indexPath = |
| [self.sharedState.tableView indexPathForRowAtPoint:touchPoint]; |
| if (indexPath == nil || [self.sharedState.tableViewModel |
| sectionIdentifierForSection:indexPath.section] != |
| BookmarkHomeSectionIdentifierBookmarks) { |
| return; |
| } |
| |
| const BookmarkNode* node = [self nodeAtIndexPath:indexPath]; |
| // Disable the long press gesture if it is a permanent node (not an URL or |
| // Folder). |
| if (!node || ![self isUrlOrFolder:node]) { |
| return; |
| } |
| |
| [self bookmarkTableView:self.bookmarksTableView showContextMenuForNode:node]; |
| } |
| |
| #pragma mark - Keyboard |
| |
| - (void)registerForKeyboardNotifications { |
| [[NSNotificationCenter defaultCenter] |
| addObserver:self |
| selector:@selector(keyboardWasShown:) |
| name:UIKeyboardDidShowNotification |
| object:nil]; |
| |
| [[NSNotificationCenter defaultCenter] |
| addObserver:self |
| selector:@selector(keyboardWillBeHidden:) |
| name:UIKeyboardWillHideNotification |
| object:nil]; |
| } |
| |
| - (void)removeKeyboardObservers { |
| NSNotificationCenter* notificationCenter = |
| [NSNotificationCenter defaultCenter]; |
| [notificationCenter removeObserver:self |
| name:UIKeyboardDidShowNotification |
| object:nil]; |
| [notificationCenter removeObserver:self |
| name:UIKeyboardWillHideNotification |
| object:nil]; |
| } |
| |
| // Called when the UIKeyboardDidShowNotification is sent |
| - (void)keyboardWasShown:(NSNotification*)aNotification { |
| if (![self isAtTopOfNavigation:self.bookmarksTableView]) { |
| return; |
| } |
| NSDictionary* info = [aNotification userInfo]; |
| CGFloat keyboardTop = |
| [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue].origin.y; |
| CGFloat tableBottom = CGRectGetMaxY([self.bookmarksTableView |
| convertRect:self.sharedState.tableView.frame |
| toView:nil]); |
| CGFloat shiftY = |
| tableBottom - keyboardTop + [BookmarkHomeSharedState keyboardSpacingPt]; |
| |
| if (shiftY >= 0) { |
| UIEdgeInsets previousContentInsets = |
| self.sharedState.tableView.contentInset; |
| // Shift the content inset to prevent the editing content from being hidden |
| // by the keyboard. |
| UIEdgeInsets contentInsets = |
| UIEdgeInsetsMake(previousContentInsets.top, 0.0, shiftY, 0.0); |
| self.sharedState.tableView.contentInset = contentInsets; |
| self.sharedState.tableView.scrollIndicatorInsets = contentInsets; |
| } |
| } |
| |
| // Called when the UIKeyboardWillHideNotification is sent |
| - (void)keyboardWillBeHidden:(NSNotification*)aNotification { |
| if (![self isAtTopOfNavigation:self.bookmarksTableView]) { |
| return; |
| } |
| UIEdgeInsets previousContentInsets = self.sharedState.tableView.contentInset; |
| // Restore the content inset now that the keyboard has been hidden. |
| UIEdgeInsets contentInsets = |
| UIEdgeInsetsMake(previousContentInsets.top, 0, 0, 0); |
| self.sharedState.tableView.contentInset = contentInsets; |
| self.sharedState.tableView.scrollIndicatorInsets = contentInsets; |
| } |
| |
| #pragma mark - BookmarkHomeSharedStateObserver |
| |
| - (void)sharedStateDidClearEditNodes:(BookmarkHomeSharedState*)sharedState { |
| [self bookmarkTableView:self.bookmarksTableView |
| selectedEditNodes:sharedState.editNodes]; |
| } |
| |
| #pragma mark UIScrollViewDelegate |
| |
| - (void)scrollViewDidScroll:(UIScrollView*)scrollView { |
| if (scrollView == self.sharedState.headerView.trackingScrollView) { |
| [self.sharedState.headerView trackingScrollViewDidScroll]; |
| } |
| } |
| |
| - (void)scrollViewDidEndDecelerating:(UIScrollView*)scrollView { |
| if (scrollView == self.sharedState.headerView.trackingScrollView) { |
| [self.sharedState.headerView trackingScrollViewDidEndDecelerating]; |
| } |
| } |
| |
| - (void)scrollViewDidEndDragging:(UIScrollView*)scrollView |
| willDecelerate:(BOOL)decelerate { |
| if (scrollView == self.sharedState.headerView.trackingScrollView) { |
| [self.sharedState.headerView |
| trackingScrollViewDidEndDraggingWillDecelerate:decelerate]; |
| } |
| } |
| |
| - (void)scrollViewWillEndDragging:(UIScrollView*)scrollView |
| withVelocity:(CGPoint)velocity |
| targetContentOffset:(inout CGPoint*)targetContentOffset { |
| if (scrollView == self.sharedState.headerView.trackingScrollView) { |
| [self.sharedState.headerView |
| trackingScrollViewWillEndDraggingWithVelocity:velocity |
| targetContentOffset:targetContentOffset]; |
| } |
| } |
| |
| #pragma mark - UITableViewDataSource |
| |
| - (NSInteger)numberOfSectionsInTableView:(UITableView*)tableView { |
| return [self.sharedState.tableViewModel numberOfSections]; |
| } |
| |
| - (NSInteger)tableView:(UITableView*)tableView |
| numberOfRowsInSection:(NSInteger)section { |
| return [self.sharedState.tableViewModel numberOfItemsInSection:section]; |
| } |
| |
| - (UITableViewCell*)tableView:(UITableView*)tableView |
| cellForRowAtIndexPath:(NSIndexPath*)indexPath { |
| TableViewItem* item = |
| [self.sharedState.tableViewModel itemAtIndexPath:indexPath]; |
| Class cellClass = [item cellClass]; |
| NSString* reuseIdentifier = NSStringFromClass(cellClass); |
| [self.sharedState.tableView registerClass:cellClass |
| forCellReuseIdentifier:reuseIdentifier]; |
| UITableViewCell* cell = [self.sharedState.tableView |
| dequeueReusableCellWithIdentifier:reuseIdentifier |
| forIndexPath:indexPath]; |
| [item configureCell:cell withStyler:self.tableViewStyler]; |
| |
| if (item.type == BookmarkHomeItemTypeBookmark) { |
| BookmarkHomeNodeItem* nodeItem = |
| base::mac::ObjCCastStrict<BookmarkHomeNodeItem>(item); |
| BookmarkTableCell* tableCell = |
| base::mac::ObjCCastStrict<BookmarkTableCell>(cell); |
| if (nodeItem.bookmarkNode == self.sharedState.editingFolderNode) { |
| // Delay starting edit, so that the cell is fully created. |
| dispatch_async(dispatch_get_main_queue(), ^{ |
| self.sharedState.editingFolderCell = tableCell; |
| [tableCell startEdit]; |
| tableCell.textDelegate = self; |
| }); |
| } |
| |
| // Cancel previous load attempts. |
| [self cancelLoadingFaviconAtIndexPath:indexPath]; |
| // Load the favicon from cache. If not found, try fetching it from a Google |
| // Server. |
| [self loadFaviconAtIndexPath:indexPath continueToGoogleServer:YES]; |
| } |
| |
| return cell; |
| } |
| |
| - (BOOL)tableView:(UITableView*)tableView |
| canEditRowAtIndexPath:(NSIndexPath*)indexPath { |
| TableViewItem* item = |
| [self.sharedState.tableViewModel itemAtIndexPath:indexPath]; |
| if (item.type != BookmarkHomeItemTypeBookmark) { |
| // Can only edit bookmarks. |
| return NO; |
| } |
| |
| // Enable the swipe-to-delete gesture and reordering control for nodes of |
| // type URL or Folder, but not the permanent ones. |
| BookmarkHomeNodeItem* nodeItem = |
| base::mac::ObjCCastStrict<BookmarkHomeNodeItem>(item); |
| const BookmarkNode* node = nodeItem.bookmarkNode; |
| return [self isUrlOrFolder:node]; |
| } |
| |
| - (void)tableView:(UITableView*)tableView |
| commitEditingStyle:(UITableViewCellEditingStyle)editingStyle |
| forRowAtIndexPath:(NSIndexPath*)indexPath { |
| TableViewItem* item = |
| [self.sharedState.tableViewModel itemAtIndexPath:indexPath]; |
| if (item.type != BookmarkHomeItemTypeBookmark) { |
| // Can only commit edits for bookmarks. |
| return; |
| } |
| |
| if (editingStyle == UITableViewCellEditingStyleDelete) { |
| BookmarkHomeNodeItem* nodeItem = |
| base::mac::ObjCCastStrict<BookmarkHomeNodeItem>(item); |
| const BookmarkNode* node = nodeItem.bookmarkNode; |
| std::set<const BookmarkNode*> nodes; |
| nodes.insert(node); |
| [self bookmarkTableView:self.bookmarksTableView |
| selectedNodesForDeletion:nodes]; |
| } |
| } |
| |
| - (BOOL)tableView:(UITableView*)tableView |
| canMoveRowAtIndexPath:(NSIndexPath*)indexPath { |
| TableViewItem* item = |
| [self.sharedState.tableViewModel itemAtIndexPath:indexPath]; |
| if (item.type != BookmarkHomeItemTypeBookmark) { |
| // Can only move bookmarks. |
| return NO; |
| } |
| |
| return YES; |
| } |
| |
| - (void)tableView:(UITableView*)tableView |
| moveRowAtIndexPath:(NSIndexPath*)sourceIndexPath |
| toIndexPath:(NSIndexPath*)destinationIndexPath { |
| if (sourceIndexPath.row == destinationIndexPath.row) { |
| return; |
| } |
| const BookmarkNode* node = [self nodeAtIndexPath:sourceIndexPath]; |
| // Calculations: Assume we have 3 nodes A B C. Node positions are A(0), B(1), |
| // C(2) respectively. When we move A to after C, we are moving node at index 0 |
| // to 3 (position after C is 3, in terms of the existing contents). Hence add |
| // 1 when moving forward. When moving backward, if C(2) is moved to Before B, |
| // we move node at index 2 to index 1 (position before B is 1, in terms of the |
| // existing contents), hence no change in index is necessary. It is required |
| // to make these adjustments because this is how bookmark_model handles move |
| // operations. |
| int newPosition = sourceIndexPath.row < destinationIndexPath.row |
| ? destinationIndexPath.row + 1 |
| : destinationIndexPath.row; |
| [self bookmarkTableView:self.bookmarksTableView |
| didMoveNode:node |
| toPosition:newPosition]; |
| } |
| |
| #pragma mark - UITableViewDelegate |
| |
| - (CGFloat)tableView:(UITableView*)tableView |
| heightForRowAtIndexPath:(NSIndexPath*)indexPath { |
| NSInteger sectionIdentifier = [self.sharedState.tableViewModel |
| sectionIdentifierForSection:indexPath.section]; |
| if (sectionIdentifier == BookmarkHomeSectionIdentifierBookmarks) { |
| return [BookmarkHomeSharedState cellHeightPt]; |
| } |
| return UITableViewAutomaticDimension; |
| } |
| |
| - (void)tableView:(UITableView*)tableView |
| didSelectRowAtIndexPath:(NSIndexPath*)indexPath { |
| NSInteger sectionIdentifier = [self.sharedState.tableViewModel |
| sectionIdentifierForSection:indexPath.section]; |
| if (sectionIdentifier == BookmarkHomeSectionIdentifierBookmarks) { |
| const BookmarkNode* node = [self nodeAtIndexPath:indexPath]; |
| DCHECK(node); |
| // If table is in edit mode, record all the nodes added to edit set. |
| if (self.sharedState.currentlyInEditMode) { |
| self.sharedState.editNodes.insert(node); |
| [self bookmarkTableView:self.bookmarksTableView |
| selectedEditNodes:self.sharedState.editNodes]; |
| return; |
| } |
| [self.sharedState.editingFolderCell stopEdit]; |
| if (node->is_folder()) { |
| [self bookmarkTableView:self.bookmarksTableView |
| selectedFolderForNavigation:node]; |
| } else { |
| // Open URL. Pass this to the delegate. |
| [self bookmarkTableView:self.bookmarksTableView |
| selectedUrlForNavigation:node->url()]; |
| } |
| } |
| // Deselect row. |
| [tableView deselectRowAtIndexPath:indexPath animated:YES]; |
| } |
| |
| - (void)tableView:(UITableView*)tableView |
| didDeselectRowAtIndexPath:(NSIndexPath*)indexPath { |
| NSInteger sectionIdentifier = [self.sharedState.tableViewModel |
| sectionIdentifierForSection:indexPath.section]; |
| if (sectionIdentifier == BookmarkHomeSectionIdentifierBookmarks && |
| self.sharedState.currentlyInEditMode) { |
| const BookmarkNode* node = [self nodeAtIndexPath:indexPath]; |
| DCHECK(node); |
| self.sharedState.editNodes.erase(node); |
| [self bookmarkTableView:self.bookmarksTableView |
| selectedEditNodes:self.sharedState.editNodes]; |
| } |
| } |
| |
| @end |