blob: 01654b9867960432c4225d6ff1c66d7ce0d16cdb [file] [log] [blame]
// Copyright 2014 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_collection_view.h"
#import <UIKit/UIGestureRecognizerSubclass.h>
#include <algorithm>
#include <map>
#include <memory>
#include "base/mac/bind_objc_block.h"
#include "base/mac/foundation_util.h"
#include "base/strings/sys_string_conversions.h"
#include "components/bookmarks/browser/bookmark_model.h"
#include "components/bookmarks/browser/bookmark_model_observer.h"
#include "components/favicon/core/fallback_url_util.h"
#include "components/favicon/core/large_icon_service.h"
#include "components/favicon_base/fallback_icon_style.h"
#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/favicon/ios_chrome_large_icon_service_factory.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"
#import "ios/chrome/browser/ui/bookmarks/bookmark_utils_ios.h"
#include "ios/chrome/browser/ui/ui_util.h"
#import "ios/chrome/browser/ui/uikit_ui_util.h"
#include "ios/chrome/grit/ios_strings.h"
#include "skia/ext/skia_utils_ios.h"
#include "ui/base/l10n/l10n_util_mac.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
using bookmarks::BookmarkNode;
namespace {
// Used to store a pair of NSIntegers when storing a NSIndexPath in C++
// collections.
using IntegerPair = std::pair<NSInteger, NSInteger>;
// The margin between the side of the view and the first and last tile.
CGFloat rowMarginTablet = 24.0;
CGFloat rowHeight = 48.0;
// Minimal acceptable favicon size, in points.
CGFloat minFaviconSizePt = 16;
// Delay in seconds to which the empty background view will be shown when the
// collection view is empty.
// 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,
UICollectionViewDelegateFlowLayout,
UIGestureRecognizerDelegate> {
std::unique_ptr<bookmarks::BookmarkModelBridge> _modelBridge;
ios::ChromeBrowserState* _browserState;
// 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;
}
// Redefined to be readwrite.
@property(nonatomic, assign) bookmarks::BookmarkModel* bookmarkModel;
// Redefined to be readwrite.
@property(nonatomic, strong) UICollectionView* collectionView;
// Redefined to be readwrite.
@property(nonatomic, assign) BOOL editing;
// Detects a long press on a cell.
@property(nonatomic, strong) UILongPressGestureRecognizer* longPressRecognizer;
// Background view of the collection view shown when there is no items.
@property(nonatomic, strong)
BookmarkCollectionViewBackground* emptyCollectionBackgroundView;
// 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;
// In landscape mode, there are 2 widths: 480pt and 568pt. Returns YES if the
// width is 568pt.
- (BOOL)wideLandscapeMode;
// 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 bookmarkModel = _bookmarkModel;
@synthesize collectionView = _collectionView;
@synthesize editing = _editing;
@synthesize emptyCollectionBackgroundView = _emptyCollectionBackgroundView;
@synthesize loader = _loader;
@synthesize longPressRecognizer = _longPressRecognizer;
@synthesize browserState = _browserState;
@synthesize shadow = _shadow;
#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 =
[[UICollectionViewFlowLayout alloc] init];
UICollectionView* collectionView =
[[UICollectionView alloc] initWithFrame:self.bounds
collectionViewLayout:layout];
self.collectionView = collectionView;
self.collectionView.backgroundColor = [UIColor clearColor];
self.collectionView.autoresizingMask =
UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
self.collectionView.alwaysBounceVertical = YES;
self.collectionView.delegate = self;
self.collectionView.dataSource = self;
[self.collectionView registerClass:[BookmarkFolderCell class]
forCellWithReuseIdentifier:[BookmarkFolderCell reuseIdentifier]];
[self.collectionView registerClass:[BookmarkItemCell class]
forCellWithReuseIdentifier:[BookmarkItemCell reuseIdentifier]];
[self.collectionView registerClass:[BookmarkHeaderView class]
forSupplementaryViewOfKind:UICollectionElementKindSectionHeader
withReuseIdentifier:[BookmarkHeaderView reuseIdentifier]];
[self.collectionView
registerClass:[BookmarkHeaderSeparatorView class]
forSupplementaryViewOfKind:UICollectionElementKindSectionHeader
withReuseIdentifier:[BookmarkHeaderSeparatorView reuseIdentifier]];
[self.collectionView registerClass:[BookmarkPromoCell class]
forCellWithReuseIdentifier:[BookmarkPromoCell reuseIdentifier]];
[self addSubview:self.collectionView];
// Set up the background view shown when the collection is empty.
BookmarkCollectionViewBackground* emptyCollectionBackgroundView =
[[BookmarkCollectionViewBackground alloc] initWithFrame:CGRectZero];
self.emptyCollectionBackgroundView = emptyCollectionBackgroundView;
self.emptyCollectionBackgroundView.autoresizingMask =
UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
self.emptyCollectionBackgroundView.alpha = 0;
self.emptyCollectionBackgroundView.text = [self textWhenCollectionIsEmpty];
self.emptyCollectionBackgroundView.frame = self.collectionView.bounds;
self.collectionView.backgroundView = self.emptyCollectionBackgroundView;
[self updateShadow];
self.longPressRecognizer = [[UILongPressGestureRecognizer alloc]
initWithTarget:self
action:@selector(longPress:)];
self.longPressRecognizer.delegate = self;
[self.collectionView addGestureRecognizer:self.longPressRecognizer];
}
- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
[self updateShadow];
}
- (void)updateShadow {
// Remove the current one, if any.
[self.shadow removeFromSuperview];
if (IsCompact(self)) {
self.shadow =
bookmark_utils_ios::dropShadowWithWidth(CGRectGetWidth(self.bounds));
} else {
self.shadow = [[UIView alloc]
initWithFrame:CGRectMake(0, 0, CGRectGetWidth(self.bounds),
1 / [[UIScreen mainScreen] scale])];
self.shadow.backgroundColor = [UIColor colorWithWhite:0.0 alpha:.12];
}
[self updateShadowFrame];
self.shadow.autoresizingMask = UIViewAutoresizingFlexibleWidth;
if (self.editing)
self.shadow.alpha = 0.0;
// Add the new shadow.
[self addSubview:self.shadow];
}
- (void)updateShadowFrame {
CGFloat shadowHeight = CGRectGetHeight(self.shadow.frame);
CGFloat y = std::min<CGFloat>(
0.0, self.collectionView.contentOffset.y - shadowHeight);
self.shadow.frame =
CGRectMake(0, y, CGRectGetWidth(self.bounds), shadowHeight);
}
#pragma mark - UIScrollViewDelegate
- (void)scrollViewDidScroll:(UIScrollView*)scrollView {
[self updateShadowFrame];
[self collectionViewScrolled];
}
#pragma mark - empty background
- (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;
}
- (void)updateEmptyBackgroundVisibility {
const BOOL showEmptyBackground =
[self isCollectionViewEmpty] && ![self shouldShowPromoCell];
[self setEmptyBackgroundVisible:showEmptyBackground];
}
- (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];
}
return numberOfSections;
}
- (UICollectionViewCell*)collectionView:(UICollectionView*)collectionView
cellForItemAtIndexPath:(NSIndexPath*)indexPath {
return [self cellAtIndexPath:indexPath];
}
- (UICollectionReusableView*)collectionView:(UICollectionView*)collectionView
viewForSupplementaryElementOfKind:(NSString*)kind
atIndexPath:(NSIndexPath*)indexPath {
return [self headerAtIndexPath:indexPath];
}
- (BOOL)collectionView:(UICollectionView*)collectionView
shouldSelectItemAtIndexPath:(NSIndexPath*)indexPath {
return [self shouldSelectCellAtIndexPath:indexPath];
}
- (void)collectionView:(UICollectionView*)collectionView
didSelectItemAtIndexPath:(NSIndexPath*)indexPath {
if (self.editing)
[self toggleSelectedForEditingAtIndexPath:indexPath];
else
[self didTapCellAtIndexPath:indexPath];
}
- (void)toggleSelectedForEditingAtIndexPath:(NSIndexPath*)indexPath {
BOOL selected = [self cellIsSelectedForEditingAtIndexPath:indexPath];
if (selected)
[self didRemoveCellForEditingAtIndexPath:indexPath];
else
[self didAddCellForEditingAtIndexPath:indexPath];
[self updateEditingStateOfCellAtIndexPath:indexPath
animateMenuVisibility:NO
animateSelectedState:YES];
}
#pragma mark - UICollectionViewDelegate
- (void)collectionView:(UICollectionView*)collectionView
didEndDisplayingCell:(UICollectionViewCell*)cell
forItemAtIndexPath:(NSIndexPath*)indexPath {
_faviconTaskTracker.TryCancel(
_faviconLoadTasks[IntegerPair(indexPath.section, indexPath.item)]);
}
#pragma mark - UICollectionViewDelegateFlowLayout
- (UIEdgeInsets)collectionView:(UICollectionView*)collectionView
layout:(UICollectionViewLayout*)collectionViewLayout
insetForSectionAtIndex:(NSInteger)section {
return [self insetForSectionAtIndex:section];
}
- (CGFloat)collectionView:(UICollectionView*)collectionView
layout:(UICollectionViewLayout*)
collectionViewLayout
minimumInteritemSpacingForSectionAtIndex:(NSInteger)section {
return [self minimumInteritemSpacingForSectionAtIndex:section];
}
- (CGFloat)collectionView:(UICollectionView*)collectionView
layout:(UICollectionViewLayout*)layout
minimumLineSpacingForSectionAtIndex:(NSInteger)section {
return [self minimumLineSpacingForSectionAtIndex:section];
}
- (CGSize)collectionView:(UICollectionView*)collectionView
layout:(UICollectionViewLayout*)collectionViewLayout
sizeForItemAtIndexPath:(NSIndexPath*)indexPath {
return [self cellSizeForIndexPath:indexPath];
}
- (CGSize)collectionView:(UICollectionView*)collectionView
layout:
(UICollectionViewLayout*)collectionViewLayout
referenceSizeForHeaderInSection:(NSInteger)section {
return [self headerSizeForSection:section];
}
#pragma mark - BookmarkItemCell callbacks
- (void)didTapMenuButton:(BookmarkItemCell*)cell view:(UIView*)view {
[self didTapMenuButtonAtIndexPath:[self.collectionView indexPathForCell:cell]
onView:view
forCell:cell];
}
#pragma mark - Convenience methods for subclasses
- (void)updateCellAtIndexPath:(NSIndexPath*)indexPath
withImage:(UIImage*)image
backgroundColor:(UIColor*)backgroundColor
textColor:(UIColor*)textColor
fallbackText:(NSString*)text {
BookmarkItemCell* cell = base::mac::ObjCCast<BookmarkItemCell>(
[self.collectionView cellForItemAtIndexPath:indexPath]);
if (!cell)
return;
if (image) {
[cell setImage:image];
} else {
[cell setPlaceholderText:text
textColor:textColor
backgroundColor:backgroundColor];
}
}
- (BookmarkItemCell*)cellForBookmark:(const BookmarkNode*)node
indexPath:(NSIndexPath*)indexPath {
DCHECK(![self isPromoSection:indexPath.section]);
BOOL selected = [self cellIsSelectedForEditingAtIndexPath:indexPath];
BookmarkItemCell* cell = [self.collectionView
dequeueReusableCellWithReuseIdentifier:[BookmarkItemCell reuseIdentifier]
forIndexPath:indexPath];
[cell updateWithTitle:bookmark_utils_ios::TitleForBookmarkNode(node)];
[cell setSelectedForEditing:selected animated:NO];
[cell setButtonTarget:self action:@selector(didTapMenuButton:view:)];
[self updateEditingStateOfCell:cell
atIndexPath:indexPath
animateMenuVisibility:NO
animateSelectedState:NO];
[self loadFaviconAtIndexPath:indexPath];
return cell;
}
- (void)cancelAllFaviconLoads {
_faviconTaskTracker.TryCancelAll();
}
- (void)cancelLoadingFaviconAtIndexPath:(NSIndexPath*)indexPath {
_faviconTaskTracker.TryCancel(
_faviconLoadTasks[IntegerPair(indexPath.section, indexPath.item)]);
}
- (void)loadFaviconAtIndexPath:(NSIndexPath*)indexPath {
// Cancel previous load attempts.
[self cancelLoadingFaviconAtIndexPath:indexPath];
// Start loading a favicon.
__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];
};
CGFloat scale = [UIScreen mainScreen].scale;
CGFloat preferredSize = scale * [BookmarkItemCell preferredImageSize];
CGFloat minSize = scale * minFaviconSizePt;
base::CancelableTaskTracker::TaskId taskId =
IOSChromeLargeIconServiceFactory::GetForBrowserState(self.browserState)
->GetLargeIconOrFallbackStyle(node->url(), minSize, preferredSize,
base::BindBlockArc(faviconBlock),
&_faviconTaskTracker);
_faviconLoadTasks[IntegerPair(indexPath.section, indexPath.item)] = taskId;
}
- (BookmarkFolderCell*)cellForFolder:(const BookmarkNode*)node
indexPath:(NSIndexPath*)indexPath {
DCHECK(![self isPromoSection:indexPath.section]);
BookmarkFolderCell* cell = [self.collectionView
dequeueReusableCellWithReuseIdentifier:[BookmarkFolderCell
reuseIdentifier]
forIndexPath:indexPath];
[self updateEditingStateOfCell:cell
atIndexPath:indexPath
animateMenuVisibility:NO
animateSelectedState:NO];
[cell updateWithTitle:bookmark_utils_ios::TitleForBookmarkNode(node)];
[cell setButtonTarget:self action:@selector(didTapMenuButton:view:)];
return cell;
}
- (void)updateEditingStateOfCell:(BookmarkCell*)cell
atIndexPath:(NSIndexPath*)indexPath
animateMenuVisibility:(BOOL)animateMenuVisibility
animateSelectedState:(BOOL)animateSelectedState {
BOOL selected = [self cellIsSelectedForEditingAtIndexPath:indexPath];
[cell setSelectedForEditing:selected animated:animateSelectedState];
BookmarkItemCell* itemCell = static_cast<BookmarkItemCell*>(cell);
[itemCell showButtonOfType:[self buttonTypeForCellAtIndexPath:indexPath]
animated:animateMenuVisibility];
}
- (void)updateEditingStateOfCellAtIndexPath:(NSIndexPath*)indexPath
animateMenuVisibility:(BOOL)animateMenuVisibility
animateSelectedState:(BOOL)animateSelectedState {
BookmarkCell* cell = base::mac::ObjCCast<BookmarkCell>(
[self.collectionView cellForItemAtIndexPath:indexPath]);
if (!cell)
return;
[self updateEditingStateOfCell:cell
atIndexPath:indexPath
animateMenuVisibility:animateMenuVisibility
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 {
if ([self isPromoSection:indexPath.section]) {
CGRect estimatedFrame = CGRectMake(0, 0, CGRectGetWidth(self.bounds), 100);
UICollectionViewCell* cell =
[self.collectionView cellForItemAtIndexPath:indexPath];
if (!cell) {
cell = [[BookmarkPromoCell alloc] initWithFrame:estimatedFrame];
}
cell.frame = estimatedFrame;
[cell layoutIfNeeded];
return [cell systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
}
UIEdgeInsets insets = [self insetForSectionAtIndex:indexPath.section];
return CGSizeMake(self.bounds.size.width - (insets.right + insets.left),
rowHeight);
}
- (CGFloat)minimumInteritemSpacingForSectionAtIndex:(NSInteger)section {
return 0;
}
- (CGFloat)minimumLineSpacingForSectionAtIndex:(NSInteger)section {
return 0;
}
- (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
- (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 {
return section == 0 && [self shouldShowPromoCell];
}
- (BOOL)shouldShowPromoCell {
return NO;
}
- (BOOL)isPromoActive {
return NO;
}
@end