Home | History | Annotate | Download | only in cocoa
      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