├── .gitignore ├── BCCollectionView+Dragging.h ├── BCCollectionView+Dragging.m ├── BCCollectionView+Keyboard.h ├── BCCollectionView+Keyboard.m ├── BCCollectionView+Mouse.h ├── BCCollectionView+Mouse.m ├── BCCollectionView+Zoom.h ├── BCCollectionView+Zoom.m ├── BCCollectionView.h ├── BCCollectionView.m ├── BCCollectionViewDelegate.h ├── BCCollectionViewGroup.h ├── BCCollectionViewGroup.m ├── BCCollectionViewLayoutItem.h ├── BCCollectionViewLayoutItem.m ├── BCCollectionViewLayoutManager.h ├── BCCollectionViewLayoutManager.m ├── BCCollectionViewLayoutOperation.h ├── BCCollectionViewLayoutOperation.m ├── BCGeometryExtensions.h └── readme.txt /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /BCCollectionView+Dragging.h: -------------------------------------------------------------------------------- 1 | // Created by Pieter Omvlee on 13/12/2010. 2 | // Copyright 2010 Bohemian Coding. All rights reserved. 3 | 4 | #import "BCCollectionView.h" 5 | 6 | @interface BCCollectionView (BCCollectionView_Dragging) 7 | - (void)initiateDraggingSessionWithEvent:(NSEvent *)anEvent; 8 | 9 | //delegate shortcuts 10 | - (BOOL)delegateSupportsDragForItemsAtIndexes:(NSIndexSet *)indexSet; 11 | - (void)delegateWriteIndexes:(NSIndexSet *)indexSet toPasteboard:(NSPasteboard *)pasteboard; 12 | - (BOOL)delegateCanDrop:(id)draggingInfo onIndex:(NSUInteger)index; 13 | - (void)setDragHoverIndex:(NSInteger)hoverIndex; 14 | @end 15 | -------------------------------------------------------------------------------- /BCCollectionView+Dragging.m: -------------------------------------------------------------------------------- 1 | // Created by Pieter Omvlee on 13/12/2010. 2 | // Copyright 2010 Bohemian Coding. All rights reserved. 3 | 4 | #import "BCCollectionView+Dragging.h" 5 | #import "BCCollectionViewLayoutManager.h" 6 | 7 | @implementation BCCollectionView (BCCollectionView_Dragging) 8 | 9 | - (void)initiateDraggingSessionWithEvent:(NSEvent *)anEvent 10 | { 11 | NSUInteger index = [layoutManager indexOfItemAtPoint:mouseDownLocation]; 12 | [self selectItemAtIndex:index]; 13 | 14 | NSRect itemRect = [layoutManager rectOfItemAtIndex:index]; 15 | NSView *currentView = [[self viewControllerForItemAtIndex:index] view]; 16 | NSData *imageData = [currentView dataWithPDFInsideRect:NSMakeRect(0,0,NSWidth(itemRect),NSHeight(itemRect))]; 17 | NSImage *pdfImage = [[[NSImage alloc] initWithData:imageData] autorelease]; 18 | NSImage *dragImage = [[NSImage alloc] initWithSize:[pdfImage size]]; 19 | 20 | if ([dragImage size].width > 0 && [dragImage size].height > 0) { 21 | [dragImage lockFocus]; 22 | [pdfImage drawAtPoint:NSZeroPoint fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:0.5]; 23 | [dragImage unlockFocus]; 24 | } 25 | 26 | NSPasteboard *pasteboard = [NSPasteboard pasteboardWithName:NSDragPboard]; 27 | [self delegateWriteIndexes:selectionIndexes toPasteboard:pasteboard]; 28 | [self retain]; 29 | [self dragImage:[dragImage autorelease] 30 | at:NSMakePoint(NSMinX(itemRect), NSMaxY(itemRect)) 31 | offset:NSMakeSize(0, 0) 32 | event:anEvent 33 | pasteboard:pasteboard 34 | source:self 35 | slideBack:YES]; 36 | } 37 | 38 | - (void)draggedImage:(NSImage *)anImage beganAt:(NSPoint)aPoint 39 | { 40 | [self retain]; 41 | } 42 | 43 | - (void)draggedImage:(NSImage *)anImage endedAt:(NSPoint)aPoint operation:(NSDragOperation)operation 44 | { 45 | [self autorelease]; 46 | } 47 | 48 | - (NSDragOperation)draggingEntered:(id)sender 49 | { 50 | if ([delegate respondsToSelector:@selector(collectionView:draggingEntered:)]) 51 | return [delegate collectionView:self draggingEntered:sender]; 52 | else 53 | return [self draggingUpdated:sender]; 54 | } 55 | 56 | - (NSDragOperation)draggingUpdated:(id )sender 57 | { 58 | if (dragHoverIndex != NSNotFound) 59 | [self setNeedsDisplayInRect:[layoutManager rectOfItemAtIndex:dragHoverIndex]]; 60 | 61 | NSPoint mouse = [self convertPoint:[sender draggingLocation] fromView:nil]; 62 | NSUInteger index = [layoutManager indexOfItemAtPoint:mouse]; 63 | 64 | NSDragOperation operation = NSDragOperationNone; 65 | if ([sender draggingSource] == self) { 66 | if ([selectionIndexes containsIndex:index]) 67 | [self setDragHoverIndex:NSNotFound]; 68 | else if ([self delegateCanDrop:sender onIndex:index]) { 69 | [self setDragHoverIndex:index]; 70 | operation = NSDragOperationMove; 71 | } else 72 | [self setDragHoverIndex:NSNotFound]; 73 | } else { 74 | if ([self delegateCanDrop:sender onIndex:index]) { 75 | [self setDragHoverIndex:index]; 76 | operation = NSDragOperationCopy; 77 | } 78 | } 79 | 80 | if (dragHoverIndex != NSNotFound) 81 | [self setNeedsDisplayInRect:[layoutManager rectOfItemAtIndex:dragHoverIndex]]; 82 | 83 | return operation; 84 | } 85 | 86 | - (void)draggingEnded:(id )sender 87 | { 88 | if (dragHoverIndex != NSNotFound) 89 | [self setNeedsDisplayInRect:[layoutManager rectOfItemAtIndex:dragHoverIndex]]; 90 | 91 | [self setDragHoverIndex:NSNotFound]; 92 | 93 | if ([delegate respondsToSelector:@selector(collectionView:draggingEnded:)]) 94 | [delegate collectionView:self draggingEnded:sender]; 95 | } 96 | 97 | - (void)draggingExited:(id)sender 98 | { 99 | NSPoint mouse = [self convertPoint:[sender draggingLocation] fromView:nil]; 100 | NSUInteger index = [layoutManager indexOfItemAtPoint:mouse]; 101 | 102 | if (index == NSNotFound) { 103 | [self setNeedsDisplayInRect:[layoutManager rectOfItemAtIndex:dragHoverIndex]]; 104 | [self setDragHoverIndex:NSNotFound]; 105 | 106 | if ([delegate respondsToSelector:@selector(collectionView:draggingExited:)]) 107 | [delegate collectionView:self draggingExited:sender]; 108 | } 109 | } 110 | 111 | - (BOOL)performDragOperation:(id )sender 112 | { 113 | id item = nil; 114 | if (dragHoverIndex >= 0 && dragHoverIndex <[contentArray count]) 115 | item = [contentArray objectAtIndex:dragHoverIndex]; 116 | 117 | if ([delegate respondsToSelector:@selector(collectionView:performDragOperation:onViewController:forItem:)]) 118 | return [delegate collectionView:self performDragOperation:sender onViewController:[self viewControllerForItemAtIndex:dragHoverIndex] forItem:item]; 119 | else 120 | return NO; 121 | } 122 | 123 | #pragma mark - 124 | #pragma mark Delegate Shortcuts 125 | 126 | - (void)setDragHoverIndex:(NSInteger)hoverIndex 127 | { 128 | if (hoverIndex != dragHoverIndex) { 129 | if (dragHoverIndex != NSNotFound) 130 | [self setNeedsDisplayInRect:[layoutManager rectOfItemAtIndex:dragHoverIndex]]; 131 | 132 | if ([delegate respondsToSelector:@selector(collectionView:dragExitedViewController:)]) 133 | [delegate collectionView:self dragExitedViewController:[self viewControllerForItemAtIndex:dragHoverIndex]]; 134 | 135 | dragHoverIndex = hoverIndex; 136 | 137 | if ([delegate respondsToSelector:@selector(collectionView:dragEnteredViewController:)]) 138 | [delegate collectionView:self dragEnteredViewController:[self viewControllerForItemAtIndex:dragHoverIndex]]; 139 | 140 | if (dragHoverIndex != NSNotFound) 141 | [self setNeedsDisplayInRect:[layoutManager rectOfItemAtIndex:dragHoverIndex]]; 142 | } 143 | } 144 | 145 | - (BOOL)delegateSupportsDragForItemsAtIndexes:(NSIndexSet *)indexSet 146 | { 147 | if ([delegate respondsToSelector:@selector(collectionView:canDragItemsAtIndexes:)]) 148 | return [delegate collectionView:self canDragItemsAtIndexes:indexSet]; 149 | return NO; 150 | } 151 | 152 | - (void)delegateWriteIndexes:(NSIndexSet *)indexSet toPasteboard:(NSPasteboard *)pasteboard 153 | { 154 | if ([delegate respondsToSelector:@selector(collectionView:writeItemsAtIndexes:toPasteboard:)]) 155 | [delegate collectionView:self writeItemsAtIndexes:indexSet toPasteboard:pasteboard]; 156 | } 157 | 158 | - (BOOL)delegateCanDrop:(id)draggingInfo onIndex:(NSUInteger)index 159 | { 160 | if ([delegate respondsToSelector:@selector(collectionView:validateDrop:onItemAtIndex:)]) 161 | return [delegate collectionView:self validateDrop:draggingInfo onItemAtIndex:index]; 162 | else 163 | return NO; 164 | } 165 | 166 | @end 167 | -------------------------------------------------------------------------------- /BCCollectionView+Keyboard.h: -------------------------------------------------------------------------------- 1 | // Created by Pieter Omvlee on 25/11/2010. 2 | // Copyright 2010 Bohemian Coding. All rights reserved. 3 | 4 | #import "BCCollectionView.h" 5 | 6 | @interface BCCollectionView (BCCollectionView_Keyboard) 7 | - (void)keyDown:(NSEvent *)theEvent; 8 | 9 | - (void)moveLeft:(id)sender; 10 | - (void)moveLeftAndModifySelection:(id)sender; 11 | 12 | - (void)moveRight:(id)sender; 13 | - (void)moveRightAndModifySelection:(id)sender; 14 | 15 | - (void)moveUp:(id)sender; 16 | - (void)moveUpAndModifySelection:(id)sender; 17 | 18 | - (void)moveDown:(id)sender; 19 | - (void)moveDownAndModifySelection:(id)sender; 20 | @end 21 | 22 | @interface NSIndexSet (BCCollectionView_IndexSet) 23 | - (NSIndexSet *)indexSetByRemovingIndex:(NSUInteger)index; 24 | @end -------------------------------------------------------------------------------- /BCCollectionView+Keyboard.m: -------------------------------------------------------------------------------- 1 | // Created by Pieter Omvlee on 25/11/2010. 2 | // Copyright 2010 Bohemian Coding. All rights reserved. 3 | 4 | #import "BCCollectionView+Keyboard.h" 5 | #import "BCCollectionViewLayoutManager.h" 6 | #import "BCCollectionViewLayoutItem.h" 7 | 8 | @implementation BCCollectionView (BCCollectionView_Keyboard) 9 | 10 | - (void)keyDown:(NSEvent *)theEvent 11 | { 12 | [self interpretKeyEvents:[NSArray arrayWithObject:theEvent]]; 13 | } 14 | 15 | - (void)clearAccumulatedBuffer 16 | { 17 | self.accumulatedKeyStrokes = @""; 18 | } 19 | 20 | - (void)insertText:(id)aString 21 | { 22 | if ([delegate respondsToSelector:@selector(collectionView:nameOfItem:startsWith:)]) { 23 | [[NSRunLoop currentRunLoop] cancelPerformSelector:@selector(clearAccumulatedBuffer) target:self argument:nil]; 24 | [self performSelector:@selector(clearAccumulatedBuffer) withObject:nil afterDelay:1.0]; 25 | 26 | self.accumulatedKeyStrokes = [[accumulatedKeyStrokes stringByAppendingString:aString] lowercaseString]; 27 | 28 | NSInteger firstIndex = [contentArray indexOfObjectWithOptions:NSEnumerationConcurrent passingTest:^BOOL(id obj, NSUInteger idx, BOOL *stop) { 29 | return [delegate collectionView:self nameOfItem:obj startsWith:accumulatedKeyStrokes]; 30 | }]; 31 | if (firstIndex != NSNotFound) { 32 | [self deselectAllItems]; 33 | [self selectItemAtIndex:firstIndex]; 34 | 35 | if (NSHeight([self frame]) > NSHeight([self visibleRect])) { 36 | NSScrollView *scrollView = [self enclosingScrollView]; 37 | NSClipView *clipView = [[self enclosingScrollView] contentView]; 38 | 39 | [clipView scrollToPoint:NSMakePoint(0, MIN(NSHeight([self frame])-NSHeight([self visibleRect]),[layoutManager rectOfItemAtIndex:firstIndex].origin.y))]; 40 | [scrollView reflectScrolledClipView:clipView]; 41 | } 42 | } 43 | } 44 | } 45 | 46 | #pragma mark Helper Methods 47 | 48 | - (void)simpleSelectItemAtIndex:(NSUInteger)anIndex 49 | { 50 | if (anIndex != NSNotFound) { 51 | [self deselectAllItems]; 52 | [self selectItemAtIndex:anIndex]; 53 | [self scrollRectToVisible:[layoutManager rectOfItemAtIndex:anIndex]]; 54 | } 55 | } 56 | 57 | - (void)simpleExtendSelectionRange:(NSRange)range newIndex:(NSUInteger)newIndex 58 | { 59 | if (newIndex != NSNotFound) { 60 | if ([selectionIndexes containsIndex:newIndex]) 61 | [self deselectItemsAtIndexes:[[NSIndexSet indexSetWithIndexesInRange:range] indexSetByRemovingIndex:newIndex]]; 62 | else 63 | [self selectItemsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:range]]; 64 | lastSelectionIndex = newIndex; 65 | [self scrollRectToVisible:[layoutManager rectOfItemAtIndex:newIndex]]; 66 | } 67 | } 68 | 69 | #pragma mark Arrow Keys 70 | 71 | - (void)moveLeft:(id)sender 72 | { 73 | if (lastSelectionIndex > 0) 74 | [self simpleSelectItemAtIndex:lastSelectionIndex-1]; 75 | } 76 | 77 | - (void)moveLeftAndModifySelection:(id)sender 78 | { 79 | if (lastSelectionIndex > 0) { 80 | NSUInteger newIndex = MAX(0, lastSelectionIndex-1); 81 | [self simpleExtendSelectionRange:NSMakeRange(newIndex, 2) newIndex:newIndex]; 82 | } 83 | } 84 | 85 | - (void)moveRight:(id)sender 86 | { 87 | [self simpleSelectItemAtIndex:MIN([[self contentArray] count]-1, lastSelectionIndex+1)]; 88 | } 89 | 90 | - (void)moveRightAndModifySelection:(id)sender 91 | { 92 | NSUInteger newIndex = MIN([[self contentArray] count]-1, lastSelectionIndex+1); 93 | [self simpleExtendSelectionRange:NSMakeRange(lastSelectionIndex, 2) newIndex:newIndex]; 94 | } 95 | 96 | - (void)moveUp:(id)sender 97 | { 98 | NSPoint position = [layoutManager rowAndColumnPositionOfItemAtIndex:lastSelectionIndex]; 99 | [self simpleSelectItemAtIndex:[layoutManager indexOfItemAtRow:position.y-1 column:position.x]]; 100 | } 101 | 102 | - (void)moveUpAndModifySelection:(id)sender 103 | { 104 | NSPoint position = [layoutManager rowAndColumnPositionOfItemAtIndex:lastSelectionIndex]; 105 | NSUInteger newIndex = [layoutManager indexOfItemAtRow:position.y-1 column:position.x]; 106 | if (newIndex == NSNotFound) 107 | newIndex = 0; 108 | 109 | NSRange range = NSMakeRange(newIndex, lastSelectionIndex-newIndex); 110 | if ([selectionIndexes containsIndex:newIndex]) 111 | range.location++; 112 | 113 | [self simpleExtendSelectionRange:range newIndex:newIndex]; 114 | } 115 | 116 | - (void)moveDown:(id)sender 117 | { 118 | NSPoint position = [layoutManager rowAndColumnPositionOfItemAtIndex:lastSelectionIndex]; 119 | [self simpleSelectItemAtIndex:[layoutManager indexOfItemAtRow:position.y+1 column:position.x]]; 120 | } 121 | 122 | - (void)moveDownAndModifySelection:(id)sender 123 | { 124 | NSPoint position = [layoutManager rowAndColumnPositionOfItemAtIndex:lastSelectionIndex]; 125 | NSUInteger newIndex = [layoutManager indexOfItemAtRow:position.y+1 column:position.x]; 126 | if (newIndex == NSNotFound) 127 | newIndex = [contentArray count]-1; 128 | 129 | NSRange range = NSMakeRange(lastSelectionIndex, newIndex-lastSelectionIndex); 130 | if (![selectionIndexes containsIndex:newIndex]) 131 | range.length++; 132 | 133 | [self simpleExtendSelectionRange:range newIndex:newIndex]; 134 | } 135 | 136 | #pragma mark Deleting 137 | 138 | - (void)deleteBackward:(id)sender 139 | { 140 | if ([delegate respondsToSelector:@selector(collectionView:deleteItemsAtIndexes:)]) 141 | [delegate collectionView:self deleteItemsAtIndexes:selectionIndexes]; 142 | } 143 | 144 | - (void)deleteForward:(id)sender 145 | { 146 | [self deleteBackward:sender]; 147 | } 148 | 149 | @end 150 | 151 | @implementation NSIndexSet (BCCollectionView_IndexSet) 152 | - (NSIndexSet *)indexSetByRemovingIndex:(NSUInteger)index 153 | { 154 | return [self indexesPassingTest:^BOOL(NSUInteger idx, BOOL *stop) { 155 | return index != idx; 156 | }]; 157 | } 158 | @end -------------------------------------------------------------------------------- /BCCollectionView+Mouse.h: -------------------------------------------------------------------------------- 1 | // Created by Pieter Omvlee on 25/11/2010. 2 | // Copyright 2010 Bohemian Coding. All rights reserved. 3 | 4 | #import "BCCollectionView.h" 5 | 6 | @interface BCCollectionView (BCCollectionView_Mouse) 7 | - (void)mouseDown:(NSEvent *)theEvent; 8 | - (void)mouseDragged:(NSEvent *)theEvent; 9 | - (void)mouseUp:(NSEvent *)theEvent; 10 | 11 | - (BOOL)shiftOrCommandKeyPressed; 12 | @end 13 | -------------------------------------------------------------------------------- /BCCollectionView+Mouse.m: -------------------------------------------------------------------------------- 1 | // Created by Pieter Omvlee on 25/11/2010. 2 | // Copyright 2010 Bohemian Coding. All rights reserved. 3 | 4 | #import "BCCollectionView+Mouse.h" 5 | #import "BCGeometryExtensions.h" 6 | #import "BCCollectionView+Dragging.h" 7 | #import "BCCollectionViewLayoutManager.h" 8 | 9 | @implementation BCCollectionView (BCCollectionView_Mouse) 10 | 11 | - (BOOL)shiftOrCommandKeyPressed 12 | { 13 | return [NSEvent modifierFlags] & NSShiftKeyMask || [NSEvent modifierFlags] & NSCommandKeyMask; 14 | } 15 | 16 | - (void)mouseDown:(NSEvent *)theEvent 17 | { 18 | [[self window] makeFirstResponder:self]; 19 | 20 | isDragging = YES; 21 | mouseDownLocation = [self convertPoint:[theEvent locationInWindow] fromView:nil]; 22 | mouseDraggedLocation = mouseDownLocation; 23 | NSUInteger index = [layoutManager indexOfItemContentRectAtPoint:mouseDownLocation]; 24 | 25 | if (index != NSNotFound && [delegate respondsToSelector:@selector(collectionView:didClickItem:withViewController:)]) 26 | [delegate collectionView:self didClickItem:[contentArray objectAtIndex:index] withViewController:[visibleViewControllers objectForKey:[NSNumber numberWithInt:index]]]; 27 | 28 | if (![self shiftOrCommandKeyPressed] && ![selectionIndexes containsIndex:index]) 29 | [self deselectAllItems]; 30 | 31 | self.originalSelectionIndexes = [[selectionIndexes copy] autorelease]; 32 | 33 | if ([theEvent type] == NSLeftMouseDown && [theEvent clickCount] == 2 && [delegate respondsToSelector:@selector(collectionView:didDoubleClickViewControllerAtIndex:)]) 34 | [delegate collectionView:self didDoubleClickViewControllerAtIndex:[visibleViewControllers objectForKey:[NSNumber numberWithInt:index]]]; 35 | 36 | if ([self shiftOrCommandKeyPressed] && [self.originalSelectionIndexes containsIndex:index]) 37 | [self deselectItemAtIndex:index]; 38 | else { 39 | if ([NSEvent modifierFlags] & NSCommandKeyMask || [[self originalSelectionIndexes] count] == 0) 40 | [self selectItemAtIndex:index]; 41 | else if ([NSEvent modifierFlags] & NSShiftKeyMask) { 42 | NSInteger one = [[self originalSelectionIndexes] lastIndex]; 43 | NSInteger two = index; 44 | 45 | if (index == NSNotFound) 46 | return; 47 | 48 | if (two > one) 49 | [self selectItemsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(MIN(one,two), 1+MAX(one,two)-MIN(one,two))]]; 50 | else 51 | [self selectItemsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(MIN(one,two), MAX(one,two)-MIN(one,two))]]; 52 | } 53 | } 54 | } 55 | 56 | - (void)regularMouseDragged:(NSEvent *)anEvent 57 | { 58 | NSIndexSet *originalSet = [[self selectionIndexes] copy]; 59 | selectionChangedDisabled = YES; 60 | 61 | [self deselectAllItems]; 62 | if ([self shiftOrCommandKeyPressed]) { 63 | [self.originalSelectionIndexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { 64 | [self selectItemAtIndex:idx]; 65 | }]; 66 | } 67 | [self setNeedsDisplayInRect:BCRectFromTwoPoints(mouseDownLocation, mouseDraggedLocation)]; 68 | 69 | mouseDraggedLocation = [self convertPoint:[anEvent locationInWindow] fromView:nil]; 70 | NSIndexSet *suggestedIndexes = [self indexesOfItemContentRectsInRect:BCRectFromTwoPoints(mouseDownLocation, mouseDraggedLocation)]; 71 | 72 | if (![self shiftOrCommandKeyPressed]) { 73 | NSMutableIndexSet *oldIndexes = [[selectionIndexes mutableCopy] autorelease]; 74 | [oldIndexes removeIndexes:suggestedIndexes]; 75 | [self deselectItemsAtIndexes:oldIndexes]; 76 | } 77 | 78 | [suggestedIndexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop){ 79 | if ([self shiftOrCommandKeyPressed]) { 80 | if ([originalSelectionIndexes containsIndex:idx]) 81 | [self deselectItemAtIndex:idx]; 82 | else 83 | [self selectItemAtIndex:idx]; 84 | } else 85 | [self selectItemAtIndex:idx]; 86 | }]; 87 | 88 | [self setNeedsDisplayInRect:BCRectFromTwoPoints(mouseDownLocation, mouseDraggedLocation)]; 89 | 90 | selectionChangedDisabled = NO; 91 | if (![selectionIndexes isEqual:originalSet]) 92 | [self performSelector:@selector(delegateCollectionViewSelectionDidChange)]; 93 | } 94 | 95 | - (void)mouseDragged:(NSEvent *)anEvent 96 | { 97 | [self autoscroll:anEvent]; 98 | 99 | if (isDragging) { 100 | NSUInteger index = [layoutManager indexOfItemContentRectAtPoint:mouseDownLocation]; 101 | if (index != NSNotFound && [selectionIndexes count] > 0 && [self delegateSupportsDragForItemsAtIndexes:selectionIndexes]) { 102 | NSPoint mouse = [self convertPoint:[anEvent locationInWindow] fromView:nil]; 103 | CGFloat distance = sqrt(pow(mouse.x-mouseDownLocation.x,2)+pow(mouse.y-mouseDownLocation.y,2)); 104 | if (distance > 3) 105 | [self initiateDraggingSessionWithEvent:anEvent]; 106 | } else 107 | [self regularMouseDragged:anEvent]; 108 | } 109 | } 110 | 111 | - (void)mouseUp:(NSEvent *)theEvent 112 | { 113 | [self setNeedsDisplayInRect:BCRectFromTwoPoints(mouseDownLocation, mouseDraggedLocation)]; 114 | 115 | mouseDownLocation = NSZeroPoint; 116 | mouseDraggedLocation = NSZeroPoint; 117 | 118 | isDragging = NO; 119 | self.originalSelectionIndexes = nil; 120 | } 121 | 122 | @end 123 | -------------------------------------------------------------------------------- /BCCollectionView+Zoom.h: -------------------------------------------------------------------------------- 1 | // Created by Pieter Omvlee on 02/02/2011. 2 | // Copyright 2011 Bohemian Coding. All rights reserved. 3 | 4 | #import 5 | #import "BCCollectionView.h" 6 | 7 | @interface BCCollectionView (BCCollectionView_Zoom) 8 | - (void)registerForZoomValueChangesInDefaultsForKey:(NSString *)key; 9 | @end 10 | -------------------------------------------------------------------------------- /BCCollectionView+Zoom.m: -------------------------------------------------------------------------------- 1 | // Created by Pieter Omvlee on 02/02/2011. 2 | // Copyright 2011 Bohemian Coding. All rights reserved. 3 | 4 | #import "BCCollectionView+Zoom.h" 5 | #import "BCCollectionViewLayoutManager.h" 6 | 7 | @interface BCCollectionView () 8 | - (void)removeInvisibleViewControllers; 9 | - (void)addMissingViewControllersToView; 10 | @end 11 | 12 | @implementation BCCollectionView (BCCollectionView_Zoom) 13 | 14 | - (void)registerForZoomValueChangesInDefaultsForKey:(NSString *)key 15 | { 16 | self.zoomValueObserverKey = key; 17 | [[NSUserDefaults standardUserDefaults] addObserver:self forKeyPath:key options:0 context:NULL]; 18 | } 19 | 20 | - (void)zoomValueDidChange 21 | { 22 | [self softReloadDataWithCompletionBlock:^{ 23 | if ([delegate respondsToSelector:@selector(collectionViewDidZoom:)]) 24 | [delegate collectionViewDidZoom:self]; 25 | }]; 26 | } 27 | 28 | - (void)beginGestureWithEvent:(NSEvent *)event 29 | { 30 | [self setAcceptsTouchEvents:YES]; 31 | } 32 | 33 | - (void)endGestureWithEvent:(NSEvent *)event 34 | { 35 | [self setAcceptsTouchEvents:NO]; 36 | } 37 | 38 | - (void)touchesMovedWithEvent:(NSEvent *)event 39 | { 40 | if (!zoomValueObserverKey) 41 | return; 42 | 43 | if (lastPinchMagnification != 0.0) 44 | [self magnifyWithEvent:event]; 45 | } 46 | 47 | - (void)magnifyWithEvent:(NSEvent *)event 48 | { 49 | if (!zoomValueObserverKey) 50 | return; 51 | 52 | CGFloat magnification = [event type] == NSEventTypeMagnify ? [event magnification] : lastPinchMagnification; 53 | 54 | CGFloat zoomValue = [[NSUserDefaults standardUserDefaults] integerForKey:zoomValueObserverKey]; 55 | zoomValue = zoomValue * (magnification+1); 56 | 57 | NSRange scalingRange = [delegate validScalingRangeForCollectionView:self]; 58 | zoomValue = MAX(MIN(zoomValue, scalingRange.location + scalingRange.length), scalingRange.location); 59 | [[NSUserDefaults standardUserDefaults] setInteger:zoomValue forKey:zoomValueObserverKey]; 60 | 61 | [self zoomValueDidChange]; 62 | [[[self enclosingScrollView] contentView] autoscroll:event]; 63 | 64 | lastPinchMagnification = magnification; 65 | } 66 | 67 | - (void)touchesEndedWithEvent:(NSEvent *)event 68 | { 69 | lastPinchMagnification = 0.0; 70 | } 71 | 72 | @end 73 | -------------------------------------------------------------------------------- /BCCollectionView.h: -------------------------------------------------------------------------------- 1 | // Created by Pieter Omvlee on 24/11/2010. 2 | // Copyright 2010 Bohemian Coding. All rights reserved. 3 | 4 | #import 5 | #import "BCCollectionViewDelegate.h" 6 | 7 | #ifndef BCArray 8 | #define BCArray(args...) [NSArray arrayWithObjects:args, nil] 9 | #endif 10 | 11 | @class BCCollectionViewLayoutManager; 12 | @interface BCCollectionView : NSView 13 | { 14 | IBOutlet id delegate; 15 | BCCollectionViewLayoutManager *layoutManager; 16 | 17 | NSArray *contentArray; 18 | NSArray *groups; 19 | 20 | NSMutableArray *reusableViewControllers; 21 | NSMutableDictionary *visibleViewControllers; 22 | NSMutableIndexSet *selectionIndexes; 23 | NSMutableDictionary *visibleGroupViewControllers; 24 | 25 | NSColor *backgroundColor; 26 | NSUInteger numberOfPreRenderedRows; 27 | 28 | @private 29 | NSPoint mouseDownLocation; 30 | NSPoint mouseDraggedLocation; 31 | NSRect previousFrameBounds; 32 | 33 | NSUInteger lastSelectionIndex; 34 | NSIndexSet *originalSelectionIndexes; 35 | NSInteger dragHoverIndex; 36 | 37 | BOOL isDragging; 38 | BOOL firstDrag; 39 | BOOL selectionChangedDisabled; 40 | 41 | NSString *zoomValueObserverKey; 42 | CGFloat lastPinchMagnification; 43 | 44 | NSString *accumulatedKeyStrokes; 45 | } 46 | @property (nonatomic, assign) id delegate; 47 | @property (nonatomic, retain) NSColor *backgroundColor; 48 | @property (nonatomic) NSUInteger numberOfPreRenderedRows; 49 | 50 | //private 51 | @property (nonatomic, copy) NSIndexSet *originalSelectionIndexes; 52 | @property (nonatomic, copy) NSArray *contentArray, *groups; 53 | @property (nonatomic, copy) NSString *zoomValueObserverKey, *accumulatedKeyStrokes; 54 | 55 | @property (readonly) NSArray *visibleViewControllerArray; 56 | @property (readonly) BCCollectionViewLayoutManager *layoutManager; 57 | 58 | //designated way to load BCCollectionView 59 | - (void)reloadDataWithItems:(NSArray *)newContent emptyCaches:(BOOL)shouldEmptyCaches; 60 | - (void)reloadDataWithItems:(NSArray *)newContent groups:(NSArray *)newGroups emptyCaches:(BOOL)shouldEmptyCaches; 61 | - (void)reloadDataWithItems:(NSArray *)newContent groups:(NSArray *)newGroups emptyCaches:(BOOL)shouldEmptyCaches completionBlock:(dispatch_block_t)completionBlock; 62 | 63 | //Managing Selections 64 | - (void)selectItemAtIndex:(NSUInteger)index; 65 | - (void)selectItemAtIndex:(NSUInteger)index inBulk:(BOOL)bulk; 66 | 67 | - (void)selectItemsAtIndexes:(NSIndexSet *)indexes; 68 | - (void)deselectItemAtIndex:(NSUInteger)index; 69 | - (void)deselectItemAtIndex:(NSUInteger)index inBulk:(BOOL)bulk; 70 | - (void)deselectItemsAtIndexes:(NSIndexSet *)indexes; 71 | - (void)deselectAllItems; 72 | - (NSIndexSet *)selectionIndexes; 73 | 74 | //Basic Cell Information 75 | - (NSSize)cellSize; 76 | - (NSUInteger)groupHeaderHeight; 77 | - (NSRange)rangeOfVisibleItems; 78 | - (NSRange)rangeOfVisibleItemsWithOverflow; 79 | 80 | - (NSIndexSet *)indexesOfItemsInRect:(NSRect)aRect; 81 | - (NSIndexSet *)indexesOfItemContentRectsInRect:(NSRect)aRect; 82 | 83 | //Querying ViewControllers 84 | - (NSIndexSet *)indexesOfViewControllers; 85 | - (NSIndexSet *)indexesOfInvisibleViewControllers; 86 | - (NSViewController *)viewControllerForItemAtIndex:(NSUInteger)index; 87 | 88 | - (void)softReloadDataWithCompletionBlock:(dispatch_block_t)block; 89 | @end 90 | -------------------------------------------------------------------------------- /BCCollectionView.m: -------------------------------------------------------------------------------- 1 | // Created by Pieter Omvlee on 24/11/2010. 2 | // Copyright 2010 Bohemian Coding. All rights reserved. 3 | 4 | #import "BCCollectionView.h" 5 | #import "BCGeometryExtensions.h" 6 | #import "BCCollectionViewLayoutManager.h" 7 | #import "BCCollectionViewLayoutItem.h" 8 | #import "BCCollectionViewGroup.h" 9 | 10 | @implementation BCCollectionView 11 | @synthesize delegate, contentArray, groups, backgroundColor, originalSelectionIndexes, zoomValueObserverKey, accumulatedKeyStrokes, numberOfPreRenderedRows, layoutManager; 12 | @dynamic visibleViewControllerArray; 13 | 14 | - (id)initWithCoder:(NSCoder *)aDecoder 15 | { 16 | self = [super initWithCoder:aDecoder]; 17 | if (self) { 18 | reusableViewControllers = [[NSMutableArray alloc] init]; 19 | visibleViewControllers = [[NSMutableDictionary alloc] init]; 20 | contentArray = [[NSArray alloc] init]; 21 | selectionIndexes = [[NSMutableIndexSet alloc] init]; 22 | dragHoverIndex = NSNotFound; 23 | accumulatedKeyStrokes = [[NSString alloc] init]; 24 | numberOfPreRenderedRows = 3; 25 | layoutManager = [[BCCollectionViewLayoutManager alloc] initWithCollectionView:self]; 26 | visibleGroupViewControllers = [[NSMutableDictionary alloc] init]; 27 | 28 | [self addObserver:self forKeyPath:@"backgroundColor" options:0 context:NULL]; 29 | 30 | NSClipView *enclosingClipView = [[self enclosingScrollView] contentView]; 31 | [enclosingClipView setPostsBoundsChangedNotifications:YES]; 32 | NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; 33 | [center addObserver:self selector:@selector(scrollViewDidScroll:) name:NSViewBoundsDidChangeNotification object:enclosingClipView]; 34 | [center addObserver:self selector:@selector(viewDidResize) name:NSViewFrameDidChangeNotification object:self]; 35 | } 36 | return self; 37 | } 38 | 39 | - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context 40 | { 41 | if ([keyPath isEqualToString:@"backgroundColor"]) 42 | [self setNeedsDisplay:YES]; 43 | else if ([keyPath isEqual:zoomValueObserverKey]) { 44 | if ([self respondsToSelector:@selector(zoomValueDidChange)]) 45 | [self performSelector:@selector(zoomValueDidChange)]; 46 | } else if ([keyPath isEqualToString:@"isCollapsed"]) { 47 | [self softReloadDataWithCompletionBlock:^{ 48 | [self performSelector:@selector(scrollViewDidScroll:)]; 49 | }]; 50 | } else 51 | [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; 52 | } 53 | 54 | - (void)dealloc 55 | { 56 | [self removeObserver:self forKeyPath:@"backgroundColor"]; 57 | [[NSUserDefaults standardUserDefaults] removeObserver:self forKeyPath:zoomValueObserverKey]; 58 | 59 | NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; 60 | [center removeObserver:self name:NSViewBoundsDidChangeNotification object:[[self enclosingScrollView] contentView]]; 61 | [center removeObserver:self name:NSViewFrameDidChangeNotification object:self]; 62 | 63 | for (BCCollectionViewGroup *group in groups) 64 | [group removeObserver:self forKeyPath:@"isCollapsed"]; 65 | 66 | [layoutManager release]; 67 | [reusableViewControllers release]; 68 | [visibleViewControllers release]; 69 | [visibleGroupViewControllers release]; 70 | [contentArray release]; 71 | [groups release]; 72 | [selectionIndexes release]; 73 | [originalSelectionIndexes release]; 74 | [accumulatedKeyStrokes release]; 75 | [zoomValueObserverKey release]; 76 | [super dealloc]; 77 | } 78 | 79 | - (BOOL)isFlipped 80 | { 81 | return YES; 82 | } 83 | 84 | #pragma mark Drawing Selections 85 | 86 | - (BOOL)shoulDrawSelections 87 | { 88 | if ([delegate respondsToSelector:@selector(collectionViewShouldDrawSelections:)]) 89 | return [delegate collectionViewShouldDrawSelections:self]; 90 | else 91 | return YES; 92 | } 93 | 94 | - (BOOL)shoulDrawHover 95 | { 96 | if ([delegate respondsToSelector:@selector(collectionViewShouldDrawHover:)]) 97 | return [delegate collectionViewShouldDrawHover:self]; 98 | else 99 | return YES; 100 | } 101 | 102 | - (void)drawItemSelectionForInRect:(NSRect)aRect 103 | { 104 | NSRect insetRect = NSInsetRect(aRect, 10, 10); 105 | if ([self needsToDrawRect:insetRect]) { 106 | [[NSColor lightGrayColor] set]; 107 | [[NSBezierPath bezierPathWithRoundedRect:insetRect xRadius:10 yRadius:10] fill]; 108 | } 109 | } 110 | 111 | - (void)drawRect:(NSRect)dirtyRect 112 | { 113 | [backgroundColor ? backgroundColor : [NSColor whiteColor] set]; 114 | NSRectFill(dirtyRect); 115 | 116 | [[NSColor grayColor] set]; 117 | NSFrameRect(BCRectFromTwoPoints(mouseDownLocation, mouseDraggedLocation)); 118 | 119 | if ([selectionIndexes count] > 0 && [self shoulDrawSelections]) { 120 | for (NSNumber *number in visibleViewControllers) 121 | if ([selectionIndexes containsIndex:[number integerValue]]) 122 | [self drawItemSelectionForInRect:[[[visibleViewControllers objectForKey:number] view] frame]]; 123 | } 124 | 125 | if (dragHoverIndex != NSNotFound && [self shoulDrawHover]) 126 | [self drawItemSelectionForInRect:[[[visibleViewControllers objectForKey:[NSNumber numberWithInteger:dragHoverIndex]] view] frame]]; 127 | } 128 | 129 | #pragma mark Delegate Call Wrappers 130 | 131 | - (void)delegateUpdateSelectionForItemAtIndex:(NSUInteger)index 132 | { 133 | if ([delegate respondsToSelector:@selector(collectionView:updateViewControllerAsSelected:forItem:)]) 134 | [delegate collectionView:self updateViewControllerAsSelected:[self viewControllerForItemAtIndex:index] 135 | forItem:[contentArray objectAtIndex:index]]; 136 | } 137 | 138 | - (void)delegateUpdateDeselectionForItemAtIndex:(NSUInteger)index 139 | { 140 | if ([delegate respondsToSelector:@selector(collectionView:updateViewControllerAsDeselected:forItem:)]) 141 | [delegate collectionView:self updateViewControllerAsDeselected:[self viewControllerForItemAtIndex:index] 142 | forItem:[contentArray objectAtIndex:index]]; 143 | } 144 | 145 | - (void)delegateCollectionViewSelectionDidChange 146 | { 147 | if (!selectionChangedDisabled && [delegate respondsToSelector:@selector(collectionViewSelectionDidChange:)]) { 148 | [[NSRunLoop currentRunLoop] cancelPerformSelector:@selector(collectionViewSelectionDidChange:) target:delegate argument:self]; 149 | [(id)delegate performSelector:@selector(collectionViewSelectionDidChange:) withObject:self afterDelay:0.0]; 150 | } 151 | } 152 | 153 | - (void)delegateDidSelectItemAtIndex:(NSUInteger)index 154 | { 155 | if ([delegate respondsToSelector:@selector(collectionView:didSelectItem:withViewController:)]) 156 | [delegate collectionView:self 157 | didSelectItem:[contentArray objectAtIndex:index] 158 | withViewController:[self viewControllerForItemAtIndex:index]]; 159 | } 160 | 161 | - (void)delegateDidDeselectItemAtIndex:(NSUInteger)index 162 | { 163 | if ([delegate respondsToSelector:@selector(collectionView:didDeselectItem:withViewController:)]) 164 | [delegate collectionView:self 165 | didDeselectItem:[contentArray objectAtIndex:index] 166 | withViewController:[self viewControllerForItemAtIndex:index]]; 167 | } 168 | 169 | - (void)delegateViewControllerBecameInvisibleAtIndex:(NSUInteger)index 170 | { 171 | if ([delegate respondsToSelector:@selector(collectionView:viewControllerBecameInvisible:)]) 172 | [delegate collectionView:self viewControllerBecameInvisible:[self viewControllerForItemAtIndex:index]]; 173 | } 174 | 175 | - (NSSize)cellSize 176 | { 177 | return [delegate cellSizeForCollectionView:self]; 178 | } 179 | 180 | - (NSUInteger)groupHeaderHeight 181 | { 182 | return [delegate groupHeaderHeightForCollectionView:self]; 183 | } 184 | 185 | - (NSIndexSet *)indexesOfItemsInRect:(NSRect)aRect 186 | { 187 | NSArray *itemLayouts = [layoutManager itemLayouts]; 188 | NSIndexSet *visibleIndexes = [itemLayouts indexesOfObjectsWithOptions:NSEnumerationConcurrent passingTest:^BOOL(id itemLayout, NSUInteger idx, BOOL *stop) { 189 | return NSIntersectsRect([itemLayout itemRect], aRect); 190 | }]; 191 | return visibleIndexes; 192 | } 193 | 194 | - (NSIndexSet *)indexesOfItemContentRectsInRect:(NSRect)aRect 195 | { 196 | NSArray *itemLayouts = [layoutManager itemLayouts]; 197 | NSIndexSet *visibleIndexes = [itemLayouts indexesOfObjectsWithOptions:NSEnumerationConcurrent passingTest:^BOOL(id itemLayout, NSUInteger idx, BOOL *stop) { 198 | return NSIntersectsRect([itemLayout itemRect], aRect); 199 | }]; 200 | return visibleIndexes; 201 | } 202 | 203 | - (NSRange)rangeOfVisibleItems 204 | { 205 | NSIndexSet *visibleIndexes = [self indexesOfItemsInRect:[self visibleRect]]; 206 | return NSMakeRange([visibleIndexes firstIndex], [visibleIndexes lastIndex]-[visibleIndexes firstIndex]); 207 | } 208 | 209 | - (NSRange)rangeOfVisibleItemsWithOverflow 210 | { 211 | NSRange range = [self rangeOfVisibleItems]; 212 | NSInteger extraItems = [layoutManager maximumNumberOfItemsPerRow] * numberOfPreRenderedRows; 213 | NSInteger min = range.location; 214 | NSInteger max = range.location + range.length; 215 | 216 | min = MAX(0, min-extraItems); 217 | max = MIN([contentArray count], max+extraItems); 218 | return NSMakeRange(min, max-min); 219 | } 220 | 221 | #pragma mark Querying ViewControllers 222 | 223 | - (NSIndexSet *)indexesOfViewControllers 224 | { 225 | NSMutableIndexSet *set = [NSMutableIndexSet indexSet]; 226 | for (NSNumber *number in [visibleViewControllers allKeys]) 227 | [set addIndex:[number integerValue]]; 228 | return set; 229 | } 230 | 231 | - (NSArray *)visibleViewControllerArray 232 | { 233 | return [visibleViewControllers allValues]; 234 | } 235 | 236 | - (NSIndexSet *)indexesOfInvisibleViewControllers 237 | { 238 | NSRange visibleRange = [self rangeOfVisibleItemsWithOverflow]; 239 | return [[self indexesOfViewControllers] indexesPassingTest:^BOOL(NSUInteger idx, BOOL *stop) { 240 | return !NSLocationInRange(idx, visibleRange); 241 | }]; 242 | } 243 | 244 | - (NSViewController *)viewControllerForItemAtIndex:(NSUInteger)index 245 | { 246 | return [visibleViewControllers objectForKey:[NSNumber numberWithInteger:index]]; 247 | } 248 | 249 | #pragma mark Swapping ViewControllers in and out 250 | 251 | - (void)removeViewControllerForItemAtIndex:(NSUInteger)anIndex 252 | { 253 | NSNumber *key = [NSNumber numberWithInteger:anIndex]; 254 | NSViewController *viewController = [visibleViewControllers objectForKey:key]; 255 | [[viewController view] removeFromSuperview]; 256 | 257 | [self delegateUpdateDeselectionForItemAtIndex:anIndex]; 258 | [self delegateViewControllerBecameInvisibleAtIndex:anIndex]; 259 | 260 | [reusableViewControllers addObject:viewController]; 261 | [visibleViewControllers removeObjectForKey:key]; 262 | } 263 | 264 | 265 | - (void)removeInvisibleViewControllers 266 | { 267 | [[self indexesOfInvisibleViewControllers] enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { 268 | [self removeViewControllerForItemAtIndex:idx]; 269 | }]; 270 | } 271 | 272 | - (NSViewController *)emptyViewControllerForInsertion 273 | { 274 | if ([reusableViewControllers count] > 0) { 275 | NSViewController *viewController = [[[reusableViewControllers lastObject] retain] autorelease]; 276 | [reusableViewControllers removeLastObject]; 277 | return viewController; 278 | } else 279 | return [delegate reusableViewControllerForCollectionView:self]; 280 | } 281 | 282 | - (void)addMissingViewControllerForItemAtIndex:(NSUInteger)anIndex withFrame:(NSRect)aRect 283 | { 284 | if (anIndex < [contentArray count]) { 285 | NSViewController *viewController = [self emptyViewControllerForInsertion]; 286 | [visibleViewControllers setObject:viewController forKey:[NSNumber numberWithInteger:anIndex]]; 287 | [[viewController view] setFrame:aRect]; 288 | [[viewController view] setAutoresizingMask:NSViewMaxXMargin | NSViewMaxYMargin]; 289 | 290 | id itemToLoad = [contentArray objectAtIndex:anIndex]; 291 | [delegate collectionView:self willShowViewController:viewController forItem:itemToLoad]; 292 | [self addSubview:[viewController view]]; 293 | if ([selectionIndexes containsIndex:anIndex]) 294 | [self delegateUpdateSelectionForItemAtIndex:anIndex]; 295 | } 296 | } 297 | 298 | - (void)addMissingGroupHeaders 299 | { 300 | if ([groups count] > 0) { 301 | [groups enumerateObjectsUsingBlock:^(id group, NSUInteger idx, BOOL *stop) { 302 | NSRect groupRect = NSMakeRect(0, NSMinY([layoutManager rectOfItemAtIndex:[group itemRange].location])-[self groupHeaderHeight], NSWidth([self visibleRect]), [self groupHeaderHeight]); 303 | if (idx == 0 && ![group isCollapsed] && [delegate respondsToSelector:@selector(topOffsetForItemsInCollectionView:)]) 304 | groupRect.origin.y -= [delegate topOffsetForItemsInCollectionView:self]; 305 | 306 | BOOL groupShouldBeVisible = NSIntersectsRect(groupRect, [self visibleRect]); 307 | NSViewController *groupViewController = [visibleGroupViewControllers objectForKey:[NSNumber numberWithInteger:idx]]; 308 | [[groupViewController view] setFrame:groupRect]; 309 | if (groupShouldBeVisible && !groupViewController) { 310 | groupViewController = [delegate collectionView:self headerForGroup:group]; 311 | [self addSubview:[groupViewController view]]; 312 | [visibleGroupViewControllers setObject:groupViewController forKey:[NSNumber numberWithInteger:idx]]; 313 | [[groupViewController view] setFrame:groupRect]; 314 | } else if (!groupShouldBeVisible && groupViewController) { 315 | [[groupViewController view] removeFromSuperview]; 316 | [visibleGroupViewControllers removeObjectForKey:[NSNumber numberWithInteger:idx]]; 317 | } 318 | }]; 319 | } 320 | } 321 | 322 | - (void)addMissingViewControllersToView 323 | { 324 | dispatch_async(dispatch_get_main_queue(), ^{ 325 | [[NSIndexSet indexSetWithIndexesInRange:[self rangeOfVisibleItemsWithOverflow]] enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { 326 | if (![visibleViewControllers objectForKey:[NSNumber numberWithInteger:idx]]) { 327 | [self addMissingViewControllerForItemAtIndex:idx withFrame:[layoutManager rectOfItemAtIndex:idx]]; 328 | } 329 | }]; 330 | [self addMissingGroupHeaders]; 331 | }); 332 | } 333 | 334 | - (void)moveViewControllersToProperPosition 335 | { 336 | for (NSNumber *number in visibleViewControllers) { 337 | NSRect r = [layoutManager rectOfItemAtIndex:[number integerValue]]; 338 | if (!NSEqualRects(r, NSZeroRect)) 339 | [[[visibleViewControllers objectForKey:number] view] setFrame:r]; 340 | } 341 | } 342 | 343 | #pragma mark Selecting and Deselecting Items 344 | 345 | - (void)selectItemAtIndex:(NSUInteger)index 346 | { 347 | [self selectItemAtIndex:index inBulk:NO]; 348 | } 349 | 350 | - (void)selectItemAtIndex:(NSUInteger)index inBulk:(BOOL)bulkSelecting 351 | { 352 | if (index >= [contentArray count]) 353 | return; 354 | 355 | BOOL maySelectItem = YES; 356 | NSViewController *viewController = [self viewControllerForItemAtIndex:index]; 357 | id item = [contentArray objectAtIndex:index]; 358 | 359 | if ([delegate respondsToSelector:@selector(collectionView:shouldSelectItem:withViewController:)]) 360 | maySelectItem = [delegate collectionView:self shouldSelectItem:item withViewController:viewController]; 361 | 362 | if (maySelectItem) { 363 | [selectionIndexes addIndex:index]; 364 | [self delegateUpdateSelectionForItemAtIndex:index]; 365 | [self delegateDidSelectItemAtIndex:index]; 366 | if (!bulkSelecting) 367 | [self delegateCollectionViewSelectionDidChange]; 368 | if ([self shoulDrawSelections]) 369 | [self setNeedsDisplayInRect:[layoutManager rectOfItemAtIndex:index]]; 370 | } 371 | 372 | if (!bulkSelecting) 373 | lastSelectionIndex = index; 374 | } 375 | 376 | - (void)selectItemsAtIndexes:(NSIndexSet *)indexes 377 | { 378 | [indexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { 379 | [self selectItemAtIndex:idx inBulk:YES]; 380 | }]; 381 | lastSelectionIndex = [indexes firstIndex]; 382 | [self delegateCollectionViewSelectionDidChange]; 383 | } 384 | 385 | - (void)deselectItemAtIndex:(NSUInteger)index 386 | { 387 | [self deselectItemAtIndex:index inBulk:NO]; 388 | } 389 | 390 | - (void)deselectItemAtIndex:(NSUInteger)index inBulk:(BOOL)bulkDeselecting 391 | { 392 | if (index < [contentArray count]) { 393 | [selectionIndexes removeIndex:index]; 394 | if ([self shoulDrawSelections]) 395 | [self setNeedsDisplayInRect:[layoutManager rectOfItemAtIndex:index]]; 396 | 397 | if (!bulkDeselecting) 398 | [self delegateCollectionViewSelectionDidChange]; 399 | [self delegateDidDeselectItemAtIndex:index]; 400 | [self delegateUpdateDeselectionForItemAtIndex:index]; 401 | } 402 | } 403 | 404 | - (void)deselectItemsAtIndexes:(NSIndexSet *)indexes 405 | { 406 | [indexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { 407 | [self deselectItemAtIndex:idx inBulk:YES]; 408 | }]; 409 | [self delegateCollectionViewSelectionDidChange]; 410 | } 411 | 412 | - (void)deselectAllItems 413 | { 414 | [self deselectItemsAtIndexes:selectionIndexes]; 415 | } 416 | 417 | - (NSIndexSet *)selectionIndexes 418 | { 419 | return selectionIndexes; 420 | } 421 | 422 | - (void)selectAll:(id)sender 423 | { 424 | [self selectItemsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0,[contentArray count])]]; 425 | } 426 | 427 | #pragma mark User-interaction 428 | 429 | - (BOOL)acceptsFirstResponder 430 | { 431 | return YES; 432 | } 433 | 434 | - (BOOL)canBecomeKeyView 435 | { 436 | return YES; 437 | } 438 | 439 | #pragma mark Reloading and Updating the Icon View 440 | 441 | - (void)softReloadVisibleViewControllers 442 | { 443 | NSMutableArray *removeKeys = [NSMutableArray array]; 444 | 445 | for (NSString *number in visibleViewControllers) { 446 | NSUInteger index = [number integerValue]; 447 | NSViewController *controller = [visibleViewControllers objectForKey:number]; 448 | 449 | if (index < [contentArray count]) { 450 | if ([selectionIndexes containsIndex:index]) 451 | [self delegateUpdateDeselectionForItemAtIndex:index]; 452 | [delegate collectionView:self willShowViewController:controller forItem:[contentArray objectAtIndex:index]]; 453 | } else { 454 | if ([selectionIndexes containsIndex:index]) 455 | [self delegateUpdateDeselectionForItemAtIndex:index]; 456 | 457 | [self delegateViewControllerBecameInvisibleAtIndex:index]; 458 | [[controller view] removeFromSuperview]; 459 | [reusableViewControllers addObject:controller]; 460 | [removeKeys addObject:number]; 461 | } 462 | } 463 | [visibleViewControllers removeObjectsForKeys:removeKeys]; 464 | } 465 | 466 | - (void)resizeFrameToFitContents 467 | { 468 | NSRect frame = [self frame]; 469 | frame.size.height = [self visibleRect].size.height; 470 | if ([contentArray count] > 0) { 471 | BCCollectionViewLayoutItem *layoutItem = [[layoutManager itemLayouts] lastObject]; 472 | frame.size.height = MAX(frame.size.height, NSMaxY([layoutItem itemRect])); 473 | } 474 | [self setFrame:frame]; 475 | } 476 | 477 | - (void)reloadDataWithItems:(NSArray *)newContent emptyCaches:(BOOL)shouldEmptyCaches 478 | { 479 | [self reloadDataWithItems:newContent groups:nil emptyCaches:shouldEmptyCaches]; 480 | } 481 | 482 | - (void)reloadDataWithItems:(NSArray *)newContent groups:(NSArray *)newGroups emptyCaches:(BOOL)shouldEmptyCaches 483 | { 484 | [self reloadDataWithItems:newContent groups:newGroups emptyCaches:shouldEmptyCaches completionBlock:^{}]; 485 | } 486 | 487 | - (void)reloadDataWithItems:(NSArray *)newContent groups:(NSArray *)newGroups emptyCaches:(BOOL)shouldEmptyCaches completionBlock:(dispatch_block_t)completionBlock 488 | { 489 | [self deselectAllItems]; 490 | [layoutManager cancelItemEnumerator]; 491 | 492 | if (!delegate) 493 | return; 494 | 495 | NSSize cellSize = [delegate cellSizeForCollectionView:self]; 496 | if (NSWidth([self frame]) < cellSize.width || NSHeight([self frame]) < cellSize.height) 497 | return; 498 | 499 | for (BCCollectionViewGroup *group in groups) 500 | [group removeObserver:self forKeyPath:@"isCollapsed"]; 501 | for (BCCollectionViewGroup *group in newGroups) 502 | [group addObserver:self forKeyPath:@"isCollapsed" options:0 context:NULL]; 503 | 504 | self.groups = newGroups; 505 | self.contentArray = newContent; 506 | 507 | for (NSViewController *viewController in [visibleGroupViewControllers allValues]) 508 | [[viewController view] removeFromSuperview]; 509 | [visibleGroupViewControllers removeAllObjects]; 510 | 511 | if (shouldEmptyCaches) { 512 | for (NSViewController *viewController in [visibleViewControllers allValues]) { 513 | [[viewController view] removeFromSuperview]; 514 | if ([delegate respondsToSelector:@selector(collectionView:viewControllerBecameInvisible:)]) 515 | [delegate collectionView:self viewControllerBecameInvisible:viewController]; 516 | } 517 | 518 | [reusableViewControllers removeAllObjects]; 519 | [visibleViewControllers removeAllObjects]; 520 | } else 521 | [self softReloadVisibleViewControllers]; 522 | 523 | [selectionIndexes removeAllIndexes]; 524 | 525 | NSRect visibleRect = [self visibleRect]; 526 | [layoutManager enumerateItems:^(BCCollectionViewLayoutItem *layoutItem) { 527 | NSViewController *viewController = [self viewControllerForItemAtIndex:[layoutItem itemIndex]]; 528 | if (viewController) { 529 | [[viewController view] setFrame:[layoutItem itemRect]]; 530 | [delegate collectionView:self willShowViewController:viewController forItem:[contentArray objectAtIndex:[layoutItem itemIndex]]]; 531 | } else if (NSIntersectsRect(visibleRect, [layoutItem itemRect])) 532 | [self addMissingViewControllerForItemAtIndex:[layoutItem itemIndex] withFrame:[layoutItem itemRect]]; 533 | } completionBlock:^{ 534 | [self resizeFrameToFitContents]; 535 | [self addMissingGroupHeaders]; 536 | dispatch_async(dispatch_get_main_queue(), completionBlock); 537 | }]; 538 | } 539 | 540 | - (void)scrollViewDidScroll:(NSNotification *)note 541 | { 542 | dispatch_async(dispatch_get_main_queue(), ^{ 543 | [self removeInvisibleViewControllers]; 544 | [self addMissingViewControllersToView]; 545 | }); 546 | 547 | if ([delegate respondsToSelector:@selector(collectionViewDidScroll:inDirection:)]) { 548 | if ([self visibleRect].origin.y > previousFrameBounds.origin.y) 549 | [delegate collectionViewDidScroll:self inDirection:BCCollectionViewScrollDirectionDown]; 550 | else 551 | [delegate collectionViewDidScroll:self inDirection:BCCollectionViewScrollDirectionUp]; 552 | previousFrameBounds = [self visibleRect]; 553 | } 554 | } 555 | 556 | - (void)viewDidResize 557 | { 558 | if ([contentArray count] > 0 && [visibleViewControllers count] > 0) 559 | [self softReloadDataWithCompletionBlock:NULL]; 560 | } 561 | 562 | - (void)softReloadDataWithCompletionBlock:(dispatch_block_t)block 563 | { 564 | NSSize cellSize = [delegate cellSizeForCollectionView:self]; 565 | if (NSWidth([self visibleRect]) < cellSize.width || NSHeight([self visibleRect]) < cellSize.height) 566 | return; 567 | 568 | NSRange range = [self rangeOfVisibleItemsWithOverflow]; 569 | [layoutManager enumerateItems:^(BCCollectionViewLayoutItem *layoutItem) { 570 | if (NSLocationInRange([layoutItem itemIndex], range)) { 571 | NSViewController *controller = [self viewControllerForItemAtIndex:[layoutItem itemIndex]]; 572 | if (controller) 573 | [[controller view] setFrame:[layoutItem itemRect]]; 574 | else 575 | [self addMissingViewControllerForItemAtIndex:[layoutItem itemIndex] withFrame:[layoutItem itemRect]]; 576 | } else { 577 | if ([self viewControllerForItemAtIndex:[layoutItem itemIndex]]) 578 | [self removeViewControllerForItemAtIndex:[layoutItem itemIndex]]; 579 | } 580 | } completionBlock:^(void) { 581 | [self resizeFrameToFitContents]; 582 | [self addMissingGroupHeaders]; 583 | [self setNeedsDisplay:YES]; 584 | if (block != NULL) 585 | block(); 586 | }]; 587 | } 588 | 589 | - (NSMenu *)menuForEvent:(NSEvent *)anEvent 590 | { 591 | [self mouseDown:anEvent]; 592 | 593 | if ([delegate respondsToSelector:@selector(collectionView:menuForItemsAtIndexes:)]) 594 | return [delegate collectionView:self menuForItemsAtIndexes:[self selectionIndexes]]; 595 | else 596 | return nil; 597 | } 598 | 599 | - (BOOL)resignFirstResponder 600 | { 601 | if ([delegate respondsToSelector:@selector(collectionViewLostFirstResponder:)]) 602 | [delegate collectionViewLostFirstResponder:self]; 603 | return [super resignFirstResponder]; 604 | } 605 | 606 | - (BOOL)becomeFirstResponder 607 | { 608 | if ([delegate respondsToSelector:@selector(collectionViewBecameFirstResponder:)]) 609 | [delegate collectionViewBecameFirstResponder:self]; 610 | return [super becomeFirstResponder]; 611 | } 612 | 613 | - (BOOL)isOpaque 614 | { 615 | return YES; 616 | } 617 | 618 | @end 619 | -------------------------------------------------------------------------------- /BCCollectionViewDelegate.h: -------------------------------------------------------------------------------- 1 | // Created by Pieter Omvlee on 25/11/2010. 2 | // Copyright 2010 Bohemian Coding. All rights reserved. 3 | 4 | #import 5 | 6 | @class BCCollectionView, BCCollectionViewGroup; 7 | 8 | enum { 9 | BCCollectionViewScrollDirectionUp = 0, 10 | BCCollectionViewScrollDirectionDown = 1 11 | }; 12 | 13 | @protocol BCCollectionViewDelegate 14 | @required 15 | //CollectionView assumes all cells aer the same size and will resize its subviews to this size. 16 | - (NSSize)cellSizeForCollectionView:(BCCollectionView *)collectionView; 17 | 18 | //Return an empty ViewController, this might not be visible to the user immediately 19 | - (NSViewController *)reusableViewControllerForCollectionView:(BCCollectionView *)collectionView; 20 | 21 | //The CollectionView is about to display the ViewController. Use this method to populate the ViewController with data 22 | - (void)collectionView:(BCCollectionView *)collectionView willShowViewController:(NSViewController *)viewController forItem:(id)anItem; 23 | 24 | @optional 25 | //the viewController has been removed from view and storen for reuse. You can uload any resources here 26 | - (void)collectionView:(BCCollectionView *)collectionView viewControllerBecameInvisible:(NSViewController *)viewController; 27 | 28 | - (void)collectionView:(BCCollectionView *)collectionView updateViewControllerAsSelected:(NSViewController *)viewController forItem:(id)item; 29 | - (void)collectionView:(BCCollectionView *)collectionView updateViewControllerAsDeselected:(NSViewController *)viewController forItem:(id)item; 30 | 31 | //managing selections. dont update the viewController do relfect select status. use the methods above instead 32 | - (BOOL)collectionView:(BCCollectionView *)collectionView shouldSelectItem:(id)anItem withViewController:(NSViewController *)viewController; 33 | - (void)collectionView:(BCCollectionView *)collectionView didSelectItem:(id)anItem withViewController:(NSViewController *)viewController; 34 | - (void)collectionView:(BCCollectionView *)collectionView didDeselectItem:(id)anItem withViewController:(NSViewController *)viewController; 35 | - (void)collectionViewSelectionDidChange:(BCCollectionView *)collectionView; 36 | 37 | - (void)collectionView:(BCCollectionView *)collectionView didClickItem:(id)anItem withViewController:(NSViewController *)viewController; 38 | 39 | - (void)collectionViewDidScroll:(BCCollectionView *)collectionView inDirection:(NSUInteger)scrollDirection; 40 | - (void)collectionView:(BCCollectionView *)collectionView didDoubleClickViewControllerAtIndex:(NSViewController *)viewController; 41 | - (NSSize)insetMarginForSelectingItemsInCollectionView:(BCCollectionView *)collectionView; 42 | 43 | //defaults to YES 44 | - (BOOL)collectionViewShouldDrawSelections:(BCCollectionView *)collectionView; 45 | - (BOOL)collectionViewShouldDrawHover:(BCCollectionView *)collectionView; 46 | 47 | //working with groups 48 | - (NSUInteger)groupHeaderHeightForCollectionView:(BCCollectionView *)collectionView; 49 | - (NSViewController *)collectionView:(BCCollectionView *)collectionView headerForGroup:(BCCollectionViewGroup *)group; 50 | - (NSInteger)topOffsetForItemsInCollectionView:(BCCollectionView *)collectionView; 51 | 52 | //managing Drag & Drop (in order of occurence) 53 | - (BOOL)collectionView:(BCCollectionView *)collectionView canDragItemsAtIndexes:(NSIndexSet *)indexSet; 54 | - (void)collectionView:(BCCollectionView *)collectionView writeItemsAtIndexes:(NSIndexSet *)indexSet toPasteboard:(NSPasteboard *)pboard; 55 | - (BOOL)collectionView:(BCCollectionView *)collectionView validateDrop:(id )draggingInfo onItemAtIndex:(NSInteger)index; 56 | - (void)collectionView:(BCCollectionView *)collectionView dragEnteredViewController:(NSViewController *)viewController; 57 | - (void)collectionView:(BCCollectionView *)collectionView dragExitedViewController:(NSViewController *)viewController; 58 | - (BOOL)collectionView:(BCCollectionView *)collectionView 59 | performDragOperation:(id )draggingInfo 60 | onViewController:(NSViewController *)viewController 61 | forItem:(id)item; 62 | - (NSDragOperation)collectionView:(BCCollectionView *)collectionView draggingEntered:(id )draggingInfo; 63 | - (void)collectionView:(BCCollectionView *)collectionView draggingEnded:(id )draggingInfo; 64 | - (void)collectionView:(BCCollectionView *)collectionView draggingExited:(id )draggingInfo; 65 | 66 | //key events 67 | - (void)collectionView:(BCCollectionView *)collectionView deleteItemsAtIndexes:(NSIndexSet *)indexSet; 68 | - (BOOL)collectionView:(BCCollectionView *)collectionView nameOfItem:(id)anItem startsWith:(NSString *)startingString; 69 | 70 | //magnifiy events. This method is required BCCollectionView+Zoom is included 71 | - (NSRange)validScalingRangeForCollectionView:(BCCollectionView *)collectionView; 72 | - (void)collectionViewDidZoom:(BCCollectionView *)collectionView; 73 | 74 | //contextual menu 75 | - (NSMenu *)collectionView:(BCCollectionView *)collectionView menuForItemsAtIndexes:(NSIndexSet *)indexSet; 76 | 77 | - (void)collectionViewLostFirstResponder:(BCCollectionView *)collectionView; 78 | - (void)collectionViewBecameFirstResponder:(BCCollectionView *)collectionView; 79 | 80 | @end 81 | -------------------------------------------------------------------------------- /BCCollectionViewGroup.h: -------------------------------------------------------------------------------- 1 | // Created by Pieter Omvlee on 01/03/2011. 2 | // Copyright 2011 Bohemian Coding. All rights reserved. 3 | 4 | #import 5 | 6 | @interface BCCollectionViewGroup : NSObject 7 | { 8 | NSString *title; 9 | NSRange itemRange; 10 | } 11 | + (id)groupWithTitle:(NSString *)title range:(NSRange)range; 12 | @property (copy) NSString *title; 13 | @property NSRange itemRange; 14 | @property (nonatomic) BOOL isCollapsed; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /BCCollectionViewGroup.m: -------------------------------------------------------------------------------- 1 | // Created by Pieter Omvlee on 01/03/2011. 2 | // Copyright 2011 Bohemian Coding. All rights reserved. 3 | 4 | #import "BCCollectionViewGroup.h" 5 | #import "CHUserDefaults.h" 6 | 7 | @implementation BCCollectionViewGroup 8 | @synthesize title, itemRange; 9 | @dynamic isCollapsed; 10 | 11 | + (id)groupWithTitle:(NSString *)title range:(NSRange)range 12 | { 13 | BCCollectionViewGroup *group = [[BCCollectionViewGroup alloc] init]; 14 | [group setTitle:title]; 15 | [group setItemRange:range]; 16 | return [group autorelease]; 17 | } 18 | 19 | - (NSString *)description 20 | { 21 | return [NSString stringWithFormat:@"%@ %@", title, NSStringFromRange(itemRange)]; 22 | } 23 | 24 | - (void)dealloc 25 | { 26 | [title release]; 27 | [super dealloc]; 28 | } 29 | 30 | - (NSString *)defaultsIdentifier 31 | { 32 | return [NSString stringWithFormat:@"collectionGroup%@Status", title]; 33 | } 34 | 35 | - (BOOL)isCollapsed 36 | { 37 | return CHDefaultsBoolForKey([self defaultsIdentifier]); 38 | } 39 | 40 | - (void)setIsCollapsed:(BOOL)isCollapsed 41 | { 42 | CHDefaultsSetBoolForKey(isCollapsed, [self defaultsIdentifier]); 43 | } 44 | 45 | @end 46 | -------------------------------------------------------------------------------- /BCCollectionViewLayoutItem.h: -------------------------------------------------------------------------------- 1 | // Created by Pieter Omvlee on 01/03/2011. 2 | // Copyright 2011 Bohemian Coding. All rights reserved. 3 | 4 | #import 5 | 6 | @interface BCCollectionViewLayoutItem : NSObject 7 | { 8 | NSInteger rowIndex, columnIndex, itemIndex; 9 | NSRect itemRect, itemContentRect; 10 | } 11 | @property (nonatomic) NSInteger rowIndex, columnIndex, itemIndex; 12 | @property (nonatomic) NSRect itemRect, itemContentRect; 13 | + (id)layoutItem; 14 | @end 15 | -------------------------------------------------------------------------------- /BCCollectionViewLayoutItem.m: -------------------------------------------------------------------------------- 1 | // Created by Pieter Omvlee on 01/03/2011. 2 | // Copyright 2011 Bohemian Coding. All rights reserved. 3 | 4 | #import "BCCollectionViewLayoutItem.h" 5 | 6 | @implementation BCCollectionViewLayoutItem 7 | @synthesize rowIndex, columnIndex, itemRect, itemIndex, itemContentRect; 8 | 9 | + (id)layoutItem 10 | { 11 | return [[[self alloc] init] autorelease]; 12 | } 13 | 14 | - (NSString *)description 15 | { 16 | return [NSString stringWithFormat:@"i:%i r:%i c:%i", (int)itemIndex, (int)rowIndex, (int)columnIndex]; 17 | } 18 | 19 | @end 20 | -------------------------------------------------------------------------------- /BCCollectionViewLayoutManager.h: -------------------------------------------------------------------------------- 1 | // Created by Pieter Omvlee on 15/02/2011. 2 | // Copyright 2011 Bohemian Coding. All rights reserved. 3 | 4 | #import 5 | #import "BCCollectionViewLayoutOperation.h" 6 | 7 | @class BCCollectionView; 8 | @interface BCCollectionViewLayoutManager : NSObject 9 | { 10 | BCCollectionView *collectionView; 11 | NSOperationQueue *queue; 12 | 13 | NSArray *itemLayouts; 14 | } 15 | @property (retain) NSArray *itemLayouts; 16 | - (id)initWithCollectionView:(BCCollectionView *)collectionView; //assigned 17 | - (void)cancelItemEnumerator; 18 | - (void)enumerateItems:(BCCollectionViewLayoutOperationIterator)itemIterator completionBlock:(dispatch_block_t)completionBlock; 19 | 20 | #pragma mark Primitives 21 | - (NSUInteger)maximumNumberOfItemsPerRow; 22 | - (NSSize)cellSize; 23 | 24 | #pragma mark Rows and Columns 25 | - (NSUInteger)indexOfItemAtRow:(NSUInteger)rowIndex column:(NSUInteger)colIndex; 26 | - (NSPoint)rowAndColumnPositionOfItemAtIndex:(NSUInteger)anIndex; 27 | 28 | #pragma mark From Point to Index 29 | - (NSUInteger)indexOfItemAtPoint:(NSPoint)p; 30 | - (NSUInteger)indexOfItemContentRectAtPoint:(NSPoint)p; 31 | 32 | #pragma mark From Index to Rect 33 | - (NSRect)rectOfItemAtIndex:(NSUInteger)anIndex; 34 | - (NSRect)contentRectOfItemAtIndex:(NSUInteger)anIndex; 35 | @end 36 | -------------------------------------------------------------------------------- /BCCollectionViewLayoutManager.m: -------------------------------------------------------------------------------- 1 | // Created by Pieter Omvlee on 15/02/2011. 2 | // Copyright 2011 Bohemian Coding. All rights reserved. 3 | 4 | #import "BCCollectionViewLayoutManager.h" 5 | #import "BCCollectionView.h" 6 | #import "BCCollectionViewGroup.h" 7 | #import "BCCollectionViewLayoutItem.h" 8 | 9 | @implementation BCCollectionViewLayoutManager 10 | @synthesize itemLayouts; 11 | 12 | - (id)initWithCollectionView:(BCCollectionView *)aCollectionView 13 | { 14 | self = [super init]; 15 | if (self) { 16 | collectionView = aCollectionView; 17 | queue = [[NSOperationQueue alloc] init]; 18 | [queue setMaxConcurrentOperationCount:1]; 19 | } 20 | return self; 21 | } 22 | 23 | - (void)cancelItemEnumerator 24 | { 25 | [queue cancelAllOperations]; 26 | } 27 | 28 | - (void)enumerateItems:(BCCollectionViewLayoutOperationIterator)itemIterator completionBlock:(dispatch_block_t)completionBlock 29 | { 30 | BCCollectionViewLayoutOperation *operation = [[BCCollectionViewLayoutOperation alloc] init]; 31 | [operation setCollectionView:collectionView]; 32 | [operation setLayoutCallBack:itemIterator]; 33 | [operation setLayoutCompletionBlock:completionBlock]; 34 | 35 | // if ([queue operationCount] > 10) 36 | [queue cancelAllOperations]; 37 | [queue addOperation:[operation autorelease]]; 38 | } 39 | 40 | - (void)dealloc 41 | { 42 | [itemLayouts release]; 43 | [queue release]; 44 | [super dealloc]; 45 | } 46 | 47 | #pragma mark - 48 | #pragma mark Primitives 49 | 50 | - (NSUInteger)maximumNumberOfItemsPerRow 51 | { 52 | return MAX(1, [collectionView frame].size.width/[self cellSize].width); 53 | } 54 | 55 | - (NSSize)cellSize 56 | { 57 | return [collectionView cellSize]; 58 | } 59 | 60 | #pragma mark - 61 | #pragma mark Rows and Columns 62 | 63 | - (NSPoint)rowAndColumnPositionOfItemAtIndex:(NSUInteger)anIndex 64 | { 65 | if ([itemLayouts count] > anIndex) { 66 | BCCollectionViewLayoutItem *itemLayout = [itemLayouts objectAtIndex:anIndex]; 67 | return NSMakePoint(itemLayout.columnIndex, itemLayout.rowIndex); 68 | } else 69 | return NSZeroPoint; 70 | } 71 | 72 | - (NSUInteger)indexOfItemAtRow:(NSUInteger)rowIndex column:(NSUInteger)colIndex 73 | { 74 | __block NSUInteger index = NSNotFound; 75 | [itemLayouts enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(BCCollectionViewLayoutItem *item, NSUInteger idx, BOOL *stop) { 76 | if ([item rowIndex] == rowIndex && [item columnIndex] == colIndex) { 77 | index = [item itemIndex]; 78 | *stop = YES; 79 | } 80 | }]; 81 | return index; 82 | } 83 | 84 | #pragma mark - 85 | #pragma mark From Point to Index 86 | 87 | - (NSUInteger)indexOfItemAtPoint:(NSPoint)p 88 | { 89 | NSInteger count = [itemLayouts count]; 90 | for (NSInteger i=0; i 5 | 6 | @class BCCollectionView, BCCollectionViewLayoutItem; 7 | 8 | typedef void(^BCCollectionViewLayoutOperationIterator)(BCCollectionViewLayoutItem *layoutItem); 9 | 10 | @interface BCCollectionViewLayoutOperation : NSOperation 11 | { 12 | BCCollectionViewLayoutOperationIterator layoutCallBack; 13 | dispatch_block_t layoutCompletionBlock; 14 | BCCollectionView *collectionView; 15 | } 16 | @property (copy) BCCollectionViewLayoutOperationIterator layoutCallBack; 17 | @property (copy) dispatch_block_t layoutCompletionBlock; 18 | @property (assign) BCCollectionView *collectionView; 19 | 20 | @end 21 | -------------------------------------------------------------------------------- /BCCollectionViewLayoutOperation.m: -------------------------------------------------------------------------------- 1 | // Created by Pieter Omvlee on 02/03/2011. 2 | // Copyright 2011 Bohemian Coding. All rights reserved. 3 | 4 | #import "BCCollectionViewLayoutOperation.h" 5 | #import "BCCollectionView.h" 6 | #import "BCCollectionViewLayoutItem.h" 7 | #import "BCCollectionViewGroup.h" 8 | #import "BCCollectionViewLayoutManager.h" 9 | 10 | @implementation BCCollectionViewLayoutOperation 11 | @synthesize layoutCallBack, collectionView, layoutCompletionBlock; 12 | 13 | - (void)main 14 | { 15 | if ([self isCancelled]) 16 | return; 17 | 18 | NSInteger numberOfRows = 0; 19 | NSInteger startingX = 0; 20 | NSInteger x = 0; 21 | NSInteger y = 0; 22 | NSUInteger colIndex = 0; 23 | NSRect visibleRect = [collectionView visibleRect]; 24 | NSSize cellSize = [collectionView cellSize]; 25 | NSSize inset = NSZeroSize; 26 | NSInteger maxColumns = [[collectionView layoutManager] maximumNumberOfItemsPerRow]; 27 | NSUInteger gap = (NSWidth([collectionView frame]) - maxColumns*cellSize.width)/(maxColumns-1); 28 | if (maxColumns < 4 && maxColumns > 1) { 29 | gap = (NSWidth([collectionView frame]) - maxColumns*cellSize.width)/(maxColumns+1); 30 | startingX = gap; 31 | x = gap; 32 | } 33 | 34 | if ([[collectionView delegate] respondsToSelector:@selector(insetMarginForSelectingItemsInCollectionView:)]) 35 | inset = [[collectionView delegate] insetMarginForSelectingItemsInCollectionView:collectionView]; 36 | 37 | NSMutableArray *newLayouts = [NSMutableArray array]; 38 | NSEnumerator *groupEnum = [[collectionView groups] objectEnumerator]; 39 | BCCollectionViewGroup *group = [groupEnum nextObject]; 40 | 41 | if (![group isCollapsed] && [[collectionView delegate] respondsToSelector:@selector(topOffsetForItemsInCollectionView:)]) 42 | y += [[collectionView delegate] topOffsetForItemsInCollectionView:collectionView]; 43 | 44 | NSUInteger count = [[collectionView contentArray] count]; 45 | for (NSInteger i=0; i NSMaxX(visibleRect)) { 62 | numberOfRows++; 63 | colIndex = 0; 64 | y += cellSize.height; 65 | x = startingX; 66 | } 67 | [item setColumnIndex:colIndex]; 68 | [item setItemRect:NSMakeRect(x, y, cellSize.width, cellSize.height)]; 69 | x += cellSize.width + gap; 70 | colIndex++; 71 | } else { 72 | [item setItemRect:NSMakeRect(-cellSize.width*2, y, cellSize.width, cellSize.height)]; 73 | } 74 | [item setItemContentRect:NSInsetRect([item itemRect], inset.width, inset.height)]; 75 | [item setRowIndex:numberOfRows]; 76 | [newLayouts addObject:item]; 77 | 78 | if (layoutCallBack != nil) { 79 | dispatch_async(dispatch_get_main_queue(), ^{ 80 | layoutCallBack(item); 81 | }); 82 | } 83 | if ([group itemRange].location + [group itemRange].length-1 == i) 84 | group = [groupEnum nextObject]; 85 | } 86 | numberOfRows = MAX(numberOfRows, [[collectionView groups] count]); 87 | if ([[collectionView contentArray] count] > 0 && numberOfRows == -1) 88 | numberOfRows = 1; 89 | 90 | if (![self isCancelled]) { 91 | dispatch_async(dispatch_get_main_queue(), ^{ 92 | [[collectionView layoutManager] setItemLayouts:newLayouts]; 93 | layoutCompletionBlock(); 94 | }); 95 | } 96 | } 97 | 98 | - (void)dealloc 99 | { 100 | [layoutCallBack release]; 101 | [layoutCompletionBlock release]; 102 | [super dealloc]; 103 | } 104 | 105 | @end 106 | -------------------------------------------------------------------------------- /BCGeometryExtensions.h: -------------------------------------------------------------------------------- 1 | // Created by Pieter Omvlee on 25/11/2010. 2 | // Copyright 2010 Bohemian Coding. All rights reserved. 3 | 4 | #import 5 | 6 | static NSRect BCRectFromTwoPoints(NSPoint a, NSPoint b) 7 | { 8 | NSRect r; 9 | 10 | r.size.width = ABS( b.x - a.x ); 11 | r.size.height = ABS( b.y - a.y ); 12 | 13 | r.origin.x = MIN( a.x, b.x ); 14 | r.origin.y = MIN( a.y, b.y ); 15 | 16 | return r; 17 | } -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | # BCCollectionView 2 | 3 | BCCollectionView is intended as a replacement for NSCollectionView (and possibly IKImageBrowserView). It is designed to work with a lot of items and only loads the views that it actually needs. 4 | 5 | Unlike NSCollectionView, BCCollectionView smoothly displays 300.000+ items. 6 | Every 'cell' in an BCCollectionView is an NSViewController. At the moment these are only uniform; every cell is supposed to be the same NSViewController subclass. 7 | 8 | # High-level concept overview of BCCollectionView 9 | 10 | - The delegate provides empty NSViewControllers for the BCCollectionView that will be reused as often as needed; you can already style the view in it to a creation degree but the delegate doesn't know yet which item it will be displaying later. 11 | 12 | - (NSViewController *)reusableViewControllerForIconView:(BCCollectionView *)iconView 13 | 14 | - Before BCCollectionView shows them, it asks the delegate to populate an NSViewController with an item from its contentArray; this is the time to set labels or images that are dependent on the item its supposed to represent 15 | 16 | - (void)iconView:(BCCollectionView *)iconView willShowViewController:(NSViewController *)viewController forItem:(id)anItem 17 | 18 | - When the user scrolls the view, some views will become invisible. BCCollectionView removes them from view and stores them for later use. The delegate has a chance to unload any item-specific resources the item, possibly to save memory for example or do other kinds of cleanup 19 | 20 | - (void)iconView:(BCCollectionView *)iconView viewControllerBecameInvisible:(NSViewController *)viewController 21 | 22 | - If the categories on BCCollectionView are included too, the users can use either mouse or keyboard to select items. By default, BCCollectionView will (for now) just draw a rather ugly bezel under a view if it is selected. The delegate can customise this behaviour by return NO in: 23 | 24 | - (BOOL)iconViewShouldDrawSelections:(BCCollectionView *)iconView 25 | 26 | If the delegate also overrides the following two methods it can customise its view to determine how to show a selected item instead: 27 | 28 | - (void)iconView:(BCCollectionView *)iconView updateViewControllerAsSelected:(NSViewController *)viewController forItem:(id)item; 29 | - (void)iconView:(BCCollectionView *)iconView updateViewControllerAsDeselected:(NSViewController *)viewController forItem:(id)item; 30 | 31 | Note. Do not use the following methods to style viewControllers. They are intended to inform the delegates of changes in the selection. Using these methods to style the viewControllers might break when the views are being reused at a later point. 32 | 33 | - (BOOL)iconView:(BCCollectionView *)iconView shouldSelectItem:(id)anItem withViewController:(NSViewController *)viewController; 34 | - (void)iconView:(BCCollectionView *)iconView didSelectItem:(id)anItem withViewController:(NSViewController *)viewController; 35 | - (void)iconView:(BCCollectionView *)iconView didDeselectItem:(id)anItem withViewController:(NSViewController *)viewController; 36 | 37 | # Groups 38 | 39 | The latest addition to BCCollectionView has to with groups. Just like IKImageBrowserView, we can now divide the items into groups. A group is simply a range with a title, and the delegate can supply a custom NSViewController to represent the header. 40 | Groups can be set using the default way to load the BCCollectionView: 41 | 42 | - (void)reloadDataWithItems:(NSArray *)newContent groups:(NSArray *)newGroups emptyCaches:(BOOL)shouldEmptyCaches; 43 | 44 | # License 45 | 46 | BCCollectionView is licensed under the BSD license --------------------------------------------------------------------------------