| OLD | NEW |
| 1 // Copyright 2014 The Chromium Authors. All rights reserved. | 1 // Copyright 2014 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 #import "ios/chrome/browser/ui/bookmarks/bookmark_collection_view.h" | 5 #import "ios/chrome/browser/ui/bookmarks/bookmark_collection_view.h" |
| 6 | 6 |
| 7 #import <UIKit/UIGestureRecognizerSubclass.h> | 7 #import <UIKit/UIGestureRecognizerSubclass.h> |
| 8 #include <algorithm> | 8 #include <algorithm> |
| 9 #include <map> | 9 #include <map> |
| 10 #include <memory> | 10 #include <memory> |
| 11 | 11 |
| 12 #include "base/logging.h" |
| 12 #include "base/mac/bind_objc_block.h" | 13 #include "base/mac/bind_objc_block.h" |
| 13 #include "base/mac/foundation_util.h" | 14 #include "base/mac/foundation_util.h" |
| 14 #include "base/strings/sys_string_conversions.h" | 15 #include "base/strings/sys_string_conversions.h" |
| 15 #include "components/bookmarks/browser/bookmark_model.h" | 16 #include "components/bookmarks/browser/bookmark_model.h" |
| 16 #include "components/bookmarks/browser/bookmark_model_observer.h" | 17 #include "components/bookmarks/browser/bookmark_model_observer.h" |
| 17 #include "components/favicon/core/fallback_url_util.h" | 18 #include "components/favicon/core/fallback_url_util.h" |
| 18 #include "components/favicon/core/large_icon_service.h" | 19 #include "components/favicon/core/large_icon_service.h" |
| 19 #include "components/favicon_base/fallback_icon_style.h" | 20 #include "components/favicon_base/fallback_icon_style.h" |
| 20 #include "components/favicon_base/favicon_types.h" | 21 #include "components/favicon_base/favicon_types.h" |
| 21 #include "ios/chrome/browser/bookmarks/bookmark_model_factory.h" | 22 #include "ios/chrome/browser/bookmarks/bookmark_model_factory.h" |
| 22 #include "ios/chrome/browser/bookmarks/bookmarks_utils.h" | 23 #include "ios/chrome/browser/bookmarks/bookmarks_utils.h" |
| 24 #include "ios/chrome/browser/experimental_flags.h" |
| 23 #include "ios/chrome/browser/favicon/ios_chrome_large_icon_service_factory.h" | 25 #include "ios/chrome/browser/favicon/ios_chrome_large_icon_service_factory.h" |
| 26 #import "ios/chrome/browser/ui/authentication/signin_promo_view.h" |
| 27 #import "ios/chrome/browser/ui/authentication/signin_promo_view_configurator.h" |
| 28 #import "ios/chrome/browser/ui/authentication/signin_promo_view_consumer.h" |
| 29 #import "ios/chrome/browser/ui/authentication/signin_promo_view_mediator.h" |
| 24 #import "ios/chrome/browser/ui/bookmarks/bookmark_collection_cells.h" | 30 #import "ios/chrome/browser/ui/bookmarks/bookmark_collection_cells.h" |
| 25 #import "ios/chrome/browser/ui/bookmarks/bookmark_collection_view_background.h" | 31 #import "ios/chrome/browser/ui/bookmarks/bookmark_collection_view_background.h" |
| 26 #import "ios/chrome/browser/ui/bookmarks/bookmark_promo_cell.h" | 32 #import "ios/chrome/browser/ui/bookmarks/bookmark_promo_cell.h" |
| 27 #import "ios/chrome/browser/ui/bookmarks/bookmark_signin_promo_cell.h" | 33 #import "ios/chrome/browser/ui/bookmarks/bookmark_signin_promo_cell.h" |
| 28 #import "ios/chrome/browser/ui/bookmarks/bookmark_utils_ios.h" | 34 #import "ios/chrome/browser/ui/bookmarks/bookmark_utils_ios.h" |
| 29 #include "ios/chrome/browser/ui/ui_util.h" | 35 #include "ios/chrome/browser/ui/ui_util.h" |
| 30 #import "ios/chrome/browser/ui/uikit_ui_util.h" | 36 #import "ios/chrome/browser/ui/uikit_ui_util.h" |
| 31 #include "ios/chrome/grit/ios_strings.h" | 37 #include "ios/chrome/grit/ios_strings.h" |
| 32 #include "skia/ext/skia_utils_ios.h" | 38 #include "skia/ext/skia_utils_ios.h" |
| 33 #include "ui/base/l10n/l10n_util_mac.h" | 39 #include "ui/base/l10n/l10n_util_mac.h" |
| 34 | 40 |
| 35 #if !defined(__has_feature) || !__has_feature(objc_arc) | 41 #if !defined(__has_feature) || !__has_feature(objc_arc) |
| 36 #error "This file requires ARC support." | 42 #error "This file requires ARC support." |
| 37 #endif | 43 #endif |
| 38 | 44 |
| 39 using bookmarks::BookmarkNode; | 45 using bookmarks::BookmarkNode; |
| 40 | 46 |
| 41 namespace { | 47 namespace { |
| 48 // Computes the cell size based on width. |
| 49 CGSize PreferredCellSizeForWidth(UICollectionViewCell* cell, CGFloat width) { |
| 50 CGRect cellFrame = cell.frame; |
| 51 cellFrame.size.width = width; |
| 52 cellFrame.size.height = CGFLOAT_MAX; |
| 53 cell.frame = cellFrame; |
| 54 [cell setNeedsLayout]; |
| 55 [cell layoutIfNeeded]; |
| 56 CGSize result = |
| 57 [cell systemLayoutSizeFittingSize:UILayoutFittingCompressedSize |
| 58 withHorizontalFittingPriority:UILayoutPriorityRequired |
| 59 verticalFittingPriority:UILayoutPriorityDefaultLow]; |
| 60 cellFrame.size = result; |
| 61 cell.frame = cellFrame; |
| 62 return result; |
| 63 } |
| 42 | 64 |
| 43 // Used to store a pair of NSIntegers when storing a NSIndexPath in C++ | 65 // Used to store a pair of NSIntegers when storing a NSIndexPath in C++ |
| 44 // collections. | 66 // collections. |
| 45 using IntegerPair = std::pair<NSInteger, NSInteger>; | 67 using IntegerPair = std::pair<NSInteger, NSInteger>; |
| 46 | 68 |
| 47 // The margin between the side of the view and the first and last tile. | 69 // The margin between the side of the view and the first and last tile. |
| 48 CGFloat rowMarginTablet = 24.0; | 70 CGFloat rowMarginTablet = 24.0; |
| 49 CGFloat rowHeight = 48.0; | 71 CGFloat rowHeight = 48.0; |
| 50 // Minimal acceptable favicon size, in points. | 72 // Minimal acceptable favicon size, in points. |
| 51 CGFloat minFaviconSizePt = 16; | 73 CGFloat minFaviconSizePt = 16; |
| 52 | 74 |
| 53 // Delay in seconds to which the empty background view will be shown when the | 75 // Delay in seconds to which the empty background view will be shown when the |
| 54 // collection view is empty. | 76 // collection view is empty. |
| 55 // This delay should not be too small to let enough time to load bookmarks | 77 // This delay should not be too small to let enough time to load bookmarks |
| 56 // from network. | 78 // from network. |
| 57 const NSTimeInterval kShowEmptyBookmarksBackgroundRefreshDelay = 1.0; | 79 const NSTimeInterval kShowEmptyBookmarksBackgroundRefreshDelay = 1.0; |
| 80 } |
| 58 | 81 |
| 59 } // namespace | 82 @interface BookmarkCollectionView ()<BookmarkPromoCellDelegate, |
| 60 | 83 SigninPromoViewConsumer, |
| 61 @interface BookmarkCollectionView ()<UICollectionViewDataSource, | 84 UICollectionViewDataSource, |
| 62 UICollectionViewDelegateFlowLayout, | 85 UICollectionViewDelegateFlowLayout, |
| 63 UIGestureRecognizerDelegate> { | 86 UIGestureRecognizerDelegate> { |
| 87 // A vector of folders to display in the collection view. |
| 88 std::vector<const BookmarkNode*> _subFolders; |
| 89 // A vector of bookmark urls to display in the collection view. |
| 90 std::vector<const BookmarkNode*> _subItems; |
| 91 |
| 92 // True if the promo is visible. |
| 93 BOOL _promoVisible; |
| 94 |
| 95 // Mediator, helper for the sign-in promo view. |
| 96 SigninPromoViewMediator* _signinPromoViewMediator; |
| 97 |
| 64 std::unique_ptr<bookmarks::BookmarkModelBridge> _modelBridge; | 98 std::unique_ptr<bookmarks::BookmarkModelBridge> _modelBridge; |
| 65 ios::ChromeBrowserState* _browserState; | 99 ios::ChromeBrowserState* _browserState; |
| 66 | 100 |
| 67 // Map of favicon load tasks for each index path. Used to keep track of | 101 // Map of favicon load tasks for each index path. Used to keep track of |
| 68 // pending favicon load operations so that they can be cancelled upon cell | 102 // pending favicon load operations so that they can be cancelled upon cell |
| 69 // reuse. Keys are (section, item) pairs of cell index paths. | 103 // reuse. Keys are (section, item) pairs of cell index paths. |
| 70 std::map<IntegerPair, base::CancelableTaskTracker::TaskId> _faviconLoadTasks; | 104 std::map<IntegerPair, base::CancelableTaskTracker::TaskId> _faviconLoadTasks; |
| 71 // Task tracker used for async favicon loads. | 105 // Task tracker used for async favicon loads. |
| 72 base::CancelableTaskTracker _faviconTaskTracker; | 106 base::CancelableTaskTracker _faviconTaskTracker; |
| 73 } | 107 } |
| 74 | 108 |
| 75 // Redefined to be readwrite. | 109 // Redefined to be readwrite. |
| 76 @property(nonatomic, assign) bookmarks::BookmarkModel* bookmarkModel; | 110 @property(nonatomic, assign) bookmarks::BookmarkModel* bookmarkModel; |
| 77 // Redefined to be readwrite. | 111 // Redefined to be readwrite. |
| 78 @property(nonatomic, strong) UICollectionView* collectionView; | 112 @property(nonatomic, strong) UICollectionView* collectionView; |
| 79 // Redefined to be readwrite. | 113 // Redefined to be readwrite. |
| 80 @property(nonatomic, assign) BOOL editing; | 114 @property(nonatomic, assign) BOOL editing; |
| 81 // Detects a long press on a cell. | 115 // Detects a long press on a cell. |
| 82 @property(nonatomic, strong) UILongPressGestureRecognizer* longPressRecognizer; | 116 @property(nonatomic, strong) UILongPressGestureRecognizer* longPressRecognizer; |
| 83 // Background view of the collection view shown when there is no items. | 117 // Background view of the collection view shown when there is no items. |
| 84 @property(nonatomic, strong) | 118 @property(nonatomic, strong) |
| 85 BookmarkCollectionViewBackground* emptyCollectionBackgroundView; | 119 BookmarkCollectionViewBackground* emptyCollectionBackgroundView; |
| 86 // Shadow to display over the content. | 120 // Shadow to display over the content. |
| 87 @property(nonatomic, strong) UIView* shadow; | 121 @property(nonatomic, strong) UIView* shadow; |
| 88 | 122 |
| 89 // Updates the editing state for the cell. | 123 @property(nonatomic, assign) const bookmarks::BookmarkNode* folder; |
| 90 - (void)updateEditingStateOfCell:(BookmarkCell*)cell | |
| 91 atIndexPath:(NSIndexPath*)indexPath | |
| 92 animateMenuVisibility:(BOOL)animateMenuVisibility | |
| 93 animateSelectedState:(BOOL)animateSelectedState; | |
| 94 | 124 |
| 95 // Callback received when the user taps the menu button on the cell. | 125 // Section indices. |
| 96 - (void)didTapMenuButton:(BookmarkItemCell*)cell view:(UIView*)view; | 126 @property(nonatomic, readonly, assign) NSInteger promoSection; |
| 127 @property(nonatomic, readonly, assign) NSInteger folderSection; |
| 128 @property(nonatomic, readonly, assign) NSInteger itemsSection; |
| 129 @property(nonatomic, readonly, assign) NSInteger sectionCount; |
| 97 | 130 |
| 98 // In landscape mode, there are 2 widths: 480pt and 568pt. Returns YES if the | |
| 99 // width is 568pt. | |
| 100 - (BOOL)wideLandscapeMode; | |
| 101 | |
| 102 // Schedules showing or hiding the empty bookmarks background view if the | |
| 103 // collection view is empty by calling showEmptyBackgroundIfNeeded after | |
| 104 // kShowEmptyBookmarksBackgroundRefreshDelay. | |
| 105 // Multiple call to this method will cancel previous scheduled call to | |
| 106 // showEmptyBackgroundIfNeeded before scheduling a new one. | |
| 107 - (void)scheduleEmptyBackgroundVisibilityUpdate; | |
| 108 // Shows/hides empty bookmarks background view if the collections view is empty. | |
| 109 - (void)updateEmptyBackgroundVisibility; | |
| 110 // Shows/hides empty bookmarks background view with an animation. | |
| 111 - (void)setEmptyBackgroundVisible:(BOOL)visible; | |
| 112 @end | 131 @end |
| 113 | 132 |
| 114 @implementation BookmarkCollectionView | 133 @implementation BookmarkCollectionView |
| 134 @synthesize delegate = _delegate; |
| 135 @synthesize folder = _folder; |
| 115 @synthesize bookmarkModel = _bookmarkModel; | 136 @synthesize bookmarkModel = _bookmarkModel; |
| 116 @synthesize collectionView = _collectionView; | 137 @synthesize collectionView = _collectionView; |
| 117 @synthesize editing = _editing; | 138 @synthesize editing = _editing; |
| 118 @synthesize emptyCollectionBackgroundView = _emptyCollectionBackgroundView; | 139 @synthesize emptyCollectionBackgroundView = _emptyCollectionBackgroundView; |
| 119 @synthesize loader = _loader; | 140 @synthesize loader = _loader; |
| 120 @synthesize longPressRecognizer = _longPressRecognizer; | 141 @synthesize longPressRecognizer = _longPressRecognizer; |
| 121 @synthesize browserState = _browserState; | 142 @synthesize browserState = _browserState; |
| 122 @synthesize shadow = _shadow; | 143 @synthesize shadow = _shadow; |
| 123 | 144 |
| 124 #pragma mark - Initialization | 145 #pragma mark - Initialization |
| 125 | 146 |
| 126 - (id)init { | |
| 127 NOTREACHED(); | |
| 128 return nil; | |
| 129 } | |
| 130 | |
| 131 - (id)initWithFrame:(CGRect)frame { | |
| 132 NOTREACHED(); | |
| 133 return nil; | |
| 134 } | |
| 135 | |
| 136 - (instancetype)initWithBrowserState:(ios::ChromeBrowserState*)browserState | |
| 137 frame:(CGRect)frame { | |
| 138 self = [super initWithFrame:frame]; | |
| 139 if (self) { | |
| 140 _browserState = browserState; | |
| 141 | |
| 142 // Set up connection to the BookmarkModel. | |
| 143 _bookmarkModel = | |
| 144 ios::BookmarkModelFactory::GetForBrowserState(browserState); | |
| 145 | |
| 146 // Set up observers. | |
| 147 _modelBridge.reset( | |
| 148 new bookmarks::BookmarkModelBridge(self, _bookmarkModel)); | |
| 149 | |
| 150 [self setupViews]; | |
| 151 } | |
| 152 return self; | |
| 153 } | |
| 154 | |
| 155 - (void)dealloc { | |
| 156 _collectionView.dataSource = nil; | |
| 157 _collectionView.delegate = nil; | |
| 158 UIView* moi = _collectionView; | |
| 159 dispatch_async(dispatch_get_main_queue(), ^{ | |
| 160 // A collection view with a layout that uses a dynamic animator (aka | |
| 161 // something that changes the layout over time) will crash if it is | |
| 162 // deallocated while the animation is currently playing. | |
| 163 // Apparently if a tick has been dispatched it will execute, invoking a | |
| 164 // method on the deallocated collection. | |
| 165 // The only purpose of this block is to retain the collection view for a | |
| 166 // while, giving the layout a chance to perform its last tick. | |
| 167 [moi self]; | |
| 168 }); | |
| 169 _faviconTaskTracker.TryCancelAll(); | |
| 170 } | |
| 171 | |
| 172 - (void)setupViews { | 147 - (void)setupViews { |
| 173 self.backgroundColor = bookmark_utils_ios::mainBackgroundColor(); | 148 self.backgroundColor = bookmark_utils_ios::mainBackgroundColor(); |
| 174 UICollectionViewFlowLayout* layout = | 149 UICollectionViewFlowLayout* layout = |
| 175 [[UICollectionViewFlowLayout alloc] init]; | 150 [[UICollectionViewFlowLayout alloc] init]; |
| 176 | 151 |
| 177 UICollectionView* collectionView = | 152 UICollectionView* collectionView = |
| 178 [[UICollectionView alloc] initWithFrame:self.bounds | 153 [[UICollectionView alloc] initWithFrame:self.bounds |
| 179 collectionViewLayout:layout]; | 154 collectionViewLayout:layout]; |
| 180 self.collectionView = collectionView; | 155 self.collectionView = collectionView; |
| 181 self.collectionView.backgroundColor = [UIColor clearColor]; | 156 self.collectionView.backgroundColor = [UIColor clearColor]; |
| (...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 216 | 191 |
| 217 [self updateShadow]; | 192 [self updateShadow]; |
| 218 | 193 |
| 219 self.longPressRecognizer = [[UILongPressGestureRecognizer alloc] | 194 self.longPressRecognizer = [[UILongPressGestureRecognizer alloc] |
| 220 initWithTarget:self | 195 initWithTarget:self |
| 221 action:@selector(longPress:)]; | 196 action:@selector(longPress:)]; |
| 222 self.longPressRecognizer.delegate = self; | 197 self.longPressRecognizer.delegate = self; |
| 223 [self.collectionView addGestureRecognizer:self.longPressRecognizer]; | 198 [self.collectionView addGestureRecognizer:self.longPressRecognizer]; |
| 224 } | 199 } |
| 225 | 200 |
| 201 - (instancetype)initWithBrowserState:(ios::ChromeBrowserState*)browserState |
| 202 frame:(CGRect)frame { |
| 203 self = [super initWithFrame:frame]; |
| 204 if (self) { |
| 205 _browserState = browserState; |
| 206 |
| 207 // Set up connection to the BookmarkModel. |
| 208 _bookmarkModel = |
| 209 ios::BookmarkModelFactory::GetForBrowserState(browserState); |
| 210 |
| 211 // Set up observers. |
| 212 _modelBridge.reset( |
| 213 new bookmarks::BookmarkModelBridge(self, _bookmarkModel)); |
| 214 |
| 215 [self setupViews]; |
| 216 [self updateCollectionView]; |
| 217 } |
| 218 return self; |
| 219 } |
| 220 |
| 221 - (void)dealloc { |
| 222 _collectionView.dataSource = nil; |
| 223 _collectionView.delegate = nil; |
| 224 UIView* moi = _collectionView; |
| 225 dispatch_async(dispatch_get_main_queue(), ^{ |
| 226 // A collection view with a layout that uses a dynamic animator (aka |
| 227 // something that changes the layout over time) will crash if it is |
| 228 // deallocated while the animation is currently playing. |
| 229 // Apparently if a tick has been dispatched it will execute, invoking a |
| 230 // method on the deallocated collection. |
| 231 // The only purpose of this block is to retain the collection view for a |
| 232 // while, giving the layout a chance to perform its last tick. |
| 233 [moi self]; |
| 234 }); |
| 235 _faviconTaskTracker.TryCancelAll(); |
| 236 } |
| 237 |
| 238 #pragma mark - BookmarkHomePrimaryView |
| 239 |
| 240 - (void)changeOrientation:(UIInterfaceOrientation)orientation { |
| 241 [self updateCollectionView]; |
| 242 } |
| 243 |
| 244 - (CGFloat)contentPositionInPortraitOrientation { |
| 245 if (IsPortrait()) |
| 246 return self.collectionView.contentOffset.y; |
| 247 |
| 248 // In short landscape mode and portrait mode, there are 2 cells per row. |
| 249 if ([self wideLandscapeMode]) |
| 250 return self.collectionView.contentOffset.y; |
| 251 |
| 252 // In wide landscape mode, there are 3 cells per row. |
| 253 return self.collectionView.contentOffset.y * 3 / 2.0; |
| 254 } |
| 255 |
| 256 - (void)applyContentPosition:(CGFloat)position { |
| 257 if (IsLandscape() && [self wideLandscapeMode]) { |
| 258 position = position * 2 / 3.0; |
| 259 } |
| 260 |
| 261 CGFloat y = |
| 262 MIN(position, |
| 263 [self.collectionView.collectionViewLayout collectionViewContentSize] |
| 264 .height); |
| 265 self.collectionView.contentOffset = |
| 266 CGPointMake(self.collectionView.contentOffset.x, y); |
| 267 } |
| 268 |
| 269 - (void)setEditing:(BOOL)editing animated:(BOOL)animated { |
| 270 if (self.editing == editing) |
| 271 return; |
| 272 |
| 273 _editing = editing; |
| 274 [UIView animateWithDuration:animated ? 0.2 : 0.0 |
| 275 delay:0 |
| 276 options:UIViewAnimationOptionBeginFromCurrentState |
| 277 animations:^{ |
| 278 self.shadow.alpha = editing ? 0.0 : 1.0; |
| 279 } |
| 280 completion:nil]; |
| 281 |
| 282 // If the promo is active this means that it is removed and added as the edit |
| 283 // mode changes, making reloading the data mandatory. |
| 284 if ([self isPromoActive]) { |
| 285 [self.collectionView reloadData]; |
| 286 [self.collectionView.collectionViewLayout invalidateLayout]; |
| 287 } else { |
| 288 // Update the visual state of the bookmark cells without reloading the |
| 289 // section. |
| 290 // This prevents flickering of images that need to be asynchronously |
| 291 // reloaded. |
| 292 NSArray* indexPaths = [self.collectionView indexPathsForVisibleItems]; |
| 293 for (NSIndexPath* indexPath in indexPaths) { |
| 294 [self updateEditingStateOfCellAtIndexPath:indexPath |
| 295 animateMenuVisibility:animated |
| 296 animateSelectedState:NO]; |
| 297 } |
| 298 } |
| 299 [self promoStateChangedAnimated:animated]; |
| 300 } |
| 301 |
| 302 - (void)setScrollsToTop:(BOOL)scrollsToTop { |
| 303 self.collectionView.scrollsToTop = scrollsToTop; |
| 304 } |
| 305 |
| 306 #pragma mark - Public |
| 307 |
| 308 - (void)resetFolder:(const BookmarkNode*)folder { |
| 309 DCHECK(folder->is_folder()); |
| 310 self.folder = folder; |
| 311 [self updateCollectionView]; |
| 312 } |
| 313 |
| 314 - (void)setDelegate:(id<BookmarkCollectionViewDelegate>)delegate { |
| 315 _delegate = delegate; |
| 316 [self promoStateChangedAnimated:NO]; |
| 317 } |
| 318 |
| 319 - (void)collectionViewScrolled { |
| 320 [self.delegate bookmarkCollectionViewDidScroll:self]; |
| 321 } |
| 322 |
| 323 - (void)promoStateChangedAnimated:(BOOL)animate { |
| 324 BOOL newPromoState = |
| 325 !self.editing && self.folder && |
| 326 self.folder->type() == BookmarkNode::MOBILE && |
| 327 [self.delegate bookmarkCollectionViewShouldShowPromoCell:self]; |
| 328 if (newPromoState != _promoVisible) { |
| 329 // This is awful, but until the old code to do the refresh when switching |
| 330 // in and out of edit mode is fixed, this is probably the cleanest thing to |
| 331 // do. |
| 332 _promoVisible = newPromoState; |
| 333 if (experimental_flags::IsSigninPromoEnabled()) { |
| 334 if (!_promoVisible) { |
| 335 _signinPromoViewMediator.consumer = nil; |
| 336 _signinPromoViewMediator = nil; |
| 337 } else { |
| 338 _signinPromoViewMediator = [[SigninPromoViewMediator alloc] init]; |
| 339 _signinPromoViewMediator.consumer = self; |
| 340 _signinPromoViewMediator.accessPoint = |
| 341 signin_metrics::AccessPoint::ACCESS_POINT_BOOKMARK_MANAGER; |
| 342 } |
| 343 } |
| 344 [self.collectionView reloadData]; |
| 345 } |
| 346 } |
| 347 |
| 348 #pragma mark - Sections |
| 349 |
| 350 - (NSInteger)promoSection { |
| 351 return [self shouldShowPromoCell] ? 0 : -1; |
| 352 } |
| 353 |
| 354 - (NSInteger)folderSection { |
| 355 return [self shouldShowPromoCell] ? 1 : 0; |
| 356 } |
| 357 |
| 358 - (NSInteger)itemsSection { |
| 359 return [self shouldShowPromoCell] ? 2 : 1; |
| 360 } |
| 361 |
| 362 - (NSInteger)sectionCount { |
| 363 return [self shouldShowPromoCell] ? 3 : 2; |
| 364 } |
| 365 |
| 226 - (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection { | 366 - (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection { |
| 227 [self updateShadow]; | 367 [self updateShadow]; |
| 228 } | 368 } |
| 229 | 369 |
| 230 - (void)updateShadow { | 370 - (void)updateShadow { |
| 231 // Remove the current one, if any. | 371 // Remove the current one, if any. |
| 232 [self.shadow removeFromSuperview]; | 372 [self.shadow removeFromSuperview]; |
| 233 | 373 |
| 234 if (IsCompact(self)) { | 374 if (IsCompact(self)) { |
| 235 self.shadow = | 375 self.shadow = |
| (...skipping 15 matching lines...) Expand all Loading... |
| 251 } | 391 } |
| 252 | 392 |
| 253 - (void)updateShadowFrame { | 393 - (void)updateShadowFrame { |
| 254 CGFloat shadowHeight = CGRectGetHeight(self.shadow.frame); | 394 CGFloat shadowHeight = CGRectGetHeight(self.shadow.frame); |
| 255 CGFloat y = std::min<CGFloat>( | 395 CGFloat y = std::min<CGFloat>( |
| 256 0.0, self.collectionView.contentOffset.y - shadowHeight); | 396 0.0, self.collectionView.contentOffset.y - shadowHeight); |
| 257 self.shadow.frame = | 397 self.shadow.frame = |
| 258 CGRectMake(0, y, CGRectGetWidth(self.bounds), shadowHeight); | 398 CGRectMake(0, y, CGRectGetWidth(self.bounds), shadowHeight); |
| 259 } | 399 } |
| 260 | 400 |
| 401 #pragma mark - BookmarkItemCell callbacks |
| 402 |
| 403 // Callback received when the user taps the menu button on the cell. |
| 404 - (void)didTapMenuButton:(BookmarkItemCell*)cell view:(UIView*)view { |
| 405 [self didTapMenuButtonAtIndexPath:[self.collectionView indexPathForCell:cell] |
| 406 onView:view |
| 407 forCell:cell]; |
| 408 } |
| 409 |
| 410 #pragma mark - BookmarkModelBridgeObserver Callbacks |
| 411 // BookmarkModelBridgeObserver Callbacks |
| 412 // Instances of this class automatically observe the bookmark model. |
| 413 // The bookmark model has loaded. |
| 414 - (void)bookmarkModelLoaded { |
| 415 [self updateCollectionView]; |
| 416 } |
| 417 |
| 418 // The node has changed, but not its children. |
| 419 - (void)bookmarkNodeChanged:(const BookmarkNode*)bookmarkNode { |
| 420 // The base folder changed. Do nothing. |
| 421 if (bookmarkNode == self.folder) |
| 422 return; |
| 423 |
| 424 // A specific cell changed. Reload that cell. |
| 425 NSIndexPath* indexPath = [self indexPathForNode:bookmarkNode]; |
| 426 |
| 427 if (indexPath) { |
| 428 // TODO(crbug.com/603661): Ideally, we would only reload the relevant index |
| 429 // path. However, calling reloadItemsAtIndexPaths:(0,0) immediately after |
| 430 // reloadData results in a exception: NSInternalInconsistencyException |
| 431 // 'request for index path for global index 2147483645 ...' |
| 432 // One solution would be to keep track of whether we've just called |
| 433 // reloadData, but that requires experimentation to determine how long we |
| 434 // have to wait before we can safely call reloadItemsAtIndexPaths. |
| 435 [self updateCollectionView]; |
| 436 } |
| 437 } |
| 438 |
| 439 // The node has not changed, but its children have. |
| 440 - (void)bookmarkNodeChildrenChanged:(const BookmarkNode*)bookmarkNode { |
| 441 // The base folder's children changed. Reload everything. |
| 442 if (bookmarkNode == self.folder) { |
| 443 [self updateCollectionView]; |
| 444 return; |
| 445 } |
| 446 |
| 447 // A subfolder's children changed. Reload that cell. |
| 448 std::vector<const BookmarkNode*>::iterator it = |
| 449 std::find(_subFolders.begin(), _subFolders.end(), bookmarkNode); |
| 450 if (it != _subFolders.end()) { |
| 451 // TODO(crbug.com/603661): Ideally, we would only reload the relevant index |
| 452 // path. However, calling reloadItemsAtIndexPaths:(0,0) immediately after |
| 453 // reloadData results in a exception: NSInternalInconsistencyException |
| 454 // 'request for index path for global index 2147483645 ...' |
| 455 // One solution would be to keep track of whether we've just called |
| 456 // reloadData, but that requires experimentation to determine how long we |
| 457 // have to wait before we can safely call reloadItemsAtIndexPaths. |
| 458 [self updateCollectionView]; |
| 459 } |
| 460 } |
| 461 |
| 462 // The node has moved to a new parent folder. |
| 463 - (void)bookmarkNode:(const BookmarkNode*)bookmarkNode |
| 464 movedFromParent:(const BookmarkNode*)oldParent |
| 465 toParent:(const BookmarkNode*)newParent { |
| 466 if (oldParent == self.folder || newParent == self.folder) { |
| 467 // A folder was added or removed from the base folder. |
| 468 [self updateCollectionView]; |
| 469 } |
| 470 } |
| 471 |
| 472 // |node| was deleted from |folder|. |
| 473 - (void)bookmarkNodeDeleted:(const BookmarkNode*)node |
| 474 fromFolder:(const BookmarkNode*)folder { |
| 475 if (self.folder == node) { |
| 476 self.folder = nil; |
| 477 [self updateCollectionView]; |
| 478 } |
| 479 } |
| 480 |
| 481 // All non-permanent nodes have been removed. |
| 482 - (void)bookmarkModelRemovedAllNodes { |
| 483 self.folder = nil; |
| 484 [self updateCollectionView]; |
| 485 } |
| 486 |
| 487 - (void)bookmarkNodeFaviconChanged: |
| 488 (const bookmarks::BookmarkNode*)bookmarkNode { |
| 489 // Only urls have favicons. |
| 490 DCHECK(bookmarkNode->is_url()); |
| 491 |
| 492 // Update image of corresponding cell. |
| 493 NSIndexPath* indexPath = [self indexPathForNode:bookmarkNode]; |
| 494 |
| 495 if (!indexPath) |
| 496 return; |
| 497 |
| 498 // Check that this cell is visible. |
| 499 NSArray* visiblePaths = [self.collectionView indexPathsForVisibleItems]; |
| 500 if (![visiblePaths containsObject:indexPath]) |
| 501 return; |
| 502 |
| 503 [self loadFaviconAtIndexPath:indexPath]; |
| 504 } |
| 505 |
| 506 #pragma mark - non-UI |
| 507 |
| 508 // Called when a user is attempting to select a cell. |
| 509 // Returning NO prevents the cell from being selected. |
| 510 - (BOOL)shouldSelectCellAtIndexPath:(NSIndexPath*)indexPath { |
| 511 return YES; |
| 512 } |
| 513 |
| 514 // Called when a cell is tapped outside of editing mode. |
| 515 - (void)didTapCellAtIndexPath:(NSIndexPath*)indexPath { |
| 516 if (indexPath.section == self.promoSection) { |
| 517 // User tapped inside promo cell but not on one of the buttons. Ignore it. |
| 518 return; |
| 519 } |
| 520 |
| 521 const BookmarkNode* node = [self nodeAtIndexPath:indexPath]; |
| 522 DCHECK(node); |
| 523 |
| 524 if (indexPath.section == self.folderSection) { |
| 525 [self.delegate bookmarkCollectionView:self |
| 526 selectedFolderForNavigation:node]; |
| 527 } else { |
| 528 RecordBookmarkLaunch(BOOKMARK_LAUNCH_LOCATION_FOLDER); |
| 529 [self.delegate bookmarkCollectionView:self |
| 530 selectedUrlForNavigation:node->url()]; |
| 531 } |
| 532 } |
| 533 |
| 534 // Called when a user selected a cell in the editing state. |
| 535 - (void)didAddCellForEditingAtIndexPath:(NSIndexPath*)indexPath { |
| 536 const BookmarkNode* node = [self nodeAtIndexPath:indexPath]; |
| 537 UICollectionViewCell* cell = |
| 538 [self.collectionView cellForItemAtIndexPath:indexPath]; |
| 539 [self.delegate bookmarkCollectionView:self cell:cell addNodeForEditing:node]; |
| 540 } |
| 541 |
| 542 - (void)didRemoveCellForEditingAtIndexPath:(NSIndexPath*)indexPath { |
| 543 const BookmarkNode* node = [self nodeAtIndexPath:indexPath]; |
| 544 UICollectionViewCell* cell = |
| 545 [self.collectionView cellForItemAtIndexPath:indexPath]; |
| 546 [self.delegate bookmarkCollectionView:self |
| 547 cell:cell |
| 548 removeNodeForEditing:node]; |
| 549 } |
| 550 |
| 551 // Called when a user taps the menu button on a cell. |
| 552 - (void)didTapMenuButtonAtIndexPath:(NSIndexPath*)indexPath |
| 553 onView:(UIView*)view |
| 554 forCell:(BookmarkItemCell*)cell { |
| 555 [self.delegate bookmarkCollectionView:self |
| 556 wantsMenuForBookmark:[self nodeAtIndexPath:indexPath] |
| 557 onView:view |
| 558 forCell:cell]; |
| 559 } |
| 560 |
| 561 // Whether a cell should show a button and of which type. |
| 562 - (bookmark_cell::ButtonType)buttonTypeForCellAtIndexPath: |
| 563 (NSIndexPath*)indexPath { |
| 564 return self.editing ? bookmark_cell::ButtonNone : bookmark_cell::ButtonMenu; |
| 565 } |
| 566 |
| 567 // Whether a long press at the cell at |indexPath| should be allowed. |
| 568 - (BOOL)allowLongPressForCellAtIndexPath:(NSIndexPath*)indexPath { |
| 569 return !self.editing; |
| 570 } |
| 571 |
| 572 // The |cell| at |indexPath| received a long press. |
| 573 - (void)didLongPressCell:(UICollectionViewCell*)cell |
| 574 atIndexPath:(NSIndexPath*)indexPath { |
| 575 if (indexPath.section == self.promoSection) { |
| 576 // User long-pressed inside promo cell. Ignore it. |
| 577 return; |
| 578 } |
| 579 |
| 580 [self.delegate bookmarkCollectionView:self |
| 581 didLongPressCell:cell |
| 582 forBookmark:[self nodeAtIndexPath:indexPath]]; |
| 583 } |
| 584 |
| 585 // Whether the cell has been selected in editing mode. |
| 586 - (BOOL)cellIsSelectedForEditingAtIndexPath:(NSIndexPath*)indexPath { |
| 587 const BookmarkNode* node = [self nodeAtIndexPath:indexPath]; |
| 588 const std::set<const BookmarkNode*>& editingNodes = |
| 589 [self.delegate nodesBeingEdited]; |
| 590 return editingNodes.find(node) != editingNodes.end(); |
| 591 } |
| 592 |
| 593 // Updates the collection view based on the current state of all models and |
| 594 // contextual parameters, such as the interface orientation. |
| 595 - (void)updateCollectionView { |
| 596 if (!self.bookmarkModel->loaded()) |
| 597 return; |
| 598 |
| 599 // Regenerate the list of all bookmarks. |
| 600 _subFolders = std::vector<const BookmarkNode*>(); |
| 601 _subItems = std::vector<const BookmarkNode*>(); |
| 602 |
| 603 if (self.folder) { |
| 604 int childCount = self.folder->child_count(); |
| 605 for (int i = 0; i < childCount; ++i) { |
| 606 const BookmarkNode* node = self.folder->GetChild(i); |
| 607 if (node->is_folder()) |
| 608 _subFolders.push_back(node); |
| 609 else |
| 610 _subItems.push_back(node); |
| 611 } |
| 612 |
| 613 bookmark_utils_ios::SortFolders(&_subFolders); |
| 614 } |
| 615 |
| 616 [self cancelAllFaviconLoads]; |
| 617 [self.collectionView reloadData]; |
| 618 } |
| 619 |
| 620 // Returns the bookmark node associated with |indexPath|. |
| 621 - (const BookmarkNode*)nodeAtIndexPath:(NSIndexPath*)indexPath { |
| 622 if (indexPath.section == self.folderSection) |
| 623 return _subFolders[indexPath.row]; |
| 624 if (indexPath.section == self.itemsSection) |
| 625 return _subItems[indexPath.row]; |
| 626 |
| 627 NOTREACHED(); |
| 628 return nullptr; |
| 629 } |
| 630 |
| 631 #pragma mark - |
| 632 |
| 633 - (NSIndexPath*)indexPathForNode:(const bookmarks::BookmarkNode*)bookmarkNode { |
| 634 NSIndexPath* indexPath = nil; |
| 635 if (bookmarkNode->is_folder()) { |
| 636 std::vector<const BookmarkNode*>::iterator it = |
| 637 std::find(_subFolders.begin(), _subFolders.end(), bookmarkNode); |
| 638 if (it != _subFolders.end()) { |
| 639 ptrdiff_t index = std::distance(_subFolders.begin(), it); |
| 640 indexPath = |
| 641 [NSIndexPath indexPathForRow:index inSection:self.folderSection]; |
| 642 } |
| 643 } else if (bookmarkNode->is_url()) { |
| 644 std::vector<const BookmarkNode*>::iterator it = |
| 645 std::find(_subItems.begin(), _subItems.end(), bookmarkNode); |
| 646 if (it != _subItems.end()) { |
| 647 ptrdiff_t index = std::distance(_subItems.begin(), it); |
| 648 indexPath = |
| 649 [NSIndexPath indexPathForRow:index inSection:self.itemsSection]; |
| 650 } |
| 651 } |
| 652 return indexPath; |
| 653 } |
| 654 |
| 655 - (void)collectionView:(UICollectionView*)collectionView |
| 656 willDisplayCell:(UICollectionViewCell*)cell |
| 657 forItemAtIndexPath:(NSIndexPath*)indexPath { |
| 658 if (indexPath.section == self.itemsSection) { |
| 659 [self loadFaviconAtIndexPath:indexPath]; |
| 660 } |
| 661 } |
| 662 |
| 663 - (CGFloat)interitemSpacingForSectionAtIndex:(NSInteger)section { |
| 664 CGFloat interitemSpacing = 0; |
| 665 SEL minimumInteritemSpacingSelector = @selector |
| 666 (collectionView:layout:minimumInteritemSpacingForSectionAtIndex:); |
| 667 if ([self.collectionView.delegate |
| 668 respondsToSelector:minimumInteritemSpacingSelector]) { |
| 669 id collectionViewDelegate = static_cast<id>(self.collectionView.delegate); |
| 670 interitemSpacing = |
| 671 [collectionViewDelegate collectionView:self.collectionView |
| 672 layout:self.collectionView |
| 673 .collectionViewLayout |
| 674 minimumInteritemSpacingForSectionAtIndex:section]; |
| 675 } else if ([self.collectionView.collectionViewLayout |
| 676 isKindOfClass:[UICollectionViewFlowLayout class]]) { |
| 677 UICollectionViewFlowLayout* flowLayout = |
| 678 static_cast<UICollectionViewFlowLayout*>( |
| 679 self.collectionView.collectionViewLayout); |
| 680 interitemSpacing = flowLayout.minimumInteritemSpacing; |
| 681 } |
| 682 return interitemSpacing; |
| 683 } |
| 684 |
| 685 // The inset of the section. |
| 686 - (UIEdgeInsets)insetForSectionAtIndex:(NSInteger)section { |
| 687 UIEdgeInsets insets = UIEdgeInsetsZero; |
| 688 if ([self isPromoSection:section]) |
| 689 insets = UIEdgeInsetsZero; |
| 690 |
| 691 if (IsIPadIdiom()) { |
| 692 insets = UIEdgeInsetsMake(10, rowMarginTablet, 0, rowMarginTablet); |
| 693 } else { |
| 694 insets = UIEdgeInsetsZero; |
| 695 } |
| 696 |
| 697 if (section == self.folderSection) |
| 698 insets.bottom = [self interitemSpacingForSectionAtIndex:section] / 2.; |
| 699 else if (section == self.itemsSection) |
| 700 insets.top = [self interitemSpacingForSectionAtIndex:section] / 2.; |
| 701 else if (section == self.promoSection) |
| 702 (void)0; // No insets to update. |
| 703 else |
| 704 NOTREACHED(); |
| 705 return insets; |
| 706 } |
| 707 |
| 708 // The size of the cell at |indexPath|. |
| 709 - (CGSize)cellSizeForIndexPath:(NSIndexPath*)indexPath { |
| 710 if ([self isPromoSection:indexPath.section]) { |
| 711 UICollectionViewCell* cell = |
| 712 [self.collectionView cellForItemAtIndexPath:indexPath]; |
| 713 if (!cell) { |
| 714 // -[UICollectionView |
| 715 // dequeueReusableCellWithReuseIdentifier:forIndexPath:] cannot be used |
| 716 // here since this method is called by -[id<UICollectionViewDelegate> |
| 717 // collectionView:layout:sizeForItemAtIndexPath:]. This would generate |
| 718 // crash: SIGFPE, EXC_I386_DIV. |
| 719 if (experimental_flags::IsSigninPromoEnabled()) { |
| 720 DCHECK(_signinPromoViewMediator); |
| 721 BookmarkSigninPromoCell* signinPromoCell = |
| 722 [[BookmarkSigninPromoCell alloc] |
| 723 initWithFrame:CGRectMake(0, 0, 1000, 1000)]; |
| 724 [[_signinPromoViewMediator createConfigurator] |
| 725 configureSigninPromoView:signinPromoCell.signinPromoView]; |
| 726 cell = signinPromoCell; |
| 727 } else { |
| 728 cell = [[BookmarkPromoCell alloc] init]; |
| 729 } |
| 730 } |
| 731 return PreferredCellSizeForWidth(cell, CGRectGetWidth(self.bounds)); |
| 732 } |
| 733 DCHECK(![self isPromoSection:indexPath.section]); |
| 734 UIEdgeInsets insets = [self insetForSectionAtIndex:indexPath.section]; |
| 735 return CGSizeMake(self.bounds.size.width - (insets.right + insets.left), |
| 736 rowHeight); |
| 737 } |
| 738 |
| 739 - (BOOL)needsSectionHeaderForSection:(NSInteger)section { |
| 740 // Only show header when there is at least one element in the previous |
| 741 // section. |
| 742 if (section == 0) |
| 743 return NO; |
| 744 |
| 745 if ([self numberOfItemsInSection:(section - 1)] == 0) |
| 746 return NO; |
| 747 |
| 748 return YES; |
| 749 } |
| 750 |
| 751 #pragma mark - UI |
| 752 |
| 753 // The size of the header for |section|. A return value of CGSizeZero prevents |
| 754 // a header from showing. |
| 755 - (CGSize)headerSizeForSection:(NSInteger)section { |
| 756 if ([self needsSectionHeaderForSection:section]) |
| 757 return CGSizeMake(self.bounds.size.width, |
| 758 [BookmarkHeaderSeparatorView preferredHeight]); |
| 759 |
| 760 return CGSizeZero; |
| 761 } |
| 762 |
| 763 // Create a cell for display at |indexPath|. |
| 764 - (UICollectionViewCell*)cellAtIndexPath:(NSIndexPath*)indexPath { |
| 765 if (indexPath.section == self.promoSection) { |
| 766 if (experimental_flags::IsSigninPromoEnabled()) { |
| 767 BookmarkSigninPromoCell* signinPromoCell = [self.collectionView |
| 768 dequeueReusableCellWithReuseIdentifier:[BookmarkSigninPromoCell |
| 769 reuseIdentifier] |
| 770 forIndexPath:indexPath]; |
| 771 signinPromoCell.signinPromoView.delegate = _signinPromoViewMediator; |
| 772 [[_signinPromoViewMediator createConfigurator] |
| 773 configureSigninPromoView:signinPromoCell.signinPromoView]; |
| 774 __weak BookmarkCollectionView* weakSelf = self; |
| 775 signinPromoCell.closeButtonAction = ^() { |
| 776 [weakSelf.delegate bookmarkCollectionViewDismissPromo:self]; |
| 777 }; |
| 778 return signinPromoCell; |
| 779 } else { |
| 780 BookmarkPromoCell* promoCell = [self.collectionView |
| 781 dequeueReusableCellWithReuseIdentifier:[BookmarkPromoCell |
| 782 reuseIdentifier] |
| 783 forIndexPath:indexPath]; |
| 784 promoCell.delegate = self; |
| 785 return promoCell; |
| 786 } |
| 787 } |
| 788 const BookmarkNode* node = [self nodeAtIndexPath:indexPath]; |
| 789 |
| 790 if (indexPath.section == self.folderSection) |
| 791 return [self cellForFolder:node indexPath:indexPath]; |
| 792 |
| 793 BookmarkItemCell* cell = [self cellForBookmark:node indexPath:indexPath]; |
| 794 return cell; |
| 795 } |
| 796 |
| 797 // Create a header view for the element at |indexPath|. |
| 798 - (UICollectionReusableView*)headerAtIndexPath:(NSIndexPath*)indexPath { |
| 799 if (![self needsSectionHeaderForSection:indexPath.section]) |
| 800 return nil; |
| 801 |
| 802 BookmarkHeaderSeparatorView* view = [self.collectionView |
| 803 dequeueReusableSupplementaryViewOfKind: |
| 804 UICollectionElementKindSectionHeader |
| 805 withReuseIdentifier:[BookmarkHeaderSeparatorView |
| 806 reuseIdentifier] |
| 807 forIndexPath:indexPath]; |
| 808 view.backgroundColor = [UIColor colorWithWhite:1 alpha:1]; |
| 809 return view; |
| 810 } |
| 811 |
| 812 - (NSInteger)numberOfItemsInSection:(NSInteger)section { |
| 813 if (section == self.folderSection) |
| 814 return _subFolders.size(); |
| 815 if (section == self.itemsSection) |
| 816 return _subItems.size(); |
| 817 if (section == self.promoSection) |
| 818 return 1; |
| 819 |
| 820 NOTREACHED(); |
| 821 return -1; |
| 822 } |
| 823 |
| 824 - (NSInteger)numberOfSections { |
| 825 return self.sectionCount; |
| 826 } |
| 827 |
| 828 #pragma mark - BookmarkPromoCellDelegate |
| 829 |
| 830 - (void)bookmarkPromoCellDidTapSignIn:(BookmarkPromoCell*)bookmarkPromoCell { |
| 831 [self.delegate bookmarkCollectionViewShowSignIn:self]; |
| 832 } |
| 833 |
| 834 - (void)bookmarkPromoCellDidTapDismiss:(BookmarkPromoCell*)bookmarkPromoCell { |
| 835 [self.delegate bookmarkCollectionViewDismissPromo:self]; |
| 836 } |
| 837 |
| 838 #pragma mark - SigninPromoViewConsumer |
| 839 |
| 840 - (void)configureSigninPromoWithConfigurator: |
| 841 (SigninPromoViewConfigurator*)configurator |
| 842 identityChanged:(BOOL)identityChanged { |
| 843 DCHECK(_signinPromoViewMediator); |
| 844 NSIndexPath* indexPath = |
| 845 [NSIndexPath indexPathForRow:0 inSection:self.promoSection]; |
| 846 BookmarkSigninPromoCell* signinPromoCell = |
| 847 static_cast<BookmarkSigninPromoCell*>( |
| 848 [self.collectionView cellForItemAtIndexPath:indexPath]); |
| 849 if (!signinPromoCell) |
| 850 return; |
| 851 // Should always reconfigure the cell size even if it has to be reloaded. |
| 852 // -[BookmarkCollectionView cellSizeForIndexPath:] uses the current |
| 853 // cell to compute its height. |
| 854 [configurator configureSigninPromoView:signinPromoCell.signinPromoView]; |
| 855 if (identityChanged) { |
| 856 // The section should be reload to update the cell height. |
| 857 NSIndexSet* indexSet = [NSIndexSet indexSetWithIndex:self.promoSection]; |
| 858 [self.collectionView reloadSections:indexSet]; |
| 859 } |
| 860 } |
| 861 |
| 261 #pragma mark - UIScrollViewDelegate | 862 #pragma mark - UIScrollViewDelegate |
| 262 | 863 |
| 263 - (void)scrollViewDidScroll:(UIScrollView*)scrollView { | 864 - (void)scrollViewDidScroll:(UIScrollView*)scrollView { |
| 264 [self updateShadowFrame]; | 865 [self updateShadowFrame]; |
| 265 [self collectionViewScrolled]; | 866 [self collectionViewScrolled]; |
| 266 } | 867 } |
| 267 | 868 |
| 268 #pragma mark - empty background | 869 #pragma mark - empty background |
| 269 | 870 |
| 871 // Schedules showing or hiding the empty bookmarks background view if the |
| 872 // collection view is empty by calling showEmptyBackgroundIfNeeded after |
| 873 // kShowEmptyBookmarksBackgroundRefreshDelay. |
| 874 // Multiple call to this method will cancel previous scheduled call to |
| 875 // showEmptyBackgroundIfNeeded before scheduling a new one. |
| 270 - (void)scheduleEmptyBackgroundVisibilityUpdate { | 876 - (void)scheduleEmptyBackgroundVisibilityUpdate { |
| 271 [NSObject | 877 [NSObject cancelPreviousPerformRequestsWithTarget:self |
| 272 cancelPreviousPerformRequestsWithTarget:self | 878 selector:@selector |
| 273 selector: | 879 (updateEmptyBackgroundVisibility) |
| 274 @selector( | 880 object:nil]; |
| 275 updateEmptyBackgroundVisibility) | |
| 276 object:nil]; | |
| 277 [self performSelector:@selector(updateEmptyBackgroundVisibility) | 881 [self performSelector:@selector(updateEmptyBackgroundVisibility) |
| 278 withObject:nil | 882 withObject:nil |
| 279 afterDelay:kShowEmptyBookmarksBackgroundRefreshDelay]; | 883 afterDelay:kShowEmptyBookmarksBackgroundRefreshDelay]; |
| 280 } | 884 } |
| 281 | 885 |
| 282 - (BOOL)isCollectionViewEmpty { | 886 - (BOOL)isCollectionViewEmpty { |
| 283 BOOL collectionViewIsEmpty = YES; | 887 BOOL collectionViewIsEmpty = YES; |
| 284 const NSInteger numberOfSections = [self numberOfSections]; | 888 const NSInteger numberOfSections = [self numberOfSections]; |
| 285 NSInteger section = [self shouldShowPromoCell] ? 1 : 0; | 889 NSInteger section = [self shouldShowPromoCell] ? 1 : 0; |
| 286 for (; collectionViewIsEmpty && section < numberOfSections; ++section) { | 890 for (; collectionViewIsEmpty && section < numberOfSections; ++section) { |
| 287 const NSInteger numberOfItemsInSection = | 891 const NSInteger numberOfItemsInSection = |
| 288 [self numberOfItemsInSection:section]; | 892 [self numberOfItemsInSection:section]; |
| 289 collectionViewIsEmpty = numberOfItemsInSection == 0; | 893 collectionViewIsEmpty = numberOfItemsInSection == 0; |
| 290 } | 894 } |
| 291 return collectionViewIsEmpty; | 895 return collectionViewIsEmpty; |
| 292 } | 896 } |
| 293 | 897 |
| 898 // Shows/hides empty bookmarks background view if the collections view is empty. |
| 294 - (void)updateEmptyBackgroundVisibility { | 899 - (void)updateEmptyBackgroundVisibility { |
| 295 const BOOL showEmptyBackground = | 900 const BOOL showEmptyBackground = |
| 296 [self isCollectionViewEmpty] && ![self shouldShowPromoCell]; | 901 [self isCollectionViewEmpty] && ![self shouldShowPromoCell]; |
| 297 [self setEmptyBackgroundVisible:showEmptyBackground]; | 902 [self setEmptyBackgroundVisible:showEmptyBackground]; |
| 298 } | 903 } |
| 299 | 904 |
| 905 // Shows/hides empty bookmarks background view with an animation. |
| 300 - (void)setEmptyBackgroundVisible:(BOOL)emptyBackgroundVisible { | 906 - (void)setEmptyBackgroundVisible:(BOOL)emptyBackgroundVisible { |
| 301 [UIView beginAnimations:@"alpha" context:NULL]; | 907 [UIView beginAnimations:@"alpha" context:NULL]; |
| 302 self.emptyCollectionBackgroundView.alpha = emptyBackgroundVisible ? 1 : 0; | 908 self.emptyCollectionBackgroundView.alpha = emptyBackgroundVisible ? 1 : 0; |
| 303 [UIView commitAnimations]; | 909 [UIView commitAnimations]; |
| 304 } | 910 } |
| 305 | 911 |
| 306 #pragma mark - UICollectionViewDataSource | 912 #pragma mark - UICollectionViewDataSource |
| 307 | 913 |
| 308 - (NSInteger)collectionView:(UICollectionView*)collectionView | 914 - (NSInteger)collectionView:(UICollectionView*)collectionView |
| 309 numberOfItemsInSection:(NSInteger)section { | 915 numberOfItemsInSection:(NSInteger)section { |
| (...skipping 96 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 406 return [self cellSizeForIndexPath:indexPath]; | 1012 return [self cellSizeForIndexPath:indexPath]; |
| 407 } | 1013 } |
| 408 | 1014 |
| 409 - (CGSize)collectionView:(UICollectionView*)collectionView | 1015 - (CGSize)collectionView:(UICollectionView*)collectionView |
| 410 layout: | 1016 layout: |
| 411 (UICollectionViewLayout*)collectionViewLayout | 1017 (UICollectionViewLayout*)collectionViewLayout |
| 412 referenceSizeForHeaderInSection:(NSInteger)section { | 1018 referenceSizeForHeaderInSection:(NSInteger)section { |
| 413 return [self headerSizeForSection:section]; | 1019 return [self headerSizeForSection:section]; |
| 414 } | 1020 } |
| 415 | 1021 |
| 416 #pragma mark - BookmarkItemCell callbacks | 1022 #pragma mark - UIGestureRecognizer Callbacks |
| 417 | 1023 |
| 418 - (void)didTapMenuButton:(BookmarkItemCell*)cell view:(UIView*)view { | 1024 - (void)longPress:(UILongPressGestureRecognizer*)recognizer { |
| 419 [self didTapMenuButtonAtIndexPath:[self.collectionView indexPathForCell:cell] | 1025 if (self.longPressRecognizer.numberOfTouches != 1 || self.editing) |
| 420 onView:view | 1026 return; |
| 421 forCell:cell]; | 1027 |
| 1028 if (self.longPressRecognizer.state == UIGestureRecognizerStateRecognized || |
| 1029 self.longPressRecognizer.state == UIGestureRecognizerStateBegan) { |
| 1030 CGPoint point = |
| 1031 [self.longPressRecognizer locationOfTouch:0 inView:self.collectionView]; |
| 1032 NSIndexPath* indexPath = |
| 1033 [self.collectionView indexPathForItemAtPoint:point]; |
| 1034 if (!indexPath) |
| 1035 return; |
| 1036 |
| 1037 UICollectionViewCell* cell = |
| 1038 [self.collectionView cellForItemAtIndexPath:indexPath]; |
| 1039 |
| 1040 // Notify the subclass that long press has been received. |
| 1041 if (cell) |
| 1042 [self didLongPressCell:cell atIndexPath:indexPath]; |
| 1043 } |
| 422 } | 1044 } |
| 423 | 1045 |
| 424 #pragma mark - Convenience methods for subclasses | 1046 #pragma mark - UIGestureRecognizerDelegate |
| 1047 |
| 1048 - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer*)gestureRecognizer { |
| 1049 DCHECK(gestureRecognizer == self.longPressRecognizer); |
| 1050 CGPoint point = |
| 1051 [gestureRecognizer locationOfTouch:0 inView:self.collectionView]; |
| 1052 NSIndexPath* indexPath = [self.collectionView indexPathForItemAtPoint:point]; |
| 1053 if (!indexPath) |
| 1054 return NO; |
| 1055 return [self allowLongPressForCellAtIndexPath:indexPath]; |
| 1056 } |
| 1057 |
| 1058 #pragma mark - Convenience methods |
| 425 | 1059 |
| 426 - (void)updateCellAtIndexPath:(NSIndexPath*)indexPath | 1060 - (void)updateCellAtIndexPath:(NSIndexPath*)indexPath |
| 427 withImage:(UIImage*)image | 1061 withImage:(UIImage*)image |
| 428 backgroundColor:(UIColor*)backgroundColor | 1062 backgroundColor:(UIColor*)backgroundColor |
| 429 textColor:(UIColor*)textColor | 1063 textColor:(UIColor*)textColor |
| 430 fallbackText:(NSString*)text { | 1064 fallbackText:(NSString*)text { |
| 431 BookmarkItemCell* cell = base::mac::ObjCCast<BookmarkItemCell>( | 1065 BookmarkItemCell* cell = base::mac::ObjCCast<BookmarkItemCell>( |
| 432 [self.collectionView cellForItemAtIndexPath:indexPath]); | 1066 [self.collectionView cellForItemAtIndexPath:indexPath]); |
| 433 if (!cell) | 1067 if (!cell) |
| 434 return; | 1068 return; |
| (...skipping 24 matching lines...) Expand all Loading... |
| 459 [self updateEditingStateOfCell:cell | 1093 [self updateEditingStateOfCell:cell |
| 460 atIndexPath:indexPath | 1094 atIndexPath:indexPath |
| 461 animateMenuVisibility:NO | 1095 animateMenuVisibility:NO |
| 462 animateSelectedState:NO]; | 1096 animateSelectedState:NO]; |
| 463 | 1097 |
| 464 [self loadFaviconAtIndexPath:indexPath]; | 1098 [self loadFaviconAtIndexPath:indexPath]; |
| 465 | 1099 |
| 466 return cell; | 1100 return cell; |
| 467 } | 1101 } |
| 468 | 1102 |
| 1103 // Cancels all async loads of favicons. Subclasses should call this method when |
| 1104 // the bookmark model is going through significant changes, then manually call |
| 1105 // loadFaviconAtIndexPath: for everything that needs to be loaded; or |
| 1106 // just reload relevant cells. |
| 469 - (void)cancelAllFaviconLoads { | 1107 - (void)cancelAllFaviconLoads { |
| 470 _faviconTaskTracker.TryCancelAll(); | 1108 _faviconTaskTracker.TryCancelAll(); |
| 471 } | 1109 } |
| 472 | 1110 |
| 473 - (void)cancelLoadingFaviconAtIndexPath:(NSIndexPath*)indexPath { | 1111 - (void)cancelLoadingFaviconAtIndexPath:(NSIndexPath*)indexPath { |
| 474 _faviconTaskTracker.TryCancel( | 1112 _faviconTaskTracker.TryCancel( |
| 475 _faviconLoadTasks[IntegerPair(indexPath.section, indexPath.item)]); | 1113 _faviconLoadTasks[IntegerPair(indexPath.section, indexPath.item)]); |
| 476 } | 1114 } |
| 477 | 1115 |
| 1116 // Asynchronously loads favicon for given index path. The loads are cancelled |
| 1117 // upon cell reuse automatically. |
| 478 - (void)loadFaviconAtIndexPath:(NSIndexPath*)indexPath { | 1118 - (void)loadFaviconAtIndexPath:(NSIndexPath*)indexPath { |
| 479 // Cancel previous load attempts. | 1119 // Cancel previous load attempts. |
| 480 [self cancelLoadingFaviconAtIndexPath:indexPath]; | 1120 [self cancelLoadingFaviconAtIndexPath:indexPath]; |
| 481 | 1121 |
| 482 // Start loading a favicon. | 1122 // Start loading a favicon. |
| 483 __weak BookmarkCollectionView* weakSelf = self; | 1123 __weak BookmarkCollectionView* weakSelf = self; |
| 484 const bookmarks::BookmarkNode* node = [self nodeAtIndexPath:indexPath]; | 1124 const bookmarks::BookmarkNode* node = [self nodeAtIndexPath:indexPath]; |
| 485 GURL blockURL(node->url()); | 1125 GURL blockURL(node->url()); |
| 486 void (^faviconBlock)(const favicon_base::LargeIconResult&) = ^( | 1126 void (^faviconBlock)(const favicon_base::LargeIconResult&) = |
| 487 const favicon_base::LargeIconResult& result) { | 1127 ^(const favicon_base::LargeIconResult& result) { |
| 488 BookmarkCollectionView* strongSelf = weakSelf; | 1128 BookmarkCollectionView* strongSelf = weakSelf; |
| 489 if (!strongSelf) | 1129 if (!strongSelf) |
| 490 return; | 1130 return; |
| 491 UIImage* favIcon = nil; | 1131 UIImage* favIcon = nil; |
| 492 UIColor* backgroundColor = nil; | 1132 UIColor* backgroundColor = nil; |
| 493 UIColor* textColor = nil; | 1133 UIColor* textColor = nil; |
| 494 NSString* fallbackText = nil; | 1134 NSString* fallbackText = nil; |
| 495 if (result.bitmap.is_valid()) { | 1135 if (result.bitmap.is_valid()) { |
| 496 scoped_refptr<base::RefCountedMemory> data = result.bitmap.bitmap_data; | 1136 scoped_refptr<base::RefCountedMemory> data = |
| 497 favIcon = [UIImage imageWithData:[NSData dataWithBytes:data->front() | 1137 result.bitmap.bitmap_data; |
| 498 length:data->size()]]; | 1138 favIcon = [UIImage imageWithData:[NSData dataWithBytes:data->front() |
| 499 } else if (result.fallback_icon_style) { | 1139 length:data->size()]]; |
| 500 backgroundColor = skia::UIColorFromSkColor( | 1140 } else if (result.fallback_icon_style) { |
| 501 result.fallback_icon_style->background_color); | 1141 backgroundColor = skia::UIColorFromSkColor( |
| 502 textColor = | 1142 result.fallback_icon_style->background_color); |
| 503 skia::UIColorFromSkColor(result.fallback_icon_style->text_color); | 1143 textColor = |
| 1144 skia::UIColorFromSkColor(result.fallback_icon_style->text_color); |
| 504 | 1145 |
| 505 fallbackText = | 1146 fallbackText = |
| 506 base::SysUTF16ToNSString(favicon::GetFallbackIconText(blockURL)); | 1147 base::SysUTF16ToNSString(favicon::GetFallbackIconText(blockURL)); |
| 507 } | 1148 } |
| 508 | 1149 |
| 509 [strongSelf updateCellAtIndexPath:indexPath | 1150 [strongSelf updateCellAtIndexPath:indexPath |
| 510 withImage:favIcon | 1151 withImage:favIcon |
| 511 backgroundColor:backgroundColor | 1152 backgroundColor:backgroundColor |
| 512 textColor:textColor | 1153 textColor:textColor |
| 513 fallbackText:fallbackText]; | 1154 fallbackText:fallbackText]; |
| 514 }; | 1155 }; |
| 515 | 1156 |
| 516 CGFloat scale = [UIScreen mainScreen].scale; | 1157 CGFloat scale = [UIScreen mainScreen].scale; |
| 517 CGFloat preferredSize = scale * [BookmarkItemCell preferredImageSize]; | 1158 CGFloat preferredSize = scale * [BookmarkItemCell preferredImageSize]; |
| 518 CGFloat minSize = scale * minFaviconSizePt; | 1159 CGFloat minSize = scale * minFaviconSizePt; |
| 519 | 1160 |
| 520 base::CancelableTaskTracker::TaskId taskId = | 1161 base::CancelableTaskTracker::TaskId taskId = |
| 521 IOSChromeLargeIconServiceFactory::GetForBrowserState(self.browserState) | 1162 IOSChromeLargeIconServiceFactory::GetForBrowserState(self.browserState) |
| 522 ->GetLargeIconOrFallbackStyle(node->url(), minSize, preferredSize, | 1163 ->GetLargeIconOrFallbackStyle(node->url(), minSize, preferredSize, |
| 523 base::BindBlockArc(faviconBlock), | 1164 base::BindBlockArc(faviconBlock), |
| 524 &_faviconTaskTracker); | 1165 &_faviconTaskTracker); |
| (...skipping 10 matching lines...) Expand all Loading... |
| 535 [self updateEditingStateOfCell:cell | 1176 [self updateEditingStateOfCell:cell |
| 536 atIndexPath:indexPath | 1177 atIndexPath:indexPath |
| 537 animateMenuVisibility:NO | 1178 animateMenuVisibility:NO |
| 538 animateSelectedState:NO]; | 1179 animateSelectedState:NO]; |
| 539 [cell updateWithTitle:bookmark_utils_ios::TitleForBookmarkNode(node)]; | 1180 [cell updateWithTitle:bookmark_utils_ios::TitleForBookmarkNode(node)]; |
| 540 [cell setButtonTarget:self action:@selector(didTapMenuButton:view:)]; | 1181 [cell setButtonTarget:self action:@selector(didTapMenuButton:view:)]; |
| 541 | 1182 |
| 542 return cell; | 1183 return cell; |
| 543 } | 1184 } |
| 544 | 1185 |
| 1186 // Updates the editing state for the cell. |
| 545 - (void)updateEditingStateOfCell:(BookmarkCell*)cell | 1187 - (void)updateEditingStateOfCell:(BookmarkCell*)cell |
| 546 atIndexPath:(NSIndexPath*)indexPath | 1188 atIndexPath:(NSIndexPath*)indexPath |
| 547 animateMenuVisibility:(BOOL)animateMenuVisibility | 1189 animateMenuVisibility:(BOOL)animateMenuVisibility |
| 548 animateSelectedState:(BOOL)animateSelectedState { | 1190 animateSelectedState:(BOOL)animateSelectedState { |
| 549 BOOL selected = [self cellIsSelectedForEditingAtIndexPath:indexPath]; | 1191 BOOL selected = [self cellIsSelectedForEditingAtIndexPath:indexPath]; |
| 550 [cell setSelectedForEditing:selected animated:animateSelectedState]; | 1192 [cell setSelectedForEditing:selected animated:animateSelectedState]; |
| 551 BookmarkItemCell* itemCell = static_cast<BookmarkItemCell*>(cell); | 1193 BookmarkItemCell* itemCell = static_cast<BookmarkItemCell*>(cell); |
| 552 [itemCell showButtonOfType:[self buttonTypeForCellAtIndexPath:indexPath] | 1194 [itemCell showButtonOfType:[self buttonTypeForCellAtIndexPath:indexPath] |
| 553 animated:animateMenuVisibility]; | 1195 animated:animateMenuVisibility]; |
| 554 } | 1196 } |
| 555 | 1197 |
| 1198 // |animateMenuVisibility| refers to whether the change in the visibility of the |
| 1199 // menu button is animated. |
| 1200 // |animateSelectedState| refers to whether the change in the selected state (in |
| 1201 // editing mode) of the cell is animated. |
| 1202 // This method updates the visibility of the menu button. |
| 1203 // This method updates the selected state of the cell (in editing mode). |
| 556 - (void)updateEditingStateOfCellAtIndexPath:(NSIndexPath*)indexPath | 1204 - (void)updateEditingStateOfCellAtIndexPath:(NSIndexPath*)indexPath |
| 557 animateMenuVisibility:(BOOL)animateMenuVisibility | 1205 animateMenuVisibility:(BOOL)animateMenuVisibility |
| 558 animateSelectedState:(BOOL)animateSelectedState { | 1206 animateSelectedState:(BOOL)animateSelectedState { |
| 559 BookmarkCell* cell = base::mac::ObjCCast<BookmarkCell>( | 1207 BookmarkCell* cell = base::mac::ObjCCast<BookmarkCell>( |
| 560 [self.collectionView cellForItemAtIndexPath:indexPath]); | 1208 [self.collectionView cellForItemAtIndexPath:indexPath]); |
| 561 if (!cell) | 1209 if (!cell) |
| 562 return; | 1210 return; |
| 563 | 1211 |
| 564 [self updateEditingStateOfCell:cell | 1212 [self updateEditingStateOfCell:cell |
| 565 atIndexPath:indexPath | 1213 atIndexPath:indexPath |
| 566 animateMenuVisibility:animateMenuVisibility | 1214 animateMenuVisibility:animateMenuVisibility |
| 567 animateSelectedState:animateSelectedState]; | 1215 animateSelectedState:animateSelectedState]; |
| 568 } | 1216 } |
| 569 | 1217 |
| 570 #pragma mark - BookmarkModelObserver Callbacks | 1218 // The minimal horizontal space between items to respect between cells in |
| 571 | 1219 // |section|. |
| 572 - (void)bookmarkModelLoaded { | |
| 573 NOTREACHED(); | |
| 574 } | |
| 575 | |
| 576 - (void)bookmarkNodeChanged:(const BookmarkNode*)bookmarkNode { | |
| 577 NOTREACHED(); | |
| 578 } | |
| 579 | |
| 580 - (void)bookmarkNodeChildrenChanged:(const BookmarkNode*)bookmarkNode { | |
| 581 NOTREACHED(); | |
| 582 } | |
| 583 | |
| 584 - (void)bookmarkNode:(const BookmarkNode*)bookmarkNode | |
| 585 movedFromParent:(const BookmarkNode*)oldParent | |
| 586 toParent:(const BookmarkNode*)newParent { | |
| 587 NOTREACHED(); | |
| 588 } | |
| 589 | |
| 590 - (void)bookmarkNodeDeleted:(const BookmarkNode*)node | |
| 591 fromFolder:(const BookmarkNode*)folder { | |
| 592 NOTREACHED(); | |
| 593 } | |
| 594 | |
| 595 - (void)bookmarkModelRemovedAllNodes { | |
| 596 NOTREACHED(); | |
| 597 } | |
| 598 | |
| 599 #pragma mark - Public Methods That Must Be Overridden | |
| 600 | |
| 601 - (BOOL)shouldSelectCellAtIndexPath:(NSIndexPath*)indexPath { | |
| 602 NOTREACHED(); | |
| 603 return NO; | |
| 604 } | |
| 605 | |
| 606 - (void)didTapCellAtIndexPath:(NSIndexPath*)indexPath { | |
| 607 NOTREACHED(); | |
| 608 } | |
| 609 | |
| 610 - (void)didAddCellForEditingAtIndexPath:(NSIndexPath*)indexPath { | |
| 611 NOTREACHED(); | |
| 612 } | |
| 613 | |
| 614 - (void)didRemoveCellForEditingAtIndexPath:(NSIndexPath*)indexPath { | |
| 615 NOTREACHED(); | |
| 616 } | |
| 617 | |
| 618 - (void)didTapMenuButtonAtIndexPath:(NSIndexPath*)indexPath | |
| 619 onView:(UIView*)view | |
| 620 forCell:(BookmarkItemCell*)cell { | |
| 621 NOTREACHED(); | |
| 622 } | |
| 623 | |
| 624 - (bookmark_cell::ButtonType)buttonTypeForCellAtIndexPath: | |
| 625 (NSIndexPath*)indexPath { | |
| 626 NOTREACHED(); | |
| 627 return bookmark_cell::ButtonNone; | |
| 628 } | |
| 629 | |
| 630 - (BOOL)allowLongPressForCellAtIndexPath:(NSIndexPath*)indexPath { | |
| 631 NOTREACHED(); | |
| 632 return NO; | |
| 633 } | |
| 634 | |
| 635 - (void)didLongPressCell:(UICollectionViewCell*)cell | |
| 636 atIndexPath:(NSIndexPath*)indexPath { | |
| 637 NOTREACHED(); | |
| 638 } | |
| 639 | |
| 640 - (BOOL)cellIsSelectedForEditingAtIndexPath:(NSIndexPath*)indexPath { | |
| 641 NOTREACHED(); | |
| 642 return NO; | |
| 643 } | |
| 644 | |
| 645 - (CGSize)headerSizeForSection:(NSInteger)section { | |
| 646 NOTREACHED(); | |
| 647 return CGSizeZero; | |
| 648 } | |
| 649 | |
| 650 - (UICollectionViewCell*)cellAtIndexPath:(NSIndexPath*)indexPath { | |
| 651 NOTREACHED(); | |
| 652 return nil; | |
| 653 } | |
| 654 | |
| 655 - (UICollectionReusableView*)headerAtIndexPath:(NSIndexPath*)indexPath { | |
| 656 NOTREACHED(); | |
| 657 return nil; | |
| 658 } | |
| 659 | |
| 660 - (NSInteger)numberOfItemsInSection:(NSInteger)section { | |
| 661 NOTREACHED(); | |
| 662 return 0; | |
| 663 } | |
| 664 - (NSInteger)numberOfSections { | |
| 665 NOTREACHED(); | |
| 666 return 0; | |
| 667 } | |
| 668 | |
| 669 - (void)updateCollectionView { | |
| 670 NOTREACHED(); | |
| 671 } | |
| 672 | |
| 673 - (const bookmarks::BookmarkNode*)nodeAtIndexPath:(NSIndexPath*)indexPath { | |
| 674 NOTREACHED(); | |
| 675 return nullptr; | |
| 676 } | |
| 677 | |
| 678 #pragma mark - Methods that subclasses can override (UI) | |
| 679 | |
| 680 - (UIEdgeInsets)insetForSectionAtIndex:(NSInteger)section { | |
| 681 if ([self isPromoSection:section]) | |
| 682 return UIEdgeInsetsZero; | |
| 683 | |
| 684 if (IsIPadIdiom()) { | |
| 685 return UIEdgeInsetsMake(10, rowMarginTablet, 0, rowMarginTablet); | |
| 686 } else { | |
| 687 return UIEdgeInsetsZero; | |
| 688 } | |
| 689 } | |
| 690 | |
| 691 - (CGSize)cellSizeForIndexPath:(NSIndexPath*)indexPath { | |
| 692 DCHECK(![self isPromoSection:indexPath.section]); | |
| 693 UIEdgeInsets insets = [self insetForSectionAtIndex:indexPath.section]; | |
| 694 return CGSizeMake(self.bounds.size.width - (insets.right + insets.left), | |
| 695 rowHeight); | |
| 696 } | |
| 697 | |
| 698 - (CGFloat)minimumInteritemSpacingForSectionAtIndex:(NSInteger)section { | 1220 - (CGFloat)minimumInteritemSpacingForSectionAtIndex:(NSInteger)section { |
| 699 return 0; | 1221 return 0; |
| 700 } | 1222 } |
| 701 | 1223 |
| 1224 // The minimal vertical space between items to respect between cells in |
| 1225 // |section|. |
| 702 - (CGFloat)minimumLineSpacingForSectionAtIndex:(NSInteger)section { | 1226 - (CGFloat)minimumLineSpacingForSectionAtIndex:(NSInteger)section { |
| 703 return 0; | 1227 return 0; |
| 704 } | 1228 } |
| 705 | 1229 |
| 1230 // The text to display when there are no items in the collection. Default is |
| 1231 // |IDS_IOS_BOOKMARK_NO_BOOKMARKS_LABEL|. |
| 706 - (NSString*)textWhenCollectionIsEmpty { | 1232 - (NSString*)textWhenCollectionIsEmpty { |
| 707 return l10n_util::GetNSString(IDS_IOS_BOOKMARK_NO_BOOKMARKS_LABEL); | 1233 return l10n_util::GetNSString(IDS_IOS_BOOKMARK_NO_BOOKMARKS_LABEL); |
| 708 } | 1234 } |
| 709 | 1235 |
| 710 #pragma mark - Public Methods That Can Be Overridden | 1236 // In landscape mode, there are 2 widths: 480pt and 568pt. Returns YES if the |
| 711 | 1237 // width is 568pt. |
| 712 - (void)collectionViewScrolled { | |
| 713 } | |
| 714 | |
| 715 #pragma mark - Editing | |
| 716 | |
| 717 - (void)setEditing:(BOOL)editing animated:(BOOL)animated { | |
| 718 if (self.editing == editing) | |
| 719 return; | |
| 720 | |
| 721 _editing = editing; | |
| 722 [UIView animateWithDuration:animated ? 0.2 : 0.0 | |
| 723 delay:0 | |
| 724 options:UIViewAnimationOptionBeginFromCurrentState | |
| 725 animations:^{ | |
| 726 self.shadow.alpha = editing ? 0.0 : 1.0; | |
| 727 } | |
| 728 completion:nil]; | |
| 729 | |
| 730 // If the promo is active this means that it is removed and added as the edit | |
| 731 // mode changes, making reloading the data mandatory. | |
| 732 if ([self isPromoActive]) { | |
| 733 [self.collectionView reloadData]; | |
| 734 [self.collectionView.collectionViewLayout invalidateLayout]; | |
| 735 } else { | |
| 736 // Update the visual state of the bookmark cells without reloading the | |
| 737 // section. | |
| 738 // This prevents flickering of images that need to be asynchronously | |
| 739 // reloaded. | |
| 740 NSArray* indexPaths = [self.collectionView indexPathsForVisibleItems]; | |
| 741 for (NSIndexPath* indexPath in indexPaths) { | |
| 742 [self updateEditingStateOfCellAtIndexPath:indexPath | |
| 743 animateMenuVisibility:animated | |
| 744 animateSelectedState:NO]; | |
| 745 } | |
| 746 } | |
| 747 } | |
| 748 | |
| 749 #pragma mark - Public Methods | |
| 750 | |
| 751 - (void)changeOrientation:(UIInterfaceOrientation)orientation { | |
| 752 [self updateCollectionView]; | |
| 753 } | |
| 754 | |
| 755 - (CGFloat)contentPositionInPortraitOrientation { | |
| 756 if (IsPortrait()) | |
| 757 return self.collectionView.contentOffset.y; | |
| 758 | |
| 759 // In short landscape mode and portrait mode, there are 2 cells per row. | |
| 760 if ([self wideLandscapeMode]) | |
| 761 return self.collectionView.contentOffset.y; | |
| 762 | |
| 763 // In wide landscape mode, there are 3 cells per row. | |
| 764 return self.collectionView.contentOffset.y * 3 / 2.0; | |
| 765 } | |
| 766 | |
| 767 - (void)applyContentPosition:(CGFloat)position { | |
| 768 if (IsLandscape() && [self wideLandscapeMode]) { | |
| 769 position = position * 2 / 3.0; | |
| 770 } | |
| 771 | |
| 772 CGFloat y = | |
| 773 MIN(position, | |
| 774 [self.collectionView.collectionViewLayout collectionViewContentSize] | |
| 775 .height); | |
| 776 self.collectionView.contentOffset = | |
| 777 CGPointMake(self.collectionView.contentOffset.x, y); | |
| 778 } | |
| 779 | |
| 780 - (void)setScrollsToTop:(BOOL)scrollsToTop { | |
| 781 self.collectionView.scrollsToTop = scrollsToTop; | |
| 782 } | |
| 783 | |
| 784 #pragma mark - Private Methods | |
| 785 | |
| 786 - (BOOL)wideLandscapeMode { | 1238 - (BOOL)wideLandscapeMode { |
| 787 return self.frame.size.width > 567; | 1239 return self.frame.size.width > 567; |
| 788 } | 1240 } |
| 789 | 1241 |
| 790 #pragma mark - UIGestureRecognizer Callbacks | |
| 791 | |
| 792 - (void)longPress:(UILongPressGestureRecognizer*)recognizer { | |
| 793 if (self.longPressRecognizer.numberOfTouches != 1 || self.editing) | |
| 794 return; | |
| 795 | |
| 796 if (self.longPressRecognizer.state == UIGestureRecognizerStateRecognized || | |
| 797 self.longPressRecognizer.state == UIGestureRecognizerStateBegan) { | |
| 798 CGPoint point = | |
| 799 [self.longPressRecognizer locationOfTouch:0 inView:self.collectionView]; | |
| 800 NSIndexPath* indexPath = | |
| 801 [self.collectionView indexPathForItemAtPoint:point]; | |
| 802 if (!indexPath) | |
| 803 return; | |
| 804 | |
| 805 UICollectionViewCell* cell = | |
| 806 [self.collectionView cellForItemAtIndexPath:indexPath]; | |
| 807 | |
| 808 // Notify the subclass that long press has been received. | |
| 809 if (cell) | |
| 810 [self didLongPressCell:cell atIndexPath:indexPath]; | |
| 811 } | |
| 812 } | |
| 813 | |
| 814 #pragma mark - UIGestureRecognizerDelegate | |
| 815 | |
| 816 - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer*)gestureRecognizer { | |
| 817 DCHECK(gestureRecognizer == self.longPressRecognizer); | |
| 818 CGPoint point = | |
| 819 [gestureRecognizer locationOfTouch:0 inView:self.collectionView]; | |
| 820 NSIndexPath* indexPath = [self.collectionView indexPathForItemAtPoint:point]; | |
| 821 if (!indexPath) | |
| 822 return NO; | |
| 823 return [self allowLongPressForCellAtIndexPath:indexPath]; | |
| 824 } | |
| 825 | |
| 826 #pragma mark - Promo Cell | 1242 #pragma mark - Promo Cell |
| 827 | 1243 |
| 828 - (BOOL)isPromoSection:(NSInteger)section { | 1244 - (BOOL)isPromoSection:(NSInteger)section { |
| 829 return section == 0 && [self shouldShowPromoCell]; | 1245 return section == 0 && [self shouldShowPromoCell]; |
| 830 } | 1246 } |
| 831 | 1247 |
| 832 - (BOOL)shouldShowPromoCell { | 1248 - (BOOL)shouldShowPromoCell { |
| 833 return NO; | 1249 return _promoVisible; |
| 834 } | 1250 } |
| 835 | 1251 |
| 836 - (BOOL)isPromoActive { | 1252 - (BOOL)isPromoActive { |
| 837 return NO; | 1253 return NO; |
| 838 } | 1254 } |
| 839 | 1255 |
| 840 @end | 1256 @end |
| OLD | NEW |