apps_collection_view_drag_manager.mm revision eb525c5499e34cc9c4b825d6d9e75bb07cc06ace
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_collection_view_drag_manager.h"
6
7#include "base/logging.h"
8#include "base/mac/foundation_util.h"
9#import "ui/app_list/cocoa/apps_grid_controller.h"
10#import "ui/app_list/cocoa/apps_grid_view_item.h"
11#import "ui/app_list/cocoa/item_drag_controller.h"
12
13namespace {
14
15// Distance cursor must travel in either x or y direction to start a drag.
16const CGFloat kDragThreshold = 5;
17
18}  // namespace
19
20@interface AppsCollectionViewDragManager ()
21
22// Returns the item index that |theEvent| would hit in the page at |pageIndex|
23// or NSNotFound if no item is hit.
24- (size_t)itemIndexForPage:(size_t)pageIndex
25              hitWithEvent:(NSEvent*)theEvent;
26
27- (void)initiateDrag:(NSEvent*)theEvent;
28- (void)updateDrag:(NSEvent*)theEvent;
29- (void)completeDrag;
30
31- (NSMenu*)menuForEvent:(NSEvent*)theEvent
32                 inPage:(NSCollectionView*)page;
33
34@end
35
36// An NSCollectionView that forwards mouse events to the factory they share.
37@interface GridCollectionView : NSCollectionView {
38 @private
39  AppsCollectionViewDragManager* factory_;
40}
41
42@property(assign, nonatomic) AppsCollectionViewDragManager* factory;
43
44@end
45
46@implementation AppsCollectionViewDragManager
47
48- (id)initWithCellSize:(NSSize)cellSize
49                  rows:(size_t)rows
50               columns:(size_t)columns
51        gridController:(AppsGridController*)gridController {
52  if ((self = [super init])) {
53    cellSize_ = cellSize;
54    rows_ = rows;
55    columns_ = columns;
56    gridController_ = gridController;
57  }
58  return self;
59}
60
61- (NSCollectionView*)makePageWithFrame:(NSRect)pageFrame {
62  base::scoped_nsobject<GridCollectionView> itemCollectionView(
63      [[GridCollectionView alloc] initWithFrame:pageFrame]);
64  [itemCollectionView setFactory:self];
65  [itemCollectionView setMaxNumberOfRows:rows_];
66  [itemCollectionView setMinItemSize:cellSize_];
67  [itemCollectionView setMaxItemSize:cellSize_];
68  [itemCollectionView setSelectable:YES];
69  [itemCollectionView setFocusRingType:NSFocusRingTypeNone];
70  [itemCollectionView setBackgroundColors:
71      [NSArray arrayWithObject:[NSColor clearColor]]];
72
73  base::scoped_nsobject<AppsGridViewItem> itemPrototype(
74      [[AppsGridViewItem alloc] initWithSize:cellSize_]);
75  [[itemPrototype button] setTarget:gridController_];
76  [[itemPrototype button] setAction:@selector(onItemClicked:)];
77
78  [itemCollectionView setItemPrototype:itemPrototype];
79  return itemCollectionView.autorelease();
80}
81
82- (void)onMouseDownInPage:(NSCollectionView*)page
83                withEvent:(NSEvent*)theEvent {
84  size_t pageIndex = [gridController_ pageIndexForCollectionView:page];
85  itemHitIndex_ = [self itemIndexForPage:pageIndex
86                            hitWithEvent:theEvent];
87  if (itemHitIndex_ == NSNotFound)
88    return;
89
90  mouseDownLocation_ = [theEvent locationInWindow];
91  [[[gridController_ itemAtIndex:itemHitIndex_] view] mouseDown:theEvent];
92}
93
94- (void)onMouseDragged:(NSEvent*)theEvent {
95  if (itemHitIndex_ == NSNotFound)
96    return;
97
98  if (dragging_) {
99    [self updateDrag:theEvent];
100    return;
101  }
102
103  NSPoint mouseLocation = [theEvent locationInWindow];
104  CGFloat deltaX = mouseLocation.x - mouseDownLocation_.x;
105  CGFloat deltaY = mouseLocation.y - mouseDownLocation_.y;
106  if (deltaX * deltaX + deltaY * deltaY > kDragThreshold * kDragThreshold) {
107    [self initiateDrag:theEvent];
108    return;
109  }
110
111  [[[gridController_ itemAtIndex:itemHitIndex_] view] mouseDragged:theEvent];
112}
113
114- (void)onMouseUp:(NSEvent*)theEvent {
115  if (itemHitIndex_ == NSNotFound)
116    return;
117
118  if (dragging_) {
119    [self completeDrag];
120    return;
121  }
122
123  [[[gridController_ itemAtIndex:itemHitIndex_] view] mouseUp:theEvent];
124}
125
126- (size_t)itemIndexForPage:(size_t)pageIndex
127              hitWithEvent:(NSEvent*)theEvent {
128  NSCollectionView* page =
129      [gridController_ collectionViewAtPageIndex:pageIndex];
130  NSPoint pointInView = [page convertPoint:[theEvent locationInWindow]
131                                  fromView:nil];
132
133  const size_t itemsInPage = [[page content] count];
134  for (size_t indexInPage = 0; indexInPage < itemsInPage; ++indexInPage) {
135    if ([page mouse:pointInView
136             inRect:[page frameForItemAtIndex:indexInPage]]) {
137      return indexInPage + pageIndex * rows_ * columns_;
138    }
139  }
140
141  return NSNotFound;
142}
143
144- (void)initiateDrag:(NSEvent*)theEvent {
145  DCHECK_LT(itemHitIndex_, [gridController_ itemCount]);
146  dragging_ = YES;
147
148  if (!itemDragController_) {
149    itemDragController_.reset(
150        [[ItemDragController alloc] initWithGridCellSize:cellSize_]);
151    [[[gridController_ view] superview] addSubview:[itemDragController_ view]];
152  }
153
154  [itemDragController_ initiate:[gridController_ itemAtIndex:itemHitIndex_]
155              mouseDownLocation:mouseDownLocation_
156                currentLocation:[theEvent locationInWindow]
157                      timestamp:[theEvent timestamp]];
158
159  itemDragIndex_ = itemHitIndex_;
160  [self updateDrag:theEvent];
161}
162
163- (void)updateDrag:(NSEvent*)theEvent {
164  [itemDragController_ update:[theEvent locationInWindow]
165                    timestamp:[theEvent timestamp]];
166  [gridController_ maybeChangePageForPoint:[theEvent locationInWindow]];
167
168  size_t visiblePage = [gridController_ visiblePage];
169  size_t itemIndexOver = [self itemIndexForPage:visiblePage
170                                   hitWithEvent:theEvent];
171  DCHECK_NE(0u, [gridController_ itemCount]);
172  if (itemIndexOver == NSNotFound) {
173    if (visiblePage != [gridController_ pageCount] - 1)
174      return;
175
176    // If nothing was hit, but the last page is shown, move to the end.
177    itemIndexOver = [gridController_ itemCount] - 1;
178  }
179
180  if (itemDragIndex_ == itemIndexOver)
181    return;
182
183  [gridController_ moveItemInView:itemDragIndex_
184                      toItemIndex:itemIndexOver];
185  // A new item may be created when moving between pages. Ensure it is hidden.
186  [[[gridController_ itemAtIndex:itemIndexOver] button] setHidden:YES];
187  itemDragIndex_ = itemIndexOver;
188}
189
190- (void)cancelDrag {
191  if (!dragging_)
192    return;
193
194  [gridController_ moveItemInView:itemDragIndex_
195                      toItemIndex:itemHitIndex_];
196  itemDragIndex_ = itemHitIndex_;
197  [self completeDrag];
198  itemHitIndex_ = NSNotFound;  // Ignore future mouse events for this drag.
199}
200
201- (void)completeDrag {
202  DCHECK_GE(itemDragIndex_, 0u);
203  [gridController_ cancelScrollTimer];
204  AppsGridViewItem* item = [gridController_ itemAtIndex:itemDragIndex_];
205
206  // The item could still be animating in the NSCollectionView, so ask it where
207  // it would place the item.
208  NSCollectionView* pageView = base::mac::ObjCCastStrict<NSCollectionView>(
209      [[item view] superview]);
210  size_t indexInPage = itemDragIndex_ % (rows_ * columns_);
211  NSPoint targetOrigin = [[[itemDragController_ view] superview]
212      convertPoint:[pageView frameForItemAtIndex:indexInPage].origin
213          fromView:pageView];
214
215  [itemDragController_ complete:item
216                   targetOrigin:targetOrigin];
217  [gridController_ moveItemWithIndex:itemHitIndex_
218                        toModelIndex:itemDragIndex_];
219
220  dragging_ = NO;
221}
222
223- (NSMenu*)menuForEvent:(NSEvent*)theEvent
224                 inPage:(NSCollectionView*)page {
225  size_t pageIndex = [gridController_ pageIndexForCollectionView:page];
226  size_t itemIndex = [self itemIndexForPage:pageIndex
227                               hitWithEvent:theEvent];
228  if (itemIndex == NSNotFound)
229    return nil;
230
231  return [[gridController_ itemAtIndex:itemIndex] contextMenu];
232}
233
234@end
235
236@implementation GridCollectionView
237
238@synthesize factory = factory_;
239
240- (NSMenu*)menuForEvent:(NSEvent*)theEvent {
241  return [factory_ menuForEvent:theEvent
242                         inPage:self];
243}
244
245- (void)mouseDown:(NSEvent*)theEvent {
246  [factory_ onMouseDownInPage:self
247                    withEvent:theEvent];
248}
249
250- (void)mouseDragged:(NSEvent*)theEvent {
251  [factory_ onMouseDragged:theEvent];
252}
253
254- (void)mouseUp:(NSEvent*)theEvent {
255  [factory_ onMouseUp:theEvent];
256}
257
258- (BOOL)acceptsFirstResponder {
259  return NO;
260}
261
262@end
263