| Index: ios/chrome/browser/ui/bookmarks/bookmark_collection_view.mm
|
| diff --git a/ios/chrome/browser/ui/bookmarks/bookmark_collection_view.mm b/ios/chrome/browser/ui/bookmarks/bookmark_collection_view.mm
|
| index 898a2bbeccdb6cfd8a5c5366818832d18de44059..28cc755dbb44cc7bc27c3d588966e6491437fb92 100644
|
| --- a/ios/chrome/browser/ui/bookmarks/bookmark_collection_view.mm
|
| +++ b/ios/chrome/browser/ui/bookmarks/bookmark_collection_view.mm
|
| @@ -9,6 +9,7 @@
|
| #include <map>
|
| #include <memory>
|
|
|
| +#include "base/logging.h"
|
| #include "base/mac/bind_objc_block.h"
|
| #include "base/mac/foundation_util.h"
|
| #include "base/strings/sys_string_conversions.h"
|
| @@ -20,7 +21,12 @@
|
| #include "components/favicon_base/favicon_types.h"
|
| #include "ios/chrome/browser/bookmarks/bookmark_model_factory.h"
|
| #include "ios/chrome/browser/bookmarks/bookmarks_utils.h"
|
| +#include "ios/chrome/browser/experimental_flags.h"
|
| #include "ios/chrome/browser/favicon/ios_chrome_large_icon_service_factory.h"
|
| +#import "ios/chrome/browser/ui/authentication/signin_promo_view.h"
|
| +#import "ios/chrome/browser/ui/authentication/signin_promo_view_configurator.h"
|
| +#import "ios/chrome/browser/ui/authentication/signin_promo_view_consumer.h"
|
| +#import "ios/chrome/browser/ui/authentication/signin_promo_view_mediator.h"
|
| #import "ios/chrome/browser/ui/bookmarks/bookmark_collection_cells.h"
|
| #import "ios/chrome/browser/ui/bookmarks/bookmark_collection_view_background.h"
|
| #import "ios/chrome/browser/ui/bookmarks/bookmark_promo_cell.h"
|
| @@ -39,6 +45,22 @@
|
| using bookmarks::BookmarkNode;
|
|
|
| namespace {
|
| +// Computes the cell size based on width.
|
| +CGSize PreferredCellSizeForWidth(UICollectionViewCell* cell, CGFloat width) {
|
| + CGRect cellFrame = cell.frame;
|
| + cellFrame.size.width = width;
|
| + cellFrame.size.height = CGFLOAT_MAX;
|
| + cell.frame = cellFrame;
|
| + [cell setNeedsLayout];
|
| + [cell layoutIfNeeded];
|
| + CGSize result =
|
| + [cell systemLayoutSizeFittingSize:UILayoutFittingCompressedSize
|
| + withHorizontalFittingPriority:UILayoutPriorityRequired
|
| + verticalFittingPriority:UILayoutPriorityDefaultLow];
|
| + cellFrame.size = result;
|
| + cell.frame = cellFrame;
|
| + return result;
|
| +}
|
|
|
| // Used to store a pair of NSIntegers when storing a NSIndexPath in C++
|
| // collections.
|
| @@ -55,12 +77,24 @@ CGFloat minFaviconSizePt = 16;
|
| // This delay should not be too small to let enough time to load bookmarks
|
| // from network.
|
| const NSTimeInterval kShowEmptyBookmarksBackgroundRefreshDelay = 1.0;
|
| +}
|
|
|
| -} // namespace
|
| -
|
| -@interface BookmarkCollectionView ()<UICollectionViewDataSource,
|
| +@interface BookmarkCollectionView ()<BookmarkPromoCellDelegate,
|
| + SigninPromoViewConsumer,
|
| + UICollectionViewDataSource,
|
| UICollectionViewDelegateFlowLayout,
|
| UIGestureRecognizerDelegate> {
|
| + // A vector of folders to display in the collection view.
|
| + std::vector<const BookmarkNode*> _subFolders;
|
| + // A vector of bookmark urls to display in the collection view.
|
| + std::vector<const BookmarkNode*> _subItems;
|
| +
|
| + // True if the promo is visible.
|
| + BOOL _promoVisible;
|
| +
|
| + // Mediator, helper for the sign-in promo view.
|
| + SigninPromoViewMediator* _signinPromoViewMediator;
|
| +
|
| std::unique_ptr<bookmarks::BookmarkModelBridge> _modelBridge;
|
| ios::ChromeBrowserState* _browserState;
|
|
|
| @@ -86,32 +120,19 @@ const NSTimeInterval kShowEmptyBookmarksBackgroundRefreshDelay = 1.0;
|
| // Shadow to display over the content.
|
| @property(nonatomic, strong) UIView* shadow;
|
|
|
| -// Updates the editing state for the cell.
|
| -- (void)updateEditingStateOfCell:(BookmarkCell*)cell
|
| - atIndexPath:(NSIndexPath*)indexPath
|
| - animateMenuVisibility:(BOOL)animateMenuVisibility
|
| - animateSelectedState:(BOOL)animateSelectedState;
|
| -
|
| -// Callback received when the user taps the menu button on the cell.
|
| -- (void)didTapMenuButton:(BookmarkItemCell*)cell view:(UIView*)view;
|
| +@property(nonatomic, assign) const bookmarks::BookmarkNode* folder;
|
|
|
| -// In landscape mode, there are 2 widths: 480pt and 568pt. Returns YES if the
|
| -// width is 568pt.
|
| -- (BOOL)wideLandscapeMode;
|
| +// Section indices.
|
| +@property(nonatomic, readonly, assign) NSInteger promoSection;
|
| +@property(nonatomic, readonly, assign) NSInteger folderSection;
|
| +@property(nonatomic, readonly, assign) NSInteger itemsSection;
|
| +@property(nonatomic, readonly, assign) NSInteger sectionCount;
|
|
|
| -// Schedules showing or hiding the empty bookmarks background view if the
|
| -// collection view is empty by calling showEmptyBackgroundIfNeeded after
|
| -// kShowEmptyBookmarksBackgroundRefreshDelay.
|
| -// Multiple call to this method will cancel previous scheduled call to
|
| -// showEmptyBackgroundIfNeeded before scheduling a new one.
|
| -- (void)scheduleEmptyBackgroundVisibilityUpdate;
|
| -// Shows/hides empty bookmarks background view if the collections view is empty.
|
| -- (void)updateEmptyBackgroundVisibility;
|
| -// Shows/hides empty bookmarks background view with an animation.
|
| -- (void)setEmptyBackgroundVisible:(BOOL)visible;
|
| @end
|
|
|
| @implementation BookmarkCollectionView
|
| +@synthesize delegate = _delegate;
|
| +@synthesize folder = _folder;
|
| @synthesize bookmarkModel = _bookmarkModel;
|
| @synthesize collectionView = _collectionView;
|
| @synthesize editing = _editing;
|
| @@ -123,52 +144,6 @@ const NSTimeInterval kShowEmptyBookmarksBackgroundRefreshDelay = 1.0;
|
|
|
| #pragma mark - Initialization
|
|
|
| -- (id)init {
|
| - NOTREACHED();
|
| - return nil;
|
| -}
|
| -
|
| -- (id)initWithFrame:(CGRect)frame {
|
| - NOTREACHED();
|
| - return nil;
|
| -}
|
| -
|
| -- (instancetype)initWithBrowserState:(ios::ChromeBrowserState*)browserState
|
| - frame:(CGRect)frame {
|
| - self = [super initWithFrame:frame];
|
| - if (self) {
|
| - _browserState = browserState;
|
| -
|
| - // Set up connection to the BookmarkModel.
|
| - _bookmarkModel =
|
| - ios::BookmarkModelFactory::GetForBrowserState(browserState);
|
| -
|
| - // Set up observers.
|
| - _modelBridge.reset(
|
| - new bookmarks::BookmarkModelBridge(self, _bookmarkModel));
|
| -
|
| - [self setupViews];
|
| - }
|
| - return self;
|
| -}
|
| -
|
| -- (void)dealloc {
|
| - _collectionView.dataSource = nil;
|
| - _collectionView.delegate = nil;
|
| - UIView* moi = _collectionView;
|
| - dispatch_async(dispatch_get_main_queue(), ^{
|
| - // A collection view with a layout that uses a dynamic animator (aka
|
| - // something that changes the layout over time) will crash if it is
|
| - // deallocated while the animation is currently playing.
|
| - // Apparently if a tick has been dispatched it will execute, invoking a
|
| - // method on the deallocated collection.
|
| - // The only purpose of this block is to retain the collection view for a
|
| - // while, giving the layout a chance to perform its last tick.
|
| - [moi self];
|
| - });
|
| - _faviconTaskTracker.TryCancelAll();
|
| -}
|
| -
|
| - (void)setupViews {
|
| self.backgroundColor = bookmark_utils_ios::mainBackgroundColor();
|
| UICollectionViewFlowLayout* layout =
|
| @@ -223,6 +198,171 @@ const NSTimeInterval kShowEmptyBookmarksBackgroundRefreshDelay = 1.0;
|
| [self.collectionView addGestureRecognizer:self.longPressRecognizer];
|
| }
|
|
|
| +- (instancetype)initWithBrowserState:(ios::ChromeBrowserState*)browserState
|
| + frame:(CGRect)frame {
|
| + self = [super initWithFrame:frame];
|
| + if (self) {
|
| + _browserState = browserState;
|
| +
|
| + // Set up connection to the BookmarkModel.
|
| + _bookmarkModel =
|
| + ios::BookmarkModelFactory::GetForBrowserState(browserState);
|
| +
|
| + // Set up observers.
|
| + _modelBridge.reset(
|
| + new bookmarks::BookmarkModelBridge(self, _bookmarkModel));
|
| +
|
| + [self setupViews];
|
| + [self updateCollectionView];
|
| + }
|
| + return self;
|
| +}
|
| +
|
| +- (void)dealloc {
|
| + _collectionView.dataSource = nil;
|
| + _collectionView.delegate = nil;
|
| + UIView* moi = _collectionView;
|
| + dispatch_async(dispatch_get_main_queue(), ^{
|
| + // A collection view with a layout that uses a dynamic animator (aka
|
| + // something that changes the layout over time) will crash if it is
|
| + // deallocated while the animation is currently playing.
|
| + // Apparently if a tick has been dispatched it will execute, invoking a
|
| + // method on the deallocated collection.
|
| + // The only purpose of this block is to retain the collection view for a
|
| + // while, giving the layout a chance to perform its last tick.
|
| + [moi self];
|
| + });
|
| + _faviconTaskTracker.TryCancelAll();
|
| +}
|
| +
|
| +#pragma mark - BookmarkHomePrimaryView
|
| +
|
| +- (void)changeOrientation:(UIInterfaceOrientation)orientation {
|
| + [self updateCollectionView];
|
| +}
|
| +
|
| +- (CGFloat)contentPositionInPortraitOrientation {
|
| + if (IsPortrait())
|
| + return self.collectionView.contentOffset.y;
|
| +
|
| + // In short landscape mode and portrait mode, there are 2 cells per row.
|
| + if ([self wideLandscapeMode])
|
| + return self.collectionView.contentOffset.y;
|
| +
|
| + // In wide landscape mode, there are 3 cells per row.
|
| + return self.collectionView.contentOffset.y * 3 / 2.0;
|
| +}
|
| +
|
| +- (void)applyContentPosition:(CGFloat)position {
|
| + if (IsLandscape() && [self wideLandscapeMode]) {
|
| + position = position * 2 / 3.0;
|
| + }
|
| +
|
| + CGFloat y =
|
| + MIN(position,
|
| + [self.collectionView.collectionViewLayout collectionViewContentSize]
|
| + .height);
|
| + self.collectionView.contentOffset =
|
| + CGPointMake(self.collectionView.contentOffset.x, y);
|
| +}
|
| +
|
| +- (void)setEditing:(BOOL)editing animated:(BOOL)animated {
|
| + if (self.editing == editing)
|
| + return;
|
| +
|
| + _editing = editing;
|
| + [UIView animateWithDuration:animated ? 0.2 : 0.0
|
| + delay:0
|
| + options:UIViewAnimationOptionBeginFromCurrentState
|
| + animations:^{
|
| + self.shadow.alpha = editing ? 0.0 : 1.0;
|
| + }
|
| + completion:nil];
|
| +
|
| + // If the promo is active this means that it is removed and added as the edit
|
| + // mode changes, making reloading the data mandatory.
|
| + if ([self isPromoActive]) {
|
| + [self.collectionView reloadData];
|
| + [self.collectionView.collectionViewLayout invalidateLayout];
|
| + } else {
|
| + // Update the visual state of the bookmark cells without reloading the
|
| + // section.
|
| + // This prevents flickering of images that need to be asynchronously
|
| + // reloaded.
|
| + NSArray* indexPaths = [self.collectionView indexPathsForVisibleItems];
|
| + for (NSIndexPath* indexPath in indexPaths) {
|
| + [self updateEditingStateOfCellAtIndexPath:indexPath
|
| + animateMenuVisibility:animated
|
| + animateSelectedState:NO];
|
| + }
|
| + }
|
| + [self promoStateChangedAnimated:animated];
|
| +}
|
| +
|
| +- (void)setScrollsToTop:(BOOL)scrollsToTop {
|
| + self.collectionView.scrollsToTop = scrollsToTop;
|
| +}
|
| +
|
| +#pragma mark - Public
|
| +
|
| +- (void)resetFolder:(const BookmarkNode*)folder {
|
| + DCHECK(folder->is_folder());
|
| + self.folder = folder;
|
| + [self updateCollectionView];
|
| +}
|
| +
|
| +- (void)setDelegate:(id<BookmarkCollectionViewDelegate>)delegate {
|
| + _delegate = delegate;
|
| + [self promoStateChangedAnimated:NO];
|
| +}
|
| +
|
| +- (void)collectionViewScrolled {
|
| + [self.delegate bookmarkCollectionViewDidScroll:self];
|
| +}
|
| +
|
| +- (void)promoStateChangedAnimated:(BOOL)animate {
|
| + BOOL newPromoState =
|
| + !self.editing && self.folder &&
|
| + self.folder->type() == BookmarkNode::MOBILE &&
|
| + [self.delegate bookmarkCollectionViewShouldShowPromoCell:self];
|
| + if (newPromoState != _promoVisible) {
|
| + // This is awful, but until the old code to do the refresh when switching
|
| + // in and out of edit mode is fixed, this is probably the cleanest thing to
|
| + // do.
|
| + _promoVisible = newPromoState;
|
| + if (experimental_flags::IsSigninPromoEnabled()) {
|
| + if (!_promoVisible) {
|
| + _signinPromoViewMediator.consumer = nil;
|
| + _signinPromoViewMediator = nil;
|
| + } else {
|
| + _signinPromoViewMediator = [[SigninPromoViewMediator alloc] init];
|
| + _signinPromoViewMediator.consumer = self;
|
| + _signinPromoViewMediator.accessPoint =
|
| + signin_metrics::AccessPoint::ACCESS_POINT_BOOKMARK_MANAGER;
|
| + }
|
| + }
|
| + [self.collectionView reloadData];
|
| + }
|
| +}
|
| +
|
| +#pragma mark - Sections
|
| +
|
| +- (NSInteger)promoSection {
|
| + return [self shouldShowPromoCell] ? 0 : -1;
|
| +}
|
| +
|
| +- (NSInteger)folderSection {
|
| + return [self shouldShowPromoCell] ? 1 : 0;
|
| +}
|
| +
|
| +- (NSInteger)itemsSection {
|
| + return [self shouldShowPromoCell] ? 2 : 1;
|
| +}
|
| +
|
| +- (NSInteger)sectionCount {
|
| + return [self shouldShowPromoCell] ? 3 : 2;
|
| +}
|
| +
|
| - (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
|
| [self updateShadow];
|
| }
|
| @@ -258,75 +398,541 @@ const NSTimeInterval kShowEmptyBookmarksBackgroundRefreshDelay = 1.0;
|
| CGRectMake(0, y, CGRectGetWidth(self.bounds), shadowHeight);
|
| }
|
|
|
| -#pragma mark - UIScrollViewDelegate
|
| +#pragma mark - BookmarkItemCell callbacks
|
|
|
| -- (void)scrollViewDidScroll:(UIScrollView*)scrollView {
|
| - [self updateShadowFrame];
|
| - [self collectionViewScrolled];
|
| +// Callback received when the user taps the menu button on the cell.
|
| +- (void)didTapMenuButton:(BookmarkItemCell*)cell view:(UIView*)view {
|
| + [self didTapMenuButtonAtIndexPath:[self.collectionView indexPathForCell:cell]
|
| + onView:view
|
| + forCell:cell];
|
| }
|
|
|
| -#pragma mark - empty background
|
| -
|
| -- (void)scheduleEmptyBackgroundVisibilityUpdate {
|
| - [NSObject
|
| - cancelPreviousPerformRequestsWithTarget:self
|
| - selector:
|
| - @selector(
|
| - updateEmptyBackgroundVisibility)
|
| - object:nil];
|
| - [self performSelector:@selector(updateEmptyBackgroundVisibility)
|
| - withObject:nil
|
| - afterDelay:kShowEmptyBookmarksBackgroundRefreshDelay];
|
| +#pragma mark - BookmarkModelBridgeObserver Callbacks
|
| +// BookmarkModelBridgeObserver Callbacks
|
| +// Instances of this class automatically observe the bookmark model.
|
| +// The bookmark model has loaded.
|
| +- (void)bookmarkModelLoaded {
|
| + [self updateCollectionView];
|
| }
|
|
|
| -- (BOOL)isCollectionViewEmpty {
|
| - BOOL collectionViewIsEmpty = YES;
|
| - const NSInteger numberOfSections = [self numberOfSections];
|
| - NSInteger section = [self shouldShowPromoCell] ? 1 : 0;
|
| - for (; collectionViewIsEmpty && section < numberOfSections; ++section) {
|
| - const NSInteger numberOfItemsInSection =
|
| - [self numberOfItemsInSection:section];
|
| - collectionViewIsEmpty = numberOfItemsInSection == 0;
|
| +// The node has changed, but not its children.
|
| +- (void)bookmarkNodeChanged:(const BookmarkNode*)bookmarkNode {
|
| + // The base folder changed. Do nothing.
|
| + if (bookmarkNode == self.folder)
|
| + return;
|
| +
|
| + // A specific cell changed. Reload that cell.
|
| + NSIndexPath* indexPath = [self indexPathForNode:bookmarkNode];
|
| +
|
| + if (indexPath) {
|
| + // TODO(crbug.com/603661): Ideally, we would only reload the relevant index
|
| + // path. However, calling reloadItemsAtIndexPaths:(0,0) immediately after
|
| + // reloadData results in a exception: NSInternalInconsistencyException
|
| + // 'request for index path for global index 2147483645 ...'
|
| + // One solution would be to keep track of whether we've just called
|
| + // reloadData, but that requires experimentation to determine how long we
|
| + // have to wait before we can safely call reloadItemsAtIndexPaths.
|
| + [self updateCollectionView];
|
| }
|
| - return collectionViewIsEmpty;
|
| }
|
|
|
| -- (void)updateEmptyBackgroundVisibility {
|
| - const BOOL showEmptyBackground =
|
| - [self isCollectionViewEmpty] && ![self shouldShowPromoCell];
|
| - [self setEmptyBackgroundVisible:showEmptyBackground];
|
| -}
|
| +// The node has not changed, but its children have.
|
| +- (void)bookmarkNodeChildrenChanged:(const BookmarkNode*)bookmarkNode {
|
| + // The base folder's children changed. Reload everything.
|
| + if (bookmarkNode == self.folder) {
|
| + [self updateCollectionView];
|
| + return;
|
| + }
|
|
|
| -- (void)setEmptyBackgroundVisible:(BOOL)emptyBackgroundVisible {
|
| - [UIView beginAnimations:@"alpha" context:NULL];
|
| - self.emptyCollectionBackgroundView.alpha = emptyBackgroundVisible ? 1 : 0;
|
| - [UIView commitAnimations];
|
| + // A subfolder's children changed. Reload that cell.
|
| + std::vector<const BookmarkNode*>::iterator it =
|
| + std::find(_subFolders.begin(), _subFolders.end(), bookmarkNode);
|
| + if (it != _subFolders.end()) {
|
| + // TODO(crbug.com/603661): Ideally, we would only reload the relevant index
|
| + // path. However, calling reloadItemsAtIndexPaths:(0,0) immediately after
|
| + // reloadData results in a exception: NSInternalInconsistencyException
|
| + // 'request for index path for global index 2147483645 ...'
|
| + // One solution would be to keep track of whether we've just called
|
| + // reloadData, but that requires experimentation to determine how long we
|
| + // have to wait before we can safely call reloadItemsAtIndexPaths.
|
| + [self updateCollectionView];
|
| + }
|
| }
|
|
|
| -#pragma mark - UICollectionViewDataSource
|
| +// The node has moved to a new parent folder.
|
| +- (void)bookmarkNode:(const BookmarkNode*)bookmarkNode
|
| + movedFromParent:(const BookmarkNode*)oldParent
|
| + toParent:(const BookmarkNode*)newParent {
|
| + if (oldParent == self.folder || newParent == self.folder) {
|
| + // A folder was added or removed from the base folder.
|
| + [self updateCollectionView];
|
| + }
|
| +}
|
|
|
| -- (NSInteger)collectionView:(UICollectionView*)collectionView
|
| - numberOfItemsInSection:(NSInteger)section {
|
| - const NSInteger numberOfItemsInSection =
|
| - [self numberOfItemsInSection:section];
|
| - const BOOL isCollectionViewEmpty = [self isCollectionViewEmpty];
|
| - self.collectionView.scrollEnabled = !isCollectionViewEmpty;
|
| - if (isCollectionViewEmpty) {
|
| - [self scheduleEmptyBackgroundVisibilityUpdate];
|
| - } else {
|
| - // Hide empty bookmarks now.
|
| - [self setEmptyBackgroundVisible:NO];
|
| +// |node| was deleted from |folder|.
|
| +- (void)bookmarkNodeDeleted:(const BookmarkNode*)node
|
| + fromFolder:(const BookmarkNode*)folder {
|
| + if (self.folder == node) {
|
| + self.folder = nil;
|
| + [self updateCollectionView];
|
| }
|
| - return numberOfItemsInSection;
|
| }
|
|
|
| -- (NSInteger)numberOfSectionsInCollectionView:
|
| - (UICollectionView*)collectionView {
|
| - const NSInteger numberOfSections = [self numberOfSections];
|
| - const BOOL collectionViewIsEmpty = 0 == numberOfSections;
|
| - self.collectionView.scrollEnabled = !collectionViewIsEmpty;
|
| - if (collectionViewIsEmpty) {
|
| - [self scheduleEmptyBackgroundVisibilityUpdate];
|
| +// All non-permanent nodes have been removed.
|
| +- (void)bookmarkModelRemovedAllNodes {
|
| + self.folder = nil;
|
| + [self updateCollectionView];
|
| +}
|
| +
|
| +- (void)bookmarkNodeFaviconChanged:
|
| + (const bookmarks::BookmarkNode*)bookmarkNode {
|
| + // Only urls have favicons.
|
| + DCHECK(bookmarkNode->is_url());
|
| +
|
| + // Update image of corresponding cell.
|
| + NSIndexPath* indexPath = [self indexPathForNode:bookmarkNode];
|
| +
|
| + if (!indexPath)
|
| + return;
|
| +
|
| + // Check that this cell is visible.
|
| + NSArray* visiblePaths = [self.collectionView indexPathsForVisibleItems];
|
| + if (![visiblePaths containsObject:indexPath])
|
| + return;
|
| +
|
| + [self loadFaviconAtIndexPath:indexPath];
|
| +}
|
| +
|
| +#pragma mark - non-UI
|
| +
|
| +// Called when a user is attempting to select a cell.
|
| +// Returning NO prevents the cell from being selected.
|
| +- (BOOL)shouldSelectCellAtIndexPath:(NSIndexPath*)indexPath {
|
| + return YES;
|
| +}
|
| +
|
| +// Called when a cell is tapped outside of editing mode.
|
| +- (void)didTapCellAtIndexPath:(NSIndexPath*)indexPath {
|
| + if (indexPath.section == self.promoSection) {
|
| + // User tapped inside promo cell but not on one of the buttons. Ignore it.
|
| + return;
|
| + }
|
| +
|
| + const BookmarkNode* node = [self nodeAtIndexPath:indexPath];
|
| + DCHECK(node);
|
| +
|
| + if (indexPath.section == self.folderSection) {
|
| + [self.delegate bookmarkCollectionView:self
|
| + selectedFolderForNavigation:node];
|
| + } else {
|
| + RecordBookmarkLaunch(BOOKMARK_LAUNCH_LOCATION_FOLDER);
|
| + [self.delegate bookmarkCollectionView:self
|
| + selectedUrlForNavigation:node->url()];
|
| + }
|
| +}
|
| +
|
| +// Called when a user selected a cell in the editing state.
|
| +- (void)didAddCellForEditingAtIndexPath:(NSIndexPath*)indexPath {
|
| + const BookmarkNode* node = [self nodeAtIndexPath:indexPath];
|
| + UICollectionViewCell* cell =
|
| + [self.collectionView cellForItemAtIndexPath:indexPath];
|
| + [self.delegate bookmarkCollectionView:self cell:cell addNodeForEditing:node];
|
| +}
|
| +
|
| +- (void)didRemoveCellForEditingAtIndexPath:(NSIndexPath*)indexPath {
|
| + const BookmarkNode* node = [self nodeAtIndexPath:indexPath];
|
| + UICollectionViewCell* cell =
|
| + [self.collectionView cellForItemAtIndexPath:indexPath];
|
| + [self.delegate bookmarkCollectionView:self
|
| + cell:cell
|
| + removeNodeForEditing:node];
|
| +}
|
| +
|
| +// Called when a user taps the menu button on a cell.
|
| +- (void)didTapMenuButtonAtIndexPath:(NSIndexPath*)indexPath
|
| + onView:(UIView*)view
|
| + forCell:(BookmarkItemCell*)cell {
|
| + [self.delegate bookmarkCollectionView:self
|
| + wantsMenuForBookmark:[self nodeAtIndexPath:indexPath]
|
| + onView:view
|
| + forCell:cell];
|
| +}
|
| +
|
| +// Whether a cell should show a button and of which type.
|
| +- (bookmark_cell::ButtonType)buttonTypeForCellAtIndexPath:
|
| + (NSIndexPath*)indexPath {
|
| + return self.editing ? bookmark_cell::ButtonNone : bookmark_cell::ButtonMenu;
|
| +}
|
| +
|
| +// Whether a long press at the cell at |indexPath| should be allowed.
|
| +- (BOOL)allowLongPressForCellAtIndexPath:(NSIndexPath*)indexPath {
|
| + return !self.editing;
|
| +}
|
| +
|
| +// The |cell| at |indexPath| received a long press.
|
| +- (void)didLongPressCell:(UICollectionViewCell*)cell
|
| + atIndexPath:(NSIndexPath*)indexPath {
|
| + if (indexPath.section == self.promoSection) {
|
| + // User long-pressed inside promo cell. Ignore it.
|
| + return;
|
| + }
|
| +
|
| + [self.delegate bookmarkCollectionView:self
|
| + didLongPressCell:cell
|
| + forBookmark:[self nodeAtIndexPath:indexPath]];
|
| +}
|
| +
|
| +// Whether the cell has been selected in editing mode.
|
| +- (BOOL)cellIsSelectedForEditingAtIndexPath:(NSIndexPath*)indexPath {
|
| + const BookmarkNode* node = [self nodeAtIndexPath:indexPath];
|
| + const std::set<const BookmarkNode*>& editingNodes =
|
| + [self.delegate nodesBeingEdited];
|
| + return editingNodes.find(node) != editingNodes.end();
|
| +}
|
| +
|
| +// Updates the collection view based on the current state of all models and
|
| +// contextual parameters, such as the interface orientation.
|
| +- (void)updateCollectionView {
|
| + if (!self.bookmarkModel->loaded())
|
| + return;
|
| +
|
| + // Regenerate the list of all bookmarks.
|
| + _subFolders = std::vector<const BookmarkNode*>();
|
| + _subItems = std::vector<const BookmarkNode*>();
|
| +
|
| + if (self.folder) {
|
| + int childCount = self.folder->child_count();
|
| + for (int i = 0; i < childCount; ++i) {
|
| + const BookmarkNode* node = self.folder->GetChild(i);
|
| + if (node->is_folder())
|
| + _subFolders.push_back(node);
|
| + else
|
| + _subItems.push_back(node);
|
| + }
|
| +
|
| + bookmark_utils_ios::SortFolders(&_subFolders);
|
| + }
|
| +
|
| + [self cancelAllFaviconLoads];
|
| + [self.collectionView reloadData];
|
| +}
|
| +
|
| +// Returns the bookmark node associated with |indexPath|.
|
| +- (const BookmarkNode*)nodeAtIndexPath:(NSIndexPath*)indexPath {
|
| + if (indexPath.section == self.folderSection)
|
| + return _subFolders[indexPath.row];
|
| + if (indexPath.section == self.itemsSection)
|
| + return _subItems[indexPath.row];
|
| +
|
| + NOTREACHED();
|
| + return nullptr;
|
| +}
|
| +
|
| +#pragma mark -
|
| +
|
| +- (NSIndexPath*)indexPathForNode:(const bookmarks::BookmarkNode*)bookmarkNode {
|
| + NSIndexPath* indexPath = nil;
|
| + if (bookmarkNode->is_folder()) {
|
| + std::vector<const BookmarkNode*>::iterator it =
|
| + std::find(_subFolders.begin(), _subFolders.end(), bookmarkNode);
|
| + if (it != _subFolders.end()) {
|
| + ptrdiff_t index = std::distance(_subFolders.begin(), it);
|
| + indexPath =
|
| + [NSIndexPath indexPathForRow:index inSection:self.folderSection];
|
| + }
|
| + } else if (bookmarkNode->is_url()) {
|
| + std::vector<const BookmarkNode*>::iterator it =
|
| + std::find(_subItems.begin(), _subItems.end(), bookmarkNode);
|
| + if (it != _subItems.end()) {
|
| + ptrdiff_t index = std::distance(_subItems.begin(), it);
|
| + indexPath =
|
| + [NSIndexPath indexPathForRow:index inSection:self.itemsSection];
|
| + }
|
| + }
|
| + return indexPath;
|
| +}
|
| +
|
| +- (void)collectionView:(UICollectionView*)collectionView
|
| + willDisplayCell:(UICollectionViewCell*)cell
|
| + forItemAtIndexPath:(NSIndexPath*)indexPath {
|
| + if (indexPath.section == self.itemsSection) {
|
| + [self loadFaviconAtIndexPath:indexPath];
|
| + }
|
| +}
|
| +
|
| +- (CGFloat)interitemSpacingForSectionAtIndex:(NSInteger)section {
|
| + CGFloat interitemSpacing = 0;
|
| + SEL minimumInteritemSpacingSelector = @selector
|
| + (collectionView:layout:minimumInteritemSpacingForSectionAtIndex:);
|
| + if ([self.collectionView.delegate
|
| + respondsToSelector:minimumInteritemSpacingSelector]) {
|
| + id collectionViewDelegate = static_cast<id>(self.collectionView.delegate);
|
| + interitemSpacing =
|
| + [collectionViewDelegate collectionView:self.collectionView
|
| + layout:self.collectionView
|
| + .collectionViewLayout
|
| + minimumInteritemSpacingForSectionAtIndex:section];
|
| + } else if ([self.collectionView.collectionViewLayout
|
| + isKindOfClass:[UICollectionViewFlowLayout class]]) {
|
| + UICollectionViewFlowLayout* flowLayout =
|
| + static_cast<UICollectionViewFlowLayout*>(
|
| + self.collectionView.collectionViewLayout);
|
| + interitemSpacing = flowLayout.minimumInteritemSpacing;
|
| + }
|
| + return interitemSpacing;
|
| +}
|
| +
|
| +// The inset of the section.
|
| +- (UIEdgeInsets)insetForSectionAtIndex:(NSInteger)section {
|
| + UIEdgeInsets insets = UIEdgeInsetsZero;
|
| + if ([self isPromoSection:section])
|
| + insets = UIEdgeInsetsZero;
|
| +
|
| + if (IsIPadIdiom()) {
|
| + insets = UIEdgeInsetsMake(10, rowMarginTablet, 0, rowMarginTablet);
|
| + } else {
|
| + insets = UIEdgeInsetsZero;
|
| + }
|
| +
|
| + if (section == self.folderSection)
|
| + insets.bottom = [self interitemSpacingForSectionAtIndex:section] / 2.;
|
| + else if (section == self.itemsSection)
|
| + insets.top = [self interitemSpacingForSectionAtIndex:section] / 2.;
|
| + else if (section == self.promoSection)
|
| + (void)0; // No insets to update.
|
| + else
|
| + NOTREACHED();
|
| + return insets;
|
| +}
|
| +
|
| +// The size of the cell at |indexPath|.
|
| +- (CGSize)cellSizeForIndexPath:(NSIndexPath*)indexPath {
|
| + if ([self isPromoSection:indexPath.section]) {
|
| + UICollectionViewCell* cell =
|
| + [self.collectionView cellForItemAtIndexPath:indexPath];
|
| + if (!cell) {
|
| + // -[UICollectionView
|
| + // dequeueReusableCellWithReuseIdentifier:forIndexPath:] cannot be used
|
| + // here since this method is called by -[id<UICollectionViewDelegate>
|
| + // collectionView:layout:sizeForItemAtIndexPath:]. This would generate
|
| + // crash: SIGFPE, EXC_I386_DIV.
|
| + if (experimental_flags::IsSigninPromoEnabled()) {
|
| + DCHECK(_signinPromoViewMediator);
|
| + BookmarkSigninPromoCell* signinPromoCell =
|
| + [[BookmarkSigninPromoCell alloc]
|
| + initWithFrame:CGRectMake(0, 0, 1000, 1000)];
|
| + [[_signinPromoViewMediator createConfigurator]
|
| + configureSigninPromoView:signinPromoCell.signinPromoView];
|
| + cell = signinPromoCell;
|
| + } else {
|
| + cell = [[BookmarkPromoCell alloc] init];
|
| + }
|
| + }
|
| + return PreferredCellSizeForWidth(cell, CGRectGetWidth(self.bounds));
|
| + }
|
| + DCHECK(![self isPromoSection:indexPath.section]);
|
| + UIEdgeInsets insets = [self insetForSectionAtIndex:indexPath.section];
|
| + return CGSizeMake(self.bounds.size.width - (insets.right + insets.left),
|
| + rowHeight);
|
| +}
|
| +
|
| +- (BOOL)needsSectionHeaderForSection:(NSInteger)section {
|
| + // Only show header when there is at least one element in the previous
|
| + // section.
|
| + if (section == 0)
|
| + return NO;
|
| +
|
| + if ([self numberOfItemsInSection:(section - 1)] == 0)
|
| + return NO;
|
| +
|
| + return YES;
|
| +}
|
| +
|
| +#pragma mark - UI
|
| +
|
| +// The size of the header for |section|. A return value of CGSizeZero prevents
|
| +// a header from showing.
|
| +- (CGSize)headerSizeForSection:(NSInteger)section {
|
| + if ([self needsSectionHeaderForSection:section])
|
| + return CGSizeMake(self.bounds.size.width,
|
| + [BookmarkHeaderSeparatorView preferredHeight]);
|
| +
|
| + return CGSizeZero;
|
| +}
|
| +
|
| +// Create a cell for display at |indexPath|.
|
| +- (UICollectionViewCell*)cellAtIndexPath:(NSIndexPath*)indexPath {
|
| + if (indexPath.section == self.promoSection) {
|
| + if (experimental_flags::IsSigninPromoEnabled()) {
|
| + BookmarkSigninPromoCell* signinPromoCell = [self.collectionView
|
| + dequeueReusableCellWithReuseIdentifier:[BookmarkSigninPromoCell
|
| + reuseIdentifier]
|
| + forIndexPath:indexPath];
|
| + signinPromoCell.signinPromoView.delegate = _signinPromoViewMediator;
|
| + [[_signinPromoViewMediator createConfigurator]
|
| + configureSigninPromoView:signinPromoCell.signinPromoView];
|
| + __weak BookmarkCollectionView* weakSelf = self;
|
| + signinPromoCell.closeButtonAction = ^() {
|
| + [weakSelf.delegate bookmarkCollectionViewDismissPromo:self];
|
| + };
|
| + return signinPromoCell;
|
| + } else {
|
| + BookmarkPromoCell* promoCell = [self.collectionView
|
| + dequeueReusableCellWithReuseIdentifier:[BookmarkPromoCell
|
| + reuseIdentifier]
|
| + forIndexPath:indexPath];
|
| + promoCell.delegate = self;
|
| + return promoCell;
|
| + }
|
| + }
|
| + const BookmarkNode* node = [self nodeAtIndexPath:indexPath];
|
| +
|
| + if (indexPath.section == self.folderSection)
|
| + return [self cellForFolder:node indexPath:indexPath];
|
| +
|
| + BookmarkItemCell* cell = [self cellForBookmark:node indexPath:indexPath];
|
| + return cell;
|
| +}
|
| +
|
| +// Create a header view for the element at |indexPath|.
|
| +- (UICollectionReusableView*)headerAtIndexPath:(NSIndexPath*)indexPath {
|
| + if (![self needsSectionHeaderForSection:indexPath.section])
|
| + return nil;
|
| +
|
| + BookmarkHeaderSeparatorView* view = [self.collectionView
|
| + dequeueReusableSupplementaryViewOfKind:
|
| + UICollectionElementKindSectionHeader
|
| + withReuseIdentifier:[BookmarkHeaderSeparatorView
|
| + reuseIdentifier]
|
| + forIndexPath:indexPath];
|
| + view.backgroundColor = [UIColor colorWithWhite:1 alpha:1];
|
| + return view;
|
| +}
|
| +
|
| +- (NSInteger)numberOfItemsInSection:(NSInteger)section {
|
| + if (section == self.folderSection)
|
| + return _subFolders.size();
|
| + if (section == self.itemsSection)
|
| + return _subItems.size();
|
| + if (section == self.promoSection)
|
| + return 1;
|
| +
|
| + NOTREACHED();
|
| + return -1;
|
| +}
|
| +
|
| +- (NSInteger)numberOfSections {
|
| + return self.sectionCount;
|
| +}
|
| +
|
| +#pragma mark - BookmarkPromoCellDelegate
|
| +
|
| +- (void)bookmarkPromoCellDidTapSignIn:(BookmarkPromoCell*)bookmarkPromoCell {
|
| + [self.delegate bookmarkCollectionViewShowSignIn:self];
|
| +}
|
| +
|
| +- (void)bookmarkPromoCellDidTapDismiss:(BookmarkPromoCell*)bookmarkPromoCell {
|
| + [self.delegate bookmarkCollectionViewDismissPromo:self];
|
| +}
|
| +
|
| +#pragma mark - SigninPromoViewConsumer
|
| +
|
| +- (void)configureSigninPromoWithConfigurator:
|
| + (SigninPromoViewConfigurator*)configurator
|
| + identityChanged:(BOOL)identityChanged {
|
| + DCHECK(_signinPromoViewMediator);
|
| + NSIndexPath* indexPath =
|
| + [NSIndexPath indexPathForRow:0 inSection:self.promoSection];
|
| + BookmarkSigninPromoCell* signinPromoCell =
|
| + static_cast<BookmarkSigninPromoCell*>(
|
| + [self.collectionView cellForItemAtIndexPath:indexPath]);
|
| + if (!signinPromoCell)
|
| + return;
|
| + // Should always reconfigure the cell size even if it has to be reloaded.
|
| + // -[BookmarkCollectionView cellSizeForIndexPath:] uses the current
|
| + // cell to compute its height.
|
| + [configurator configureSigninPromoView:signinPromoCell.signinPromoView];
|
| + if (identityChanged) {
|
| + // The section should be reload to update the cell height.
|
| + NSIndexSet* indexSet = [NSIndexSet indexSetWithIndex:self.promoSection];
|
| + [self.collectionView reloadSections:indexSet];
|
| + }
|
| +}
|
| +
|
| +#pragma mark - UIScrollViewDelegate
|
| +
|
| +- (void)scrollViewDidScroll:(UIScrollView*)scrollView {
|
| + [self updateShadowFrame];
|
| + [self collectionViewScrolled];
|
| +}
|
| +
|
| +#pragma mark - empty background
|
| +
|
| +// Schedules showing or hiding the empty bookmarks background view if the
|
| +// collection view is empty by calling showEmptyBackgroundIfNeeded after
|
| +// kShowEmptyBookmarksBackgroundRefreshDelay.
|
| +// Multiple call to this method will cancel previous scheduled call to
|
| +// showEmptyBackgroundIfNeeded before scheduling a new one.
|
| +- (void)scheduleEmptyBackgroundVisibilityUpdate {
|
| + [NSObject cancelPreviousPerformRequestsWithTarget:self
|
| + selector:@selector
|
| + (updateEmptyBackgroundVisibility)
|
| + object:nil];
|
| + [self performSelector:@selector(updateEmptyBackgroundVisibility)
|
| + withObject:nil
|
| + afterDelay:kShowEmptyBookmarksBackgroundRefreshDelay];
|
| +}
|
| +
|
| +- (BOOL)isCollectionViewEmpty {
|
| + BOOL collectionViewIsEmpty = YES;
|
| + const NSInteger numberOfSections = [self numberOfSections];
|
| + NSInteger section = [self shouldShowPromoCell] ? 1 : 0;
|
| + for (; collectionViewIsEmpty && section < numberOfSections; ++section) {
|
| + const NSInteger numberOfItemsInSection =
|
| + [self numberOfItemsInSection:section];
|
| + collectionViewIsEmpty = numberOfItemsInSection == 0;
|
| + }
|
| + return collectionViewIsEmpty;
|
| +}
|
| +
|
| +// Shows/hides empty bookmarks background view if the collections view is empty.
|
| +- (void)updateEmptyBackgroundVisibility {
|
| + const BOOL showEmptyBackground =
|
| + [self isCollectionViewEmpty] && ![self shouldShowPromoCell];
|
| + [self setEmptyBackgroundVisible:showEmptyBackground];
|
| +}
|
| +
|
| +// Shows/hides empty bookmarks background view with an animation.
|
| +- (void)setEmptyBackgroundVisible:(BOOL)emptyBackgroundVisible {
|
| + [UIView beginAnimations:@"alpha" context:NULL];
|
| + self.emptyCollectionBackgroundView.alpha = emptyBackgroundVisible ? 1 : 0;
|
| + [UIView commitAnimations];
|
| +}
|
| +
|
| +#pragma mark - UICollectionViewDataSource
|
| +
|
| +- (NSInteger)collectionView:(UICollectionView*)collectionView
|
| + numberOfItemsInSection:(NSInteger)section {
|
| + const NSInteger numberOfItemsInSection =
|
| + [self numberOfItemsInSection:section];
|
| + const BOOL isCollectionViewEmpty = [self isCollectionViewEmpty];
|
| + self.collectionView.scrollEnabled = !isCollectionViewEmpty;
|
| + if (isCollectionViewEmpty) {
|
| + [self scheduleEmptyBackgroundVisibilityUpdate];
|
| + } else {
|
| + // Hide empty bookmarks now.
|
| + [self setEmptyBackgroundVisible:NO];
|
| + }
|
| + return numberOfItemsInSection;
|
| +}
|
| +
|
| +- (NSInteger)numberOfSectionsInCollectionView:
|
| + (UICollectionView*)collectionView {
|
| + const NSInteger numberOfSections = [self numberOfSections];
|
| + const BOOL collectionViewIsEmpty = 0 == numberOfSections;
|
| + self.collectionView.scrollEnabled = !collectionViewIsEmpty;
|
| + if (collectionViewIsEmpty) {
|
| + [self scheduleEmptyBackgroundVisibilityUpdate];
|
| } else {
|
| // Hide empty bookmarks now.
|
| [self setEmptyBackgroundVisible:NO];
|
| @@ -413,15 +1019,43 @@ const NSTimeInterval kShowEmptyBookmarksBackgroundRefreshDelay = 1.0;
|
| return [self headerSizeForSection:section];
|
| }
|
|
|
| -#pragma mark - BookmarkItemCell callbacks
|
| +#pragma mark - UIGestureRecognizer Callbacks
|
|
|
| -- (void)didTapMenuButton:(BookmarkItemCell*)cell view:(UIView*)view {
|
| - [self didTapMenuButtonAtIndexPath:[self.collectionView indexPathForCell:cell]
|
| - onView:view
|
| - forCell:cell];
|
| +- (void)longPress:(UILongPressGestureRecognizer*)recognizer {
|
| + if (self.longPressRecognizer.numberOfTouches != 1 || self.editing)
|
| + return;
|
| +
|
| + if (self.longPressRecognizer.state == UIGestureRecognizerStateRecognized ||
|
| + self.longPressRecognizer.state == UIGestureRecognizerStateBegan) {
|
| + CGPoint point =
|
| + [self.longPressRecognizer locationOfTouch:0 inView:self.collectionView];
|
| + NSIndexPath* indexPath =
|
| + [self.collectionView indexPathForItemAtPoint:point];
|
| + if (!indexPath)
|
| + return;
|
| +
|
| + UICollectionViewCell* cell =
|
| + [self.collectionView cellForItemAtIndexPath:indexPath];
|
| +
|
| + // Notify the subclass that long press has been received.
|
| + if (cell)
|
| + [self didLongPressCell:cell atIndexPath:indexPath];
|
| + }
|
| +}
|
| +
|
| +#pragma mark - UIGestureRecognizerDelegate
|
| +
|
| +- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer*)gestureRecognizer {
|
| + DCHECK(gestureRecognizer == self.longPressRecognizer);
|
| + CGPoint point =
|
| + [gestureRecognizer locationOfTouch:0 inView:self.collectionView];
|
| + NSIndexPath* indexPath = [self.collectionView indexPathForItemAtPoint:point];
|
| + if (!indexPath)
|
| + return NO;
|
| + return [self allowLongPressForCellAtIndexPath:indexPath];
|
| }
|
|
|
| -#pragma mark - Convenience methods for subclasses
|
| +#pragma mark - Convenience methods
|
|
|
| - (void)updateCellAtIndexPath:(NSIndexPath*)indexPath
|
| withImage:(UIImage*)image
|
| @@ -466,6 +1100,10 @@ const NSTimeInterval kShowEmptyBookmarksBackgroundRefreshDelay = 1.0;
|
| return cell;
|
| }
|
|
|
| +// 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();
|
| }
|
| @@ -475,6 +1113,8 @@ const NSTimeInterval kShowEmptyBookmarksBackgroundRefreshDelay = 1.0;
|
| _faviconLoadTasks[IntegerPair(indexPath.section, indexPath.item)]);
|
| }
|
|
|
| +// Asynchronously loads favicon for given index path. The loads are cancelled
|
| +// upon cell reuse automatically.
|
| - (void)loadFaviconAtIndexPath:(NSIndexPath*)indexPath {
|
| // Cancel previous load attempts.
|
| [self cancelLoadingFaviconAtIndexPath:indexPath];
|
| @@ -483,35 +1123,36 @@ const NSTimeInterval kShowEmptyBookmarksBackgroundRefreshDelay = 1.0;
|
| __weak BookmarkCollectionView* weakSelf = self;
|
| const bookmarks::BookmarkNode* node = [self nodeAtIndexPath:indexPath];
|
| GURL blockURL(node->url());
|
| - void (^faviconBlock)(const favicon_base::LargeIconResult&) = ^(
|
| - const favicon_base::LargeIconResult& result) {
|
| - BookmarkCollectionView* strongSelf = weakSelf;
|
| - if (!strongSelf)
|
| - return;
|
| - UIImage* favIcon = nil;
|
| - UIColor* backgroundColor = nil;
|
| - UIColor* textColor = nil;
|
| - NSString* fallbackText = 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()]];
|
| - } else if (result.fallback_icon_style) {
|
| - backgroundColor = skia::UIColorFromSkColor(
|
| - result.fallback_icon_style->background_color);
|
| - textColor =
|
| - skia::UIColorFromSkColor(result.fallback_icon_style->text_color);
|
| -
|
| - fallbackText =
|
| - base::SysUTF16ToNSString(favicon::GetFallbackIconText(blockURL));
|
| - }
|
| -
|
| - [strongSelf updateCellAtIndexPath:indexPath
|
| - withImage:favIcon
|
| - backgroundColor:backgroundColor
|
| - textColor:textColor
|
| - fallbackText:fallbackText];
|
| - };
|
| + void (^faviconBlock)(const favicon_base::LargeIconResult&) =
|
| + ^(const favicon_base::LargeIconResult& result) {
|
| + BookmarkCollectionView* strongSelf = weakSelf;
|
| + if (!strongSelf)
|
| + return;
|
| + UIImage* favIcon = nil;
|
| + UIColor* backgroundColor = nil;
|
| + UIColor* textColor = nil;
|
| + NSString* fallbackText = 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()]];
|
| + } else if (result.fallback_icon_style) {
|
| + backgroundColor = skia::UIColorFromSkColor(
|
| + result.fallback_icon_style->background_color);
|
| + textColor =
|
| + skia::UIColorFromSkColor(result.fallback_icon_style->text_color);
|
| +
|
| + fallbackText =
|
| + base::SysUTF16ToNSString(favicon::GetFallbackIconText(blockURL));
|
| + }
|
| +
|
| + [strongSelf updateCellAtIndexPath:indexPath
|
| + withImage:favIcon
|
| + backgroundColor:backgroundColor
|
| + textColor:textColor
|
| + fallbackText:fallbackText];
|
| + };
|
|
|
| CGFloat scale = [UIScreen mainScreen].scale;
|
| CGFloat preferredSize = scale * [BookmarkItemCell preferredImageSize];
|
| @@ -542,6 +1183,7 @@ const NSTimeInterval kShowEmptyBookmarksBackgroundRefreshDelay = 1.0;
|
| return cell;
|
| }
|
|
|
| +// Updates the editing state for the cell.
|
| - (void)updateEditingStateOfCell:(BookmarkCell*)cell
|
| atIndexPath:(NSIndexPath*)indexPath
|
| animateMenuVisibility:(BOOL)animateMenuVisibility
|
| @@ -553,6 +1195,12 @@ const NSTimeInterval kShowEmptyBookmarksBackgroundRefreshDelay = 1.0;
|
| animated:animateMenuVisibility];
|
| }
|
|
|
| +// |animateMenuVisibility| refers to whether the change in the visibility of the
|
| +// menu button is animated.
|
| +// |animateSelectedState| refers to whether the change in the selected state (in
|
| +// editing mode) of the cell is animated.
|
| +// This method updates the visibility of the menu button.
|
| +// This method updates the selected state of the cell (in editing mode).
|
| - (void)updateEditingStateOfCellAtIndexPath:(NSIndexPath*)indexPath
|
| animateMenuVisibility:(BOOL)animateMenuVisibility
|
| animateSelectedState:(BOOL)animateSelectedState {
|
| @@ -567,262 +1215,30 @@ const NSTimeInterval kShowEmptyBookmarksBackgroundRefreshDelay = 1.0;
|
| animateSelectedState:animateSelectedState];
|
| }
|
|
|
| -#pragma mark - BookmarkModelObserver Callbacks
|
| -
|
| -- (void)bookmarkModelLoaded {
|
| - NOTREACHED();
|
| -}
|
| -
|
| -- (void)bookmarkNodeChanged:(const BookmarkNode*)bookmarkNode {
|
| - NOTREACHED();
|
| -}
|
| -
|
| -- (void)bookmarkNodeChildrenChanged:(const BookmarkNode*)bookmarkNode {
|
| - NOTREACHED();
|
| -}
|
| -
|
| -- (void)bookmarkNode:(const BookmarkNode*)bookmarkNode
|
| - movedFromParent:(const BookmarkNode*)oldParent
|
| - toParent:(const BookmarkNode*)newParent {
|
| - NOTREACHED();
|
| -}
|
| -
|
| -- (void)bookmarkNodeDeleted:(const BookmarkNode*)node
|
| - fromFolder:(const BookmarkNode*)folder {
|
| - NOTREACHED();
|
| -}
|
| -
|
| -- (void)bookmarkModelRemovedAllNodes {
|
| - NOTREACHED();
|
| -}
|
| -
|
| -#pragma mark - Public Methods That Must Be Overridden
|
| -
|
| -- (BOOL)shouldSelectCellAtIndexPath:(NSIndexPath*)indexPath {
|
| - NOTREACHED();
|
| - return NO;
|
| -}
|
| -
|
| -- (void)didTapCellAtIndexPath:(NSIndexPath*)indexPath {
|
| - NOTREACHED();
|
| -}
|
| -
|
| -- (void)didAddCellForEditingAtIndexPath:(NSIndexPath*)indexPath {
|
| - NOTREACHED();
|
| -}
|
| -
|
| -- (void)didRemoveCellForEditingAtIndexPath:(NSIndexPath*)indexPath {
|
| - NOTREACHED();
|
| -}
|
| -
|
| -- (void)didTapMenuButtonAtIndexPath:(NSIndexPath*)indexPath
|
| - onView:(UIView*)view
|
| - forCell:(BookmarkItemCell*)cell {
|
| - NOTREACHED();
|
| -}
|
| -
|
| -- (bookmark_cell::ButtonType)buttonTypeForCellAtIndexPath:
|
| - (NSIndexPath*)indexPath {
|
| - NOTREACHED();
|
| - return bookmark_cell::ButtonNone;
|
| -}
|
| -
|
| -- (BOOL)allowLongPressForCellAtIndexPath:(NSIndexPath*)indexPath {
|
| - NOTREACHED();
|
| - return NO;
|
| -}
|
| -
|
| -- (void)didLongPressCell:(UICollectionViewCell*)cell
|
| - atIndexPath:(NSIndexPath*)indexPath {
|
| - NOTREACHED();
|
| -}
|
| -
|
| -- (BOOL)cellIsSelectedForEditingAtIndexPath:(NSIndexPath*)indexPath {
|
| - NOTREACHED();
|
| - return NO;
|
| -}
|
| -
|
| -- (CGSize)headerSizeForSection:(NSInteger)section {
|
| - NOTREACHED();
|
| - return CGSizeZero;
|
| -}
|
| -
|
| -- (UICollectionViewCell*)cellAtIndexPath:(NSIndexPath*)indexPath {
|
| - NOTREACHED();
|
| - return nil;
|
| -}
|
| -
|
| -- (UICollectionReusableView*)headerAtIndexPath:(NSIndexPath*)indexPath {
|
| - NOTREACHED();
|
| - return nil;
|
| -}
|
| -
|
| -- (NSInteger)numberOfItemsInSection:(NSInteger)section {
|
| - NOTREACHED();
|
| - return 0;
|
| -}
|
| -- (NSInteger)numberOfSections {
|
| - NOTREACHED();
|
| - return 0;
|
| -}
|
| -
|
| -- (void)updateCollectionView {
|
| - NOTREACHED();
|
| -}
|
| -
|
| -- (const bookmarks::BookmarkNode*)nodeAtIndexPath:(NSIndexPath*)indexPath {
|
| - NOTREACHED();
|
| - return nullptr;
|
| -}
|
| -
|
| -#pragma mark - Methods that subclasses can override (UI)
|
| -
|
| -- (UIEdgeInsets)insetForSectionAtIndex:(NSInteger)section {
|
| - if ([self isPromoSection:section])
|
| - return UIEdgeInsetsZero;
|
| -
|
| - if (IsIPadIdiom()) {
|
| - return UIEdgeInsetsMake(10, rowMarginTablet, 0, rowMarginTablet);
|
| - } else {
|
| - return UIEdgeInsetsZero;
|
| - }
|
| -}
|
| -
|
| -- (CGSize)cellSizeForIndexPath:(NSIndexPath*)indexPath {
|
| - DCHECK(![self isPromoSection:indexPath.section]);
|
| - UIEdgeInsets insets = [self insetForSectionAtIndex:indexPath.section];
|
| - return CGSizeMake(self.bounds.size.width - (insets.right + insets.left),
|
| - rowHeight);
|
| -}
|
| -
|
| +// The minimal horizontal space between items to respect between cells in
|
| +// |section|.
|
| - (CGFloat)minimumInteritemSpacingForSectionAtIndex:(NSInteger)section {
|
| return 0;
|
| }
|
|
|
| +// The minimal vertical space between items to respect between cells in
|
| +// |section|.
|
| - (CGFloat)minimumLineSpacingForSectionAtIndex:(NSInteger)section {
|
| return 0;
|
| }
|
|
|
| +// The text to display when there are no items in the collection. Default is
|
| +// |IDS_IOS_BOOKMARK_NO_BOOKMARKS_LABEL|.
|
| - (NSString*)textWhenCollectionIsEmpty {
|
| return l10n_util::GetNSString(IDS_IOS_BOOKMARK_NO_BOOKMARKS_LABEL);
|
| }
|
|
|
| -#pragma mark - Public Methods That Can Be Overridden
|
| -
|
| -- (void)collectionViewScrolled {
|
| -}
|
| -
|
| -#pragma mark - Editing
|
| -
|
| -- (void)setEditing:(BOOL)editing animated:(BOOL)animated {
|
| - if (self.editing == editing)
|
| - return;
|
| -
|
| - _editing = editing;
|
| - [UIView animateWithDuration:animated ? 0.2 : 0.0
|
| - delay:0
|
| - options:UIViewAnimationOptionBeginFromCurrentState
|
| - animations:^{
|
| - self.shadow.alpha = editing ? 0.0 : 1.0;
|
| - }
|
| - completion:nil];
|
| -
|
| - // If the promo is active this means that it is removed and added as the edit
|
| - // mode changes, making reloading the data mandatory.
|
| - if ([self isPromoActive]) {
|
| - [self.collectionView reloadData];
|
| - [self.collectionView.collectionViewLayout invalidateLayout];
|
| - } else {
|
| - // Update the visual state of the bookmark cells without reloading the
|
| - // section.
|
| - // This prevents flickering of images that need to be asynchronously
|
| - // reloaded.
|
| - NSArray* indexPaths = [self.collectionView indexPathsForVisibleItems];
|
| - for (NSIndexPath* indexPath in indexPaths) {
|
| - [self updateEditingStateOfCellAtIndexPath:indexPath
|
| - animateMenuVisibility:animated
|
| - animateSelectedState:NO];
|
| - }
|
| - }
|
| -}
|
| -
|
| -#pragma mark - Public Methods
|
| -
|
| -- (void)changeOrientation:(UIInterfaceOrientation)orientation {
|
| - [self updateCollectionView];
|
| -}
|
| -
|
| -- (CGFloat)contentPositionInPortraitOrientation {
|
| - if (IsPortrait())
|
| - return self.collectionView.contentOffset.y;
|
| -
|
| - // In short landscape mode and portrait mode, there are 2 cells per row.
|
| - if ([self wideLandscapeMode])
|
| - return self.collectionView.contentOffset.y;
|
| -
|
| - // In wide landscape mode, there are 3 cells per row.
|
| - return self.collectionView.contentOffset.y * 3 / 2.0;
|
| -}
|
| -
|
| -- (void)applyContentPosition:(CGFloat)position {
|
| - if (IsLandscape() && [self wideLandscapeMode]) {
|
| - position = position * 2 / 3.0;
|
| - }
|
| -
|
| - CGFloat y =
|
| - MIN(position,
|
| - [self.collectionView.collectionViewLayout collectionViewContentSize]
|
| - .height);
|
| - self.collectionView.contentOffset =
|
| - CGPointMake(self.collectionView.contentOffset.x, y);
|
| -}
|
| -
|
| -- (void)setScrollsToTop:(BOOL)scrollsToTop {
|
| - self.collectionView.scrollsToTop = scrollsToTop;
|
| -}
|
| -
|
| -#pragma mark - Private Methods
|
| -
|
| +// In landscape mode, there are 2 widths: 480pt and 568pt. Returns YES if the
|
| +// width is 568pt.
|
| - (BOOL)wideLandscapeMode {
|
| return self.frame.size.width > 567;
|
| }
|
|
|
| -#pragma mark - UIGestureRecognizer Callbacks
|
| -
|
| -- (void)longPress:(UILongPressGestureRecognizer*)recognizer {
|
| - if (self.longPressRecognizer.numberOfTouches != 1 || self.editing)
|
| - return;
|
| -
|
| - if (self.longPressRecognizer.state == UIGestureRecognizerStateRecognized ||
|
| - self.longPressRecognizer.state == UIGestureRecognizerStateBegan) {
|
| - CGPoint point =
|
| - [self.longPressRecognizer locationOfTouch:0 inView:self.collectionView];
|
| - NSIndexPath* indexPath =
|
| - [self.collectionView indexPathForItemAtPoint:point];
|
| - if (!indexPath)
|
| - return;
|
| -
|
| - UICollectionViewCell* cell =
|
| - [self.collectionView cellForItemAtIndexPath:indexPath];
|
| -
|
| - // Notify the subclass that long press has been received.
|
| - if (cell)
|
| - [self didLongPressCell:cell atIndexPath:indexPath];
|
| - }
|
| -}
|
| -
|
| -#pragma mark - UIGestureRecognizerDelegate
|
| -
|
| -- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer*)gestureRecognizer {
|
| - DCHECK(gestureRecognizer == self.longPressRecognizer);
|
| - CGPoint point =
|
| - [gestureRecognizer locationOfTouch:0 inView:self.collectionView];
|
| - NSIndexPath* indexPath = [self.collectionView indexPathForItemAtPoint:point];
|
| - if (!indexPath)
|
| - return NO;
|
| - return [self allowLongPressForCellAtIndexPath:indexPath];
|
| -}
|
| -
|
| #pragma mark - Promo Cell
|
|
|
| - (BOOL)isPromoSection:(NSInteger)section {
|
| @@ -830,7 +1246,7 @@ const NSTimeInterval kShowEmptyBookmarksBackgroundRefreshDelay = 1.0;
|
| }
|
|
|
| - (BOOL)shouldShowPromoCell {
|
| - return NO;
|
| + return _promoVisible;
|
| }
|
|
|
| - (BOOL)isPromoActive {
|
|
|