1/** <title>NSCollectionView</title>
2
3   Copyright (C) 2013 Free Software Foundation, Inc.
4
5   Author: Doug Simons (doug.simons@testplant.com)
6           Frank LeGrand (frank.legrand@testplant.com)
7   Date: February 2013
8
9   This file is part of the GNUstep GUI Library.
10
11   This library is free software; you can redistribute it and/or
12   modify it under the terms of the GNU Lesser General Public
13   License as published by the Free Software Foundation; either
14   version 2 of the License, or (at your option) any later version.
15
16   This library is distributed in the hope that it will be useful,
17   but WITHOUT ANY WARRANTY; without even the implied warranty of
18   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
19   Lesser General Public License for more details.
20
21   You should have received a copy of the GNU Lesser General Public
22   License along with this library; see the file COPYING.LIB.
23   If not, see <http://www.gnu.org/licenses/> or write to the
24   Free Software Foundation, 51 Franklin Street, Fifth Floor,
25   Boston, MA 02110-1301, USA.
26*/
27
28#import "Foundation/NSKeyedArchiver.h"
29#import <Foundation/NSGeometry.h>
30#import <Foundation/NSIndexSet.h>
31#import <Foundation/NSKeyedArchiver.h>
32
33#import "AppKit/NSApplication.h"
34#import "AppKit/NSClipView.h"
35#import "AppKit/NSCollectionView.h"
36#import "AppKit/NSCollectionViewItem.h"
37#import "AppKit/NSEvent.h"
38#import "AppKit/NSGraphics.h"
39#import "AppKit/NSImage.h"
40#import "AppKit/NSKeyValueBinding.h"
41#import "AppKit/NSPasteboard.h"
42#import "AppKit/NSWindow.h"
43
44#include <math.h>
45
46static NSString* NSCollectionViewMinItemSizeKey              = @"NSMinGridSize";
47static NSString* NSCollectionViewMaxItemSizeKey              = @"NSMaxGridSize";
48//static NSString* NSCollectionViewVerticalMarginKey           = @"NSCollectionViewVerticalMarginKey";
49static NSString* NSCollectionViewMaxNumberOfRowsKey          = @"NSMaxNumberOfGridRows";
50static NSString* NSCollectionViewMaxNumberOfColumnsKey       = @"NSMaxNumberOfGridColumns";
51static NSString* NSCollectionViewSelectableKey               = @"NSSelectable";
52static NSString* NSCollectionViewAllowsMultipleSelectionKey  = @"NSAllowsMultipleSelection";
53static NSString* NSCollectionViewBackgroundColorsKey         = @"NSBackgroundColors";
54
55/*
56 * Class variables
57 */
58static NSString *placeholderItem = nil;
59
60@interface NSCollectionView (CollectionViewInternalPrivate)
61
62- (void) _initDefaults;
63- (void) _resetItemSize;
64- (void) _removeItemsViews;
65- (NSInteger) _indexAtPoint: (NSPoint)point;
66
67- (NSRect) _frameForRowOfItemAtIndex: (NSUInteger)theIndex;
68- (NSRect) _frameForRowsAroundItemAtIndex: (NSUInteger)theIndex;
69
70- (void) _modifySelectionWithNewIndex: (NSUInteger)anIndex
71                            direction: (int)aDirection
72                               expand: (BOOL)shouldExpand;
73
74- (void) _moveDownAndExpandSelection: (BOOL)shouldExpand;
75- (void) _moveUpAndExpandSelection: (BOOL)shouldExpand;
76- (void) _moveLeftAndExpandSelection: (BOOL)shouldExpand;
77- (void) _moveRightAndExpandSelection: (BOOL)shouldExpand;
78
79- (BOOL) _writeItemsAtIndexes: (NSIndexSet *)indexes
80                 toPasteboard: (NSPasteboard *)pasteboard;
81
82- (BOOL) _startDragOperationWithEvent: (NSEvent*)event
83                         clickedIndex: (NSUInteger)index;
84
85- (void) _selectWithEvent: (NSEvent *)theEvent
86                    index: (NSUInteger)index;
87
88@end
89
90
91@implementation NSCollectionView
92
93//
94// Class methods
95//
96+ (void) initialize
97{
98  if (self == [NSCollectionView class])
99    {
100      placeholderItem = @"Placeholder";
101      [self exposeBinding: NSContentBinding];
102    }
103}
104
105- (id) initWithFrame: (NSRect)frame
106{
107  if ((self = [super initWithFrame:frame]))
108    {
109      [self _initDefaults];
110    }
111  return self;
112}
113
114-(void) _initDefaults
115{
116//  _draggingSourceOperationMaskForLocal = NSDragOperationCopy | NSDragOperationLink | NSDragOperationGeneric | NSDragOperationPrivate;
117  _draggingSourceOperationMaskForLocal = NSDragOperationGeneric | NSDragOperationMove | NSDragOperationCopy;
118  _draggingSourceOperationMaskForRemote = NSDragOperationGeneric | NSDragOperationMove | NSDragOperationCopy;
119  [self _resetItemSize];
120  _content = [[NSArray alloc] init];
121  _items = [[NSMutableArray alloc] init];
122  _selectionIndexes = [[NSIndexSet alloc] init];
123  _draggingOnIndex = NSNotFound;
124}
125
126- (void) _resetItemSize
127{
128  if (itemPrototype && ([itemPrototype view] != nil))
129    {
130      _itemSize = [[itemPrototype view] frame].size;
131      _minItemSize = NSMakeSize (_itemSize.width, _itemSize.height);
132      _maxItemSize = NSMakeSize (_itemSize.width, _itemSize.height);
133    }
134  else
135    {
136      // FIXME: This is just arbitrary.
137      // What are we suppose to do when we don't have a prototype?
138      _itemSize = NSMakeSize(120.0, 100.0);
139      _minItemSize = NSMakeSize(120.0, 100.0);
140      _maxItemSize = NSMakeSize(120.0, 100.0);
141    }
142}
143
144- (void) drawRect: (NSRect)dirtyRect
145{
146  // TODO: Implement "use Alternating Colors"
147  if (_backgroundColors && [_backgroundColors count] > 0)
148    {
149      NSColor *bgColor = [_backgroundColors objectAtIndex: 0];
150      [bgColor set];
151      NSRectFill(dirtyRect);
152    }
153
154  NSPoint origin = dirtyRect.origin;
155  NSSize size = dirtyRect.size;
156  NSPoint oppositeOrigin = NSMakePoint (origin.x + size.width, origin.y + size.height);
157
158  NSInteger firstIndexInRect = MAX(0, [self _indexAtPoint: origin]);
159  // I had to extract these values from the macro to get it
160  // working correctly.
161  NSInteger index = [self _indexAtPoint: oppositeOrigin];
162  NSInteger last = [_items count] - 1;
163  NSInteger lastIndexInRect = MIN(last, index);
164
165  for (index = firstIndexInRect; index <= lastIndexInRect; index++)
166    {
167      // Calling itemAtIndex: will eventually instantiate the collection view item,
168      // if it hasn't been done already.
169      NSCollectionViewItem *collectionItem = [self itemAtIndex: index];
170      NSView *view = [collectionItem view];
171      [view setFrame: [self frameForItemAtIndex: index]];
172    }
173}
174
175- (void) dealloc
176{
177  //[[NSNotificationCenter defaultCenter] removeObserver: self];
178
179  DESTROY (_content);
180
181  // FIXME: Not clear if we should destroy the top-level item "itemPrototype" loaded in the nib file.
182  DESTROY (itemPrototype);
183
184  DESTROY (_backgroundColors);
185  DESTROY (_selectionIndexes);
186  DESTROY (_items);
187  //DESTROY (_mouseDownEvent);
188  [super dealloc];
189}
190
191- (BOOL) isFlipped
192{
193  return YES;
194}
195
196- (BOOL) allowsMultipleSelection
197{
198  return _allowsMultipleSelection;
199}
200
201- (void) setAllowsMultipleSelection: (BOOL)flag
202{
203  _allowsMultipleSelection = flag;
204}
205
206- (NSArray *) backgroundColors
207{
208  return _backgroundColors;
209}
210
211- (void) setBackgroundColors: (NSArray *)colors
212{
213  _backgroundColors = [colors copy];
214  [self setNeedsDisplay: YES];
215}
216
217- (NSArray *) content
218{
219  return _content;
220}
221
222- (void) setContent: (NSArray *)content
223{
224  NSInteger i;
225
226  ASSIGN(_content, content);
227  [self _removeItemsViews];
228
229  RELEASE (_items);
230  _items = [[NSMutableArray alloc] initWithCapacity: [_content count]];
231
232  for (i = 0; i < [_content count]; i++)
233    {
234      [_items addObject: placeholderItem];
235    }
236
237  if (!itemPrototype)
238    {
239      return;
240    }
241  else
242    {
243      [self _resetItemSize];
244      // Force recalculation of each item's frame
245      _itemSize = _minItemSize;
246      _tileWidth = -1.0;
247      [self tile];
248    }
249}
250
251- (id < NSCollectionViewDelegate >) delegate
252{
253  return delegate;
254}
255
256- (void) setDelegate: (id < NSCollectionViewDelegate >)aDelegate
257{
258  delegate = aDelegate;
259}
260
261- (NSCollectionViewItem *) itemPrototype
262{
263  return itemPrototype;
264}
265
266- (void) setItemPrototype: (NSCollectionViewItem *)prototype
267{
268  ASSIGN(itemPrototype, prototype);
269  [self _resetItemSize];
270}
271
272- (CGFloat) verticalMargin
273{
274  return _verticalMargin;
275}
276
277- (void) setVerticalMargin: (CGFloat)margin
278{
279  if (_verticalMargin == margin)
280    return;
281
282  _verticalMargin = margin;
283  [self tile];
284}
285
286- (NSSize) maxItemSize
287{
288  return _maxItemSize;
289}
290
291- (void) setMaxItemSize: (NSSize)size
292{
293  if (NSEqualSizes(_maxItemSize, size))
294    return;
295
296  _maxItemSize = size;
297  [self tile];
298}
299
300- (NSUInteger) maxNumberOfColumns
301{
302  return _maxNumberOfColumns;
303}
304
305- (void) setMaxNumberOfColumns: (NSUInteger)number
306{
307  _maxNumberOfColumns = number;
308}
309
310- (NSUInteger) maxNumberOfRows
311{
312  return _maxNumberOfRows;
313}
314
315- (void) setMaxNumberOfRows: (NSUInteger)number
316{
317  _maxNumberOfRows = number;
318}
319
320- (NSSize) minItemSize
321{
322  return _minItemSize;
323}
324
325- (void) setMinItemSize: (NSSize)size
326{
327  if (NSEqualSizes(_minItemSize, size))
328    return;
329
330  _minItemSize = size;
331  [self tile];
332}
333
334- (BOOL) isSelectable
335{
336  return _isSelectable;
337}
338
339- (void) setSelectable: (BOOL)flag
340{
341  _isSelectable = flag;
342  if (!_isSelectable)
343    {
344      NSInteger index = -1;
345      while ((index = [_selectionIndexes indexGreaterThanIndex: index]) != NSNotFound)
346        {
347          id item = [_items objectAtIndex: index];
348          if ([item respondsToSelector: @selector(setSelected:)])
349            {
350              [item setSelected:NO];
351            }
352        }
353    }
354}
355
356- (NSIndexSet *) selectionIndexes
357{
358  return _selectionIndexes;
359}
360
361- (void) setSelectionIndexes: (NSIndexSet *)indexes
362{
363  if (!_isSelectable)
364    {
365      return;
366    }
367
368  if (![_selectionIndexes isEqual: indexes])
369    {
370      ASSIGN(_selectionIndexes, indexes);
371    }
372
373  NSUInteger index = 0;
374  while (index < [_items count])
375    {
376      id item = [_items objectAtIndex: index];
377      if ([item respondsToSelector: @selector(setSelected:)])
378        {
379          [item setSelected:NO];
380        }
381      index++;
382    }
383
384  index = -1;
385  while ((index = [_selectionIndexes indexGreaterThanIndex: index]) !=
386         NSNotFound)
387    {
388      id item = [_items objectAtIndex: index];
389      if ([item respondsToSelector: @selector(setSelected:)])
390        {
391          [item setSelected: YES];
392        }
393    }
394}
395
396- (NSRect) frameForItemAtIndex: (NSUInteger)theIndex
397{
398  NSRect itemFrame = NSMakeRect (0,0,0,0);
399  NSInteger index;
400  NSUInteger count = [_items count];
401  CGFloat x = _horizontalMargin;
402  CGFloat y = -_itemSize.height;
403
404  if (_maxNumberOfColumns > 0 && _maxNumberOfRows > 0)
405    {
406      count = MIN(count, _maxNumberOfColumns * _maxNumberOfRows);
407    }
408
409  for (index = 0; index < count; ++index)
410    {
411      if (index % _numberOfColumns == 0)
412        {
413          x = _horizontalMargin;
414          y += _verticalMargin + _itemSize.height;
415        }
416
417      if (index == theIndex)
418        {
419          NSInteger draggingOffset = 0;
420
421          if (_draggingOnIndex != NSNotFound)
422            {
423              NSInteger draggingOnRow = (_draggingOnIndex / _numberOfColumns);
424              NSInteger currentIndexRow = (theIndex / _numberOfColumns);
425
426              if (draggingOnRow == currentIndexRow)
427                {
428                  if (index < _draggingOnIndex)
429                    {
430                      draggingOffset = -20;
431                    }
432                  else
433                    {
434                      draggingOffset = 20;
435                    }
436                }
437            }
438          itemFrame = NSMakeRect ((x + draggingOffset), y, _itemSize.width, _itemSize.height);
439          break;
440        }
441
442      x += _itemSize.width + _horizontalMargin;
443    }
444  return itemFrame;
445}
446
447- (NSRect) _frameForRowOfItemAtIndex: (NSUInteger)theIndex
448{
449  NSRect itemFrame = [self frameForItemAtIndex: theIndex];
450
451  return NSMakeRect (0, itemFrame.origin.y, [self bounds].size.width, itemFrame.size.height);
452}
453
454// Returns the frame of an item's row with the row above and the row below
455- (NSRect) _frameForRowsAroundItemAtIndex: (NSUInteger)theIndex
456{
457  NSRect itemRowFrame = [self _frameForRowOfItemAtIndex: theIndex];
458  CGFloat y = MAX (0, itemRowFrame.origin.y - itemRowFrame.size.height);
459  CGFloat height = MIN (itemRowFrame.size.height * 3, [self bounds].size.height);
460
461  return NSMakeRect(0, y, itemRowFrame.size.width, height);
462}
463
464- (NSCollectionViewItem *) itemAtIndex: (NSUInteger)index
465{
466  id item = [_items objectAtIndex: index];
467
468  if (item == placeholderItem)
469    {
470      item = [self newItemForRepresentedObject: [_content objectAtIndex: index]];
471      [_items replaceObjectAtIndex: index withObject: item];
472      if ([[self selectionIndexes] containsIndex: index])
473        {
474          [item setSelected: YES];
475        }
476      [self addSubview: [item view]];
477      RELEASE(item);
478    }
479  return item;
480}
481
482- (NSCollectionViewItem *) newItemForRepresentedObject: (id)object
483{
484  NSCollectionViewItem *collectionItem = nil;
485  if (itemPrototype)
486    {
487      collectionItem = [itemPrototype copy];
488      [collectionItem setRepresentedObject: object];
489    }
490  return collectionItem;
491}
492
493- (void) _removeItemsViews
494{
495  if (!_items)
496    return;
497
498  NSUInteger count = [_items count];
499
500  while (count--)
501    {
502      id item = [_items objectAtIndex: count];
503
504      if ([item respondsToSelector: @selector(view)])
505        {
506          [[item view] removeFromSuperview];
507          [item setSelected: NO];
508        }
509    }
510}
511
512- (void) tile
513{
514  // TODO: - Animate items, Add Fade-in/Fade-out (as in Cocoa)
515  //       - Put the tiling on a delay
516  if (!_items)
517    return;
518
519  CGFloat width = [self bounds].size.width;
520
521  if (width == _tileWidth)
522    return;
523
524  NSSize itemSize = NSMakeSize(_minItemSize.width, _minItemSize.height);
525
526  _numberOfColumns = MAX(1.0, floor(width / itemSize.width));
527
528  if (_maxNumberOfColumns > 0)
529    {
530      _numberOfColumns = MIN(_maxNumberOfColumns, _numberOfColumns);
531    }
532
533  if (_numberOfColumns == 0)
534    {
535      _numberOfColumns = 1;
536    }
537
538  CGFloat remaining = width - _numberOfColumns * itemSize.width;
539
540  if (remaining > 0 && itemSize.width < _maxItemSize.width)
541    {
542      itemSize.width = MIN(_maxItemSize.width, itemSize.width +
543                           floor(remaining / _numberOfColumns));
544    }
545
546  if (_maxNumberOfColumns == 1 && itemSize.width <
547      _maxItemSize.width && itemSize.width < width)
548    {
549      itemSize.width = MIN(_maxItemSize.width, width);
550    }
551
552  if (!NSEqualSizes(_itemSize, itemSize))
553    {
554      _itemSize = itemSize;
555    }
556
557  NSInteger index;
558  NSUInteger count = [_items count];
559
560  if (_maxNumberOfColumns > 0 && _maxNumberOfRows > 0)
561    {
562      count = MIN(count, _maxNumberOfColumns * _maxNumberOfRows);
563    }
564
565  _horizontalMargin = floor((width - _numberOfColumns * itemSize.width) /
566                            (_numberOfColumns + 1));
567  CGFloat y = -itemSize.height;
568
569  for (index = 0; index < count; ++index)
570    {
571      if (index % _numberOfColumns == 0)
572        {
573          y += _verticalMargin + itemSize.height;
574        }
575    }
576
577  id superview = [self superview];
578  CGFloat proposedHeight = y + itemSize.height + _verticalMargin;
579  if ([superview isKindOfClass: [NSClipView class]])
580    {
581      NSSize superviewSize = [superview bounds].size;
582      proposedHeight = MAX(superviewSize.height, proposedHeight);
583    }
584
585  _tileWidth = width;
586  [self setFrameSize: NSMakeSize(width, proposedHeight)];
587  [self setNeedsDisplay: YES];
588}
589
590- (void) resizeSubviewsWithOldSize: (NSSize)aSize
591{
592  NSSize currentSize = [self frame].size;
593  if (!NSEqualSizes(currentSize, aSize))
594    {
595      [self tile];
596    }
597}
598
599- (id) initWithCoder: (NSCoder *)aCoder
600{
601  self = [super initWithCoder:aCoder];
602
603  if (self)
604    {
605      if ([aCoder allowsKeyedCoding])
606        {
607          _itemSize = NSMakeSize(0, 0);
608          _tileWidth = -1.0;
609
610          _minItemSize = [aCoder decodeSizeForKey: NSCollectionViewMinItemSizeKey];
611          _maxItemSize = [aCoder decodeSizeForKey: NSCollectionViewMaxItemSizeKey];
612
613          _maxNumberOfRows = [aCoder decodeInt64ForKey: NSCollectionViewMaxNumberOfRowsKey];
614          _maxNumberOfColumns = [aCoder decodeInt64ForKey: NSCollectionViewMaxNumberOfColumnsKey];
615
616          //_verticalMargin = [aCoder decodeFloatForKey: NSCollectionViewVerticalMarginKey];
617
618          _isSelectable = [aCoder decodeBoolForKey: NSCollectionViewSelectableKey];
619          _allowsMultipleSelection = [aCoder decodeBoolForKey: NSCollectionViewAllowsMultipleSelectionKey];
620
621          [self setBackgroundColors: [aCoder decodeObjectForKey: NSCollectionViewBackgroundColorsKey]];
622        }
623      else
624        {
625        }
626    }
627  [self _initDefaults];
628
629  return self;
630}
631
632- (void) encodeWithCoder: (NSCoder *)aCoder
633{
634  [super encodeWithCoder: aCoder];
635  if ([aCoder allowsKeyedCoding])
636    {
637      if (!NSEqualSizes(_minItemSize, NSMakeSize(0, 0)))
638        {
639          [aCoder encodeSize: _minItemSize forKey: NSCollectionViewMinItemSizeKey];
640        }
641
642      if (!NSEqualSizes(_maxItemSize, NSMakeSize(0, 0)))
643        {
644          [aCoder encodeSize: _maxItemSize forKey: NSCollectionViewMaxItemSizeKey];
645        }
646
647      [aCoder encodeInt64: _maxNumberOfRows
648                   forKey: NSCollectionViewMaxNumberOfRowsKey];
649      [aCoder encodeInt64: _maxNumberOfColumns
650                   forKey: NSCollectionViewMaxNumberOfColumnsKey];
651
652      [aCoder encodeBool: _isSelectable
653                  forKey: NSCollectionViewSelectableKey];
654      [aCoder encodeBool: _allowsMultipleSelection
655                  forKey: NSCollectionViewAllowsMultipleSelectionKey];
656
657      //[aCoder encodeCGFloat: _verticalMargin forKey: NSCollectionViewVerticalMarginKey];
658      [aCoder encodeObject: _backgroundColors
659                    forKey: NSCollectionViewBackgroundColorsKey];
660    }
661  else
662    {
663    }
664}
665
666- (void) mouseDown: (NSEvent *)theEvent
667{
668  NSPoint initialLocation = [theEvent locationInWindow];
669  NSPoint location = [self convertPoint: initialLocation fromView: nil];
670  NSInteger index = [self _indexAtPoint: location];
671  NSEvent *lastEvent = theEvent;
672  BOOL done = NO;
673  NSUInteger eventMask = (NSLeftMouseUpMask
674                        | NSLeftMouseDownMask
675                        | NSLeftMouseDraggedMask
676                        | NSPeriodicMask);
677  NSDate *distantFuture = [NSDate distantFuture];
678
679  while (!done)
680    {
681      lastEvent = [NSApp nextEventMatchingMask: eventMask
682                                     untilDate: distantFuture
683                                        inMode: NSEventTrackingRunLoopMode
684                                       dequeue: YES];
685      NSEventType eventType = [lastEvent type];
686      NSPoint mouseLocationWin = [lastEvent locationInWindow];
687      switch (eventType)
688        {
689        case NSLeftMouseDown:
690          break;
691        case NSLeftMouseDragged:
692          if (fabs(mouseLocationWin.x - initialLocation.x) >= 2
693              || fabs(mouseLocationWin.y - initialLocation.y) >= 2)
694            {
695              if ([self _startDragOperationWithEvent: theEvent clickedIndex: index])
696                {
697                  done = YES;
698                }
699            }
700          break;
701        case NSLeftMouseUp:
702          [self _selectWithEvent: theEvent index: index];
703          done = YES;
704          break;
705        default:
706          done = NO;
707          break;
708        }
709    }
710}
711
712- (void) _selectWithEvent: (NSEvent *)theEvent index: (NSUInteger)index
713{
714  NSMutableIndexSet *currentIndexSet = [[NSMutableIndexSet alloc] initWithIndexSet: [self selectionIndexes]];
715
716  if (_isSelectable && (index < [_items count]))
717    {
718      if (_allowsMultipleSelection
719          && (([theEvent modifierFlags] & NSControlKeyMask)
720              || ([theEvent modifierFlags] & NSShiftKeyMask)))
721        {
722          if ([theEvent modifierFlags] & NSControlKeyMask)
723            {
724              if ([currentIndexSet containsIndex: index])
725                {
726                  [currentIndexSet removeIndex: index];
727                }
728              else
729                {
730                  [currentIndexSet addIndex: index];
731                }
732              [self setSelectionIndexes: currentIndexSet];
733            }
734          else if ([theEvent modifierFlags] & NSShiftKeyMask)
735            {
736              NSUInteger firstSelectedIndex = [currentIndexSet firstIndex];
737              NSRange selectedRange;
738
739              if (firstSelectedIndex == NSNotFound)
740                {
741                  selectedRange = NSMakeRange(index, index);
742                }
743              else if (index < firstSelectedIndex)
744                {
745                  selectedRange = NSMakeRange(index, (firstSelectedIndex - index + 1));
746                }
747              else
748                {
749                  selectedRange = NSMakeRange(firstSelectedIndex, (index - firstSelectedIndex + 1));
750                }
751              [currentIndexSet addIndexesInRange: selectedRange];
752              [self setSelectionIndexes: currentIndexSet];
753            }
754        }
755      else
756        {
757          [self setSelectionIndexes: [NSIndexSet indexSetWithIndex: index]];
758        }
759      [[self window] makeFirstResponder: self];
760    }
761  else
762    {
763      [self setSelectionIndexes: [NSIndexSet indexSet]];
764    }
765  RELEASE (currentIndexSet);
766}
767
768- (NSInteger) _indexAtPoint: (NSPoint)point
769{
770  NSInteger row = floor(point.y / (_itemSize.height + _verticalMargin));
771  NSInteger column = floor(point.x / (_itemSize.width + _horizontalMargin));
772  return (column + (row * _numberOfColumns));
773}
774
775- (BOOL) acceptsFirstResponder
776{
777  return YES;
778}
779
780/* MARK: Keyboard Interaction */
781
782- (void) keyDown: (NSEvent *)theEvent
783{
784  [self interpretKeyEvents: [NSArray arrayWithObject: theEvent]];
785}
786
787-(void) moveUp: (id)sender
788{
789  [self _moveUpAndExpandSelection: NO];
790}
791
792-(void) moveUpAndModifySelection: (id)sender
793{
794  [self _moveUpAndExpandSelection: YES];
795}
796
797- (void) _moveUpAndExpandSelection: (BOOL)shouldExpand
798{
799  NSInteger index = [[self selectionIndexes] firstIndex];
800  if (index != NSNotFound && index >= _numberOfColumns)
801    {
802      [self _modifySelectionWithNewIndex: index - _numberOfColumns
803                               direction: -1
804                                  expand: shouldExpand];
805    }
806}
807
808-(void) moveDown: (id)sender
809{
810  [self _moveDownAndExpandSelection: NO];
811}
812
813-(void) moveDownAndModifySelection: (id)sender
814{
815  [self _moveDownAndExpandSelection: YES];
816}
817
818-(void) _moveDownAndExpandSelection: (BOOL)shouldExpand
819{
820  NSInteger index = [[self selectionIndexes] lastIndex];
821  if (index != NSNotFound && (index + _numberOfColumns) < [_items count])
822    {
823      [self _modifySelectionWithNewIndex: index + _numberOfColumns
824                               direction: 1
825                                  expand: shouldExpand];
826    }
827}
828
829-(void) moveLeft: (id)sender
830{
831  [self _moveLeftAndExpandSelection: NO];
832}
833
834-(void) moveLeftAndModifySelection: (id)sender
835{
836  [self _moveLeftAndExpandSelection: YES];
837}
838
839-(void) moveBackwardAndModifySelection: (id)sender
840{
841  [self _moveLeftAndExpandSelection: YES];
842}
843
844-(void) _moveLeftAndExpandSelection: (BOOL)shouldExpand
845{
846  NSUInteger index = [[self selectionIndexes] firstIndex];
847  if (index != NSNotFound && index != 0)
848    {
849      [self _modifySelectionWithNewIndex: index - 1 direction: -1 expand: shouldExpand];
850    }
851}
852
853-(void) moveRight: (id)sender
854{
855  [self _moveRightAndExpandSelection: NO];
856}
857
858-(void) moveRightAndModifySelection: (id)sender
859{
860  [self _moveRightAndExpandSelection: YES];
861}
862
863-(void) moveForwardAndModifySelection: (id)sender
864{
865  [self _moveRightAndExpandSelection: YES];
866}
867
868-(void) _moveRightAndExpandSelection: (BOOL)shouldExpand
869{
870  NSUInteger index = [[self selectionIndexes] lastIndex];
871  if (index != NSNotFound && index != ([_items count] - 1))
872    {
873      [self _modifySelectionWithNewIndex: index + 1 direction: 1 expand: shouldExpand];
874    }
875}
876
877- (void) _modifySelectionWithNewIndex: (NSUInteger)anIndex
878                            direction: (int)aDirection
879                               expand: (BOOL)shouldExpand
880{
881  anIndex = MIN(MAX(anIndex, 0), [_items count] - 1);
882
883  if (_allowsMultipleSelection && shouldExpand)
884    {
885      NSMutableIndexSet *newIndexSet = [[NSMutableIndexSet alloc] initWithIndexSet: _selectionIndexes];
886      NSUInteger firstIndex = [newIndexSet firstIndex];
887      NSUInteger lastIndex = [newIndexSet lastIndex];
888      if (aDirection == -1)
889        {
890          [newIndexSet addIndexesInRange:NSMakeRange (anIndex, firstIndex - anIndex + 1)];
891        }
892      else
893        {
894          [newIndexSet addIndexesInRange:NSMakeRange (lastIndex, anIndex - lastIndex + 1)];
895        }
896      [self setSelectionIndexes: newIndexSet];
897      RELEASE (newIndexSet);
898    }
899  else
900    {
901      [self setSelectionIndexes: [NSIndexSet indexSetWithIndex: anIndex]];
902    }
903
904  [self scrollRectToVisible: [self frameForItemAtIndex: anIndex]];
905}
906
907
908/* MARK: Drag & Drop */
909
910-(NSDragOperation) draggingSourceOperationMaskForLocal: (BOOL)isLocal
911{
912  if (isLocal)
913    {
914      return _draggingSourceOperationMaskForLocal;
915    }
916  else
917    {
918      return _draggingSourceOperationMaskForRemote;
919    }
920}
921
922-(void) setDraggingSourceOperationMask: (NSDragOperation)mask
923                              forLocal: (BOOL)isLocal
924{
925  if (isLocal)
926    {
927      _draggingSourceOperationMaskForLocal = mask;
928    }
929  else
930    {
931      _draggingSourceOperationMaskForRemote = mask;
932    }
933}
934
935- (BOOL) _startDragOperationWithEvent: (NSEvent*)event
936                         clickedIndex: (NSUInteger)index
937{
938  NSIndexSet *dragIndexes = _selectionIndexes;
939
940  if (![dragIndexes containsIndex: index]
941      && (index < [_items count]))
942    {
943      dragIndexes = [NSIndexSet indexSetWithIndex: index];
944    }
945
946  if (![dragIndexes count])
947    return NO;
948
949  if (![delegate respondsToSelector: @selector(collectionView:writeItemsAtIndexes:toPasteboard:)])
950    return NO;
951
952  if ([delegate respondsToSelector: @selector(collectionView:canDragItemsAtIndexes:withEvent:)])
953    {
954      if (![delegate collectionView: self
955              canDragItemsAtIndexes: dragIndexes
956                          withEvent: event])
957        {
958          return NO;
959        }
960    }
961
962  NSPoint downPoint = [event locationInWindow];
963  NSPoint convertedDownPoint = [self convertPoint: downPoint fromView: nil];
964
965  NSPasteboard *pasteboard = [NSPasteboard pasteboardWithName: NSDragPboard];
966  if ([self _writeItemsAtIndexes:dragIndexes toPasteboard: pasteboard])
967    {
968      NSImage *dragImage = [self draggingImageForItemsAtIndexes: dragIndexes
969                                                      withEvent: event
970                                                         offset: NULL];
971
972      [self dragImage: dragImage
973                   at: convertedDownPoint
974               offset: NSMakeSize(0,0)
975                event: event
976           pasteboard: pasteboard
977               source: self
978            slideBack: YES];
979
980      return YES;
981    }
982  return NO;
983}
984
985- (NSImage *) draggingImageForItemsAtIndexes: (NSIndexSet *)indexes
986                                   withEvent: (NSEvent *)event
987                                      offset: (NSPointPointer)dragImageOffset
988{
989  if ([delegate respondsToSelector: @selector(collectionView:draggingImageForItemsAtIndexes:withEvent:offset:)])
990    {
991      return [delegate collectionView: self
992                       draggingImageForItemsAtIndexes: indexes
993                            withEvent: event
994                               offset: dragImageOffset];
995    }
996  else
997    {
998      return [[NSImage alloc] initWithData: [self dataWithPDFInsideRect: [self bounds]]];
999    }
1000}
1001
1002- (BOOL) _writeItemsAtIndexes: (NSIndexSet *)indexes
1003                 toPasteboard: (NSPasteboard *)pasteboard
1004{
1005  if (![delegate respondsToSelector: @selector(collectionView:writeItemsAtIndexes:toPasteboard:)])
1006    {
1007      return NO;
1008    }
1009  else
1010    {
1011      return [delegate collectionView: self
1012                  writeItemsAtIndexes: indexes
1013                         toPasteboard: pasteboard];
1014    }
1015}
1016
1017- (void) draggedImage: (NSImage *)image
1018              endedAt: (NSPoint)point
1019            operation: (NSDragOperation)operation
1020{
1021}
1022
1023- (NSDragOperation) _draggingEnteredOrUpdated: (id<NSDraggingInfo>)sender
1024{
1025  NSDragOperation result = NSDragOperationNone;
1026
1027  if ([delegate respondsToSelector: @selector(collectionView:validateDrop:proposedIndex:dropOperation:)])
1028    {
1029      NSPoint location = [self convertPoint: [sender draggingLocation] fromView: nil];
1030      NSInteger index = [self _indexAtPoint: location];
1031      index = (index > [_items count] - 1) ? [_items count] - 1 : index;
1032      _draggingOnIndex = index;
1033
1034      NSInteger *proposedIndex = &index;
1035      NSInteger dropOperationInt = NSCollectionViewDropOn;
1036      NSCollectionViewDropOperation *dropOperation = &dropOperationInt;
1037
1038      // TODO: We currently don't do anything with the proposedIndex & dropOperation that
1039      // may get altered by the delegate.
1040      result = [delegate collectionView: self
1041                           validateDrop: sender
1042                          proposedIndex: proposedIndex
1043                          dropOperation: dropOperation];
1044
1045      if (result == NSDragOperationNone)
1046        {
1047          _draggingOnIndex = NSNotFound;
1048        }
1049      [self setNeedsDisplayInRect: [self _frameForRowsAroundItemAtIndex: index]];
1050    }
1051
1052  return result;
1053}
1054
1055- (NSDragOperation) draggingEntered: (id<NSDraggingInfo>)sender
1056{
1057  return [self _draggingEnteredOrUpdated: sender];
1058}
1059
1060- (void) draggingExited: (id<NSDraggingInfo>)sender
1061{
1062  [self setNeedsDisplayInRect: [self _frameForRowsAroundItemAtIndex: _draggingOnIndex]];
1063  _draggingOnIndex = NSNotFound;
1064}
1065
1066- (NSDragOperation) draggingUpdated: (id<NSDraggingInfo>)sender
1067{
1068  return [self _draggingEnteredOrUpdated: sender];
1069}
1070
1071- (BOOL) prepareForDragOperation: (id<NSDraggingInfo>)sender
1072{
1073  NSPoint location = [self convertPoint: [sender draggingLocation] fromView: nil];
1074  NSInteger index = [self _indexAtPoint: location];
1075
1076  _draggingOnIndex = NSNotFound;
1077  [self setNeedsDisplayInRect: [self _frameForRowsAroundItemAtIndex: index]];
1078  return YES;
1079}
1080
1081- (BOOL) performDragOperation: (id<NSDraggingInfo>)sender
1082{
1083  NSPoint location = [self convertPoint: [sender draggingLocation] fromView: nil];
1084  NSInteger index = [self _indexAtPoint: location];
1085  index = (index > [_items count] - 1) ? [_items count] - 1 : index;
1086
1087  BOOL result = NO;
1088  if ([delegate respondsToSelector: @selector(collectionView:acceptDrop:index:dropOperation:)])
1089    {
1090      // TODO: dropOperation should be retrieved from the validateDrop delegate method.
1091      result = [delegate collectionView: self
1092                             acceptDrop: sender
1093                                  index: index
1094                          dropOperation: NSCollectionViewDropOn];
1095    }
1096  return result;
1097}
1098
1099- (BOOL) wantsPeriodicDraggingUpdates
1100{
1101  return YES;
1102}
1103
1104@end
1105