Views Omnibox: tolerate minor click-to-select-all dragging.
[chromium-blink-merge.git] / ui / app_list / cocoa / apps_grid_controller.mm
blob998b11e93e66d533fb4238b86a358ad524877dfb
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 // -[NSCollectionView frameForItemAtIndex:] misreports the frame origin of an
92 // item when the method is called during a scroll animation provided by the
93 // NSScrollView. This returns the correct value.
94 - (NSRect)trueFrameForItemAtIndex:(size_t)itemIndex;
96 @end
98 namespace app_list {
100 class AppsGridDelegateBridge : public AppListItemListObserver {
101  public:
102   AppsGridDelegateBridge(AppsGridController* parent) : parent_(parent) {}
104  private:
105   // Overridden from AppListItemListObserver:
106   virtual void OnListItemAdded(size_t index, AppListItem* item) OVERRIDE {
107     [parent_ listItemAdded:index
108                       item:item];
109   }
110   virtual void OnListItemRemoved(size_t index, AppListItem* item) OVERRIDE {
111     [parent_ listItemRemoved:index];
112   }
113   virtual void OnListItemMoved(size_t from_index,
114                                size_t to_index,
115                                AppListItem* item) OVERRIDE {
116     [parent_ listItemMovedFromIndex:from_index
117                        toModelIndex:to_index];
118   }
120   AppsGridController* parent_;  // Weak, owns us.
122   DISALLOW_COPY_AND_ASSIGN(AppsGridDelegateBridge);
125 }  // namespace app_list
127 @interface PageContainerView : NSView;
128 @end
130 // The container view needs to flip coordinates so that it is laid out
131 // correctly whether or not there is a horizontal scrollbar.
132 @implementation PageContainerView
134 - (BOOL)isFlipped {
135   return YES;
138 @end
140 @implementation AppsGridController
142 + (void)setScrollAnimationDuration:(NSTimeInterval)duration {
143   g_scroll_duration = duration;
146 + (CGFloat)scrollerPadding {
147   return kScrollerPadding;
150 @synthesize paginationObserver = paginationObserver_;
152 - (id)init {
153   if ((self = [super init])) {
154     bridge_.reset(new app_list::AppsGridDelegateBridge(self));
155     NSSize cellSize = NSMakeSize(kPreferredTileWidth, kPreferredTileHeight);
156     dragManager_.reset(
157         [[AppsCollectionViewDragManager alloc] initWithCellSize:cellSize
158                                                            rows:kFixedRows
159                                                         columns:kFixedColumns
160                                                  gridController:self]);
161     pages_.reset([[NSMutableArray alloc] init]);
162     items_.reset([[NSMutableArray alloc] init]);
163     [self loadAndSetView];
164     [self updatePages:0];
165   }
166   return self;
169 - (void)dealloc {
170   [[NSNotificationCenter defaultCenter] removeObserver:self];
171   [super dealloc];
174 - (NSCollectionView*)collectionViewAtPageIndex:(size_t)pageIndex {
175   return [pages_ objectAtIndex:pageIndex];
178 - (size_t)pageIndexForCollectionView:(NSCollectionView*)page {
179   for (size_t pageIndex = 0; pageIndex < [pages_ count]; ++pageIndex) {
180     if (page == [self collectionViewAtPageIndex:pageIndex])
181       return pageIndex;
182   }
183   return NSNotFound;
186 - (app_list::AppListModel*)model {
187   return delegate_ ? delegate_->GetModel() : NULL;
190 - (void)setDelegate:(app_list::AppListViewDelegate*)newDelegate {
191   if (delegate_) {
192     app_list::AppListModel* oldModel = delegate_->GetModel();
193     if (oldModel)
194       oldModel->item_list()->RemoveObserver(bridge_.get());
195   }
197   // Since the old model may be getting deleted, and the AppKit objects might
198   // be sitting in an NSAutoreleasePool, ensure there are no references to
199   // the model.
200   for (size_t i = 0; i < [items_ count]; ++i)
201     [[self itemAtIndex:i] setModel:NULL];
203   [items_ removeAllObjects];
204   [self updatePages:0];
205   [self scrollToPage:0];
207   delegate_ = newDelegate;
208   if (!delegate_)
209     return;
211   app_list::AppListModel* newModel = delegate_->GetModel();
212   if (!newModel)
213     return;
215   newModel->item_list()->AddObserver(bridge_.get());
216   for (size_t i = 0; i < newModel->item_list()->item_count(); ++i) {
217     app_list::AppListItem* itemModel = newModel->item_list()->item_at(i);
218     [items_ insertObject:[NSValue valueWithPointer:itemModel]
219                  atIndex:i];
220   }
221   [self updatePages:0];
224 - (size_t)visiblePage {
225   return visiblePage_;
228 - (void)activateSelection {
229   [[self selectedButton] performClick:self];
232 - (size_t)pageCount {
233   return [pages_ count];
236 - (size_t)itemCount {
237   return [items_ count];
240 - (void)scrollToPage:(size_t)pageIndex {
241   NSClipView* clipView = [[self gridScrollView] contentView];
242   NSPoint newOrigin = [clipView bounds].origin;
244   // Scrolling outside of this range is edge elasticity, which animates
245   // automatically.
246   if ((pageIndex == 0 && (newOrigin.x <= 0)) ||
247       (pageIndex + 1 == [self pageCount] &&
248           newOrigin.x >= pageIndex * kViewWidth)) {
249     return;
250   }
252   // Clear any selection on the current page (unless it has been removed).
253   if (visiblePage_ < [pages_ count]) {
254     [[self collectionViewAtPageIndex:visiblePage_]
255         setSelectionIndexes:[NSIndexSet indexSet]];
256   }
258   newOrigin.x = pageIndex * kViewWidth;
259   [NSAnimationContext beginGrouping];
260   [[NSAnimationContext currentContext] setDuration:g_scroll_duration];
261   [[clipView animator] setBoundsOrigin:newOrigin];
262   [NSAnimationContext endGrouping];
263   animatingScroll_ = YES;
264   targetScrollPage_ = pageIndex;
265   [self cancelScrollTimer];
268 - (void)maybeChangePageForPoint:(NSPoint)locationInWindow {
269   NSPoint pointInView = [[self view] convertPoint:locationInWindow
270                                          fromView:nil];
271   // Check if the point is outside the view on the left or right.
272   if (pointInView.x <= 0 || pointInView.x >= NSWidth([[self view] bounds])) {
273     size_t targetPage = visiblePage_;
274     if (pointInView.x <= 0)
275       targetPage -= targetPage != 0 ? 1 : 0;
276     else
277       targetPage += targetPage < [pages_ count] - 1 ? 1 : 0;
278     [self scrollToPageWithTimer:targetPage];
279     return;
280   }
282   if (paginationObserver_) {
283     NSInteger segment =
284         [paginationObserver_ pagerSegmentAtLocation:locationInWindow];
285     if (segment >= 0 && static_cast<size_t>(segment) != targetScrollPage_) {
286       [self scrollToPageWithTimer:segment];
287       return;
288     }
289   }
291   // Otherwise the point may have moved back into the view.
292   [self cancelScrollTimer];
295 - (void)cancelScrollTimer {
296   scheduledScrollPage_ = targetScrollPage_;
297   [scrollWhileDraggingTimer_ invalidate];
300 - (void)scrollToPageWithTimer:(size_t)targetPage {
301   if (targetPage == targetScrollPage_) {
302     [self cancelScrollTimer];
303     return;
304   }
306   if (targetPage == scheduledScrollPage_)
307     return;
309   scheduledScrollPage_ = targetPage;
310   [scrollWhileDraggingTimer_ invalidate];
311   scrollWhileDraggingTimer_.reset(
312       [[NSTimer scheduledTimerWithTimeInterval:kScrollWhileDraggingDelay
313                                         target:self
314                                       selector:@selector(onTimer:)
315                                       userInfo:nil
316                                        repeats:NO] retain]);
319 - (void)onTimer:(NSTimer*)theTimer {
320   if (scheduledScrollPage_ == targetScrollPage_)
321     return;  // Already animating scroll.
323   [self scrollToPage:scheduledScrollPage_];
326 - (void)cancelScrollAnimation {
327   NSClipView* clipView = [[self gridScrollView] contentView];
328   [NSAnimationContext beginGrouping];
329   [[NSAnimationContext currentContext] setDuration:0];
330   [[clipView animator] setBoundsOrigin:[clipView bounds].origin];
331   [NSAnimationContext endGrouping];
332   animatingScroll_ = NO;
335 - (size_t)nearestPageIndex {
336   return lround(
337       NSMinX([[[self gridScrollView] contentView] bounds]) / kViewWidth);
340 - (void)userScrolling:(BOOL)isScrolling {
341   if (isScrolling) {
342     if (animatingScroll_)
343       [self cancelScrollAnimation];
344   } else {
345     [self scrollToPage:[self nearestPageIndex]];
346   }
349 - (void)loadAndSetView {
350   base::scoped_nsobject<PageContainerView> pagesContainer(
351       [[PageContainerView alloc] initWithFrame:NSZeroRect]);
353   NSRect scrollFrame = NSMakeRect(0, kGridTopPadding, kViewWidth,
354                                   kViewHeight + kScrollerPadding);
355   base::scoped_nsobject<ScrollViewWithNoScrollbars> scrollView(
356       [[ScrollViewWithNoScrollbars alloc] initWithFrame:scrollFrame]);
357   [scrollView setBorderType:NSNoBorder];
358   [scrollView setLineScroll:kViewWidth];
359   [scrollView setPageScroll:kViewWidth];
360   [scrollView setDelegate:self];
361   [scrollView setDocumentView:pagesContainer];
362   [scrollView setDrawsBackground:NO];
364   [[NSNotificationCenter defaultCenter]
365       addObserver:self
366          selector:@selector(boundsDidChange:)
367              name:NSViewBoundsDidChangeNotification
368            object:[scrollView contentView]];
370   [self setView:scrollView];
373 - (void)boundsDidChange:(NSNotification*)notification {
374   size_t newPage = [self nearestPageIndex];
375   if (newPage == visiblePage_) {
376     [paginationObserver_ pageVisibilityChanged];
377     return;
378   }
380   visiblePage_ = newPage;
381   [paginationObserver_ selectedPageChanged:newPage];
382   [paginationObserver_ pageVisibilityChanged];
385 - (void)onItemClicked:(id)sender {
386   for (size_t i = 0; i < [items_ count]; ++i) {
387     AppsGridViewItem* gridItem = [self itemAtIndex:i];
388     if ([[gridItem button] isEqual:sender])
389       [gridItem model]->Activate(0);
390   }
393 - (AppsGridViewItem*)itemAtPageIndex:(size_t)pageIndex
394                          indexInPage:(size_t)indexInPage {
395   return base::mac::ObjCCastStrict<AppsGridViewItem>(
396       [[self collectionViewAtPageIndex:pageIndex] itemAtIndex:indexInPage]);
399 - (AppsGridViewItem*)itemAtIndex:(size_t)itemIndex {
400   const size_t pageIndex = itemIndex / kItemsPerPage;
401   return [self itemAtPageIndex:pageIndex
402                    indexInPage:itemIndex - pageIndex * kItemsPerPage];
405 - (NSUInteger)selectedItemIndex {
406   NSCollectionView* page = [self collectionViewAtPageIndex:visiblePage_];
407   NSUInteger indexOnPage = [[page selectionIndexes] firstIndex];
408   if (indexOnPage == NSNotFound)
409     return NSNotFound;
411   return indexOnPage + visiblePage_ * kItemsPerPage;
414 - (NSButton*)selectedButton {
415   NSUInteger index = [self selectedItemIndex];
416   if (index == NSNotFound)
417     return nil;
419   return [[self itemAtIndex:index] button];
422 - (NSScrollView*)gridScrollView {
423   return base::mac::ObjCCastStrict<NSScrollView>([self view]);
426 - (NSView*)pagesContainerView {
427   return [[self gridScrollView] documentView];
430 - (void)updatePages:(size_t)startItemIndex {
431   // Note there is always at least one page.
432   size_t targetPages = 1;
433   if ([items_ count] != 0)
434     targetPages = ([items_ count] - 1) / kItemsPerPage + 1;
436   const size_t currentPages = [self pageCount];
437   // First see if the number of pages have changed.
438   if (targetPages != currentPages) {
439     if (targetPages < currentPages) {
440       // Pages need to be removed.
441       [pages_ removeObjectsInRange:NSMakeRange(targetPages,
442                                                currentPages - targetPages)];
443     } else {
444       // Pages need to be added.
445       for (size_t i = currentPages; i < targetPages; ++i) {
446         NSRect pageFrame = NSMakeRect(
447             kLeftRightPadding + kViewWidth * i, 0,
448             kViewWidth, kViewHeight);
449         [pages_ addObject:[dragManager_ makePageWithFrame:pageFrame]];
450       }
451     }
453     [[self pagesContainerView] setSubviews:pages_];
454     NSSize pagesSize = NSMakeSize(kViewWidth * targetPages, kViewHeight);
455     [[self pagesContainerView] setFrameSize:pagesSize];
456     [paginationObserver_ totalPagesChanged];
457   }
459   const size_t startPage = startItemIndex / kItemsPerPage;
460   // All pages on or after |startPage| may need items added or removed.
461   for (size_t pageIndex = startPage; pageIndex < targetPages; ++pageIndex) {
462     [self updatePageContent:pageIndex
463                  resetModel:YES];
464   }
467 - (void)updatePageContent:(size_t)pageIndex
468                resetModel:(BOOL)resetModel {
469   NSCollectionView* pageView = [self collectionViewAtPageIndex:pageIndex];
470   if (resetModel) {
471     // Clear the models first, otherwise removed items could be autoreleased at
472     // an unknown point in the future, when the model owner may have gone away.
473     for (size_t i = 0; i < [[pageView content] count]; ++i) {
474       AppsGridViewItem* gridItem = base::mac::ObjCCastStrict<AppsGridViewItem>(
475           [pageView itemAtIndex:i]);
476       [gridItem setModel:NULL];
477     }
478   }
480   NSRange inPageRange = NSIntersectionRange(
481       NSMakeRange(pageIndex * kItemsPerPage, kItemsPerPage),
482       NSMakeRange(0, [items_ count]));
483   NSArray* pageContent = [items_ subarrayWithRange:inPageRange];
484   [pageView setContent:pageContent];
485   if (!resetModel)
486     return;
488   for (size_t i = 0; i < [pageContent count]; ++i) {
489     AppsGridViewItem* gridItem = base::mac::ObjCCastStrict<AppsGridViewItem>(
490         [pageView itemAtIndex:i]);
491     [gridItem setModel:static_cast<app_list::AppListItem*>(
492         [[pageContent objectAtIndex:i] pointerValue])];
493     [gridItem setInitialFrameRect:[self trueFrameForItemAtIndex:i]];
494   }
497 - (void)moveItemInView:(size_t)fromIndex
498            toItemIndex:(size_t)toIndex {
499   base::scoped_nsobject<NSValue> item(
500       [[items_ objectAtIndex:fromIndex] retain]);
501   [items_ removeObjectAtIndex:fromIndex];
502   [items_ insertObject:item
503                atIndex:toIndex];
505   size_t fromPageIndex = fromIndex / kItemsPerPage;
506   size_t toPageIndex = toIndex / kItemsPerPage;
507   if (fromPageIndex == toPageIndex) {
508     [self updatePageContent:fromPageIndex
509                  resetModel:NO];  // Just reorder items.
510     return;
511   }
513   if (fromPageIndex > toPageIndex)
514     std::swap(fromPageIndex, toPageIndex);
516   for (size_t i = fromPageIndex; i <= toPageIndex; ++i) {
517     [self updatePageContent:i
518                  resetModel:YES];
519   }
522 // Compare with views implementation in AppsGridView::MoveItemInModel().
523 - (void)moveItemWithIndex:(size_t)itemIndex
524              toModelIndex:(size_t)modelIndex {
525   // Ingore no-op moves. Note that this is always the case when canceled.
526   if (itemIndex == modelIndex)
527     return;
529   app_list::AppListItemList* itemList = [self model]->item_list();
530   itemList->RemoveObserver(bridge_.get());
531   itemList->MoveItem(itemIndex, modelIndex);
532   itemList->AddObserver(bridge_.get());
535 - (AppsCollectionViewDragManager*)dragManager {
536   return dragManager_;
539 - (size_t)scheduledScrollPage {
540   return scheduledScrollPage_;
543 - (void)listItemAdded:(size_t)index
544                  item:(app_list::AppListItem*)itemModel {
545   // Cancel any drag, to ensure the model stays consistent.
546   [dragManager_ cancelDrag];
548   [items_ insertObject:[NSValue valueWithPointer:itemModel]
549               atIndex:index];
551   [self updatePages:index];
554 - (void)listItemRemoved:(size_t)index {
555   [dragManager_ cancelDrag];
557   // Clear the models explicitly to avoid surprises from autorelease.
558   [[self itemAtIndex:index] setModel:NULL];
560   [items_ removeObjectsInRange:NSMakeRange(index, 1)];
561   [self updatePages:index];
564 - (void)listItemMovedFromIndex:(size_t)fromIndex
565                   toModelIndex:(size_t)toIndex {
566   [dragManager_ cancelDrag];
567   [self moveItemInView:fromIndex
568            toItemIndex:toIndex];
571 - (CGFloat)visiblePortionOfPage:(int)page {
572   CGFloat scrollOffsetOfPage =
573       NSMinX([[[self gridScrollView] contentView] bounds]) / kViewWidth - page;
574   if (scrollOffsetOfPage <= -1.0 || scrollOffsetOfPage >= 1.0)
575     return 0.0;
577   if (scrollOffsetOfPage <= 0.0)
578     return scrollOffsetOfPage + 1.0;
580   return -1.0 + scrollOffsetOfPage;
583 - (void)onPagerClicked:(AppListPagerView*)sender {
584   int selectedSegment = [sender selectedSegment];
585   if (selectedSegment < 0)
586     return;  // No selection.
588   int pageIndex = [[sender cell] tagForSegment:selectedSegment];
589   if (pageIndex >= 0)
590     [self scrollToPage:pageIndex];
593 - (BOOL)moveSelectionByDelta:(int)indexDelta {
594   if (indexDelta == 0)
595     return NO;
597   NSUInteger oldIndex = [self selectedItemIndex];
599   // If nothing is currently selected, select the first item on the page.
600   if (oldIndex == NSNotFound) {
601     [self selectItemAtIndex:visiblePage_ * kItemsPerPage];
602     return YES;
603   }
605   // Can't select a negative index.
606   if (indexDelta < 0 && static_cast<NSUInteger>(-indexDelta) > oldIndex)
607     return NO;
609   // Can't select an index greater or equal to the number of items.
610   if (oldIndex + indexDelta >= [items_ count]) {
611     if (visiblePage_ == [pages_ count] - 1)
612       return NO;
614     // If we're not on the last page, then select the last item.
615     [self selectItemAtIndex:[items_ count] - 1];
616     return YES;
617   }
619   [self selectItemAtIndex:oldIndex + indexDelta];
620   return YES;
623 - (NSRect)trueFrameForItemAtIndex:(size_t)itemIndex {
624   size_t column = itemIndex % kFixedColumns;
625   size_t row = itemIndex % kItemsPerPage / kFixedColumns;
626   return NSMakeRect(column * kPreferredTileWidth,
627                     row * kPreferredTileHeight,
628                     kPreferredTileWidth,
629                     kPreferredTileHeight);
632 - (void)selectItemAtIndex:(NSUInteger)index {
633   if (index >= [items_ count])
634     return;
636   if (index / kItemsPerPage != visiblePage_)
637     [self scrollToPage:index / kItemsPerPage];
639   [[self itemAtIndex:index] setSelected:YES];
642 - (BOOL)handleCommandBySelector:(SEL)command {
643   if (command == @selector(insertNewline:) ||
644       command == @selector(insertLineBreak:)) {
645     [self activateSelection];
646     return YES;
647   }
649   NSUInteger oldIndex = [self selectedItemIndex];
650   // If nothing is currently selected, select the first item on the page.
651   if (oldIndex == NSNotFound) {
652     [self selectItemAtIndex:visiblePage_ * kItemsPerPage];
653     return YES;
654   }
656   if (command == @selector(moveLeft:)) {
657     return oldIndex % kFixedColumns == 0 ?
658         [self moveSelectionByDelta:-kItemsPerPage + kFixedColumns - 1] :
659         [self moveSelectionByDelta:-1];
660   }
662   if (command == @selector(moveRight:)) {
663     return oldIndex % kFixedColumns == kFixedColumns - 1 ?
664         [self moveSelectionByDelta:+kItemsPerPage - kFixedColumns + 1] :
665         [self moveSelectionByDelta:1];
666   }
668   if (command == @selector(moveUp:)) {
669     return oldIndex / kFixedColumns % kFixedRows == 0 ?
670         NO : [self moveSelectionByDelta:-kFixedColumns];
671   }
673   if (command == @selector(moveDown:)) {
674     return oldIndex / kFixedColumns % kFixedRows == kFixedRows - 1 ?
675         NO : [self moveSelectionByDelta:kFixedColumns];
676   }
678   if (command == @selector(pageUp:) ||
679       command == @selector(scrollPageUp:))
680     return [self moveSelectionByDelta:-kItemsPerPage];
682   if (command == @selector(pageDown:) ||
683       command == @selector(scrollPageDown:))
684     return [self moveSelectionByDelta:kItemsPerPage];
686   return NO;
689 @end