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 { |