apps_grid_controller.mm revision a93a17c8d99d686bd4a1511e5504e5e6cc9fcadf
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 of the selected item. 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// Moves the selection by |indexDelta| items. 87- (BOOL)moveSelectionByDelta:(int)indexDelta; 88 89@end 90 91namespace app_list { 92 93class AppsGridDelegateBridge : public ui::ListModelObserver { 94 public: 95 AppsGridDelegateBridge(AppsGridController* parent) : parent_(parent) {} 96 97 private: 98 // Overridden from ui::ListModelObserver: 99 virtual void ListItemsAdded(size_t start, size_t count) OVERRIDE { 100 [parent_ listItemsAdded:start 101 count:count]; 102 } 103 virtual void ListItemsRemoved(size_t start, size_t count) OVERRIDE { 104 [parent_ listItemsRemoved:start 105 count:count]; 106 } 107 virtual void ListItemMoved(size_t index, size_t target_index) OVERRIDE { 108 [parent_ listItemMovedFromIndex:index 109 toModelIndex:target_index]; 110 } 111 virtual void ListItemsChanged(size_t start, size_t count) OVERRIDE { 112 NOTREACHED(); 113 } 114 115 AppsGridController* parent_; // Weak, owns us. 116 117 DISALLOW_COPY_AND_ASSIGN(AppsGridDelegateBridge); 118}; 119 120} // namespace app_list 121 122@implementation AppsGridController 123 124+ (void)setScrollAnimationDuration:(NSTimeInterval)duration { 125 g_scroll_duration = duration; 126} 127 128@synthesize paginationObserver = paginationObserver_; 129 130- (id)init { 131 if ((self = [super init])) { 132 bridge_.reset(new app_list::AppsGridDelegateBridge(self)); 133 NSSize cellSize = NSMakeSize(kPreferredTileWidth, kPreferredTileHeight); 134 dragManager_.reset( 135 [[AppsCollectionViewDragManager alloc] initWithCellSize:cellSize 136 rows:kFixedRows 137 columns:kFixedColumns 138 gridController:self]); 139 pages_.reset([[NSMutableArray alloc] init]); 140 items_.reset([[NSMutableArray alloc] init]); 141 [self loadAndSetView]; 142 [self updatePages:0]; 143 } 144 return self; 145} 146 147- (void)dealloc { 148 [[NSNotificationCenter defaultCenter] removeObserver:self]; 149 [self setModel:scoped_ptr<app_list::AppListModel>()]; 150 [super dealloc]; 151} 152 153- (NSCollectionView*)collectionViewAtPageIndex:(size_t)pageIndex { 154 return [pages_ objectAtIndex:pageIndex]; 155} 156 157- (size_t)pageIndexForCollectionView:(NSCollectionView*)page { 158 for (size_t pageIndex = 0; pageIndex < [pages_ count]; ++pageIndex) { 159 if (page == [self collectionViewAtPageIndex:pageIndex]) 160 return pageIndex; 161 } 162 return NSNotFound; 163} 164 165- (app_list::AppListModel*)model { 166 return model_.get(); 167} 168 169- (void)setModel:(scoped_ptr<app_list::AppListModel>)newModel { 170 if (model_) { 171 model_->apps()->RemoveObserver(bridge_.get()); 172 173 // Since the model is about to be deleted, and the AppKit objects might be 174 // sitting in an NSAutoreleasePool, ensure there are no references to the 175 // model. 176 for (size_t i = 0; i < [items_ count]; ++i) 177 [[self itemAtIndex:i] setModel:NULL]; 178 } 179 180 model_.reset(newModel.release()); 181 if (model_) 182 model_->apps()->AddObserver(bridge_.get()); 183 184 [self modelUpdated]; 185} 186 187- (void)setDelegate:(app_list::AppListViewDelegate*)newDelegate { 188 scoped_ptr<app_list::AppListModel> newModel(new app_list::AppListModel); 189 delegate_ = newDelegate; 190 if (delegate_) 191 delegate_->SetModel(newModel.get()); // Populates items. 192 [self setModel:newModel.Pass()]; 193} 194 195- (size_t)visiblePage { 196 return visiblePage_; 197} 198 199- (void)activateSelection { 200 [[self selectedButton] performClick:self]; 201} 202 203- (size_t)pageCount { 204 return [pages_ count]; 205} 206 207- (size_t)itemCount { 208 return [items_ count]; 209} 210 211- (void)scrollToPage:(size_t)pageIndex { 212 NSClipView* clipView = [[self gridScrollView] contentView]; 213 NSPoint newOrigin = [clipView bounds].origin; 214 215 // Scrolling outside of this range is edge elasticity, which animates 216 // automatically. 217 if ((pageIndex == 0 && (newOrigin.x <= 0)) || 218 (pageIndex + 1 == [self pageCount] && 219 newOrigin.x >= pageIndex * kViewWidth)) { 220 return; 221 } 222 223 // Clear any selection on the current page (unless it has been removed). 224 if (visiblePage_ < [pages_ count]) { 225 [[self collectionViewAtPageIndex:visiblePage_] 226 setSelectionIndexes:[NSIndexSet indexSet]]; 227 } 228 229 newOrigin.x = pageIndex * kViewWidth; 230 [NSAnimationContext beginGrouping]; 231 [[NSAnimationContext currentContext] setDuration:g_scroll_duration]; 232 [[clipView animator] setBoundsOrigin:newOrigin]; 233 [NSAnimationContext endGrouping]; 234 animatingScroll_ = YES; 235} 236 237- (void)cancelScrollAnimation { 238 NSClipView* clipView = [[self gridScrollView] contentView]; 239 [NSAnimationContext beginGrouping]; 240 [[NSAnimationContext currentContext] setDuration:0]; 241 [[clipView animator] setBoundsOrigin:[clipView bounds].origin]; 242 [NSAnimationContext endGrouping]; 243 animatingScroll_ = NO; 244} 245 246- (size_t)nearestPageIndex { 247 return lround( 248 NSMinX([[[self gridScrollView] contentView] bounds]) / kViewWidth); 249} 250 251- (void)userScrolling:(BOOL)isScrolling { 252 if (isScrolling) { 253 if (animatingScroll_) 254 [self cancelScrollAnimation]; 255 } else { 256 [self scrollToPage:[self nearestPageIndex]]; 257 } 258} 259 260- (void)loadAndSetView { 261 scoped_nsobject<NSView> pagesContainer( 262 [[NSView alloc] initWithFrame:NSZeroRect]); 263 264 NSRect scrollFrame = NSMakeRect(0, 0, kViewWidth, kViewHeight + kTopPadding); 265 scoped_nsobject<ScrollViewWithNoScrollbars> scrollView( 266 [[ScrollViewWithNoScrollbars alloc] initWithFrame:scrollFrame]); 267 [scrollView setBorderType:NSNoBorder]; 268 [scrollView setLineScroll:kViewWidth]; 269 [scrollView setPageScroll:kViewWidth]; 270 [scrollView setDelegate:self]; 271 [scrollView setDocumentView:pagesContainer]; 272 [scrollView setDrawsBackground:NO]; 273 274 [[NSNotificationCenter defaultCenter] 275 addObserver:self 276 selector:@selector(boundsDidChange:) 277 name:NSViewBoundsDidChangeNotification 278 object:[scrollView contentView]]; 279 280 [self setView:scrollView]; 281} 282 283- (void)boundsDidChange:(NSNotification*)notification { 284 size_t newPage = [self nearestPageIndex]; 285 if (newPage == visiblePage_) { 286 [paginationObserver_ pageVisibilityChanged]; 287 return; 288 } 289 290 visiblePage_ = newPage; 291 [paginationObserver_ selectedPageChanged:newPage]; 292 [paginationObserver_ pageVisibilityChanged]; 293} 294 295- (void)onItemClicked:(id)sender { 296 for (size_t i = 0; i < [items_ count]; ++i) { 297 AppsGridViewItem* item = [self itemAtIndex:i]; 298 if ([[item button] isEqual:sender]) 299 delegate_->ActivateAppListItem([item model], 0); 300 } 301} 302 303- (AppsGridViewItem*)itemAtPageIndex:(size_t)pageIndex 304 indexInPage:(size_t)indexInPage { 305 return base::mac::ObjCCastStrict<AppsGridViewItem>( 306 [[self collectionViewAtPageIndex:pageIndex] itemAtIndex:indexInPage]); 307} 308 309- (AppsGridViewItem*)itemAtIndex:(size_t)itemIndex { 310 const size_t pageIndex = itemIndex / kItemsPerPage; 311 return [self itemAtPageIndex:pageIndex 312 indexInPage:itemIndex - pageIndex * kItemsPerPage]; 313} 314 315- (void)modelUpdated { 316 [items_ removeAllObjects]; 317 if (model_ && model_->apps()->item_count()) { 318 [self listItemsAdded:0 319 count:model_->apps()->item_count()]; 320 } else { 321 [self updatePages:0]; 322 } 323 [self scrollToPage:0]; 324} 325 326- (NSUInteger)selectedItemIndex { 327 NSCollectionView* page = [self collectionViewAtPageIndex:visiblePage_]; 328 NSUInteger indexOnPage = [[page selectionIndexes] firstIndex]; 329 if (indexOnPage == NSNotFound) 330 return NSNotFound; 331 332 return indexOnPage + visiblePage_ * kItemsPerPage; 333} 334 335- (NSButton*)selectedButton { 336 NSUInteger index = [self selectedItemIndex]; 337 if (index == NSNotFound) 338 return nil; 339 340 return [[self itemAtIndex:index] button]; 341} 342 343- (NSScrollView*)gridScrollView { 344 return base::mac::ObjCCastStrict<NSScrollView>([self view]); 345} 346 347- (NSView*)pagesContainerView { 348 return [[self gridScrollView] documentView]; 349} 350 351- (void)updatePages:(size_t)startItemIndex { 352 // Note there is always at least one page. 353 size_t targetPages = 1; 354 if ([items_ count] != 0) 355 targetPages = ([items_ count] - 1) / kItemsPerPage + 1; 356 357 const size_t currentPages = [self pageCount]; 358 // First see if the number of pages have changed. 359 if (targetPages != currentPages) { 360 if (targetPages < currentPages) { 361 // Pages need to be removed. 362 [pages_ removeObjectsInRange:NSMakeRange(targetPages, 363 currentPages - targetPages)]; 364 } else { 365 // Pages need to be added. 366 for (size_t i = currentPages; i < targetPages; ++i) { 367 NSRect pageFrame = NSMakeRect( 368 kLeftRightPadding + kViewWidth * i, 0, 369 kViewWidth, kViewHeight); 370 [pages_ addObject:[dragManager_ makePageWithFrame:pageFrame]]; 371 } 372 } 373 374 [[self pagesContainerView] setSubviews:pages_]; 375 NSSize pagesSize = NSMakeSize(kViewWidth * targetPages, kViewHeight); 376 [[self pagesContainerView] setFrameSize:pagesSize]; 377 [paginationObserver_ totalPagesChanged]; 378 } 379 380 const size_t startPage = startItemIndex / kItemsPerPage; 381 // All pages on or after |startPage| may need items added or removed. 382 for (size_t pageIndex = startPage; pageIndex < targetPages; ++pageIndex) { 383 [self updatePageContent:pageIndex 384 resetModel:YES]; 385 } 386} 387 388- (void)updatePageContent:(size_t)pageIndex 389 resetModel:(BOOL)resetModel { 390 NSCollectionView* pageView = [self collectionViewAtPageIndex:pageIndex]; 391 if (resetModel) { 392 // Clear the models first, otherwise removed items could be autoreleased at 393 // an unknown point in the future, when the model owner may have gone away. 394 for (size_t i = 0; i < [[pageView content] count]; ++i) { 395 AppsGridViewItem* item = base::mac::ObjCCastStrict<AppsGridViewItem>( 396 [pageView itemAtIndex:i]); 397 [item setModel:NULL]; 398 } 399 } 400 401 NSRange inPageRange = NSIntersectionRange( 402 NSMakeRange(pageIndex * kItemsPerPage, kItemsPerPage), 403 NSMakeRange(0, [items_ count])); 404 NSArray* pageContent = [items_ subarrayWithRange:inPageRange]; 405 [pageView setContent:pageContent]; 406 if (!resetModel) 407 return; 408 409 for (size_t i = 0; i < [pageContent count]; ++i) { 410 AppsGridViewItem* item = base::mac::ObjCCastStrict<AppsGridViewItem>( 411 [pageView itemAtIndex:i]); 412 [item setModel:static_cast<app_list::AppListItemModel*>( 413 [[pageContent objectAtIndex:i] pointerValue])]; 414 } 415} 416 417- (void)moveItemInView:(size_t)fromIndex 418 toItemIndex:(size_t)toIndex { 419 scoped_nsobject<NSValue> item([[items_ objectAtIndex:fromIndex] retain]); 420 [items_ removeObjectAtIndex:fromIndex]; 421 [items_ insertObject:item 422 atIndex:toIndex]; 423 424 size_t fromPageIndex = fromIndex / kItemsPerPage; 425 size_t toPageIndex = toIndex / kItemsPerPage; 426 if (fromPageIndex == toPageIndex) { 427 [self updatePageContent:fromPageIndex 428 resetModel:NO]; // Just reorder items. 429 return; 430 } 431 432 if (fromPageIndex > toPageIndex) 433 std::swap(fromPageIndex, toPageIndex); 434 435 for (size_t i = fromPageIndex; i <= toPageIndex; ++i) { 436 [self updatePageContent:i 437 resetModel:YES]; 438 } 439} 440 441// Compare with views implementation in AppsGridView::MoveItemInModel(). 442- (void)moveItemWithIndex:(size_t)itemIndex 443 toModelIndex:(size_t)modelIndex { 444 // Ingore no-op moves. Note that this is always the case when canceled. 445 if (itemIndex == modelIndex) 446 return; 447 448 model_->apps()->RemoveObserver(bridge_.get()); 449 model_->apps()->Move(itemIndex, modelIndex); 450 model_->apps()->AddObserver(bridge_.get()); 451} 452 453- (AppsCollectionViewDragManager*)dragManager { 454 return dragManager_; 455} 456 457- (void)listItemsAdded:(size_t)start 458 count:(size_t)count { 459 // Cancel any drag, to ensure the model stays consistent. 460 [dragManager_ cancelDrag]; 461 462 for (size_t i = start; i < start + count; ++i) { 463 app_list::AppListItemModel* itemModel = model_->apps()->GetItemAt(i); 464 [items_ insertObject:[NSValue valueWithPointer:itemModel] 465 atIndex:i]; 466 } 467 468 [self updatePages:start]; 469} 470 471- (void)listItemsRemoved:(size_t)start 472 count:(size_t)count { 473 [dragManager_ cancelDrag]; 474 475 // Clear the models explicitly to avoid surprises from autorelease. 476 for (size_t i = start; i < start + count; ++i) 477 [[self itemAtIndex:i] setModel:NULL]; 478 479 [items_ removeObjectsInRange:NSMakeRange(start, count)]; 480 [self updatePages:start]; 481} 482 483- (void)listItemMovedFromIndex:(size_t)fromIndex 484 toModelIndex:(size_t)toIndex { 485 [dragManager_ cancelDrag]; 486 [self moveItemInView:fromIndex 487 toItemIndex:toIndex]; 488} 489 490- (CGFloat)visiblePortionOfPage:(int)page { 491 CGFloat scrollOffsetOfPage = 492 NSMinX([[[self gridScrollView] contentView] bounds]) / kViewWidth - page; 493 if (scrollOffsetOfPage <= -1.0 || scrollOffsetOfPage >= 1.0) 494 return 0.0; 495 496 if (scrollOffsetOfPage <= 0.0) 497 return scrollOffsetOfPage + 1.0; 498 499 return -1.0 + scrollOffsetOfPage; 500} 501 502- (void)onPagerClicked:(AppListPagerView*)sender { 503 int selectedSegment = [sender selectedSegment]; 504 if (selectedSegment < 0) 505 return; // No selection. 506 507 int pageIndex = [[sender cell] tagForSegment:selectedSegment]; 508 if (pageIndex >= 0) 509 [self scrollToPage:pageIndex]; 510} 511 512- (BOOL)moveSelectionByDelta:(int)indexDelta { 513 if (indexDelta == 0) 514 return NO; 515 516 NSUInteger oldIndex = [self selectedItemIndex]; 517 518 // If nothing is currently selected, select the first item on the page. 519 if (oldIndex == NSNotFound) { 520 [self selectItemAtIndex:visiblePage_ * kItemsPerPage]; 521 return YES; 522 } 523 524 if ((indexDelta < 0 && static_cast<NSUInteger>(-indexDelta) > oldIndex) || 525 oldIndex + indexDelta >= [items_ count]) { 526 return NO; 527 } 528 529 [self selectItemAtIndex:oldIndex + indexDelta]; 530 return YES; 531} 532 533- (void)selectItemAtIndex:(NSUInteger)index { 534 if (index >= [items_ count]) 535 return; 536 537 if (index / kItemsPerPage != visiblePage_) 538 [self scrollToPage:index / kItemsPerPage]; 539 540 [[self itemAtIndex:index] setSelected:YES]; 541} 542 543- (BOOL)handleCommandBySelector:(SEL)command { 544 if (command == @selector(insertNewline:) || 545 command == @selector(insertLineBreak:)) { 546 [self activateSelection]; 547 return YES; 548 } 549 550 if (command == @selector(moveLeft:)) 551 return [self moveSelectionByDelta:-1]; 552 553 if (command == @selector(moveRight:)) 554 return [self moveSelectionByDelta:1]; 555 556 if (command == @selector(moveUp:)) 557 return [self moveSelectionByDelta:-kFixedColumns]; 558 559 if (command == @selector(moveDown:)) 560 return [self moveSelectionByDelta:kFixedColumns]; 561 562 if (command == @selector(pageUp:) || 563 command == @selector(scrollPageUp:)) 564 return [self moveSelectionByDelta:-kItemsPerPage]; 565 566 if (command == @selector(pageDown:) || 567 command == @selector(scrollPageDown:)) 568 return [self moveSelectionByDelta:kItemsPerPage]; 569 570 return NO; 571} 572 573@end 574