Supervised user whitelists: Cleanup
[chromium-blink-merge.git] / ui / app_list / cocoa / apps_grid_controller.mm
blobd9f5e7b37c6a991137f7e0c47665fc0860a2389b
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.
5 #import "ui/app_list/cocoa/apps_grid_controller.h"
7 #include "base/mac/foundation_util.h"
8 #include "ui/app_list/app_list_item.h"
9 #include "ui/app_list/app_list_model.h"
10 #include "ui/app_list/app_list_model_observer.h"
11 #include "ui/app_list/app_list_view_delegate.h"
12 #import "ui/app_list/cocoa/apps_collection_view_drag_manager.h"
13 #import "ui/app_list/cocoa/apps_grid_view_item.h"
14 #import "ui/app_list/cocoa/apps_pagination_model_observer.h"
15 #include "ui/base/models/list_model_observer.h"
17 namespace {
19 // OSX app list has hardcoded rows and columns for now.
20 const int kFixedRows = 4;
21 const int kFixedColumns = 4;
22 const int kItemsPerPage = kFixedRows * kFixedColumns;
24 // Padding space in pixels for fixed layout.
25 const CGFloat kGridTopPadding = 1;
26 const CGFloat kLeftRightPadding = 21;
27 const CGFloat kScrollerPadding = 16;
29 // Preferred tile size when showing in fixed layout. These should be even
30 // numbers to ensure that if they are grown 50% they remain integers.
31 const CGFloat kPreferredTileWidth = 88;
32 const CGFloat kPreferredTileHeight = 98;
34 const CGFloat kViewWidth =
35     kFixedColumns * kPreferredTileWidth + 2 * kLeftRightPadding;
36 const CGFloat kViewHeight = kFixedRows * kPreferredTileHeight;
38 const NSTimeInterval kScrollWhileDraggingDelay = 1.0;
39 NSTimeInterval g_scroll_duration = 0.18;
41 }  // namespace
43 @interface AppsGridController ()
45 - (void)scrollToPageWithTimer:(size_t)targetPage;
46 - (void)onTimer:(NSTimer*)theTimer;
48 // Cancel a currently running scroll animation.
49 - (void)cancelScrollAnimation;
51 // Index of the page with the most content currently visible.
52 - (size_t)nearestPageIndex;
54 // Bootstrap the views this class controls.
55 - (void)loadAndSetView;
57 - (void)boundsDidChange:(NSNotification*)notification;
59 // Action for buttons in the grid.
60 - (void)onItemClicked:(id)sender;
62 - (AppsGridViewItem*)itemAtPageIndex:(size_t)pageIndex
63                          indexInPage:(size_t)indexInPage;
65 // Return the button of the selected item.
66 - (NSButton*)selectedButton;
68 // The scroll view holding the grid pages.
69 - (NSScrollView*)gridScrollView;
71 - (NSView*)pagesContainerView;
73 // Create any new pages after updating |items_|.
74 - (void)updatePages:(size_t)startItemIndex;
76 - (void)updatePageContent:(size_t)pageIndex
77                resetModel:(BOOL)resetModel;
79 // Bridged methods for AppListItemListObserver.
80 - (void)listItemAdded:(size_t)index
81                  item:(app_list::AppListItem*)item;
83 - (void)listItemRemoved:(size_t)index;
85 - (void)listItemMovedFromIndex:(size_t)fromIndex
86                   toModelIndex:(size_t)toIndex;
88 // Moves the selection by |indexDelta| items.
89 - (BOOL)moveSelectionByDelta:(int)indexDelta;
91 @end
93 namespace app_list {
95 class AppsGridDelegateBridge : public AppListItemListObserver {
96  public:
97   AppsGridDelegateBridge(AppsGridController* parent) : parent_(parent) {}
99  private:
100   // Overridden from AppListItemListObserver:
101   void OnListItemAdded(size_t index, AppListItem* item) override {
102     [parent_ listItemAdded:index
103                       item:item];
104   }
105   void OnListItemRemoved(size_t index, AppListItem* item) override {
106     [parent_ listItemRemoved:index];
107   }
108   void OnListItemMoved(size_t from_index,
109                        size_t to_index,
110                        AppListItem* item) override {
111     [parent_ listItemMovedFromIndex:from_index
112                        toModelIndex:to_index];
113   }
114   void OnAppListItemHighlight(size_t index, bool highlight) override {
115     // NSCollectionView (or -[AppsGridController scrollToPage]) ensures only one
116     // item is highlighted, so clearing a highlight isn't necessary.
117     if (!highlight)
118       return;
120     [parent_ selectItemAtIndex:index];
121     [parent_ scrollToPage:index / kItemsPerPage];
122   }
124   AppsGridController* parent_;  // Weak, owns us.
126   DISALLOW_COPY_AND_ASSIGN(AppsGridDelegateBridge);
129 }  // namespace app_list
131 @interface PageContainerView : NSView;
132 @end
134 // The container view needs to flip coordinates so that it is laid out
135 // correctly whether or not there is a horizontal scrollbar.
136 @implementation PageContainerView
138 - (BOOL)isFlipped {
139   return YES;
142 @end
144 @implementation AppsGridController
146 + (void)setScrollAnimationDuration:(NSTimeInterval)duration {
147   g_scroll_duration = duration;
150 + (CGFloat)scrollerPadding {
151   return kScrollerPadding;
154 @synthesize paginationObserver = paginationObserver_;
156 - (id)init {
157   if ((self = [super init])) {
158     bridge_.reset(new app_list::AppsGridDelegateBridge(self));
159     NSSize cellSize = NSMakeSize(kPreferredTileWidth, kPreferredTileHeight);
160     dragManager_.reset(
161         [[AppsCollectionViewDragManager alloc] initWithCellSize:cellSize
162                                                            rows:kFixedRows
163                                                         columns:kFixedColumns
164                                                  gridController:self]);
165     pages_.reset([[NSMutableArray alloc] init]);
166     items_.reset([[NSMutableArray alloc] init]);
167     [self loadAndSetView];
168     [self updatePages:0];
169   }
170   return self;
173 - (void)dealloc {
174   [[NSNotificationCenter defaultCenter] removeObserver:self];
175   [super dealloc];
178 - (NSCollectionView*)collectionViewAtPageIndex:(size_t)pageIndex {
179   return [pages_ objectAtIndex:pageIndex];
182 - (size_t)pageIndexForCollectionView:(NSCollectionView*)page {
183   for (size_t pageIndex = 0; pageIndex < [pages_ count]; ++pageIndex) {
184     if (page == [self collectionViewAtPageIndex:pageIndex])
185       return pageIndex;
186   }
187   return NSNotFound;
190 - (app_list::AppListModel*)model {
191   return delegate_ ? delegate_->GetModel() : NULL;
194 - (void)setDelegate:(app_list::AppListViewDelegate*)newDelegate {
195   if (delegate_) {
196     app_list::AppListModel* oldModel = delegate_->GetModel();
197     if (oldModel)
198       oldModel->top_level_item_list()->RemoveObserver(bridge_.get());
199   }
201   // Since the old model may be getting deleted, and the AppKit objects might
202   // be sitting in an NSAutoreleasePool, ensure there are no references to
203   // the model.
204   for (size_t i = 0; i < [items_ count]; ++i)
205     [[self itemAtIndex:i] setModel:NULL];
207   [items_ removeAllObjects];
208   [self updatePages:0];
209   [self scrollToPage:0];
211   delegate_ = newDelegate;
212   if (!delegate_)
213     return;
215   app_list::AppListModel* newModel = delegate_->GetModel();
216   if (!newModel)
217     return;
219   newModel->top_level_item_list()->AddObserver(bridge_.get());
220   for (size_t i = 0; i < newModel->top_level_item_list()->item_count(); ++i) {
221     app_list::AppListItem* itemModel =
222         newModel->top_level_item_list()->item_at(i);
223     [items_ insertObject:[NSValue valueWithPointer:itemModel]
224                  atIndex:i];
225   }
226   [self updatePages:0];
229 - (size_t)visiblePage {
230   return visiblePage_;
233 - (void)activateSelection {
234   [[self selectedButton] performClick:self];
237 - (size_t)pageCount {
238   return [pages_ count];
241 - (size_t)itemCount {
242   return [items_ count];
245 - (void)scrollToPage:(size_t)pageIndex {
246   NSClipView* clipView = [[self gridScrollView] contentView];
247   NSPoint newOrigin = [clipView bounds].origin;
249   // Scrolling outside of this range is edge elasticity, which animates
250   // automatically.
251   if ((pageIndex == 0 && (newOrigin.x <= 0)) ||
252       (pageIndex + 1 == [self pageCount] &&
253           newOrigin.x >= pageIndex * kViewWidth)) {
254     return;
255   }
257   // Clear any selection on the current page (unless it has been removed).
258   if (visiblePage_ < [pages_ count]) {
259     [[self collectionViewAtPageIndex:visiblePage_]
260         setSelectionIndexes:[NSIndexSet indexSet]];
261   }
263   newOrigin.x = pageIndex * kViewWidth;
264   [NSAnimationContext beginGrouping];
265   [[NSAnimationContext currentContext] setDuration:g_scroll_duration];
266   [[clipView animator] setBoundsOrigin:newOrigin];
267   [NSAnimationContext endGrouping];
268   animatingScroll_ = YES;
269   targetScrollPage_ = pageIndex;
270   [self cancelScrollTimer];
273 - (void)maybeChangePageForPoint:(NSPoint)locationInWindow {
274   NSPoint pointInView = [[self view] convertPoint:locationInWindow
275                                          fromView:nil];
276   // Check if the point is outside the view on the left or right.
277   if (pointInView.x <= 0 || pointInView.x >= NSWidth([[self view] bounds])) {
278     size_t targetPage = visiblePage_;
279     if (pointInView.x <= 0)
280       targetPage -= targetPage != 0 ? 1 : 0;
281     else
282       targetPage += targetPage < [pages_ count] - 1 ? 1 : 0;
283     [self scrollToPageWithTimer:targetPage];
284     return;
285   }
287   if (paginationObserver_) {
288     NSInteger segment =
289         [paginationObserver_ pagerSegmentAtLocation:locationInWindow];
290     if (segment >= 0 && static_cast<size_t>(segment) != targetScrollPage_) {
291       [self scrollToPageWithTimer:segment];
292       return;
293     }
294   }
296   // Otherwise the point may have moved back into the view.
297   [self cancelScrollTimer];
300 - (void)cancelScrollTimer {
301   scheduledScrollPage_ = targetScrollPage_;
302   [scrollWhileDraggingTimer_ invalidate];
305 - (void)scrollToPageWithTimer:(size_t)targetPage {
306   if (targetPage == targetScrollPage_) {
307     [self cancelScrollTimer];
308     return;
309   }
311   if (targetPage == scheduledScrollPage_)
312     return;
314   scheduledScrollPage_ = targetPage;
315   [scrollWhileDraggingTimer_ invalidate];
316   scrollWhileDraggingTimer_.reset(
317       [[NSTimer scheduledTimerWithTimeInterval:kScrollWhileDraggingDelay
318                                         target:self
319                                       selector:@selector(onTimer:)
320                                       userInfo:nil
321                                        repeats:NO] retain]);
324 - (void)onTimer:(NSTimer*)theTimer {
325   if (scheduledScrollPage_ == targetScrollPage_)
326     return;  // Already animating scroll.
328   [self scrollToPage:scheduledScrollPage_];
331 - (void)cancelScrollAnimation {
332   NSClipView* clipView = [[self gridScrollView] contentView];
333   [NSAnimationContext beginGrouping];
334   [[NSAnimationContext currentContext] setDuration:0];
335   [[clipView animator] setBoundsOrigin:[clipView bounds].origin];
336   [NSAnimationContext endGrouping];
337   animatingScroll_ = NO;
340 - (size_t)nearestPageIndex {
341   return lround(
342       NSMinX([[[self gridScrollView] contentView] bounds]) / kViewWidth);
345 - (void)userScrolling:(BOOL)isScrolling {
346   if (isScrolling) {
347     if (animatingScroll_)
348       [self cancelScrollAnimation];
349   } else {
350     [self scrollToPage:[self nearestPageIndex]];
351   }
354 - (void)loadAndSetView {
355   base::scoped_nsobject<PageContainerView> pagesContainer(
356       [[PageContainerView alloc] initWithFrame:NSZeroRect]);
358   NSRect scrollFrame = NSMakeRect(0, kGridTopPadding, kViewWidth,
359                                   kViewHeight + kScrollerPadding);
360   base::scoped_nsobject<ScrollViewWithNoScrollbars> scrollView(
361       [[ScrollViewWithNoScrollbars alloc] initWithFrame:scrollFrame]);
362   [scrollView setBorderType:NSNoBorder];
363   [scrollView setLineScroll:kViewWidth];
364   [scrollView setPageScroll:kViewWidth];
365   [scrollView setDelegate:self];
366   [scrollView setDocumentView:pagesContainer];
367   [scrollView setDrawsBackground:NO];
369   [[NSNotificationCenter defaultCenter]
370       addObserver:self
371          selector:@selector(boundsDidChange:)
372              name:NSViewBoundsDidChangeNotification
373            object:[scrollView contentView]];
375   [self setView:scrollView];
378 - (void)boundsDidChange:(NSNotification*)notification {
379   size_t newPage = [self nearestPageIndex];
380   if (newPage == visiblePage_) {
381     [paginationObserver_ pageVisibilityChanged];
382     return;
383   }
385   visiblePage_ = newPage;
386   [paginationObserver_ selectedPageChanged:newPage];
387   [paginationObserver_ pageVisibilityChanged];
390 - (void)onItemClicked:(id)sender {
391   for (size_t i = 0; i < [items_ count]; ++i) {
392     AppsGridViewItem* gridItem = [self itemAtIndex:i];
393     if ([[gridItem button] isEqual:sender])
394       [gridItem model]->Activate(0);
395   }
398 - (AppsGridViewItem*)itemAtPageIndex:(size_t)pageIndex
399                          indexInPage:(size_t)indexInPage {
400   return base::mac::ObjCCastStrict<AppsGridViewItem>(
401       [[self collectionViewAtPageIndex:pageIndex] itemAtIndex:indexInPage]);
404 - (AppsGridViewItem*)itemAtIndex:(size_t)itemIndex {
405   const size_t pageIndex = itemIndex / kItemsPerPage;
406   return [self itemAtPageIndex:pageIndex
407                    indexInPage:itemIndex - pageIndex * kItemsPerPage];
410 - (NSUInteger)selectedItemIndex {
411   NSCollectionView* page = [self collectionViewAtPageIndex:visiblePage_];
412   NSUInteger indexOnPage = [[page selectionIndexes] firstIndex];
413   if (indexOnPage == NSNotFound)
414     return NSNotFound;
416   return indexOnPage + visiblePage_ * kItemsPerPage;
419 - (NSButton*)selectedButton {
420   NSUInteger index = [self selectedItemIndex];
421   if (index == NSNotFound)
422     return nil;
424   return [[self itemAtIndex:index] button];
427 - (NSScrollView*)gridScrollView {
428   return base::mac::ObjCCastStrict<NSScrollView>([self view]);
431 - (NSView*)pagesContainerView {
432   return [[self gridScrollView] documentView];
435 - (void)updatePages:(size_t)startItemIndex {
436   // Note there is always at least one page.
437   size_t targetPages = 1;
438   if ([items_ count] != 0)
439     targetPages = ([items_ count] - 1) / kItemsPerPage + 1;
441   const size_t currentPages = [self pageCount];
442   // First see if the number of pages have changed.
443   if (targetPages != currentPages) {
444     if (targetPages < currentPages) {
445       // Pages need to be removed.
446       [pages_ removeObjectsInRange:NSMakeRange(targetPages,
447                                                currentPages - targetPages)];
448     } else {
449       // Pages need to be added.
450       for (size_t i = currentPages; i < targetPages; ++i) {
451         NSRect pageFrame = NSMakeRect(
452             kLeftRightPadding + kViewWidth * i, 0,
453             kViewWidth, kViewHeight);
454         [pages_ addObject:[dragManager_ makePageWithFrame:pageFrame]];
455       }
456     }
458     [[self pagesContainerView] setSubviews:pages_];
459     NSSize pagesSize = NSMakeSize(kViewWidth * targetPages, kViewHeight);
460     [[self pagesContainerView] setFrameSize:pagesSize];
461     [paginationObserver_ totalPagesChanged];
462   }
464   const size_t startPage = startItemIndex / kItemsPerPage;
465   // All pages on or after |startPage| may need items added or removed.
466   for (size_t pageIndex = startPage; pageIndex < targetPages; ++pageIndex) {
467     [self updatePageContent:pageIndex
468                  resetModel:YES];
469   }
472 - (void)updatePageContent:(size_t)pageIndex
473                resetModel:(BOOL)resetModel {
474   NSCollectionView* pageView = [self collectionViewAtPageIndex:pageIndex];
475   if (resetModel) {
476     // Clear the models first, otherwise removed items could be autoreleased at
477     // an unknown point in the future, when the model owner may have gone away.
478     for (size_t i = 0; i < [[pageView content] count]; ++i) {
479       AppsGridViewItem* gridItem = base::mac::ObjCCastStrict<AppsGridViewItem>(
480           [pageView itemAtIndex:i]);
481       [gridItem setModel:NULL];
482     }
483   }
485   NSRange inPageRange = NSIntersectionRange(
486       NSMakeRange(pageIndex * kItemsPerPage, kItemsPerPage),
487       NSMakeRange(0, [items_ count]));
488   NSArray* pageContent = [items_ subarrayWithRange:inPageRange];
489   [pageView setContent:pageContent];
490   if (!resetModel)
491     return;
493   for (size_t i = 0; i < [pageContent count]; ++i) {
494     AppsGridViewItem* gridItem = base::mac::ObjCCastStrict<AppsGridViewItem>(
495         [pageView itemAtIndex:i]);
496     [gridItem setModel:static_cast<app_list::AppListItem*>(
497         [[pageContent objectAtIndex:i] pointerValue])];
498   }
501 - (void)moveItemInView:(size_t)fromIndex
502            toItemIndex:(size_t)toIndex {
503   base::scoped_nsobject<NSValue> item(
504       [[items_ objectAtIndex:fromIndex] retain]);
505   [items_ removeObjectAtIndex:fromIndex];
506   [items_ insertObject:item
507                atIndex:toIndex];
509   size_t fromPageIndex = fromIndex / kItemsPerPage;
510   size_t toPageIndex = toIndex / kItemsPerPage;
511   if (fromPageIndex == toPageIndex) {
512     [self updatePageContent:fromPageIndex
513                  resetModel:NO];  // Just reorder items.
514     return;
515   }
517   if (fromPageIndex > toPageIndex)
518     std::swap(fromPageIndex, toPageIndex);
520   for (size_t i = fromPageIndex; i <= toPageIndex; ++i) {
521     [self updatePageContent:i
522                  resetModel:YES];
523   }
526 // Compare with views implementation in AppsGridView::MoveItemInModel().
527 - (void)moveItemWithIndex:(size_t)itemIndex
528              toModelIndex:(size_t)modelIndex {
529   // Ingore no-op moves. Note that this is always the case when canceled.
530   if (itemIndex == modelIndex)
531     return;
533   app_list::AppListItemList* itemList = [self model]->top_level_item_list();
534   itemList->RemoveObserver(bridge_.get());
535   itemList->MoveItem(itemIndex, modelIndex);
536   itemList->AddObserver(bridge_.get());
539 - (AppsCollectionViewDragManager*)dragManager {
540   return dragManager_;
543 - (size_t)scheduledScrollPage {
544   return scheduledScrollPage_;
547 - (void)listItemAdded:(size_t)index
548                  item:(app_list::AppListItem*)itemModel {
549   // Cancel any drag, to ensure the model stays consistent.
550   [dragManager_ cancelDrag];
552   [items_ insertObject:[NSValue valueWithPointer:itemModel]
553               atIndex:index];
555   [self updatePages:index];
558 - (void)listItemRemoved:(size_t)index {
559   [dragManager_ cancelDrag];
561   // Clear the models explicitly to avoid surprises from autorelease.
562   [[self itemAtIndex:index] setModel:NULL];
564   [items_ removeObjectsInRange:NSMakeRange(index, 1)];
565   [self updatePages:index];
568 - (void)listItemMovedFromIndex:(size_t)fromIndex
569                   toModelIndex:(size_t)toIndex {
570   [dragManager_ cancelDrag];
571   [self moveItemInView:fromIndex
572            toItemIndex:toIndex];
575 - (CGFloat)visiblePortionOfPage:(int)page {
576   CGFloat scrollOffsetOfPage =
577       NSMinX([[[self gridScrollView] contentView] bounds]) / kViewWidth - page;
578   if (scrollOffsetOfPage <= -1.0 || scrollOffsetOfPage >= 1.0)
579     return 0.0;
581   if (scrollOffsetOfPage <= 0.0)
582     return scrollOffsetOfPage + 1.0;
584   return -1.0 + scrollOffsetOfPage;
587 - (void)onPagerClicked:(AppListPagerView*)sender {
588   int selectedSegment = [sender selectedSegment];
589   if (selectedSegment < 0)
590     return;  // No selection.
592   int pageIndex = [[sender cell] tagForSegment:selectedSegment];
593   if (pageIndex >= 0)
594     [self scrollToPage:pageIndex];
597 - (BOOL)moveSelectionByDelta:(int)indexDelta {
598   if (indexDelta == 0)
599     return NO;
601   NSUInteger oldIndex = [self selectedItemIndex];
603   // If nothing is currently selected, select the first item on the page.
604   if (oldIndex == NSNotFound) {
605     [self selectItemAtIndex:visiblePage_ * kItemsPerPage];
606     return YES;
607   }
609   // Can't select a negative index.
610   if (indexDelta < 0 && static_cast<NSUInteger>(-indexDelta) > oldIndex)
611     return NO;
613   // Can't select an index greater or equal to the number of items.
614   if (oldIndex + indexDelta >= [items_ count]) {
615     if (visiblePage_ == [pages_ count] - 1)
616       return NO;
618     // If we're not on the last page, then select the last item.
619     [self selectItemAtIndex:[items_ count] - 1];
620     return YES;
621   }
623   [self selectItemAtIndex:oldIndex + indexDelta];
624   return YES;
627 - (void)selectItemAtIndex:(NSUInteger)index {
628   if (index >= [items_ count])
629     return;
631   if (index / kItemsPerPage != visiblePage_)
632     [self scrollToPage:index / kItemsPerPage];
634   [[self itemAtIndex:index] setSelected:YES];
637 - (BOOL)handleCommandBySelector:(SEL)command {
638   if (command == @selector(insertNewline:) ||
639       command == @selector(insertLineBreak:)) {
640     [self activateSelection];
641     return YES;
642   }
644   NSUInteger oldIndex = [self selectedItemIndex];
645   // If nothing is currently selected, select the first item on the page.
646   if (oldIndex == NSNotFound) {
647     [self selectItemAtIndex:visiblePage_ * kItemsPerPage];
648     return YES;
649   }
651   if (command == @selector(moveLeft:)) {
652     return oldIndex % kFixedColumns == 0 ?
653         [self moveSelectionByDelta:-kItemsPerPage + kFixedColumns - 1] :
654         [self moveSelectionByDelta:-1];
655   }
657   if (command == @selector(moveRight:)) {
658     return oldIndex % kFixedColumns == kFixedColumns - 1 ?
659         [self moveSelectionByDelta:+kItemsPerPage - kFixedColumns + 1] :
660         [self moveSelectionByDelta:1];
661   }
663   if (command == @selector(moveUp:)) {
664     return oldIndex / kFixedColumns % kFixedRows == 0 ?
665         NO : [self moveSelectionByDelta:-kFixedColumns];
666   }
668   if (command == @selector(moveDown:)) {
669     return oldIndex / kFixedColumns % kFixedRows == kFixedRows - 1 ?
670         NO : [self moveSelectionByDelta:kFixedColumns];
671   }
673   if (command == @selector(pageUp:) ||
674       command == @selector(scrollPageUp:))
675     return [self moveSelectionByDelta:-kItemsPerPage];
677   if (command == @selector(pageDown:) ||
678       command == @selector(scrollPageDown:))
679     return [self moveSelectionByDelta:kItemsPerPage];
681   return NO;
684 @end