blob: f60515cd8ffc965dcd60c59110cb0ce23d0a3942 [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/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