apps_grid_controller.mm revision b2df76ea8fec9e32f6f3718986dba0d95315b29c
1// Copyright 2013 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#import "ui/app_list/cocoa/apps_grid_controller.h" 6 7#include "base/mac/foundation_util.h" 8#include "ui/app_list/app_list_model.h" 9#include "ui/app_list/app_list_model_observer.h" 10#include "ui/app_list/app_list_view_delegate.h" 11#import "ui/app_list/cocoa/apps_collection_view_drag_manager.h" 12#import "ui/app_list/cocoa/apps_grid_view_item.h" 13#import "ui/app_list/cocoa/apps_pagination_model_observer.h" 14#include "ui/base/models/list_model_observer.h" 15 16namespace { 17 18// OSX app list has hardcoded rows and columns for now. 19const int kFixedRows = 4; 20const int kFixedColumns = 4; 21const int kItemsPerPage = kFixedRows * kFixedColumns; 22 23// Padding space in pixels for fixed layout. 24const CGFloat kLeftRightPadding = 16; 25const CGFloat kTopPadding = 30; 26 27// Preferred tile size when showing in fixed layout. These should be even 28// numbers to ensure that if they are grown 50% they remain integers. 29const CGFloat kPreferredTileWidth = 88; 30const CGFloat kPreferredTileHeight = 98; 31 32const CGFloat kViewWidth = 33 kFixedColumns * kPreferredTileWidth + 2 * kLeftRightPadding; 34const CGFloat kViewHeight = kFixedRows * kPreferredTileHeight; 35 36NSTimeInterval g_scroll_duration = 0.18; 37 38} // namespace 39 40@interface AppsGridController () 41 42// Cancel a currently running scroll animation. 43- (void)cancelScrollAnimation; 44 45// Index of the page with the most content currently visible. 46- (size_t)nearestPageIndex; 47 48// Bootstrap the views this class controls. 49- (void)loadAndSetView; 50 51- (void)boundsDidChange:(NSNotification*)notification; 52 53// Action for buttons in the grid. 54- (void)onItemClicked:(id)sender; 55 56- (AppsGridViewItem*)itemAtPageIndex:(size_t)pageIndex 57 indexInPage:(size_t)indexInPage; 58 59// Update the model in full, and rebuild subviews. 60- (void)modelUpdated; 61 62// Return the button selected in first page with a selection. 63- (NSButton*)selectedButton; 64 65// The scroll view holding the grid pages. 66- (NSScrollView*)gridScrollView; 67 68- (NSView*)pagesContainerView; 69 70// Create any new pages after updating |items_|. 71- (void)updatePages:(size_t)startItemIndex; 72 73- (void)updatePageContent:(size_t)pageIndex 74 resetModel:(BOOL)resetModel; 75 76// Bridged methods for ui::ListModelObserver. 77- (void)listItemsAdded:(size_t)start 78 count:(size_t)count; 79 80- (void)listItemsRemoved:(size_t)start 81 count:(size_t)count; 82 83- (void)listItemMovedFromIndex:(size_t)fromIndex 84 toModelIndex:(size_t)toIndex; 85 86@end 87 88namespace app_list { 89 90class AppsGridDelegateBridge : public ui::ListModelObserver { 91 public: 92 AppsGridDelegateBridge(AppsGridController* parent) : parent_(parent) {} 93 94 private: 95 // Overridden from ui::ListModelObserver: 96 virtual void ListItemsAdded(size_t start, size_t count) OVERRIDE { 97 [parent_ listItemsAdded:start 98 count:count]; 99 } 100 virtual void ListItemsRemoved(size_t start, size_t count) OVERRIDE { 101 [parent_ listItemsRemoved:start 102 count:count]; 103 } 104 virtual void ListItemMoved(size_t index, size_t target_index) OVERRIDE { 105 [parent_ listItemMovedFromIndex:index 106 toModelIndex:target_index]; 107 } 108 virtual void ListItemsChanged(size_t start, size_t count) OVERRIDE { 109 NOTREACHED(); 110 } 111 112 AppsGridController* parent_; // Weak, owns us. 113 114 DISALLOW_COPY_AND_ASSIGN(AppsGridDelegateBridge); 115}; 116 117} // namespace app_list 118 119@implementation AppsGridController 120 121+ (void)setScrollAnimationDuration:(NSTimeInterval)duration { 122 g_scroll_duration = duration; 123} 124 125@synthesize paginationObserver = paginationObserver_; 126 127- (id)init { 128 if ((self = [super init])) { 129 bridge_.reset(new app_list::AppsGridDelegateBridge(self)); 130 NSSize cellSize = NSMakeSize(kPreferredTileWidth, kPreferredTileHeight); 131 dragManager_.reset( 132 [[AppsCollectionViewDragManager alloc] initWithCellSize:cellSize 133 rows:kFixedRows 134 columns:kFixedColumns 135 gridController:self]); 136 pages_.reset([[NSMutableArray alloc] init]); 137 items_.reset([[NSMutableArray alloc] init]); 138 [self loadAndSetView]; 139 [self updatePages:0]; 140 } 141 return self; 142} 143 144- (void)dealloc { 145 [[NSNotificationCenter defaultCenter] removeObserver:self]; 146 [self setModel:scoped_ptr<app_list::AppListModel>()]; 147 [super dealloc]; 148} 149 150- (NSCollectionView*)collectionViewAtPageIndex:(size_t)pageIndex { 151 return [pages_ objectAtIndex:pageIndex]; 152} 153 154- (size_t)pageIndexForCollectionView:(NSCollectionView*)page { 155 for (size_t pageIndex = 0; pageIndex < [pages_ count]; ++pageIndex) { 156 if (page == [self collectionViewAtPageIndex:pageIndex]) 157 return pageIndex; 158 } 159 return NSNotFound; 160} 161 162- (app_list::AppListModel*)model { 163 return model_.get(); 164} 165 166- (void)setModel:(scoped_ptr<app_list::AppListModel>)newModel { 167 if (model_) { 168 model_->apps()->RemoveObserver(bridge_.get()); 169 170 // Since the model is about to be deleted, and the AppKit objects might be 171 // sitting in an NSAutoreleasePool, ensure there are no references to the 172 // model. 173 for (size_t i = 0; i < [items_ count]; ++i) 174 [[self itemAtIndex:i] setModel:NULL]; 175 } 176 177 model_.reset(newModel.release()); 178 if (model_) 179 model_->apps()->AddObserver(bridge_.get()); 180 181 [self modelUpdated]; 182} 183 184- (void)setDelegate:(app_list::AppListViewDelegate*)newDelegate { 185 scoped_ptr<app_list::AppListModel> newModel(new app_list::AppListModel); 186 delegate_ = newDelegate; 187 if (delegate_) 188 delegate_->SetModel(newModel.get()); // Populates items. 189 [self setModel:newModel.Pass()]; 190} 191 192- (size_t)visiblePage { 193 return visiblePage_; 194} 195 196- (void)activateSelection { 197 [[self selectedButton] performClick:self]; 198} 199 200- (size_t)pageCount { 201 return [pages_ count]; 202} 203 204- (size_t)itemCount { 205 return [items_ count]; 206} 207 208- (void)scrollToPage:(size_t)pageIndex { 209 NSClipView* clipView = [[self gridScrollView] contentView]; 210 NSPoint newOrigin = [clipView bounds].origin; 211 212 // Scrolling outside of this range is edge elasticity, which animates 213 // automatically. 214 if ((pageIndex == 0 && (newOrigin.x <= 0)) || 215 (pageIndex + 1 == [self pageCount] && 216 newOrigin.x >= pageIndex * kViewWidth)) { 217 return; 218 } 219 220 newOrigin.x = pageIndex * kViewWidth; 221 [NSAnimationContext beginGrouping]; 222 [[NSAnimationContext currentContext] setDuration:g_scroll_duration]; 223 [[clipView animator] setBoundsOrigin:newOrigin]; 224 [NSAnimationContext endGrouping]; 225 animatingScroll_ = YES; 226} 227 228- (void)cancelScrollAnimation { 229 NSClipView* clipView = [[self gridScrollView] contentView]; 230 [NSAnimationContext beginGrouping]; 231 [[NSAnimationContext currentContext] setDuration:0]; 232 [[clipView animator] setBoundsOrigin:[clipView bounds].origin]; 233 [NSAnimationContext endGrouping]; 234 animatingScroll_ = NO; 235} 236 237- (size_t)nearestPageIndex { 238 return lround( 239 NSMinX([[[self gridScrollView] contentView] bounds]) / kViewWidth); 240} 241 242- (void)userScrolling:(BOOL)isScrolling { 243 if (isScrolling) { 244 if (animatingScroll_) 245 [self cancelScrollAnimation]; 246 } else { 247 [self scrollToPage:[self nearestPageIndex]]; 248 } 249} 250 251- (void)loadAndSetView { 252 scoped_nsobject<NSView> pagesContainer( 253 [[NSView alloc] initWithFrame:NSZeroRect]); 254 255 NSRect scrollFrame = NSMakeRect(0, 0, kViewWidth, kViewHeight + kTopPadding); 256 scoped_nsobject<ScrollViewWithNoScrollbars> scrollView( 257 [[ScrollViewWithNoScrollbars alloc] initWithFrame:scrollFrame]); 258 [scrollView setBorderType:NSNoBorder]; 259 [scrollView setLineScroll:kViewWidth]; 260 [scrollView setPageScroll:kViewWidth]; 261 [scrollView setDelegate:self]; 262 [scrollView setDocumentView:pagesContainer]; 263 [scrollView setDrawsBackground:NO]; 264 265 [[NSNotificationCenter defaultCenter] 266 addObserver:self 267 selector:@selector(boundsDidChange:) 268 name:NSViewBoundsDidChangeNotification 269 object:[scrollView contentView]]; 270 271 [self setView:scrollView]; 272} 273 274- (void)boundsDidChange:(NSNotification*)notification { 275 if ([self nearestPageIndex] == visiblePage_) { 276 [paginationObserver_ pageVisibilityChanged]; 277 return; 278 } 279 280 // Clear any selection on the previous page (unless it has been removed). 281 if (visiblePage_ < [pages_ count]) { 282 [[self collectionViewAtPageIndex:visiblePage_] 283 setSelectionIndexes:[NSIndexSet indexSet]]; 284 } 285 visiblePage_ = [self nearestPageIndex]; 286 [paginationObserver_ selectedPageChanged:visiblePage_]; 287 [paginationObserver_ pageVisibilityChanged]; 288} 289 290- (void)onItemClicked:(id)sender { 291 for (size_t i = 0; i < [items_ count]; ++i) { 292 AppsGridViewItem* item = [self itemAtIndex:i]; 293 if ([[item button] isEqual:sender]) 294 delegate_->ActivateAppListItem([item model], 0); 295 } 296} 297 298- (AppsGridViewItem*)itemAtPageIndex:(size_t)pageIndex 299 indexInPage:(size_t)indexInPage { 300 return base::mac::ObjCCastStrict<AppsGridViewItem>( 301 [[self collectionViewAtPageIndex:pageIndex] itemAtIndex:indexInPage]); 302} 303 304- (AppsGridViewItem*)itemAtIndex:(size_t)itemIndex { 305 const size_t pageIndex = itemIndex / kItemsPerPage; 306 return [self itemAtPageIndex:pageIndex 307 indexInPage:itemIndex - pageIndex * kItemsPerPage]; 308} 309 310- (void)modelUpdated { 311 [items_ removeAllObjects]; 312 if (model_ && model_->apps()->item_count()) { 313 [self listItemsAdded:0 314 count:model_->apps()->item_count()]; 315 } else { 316 [self updatePages:0]; 317 } 318} 319 320- (NSButton*)selectedButton { 321 NSIndexSet* selection = nil; 322 size_t pageIndex = 0; 323 for (; pageIndex < [self pageCount]; ++pageIndex) { 324 selection = [[self collectionViewAtPageIndex:pageIndex] selectionIndexes]; 325 if ([selection count] > 0) 326 break; 327 } 328 329 if (pageIndex == [self pageCount]) 330 return nil; 331 332 return [[self itemAtPageIndex:pageIndex 333 indexInPage:[selection firstIndex]] button]; 334} 335 336- (NSScrollView*)gridScrollView { 337 return base::mac::ObjCCastStrict<NSScrollView>([self view]); 338} 339 340- (NSView*)pagesContainerView { 341 return [[self gridScrollView] documentView]; 342} 343 344- (void)updatePages:(size_t)startItemIndex { 345 // Note there is always at least one page. 346 size_t targetPages = 1; 347 if ([items_ count] != 0) 348 targetPages = ([items_ count] - 1) / kItemsPerPage + 1; 349 350 const size_t currentPages = [self pageCount]; 351 // First see if the number of pages have changed. 352 if (targetPages != currentPages) { 353 if (targetPages < currentPages) { 354 // Pages need to be removed. 355 [pages_ removeObjectsInRange:NSMakeRange(targetPages, 356 currentPages - targetPages)]; 357 } else { 358 // Pages need to be added. 359 for (size_t i = currentPages; i < targetPages; ++i) { 360 NSRect pageFrame = NSMakeRect( 361 kLeftRightPadding + kViewWidth * i, 0, 362 kViewWidth, kViewHeight); 363 [pages_ addObject:[dragManager_ makePageWithFrame:pageFrame]]; 364 } 365 } 366 367 [[self pagesContainerView] setSubviews:pages_]; 368 NSSize pagesSize = NSMakeSize(kViewWidth * targetPages, kViewHeight); 369 [[self pagesContainerView] setFrameSize:pagesSize]; 370 [paginationObserver_ totalPagesChanged]; 371 } 372 373 const size_t startPage = startItemIndex / kItemsPerPage; 374 // All pages on or after |startPage| may need items added or removed. 375 for (size_t pageIndex = startPage; pageIndex < targetPages; ++pageIndex) { 376 [self updatePageContent:pageIndex 377 resetModel:YES]; 378 } 379} 380 381- (void)updatePageContent:(size_t)pageIndex 382 resetModel:(BOOL)resetModel { 383 NSCollectionView* pageView = [self collectionViewAtPageIndex:pageIndex]; 384 if (resetModel) { 385 // Clear the models first, otherwise removed items could be autoreleased at 386 // an unknown point in the future, when the model owner may have gone away. 387 for (size_t i = 0; i < [[pageView content] count]; ++i) { 388 AppsGridViewItem* item = base::mac::ObjCCastStrict<AppsGridViewItem>( 389 [pageView itemAtIndex:i]); 390 [item setModel:NULL]; 391 } 392 } 393 394 NSRange inPageRange = NSIntersectionRange( 395 NSMakeRange(pageIndex * kItemsPerPage, kItemsPerPage), 396 NSMakeRange(0, [items_ count])); 397 NSArray* pageContent = [items_ subarrayWithRange:inPageRange]; 398 [pageView setContent:pageContent]; 399 if (!resetModel) 400 return; 401 402 for (size_t i = 0; i < [pageContent count]; ++i) { 403 AppsGridViewItem* item = base::mac::ObjCCastStrict<AppsGridViewItem>( 404 [pageView itemAtIndex:i]); 405 [item setModel:static_cast<app_list::AppListItemModel*>( 406 [[pageContent objectAtIndex:i] pointerValue])]; 407 } 408} 409 410- (void)moveItemInView:(size_t)fromIndex 411 toItemIndex:(size_t)toIndex { 412 scoped_nsobject<NSValue> item([[items_ objectAtIndex:fromIndex] retain]); 413 [items_ removeObjectAtIndex:fromIndex]; 414 [items_ insertObject:item 415 atIndex:toIndex]; 416 417 size_t fromPageIndex = fromIndex / kItemsPerPage; 418 size_t toPageIndex = toIndex / kItemsPerPage; 419 if (fromPageIndex == toPageIndex) { 420 [self updatePageContent:fromPageIndex 421 resetModel:NO]; // Just reorder items. 422 return; 423 } 424 425 if (fromPageIndex > toPageIndex) 426 std::swap(fromPageIndex, toPageIndex); 427 428 for (size_t i = fromPageIndex; i <= toPageIndex; ++i) { 429 [self updatePageContent:i 430 resetModel:YES]; 431 } 432} 433 434// Compare with views implementation in AppsGridView::MoveItemInModel(). 435- (void)moveItemWithIndex:(size_t)itemIndex 436 toModelIndex:(size_t)modelIndex { 437 // Ingore no-op moves. Note that this is always the case when canceled. 438 if (itemIndex == modelIndex) 439 return; 440 441 model_->apps()->RemoveObserver(bridge_.get()); 442 model_->apps()->Move(itemIndex, modelIndex); 443 model_->apps()->AddObserver(bridge_.get()); 444} 445 446- (AppsCollectionViewDragManager*)dragManager { 447 return dragManager_; 448} 449 450- (void)listItemsAdded:(size_t)start 451 count:(size_t)count { 452 // Cancel any drag, to ensure the model stays consistent. 453 [dragManager_ cancelDrag]; 454 455 for (size_t i = start; i < start + count; ++i) { 456 app_list::AppListItemModel* itemModel = model_->apps()->GetItemAt(i); 457 [items_ insertObject:[NSValue valueWithPointer:itemModel] 458 atIndex:i]; 459 } 460 461 [self updatePages:start]; 462} 463 464- (void)listItemsRemoved:(size_t)start 465 count:(size_t)count { 466 [dragManager_ cancelDrag]; 467 468 // Clear the models explicitly to avoid surprises from autorelease. 469 for (size_t i = start; i < start + count; ++i) 470 [[self itemAtIndex:i] setModel:NULL]; 471 472 [items_ removeObjectsInRange:NSMakeRange(start, count)]; 473 [self updatePages:start]; 474} 475 476- (void)listItemMovedFromIndex:(size_t)fromIndex 477 toModelIndex:(size_t)toIndex { 478 [dragManager_ cancelDrag]; 479 [self moveItemInView:fromIndex 480 toItemIndex:toIndex]; 481} 482 483- (CGFloat)visiblePortionOfPage:(int)page { 484 CGFloat scrollOffsetOfPage = 485 NSMinX([[[self gridScrollView] contentView] bounds]) / kViewWidth - page; 486 if (scrollOffsetOfPage <= -1.0 || scrollOffsetOfPage >= 1.0) 487 return 0.0; 488 489 if (scrollOffsetOfPage <= 0.0) 490 return scrollOffsetOfPage + 1.0; 491 492 return -1.0 + scrollOffsetOfPage; 493} 494 495- (void)onPagerClicked:(AppListPagerView*)sender { 496 int selectedSegment = [sender selectedSegment]; 497 if (selectedSegment < 0) 498 return; // No selection. 499 500 int pageIndex = [[sender cell] tagForSegment:selectedSegment]; 501 if (pageIndex >= 0) 502 [self scrollToPage:pageIndex]; 503} 504 505@end 506