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 13 namespace { 14 15 // Distance cursor must travel in either x or y direction to start a drag. 16 const 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 [itemCollectionView setDelegate:gridController_]; 73 74 base::scoped_nsobject<AppsGridViewItem> itemPrototype( 75 [[AppsGridViewItem alloc] initWithSize:cellSize_]); 76 [[itemPrototype button] setTarget:gridController_]; 77 [[itemPrototype button] setAction:@selector(onItemClicked:)]; 78 79 [itemCollectionView setItemPrototype:itemPrototype]; 80 return itemCollectionView.autorelease(); 81 } 82 83 - (void)onMouseDownInPage:(NSCollectionView*)page 84 withEvent:(NSEvent*)theEvent { 85 size_t pageIndex = [gridController_ pageIndexForCollectionView:page]; 86 itemHitIndex_ = [self itemIndexForPage:pageIndex 87 hitWithEvent:theEvent]; 88 if (itemHitIndex_ == NSNotFound) 89 return; 90 91 mouseDownLocation_ = [theEvent locationInWindow]; 92 [[[gridController_ itemAtIndex:itemHitIndex_] view] mouseDown:theEvent]; 93 } 94 95 - (void)onMouseDragged:(NSEvent*)theEvent { 96 if (itemHitIndex_ == NSNotFound) 97 return; 98 99 if (dragging_) { 100 [self updateDrag:theEvent]; 101 return; 102 } 103 104 NSPoint mouseLocation = [theEvent locationInWindow]; 105 CGFloat deltaX = mouseLocation.x - mouseDownLocation_.x; 106 CGFloat deltaY = mouseLocation.y - mouseDownLocation_.y; 107 if (deltaX * deltaX + deltaY * deltaY > kDragThreshold * kDragThreshold) { 108 [self initiateDrag:theEvent]; 109 return; 110 } 111 112 [[[gridController_ itemAtIndex:itemHitIndex_] view] mouseDragged:theEvent]; 113 } 114 115 - (void)onMouseUp:(NSEvent*)theEvent { 116 if (itemHitIndex_ == NSNotFound) 117 return; 118 119 if (dragging_) { 120 [self completeDrag]; 121 return; 122 } 123 124 [[[gridController_ itemAtIndex:itemHitIndex_] view] mouseUp:theEvent]; 125 } 126 127 - (size_t)itemIndexForPage:(size_t)pageIndex 128 hitWithEvent:(NSEvent*)theEvent { 129 NSCollectionView* page = 130 [gridController_ collectionViewAtPageIndex:pageIndex]; 131 NSPoint pointInView = [page convertPoint:[theEvent locationInWindow] 132 fromView:nil]; 133 134 const size_t itemsInPage = [[page content] count]; 135 for (size_t indexInPage = 0; indexInPage < itemsInPage; ++indexInPage) { 136 if ([page mouse:pointInView 137 inRect:[page frameForItemAtIndex:indexInPage]]) { 138 return indexInPage + pageIndex * rows_ * columns_; 139 } 140 } 141 142 return NSNotFound; 143 } 144 145 - (void)initiateDrag:(NSEvent*)theEvent { 146 DCHECK_LT(itemHitIndex_, [gridController_ itemCount]); 147 dragging_ = YES; 148 149 if (!itemDragController_) { 150 itemDragController_.reset( 151 [[ItemDragController alloc] initWithGridCellSize:cellSize_]); 152 [[[gridController_ view] superview] addSubview:[itemDragController_ view]]; 153 } 154 155 [itemDragController_ initiate:[gridController_ itemAtIndex:itemHitIndex_] 156 mouseDownLocation:mouseDownLocation_ 157 currentLocation:[theEvent locationInWindow] 158 timestamp:[theEvent timestamp]]; 159 160 itemDragIndex_ = itemHitIndex_; 161 [self updateDrag:theEvent]; 162 } 163 164 - (void)updateDrag:(NSEvent*)theEvent { 165 [itemDragController_ update:[theEvent locationInWindow] 166 timestamp:[theEvent timestamp]]; 167 [gridController_ maybeChangePageForPoint:[theEvent locationInWindow]]; 168 169 size_t visiblePage = [gridController_ visiblePage]; 170 size_t itemIndexOver = [self itemIndexForPage:visiblePage 171 hitWithEvent:theEvent]; 172 DCHECK_NE(0u, [gridController_ itemCount]); 173 if (itemIndexOver == NSNotFound) { 174 if (visiblePage != [gridController_ pageCount] - 1) 175 return; 176 177 // If nothing was hit, but the last page is shown, move to the end. 178 itemIndexOver = [gridController_ itemCount] - 1; 179 } 180 181 if (itemDragIndex_ == itemIndexOver) 182 return; 183 184 [gridController_ moveItemInView:itemDragIndex_ 185 toItemIndex:itemIndexOver]; 186 // A new item may be created when moving between pages. Ensure it is hidden. 187 [[[gridController_ itemAtIndex:itemIndexOver] button] setHidden:YES]; 188 itemDragIndex_ = itemIndexOver; 189 } 190 191 - (void)cancelDrag { 192 if (!dragging_) 193 return; 194 195 [gridController_ moveItemInView:itemDragIndex_ 196 toItemIndex:itemHitIndex_]; 197 itemDragIndex_ = itemHitIndex_; 198 [self completeDrag]; 199 itemHitIndex_ = NSNotFound; // Ignore future mouse events for this drag. 200 } 201 202 - (void)completeDrag { 203 DCHECK_GE(itemDragIndex_, 0u); 204 [gridController_ cancelScrollTimer]; 205 AppsGridViewItem* item = [gridController_ itemAtIndex:itemDragIndex_]; 206 207 // The item could still be animating in the NSCollectionView, so ask it where 208 // it would place the item. 209 NSCollectionView* pageView = base::mac::ObjCCastStrict<NSCollectionView>( 210 [[item view] superview]); 211 size_t indexInPage = itemDragIndex_ % (rows_ * columns_); 212 NSPoint targetOrigin = [[[itemDragController_ view] superview] 213 convertPoint:[pageView frameForItemAtIndex:indexInPage].origin 214 fromView:pageView]; 215 216 [itemDragController_ complete:item 217 targetOrigin:targetOrigin]; 218 [gridController_ moveItemWithIndex:itemHitIndex_ 219 toModelIndex:itemDragIndex_]; 220 221 dragging_ = NO; 222 } 223 224 - (NSMenu*)menuForEvent:(NSEvent*)theEvent 225 inPage:(NSCollectionView*)page { 226 size_t pageIndex = [gridController_ pageIndexForCollectionView:page]; 227 size_t itemIndex = [self itemIndexForPage:pageIndex 228 hitWithEvent:theEvent]; 229 if (itemIndex == NSNotFound) 230 return nil; 231 232 return [[gridController_ itemAtIndex:itemIndex] contextMenu]; 233 } 234 235 @end 236 237 @implementation GridCollectionView 238 239 @synthesize factory = factory_; 240 241 - (NSMenu*)menuForEvent:(NSEvent*)theEvent { 242 return [factory_ menuForEvent:theEvent 243 inPage:self]; 244 } 245 246 - (void)mouseDown:(NSEvent*)theEvent { 247 [factory_ onMouseDownInPage:self 248 withEvent:theEvent]; 249 } 250 251 - (void)mouseDragged:(NSEvent*)theEvent { 252 [factory_ onMouseDragged:theEvent]; 253 } 254 255 - (void)mouseUp:(NSEvent*)theEvent { 256 [factory_ onMouseUp:theEvent]; 257 } 258 259 - (BOOL)acceptsFirstResponder { 260 return NO; 261 } 262 263 @end 264