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 |
new file mode 100644 |
index 0000000000000000000000000000000000000000..e342f092a4d743ec0c3ae614a2943f93f13a33f2 |
--- /dev/null |
+++ b/ios/chrome/browser/ui/bookmarks/bookmark_collection_view.mm |
@@ -0,0 +1,859 @@ |
+// 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/ios/weak_nsobject.h" |
+#include "base/mac/bind_objc_block.h" |
+#include "base/mac/foundation_util.h" |
+#include "base/mac/objc_property_releaser.h" |
+#include "base/mac/scoped_nsobject.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" |
+ |
+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; |
+ |
+ base::mac::ObjCPropertyReleaser _propertyReleaser_BookmarkCollectionView; |
+ |
+ // 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, retain) UICollectionView* collectionView; |
+// Redefined to be readwrite. |
+@property(nonatomic, assign) BOOL editing; |
+// Detects a long press on a cell. |
+@property(nonatomic, retain) UILongPressGestureRecognizer* longPressRecognizer; |
+// Background view of the collection view shown when there is no items. |
+@property(nonatomic, retain) |
+ BookmarkCollectionViewBackground* emptyCollectionBackgroundView; |
+// Shadow to display over the content. |
+@property(nonatomic, retain) 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) { |
+ _propertyReleaser_BookmarkCollectionView.Init( |
+ self, [BookmarkCollectionView class]); |
+ |
+ _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(); |
+ [super dealloc]; |
+} |
+ |
+- (void)setupViews { |
+ self.backgroundColor = bookmark_utils_ios::mainBackgroundColor(); |
+ base::scoped_nsobject<UICollectionViewFlowLayout> layout( |
+ [[UICollectionViewFlowLayout alloc] init]); |
+ |
+ base::scoped_nsobject<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. |
+ base::scoped_nsobject<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 = |
+ base::scoped_nsobject<UILongPressGestureRecognizer>( |
+ [[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])] |
+ autorelease]; |
+ 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. |
+ base::WeakNSObject<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) { |
+ base::scoped_nsobject<BookmarkCollectionView> strongSelf([weakSelf retain]); |
+ 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.get(); |
+ 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::BindBlock(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] |
+ autorelease]; |
+ } |
+ 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 |