blob: 512d886cbe39a4bd47f11c772bf8521caa2769ad [file] [log] [blame]
// 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/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/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"
#import "ios/chrome/browser/favicon/favicon_loader.h"
#include "ios/chrome/browser/favicon/ios_chrome_favicon_loader_factory.h"
#import "ios/chrome/browser/metrics/new_tab_page_uma.h"
#import "ios/chrome/browser/ui/alert_coordinator/action_sheet_coordinator.h"
#import "ios/chrome/browser/ui/alert_coordinator/alert_coordinator.h"
#import "ios/chrome/browser/ui/authentication/cells/signin_promo_view_configurator.h"
#import "ios/chrome/browser/ui/bookmarks/bookmark_edit_view_controller.h"
#include "ios/chrome/browser/ui/bookmarks/bookmark_empty_background.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_interaction_controller.h"
#import "ios/chrome/browser/ui/bookmarks/bookmark_interaction_controller_delegate.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_ui_constants.h"
#import "ios/chrome/browser/ui/bookmarks/bookmark_utils_ios.h"
#import "ios/chrome/browser/ui/bookmarks/cells/bookmark_folder_item.h"
#import "ios/chrome/browser/ui/bookmarks/cells/bookmark_home_node_item.h"
#import "ios/chrome/browser/ui/bookmarks/cells/bookmark_table_cell_title_edit_delegate.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/keyboard/UIKeyCommand+Chrome.h"
#import "ios/chrome/browser/ui/material_components/utils.h"
#import "ios/chrome/browser/ui/table_view/cells/table_view_url_item.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/table_view/table_view_navigation_controller_constants.h"
#import "ios/chrome/browser/ui/url_loader.h"
#import "ios/chrome/browser/ui/util/rtl_geometry.h"
#import "ios/chrome/browser/ui/util/ui_util.h"
#import "ios/chrome/browser/ui/util/uikit_ui_util.h"
#import "ios/chrome/common/favicon/favicon_attributes.h"
#import "ios/chrome/common/favicon/favicon_view.h"
#import "ios/chrome/common/ui_util/constraints_ui_util.h"
#include "ios/chrome/grit/ios_strings.h"
#import "ios/web/public/navigation_manager.h"
#include "ios/web/public/referrer.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.
};
// Estimated TableView row height.
const CGFloat kEstimatedRowHeight = 65.0;
// TableView rows that are hidden by the NavigationBar, causing them to be
// "visible" for the tableView but not for the user. This is used to calculate
// the top most visibile table view indexPath row.
// TODO(crbug.com/879001): This value is aproximate based on the standard (no
// dynamic type) height. If the dynamic font is too large or too small it will
// result in a small offset on the cache, in order to prevent this we need to
// calculate this value dynamically.
const int kRowsHiddenByNavigationBar = 3;
// 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 ()<BookmarkFolderViewControllerDelegate,
BookmarkHomeConsumer,
BookmarkHomeSharedStateObserver,
BookmarkInteractionControllerDelegate,
BookmarkModelBridgeObserver,
BookmarkTableCellTitleEditDelegate,
UIGestureRecognizerDelegate,
UISearchControllerDelegate,
UISearchResultsUpdating,
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;
}
// 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 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;
// FaviconLoader is a keyed service that uses LargeIconService to retrieve
// favicon images.
@property(nonatomic, assign) FaviconLoader* faviconLoader;
// 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 top most visible indexPath row of the table view. This
// property is set to nil after it is used.
@property(nonatomic, assign) int cachedIndexPathRow;
// Set to YES, only when this view controller instance is being created
// from cached path. Once the view controller is shown, this is set to NO.
// This is so that the cache code is called only once in loadBookmarkViews.
@property(nonatomic, assign) BOOL isReconstructingFromCache;
// Dispatcher for sending commands.
@property(nonatomic, readonly, weak) id<ApplicationCommands> dispatcher;
// The current search term. Set to the empty string when no search is active.
@property(nonatomic, copy) NSString* searchTerm;
// This ViewController's searchController;
@property(nonatomic, strong) UISearchController* searchController;
// Navigation UIToolbar Delete button.
@property(nonatomic, strong) UIBarButtonItem* deleteButton;
// Navigation UIToolbar More button.
@property(nonatomic, strong) UIBarButtonItem* moreButton;
// Scrim when search box in focused.
@property(nonatomic, strong) UIControl* scrimView;
// Background shown when there is no bookmarks or folders at the current root
// node.
@property(nonatomic, strong) BookmarkEmptyBackground* emptyTableBackgroundView;
// The loading spinner background which appears when loading the BookmarkModel
// or syncing.
@property(nonatomic, strong) BookmarkHomeWaitingView* spinnerView;
// The action sheet coordinator, if one is currently being shown.
@property(nonatomic, strong) AlertCoordinator* actionSheetCoordinator;
@property(nonatomic, strong)
BookmarkInteractionController* bookmarkInteractionController;
@end
@implementation BookmarkHomeViewController
@synthesize bookmarks = _bookmarks;
@synthesize browserState = _browserState;
@synthesize folderSelector = _folderSelector;
@synthesize loader = _loader;
@synthesize homeDelegate = _homeDelegate;
@synthesize contextBarState = _contextBarState;
@synthesize dispatcher = _dispatcher;
@synthesize cachedIndexPathRow = _cachedIndexPathRow;
@synthesize isReconstructingFromCache = _isReconstructingFromCache;
@synthesize sharedState = _sharedState;
@synthesize mediator = _mediator;
@synthesize searchController = _searchController;
@synthesize searchTerm = _searchTerm;
@synthesize deleteButton = _deleteButton;
@synthesize moreButton = _moreButton;
@synthesize scrimView = _scrimView;
@synthesize spinnerView = _spinnerView;
@synthesize emptyTableBackgroundView = _emptyTableBackgroundView;
@synthesize actionSheetCoordinator = _actionSheetCoordinator;
@synthesize bookmarkInteractionController = _bookmarkInteractionController;
#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 initWithTableViewStyle:UITableViewStylePlain
appBarStyle:ChromeTableViewControllerStyleNoAppBar];
if (self) {
_browserState = browserState->GetOriginalChromeBrowserState();
_loader = loader;
_dispatcher = dispatcher;
_faviconLoader =
IOSChromeFaviconLoaderFactory::GetForBrowserState(_browserState);
_bookmarks = ios::BookmarkModelFactory::GetForBrowserState(browserState);
_bridge.reset(new bookmarks::BookmarkModelBridge(self, _bookmarks));
}
return self;
}
- (void)dealloc {
[self.mediator disconnect];
_sharedState.tableView.dataSource = nil;
_sharedState.tableView.delegate = nil;
}
- (void)setRootNode:(const bookmarks::BookmarkNode*)rootNode {
_rootNode = rootNode;
}
- (NSArray<BookmarkHomeViewController*>*)cachedViewControllerStack {
// This method is only designed to be called for the view controller
// associated with the root node.
DCHECK(self.bookmarks->loaded());
DCHECK_EQ(_rootNode, self.bookmarks->root_node());
NSMutableArray<BookmarkHomeViewController*>* stack = [NSMutableArray array];
// Configure the root controller Navigationbar at this time when
// reconstructing from cache, or there will be a loading flicker if this gets
// done on viewDidLoad.
[self setupNavigationForBookmarkHomeViewController:self
usingBookmarkNode:_rootNode];
[stack addObject:self];
int64_t cachedFolderID;
int cachedIndexPathRow;
// If cache is present then reconstruct the last visited bookmark from
// cache.
if (![BookmarkPathCache
getBookmarkTopMostRowCacheWithPrefService:self.browserState
->GetPrefs()
model:self.bookmarks
folderId:&cachedFolderID
topMostRow:&cachedIndexPathRow] ||
cachedFolderID == self.bookmarks->root_node()->id()) {
return stack;
}
NSArray* path =
bookmark_utils_ios::CreateBookmarkPath(self.bookmarks, cachedFolderID);
if (!path) {
return stack;
}
DCHECK_EQ(self.bookmarks->root_node()->id(),
[[path firstObject] longLongValue]);
for (NSUInteger ii = 1; ii < [path count]; ii++) {
int64_t nodeID = [[path objectAtIndex:ii] longLongValue];
const BookmarkNode* node =
bookmark_utils_ios::FindFolderById(self.bookmarks, nodeID);
DCHECK(node);
// if node is an empty permanent node, stop.
if (node->empty() && IsPrimaryPermanentNode(node, self.bookmarks)) {
break;
}
BookmarkHomeViewController* controller =
[self createControllerWithRootFolder:node];
// Configure the controller's Navigationbar at this time when
// reconstructing from cache, or there will be a loading flicker if this
// gets done on viewDidLoad.
[self setupNavigationForBookmarkHomeViewController:controller
usingBookmarkNode:node];
if (nodeID == cachedFolderID) {
controller.cachedIndexPathRow = cachedIndexPathRow;
}
[stack addObject:controller];
}
return stack;
}
#pragma mark - UIViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Set Navigation Bar, Toolbar and TableView appearance.
self.navigationController.navigationBarHidden = NO;
self.navigationController.toolbar.translucent = YES;
// Add a tableFooterView in order to disable separators at the bottom of the
// tableView.
self.tableView.tableFooterView = [[UIView alloc] init];
self.navigationController.toolbar.accessibilityIdentifier =
kBookmarkHomeUIToolbarIdentifier;
// SearchController Configuration.
// Init the searchController with nil so the results are displayed on the
// same TableView.
self.searchController =
[[UISearchController alloc] initWithSearchResultsController:nil];
self.searchController.dimsBackgroundDuringPresentation = NO;
self.searchController.searchBar.userInteractionEnabled = NO;
self.searchController.delegate = self;
self.searchController.searchResultsUpdater = self;
self.searchController.searchBar.backgroundColor = [UIColor clearColor];
self.searchController.searchBar.accessibilityIdentifier =
kBookmarkHomeSearchBarIdentifier;
// UIKit needs to know which controller will be presenting the
// searchController. If we don't add this trying to dismiss while
// SearchController is active will fail.
self.definesPresentationContext = YES;
self.scrimView = [[UIControl alloc] init];
self.scrimView.backgroundColor =
[UIColor colorWithWhite:0
alpha:kTableViewNavigationWhiteAlphaForSearchScrim];
self.scrimView.translatesAutoresizingMaskIntoConstraints = NO;
self.scrimView.accessibilityIdentifier = kBookmarkHomeSearchScrimIdentifier;
[self.scrimView addTarget:self
action:@selector(dismissSearchController:)
forControlEvents:UIControlEventAllTouchEvents];
// Place the search bar in the navigation bar.
self.navigationItem.searchController = self.searchController;
self.navigationItem.hidesSearchBarWhenScrolling = NO;
// Center search bar vertically so it looks centered in the header when
// searching. The cancel button is centered / decentered on
// viewWillAppear and viewDidDisappear.
UIOffset offset =
UIOffsetMake(0.0f, kTableViewNavigationVerticalOffsetForSearchHeader);
self.searchController.searchBar.searchFieldBackgroundPositionAdjustment =
offset;
self.searchTerm = @"";
if (self.bookmarks->loaded()) {
[self loadBookmarkViews];
} else {
[self showLoadingSpinnerBackground];
}
}
- (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;
// Hide the toolbar if we're displaying the root node.
if (self.bookmarks->loaded() &&
(_rootNode != self.bookmarks->root_node() ||
self.sharedState.currentlyShowingSearchResults)) {
self.navigationController.toolbarHidden = NO;
} else {
self.navigationController.toolbarHidden = YES;
}
// Center search bar's cancel button vertically so it looks centered.
// We change the cancel button proxy styles, so we will return it to
// default in viewDidDisappear.
UIOffset offset =
UIOffsetMake(0.0f, kTableViewNavigationVerticalOffsetForSearchHeader);
UIBarButtonItem* cancelButton = [UIBarButtonItem
appearanceWhenContainedInInstancesOfClasses:@[ [UISearchBar class] ]];
[cancelButton setTitlePositionAdjustment:offset
forBarMetrics:UIBarMetricsDefault];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
// Restore to default origin offset for cancel button proxy style.
UIBarButtonItem* cancelButton = [UIBarButtonItem
appearanceWhenContainedInInstancesOfClasses:@[ [UISearchBar class] ]];
[cancelButton setTitlePositionAdjustment:UIOffsetZero
forBarMetrics:UIBarMetricsDefault];
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
// Check that the tableView still contains as many rows, and that
// |self.cachedIndexPathRow| is not 0.
if (self.cachedIndexPathRow &&
[self.tableView numberOfRowsInSection:0] > self.cachedIndexPathRow) {
NSIndexPath* indexPath =
[NSIndexPath indexPathForRow:self.cachedIndexPathRow inSection:0];
[self.tableView scrollToRowAtIndexPath:indexPath
atScrollPosition:UITableViewScrollPositionTop
animated:NO];
self.cachedIndexPathRow = 0;
}
}
- (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 loadModel];
self.sharedState =
[[BookmarkHomeSharedState alloc] initWithBookmarkModel:_bookmarks
displayedRootNode:_rootNode];
self.sharedState.tableViewModel = self.tableViewModel;
self.sharedState.tableView = self.tableView;
self.sharedState.observer = self;
self.sharedState.currentlyShowingSearchResults = NO;
// Configure the table view.
self.sharedState.tableView.accessibilityIdentifier = @"bookmarksTableView";
self.sharedState.tableView.estimatedRowHeight = kEstimatedRowHeight;
self.tableView.sectionHeaderHeight = 0;
// Setting a sectionFooterHeight of 0 will be the same as not having a
// footerView, which shows a cell separator for the last cell. Removing this
// line will also create a default footer of height 30.
self.tableView.sectionFooterHeight = 1;
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 setupNavigationForBookmarkHomeViewController:self
usingBookmarkNode:_rootNode];
[self setupContextBar];
if (self.isReconstructingFromCache) {
[self setupUIStackCacheIfApplicable];
}
self.searchController.searchBar.userInteractionEnabled = YES;
DCHECK(self.bookmarks->loaded());
DCHECK([self isViewLoaded]);
}
- (void)cacheIndexPathRow {
// Cache IndexPathRow for BookmarkTableView.
int topMostVisibleIndexPathRow = [self topMostVisibleIndexPathRow];
[BookmarkPathCache
cacheBookmarkTopMostRowWithPrefService:self.browserState->GetPrefs()
folderId:_rootNode->id()
topMostRow:topMostVisibleIndexPathRow];
}
#pragma mark - BookmarkHomeConsumer
- (void)refreshContents {
if (self.sharedState.currentlyShowingSearchResults) {
NSString* noResults = l10n_util::GetNSString(IDS_HISTORY_NO_SEARCH_RESULTS);
[self.mediator computeBookmarkTableViewDataMatching:self.searchTerm
orShowMessageWhenNoResults:noResults];
} else {
[self.mediator computeBookmarkTableViewData];
}
[self handleRefreshContextBar];
[self.sharedState.editingFolderCell stopEdit];
[self.sharedState.tableView reloadData];
if (self.sharedState.currentlyInEditMode &&
!self.sharedState.editNodes.empty()) {
[self restoreRowSelection];
}
}
- (void)loadFaviconAtIndexPath:(NSIndexPath*)indexPath
fallbackToGoogleServer:(BOOL)fallbackToGoogleServer {
UITableViewCell* cell =
[self.sharedState.tableView cellForRowAtIndexPath:indexPath];
[self loadFaviconAtIndexPath:indexPath
forCell:cell
fallbackToGoogleServer:fallbackToGoogleServer];
}
// 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 |fallbackToGoogleServer| is YES,
// otherwise, use the fall back icon style.
- (void)loadFaviconAtIndexPath:(NSIndexPath*)indexPath
forCell:(UITableViewCell*)cell
fallbackToGoogleServer:(BOOL)fallbackToGoogleServer {
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());
auto faviconLoadedBlock = ^(FaviconAttributes* attributes) {
BookmarkHomeViewController* strongSelf = weakSelf;
if (!strongSelf) {
return;
}
// Due to search filtering, we also need to validate the indexPath
// requested versus what is in the table now.
if (![strongSelf hasItemAtIndexPath:indexPath] ||
[strongSelf nodeAtIndexPath:indexPath] != node) {
return;
}
TableViewURLCell* URLCell =
base::mac::ObjCCastStrict<TableViewURLCell>(cell);
[URLCell.faviconView configureWithAttributes:attributes];
};
FaviconAttributes* cachedAttributes = self.faviconLoader->FaviconForUrl(
blockURL, minFaviconSizeInPixel, desiredFaviconSizeInPixel,
/*fallback_to_google_server=*/fallbackToGoogleServer, faviconLoadedBlock);
DCHECK(cachedAttributes);
faviconLoadedBlock(cachedAttributes);
}
- (void)updateTableViewBackgroundStyle:(BookmarkHomeBackgroundStyle)style {
if (style == BookmarkHomeBackgroundStyleDefault) {
[self hideLoadingSpinnerBackground];
[self hideEmptyBackground];
} else if (style == BookmarkHomeBackgroundStyleLoading) {
[self hideEmptyBackground];
[self showLoadingSpinnerBackground];
} else if (style == BookmarkHomeBackgroundStyleEmpty) {
[self hideLoadingSpinnerBackground];
[self showEmptyBackground];
}
}
- (void)showSignin:(ShowSigninCommand*)command {
[self.dispatcher showSignin:command
baseViewController:self.navigationController];
}
- (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 {
if (!self.bookmarkInteractionController) {
self.bookmarkInteractionController = [[BookmarkInteractionController alloc]
initWithBrowserState:self.browserState
loader:self.loader
parentController:self
dispatcher:self.dispatcher];
self.bookmarkInteractionController.delegate = self;
}
[self.bookmarkInteractionController presentEditorForNode:node];
}
- (void)openAllNodes:(const std::vector<const bookmarks::BookmarkNode*>&)nodes
inIncognito:(BOOL)inIncognito
newTab:(BOOL)newTab {
[self cacheIndexPathRow];
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 - More Private Methods
- (void)handleSelectUrlForNavigation:(const GURL&)url {
[self dismissWithURL:url];
}
- (void)handleSelectFolderForNavigation:(const bookmarks::BookmarkNode*)folder {
if (self.sharedState.currentlyShowingSearchResults) {
// Clear bookmark path cache.
int64_t unusedFolderId;
int unusedIndexPathRow;
while ([BookmarkPathCache
getBookmarkTopMostRowCacheWithPrefService:self.browserState->GetPrefs()
model:self.bookmarks
folderId:&unusedFolderId
topMostRow:&unusedIndexPathRow]) {
[BookmarkPathCache
clearBookmarkTopMostRowCacheWithPrefService:self.browserState
->GetPrefs()];
}
// Rebuild folder controller list, going back up the tree.
NSMutableArray<BookmarkHomeViewController*>* stack = [NSMutableArray array];
std::vector<const bookmarks::BookmarkNode*> nodes;
const bookmarks::BookmarkNode* cursor = folder;
while (cursor) {
// Build reversed list of nodes to restore bookmark path below.
nodes.insert(nodes.begin(), cursor);
// Build reversed list of controllers.
BookmarkHomeViewController* controller =
[self createControllerWithRootFolder:cursor];
[stack insertObject:controller atIndex:0];
// Setup now, so that the back button labels shows parent folder
// title and that we don't show large title everywhere.
[self setupNavigationForBookmarkHomeViewController:controller
usingBookmarkNode:cursor];
cursor = cursor->parent();
}
// Reconstruct bookmark path cache.
for (const bookmarks::BookmarkNode* node : nodes) {
[BookmarkPathCache
cacheBookmarkTopMostRowWithPrefService:self.browserState->GetPrefs()
folderId:node->id()
topMostRow:0];
}
[self navigateAway];
// At root, since there's a large title, the search bar is lower than on
// whatever destination folder it is transitioning to (root is never
// reachable through search). To avoid a kink in the animation, the title
// is set to regular size, which means the search bar is at same level at
// beginning and end of animation. This controller will be replaced in
// |stack| so there's no need to care about restoring this.
if (_rootNode == self.bookmarks->root_node()) {
self.navigationItem.largeTitleDisplayMode =
UINavigationItemLargeTitleDisplayModeNever;
}
auto completion = ^{
[self.navigationController setViewControllers:stack animated:YES];
};
[self.searchController dismissViewControllerAnimated:YES
completion:completion];
return;
}
BookmarkHomeViewController* controller =
[self createControllerWithRootFolder:folder];
[self.navigationController pushViewController:controller animated:YES];
}
- (void)handleSelectNodesForDeletion:
(const std::set<const bookmarks::BookmarkNode*>&)nodes {
[self deleteNodes:nodes];
}
- (void)handleSelectEditNodes:
(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)handleMoveNode:(const bookmarks::BookmarkNode*)node
toPosition:(int)position {
bookmark_utils_ios::UpdateBookmarkPositionWithUndoToast(
node, _rootNode, position, self.bookmarks, self.browserState);
}
- (void)handleRefreshContextBar {
// At default state, the enable state of context bar buttons could change
// during refresh.
if (self.contextBarState == BookmarksContextBarDefault) {
[self setBookmarksContextBarButtonsDefaultState];
}
}
- (BOOL)isAtTopOfNavigation {
return (self.navigationController.topViewController == self);
}
#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.navigationController dismissViewControllerAnimated:YES completion:NULL];
self.folderSelector.delegate = nil;
self.folderSelector = nil;
}
- (void)folderPickerDidCancel:(BookmarkFolderViewController*)folderPicker {
[self setTableViewEditing:NO];
[self.navigationController dismissViewControllerAnimated:YES completion:NULL];
self.folderSelector.delegate = nil;
self.folderSelector = nil;
}
#pragma mark - BookmarkInteractionControllerDelegate
- (void)bookmarkInteractionControllerWillCommitTitleOrUrlChange:
(BookmarkInteractionController*)controller {
[self setTableViewEditing:NO];
}
- (void)bookmarkInteractionControllerDidStop:
(BookmarkInteractionController*)controller {
// TODO(crbug.com/805182): Use this method to tear down
// |self.bookmarkInteractionController|.
}
#pragma mark - BookmarkModelBridgeObserver
- (void)bookmarkModelLoaded {
DCHECK(!_rootNode);
[self setRootNode:self.bookmarks->root_node()];
// If the view hasn't loaded yet, then return early. The eventual call to
// viewDidLoad will properly initialize the views. This early return must
// come *after* the call to setRootNode above.
if (![self isViewLoaded])
return;
int64_t unusedFolderId;
int unusedIndexPathRow;
// 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
getBookmarkTopMostRowCacheWithPrefService:self.browserState
->GetPrefs()
model:self.bookmarks
folderId:&unusedFolderId
topMostRow:&unusedIndexPathRow]) {
self.isReconstructingFromCache = YES;
}
DCHECK(self.spinnerView);
__weak BookmarkHomeViewController* weakSelf = self;
[self.spinnerView stopWaitingWithCompletion:^{
BookmarkHomeViewController* strongSelf = weakSelf;
// Early return if the controller has been deallocated.
if (!strongSelf)
return;
[UIView animateWithDuration:0.2f
animations:^{
strongSelf.spinnerView.alpha = 0.0;
}
completion:^(BOOL finished) {
self.sharedState.tableView.backgroundView = nil;
self.spinnerView = nil;
}];
[strongSelf loadBookmarkViews];
[strongSelf.sharedState.tableView reloadData];
}];
}
- (void)bookmarkNodeChanged:(const BookmarkNode*)node {
// No-op here. Bookmarks might be refreshed in BookmarkHomeMediator.
}
- (void)bookmarkNodeChildrenChanged:(const BookmarkNode*)bookmarkNode {
// No-op here. Bookmarks might be refreshed in BookmarkHomeMediator.
}
- (void)bookmarkNode:(const BookmarkNode*)bookmarkNode
movedFromParent:(const BookmarkNode*)oldParent
toParent:(const BookmarkNode*)newParent {
// No-op here. Bookmarks might be refreshed in BookmarkHomeMediator.
}
- (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
// Check if any of our controller is presenting. We don't consider when this
// controller is presenting the search controller.
// Note that when adding a controller that can present, it should be added in
// context here.
- (BOOL)isAnyControllerPresenting {
return (([self presentedViewController] &&
[self presentedViewController] != self.searchController) ||
[self.searchController presentedViewController] ||
[self.navigationController presentedViewController]);
}
- (void)setupUIStackCacheIfApplicable {
self.isReconstructingFromCache = NO;
NSArray<BookmarkHomeViewController*>* replacementViewControllers =
[self cachedViewControllerStack];
DCHECK(replacementViewControllers);
[self.navigationController setViewControllers:replacementViewControllers];
}
// Set up context bar for the new UI.
- (void)setupContextBar {
if (_rootNode != self.bookmarks->root_node() ||
self.sharedState.currentlyShowingSearchResults) {
self.navigationController.toolbarHidden = NO;
[self setContextBarState:BookmarksContextBarDefault];
} else {
self.navigationController.toolbarHidden = YES;
}
}
// Set up navigation bar for |viewController|'s navigationBar using |node|.
- (void)setupNavigationForBookmarkHomeViewController:
(BookmarkHomeViewController*)viewController
usingBookmarkNode:
(const bookmarks::BookmarkNode*)node {
viewController.navigationItem.leftBarButtonItem.action = @selector(back);
// Disable large titles on every VC but the root controller.
if (node != self.bookmarks->root_node()) {
viewController.navigationItem.largeTitleDisplayMode =
UINavigationItemLargeTitleDisplayModeNever;
}
// Add custom title.
viewController.title = bookmark_utils_ios::TitleForBookmarkNode(node);
// Add custom done button.
viewController.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);
doneButton.accessibilityIdentifier =
kBookmarkHomeNavigationBarDoneButtonIdentifier;
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 cacheIndexPathRow];
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"));
web::NavigationManager::WebLoadParams params(url);
params.transition_type = ui::PAGE_TRANSITION_AUTO_BOOKMARK;
ChromeLoadParams chromeParams(params);
[self.loader loadURLWithParams:chromeParams];
}
- (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 and search state
// accordingly.
- (void)setTableViewEditing:(BOOL)editing {
self.sharedState.currentlyInEditMode = editing;
[self setContextBarState:editing ? BookmarksContextBarBeginSelection
: BookmarksContextBarDefault];
self.searchController.searchBar.userInteractionEnabled = !editing;
self.searchController.searchBar.alpha =
editing ? kTableViewNavigationAlphaForDisabledSearchBar : 1.0;
}
// 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.
if (self.sharedState.editNodes.size() != newEditNodes.size()) {
self.sharedState.editNodes = newEditNodes;
[self handleSelectEditNodes:self.sharedState.editNodes];
}
}
- (BOOL)allowsNewFolder {
// When the current root node has been removed remotely (becomes NULL),
// or when displaying search results, creating new folder is forbidden.
return self.sharedState.tableViewDisplayedRootNode != NULL &&
!self.sharedState.currentlyShowingSearchResults;
}
- (int)topMostVisibleIndexPathRow {
// If on root node screen, return 0.
if (self.sharedState.tableViewDisplayedRootNode ==
self.sharedState.bookmarkModel->root_node()) {
return 0;
}
// If no rows in table, return 0.
NSArray* visibleIndexPaths = [self.tableView indexPathsForVisibleRows];
if (!visibleIndexPaths.count)
return 0;
// If the first row is still visible, return 0.
NSIndexPath* topMostIndexPath = [visibleIndexPaths objectAtIndex:0];
if (topMostIndexPath.row == 0)
return 0;
// Return the first visible row not covered by the NavigationBar.
topMostIndexPath =
[visibleIndexPaths objectAtIndex:kRowsHiddenByNavigationBar];
return topMostIndexPath.row;
}
- (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)hasItemAtIndexPath:(NSIndexPath*)indexPath {
return [self.sharedState.tableViewModel hasItemAtIndexPath:indexPath];
}
- (BOOL)hasBookmarksOrFolders {
if (!self.sharedState.tableViewDisplayedRootNode)
return NO;
if (self.sharedState.currentlyShowingSearchResults) {
return [self
hasItemsInSectionIdentifier:BookmarkHomeSectionIdentifierBookmarks];
} else {
return !self.sharedState.tableViewDisplayedRootNode->empty();
}
}
- (BOOL)hasItemsInSectionIdentifier:(NSInteger)sectionIdentifier {
BOOL hasSection = [self.sharedState.tableViewModel
hasSectionForSectionIdentifier:sectionIdentifier];
if (!hasSection)
return NO;
NSInteger section = [self.sharedState.tableViewModel
sectionForSectionIdentifier:sectionIdentifier];
return [self.sharedState.tableViewModel numberOfItemsInSection:section] > 0;
}
- (std::vector<const bookmarks::BookmarkNode*>)getEditNodesInVector {
std::vector<const bookmarks::BookmarkNode*> nodes;
if (self.sharedState.currentlyShowingSearchResults) {
// Create a vector of edit nodes in the same order as the selected nodes.
const std::set<const bookmarks::BookmarkNode*> editNodes =
self.sharedState.editNodes;
std::copy(editNodes.begin(), editNodes.end(), std::back_inserter(nodes));
} else {
// Create a vector of edit nodes in the same order as the nodes in folder.
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;
}
// Dismiss the search controller when there's a touch event on the scrim.
- (void)dismissSearchController:(UIControl*)sender {
if (self.searchController.active) {
self.searchController.active = NO;
}
}
// Show scrim overlay and hide toolbar.
- (void)showScrim {
self.navigationController.toolbarHidden = YES;
self.scrimView.alpha = 0.0f;
[self.tableView addSubview:self.scrimView];
// We attach our constraints to the superview because the tableView is
// a scrollView and it seems that we get an empty frame when attaching to it.
AddSameConstraints(self.scrimView, self.view.superview);
self.tableView.scrollEnabled = NO;
[UIView animateWithDuration:kTableViewNavigationScrimFadeDuration
animations:^{
self.scrimView.alpha = 1.0f;
[self.view layoutIfNeeded];
}];
}
// Hide scrim and restore toolbar.
- (void)hideScrim {
[UIView animateWithDuration:kTableViewNavigationScrimFadeDuration
animations:^{
self.scrimView.alpha = 0.0f;
}
completion:^(BOOL finished) {
[self.scrimView removeFromSuperview];
self.tableView.scrollEnabled = YES;
}];
[self setupContextBar];
}
#pragma mark - Loading and Empty States
// Shows loading spinner background view.
- (void)showLoadingSpinnerBackground {
if (!self.spinnerView) {
self.spinnerView = [[BookmarkHomeWaitingView alloc]
initWithFrame:self.sharedState.tableView.bounds
backgroundColor:[UIColor clearColor]];
[self.spinnerView startWaiting];
}
self.tableView.backgroundView = self.spinnerView;
}
// Hide the loading spinner if it is showing.
- (void)hideLoadingSpinnerBackground {
if (self.spinnerView) {
[self.spinnerView stopWaitingWithCompletion:^{
[UIView animateWithDuration:0.2
animations:^{
self.spinnerView.alpha = 0.0;
}
completion:^(BOOL finished) {
self.sharedState.tableView.backgroundView = nil;
self.spinnerView = nil;
}];
}];
}
}
// Shows empty bookmarks background view.
- (void)showEmptyBackground {
if (!self.emptyTableBackgroundView) {
// Set up the background view shown when the table is empty.
self.emptyTableBackgroundView = [[BookmarkEmptyBackground alloc]
initWithFrame:self.sharedState.tableView.bounds];
self.emptyTableBackgroundView.autoresizingMask =
UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
self.emptyTableBackgroundView.text =
l10n_util::GetNSString(IDS_IOS_BOOKMARK_NO_BOOKMARKS_LABEL);
self.emptyTableBackgroundView.frame = self.sharedState.tableView.bounds;
}
self.sharedState.tableView.backgroundView = self.emptyTableBackgroundView;
}
- (void)hideEmptyBackground {
self.sharedState.tableView.backgroundView = nil;
}
#pragma mark - ContextBarDelegate implementation
// Called when the leading button is clicked.
- (void)leadingButtonClicked {
// Ignore the button tap if any of our controllers is presenting.
if ([self isAnyControllerPresenting]) {
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];
base::RecordAction(
base::UserMetricsAction("MobileBookmarkManagerRemoveSelected"));
break;
case BookmarksContextBarNone:
default:
NOTREACHED();
}
}
// Called when the center button is clicked.
- (void)centerButtonClicked {
// Ignore the button tap if any of our controller is presenting.
if ([self isAnyControllerPresenting]) {
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);
self.actionSheetCoordinator = [[ActionSheetCoordinator alloc]
initWithBaseViewController:self
title:nil
message:nil
barButtonItem:self.moreButton];
switch (self.contextBarState) {
case BookmarksContextBarSingleURLSelection:
[self configureCoordinator:self.actionSheetCoordinator
forSingleBookmarkURL:*(nodes.begin())];
break;
case BookmarksContextBarMultipleURLSelection:
[self configureCoordinator:self.actionSheetCoordinator
forMultipleBookmarkURLs:nodes];
break;
case BookmarksContextBarSingleFolderSelection:
[self configureCoordinator:self.actionSheetCoordinator
forSingleBookmarkFolder:*(nodes.begin())];
break;
case BookmarksContextBarMultipleFolderSelection:
case BookmarksContextBarMixedSelection:
[self configureCoordinator:self.actionSheetCoordinator
forMixedAndMultiFolderSelection:nodes];
break;
case BookmarksContextBarDefault:
case BookmarksContextBarBeginSelection:
case BookmarksContextBarNone:
// Center button is disabled in these states.
NOTREACHED();
break;
}
[self.actionSheetCoordinator start];
}
// Called when the trailing button, "Select" or "Cancel" is clicked.
- (void)trailingButtonClicked {
// Ignore the button tap if any of our controller is presenting.
if ([self isAnyControllerPresenting]) {
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.moreButton.enabled = YES;
self.deleteButton.enabled = YES;
break;
case BookmarksContextBarNone:
default:
break;
}
}
- (void)setBookmarksContextBarButtonsDefaultState {
// Set New Folder button
NSString* titleString =
l10n_util::GetNSString(IDS_IOS_BOOKMARK_CONTEXT_BAR_NEW_FOLDER);
UIBarButtonItem* newFolderButton =
[[UIBarButtonItem alloc] initWithTitle:titleString
style:UIBarButtonItemStylePlain
target:self
action:@selector(leadingButtonClicked)];
newFolderButton.accessibilityIdentifier =
kBookmarkHomeLeadingButtonIdentifier;
newFolderButton.enabled = [self allowsNewFolder];
// Spacer button.
UIBarButtonItem* spaceButton = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace
target:nil
action:nil];
// Set Edit button.
titleString = l10n_util::GetNSString(IDS_IOS_BOOKMARK_CONTEXT_BAR_EDIT);
UIBarButtonItem* editButton =
[[UIBarButtonItem alloc] initWithTitle:titleString
style:UIBarButtonItemStylePlain
target:self
action:@selector(trailingButtonClicked)];
editButton.accessibilityIdentifier = kBookmarkHomeTrailingButtonIdentifier;
editButton.enabled = [self hasBookmarksOrFolders];
[self setToolbarItems:@[ newFolderButton, spaceButton, editButton ]
animated:NO];
}
- (void)setBookmarksContextBarSelectionStartState {
// Disabled Delete button.
NSString* titleString =
l10n_util::GetNSString(IDS_IOS_BOOKMARK_CONTEXT_BAR_DELETE);
self.deleteButton =
[[UIBarButtonItem alloc] initWithTitle:titleString
style:UIBarButtonItemStylePlain
target:self
action:@selector(leadingButtonClicked)];
self.deleteButton.tintColor = [UIColor redColor];
self.deleteButton.enabled = NO;
self.deleteButton.accessibilityIdentifier =
kBookmarkHomeLeadingButtonIdentifier;
// Disabled More button.
titleString = l10n_util::GetNSString(IDS_IOS_BOOKMARK_CONTEXT_BAR_MORE);
self.moreButton =
[[UIBarButtonItem alloc] initWithTitle:titleString
style:UIBarButtonItemStylePlain
target:self
action:@selector(centerButtonClicked)];
self.moreButton.enabled = NO;
self.moreButton.accessibilityIdentifier = kBookmarkHomeCenterButtonIdentifier;
// Enabled Cancel button.
titleString = l10n_util::GetNSString(IDS_CANCEL);
UIBarButtonItem* cancelButton =
[[UIBarButtonItem alloc] initWithTitle:titleString
style:UIBarButtonItemStylePlain
target:self
action:@selector(trailingButtonClicked)];
cancelButton.accessibilityIdentifier = kBookmarkHomeTrailingButtonIdentifier;
// Spacer button.
UIBarButtonItem* spaceButton = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace
target:nil
action:nil];
[self setToolbarItems:@[
self.deleteButton, spaceButton, self.moreButton, spaceButton, cancelButton
]
animated:NO];
}
#pragma mark - Context Menu
- (void)configureCoordinator:(AlertCoordinator*)coordinator
forMultipleBookmarkURLs:(const std::set<const BookmarkNode*>)nodes {
__weak BookmarkHomeViewController* weakSelf = self;
coordinator.alertController.view.accessibilityIdentifier =
@"bookmark_context_menu";
[coordinator
addItemWithTitle:l10n_util::GetNSString(
IDS_IOS_BOOKMARK_CONTEXT_MENU_OPEN)
action:^{
std::vector<const BookmarkNode*> nodes =
[weakSelf getEditNodesInVector];
[weakSelf openAllNodes:nodes inIncognito:NO newTab:NO];
}
style:UIAlertActionStyleDefault];
[coordinator
addItemWithTitle:l10n_util::GetNSString(
IDS_IOS_BOOKMARK_CONTEXT_MENU_OPEN_INCOGNITO)
action:^{
std::vector<const BookmarkNode*> nodes =
[weakSelf getEditNodesInVector];
[weakSelf openAllNodes:nodes inIncognito:YES newTab:NO];
}
style:UIAlertActionStyleDefault];
[coordinator addItemWithTitle:l10n_util::GetNSString(
IDS_IOS_BOOKMARK_CONTEXT_MENU_MOVE)
action:^{
[weakSelf moveNodes:nodes];
}
style:UIAlertActionStyleDefault];
[coordinator addItemWithTitle:l10n_util::GetNSString(IDS_CANCEL)
action:nil
style:UIAlertActionStyleCancel];
}
- (void)configureCoordinator:(AlertCoordinator*)coordinator
forSingleBookmarkURL:(const BookmarkNode*)node {
__weak BookmarkHomeViewController* weakSelf = self;
std::string urlString = node->url().possibly_invalid_spec();
coordinator.alertController.view.accessibilityIdentifier =
@"bookmark_context_menu";
[coordinator addItemWithTitle:l10n_util::GetNSString(
IDS_IOS_BOOKMARK_CONTEXT_MENU_EDIT)
action:^{
[weakSelf editNode:node];
}
style:UIAlertActionStyleDefault];
[coordinator
addItemWithTitle:l10n_util::GetNSString(
IDS_IOS_CONTENT_CONTEXT_OPENLINKNEWTAB)
action:^{
std::vector<const BookmarkNode*> nodes = {node};
[weakSelf openAllNodes:nodes inIncognito:NO newTab:YES];
}
style:UIAlertActionStyleDefault];
[coordinator
addItemWithTitle:l10n_util::GetNSString(
IDS_IOS_CONTENT_CONTEXT_OPENLINKNEWINCOGNITOTAB)
action:^{
std::vector<const BookmarkNode*> nodes = {node};
[weakSelf openAllNodes:nodes inIncognito:YES newTab:YES];
}
style:UIAlertActionStyleDefault];
[coordinator
addItemWithTitle:l10n_util::GetNSString(IDS_IOS_CONTENT_CONTEXT_COPY)
action:^{
UIPasteboard* pasteboard = [UIPasteboard generalPasteboard];
pasteboard.string = base::SysUTF8ToNSString(urlString);
[weakSelf setTableViewEditing:NO];
}
style:UIAlertActionStyleDefault];
[coordinator addItemWithTitle:l10n_util::GetNSString(IDS_CANCEL)
action:nil
style:UIAlertActionStyleCancel];
}
- (void)configureCoordinator:(AlertCoordinator*)coordinator
forSingleBookmarkFolder:(const BookmarkNode*)node {
__weak BookmarkHomeViewController* weakSelf = self;
coordinator.alertController.view.accessibilityIdentifier =
@"bookmark_context_menu";
[coordinator addItemWithTitle:l10n_util::GetNSString(
IDS_IOS_BOOKMARK_CONTEXT_MENU_EDIT_FOLDER)
action:^{
[weakSelf editNode:node];
}
style:UIAlertActionStyleDefault];
[coordinator addItemWithTitle:l10n_util::GetNSString(
IDS_IOS_BOOKMARK_CONTEXT_MENU_MOVE)
action:^{
std::set<const BookmarkNode*> nodes;
nodes.insert(node);
[weakSelf moveNodes:nodes];
}
style:UIAlertActionStyleDefault];
[coordinator addItemWithTitle:l10n_util::GetNSString(IDS_CANCEL)
action:nil
style:UIAlertActionStyleCancel];
}
- (void)configureCoordinator:(AlertCoordinator*)coordinator
forMixedAndMultiFolderSelection:
(const std::set<const bookmarks::BookmarkNode*>)nodes {
__weak BookmarkHomeViewController* weakSelf = self;
coordinator.alertController.view.accessibilityIdentifier =
@"bookmark_context_menu";
[coordinator addItemWithTitle:l10n_util::GetNSString(
IDS_IOS_BOOKMARK_CONTEXT_MENU_MOVE)
action:^{
[weakSelf moveNodes:nodes];
}
style:UIAlertActionStyleDefault];
[coordinator addItemWithTitle:l10n_util::GetNSString(IDS_CANCEL)
action:nil
style:UIAlertActionStyleCancel];
}
#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.actionSheetCoordinator = [[ActionSheetCoordinator alloc]
initWithBaseViewController:self
title:nil
message:nil
rect:CGRectMake(touchPoint.x, touchPoint.y, 1, 1)
view:self.tableView];
if (node->is_url()) {
[self configureCoordinator:self.actionSheetCoordinator
forSingleBookmarkURL:node];
} else if (node->is_folder()) {
[self configureCoordinator:self.actionSheetCoordinator
forSingleBookmarkFolder:node];
} else {
NOTREACHED();
return;
}
[self.actionSheetCoordinator start];
}
#pragma mark UISearchResultsUpdating
- (void)updateSearchResultsForSearchController:
(UISearchController*)searchController {
DCHECK_EQ(self.searchController, searchController);
NSString* text = searchController.searchBar.text;
self.searchTerm = text;
if (text.length == 0) {
if (self.sharedState.currentlyShowingSearchResults) {
self.sharedState.currentlyShowingSearchResults = NO;
// Restore current list.
[self.mediator computeBookmarkTableViewData];
[self.mediator computePromoTableViewData];
[self.sharedState.tableView reloadData];
[self showScrim];
}
} else {
if (!self.sharedState.currentlyShowingSearchResults) {
self.sharedState.currentlyShowingSearchResults = YES;
[self.mediator computePromoTableViewData];
[self hideScrim];
}
// Replace current list with search result, but doesn't change
// the 'regular' model for this page, which we can restore when search
// is terminated.
NSString* noResults = l10n_util::GetNSString(IDS_HISTORY_NO_SEARCH_RESULTS);
[self.mediator computeBookmarkTableViewDataMatching:text
orShowMessageWhenNoResults:noResults];
[self.sharedState.tableView reloadData];
[self setupContextBar];
}
}
#pragma mark UISearchControllerDelegate
- (void)willPresentSearchController:(UISearchController*)searchController {
[self showScrim];
}
- (void)willDismissSearchController:(UISearchController*)searchController {
// Avoid scrim being put back on in updateSearchResultsForSearchController.
self.sharedState.currentlyShowingSearchResults = NO;
// Restore current list.
[self.mediator computeBookmarkTableViewData];
[self.sharedState.tableView reloadData];
}
- (void)didDismissSearchController:(UISearchController*)searchController {
[self hideScrim];
}
#pragma mark - BookmarkHomeSharedStateObserver
- (void)sharedStateDidClearEditNodes:(BookmarkHomeSharedState*)sharedState {
[self handleSelectEditNodes:sharedState.editNodes];
}
#pragma mark - UITableViewDataSource
- (UITableViewCell*)tableView:(UITableView*)tableView
cellForRowAtIndexPath:(NSIndexPath*)indexPath {
UITableViewCell* cell =
[super tableView:tableView cellForRowAtIndexPath:indexPath];
TableViewItem* item =
[self.sharedState.tableViewModel itemAtIndexPath:indexPath];
if (item.type == BookmarkHomeItemTypeBookmark) {
BookmarkHomeNodeItem* nodeItem =
base::mac::ObjCCastStrict<BookmarkHomeNodeItem>(item);
if (nodeItem.bookmarkNode->is_folder() &&
nodeItem.bookmarkNode == self.sharedState.editingFolderNode) {
TableViewBookmarkFolderCell* tableCell =
base::mac::ObjCCastStrict<TableViewBookmarkFolderCell>(cell);
// Delay starting edit, so that the cell is fully created. This is
// needed when scrolling away and then back into the editingCell,
// without the delay the cell will resign first responder before its
// created.
dispatch_async(dispatch_get_main_queue(), ^{
self.sharedState.editingFolderCell = tableCell;
[tableCell startEdit];
tableCell.textDelegate = self;
});
}
// Load the favicon from cache. If not found, try fetching it from a
// Google Server.
[self loadFaviconAtIndexPath:indexPath
forCell:cell
fallbackToGoogleServer:YES];
}
return cell;
}
- (BOOL)tableView:(UITableView*)tableView
canEditRowAtIndexPath:(NSIndexPath*)indexPath {
// Filtered results are always a URL and editable.
if (self.sharedState.currentlyShowingSearchResults) {
return YES;
}
TableViewItem* item =
[self.sharedState.tableViewModel itemAtIndexPath:indexPath];
if (item.type != BookmarkHomeItemTypeBookmark) {
// Can only edit bookmarks.
return NO;
}
// If the cell at |indexPath| is being edited (which happens when creating a
// new Folder) return NO.
if ([tableView indexPathForCell:self.sharedState.editingFolderCell] ==
indexPath) {
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 handleSelectNodesForDeletion:nodes];
base::RecordAction(
base::UserMetricsAction("MobileBookmarkManagerEntryDeleted"));
}
}
- (BOOL)tableView:(UITableView*)tableView
canMoveRowAtIndexPath:(NSIndexPath*)indexPath {
// No reorering with filtered results.
if (self.sharedState.currentlyShowingSearchResults) {
return NO;
}
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 ||
self.sharedState.currentlyShowingSearchResults) {
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 handleMoveNode:node toPosition:newPosition];
}
#pragma mark - UITableViewDelegate
- (CGFloat)tableView:(UITableView*)tableView
heightForRowAtIndexPath:(NSIndexPath*)indexPath {
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 handleSelectEditNodes:self.sharedState.editNodes];
return;
}
[self.sharedState.editingFolderCell stopEdit];
if (node->is_folder()) {
[self handleSelectFolderForNavigation:node];
} else {
if (self.sharedState.currentlyShowingSearchResults) {
// Set the searchController active property to NO or the SearchBar will
// cause the navigation controller to linger for a second when
// dismissing.
self.searchController.active = NO;
}
// Open URL. Pass this to the delegate.
[self handleSelectUrlForNavigation: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 handleSelectEditNodes:self.sharedState.editNodes];
}
}
@end