00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023 @import <Foundation/CPArray.j>
00024 @import <Foundation/CPData.j>
00025 @import <Foundation/CPIndexSet.j>
00026 @import <Foundation/CPKeyedArchiver.j>
00027 @import <Foundation/CPKeyedUnarchiver.j>
00028
00029 @import "CPView.j"
00030 @import "CPCollectionViewItem.j"
00031
00032
00066 @implementation CPCollectionView : CPView
00067 {
00068 CPArray _content;
00069 CPArray _items;
00070
00071 CPData _itemData;
00072 CPCollectionViewItem _itemPrototype;
00073 CPCollectionViewItem _itemForDragging;
00074 CPMutableArray _cachedItems;
00075
00076 unsigned _maxNumberOfRows;
00077 unsigned _maxNumberOfColumns;
00078
00079 CGSize _minItemSize;
00080 CGSize _maxItemSize;
00081
00082 CPArray _backgroundColors;
00083
00084 float _tileWidth;
00085
00086 BOOL _isSelectable;
00087 BOOL _allowsMultipleSelection;
00088 BOOL _allowsEmptySelection;
00089 CPIndexSet _selectionIndexes;
00090
00091 CGSize _itemSize;
00092
00093 float _horizontalMargin;
00094 float _verticalMargin;
00095
00096 unsigned _numberOfRows;
00097 unsigned _numberOfColumns;
00098
00099 id _delegate;
00100
00101 CPEvent _mouseDownEvent;
00102 }
00103
00104 - (id)initWithFrame:(CGRect)aFrame
00105 {
00106 self = [super initWithFrame:aFrame];
00107
00108 if (self)
00109 {
00110 _items = [];
00111 _content = [];
00112
00113 _cachedItems = [];
00114
00115 _itemSize = CGSizeMakeZero();
00116 _minItemSize = CGSizeMakeZero();
00117 _maxItemSize = CGSizeMakeZero();
00118
00119 [self setBackgroundColors:nil];
00120
00121 _verticalMargin = 5.0;
00122 _tileWidth = -1.0;
00123
00124 _selectionIndexes = [CPIndexSet indexSet];
00125 _allowsEmptySelection = YES;
00126 _isSelectable = YES;
00127 }
00128
00129 return self;
00130 }
00131
00136 - (void)setItemPrototype:(CPCollectionViewItem)anItem
00137 {
00138 _cachedItems = [];
00139 _itemData = nil;
00140 _itemForDragging = nil;
00141 _itemPrototype = anItem;
00142
00143 [self reloadContent];
00144 }
00145
00149 - (CPCollectionViewItem)itemPrototype
00150 {
00151 return _itemPrototype;
00152 }
00153
00158 - (CPCollectionViewItem)newItemForRepresentedObject:(id)anObject
00159 {
00160 var item = nil;
00161
00162 if (_cachedItems.length)
00163 item = _cachedItems.pop();
00164
00165 else
00166 {
00167 if (!_itemData)
00168 if (_itemPrototype)
00169 _itemData = [CPKeyedArchiver archivedDataWithRootObject:_itemPrototype];
00170
00171 item = [CPKeyedUnarchiver unarchiveObjectWithData:_itemData];
00172 }
00173
00174 [item setRepresentedObject:anObject];
00175 [[item view] setFrameSize:_itemSize];
00176
00177 return item;
00178 }
00179
00180
00184 - (BOOL)acceptsFirstResponder
00185 {
00186 return YES;
00187 }
00188
00192 - (BOOL)isFirstResponder
00193 {
00194 return [[self window] firstResponder] === self;
00195 }
00196
00197
00204 - (void)setContent:(CPArray)anArray
00205 {
00206 if (_content == anArray)
00207 return;
00208
00209 _content = anArray;
00210
00211 [self reloadContent];
00212 }
00213
00217 - (CPArray)content
00218 {
00219 return _content;
00220 }
00221
00225 - (CPArray)items
00226 {
00227 return _items;
00228 }
00229
00230
00235 - (void)setSelectable:(BOOL)isSelectable
00236 {
00237 if (_isSelectable == isSelectable)
00238 return;
00239
00240 _isSelectable = isSelectable;
00241
00242 if (!_isSelectable)
00243 {
00244 var index = CPNotFound;
00245
00246 while ((index = [_selectionIndexes indexGreaterThanIndex:index]) != CPNotFound)
00247 [_items[index] setSelected:NO];
00248 }
00249 }
00250
00255 - (BOOL)isSelectable
00256 {
00257 return _isSelectable;
00258 }
00259
00264 - (void)setAllowsEmptySelection:(BOOL)shouldAllowEmptySelection
00265 {
00266 _allowsEmptySelection = shouldAllowEmptySelection;
00267 }
00268
00272 - (BOOL)allowsEmptySelection
00273 {
00274 return _allowsEmptySelection;
00275 }
00276
00281 - (void)setAllowsMultipleSelection:(BOOL)shouldAllowMultipleSelection
00282 {
00283 _allowsMultipleSelection = shouldAllowMultipleSelection;
00284 }
00285
00289 - (BOOL)allowsMultipleSelection
00290 {
00291 return _allowsMultipleSelection;
00292 }
00293
00298 - (void)setSelectionIndexes:(CPIndexSet)anIndexSet
00299 {
00300 if (_selectionIndexes == anIndexSet || !_isSelectable)
00301 return;
00302
00303 var index = CPNotFound;
00304
00305 while ((index = [_selectionIndexes indexGreaterThanIndex:index]) != CPNotFound)
00306 [_items[index] setSelected:NO];
00307
00308 _selectionIndexes = anIndexSet;
00309
00310 var index = CPNotFound;
00311
00312 while ((index = [_selectionIndexes indexGreaterThanIndex:index]) != CPNotFound)
00313 [_items[index] setSelected:YES];
00314
00315 if ([_delegate respondsToSelector:@selector(collectionViewDidChangeSelection:)])
00316 [_delegate collectionViewDidChangeSelection:self]
00317 }
00318
00322 - (CPIndexSet)selectionIndexes
00323 {
00324 return [_selectionIndexes copy];
00325 }
00326
00327
00328 - (void)reloadContent
00329 {
00330
00331 var count = _items.length;
00332
00333 while (count--)
00334 {
00335 [[_items[count] view] removeFromSuperview];
00336 [_items[count] setSelected:NO];
00337
00338 _cachedItems.push(_items[count]);
00339 }
00340
00341 _items = [];
00342
00343 if (!_itemPrototype || !_content)
00344 return;
00345
00346 var index = 0;
00347
00348 count = _content.length;
00349
00350 for (; index < count; ++index)
00351 {
00352 _items.push([self newItemForRepresentedObject:_content[index]]);
00353
00354 [self addSubview:[_items[index] view]];
00355 }
00356
00357 index = CPNotFound;
00358 while ((index = [_selectionIndexes indexGreaterThanIndex:index]) != CPNotFound)
00359 [_items[index] setSelected:YES];
00360
00361 [self tile];
00362 }
00363
00364
00365 - (void)tile
00366 {
00367 var width = CGRectGetWidth([self bounds]);
00368
00369 if (![_content count] || width == _tileWidth)
00370 return;
00371
00372
00373
00374
00375 var itemSize = CGSizeMakeCopy(_minItemSize);
00376
00377 _numberOfColumns = MAX(1.0, FLOOR(width / itemSize.width));
00378
00379 if (_maxNumberOfColumns > 0)
00380 _numberOfColumns = MIN(_maxNumberOfColumns, _numberOfColumns);
00381
00382 var remaining = width - _numberOfColumns * itemSize.width,
00383 itemsNeedSizeUpdate = NO;
00384
00385 if (remaining > 0 && itemSize.width < _maxItemSize.width)
00386 itemSize.width = MIN(_maxItemSize.width, itemSize.width + FLOOR(remaining / _numberOfColumns));
00387
00388
00389 if (_maxNumberOfColumns == 1 && itemSize.width < _maxItemSize.width && itemSize.width < width)
00390 itemSize.width = MIN(_maxItemSize.width, width);
00391
00392 if (!CGSizeEqualToSize(_itemSize, itemSize))
00393 {
00394 _itemSize = itemSize;
00395 itemsNeedSizeUpdate = YES;
00396 }
00397
00398 var index = 0,
00399 count = _items.length;
00400
00401 if (_maxNumberOfColumns > 0 && _maxNumberOfRows > 0)
00402 count = MIN(count, _maxNumberOfColumns * _maxNumberOfRows);
00403
00404 _numberOfRows = CEIL(count / _numberOfColumns);
00405
00406 _horizontalMargin = FLOOR((width - _numberOfColumns * itemSize.width) / (_numberOfColumns + 1));
00407
00408 var x = _horizontalMargin,
00409 y = -itemSize.height;
00410
00411 for (; index < count; ++index)
00412 {
00413 if (index % _numberOfColumns == 0)
00414 {
00415 x = _horizontalMargin;
00416 y += _verticalMargin + itemSize.height;
00417 }
00418
00419 var view = [_items[index] view];
00420
00421 [view setFrameOrigin:CGPointMake(x, y)];
00422
00423 if (itemsNeedSizeUpdate)
00424 [view setFrameSize:_itemSize];
00425
00426 x += itemSize.width + _horizontalMargin;
00427 }
00428
00429 _tileWidth = width;
00430 [self setFrameSize:CGSizeMake(width, y + itemSize.height + _verticalMargin)];
00431 _tileWidth = -1.0;
00432 }
00433
00434 - (void)resizeSubviewsWithOldSize:(CGSize)aSize
00435 {
00436 [self tile];
00437 }
00438
00439
00444 - (void)setMaxNumberOfRows:(unsigned)aMaxNumberOfRows
00445 {
00446 if (_maxNumberOfRows == aMaxNumberOfRows)
00447 return;
00448
00449 _maxNumberOfRows = aMaxNumberOfRows;
00450
00451 [self tile];
00452 }
00453
00457 - (unsigned)maxNumberOfRows
00458 {
00459 return _maxNumberOfRows;
00460 }
00461
00466 - (void)setMaxNumberOfColumns:(unsigned)aMaxNumberOfColumns
00467 {
00468 if (_maxNumberOfColumns == aMaxNumberOfColumns)
00469 return;
00470
00471 _maxNumberOfColumns = aMaxNumberOfColumns;
00472
00473 [self tile];
00474 }
00475
00479 - (unsigned)maxNumberOfColumns
00480 {
00481 return _maxNumberOfColumns;
00482 }
00483
00487 - (unsigned)numberOfRows
00488 {
00489 return _numberOfRows;
00490 }
00491
00496 - (unsigned)numberOfColumns
00497 {
00498 return _numberOfColumns;
00499 }
00500
00505 - (void)setMinItemSize:(CGSize)aSize
00506 {
00507 if (CGSizeEqualToSize(_minItemSize, aSize))
00508 return;
00509
00510 _minItemSize = CGSizeMakeCopy(aSize);
00511
00512 [self tile];
00513 }
00514
00518 - (CGSize)minItemSize
00519 {
00520 return _minItemSize;
00521 }
00522
00527 - (void)setMaxItemSize:(CGSize)aSize
00528 {
00529 if (CGSizeEqualToSize(_maxItemSize, aSize))
00530 return;
00531
00532 _maxItemSize = CGSizeMakeCopy(aSize);
00533
00534 [self tile];
00535 }
00536
00540 - (CGSize)maxItemSize
00541 {
00542 return _maxItemSize;
00543 }
00544
00545 - (void)setBackgroundColors:(CPArray)backgroundColors
00546 {
00547 if (_backgroundColors === backgroundColors)
00548 return;
00549
00550 _backgroundColors = backgroundColors;
00551
00552 if (!_backgroundColors)
00553 _backgroundColors = [CPColor whiteColor];
00554
00555 if ([_backgroundColors count] === 1)
00556 [self setBackgroundColor:_backgroundColors[0]];
00557
00558 else
00559 [self setBackgroundColor:nil];
00560
00561 [self setNeedsDisplay:YES];
00562 }
00563
00564 - (CPArray)backgroundColors
00565 {
00566 return _backgroundColors;
00567 }
00568
00569 - (void)mouseUp:(CPEvent)anEvent
00570 {
00571 if ([_selectionIndexes count] && [anEvent clickCount] == 2 && [_delegate respondsToSelector:@selector(collectionView:didDoubleClickOnItemAtIndex:)])
00572 [_delegate collectionView:self didDoubleClickOnItemAtIndex:[_selectionIndexes firstIndex]];
00573 }
00574
00575 - (void)mouseDown:(CPEvent)anEvent
00576 {
00577 _mouseDownEvent = anEvent;
00578
00579 var location = [self convertPoint:[anEvent locationInWindow] fromView:nil],
00580 row = FLOOR(location.y / (_itemSize.height + _verticalMargin)),
00581 column = FLOOR(location.x / (_itemSize.width + _horizontalMargin)),
00582 index = row * _numberOfColumns + column;
00583
00584 if (index >= 0 && index < _items.length)
00585 [self setSelectionIndexes:[CPIndexSet indexSetWithIndex:index]];
00586
00587 else if (_allowsEmptySelection)
00588 [self setSelectionIndexes:[CPIndexSet indexSet]];
00589 }
00590
00591 - (void)mouseDragged:(CPEvent)anEvent
00592 {
00593 if (![_delegate respondsToSelector:@selector(collectionView:dragTypesForItemsAtIndexes:)])
00594 return;
00595
00596
00597 if (![_selectionIndexes count])
00598 return;
00599
00600 if ([_delegate respondsToSelector:@selector(collectionView:canDragItemsAtIndexes:withEvent:)] &&
00601 ![_delegate collectionView:self canDragItemsAtIndexes:_selectionIndexes withEvent:_mouseDownEvent])
00602 return;
00603
00604
00605 var dragTypes = [_delegate collectionView:self dragTypesForItemsAtIndexes:_selectionIndexes];
00606
00607 [[CPPasteboard pasteboardWithName:CPDragPboard] declareTypes:dragTypes owner:self];
00608
00609 var point = [self convertPoint:[anEvent locationInWindow] fromView:nil];
00610
00611 if (!_itemForDragging)
00612 _itemForDragging = [self newItemForRepresentedObject:_content[[_selectionIndexes firstIndex]]];
00613 else
00614 [_itemForDragging setRepresentedObject:_content[[_selectionIndexes firstIndex]]];
00615
00616 var view = [_itemForDragging view];
00617
00618 [view setFrameSize:_itemSize];
00619 [view setAlphaValue:0.7];
00620
00621 [self dragView:view
00622 at:[[_items[[_selectionIndexes firstIndex]] view] frame].origin
00623 offset:CGSizeMakeZero()
00624 event:_mouseDownEvent
00625 pasteboard:nil
00626 source:self
00627 slideBack:YES];
00628 }
00629
00635 - (void)pasteboard:(CPPasteboard)aPasteboard provideDataForType:(CPString)aType
00636 {
00637 [aPasteboard setData:[_delegate collectionView:self dataForItemsAtIndexes:_selectionIndexes forType:aType] forType:aType];
00638 }
00639
00640
00641
00647 - (void)setVerticalMargin:(float)aVerticalMargin
00648 {
00649 if (_verticalMargin == aVerticalMargin)
00650 return;
00651
00652 _verticalMargin = aVerticalMargin;
00653
00654 [self tile];
00655 }
00656
00661 - (float)verticalMargin
00662 {
00663 return _verticalMargin;
00664 }
00665
00670 - (void)setDelegate:(id)aDelegate
00671 {
00672 _delegate = aDelegate;
00673 }
00674
00678 - (id)delegate
00679 {
00680 return _delegate;
00681 }
00682
00683 - (CPCollectionViewItem)itemAtIndex:(unsigned)anIndex
00684 {
00685 return [_items objectAtIndex:anIndex];
00686 }
00687
00688 - (CGRect)frameForItemAtIndex:(unsigned)anIndex
00689 {
00690 return [[[self itemAtIndex:anIndex] view] frame];
00691 }
00692
00693 - (CGRect)frameForItemsAtIndexes:(CPIndexSet)anIndexSet
00694 {
00695 var indexArray = [],
00696 frame = CGRectNull;
00697
00698 [anIndexSet getIndexes:indexArray maxCount:-1 inIndexRange:nil];
00699
00700 var index = 0,
00701 count = [indexArray count];
00702
00703 for (; index < count; ++index)
00704 frame = CGRectUnion(frame, [self rectForItemAtIndex:indexArray[index]]);
00705
00706 return frame;
00707 }
00708
00709 @end
00710
00711 @implementation CPCollectionView (KeyboardInteraction)
00712
00713 - (void)_scrollToSelection
00714 {
00715 var frame = [self frameForItemsAtIndexes:[self selectionIndexes]];
00716
00717 if (!CGRectIsNull(frame))
00718 [self scrollRectToVisible:frame];
00719 }
00720
00721 - (void)moveLeft:(id)sender
00722 {
00723 var index = [[self selectionIndexes] firstIndex];
00724 if (index === CPNotFound)
00725 index = [[self items] count];
00726
00727 index = MAX(index - 1, 0);
00728
00729 [self setSelectionIndexes:[CPIndexSet indexSetWithIndex:index]];
00730 [self _scrollToSelection];
00731 }
00732
00733 - (void)moveRight:(id)sender
00734 {
00735 var index = MIN([[self selectionIndexes] firstIndex] + 1, [[self items] count]-1);
00736
00737 [self setSelectionIndexes:[CPIndexSet indexSetWithIndex:index]];
00738 [self _scrollToSelection];
00739 }
00740
00741 - (void)moveDown:(id)sender
00742 {
00743 var index = MIN([[self selectionIndexes] firstIndex] + [self numberOfColumns], [[self items] count]-1);
00744
00745 [self setSelectionIndexes:[CPIndexSet indexSetWithIndex:index]];
00746 [self _scrollToSelection];
00747 }
00748
00749 - (void)moveUp:(id)sender
00750 {
00751 var index = [[self selectionIndexes] firstIndex];
00752 if (index == CPNotFound)
00753 index = [[self items] count];
00754
00755 index = MAX(0, index - [self numberOfColumns]);
00756
00757 [self setSelectionIndexes:[CPIndexSet indexSetWithIndex:index]];
00758 [self _scrollToSelection];
00759 }
00760
00761 - (void)deleteBackward:(id)sender
00762 {
00763 if ([[self delegate] respondsToSelector:@selector(collectionView:shouldDeleteItemsAtIndexes:)])
00764 {
00765 [[self delegate] collectionView:self shouldDeleteItemsAtIndexes:[self selectionIndexes]];
00766
00767 var index = [[self selectionIndexes] firstIndex];
00768 if (index > [[self content] count]-1)
00769 [self setSelectionIndexes:[CPIndexSet indexSetWithIndex:[[self content] count]-1]];
00770
00771 [self _scrollToSelection];
00772 [self setNeedsDisplay:YES];
00773 }
00774 }
00775
00776 - (void)keyDown:(CPEvent)anEvent
00777 {
00778 [self interpretKeyEvents:[anEvent]];
00779 }
00780
00781 @end
00782
00783 @implementation CPCollectionView (Deprecated)
00784
00785 - (CGRect)rectForItemAtIndex:(int)anIndex
00786 {
00787 _CPReportLenientDeprecation([self class], _cmd, @selector(frameForItemAtIndex:));
00788
00789
00790
00791 return [self frameForItemAtIndex:anIndex];
00792 }
00793
00794 - (CGRect)rectForItemsAtIndexes:(CPIndexSet)anIndexSet
00795 {
00796 _CPReportLenientDeprecation([self class], _cmd, @selector(frameForItemsAtIndexes:));
00797
00798 return [self frameForItemsAtIndexes:anIndexSet];
00799 }
00800
00801 @end
00802
00803 var CPCollectionViewMinItemSizeKey = @"CPCollectionViewMinItemSizeKey",
00804 CPCollectionViewMaxItemSizeKey = @"CPCollectionViewMaxItemSizeKey",
00805 CPCollectionViewVerticalMarginKey = @"CPCollectionViewVerticalMarginKey",
00806 CPCollectionViewSelectableKey = @"CPCollectionViewSelectableKey",
00807 CPCollectionViewBackgroundColorsKey = @"CPCollectionViewBackgroundColorsKey";
00808
00809
00810 @implementation CPCollectionView (CPCoding)
00811
00812 - (id)initWithCoder:(CPCoder)aCoder
00813 {
00814 self = [super initWithCoder:aCoder];
00815
00816 if (self)
00817 {
00818 _items = [];
00819 _content = [];
00820
00821 _cachedItems = [];
00822
00823 _itemSize = CGSizeMakeZero();
00824
00825 _minItemSize = [aCoder decodeSizeForKey:CPCollectionViewMinItemSizeKey] || CGSizeMakeZero();
00826 _maxItemSize = [aCoder decodeSizeForKey:CPCollectionViewMaxItemSizeKey] || CGSizeMakeZero();
00827
00828 _verticalMargin = [aCoder decodeFloatForKey:CPCollectionViewVerticalMarginKey];
00829
00830 _isSelectable = [aCoder decodeBoolForKey:CPCollectionViewSelectableKey];
00831
00832 [self setBackgroundColors:[aCoder decodeObjectForKey:CPCollectionViewBackgroundColorsKey]];
00833
00834 _tileWidth = -1.0;
00835
00836 _selectionIndexes = [CPIndexSet indexSet];
00837
00838 _allowsEmptySelection = YES;
00839 }
00840
00841 return self;
00842 }
00843
00844 - (void)encodeWithCoder:(CPCoder)aCoder
00845 {
00846 [super encodeWithCoder:aCoder];
00847
00848 if (!CGSizeEqualToSize(_minItemSize, CGSizeMakeZero()))
00849 [aCoder encodeSize:_minItemSize forKey:CPCollectionViewMinItemSizeKey];
00850
00851 if (!CGSizeEqualToSize(_maxItemSize, CGSizeMakeZero()))
00852 [aCoder encodeSize:_maxItemSize forKey:CPCollectionViewMaxItemSizeKey];
00853
00854 [aCoder encodeBool:_isSelectable forKey:CPCollectionViewSelectableKey];
00855
00856 [aCoder encodeFloat:_verticalMargin forKey:CPCollectionViewVerticalMarginKey];
00857
00858 [aCoder encodeObject:_backgroundColors forKey:CPCollectionViewBackgroundColorsKey];
00859 }
00860
00861 @end