| // Copyright 2013 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "ui/app_list/cocoa/apps_collection_view_drag_manager.h" |
| |
| #include "base/logging.h" |
| #include "base/mac/foundation_util.h" |
| #import "ui/app_list/cocoa/apps_grid_controller.h" |
| #import "ui/app_list/cocoa/apps_grid_view_item.h" |
| #import "ui/app_list/cocoa/item_drag_controller.h" |
| |
| namespace { |
| |
| // Distance cursor must travel in either x or y direction to start a drag. |
| const CGFloat kDragThreshold = 5; |
| |
| } // namespace |
| |
| @interface AppsCollectionViewDragManager () |
| |
| // Returns the item index that |theEvent| would hit in the page at |pageIndex| |
| // or NSNotFound if no item is hit. |
| - (size_t)itemIndexForPage:(size_t)pageIndex |
| hitWithEvent:(NSEvent*)theEvent; |
| |
| - (void)initiateDrag:(NSEvent*)theEvent; |
| - (void)updateDrag:(NSEvent*)theEvent; |
| - (void)completeDrag; |
| |
| - (NSMenu*)menuForEvent:(NSEvent*)theEvent |
| inPage:(NSCollectionView*)page; |
| |
| @end |
| |
| // An NSCollectionView that forwards mouse events to the factory they share. |
| @interface GridCollectionView : NSCollectionView { |
| @private |
| AppsCollectionViewDragManager* factory_; |
| } |
| |
| @property(assign, nonatomic) AppsCollectionViewDragManager* factory; |
| |
| @end |
| |
| @implementation AppsCollectionViewDragManager |
| |
| - (id)initWithCellSize:(NSSize)cellSize |
| rows:(size_t)rows |
| columns:(size_t)columns |
| gridController:(AppsGridController*)gridController { |
| if ((self = [super init])) { |
| cellSize_ = cellSize; |
| rows_ = rows; |
| columns_ = columns; |
| gridController_ = gridController; |
| } |
| return self; |
| } |
| |
| - (NSCollectionView*)makePageWithFrame:(NSRect)pageFrame { |
| base::scoped_nsobject<GridCollectionView> itemCollectionView( |
| [[GridCollectionView alloc] initWithFrame:pageFrame]); |
| [itemCollectionView setFactory:self]; |
| [itemCollectionView setMaxNumberOfRows:rows_]; |
| [itemCollectionView setMinItemSize:cellSize_]; |
| [itemCollectionView setMaxItemSize:cellSize_]; |
| [itemCollectionView setSelectable:YES]; |
| [itemCollectionView setFocusRingType:NSFocusRingTypeNone]; |
| [itemCollectionView setBackgroundColors: |
| [NSArray arrayWithObject:[NSColor clearColor]]]; |
| [itemCollectionView setDelegate:gridController_]; |
| |
| base::scoped_nsobject<AppsGridViewItem> itemPrototype( |
| [[AppsGridViewItem alloc] initWithSize:cellSize_]); |
| [[itemPrototype button] setTarget:gridController_]; |
| [[itemPrototype button] setAction:@selector(onItemClicked:)]; |
| |
| [itemCollectionView setItemPrototype:itemPrototype]; |
| return itemCollectionView.autorelease(); |
| } |
| |
| - (void)onMouseDownInPage:(NSCollectionView*)page |
| withEvent:(NSEvent*)theEvent { |
| size_t pageIndex = [gridController_ pageIndexForCollectionView:page]; |
| itemHitIndex_ = [self itemIndexForPage:pageIndex |
| hitWithEvent:theEvent]; |
| if (itemHitIndex_ == NSNotFound) |
| return; |
| |
| mouseDownLocation_ = [theEvent locationInWindow]; |
| [[[gridController_ itemAtIndex:itemHitIndex_] view] mouseDown:theEvent]; |
| } |
| |
| - (void)onMouseDragged:(NSEvent*)theEvent { |
| if (itemHitIndex_ == NSNotFound) |
| return; |
| |
| if (dragging_) { |
| [self updateDrag:theEvent]; |
| return; |
| } |
| |
| NSPoint mouseLocation = [theEvent locationInWindow]; |
| CGFloat deltaX = mouseLocation.x - mouseDownLocation_.x; |
| CGFloat deltaY = mouseLocation.y - mouseDownLocation_.y; |
| if (deltaX * deltaX + deltaY * deltaY > kDragThreshold * kDragThreshold) { |
| [self initiateDrag:theEvent]; |
| return; |
| } |
| |
| [[[gridController_ itemAtIndex:itemHitIndex_] view] mouseDragged:theEvent]; |
| } |
| |
| - (void)onMouseUp:(NSEvent*)theEvent { |
| if (itemHitIndex_ == NSNotFound) |
| return; |
| |
| if (dragging_) { |
| [self completeDrag]; |
| return; |
| } |
| |
| [[[gridController_ itemAtIndex:itemHitIndex_] view] mouseUp:theEvent]; |
| } |
| |
| - (size_t)itemIndexForPage:(size_t)pageIndex |
| hitWithEvent:(NSEvent*)theEvent { |
| NSCollectionView* page = |
| [gridController_ collectionViewAtPageIndex:pageIndex]; |
| NSPoint pointInView = [page convertPoint:[theEvent locationInWindow] |
| fromView:nil]; |
| |
| const size_t itemsInPage = [[page content] count]; |
| for (size_t indexInPage = 0; indexInPage < itemsInPage; ++indexInPage) { |
| if ([page mouse:pointInView |
| inRect:[page frameForItemAtIndex:indexInPage]]) { |
| return indexInPage + pageIndex * rows_ * columns_; |
| } |
| } |
| |
| return NSNotFound; |
| } |
| |
| - (void)initiateDrag:(NSEvent*)theEvent { |
| DCHECK_LT(itemHitIndex_, [gridController_ itemCount]); |
| dragging_ = YES; |
| |
| if (!itemDragController_) { |
| itemDragController_.reset( |
| [[ItemDragController alloc] initWithGridCellSize:cellSize_]); |
| [[[gridController_ view] superview] addSubview:[itemDragController_ view]]; |
| } |
| |
| [itemDragController_ initiate:[gridController_ itemAtIndex:itemHitIndex_] |
| mouseDownLocation:mouseDownLocation_ |
| currentLocation:[theEvent locationInWindow] |
| timestamp:[theEvent timestamp]]; |
| |
| itemDragIndex_ = itemHitIndex_; |
| [self updateDrag:theEvent]; |
| } |
| |
| - (void)updateDrag:(NSEvent*)theEvent { |
| [itemDragController_ update:[theEvent locationInWindow] |
| timestamp:[theEvent timestamp]]; |
| [gridController_ maybeChangePageForPoint:[theEvent locationInWindow]]; |
| |
| size_t visiblePage = [gridController_ visiblePage]; |
| size_t itemIndexOver = [self itemIndexForPage:visiblePage |
| hitWithEvent:theEvent]; |
| DCHECK_NE(0u, [gridController_ itemCount]); |
| if (itemIndexOver == NSNotFound) { |
| if (visiblePage != [gridController_ pageCount] - 1) |
| return; |
| |
| // If nothing was hit, but the last page is shown, move to the end. |
| itemIndexOver = [gridController_ itemCount] - 1; |
| } |
| |
| if (itemDragIndex_ == itemIndexOver) |
| return; |
| |
| [gridController_ moveItemInView:itemDragIndex_ |
| toItemIndex:itemIndexOver]; |
| // A new item may be created when moving between pages. Ensure it is hidden. |
| [[[gridController_ itemAtIndex:itemIndexOver] button] setHidden:YES]; |
| itemDragIndex_ = itemIndexOver; |
| } |
| |
| - (void)cancelDrag { |
| if (!dragging_) |
| return; |
| |
| [gridController_ moveItemInView:itemDragIndex_ |
| toItemIndex:itemHitIndex_]; |
| itemDragIndex_ = itemHitIndex_; |
| [self completeDrag]; |
| itemHitIndex_ = NSNotFound; // Ignore future mouse events for this drag. |
| } |
| |
| - (void)completeDrag { |
| DCHECK_GE(itemDragIndex_, 0u); |
| [gridController_ cancelScrollTimer]; |
| AppsGridViewItem* item = [gridController_ itemAtIndex:itemDragIndex_]; |
| |
| // The item could still be animating in the NSCollectionView, so ask it where |
| // it would place the item. |
| NSCollectionView* pageView = base::mac::ObjCCastStrict<NSCollectionView>( |
| [[item view] superview]); |
| size_t indexInPage = itemDragIndex_ % (rows_ * columns_); |
| NSPoint targetOrigin = [[[itemDragController_ view] superview] |
| convertPoint:[pageView frameForItemAtIndex:indexInPage].origin |
| fromView:pageView]; |
| |
| [itemDragController_ complete:item |
| targetOrigin:targetOrigin]; |
| [gridController_ moveItemWithIndex:itemHitIndex_ |
| toModelIndex:itemDragIndex_]; |
| |
| dragging_ = NO; |
| } |
| |
| - (NSMenu*)menuForEvent:(NSEvent*)theEvent |
| inPage:(NSCollectionView*)page { |
| size_t pageIndex = [gridController_ pageIndexForCollectionView:page]; |
| size_t itemIndex = [self itemIndexForPage:pageIndex |
| hitWithEvent:theEvent]; |
| if (itemIndex == NSNotFound) |
| return nil; |
| |
| return [[gridController_ itemAtIndex:itemIndex] contextMenu]; |
| } |
| |
| @end |
| |
| @implementation GridCollectionView |
| |
| @synthesize factory = factory_; |
| |
| - (NSMenu*)menuForEvent:(NSEvent*)theEvent { |
| return [factory_ menuForEvent:theEvent |
| inPage:self]; |
| } |
| |
| - (void)mouseDown:(NSEvent*)theEvent { |
| [factory_ onMouseDownInPage:self |
| withEvent:theEvent]; |
| } |
| |
| - (void)mouseDragged:(NSEvent*)theEvent { |
| [factory_ onMouseDragged:theEvent]; |
| } |
| |
| - (void)mouseUp:(NSEvent*)theEvent { |
| [factory_ onMouseUp:theEvent]; |
| } |
| |
| - (BOOL)acceptsFirstResponder { |
| return NO; |
| } |
| |
| @end |