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