/* * CPCollectionView.j * AppKit * * Created by Francisco Tolmasky. * Copyright 2008, 280 North, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ import import import import import import /* This class presents an array of content as a grid of views. @delegate -(void)collectionViewDidChangeSelection:(CPCollectionView)collectionView; Called when the selection in the collection view has changed. @param collectionView the collection view who's selection changed @delegate -(void)collectionView:(CPCollectionView)collectionView didDoubleClickOnItemAtIndex:(int)index; Called when the user double-clicks on an item in the collection view. @param collectionView the collection view that received the double-click @param index the index of the item that received the double-click @delegate -(CPData)collectionView:(CPCollectionView)collectionView dataForItemsAtIndexes:(CPIndexSet)indices forType:(CPString)aType; Invoked to obtain data for a set of indices. @param collectionView the collection view to obtain data for @param indices the indices to return data for @param aType the data type @return a data object containing the index items @delegate -(CPArray)collectionView:(CPCollectionView)collectionView dragTypesForItemsAtIndexes:(CPIndexSet)indices; Invoked to obtain the data types supported by the specified indices for placement on the pasteboard. @param collectionView the collection view the items reside in @param indices the indices to obtain drag types @return an array of drag types (CPString) */ @implementation CPCollectionView : CPView { CPArray _content; CPArray _items; CPData _itemData; CPCollectionViewItem _itemPrototype; CPCollectionViewItem _itemForDragging; CPMutableArray _cachedItems; unsigned _maxNumberOfRows; unsigned _maxNumberOfColumns; CGSize _minItemSize; CGSize _maxItemSize; float _tileWidth; BOOL _isSelectable; BOOL _allowsMultipleSelection; CPIndexSet _selectionIndexes; CGSize _itemSize; float _horizontalMargin; float _verticalMargin; unsigned _numberOfRows; unsigned _numberOfColumns; id _delegate; } - (id)initWithFrame:(CGRect)aFrame { self = [super initWithFrame:aFrame]; if (self) { _items = []; _content = []; _cachedItems = []; _itemSize = CGSizeMakeZero(); _minItemSize = CGSizeMakeZero(); _maxItemSize = CGSizeMakeZero(); _verticalMargin = 5.0; _tileWidth = -1.0; _selectionIndexes = [CPIndexSet indexSet]; } return self; } /* Sets the item prototype to anItem @param anItem the new item prototype */ - (void)setItemPrototype:(CPCollectionViewItem)anItem { _itemData = [CPKeyedArchiver archivedDataWithRootObject:anItem]; _itemForDragging = anItem//[CPKeyedUnarchiver unarchiveObjectWithData:_itemData]; [self reloadContent]; } /* Returns the current item prototype */ - (CPCollectionViewItem)itemPrototype { return _itemPrototype; } /* Returns a collection view item for anObject. @param anObject the object to be represented. */ - (CPCollectionViewItem)newItemForRepresentedObject:(id)anObject { var item = nil; if (_cachedItems.length) item = _cachedItems.pop(); else item = [CPKeyedUnarchiver unarchiveObjectWithData:_itemData]; [item setRepresentedObject:anObject]; [[item view] setFrameSize:_itemSize]; return item; } // Working with the Responder Chain /* Returns YES by default. */ - (BOOL)acceptsFirstResponder { return YES; } /* Returns whether the receiver is currently the first responder. */ - (BOOL)isFirstResponder { return [[self window] firstResponder] == self; } // Setting the Content /* Sets the content of the collection view to the content in anArray. This array can be of any type, and each element will be passed to the setRepresentedObject: method. It's the responsibility of your custom collection view item to interpret the object. @param anArray the content array */ - (void)setContent:(CPArray)anArray { if (_content == anArray) return; _content = anArray; [self reloadContent]; } /* Returns the collection view content array */ - (CPArray)content { return _content; } /* Returns the collection view items. */ - (CPArray)items { return _items; } // Setting the Selection Mode /* Sets whether the user is allowed to select items @param isSelectable YES allows the user to select items. */ - (void)setSelectable:(BOOL)isSelectable { if (_isSelectable == isSelectable) return; _isSelectable = isSelectable; if (!_isSelectable) { var index = CPNotFound; while ((index = [_selectionIndexes indexGreaterThanIndex:index]) != CPNotFound) [_items[index] setSelected:NO]; } } /* Returns YES if the collection view is selected, and NO otherwise. */ - (BOOL)isSelected { return _isSelected; } /* Sets whether the user can select multiple items. @param shouldAllowMultipleSelection YES allows the user to select multiple items */ - (void)setAllowsMultipleSelection:(BOOL)shouldAllowMultipleSelection { _allowsMultipleSelection = shouldAllowMultipleSelection; } /* Returns YES if the user can select multiple items, NO otherwise. */ - (BOOL)allowsMultipleSelection { return _allowsMultipleSelection; } /* Sets the selected items based on the provided indices. @param anIndexSet the set of items to be selected */ - (void)setSelectionIndexes:(CPIndexSet)anIndexSet { if (_selectionIndexes == anIndexSet) return; var index = CPNotFound; while ((index = [_selectionIndexes indexGreaterThanIndex:index]) != CPNotFound) [_items[index] setSelected:NO]; _selectionIndexes = anIndexSet; var index = CPNotFound; while ((index = [_selectionIndexes indexGreaterThanIndex:index]) != CPNotFound) [_items[index] setSelected:YES]; if ([_delegate respondsToSelector:@selector(collectionViewDidChangeSelection:)]) [_delegate collectionViewDidChangeSelection:self] } /* Returns a set of the selected indices. */ - (CPIndexSet)selectionIndexes { return _selectionIndexes; } /* @ignore */ - (void)reloadContent { // Remove current views var count = _items.length; while (count--) { [[_items[count] view] removeFromSuperview]; _cachedItems.push(_items[count]); } _items = []; if (!_itemData || !_content) return; var index = 0; count = _content.length; for (; index < count; ++index) { _items.push([self newItemForRepresentedObject:_content[index]]); [self addSubview:[_items[index] view]]; } [self tile]; } /* @ignore */ - (void)tile { var width = CGRectGetWidth([self bounds]); if (![_content count] || width == _tileWidth) return; // We try to fit as many views per row as possible. Any remaining space is then // either proportioned out to the views (if their minSize != maxSize) or used as // margin var itemSize = CGSizeMakeCopy(_minItemSize); _numberOfColumns = MAX(1.0, FLOOR(width / itemSize.width)); if (_maxNumberOfColumns > 0) _numberOfColumns = MIN(_maxNumberOfColumns, _numberOfColumns); var remaining = width - _numberOfColumns * itemSize.width, itemsNeedSizeUpdate = NO; if (remaining > 0 && itemSize.width < _maxItemSize.width) itemSize.width = MIN(_maxItemSize.width, itemSize.width + FLOOR(remaining / _numberOfColumns)); if (!CGSizeEqualToSize(_itemSize, itemSize)) { _itemSize = itemSize; itemsNeedSizeUpdate = YES; } var index = 0, count = _items.length; if (_maxNumberOfColumns > 0 && _maxNumberOfRows > 0) count = MIN(count, _maxNumberOfColumns * _maxNumberOfRows); _numberOfRows = CEIL(count / _numberOfColumns); _horizontalMargin = FLOOR((width - _numberOfColumns * itemSize.width) / (_numberOfColumns + 1)); var x = _horizontalMargin, y = -itemSize.height; for (; index < count; ++index) { if (index % _numberOfColumns == 0) { x = _horizontalMargin; y += _verticalMargin + itemSize.height; } var view = [_items[index] view]; [view setFrameOrigin:CGPointMake(x, y)]; if (itemsNeedSizeUpdate) [view setFrameSize:_itemSize]; x += itemSize.width + _horizontalMargin; } _tileWidth = width; [self setFrameSize:CGSizeMake(width, y + itemSize.height + _verticalMargin)]; _tileWidth = -1.0; } - (void)resizeSubviewsWithOldSize:(CGSize)aSize { [self tile]; } // Laying Out the Collection View /* Sets the maximum number of rows. @param aMaxNumberOfRows the new maximum number of rows */ - (void)setMaxNumberOfRows:(unsigned)aMaxNumberOfRows { if (_maxNumberOfRows == aMaxNumberOfRows) return; _maxNumberOfRows = aMaxNumberOfRows; [self tile]; } /* Returns the maximum number of rows. */ - (unsigned)maxNumberOfRows { return _maxNumberOfRows; } /* Sets the maximum number of columns. @param aMaxNumberOfColumns the new maximum number of columns */ - (void)setMaxNumberOfColumns:(unsigned)aMaxNumberOfColumns { if (_maxNumberOfColumns == aMaxNumberOfColumns) return; _maxNumberOfColumns = aMaxNumberOfColumns; [self tile]; } /* Returns the maximum number of columns */ - (unsigned)maxNumberOfColumns { return _maxNumberOfColumns; } /* Sets the minimum size for an item @param aSize the new minimum item size */ - (void)setMinItemSize:(CGSize)aSize { if (CGSizeEqualToSize(_minItemSize, aSize)) return; _minItemSize = CGSizeMakeCopy(aSize); [self tile]; } /* Returns the current minimum item size */ - (CGSize)minItemSize { return _minItemSize; } /* Sets the maximum item size. @param aSize the new maximum item size */ - (void)setMaxItemSize:(CGSize)aSize { if (CGSizeEqualToSize(_maxItemSize, aSize)) return; _maxItemSize = CGSizeMakeCopy(aSize); [self tile]; } /* Returns the current maximum item size. */ - (CGSize)maxItemSize { return _maxItemSize; } - (void)mouseUp:(CPEvent)anEvent { if ([_selectionIndexes count] && [anEvent clickCount] == 2 && [_delegate respondsToSelector:@selector(collectionView:didDoubleClickOnItemAtIndex:)]) [_delegate collectionView:self didDoubleClickOnItemAtIndex:[_selectionIndexes firstIndex]]; } - (void)mouseDown:(CPEvent)anEvent { var location = [self convertPoint:[anEvent locationInWindow] fromView:nil], row = FLOOR(location.y / (_itemSize.height + _verticalMargin)), column = FLOOR(location.x / (_itemSize.width + _horizontalMargin)), index = row * _numberOfColumns + column; if (index >= 0 && index < _items.length) [self setSelectionIndexes:[CPIndexSet indexSetWithIndex:index]]; } - (void)mouseDragged:(CPEvent)anEvent { if (![_delegate respondsToSelector:@selector(collectionView:dragTypesForItemsAtIndexes:)]) return; // If we don't have any selected items, we've clicked away, and thus the drag is meaningless. if (![_selectionIndexes count]) return; // Set up the pasteboard var dragTypes = [_delegate collectionView:self dragTypesForItemsAtIndexes:_selectionIndexes]; [[CPPasteboard pasteboardWithName:CPDragPboard] declareTypes:dragTypes owner:self]; var point = [self convertPoint:[anEvent locationInWindow] fromView:nil]; [_itemForDragging setRepresentedObject:_content[[_selectionIndexes firstIndex]]]; var view = [_itemForDragging view], frame = [view frame]; [view setFrameSize:_itemSize]; [self dragView:view at:[[_items[[_selectionIndexes firstIndex]] view] frame].origin offset:CGPointMakeZero() event:anEvent pasteboard:nil source:self slideBack:YES]; } /* Places the selected items on the specified pasteboard. The items are requested from the collection's delegate. @param aPasteboard the pasteboard to put the items on @param aType the format the pasteboard data */ - (void)pasteboard:(CPPasteboard)aPasteboard provideDataForType:(CPString)aType { [aPasteboard setData:[_delegate collectionView:self dataForItemsAtIndexes:_selectionIndexes forType:aType] forType:aType]; } // Cappuccino Additions - (void)setVerticalMargin:(float)aVerticalMargin { if (_verticalMargin == aVerticalMargin) return; _verticalMargin = aVerticalMargin; [self tile]; } /* Sets the collection view's delegate @param aDelegate the new delegate */ - (void)setDelegate:(id)aDelegate { _delegate = aDelegate; } /* Returns the collection view's delegate */ - (id)delegate { return _delegate; } @end /* Represents an object inside a CPCollectionView. */ @implementation CPCollectionViewItem : CPObject { id _representedObject; CPView _view; BOOL _isSelected; } // Setting the Represented Object /* Sets the object to be represented by this item. @param anObject the object to be represented */ - (void)setRepresentedObject:(id)anObject { if (_representedObject == anObject) return; _representedObject = anObject; // FIXME: This should be set up by bindings [_view setRepresentedObject:anObject]; } /* Returns the object represented by this view item */ - (id)representedObject { return _representedObject; } // Modifying the View /* Sets the view that is used represent this object. @param aView the view used to represent this object */ - (void)setView:(CPView)aView { _view = aView; } /* Returns the view that represents this object. */ - (CPView)view { return _view; } // Modifying the Selection /* Sets whether this view item should be selected. @param shouldBeSelected YES makes the item selected. NO deselects it. */ - (void)setSelected:(BOOL)shouldBeSelected { if (_isSelected == shouldBeSelected) return; _isSelected = shouldBeSelected; // FIXME: This should be set up by bindings [_view setSelected:_isSelected]; } /* Returns YES if the item is currently selected. NO if the item is not selected. */ - (BOOL)isSelected { return _isSelected; } // Parent Collection View /* Returns the collection view of which this item is a part. */ - (CPCollectionView)collectionView { return [_view superview]; } @end var CPCollectionViewItemViewKey = @"CPCollectionViewItemViewKey"; @implementation CPCollectionViewItem (CPCoding) /* FIXME Not yet implemented */ - (id)copy { } @end var CPCollectionViewItemViewKey = @"CPCollectionViewItemViewKey"; @implementation CPCollectionViewItem (CPCoding) /* Initializes the view item by unarchiving data from a coder. @param aCoder the coder from which the data will be unarchived @return the initialized collection view item */ - (id)initWithCoder:(CPCoder)aCoder { self = [super init]; if (self) _view = [aCoder decodeObjectForKey:CPCollectionViewItemViewKey]; return self; } /* Archives the colletion view item to the provided coder. @param aCoder the coder to which the view item should be archived */ - (void)encodeWithCoder:(CPCoder)aCoder { [aCoder encodeObject:_view forKey:CPCollectionViewItemViewKey]; } @end