├── .gitignore ├── NSColor+OEAdditions.h ├── NSColor+OEAdditions.m ├── OEGridLayer.h ├── OEGridLayer.m ├── OEGridView+OEGridViewCell.h ├── OEGridView.h ├── OEGridView.m ├── OEGridView.podspec ├── OEGridViewCell+OEGridView.h ├── OEGridViewCell.h ├── OEGridViewCell.m ├── OEGridViewFieldEditor.h ├── OEGridViewFieldEditor.m ├── OEGridViewLayoutManager.h ├── OEGridViewLayoutManager.m └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # osx noise 2 | .DS_Store 3 | profile 4 | 5 | # xcode noise 6 | build/* 7 | *.mode1 8 | *.mode1v3 9 | *.mode2v3 10 | *.perspective 11 | *.perspectivev3 12 | *.pbxuser 13 | *.xcworkspace 14 | xcuserdata 15 | 16 | # svn & cvs 17 | .svn 18 | CVS 19 | -------------------------------------------------------------------------------- /NSColor+OEAdditions.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2012, OpenEmu Team 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the OpenEmu Team nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY OpenEmu Team ''AS IS'' AND ANY 16 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL OpenEmu Team BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | */ 26 | 27 | #import 28 | 29 | @interface NSColor (OEAdditions) 30 | 31 | + (NSColor *)colorWithCGColor:(CGColorRef)color; 32 | - (CGColorRef)CGColor; 33 | 34 | @end 35 | -------------------------------------------------------------------------------- /NSColor+OEAdditions.m: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2012, OpenEmu Team 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the OpenEmu Team nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY OpenEmu Team ''AS IS'' AND ANY 16 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL OpenEmu Team BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | */ 26 | 27 | #import "NSColor+OEAdditions.h" 28 | #import 29 | 30 | // Note: this file is compiled under MRR. It cannot be ARC because there is no supported way 31 | // to return an autoreleased CF object under ARC. 32 | 33 | #if __has_feature(objc_arc) 34 | #error This file cannot be compiled with ARC 35 | #endif 36 | 37 | @implementation NSColor (OEAdditionsDynamic) 38 | 39 | static CGColorRef _NSColor_CGColor(NSColor *self, SEL _cmd); 40 | static NSColor *_NSColor_colorWithCGColor_(Class self, SEL _cmd, CGColorRef color); 41 | 42 | + (void)load 43 | { 44 | if(![self instancesRespondToSelector:@selector(CGColor)]) 45 | class_addMethod(self, @selector(CGColor), (IMP)_NSColor_CGColor, "^{CGColor=}@:"); 46 | 47 | if(![self respondsToSelector:@selector(colorWithCGColor:)]) 48 | class_addMethod(object_getClass(self), @selector(colorWithCGColor:), (IMP)_NSColor_colorWithCGColor_, "@@:^{CGColor=}"); 49 | } 50 | 51 | static NSColor *_NSColor_colorWithCGColor_(Class self, SEL _cmd, CGColorRef color) 52 | { 53 | const CGFloat *components = CGColorGetComponents(color); 54 | NSColorSpace *colorSpace = [[NSColorSpace alloc] initWithCGColorSpace:CGColorGetColorSpace(color)]; 55 | NSColor *result = [NSColor colorWithColorSpace:colorSpace components:components count:CGColorGetNumberOfComponents(color)]; 56 | [colorSpace release]; 57 | 58 | return result; 59 | } 60 | 61 | static CGColorRef _NSColor_CGColor(NSColor *self, SEL _cmd) 62 | { 63 | if([self isEqualTo:[NSColor blackColor]]) return CGColorGetConstantColor(kCGColorBlack); 64 | if([self isEqualTo:[NSColor whiteColor]]) return CGColorGetConstantColor(kCGColorWhite); 65 | if([self isEqualTo:[NSColor clearColor]]) return CGColorGetConstantColor(kCGColorClear); 66 | 67 | NSColor *rgbColor = [self colorUsingColorSpaceName:NSCalibratedRGBColorSpace]; 68 | CGFloat components[4]; 69 | [rgbColor getComponents:components]; 70 | 71 | CGColorSpaceRef theColorSpace = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB); 72 | CGColorRef theColor = CGColorCreate(theColorSpace, components); 73 | CGColorSpaceRelease(theColorSpace); 74 | 75 | return (CGColorRef)[(id)theColor autorelease]; 76 | } 77 | 78 | @end 79 | -------------------------------------------------------------------------------- /OEGridLayer.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2012, OpenEmu Team 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the OpenEmu Team nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY OpenEmu Team ''AS IS'' AND ANY 16 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL OpenEmu Team BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | */ 26 | 27 | #import 28 | #import "OEGridViewLayoutManager.h" 29 | 30 | @interface OEGridLayer : CALayer 31 | 32 | #pragma mark - 33 | #pragma mark Mouse Handling Operations 34 | 35 | - (void)mouseDownAtPointInLayer:(NSPoint)point withEvent:(NSEvent *)theEvent; 36 | - (void)mouseUpAtPointInLayer:(NSPoint)point withEvent:(NSEvent *)theEvent; 37 | - (void)mouseDraggedAtPointInLayer:(NSPoint)point withEvent:(NSEvent *)theEvent; 38 | - (void)mouseMovedAtPointInLayer:(NSPoint)point withEvent:(NSEvent *)theEvent; 39 | - (void)mouseEnteredAtPointInLayer:(NSPoint)point withEvent:(NSEvent *)theEvent; 40 | - (void)mouseExitedAtPointInLayer:(NSPoint)point withEvent:(NSEvent *)theEvent; 41 | - (void)mouseMovedAtPointInLayer:(NSPoint)point withEvent:(NSEvent *)theEvent; 42 | 43 | #pragma mark - 44 | #pragma mark Dragging Operations 45 | 46 | - (NSDragOperation)draggingEntered:(id)sender; 47 | - (void)draggingExited:(id)sender; 48 | - (NSDragOperation)draggingUpdated:(id)sender; 49 | - (BOOL)performDragOperation:(id)sender; 50 | 51 | #pragma mark - 52 | #pragma mark Layer Operations 53 | 54 | - (void)willMoveToSuperlayer:(OEGridLayer *)superlayer; 55 | - (void)didMoveToSuperlayer; 56 | 57 | #pragma mark - 58 | #pragma mark Properties 59 | 60 | @property(nonatomic, getter=isTracking) BOOL tracking; 61 | @property(nonatomic, getter=isInteractive) BOOL interactive; 62 | @property(nonatomic, assign) BOOL receivesHoverEvents; 63 | @property(nonatomic, readonly) NSWindow *window; 64 | @property(nonatomic, readonly) NSView *view; 65 | 66 | @end 67 | -------------------------------------------------------------------------------- /OEGridLayer.m: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2012, OpenEmu Team 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the OpenEmu Team nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY OpenEmu Team ''AS IS'' AND ANY 16 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL OpenEmu Team BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | */ 26 | 27 | #import "OEGridLayer.h" 28 | 29 | @implementation OEGridLayer 30 | @synthesize receivesHoverEvents = _receivesHoverEvents; 31 | @synthesize tracking = _tracking, interactive = _interactive; 32 | 33 | - (id)init 34 | { 35 | if((self = [super init])) 36 | { 37 | [self setLayoutManager:[OEGridViewLayoutManager layoutManager]]; 38 | NSWindow *mainWindow = [NSApp mainWindow]; 39 | NSWindow *layerWindow = [[self view] window]; 40 | if (mainWindow || layerWindow) { 41 | [self setContentsScale:[(layerWindow != nil) ? layerWindow : mainWindow backingScaleFactor]]; 42 | } 43 | } 44 | return self; 45 | } 46 | 47 | #pragma mark - 48 | #pragma mark Mouse Handling Operations 49 | 50 | - (void)mouseDownAtPointInLayer:(NSPoint)point withEvent:(NSEvent *)theEvent 51 | { 52 | } 53 | 54 | - (void)mouseUpAtPointInLayer:(NSPoint)point withEvent:(NSEvent *)theEvent 55 | { 56 | } 57 | 58 | - (void)mouseMovedAtPointInLayer:(NSPoint)point withEvent:(NSEvent *)theEvent 59 | { 60 | } 61 | 62 | - (void)mouseEnteredAtPointInLayer:(NSPoint)point withEvent:(NSEvent *)theEvent 63 | { 64 | } 65 | 66 | - (void)mouseExitedAtPointInLayer:(NSPoint)point withEvent:(NSEvent *)theEvent 67 | { 68 | } 69 | 70 | - (void)mouseDraggedAtPointInLayer:(NSPoint)point withEvent:(NSEvent *)theEvent 71 | { 72 | 73 | } 74 | 75 | #pragma mark - 76 | #pragma mark Dragging Operations 77 | 78 | - (NSDragOperation)draggingEntered:(id)sender 79 | { 80 | return NSDragOperationNone; 81 | } 82 | 83 | - (void)draggingExited:(id)sender 84 | { 85 | } 86 | 87 | - (NSDragOperation)draggingUpdated:(id)sender 88 | { 89 | return NSDragOperationNone; 90 | } 91 | 92 | - (BOOL)performDragOperation:(id)sender 93 | { 94 | return NO; 95 | } 96 | 97 | - (void)layoutSublayers 98 | { 99 | if([[self delegate] respondsToSelector:@selector(layoutSublayers)]) [[self delegate] layoutSublayers]; 100 | } 101 | 102 | #pragma mark - 103 | #pragma mark Layer Operations 104 | 105 | - (id)actionForKey:(NSString *)event 106 | { 107 | return nil; 108 | } 109 | 110 | - (CALayer *)hitTest:(CGPoint)p 111 | { 112 | if(!_interactive && !_receivesHoverEvents) return nil; 113 | 114 | if(CGRectContainsPoint([self frame], p)) 115 | return [super hitTest:p] ? : self; 116 | 117 | return nil; 118 | } 119 | 120 | - (void)willMoveToSuperlayer:(OEGridLayer *)superlayer 121 | { 122 | } 123 | 124 | - (void)didMoveToSuperlayer 125 | { 126 | } 127 | 128 | - (void)addSublayer:(CALayer *)layer 129 | { 130 | if([layer isKindOfClass:[OEGridLayer class]]) 131 | { 132 | [(OEGridLayer *)layer willMoveToSuperlayer:self]; 133 | [super addSublayer:layer]; 134 | [(OEGridLayer *)layer didMoveToSuperlayer]; 135 | } 136 | else 137 | { 138 | [super addSublayer:layer]; 139 | } 140 | } 141 | 142 | - (void)insertSublayer:(CALayer *)layer atIndex:(unsigned int)idx 143 | { 144 | if([layer isKindOfClass:[OEGridLayer class]]) 145 | { 146 | [(OEGridLayer *)layer willMoveToSuperlayer:self]; 147 | [super insertSublayer:layer atIndex:idx]; 148 | [(OEGridLayer *)layer didMoveToSuperlayer]; 149 | } 150 | else 151 | { 152 | [super insertSublayer:layer atIndex:idx]; 153 | } 154 | } 155 | 156 | - (void)removeFromSuperlayer 157 | { 158 | [self willMoveToSuperlayer:nil]; 159 | 160 | [super removeFromSuperlayer]; 161 | 162 | [self didMoveToSuperlayer]; 163 | } 164 | 165 | #pragma mark - 166 | #pragma mark Properties 167 | 168 | - (NSWindow *)window 169 | { 170 | return [[self view] window]; 171 | } 172 | 173 | - (NSView *)view 174 | { 175 | CALayer *superlayer = self; 176 | while(superlayer) 177 | { 178 | NSView *delegate = [superlayer delegate]; 179 | 180 | if([delegate isKindOfClass:[NSView class]]) return delegate; 181 | 182 | superlayer = [superlayer superlayer]; 183 | } 184 | 185 | return nil; 186 | } 187 | 188 | @end 189 | -------------------------------------------------------------------------------- /OEGridView+OEGridViewCell.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2012, OpenEmu Team 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the OpenEmu Team nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY OpenEmu Team ''AS IS'' AND ANY 16 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL OpenEmu Team BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | */ 26 | 27 | #import "OEGridView.h" 28 | 29 | @interface OEGridView (OEGridViewCell) 30 | 31 | - (void)OE_willBeginEditingCell:(OEGridViewCell *)cell; 32 | - (void)OE_didEndEditingCell:(OEGridViewCell *)cell; 33 | 34 | @end 35 | -------------------------------------------------------------------------------- /OEGridView.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2012, OpenEmu Team 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the OpenEmu Team nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY OpenEmu Team ''AS IS'' AND ANY 16 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL OpenEmu Team BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | */ 26 | 27 | #import 28 | #import "OEGridViewCell.h" 29 | #import "OEGridViewLayoutManager.h" 30 | #import "OEGridViewFieldEditor.h" 31 | 32 | @class OEGridView; 33 | 34 | @protocol OEGridViewDelegate 35 | 36 | @optional 37 | - (void)selectionChangedInGridView:(OEGridView *)gridView; 38 | - (void)gridView:(OEGridView *)gridView doubleClickedCellForItemAtIndex:(NSUInteger)index; 39 | - (NSDragOperation)gridView:(OEGridView *)gridView validateDrop:(id)sender; 40 | - (NSDragOperation)gridView:(OEGridView *)gridView draggingUpdated:(id)sender; 41 | - (BOOL)gridView:(OEGridView *)gridView acceptDrop:(id)sender; 42 | - (void)gridView:(OEGridView *)gridView magnifiedWithEvent:(NSEvent*)event; 43 | - (void)gridView:(OEGridView *)gridView magnifyEndedWithEvent:(NSEvent*)event; 44 | @end 45 | 46 | #pragma mark - 47 | @protocol OEGridViewDataSource 48 | 49 | @required 50 | - (OEGridViewCell *)gridView:(OEGridView *)gridView cellForItemAtIndex:(NSUInteger)index; 51 | - (NSUInteger)numberOfItemsInGridView:(OEGridView *)gridView; 52 | 53 | @optional 54 | - (NSView *)viewForNoItemsInGridView:(OEGridView *)gridView; 55 | - (void)gridView:(OEGridView *)gridView willBeginEditingCellForItemAtIndex:(NSUInteger)index; 56 | - (void)gridView:(OEGridView *)gridView didEndEditingCellForItemAtIndex:(NSUInteger)index; 57 | - (id)gridView:(OEGridView *)gridView pasteboardWriterForIndex:(NSInteger)index; 58 | - (NSMenu *)gridView:(OEGridView *)gridView menuForItemsAtIndexes:(NSIndexSet *)indexes; 59 | 60 | @end 61 | 62 | #pragma mark - 63 | @interface OEGridView : NSView 64 | #pragma mark - 65 | #pragma mark Query Data Sources 66 | 67 | - (id)dequeueReusableCell; 68 | - (NSUInteger)numberOfItems; 69 | - (OEGridViewCell *)cellForItemAtIndex:(NSUInteger)index makeIfNecessary:(BOOL)necessary; 70 | 71 | #pragma mark - 72 | #pragma mark Query Cells 73 | 74 | - (NSUInteger)indexForCell:(OEGridViewCell *)cell; 75 | - (NSUInteger)indexForCellAtPoint:(NSPoint)point; 76 | - (NSIndexSet *)indexesForCellsInRect:(NSRect)rect; 77 | - (NSArray *)visibleCells; 78 | - (NSIndexSet *)indexesForVisibleCells; 79 | - (NSRect)rectForCellAtIndex:(NSUInteger)index; 80 | 81 | #pragma mark - 82 | #pragma mark Selection 83 | 84 | - (NSUInteger)indexForSelectedCell; 85 | - (NSIndexSet *)indexesForSelectedCells; 86 | - (void)selectCellAtIndex:(NSUInteger)index; 87 | - (void)deselectCellAtIndex:(NSUInteger)index; 88 | - (void)selectAll:(id)sender; 89 | - (void)deselectAll:(id)sender; 90 | 91 | #pragma mark - 92 | #pragma mark Data Reload 93 | 94 | - (void)noteNumberOfCellsChanged; 95 | - (void)reloadData; 96 | - (void)reloadCellsAtIndexes:(NSIndexSet *)indexes; 97 | 98 | #pragma mark - 99 | #pragma mark Properties 100 | 101 | @property(nonatomic, strong) CALayer *foregroundLayer; // A decorative background layer, the layer should return nil for -hitTest 102 | @property(nonatomic, strong) CALayer *backgroundLayer; // A decorative foreground layer, the layer should return nil for -hitTest 103 | @property(nonatomic, assign) CGFloat minimumColumnSpacing; // Minimum spacing between columns 104 | @property(nonatomic, assign) CGFloat rowSpacing; // Minimum spacing between rows 105 | @property(nonatomic, assign) CGSize itemSize; // User defined cell size (defaults to 250 x 250) 106 | @property(nonatomic, copy) NSIndexSet *selectionIndexes; // NSIndexSet of selected indexes 107 | 108 | @property(nonatomic, assign) id dataSource; // Responsible for supplying the cells of each object represented in the grid 109 | @property(nonatomic, assign) id delegate; // Receives information regarding the user interaction of the grid and it's cells 110 | 111 | @end 112 | -------------------------------------------------------------------------------- /OEGridView.m: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2012, OpenEmu Team 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the OpenEmu Team nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY OpenEmu Team ''AS IS'' AND ANY 16 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL OpenEmu Team BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | */ 26 | 27 | #import "OEGridView.h" 28 | #import "OEGridViewCell+OEGridView.h" 29 | #import "NSColor+OEAdditions.h" 30 | #import 31 | 32 | #define OERunningMountainLion (floor(NSAppKitVersionNumber) > NSAppKitVersionNumber10_7) 33 | 34 | const NSTimeInterval OEInitialPeriodicDelay = 0.4; // Initial delay of a periodic events 35 | const NSTimeInterval OEPeriodicInterval = 0.075; // Subsequent interval of periodic events 36 | 37 | @interface OEGridView () 38 | 39 | - (void)OE_commonGridViewInit; 40 | 41 | - (void)OE_updateSelectedCellsActiveSelectorWithFocus:(BOOL)focus; 42 | - (void)OE_windowChangedKey:(NSNotification *)notification; 43 | - (void)OE_clipViewFrameChanged:(NSNotification *)notification; 44 | - (void)OE_clipViewBoundsChanged:(NSNotification *)notification; 45 | 46 | - (void)OE_moveKeyboardSelectionToIndex:(NSUInteger)index; 47 | 48 | - (void)OE_setNeedsLayoutGridView; 49 | - (void)OE_layoutGridViewIfNeeded; 50 | - (void)OE_layoutGridView; 51 | 52 | - (void)OE_enqueueCellsAtIndexes:(NSIndexSet *)indexes; 53 | - (void)OE_calculateCachedValuesAndQueryForDataChanges:(BOOL)queryForDataChanges; 54 | - (void)OE_checkForDataReload; 55 | 56 | - (void)OE_setNeedsReloadData; 57 | - (void)OE_reloadDataIfNeeded; 58 | 59 | - (void)OE_centerNoItemsView; 60 | - (void)OE_reorderSublayers; 61 | - (void)OE_updateDecorativeLayers; 62 | 63 | - (NSPoint)OE_pointInViewFromEvent:(NSEvent *)theEvent; 64 | - (OEGridLayer *)OE_gridLayerForPoint:(const NSPoint)point; 65 | 66 | - (NSDragOperation)OE_dragOperationForDestinationLayer:(id)sender; 67 | 68 | - (void)OE_setupFieldEditorForCell:(OEGridViewCell *)cell titleLayer:(CATextLayer *)textLayer; 69 | - (void)OE_cancelFieldEditor; 70 | 71 | @end 72 | 73 | @implementation OEGridView { 74 | NSTrackingArea *_trackingArea; 75 | OEGridLayer *_rootLayer; // Root layer, where all other layers are inserted into 76 | CALayer *_selectionLayer; // Selection box that appears when selecting multiple cells 77 | OEGridLayer *_dragIndicationLayer; // A visual indication that a file is being dragged onto the grid view 78 | NSView *_noItemsView; // A decorative view when there are no items to show, e.g. blank slate 79 | 80 | NSScrollElasticity _previousElasticity; // Caches the original elasticity of the scroller eview before the blank slate is added 81 | 82 | NSMutableIndexSet *_originalSelectionIndexes; // Original set of indexes selected before an inverted (cmd key) selection operation 83 | NSMutableIndexSet *_selectionIndexes; // Index or indexes that are currently selected 84 | NSUInteger _indexOfKeyboardSelection; // Last index of the selected cell using the keyboard 85 | 86 | NSMutableDictionary *_visibleCellByIndex; // Cached visible cells 87 | NSMutableIndexSet *_visibleCellsIndexes; // Cached indexes of the visible cells 88 | NSMutableSet *_reuseableCells; // Cached cells that are no longer in view 89 | 90 | NSDraggingSession *_draggingSession; // Drag session used during a drag operation 91 | OEGridLayer *_prevDragDestinationLayer; // Previous destination cell of a drag operation, used to prevent multiple messages to same cell 92 | OEGridLayer *_dragDestinationLayer; // Destination cell of a drag operation 93 | NSDragOperation _lastDragOperation; // Last drag operation generated by -draggingEntered: 94 | OEGridLayer *_trackingLayer; // The layer receiving all the drag operations (can be root layer) 95 | NSPoint _initialPoint; // Initial position of the mouse of a drag operation 96 | OEGridLayer *_hoveringLayer; 97 | BOOL _needsReloadData; // Determines if the data should be reloaded 98 | BOOL _abortReloadCells; 99 | BOOL _needsLayoutGridView; // Determines if the cells should really be laid out 100 | 101 | NSUInteger _cachedNumberOfVisibleColumns; // Cached number of visible columns 102 | NSUInteger _cachedNumberOfVisibleRows; // Cached number of visiabl rows (include partially visible rows) 103 | NSUInteger _cachedNumberOfItems; // Cached number of items in the data source 104 | NSUInteger _cachedNumberOfRows; // Cached number of rows (including hidden ones) 105 | 106 | NSPoint _cachedContentOffset; // Last known content offset 107 | NSSize _cachedViewSize; // Last known view size 108 | NSSize _cachedItemSize; // Cached cell size that includes row spacing and cached column spacing 109 | CGFloat _cachedColumnSpacing; // Cached column spacing is the dynamic spacing between columns, no less than minimumColumnSpacing 110 | 111 | NSUInteger _supressFrameResize; 112 | OEGridViewFieldEditor *_fieldEditor; // Text field editor of a CATextLayer 113 | 114 | struct 115 | { 116 | unsigned int selectionChanged : 1; 117 | unsigned int doubleClickedCellForItemAtIndex : 1; 118 | unsigned int validateDrop : 1; 119 | unsigned int draggingUpdated : 1; 120 | unsigned int acceptDrop : 1; 121 | unsigned int magnifiedWithEvent : 1; 122 | unsigned int magnifyEndedWithEvent : 1; 123 | } _delegateHas; // Cached methods that the delegate implements 124 | 125 | struct 126 | { 127 | unsigned int viewForNoItemsInGridView : 1; 128 | unsigned int willBeginEditingCellForItemAtIndex : 1; 129 | unsigned int didEndEditingCellForItemAtIndex : 1; 130 | unsigned int pasteboardWriterForIndex : 1; 131 | unsigned int menuForItemsAtIndexes : 1; 132 | } _dataSourceHas; // Cached methods that the dataSource implements 133 | } 134 | 135 | @synthesize foregroundLayer=_foregroundLayer; 136 | @synthesize backgroundLayer=_backgroundLayer; 137 | @synthesize minimumColumnSpacing=_minimumColumnSpacing; 138 | @synthesize rowSpacing=_rowSpacing; 139 | @synthesize itemSize=_itemSize; 140 | @synthesize delegate = _delegate, dataSource = _dataSource; 141 | 142 | - (id)initWithFrame:(NSRect)frame 143 | { 144 | if((self = [super initWithFrame:frame])) 145 | { 146 | [self OE_commonGridViewInit]; 147 | } 148 | 149 | return self; 150 | } 151 | 152 | - (id)initWithCoder:(NSCoder *)aDecoder 153 | { 154 | if((self = [super initWithCoder:aDecoder])) 155 | { 156 | [self OE_commonGridViewInit]; 157 | } 158 | 159 | return self; 160 | } 161 | 162 | - (void)OE_commonGridViewInit 163 | { 164 | // Set default values 165 | _minimumColumnSpacing = 24.0; 166 | _rowSpacing = 20.0; 167 | _itemSize = CGSizeMake(250.0, 250.0); 168 | 169 | // Allocate memory for objects 170 | _selectionIndexes = [[NSMutableIndexSet alloc] init]; 171 | _visibleCellByIndex = [[NSMutableDictionary alloc] init]; 172 | _visibleCellsIndexes = [[NSMutableIndexSet alloc] init]; 173 | _reuseableCells = [[NSMutableSet alloc] init]; 174 | 175 | [self setWantsLayer:YES]; 176 | } 177 | 178 | - (CALayer *)makeBackingLayer 179 | { 180 | CALayer *layer = [[CALayer alloc] init]; 181 | [layer setFrame:[self bounds]]; 182 | 183 | if (!_rootLayer) 184 | { 185 | _rootLayer = [[OEGridLayer alloc] init]; 186 | [_rootLayer setInteractive:YES]; 187 | [_rootLayer setLayoutManager:[OEGridViewLayoutManager layoutManager]]; 188 | [_rootLayer setDelegate:self]; 189 | [_rootLayer setAutoresizingMask:kCALayerWidthSizable | kCALayerHeightSizable]; 190 | [_rootLayer setFrame:[self bounds]]; 191 | CGFloat scaleFactor = [[self window] backingScaleFactor]; 192 | if (!OERunningMountainLion) { 193 | [_rootLayer setGeometryFlipped:YES]; 194 | } 195 | [_rootLayer setContentsScale:scaleFactor]; 196 | 197 | _dragIndicationLayer = [[OEGridLayer alloc] init]; 198 | [_dragIndicationLayer setInteractive:NO]; 199 | [_dragIndicationLayer setBorderColor:[[NSColor colorWithDeviceRed:0.03 green:0.41 blue:0.85 alpha:1.0] CGColor]]; 200 | [_dragIndicationLayer setBorderWidth:2.0]; 201 | [_dragIndicationLayer setCornerRadius:8.0]; 202 | [_dragIndicationLayer setHidden:YES]; 203 | [_dragIndicationLayer setContentsScale:scaleFactor]; 204 | [_rootLayer addSublayer:_dragIndicationLayer]; 205 | 206 | _fieldEditor = [[OEGridViewFieldEditor alloc] initWithFrame:NSMakeRect(50, 50, 50, 50)]; 207 | [self addSubview:_fieldEditor]; 208 | 209 | [self OE_reorderSublayers]; 210 | [self OE_setNeedsReloadData]; 211 | } 212 | 213 | [layer addSublayer:_rootLayer]; 214 | 215 | return layer; 216 | } 217 | 218 | #pragma mark - CALayer delegate 219 | 220 | - (BOOL)layer:(CALayer *)layer shouldInheritContentsScale:(CGFloat)newScale 221 | fromWindow:(NSWindow *)window 222 | { 223 | return YES; 224 | } 225 | 226 | #pragma mark - 227 | #pragma mark Query Data Sources 228 | 229 | - (id)dequeueReusableCell 230 | { 231 | if([_reuseableCells count] == 0) return nil; 232 | 233 | OEGridViewCell *cell = [_reuseableCells anyObject]; 234 | [_reuseableCells removeObject:cell]; 235 | [cell prepareForReuse]; 236 | 237 | return cell; 238 | } 239 | 240 | - (NSUInteger)numberOfItems 241 | { 242 | return _cachedNumberOfItems; 243 | } 244 | 245 | - (OEGridViewCell *)cellForItemAtIndex:(NSUInteger)index makeIfNecessary:(BOOL)necessary 246 | { 247 | OEGridViewCell *result = [_visibleCellByIndex objectForKey:[NSNumber numberWithUnsignedInt:index]]; 248 | if(result == nil && necessary) 249 | { 250 | result = [_dataSource gridView:self cellForItemAtIndex:index]; 251 | [result OE_setIndex:index]; 252 | [result setSelected:[_selectionIndexes containsIndex:index] animated:NO]; 253 | [result setFrame:[self rectForCellAtIndex:index]]; 254 | } 255 | 256 | return result; 257 | } 258 | 259 | #pragma mark - 260 | #pragma mark Query Cells 261 | 262 | - (NSUInteger)indexForCell:(OEGridViewCell *)cell 263 | { 264 | return [cell OE_index]; 265 | } 266 | 267 | - (NSUInteger)indexForCellAtPoint:(NSPoint)point 268 | { 269 | return [[self indexesForCellsInRect:NSMakeRect(point.x, point.y, 1.0, 1.0)] firstIndex]; 270 | } 271 | 272 | - (NSIndexSet *)indexesForCellsInRect:(NSRect)rect 273 | { 274 | // This needs to return both on and off screen cells, make sure that the rect requested is even within the bounds 275 | if(NSIsEmptyRect(rect) || _cachedNumberOfItems == 0) return [NSIndexSet indexSet]; 276 | 277 | // Figure out the first row and column, and the number of cells and rows within the rect. 278 | NSMutableIndexSet *result = [NSMutableIndexSet indexSet]; 279 | const NSUInteger firstCol = (NSUInteger)floor(NSMinX(rect) / _cachedItemSize.width); 280 | const NSUInteger firstRow = (NSUInteger)floor(NSMinY(rect) / _cachedItemSize.height); 281 | const NSUInteger numCols = (NSUInteger)ceil(NSMaxX(rect) / _cachedItemSize.width) - firstCol; 282 | const NSUInteger numRows = (NSUInteger)ceil(NSMaxY(rect) / _cachedItemSize.height) - firstRow; 283 | 284 | // Calculate the starting index 285 | NSUInteger startIndex = firstCol + (firstRow * _cachedNumberOfVisibleColumns); 286 | NSUInteger index; 287 | 288 | // As long as the start index is within the number of known items, then we can return some cells 289 | if(startIndex < _cachedNumberOfItems) 290 | { 291 | // Iterate through each row and column, as a row is iterated move the start index by the number of visible columns. 292 | OEGridViewCell *cell; 293 | NSRect hitRect, frame; 294 | 295 | for(NSUInteger row = 0; row < numRows; row++) 296 | { 297 | index = startIndex; 298 | for(NSUInteger col = 0; col < numCols; col++, index++) 299 | { 300 | if(index >= _cachedNumberOfItems) break; 301 | 302 | cell = [self cellForItemAtIndex:index makeIfNecessary:YES]; 303 | frame = [cell frame]; 304 | hitRect = NSOffsetRect([cell hitRect], NSMinX(frame), NSMinY(frame)); 305 | 306 | if(NSIntersectsRect(rect, hitRect)) [result addIndex:index]; 307 | } 308 | 309 | if(index >= _cachedNumberOfItems) break; 310 | 311 | startIndex += _cachedNumberOfVisibleColumns; 312 | } 313 | } 314 | else 315 | { 316 | result = [NSIndexSet indexSet]; 317 | } 318 | 319 | // Return an immutable copy 320 | return [result copy]; 321 | } 322 | 323 | - (NSArray *)visibleCells 324 | { 325 | return [_visibleCellByIndex allValues]; 326 | } 327 | 328 | - (NSIndexSet *)indexesForVisibleCells 329 | { 330 | // Return an immutable copy 331 | return [_visibleCellsIndexes copy]; 332 | } 333 | 334 | - (NSRect)rectForCellAtIndex:(NSUInteger)index 335 | { 336 | if(index >= _cachedNumberOfItems) return NSZeroRect; 337 | 338 | const NSUInteger col = index % _cachedNumberOfVisibleColumns; 339 | const NSUInteger row = index / _cachedNumberOfVisibleColumns; 340 | 341 | return NSMakeRect(floor(col * _cachedItemSize.width + _cachedColumnSpacing), floor(row * _cachedItemSize.height + (_rowSpacing / 2.f)), _itemSize.width, _itemSize.height); 342 | } 343 | 344 | #pragma mark - 345 | #pragma mark Selection 346 | 347 | - (NSUInteger)indexForSelectedCell 348 | { 349 | return [_selectionIndexes firstIndex]; 350 | } 351 | 352 | - (NSIndexSet *)indexesForSelectedCells 353 | { 354 | // Return an immutable copy 355 | return [_selectionIndexes copy]; 356 | } 357 | 358 | - (void)selectCellAtIndex:(NSUInteger)index 359 | { 360 | if(index == NSNotFound) return; 361 | 362 | OEGridViewCell *item = [self cellForItemAtIndex:index makeIfNecessary:NO]; 363 | [item setSelected:YES animated:![CATransaction disableActions]]; 364 | 365 | [_selectionIndexes addIndex:index]; 366 | 367 | if(_delegateHas.selectionChanged) [_delegate selectionChangedInGridView:self]; 368 | } 369 | 370 | - (void)deselectCellAtIndex:(NSUInteger)index 371 | { 372 | if(index == NSNotFound) return; 373 | 374 | OEGridViewCell *item = [self cellForItemAtIndex:index makeIfNecessary:NO]; 375 | [item setSelected:NO animated:![CATransaction disableActions]]; 376 | 377 | [_selectionIndexes removeIndex:index]; 378 | 379 | if(_delegateHas.selectionChanged) [_delegate selectionChangedInGridView:self]; 380 | } 381 | 382 | - (void)selectAll:(id)sender 383 | { 384 | // We add all the indexes immediately in case the visible cells shift while we are performing this operaiton 385 | [_selectionIndexes addIndexesInRange:NSMakeRange(0, _cachedNumberOfItems)]; 386 | [_visibleCellByIndex enumerateKeysAndObjectsUsingBlock: 387 | ^ (NSNumber *key, OEGridViewCell *obj, BOOL *stop) 388 | { 389 | [obj setSelected:YES animated:YES]; 390 | }]; 391 | 392 | if(_delegateHas.selectionChanged) [_delegate selectionChangedInGridView:self]; 393 | } 394 | 395 | - (void)deselectAll:(id)sender 396 | { 397 | _indexOfKeyboardSelection = NSNotFound; 398 | if([_selectionIndexes count] == 0) return; 399 | 400 | // We remove all the indexes immediately in case the visible cells shift while we are performing this operaiton 401 | [_selectionIndexes removeAllIndexes]; 402 | [_visibleCellByIndex enumerateKeysAndObjectsUsingBlock: 403 | ^ (NSNumber *key, OEGridViewCell *obj, BOOL *stop) 404 | { 405 | [obj setSelected:NO animated:YES]; 406 | }]; 407 | 408 | if(_delegateHas.selectionChanged) [_delegate selectionChangedInGridView:self]; 409 | } 410 | 411 | #pragma mark - 412 | #pragma mark Data Reload 413 | 414 | - (void)OE_enqueueCellsAtIndexes:(NSIndexSet *)indexes 415 | { 416 | if(!indexes || [indexes count] == 0) return; 417 | 418 | [indexes enumerateIndexesUsingBlock: 419 | ^ (NSUInteger idx, BOOL *stop) 420 | { 421 | NSNumber *key = [NSNumber numberWithUnsignedInteger:idx]; 422 | OEGridViewCell *cell = [_visibleCellByIndex objectForKey:key]; 423 | if(cell) 424 | { 425 | if([_fieldEditor delegate] == cell) [self OE_cancelFieldEditor]; 426 | 427 | [_visibleCellByIndex removeObjectForKey:key]; 428 | [_reuseableCells addObject:cell]; 429 | [cell removeFromSuperlayer]; 430 | } 431 | }]; 432 | } 433 | 434 | - (void)OE_calculateCachedValuesAndQueryForDataChanges:(BOOL)shouldQueryForDataChanges 435 | { 436 | // Collect some basic information of the current environment 437 | NSScrollView *enclosingScrollView = [self enclosingScrollView]; 438 | NSRect visibleRect = [enclosingScrollView documentVisibleRect]; 439 | NSPoint contentOffset = visibleRect.origin; 440 | 441 | const NSSize cachedContentSize = [self bounds].size; 442 | const NSSize viewSize = visibleRect.size; 443 | 444 | // These variables help determine if the calculated values are different than their cached counter parts. These values 445 | // are recalculated only if needed, so they are all initialized with their cached counter parts. If the recalculated 446 | // values do not change from their cached counter parts, then there is nothing that we need to do. 447 | NSUInteger numberOfVisibleColumns = _cachedNumberOfVisibleColumns; // Number of visible columns 448 | NSUInteger numberOfVisibleRows = _cachedNumberOfVisibleRows; // Number of visible rows 449 | NSUInteger numberOfItems = _cachedNumberOfItems; // Number of items in the data source 450 | NSUInteger numberOfRows = _cachedNumberOfRows; 451 | NSSize itemSize = NSMakeSize(_itemSize.width + _minimumColumnSpacing, _itemSize.height + _rowSpacing); 452 | // Item Size (within minimumColumnSpacing and rowSpacing) 453 | NSSize contentSize = cachedContentSize; // The scroll view's content size 454 | BOOL checkForDataReload = FALSE; // Used to determine if we should consider reloading the data 455 | 456 | // Query the data source for the number of items it has, this is only done if the caller explicitly sets shouldQueryForDataChanges. 457 | if(shouldQueryForDataChanges && _dataSource) numberOfItems = [_dataSource numberOfItemsInGridView:self]; 458 | numberOfRows = ceil((CGFloat)numberOfItems / MAX((CGFloat)numberOfVisibleColumns, 1)); 459 | 460 | // Check to see if the frame's width has changed to update the number of visible columns and the cached cell size 461 | if(itemSize.width == 0) 462 | { 463 | numberOfVisibleColumns = 1; 464 | numberOfRows = ceil((CGFloat)numberOfItems / MAX((CGFloat)numberOfVisibleColumns, 1)); 465 | } 466 | else if(_cachedViewSize.width != viewSize.width || !NSEqualSizes(_cachedItemSize, itemSize)) 467 | { 468 | // Set the number of visible columns based on the view's width, there must be at least 1 visible column and no more than the total number 469 | // of items within the data source. Just because a column is potentially visible doesn't mean that there is enough data to populate it. 470 | numberOfVisibleColumns = MAX((NSUInteger)(floor(viewSize.width / itemSize.width)), 1); 471 | numberOfRows = ceil((CGFloat)numberOfItems / MAX((CGFloat)numberOfVisibleColumns, 1)); 472 | 473 | // The cell's height include the original itemSize.height + rowSpacing. The cell's column spacing is based on the number of visible columns. 474 | // The cell will be at least itemSize.width + minimumColumnSpacing, it could grow as larg as the width of the view 475 | itemSize = NSMakeSize(MAX(itemSize.width, round(viewSize.width / numberOfVisibleColumns)), itemSize.height); 476 | 477 | // Make sure that the scroll view's content width reflects the view's width. The scroll view's content height is be calculated later (if 478 | // needed). 479 | contentSize.width = viewSize.width; 480 | } 481 | 482 | // Check to see if the frame's height has changed to update the number of visible rows 483 | if(itemSize.height == 0) 484 | { 485 | numberOfVisibleRows = 1; 486 | } 487 | else if(_cachedViewSize.height != viewSize.height || itemSize.height != _cachedItemSize.height) 488 | { 489 | numberOfVisibleRows = (NSUInteger)ceil(viewSize.height / itemSize.height); 490 | } 491 | 492 | // Check to see if the number of items, number of visible columns, or cached cell size has changed 493 | if((_cachedNumberOfRows != numberOfRows) || (_cachedNumberOfItems != numberOfItems) || (_cachedNumberOfVisibleColumns != numberOfVisibleColumns) || !NSEqualSizes(_cachedItemSize, itemSize) || !NSEqualSizes(_cachedViewSize, viewSize)) 494 | { 495 | // These three events may require a data reload but will most definitely cause the scroll view's content size to change 496 | checkForDataReload = YES; 497 | 498 | if(numberOfItems == 0) 499 | { 500 | contentSize.height = viewSize.height; 501 | 502 | // If we previously had items and now we don't, then add the no items view 503 | if(_cachedNumberOfItems > 0) [self OE_addNoItemsView]; 504 | } 505 | else 506 | { 507 | contentSize.height = MAX(viewSize.height, ceil(numberOfRows * itemSize.height)); 508 | [self OE_removeNoItemsView]; 509 | } 510 | ++_supressFrameResize; 511 | [super setFrameSize:contentSize]; 512 | [enclosingScrollView reflectScrolledClipView:(NSClipView *)[self superview]]; 513 | --_supressFrameResize; 514 | 515 | // Changing the size of the frame may also change the contentOffset, recalculate that value 516 | visibleRect = [enclosingScrollView documentVisibleRect]; 517 | contentOffset = visibleRect.origin; 518 | 519 | // Check to see if the number visible columns or the cell size has changed as these vents will cause the layout to be recalculated 520 | if(_cachedNumberOfVisibleColumns != numberOfVisibleColumns || !NSEqualSizes(_cachedItemSize, itemSize)) [self OE_setNeedsLayoutGridView]; 521 | } 522 | 523 | // Check to see if the number of visible rows have changed 524 | // Check to see if the scroll view's content offset or the view's height has changed 525 | if((_cachedNumberOfVisibleRows != numberOfVisibleRows) || (_cachedContentOffset.y != contentOffset.y) || (_cachedViewSize.height != viewSize.height)) 526 | { 527 | // This event may require a data reload 528 | checkForDataReload = YES; 529 | } 530 | 531 | // Update the cached values 532 | _cachedViewSize = viewSize; 533 | _cachedItemSize = itemSize; 534 | _cachedColumnSpacing = round((itemSize.width - _itemSize.width) / 2.0); 535 | _cachedNumberOfVisibleColumns = numberOfVisibleColumns; 536 | _cachedNumberOfVisibleRows = numberOfVisibleRows; 537 | _cachedNumberOfItems = numberOfItems; 538 | _cachedNumberOfRows = numberOfRows; 539 | _cachedContentOffset = contentOffset; 540 | 541 | // Reload data when appropriate 542 | if(checkForDataReload) [self OE_checkForDataReload]; 543 | } 544 | 545 | - (void)OE_checkForDataReload 546 | { 547 | if(_cachedNumberOfItems == 0) return; 548 | 549 | // Check to see if the visible cells have changed 550 | const CGFloat contentOffsetY = NSMinY([[self enclosingScrollView] documentVisibleRect]); 551 | const NSUInteger firstVisibleIndex = MAX((NSInteger)floor(contentOffsetY / _cachedItemSize.height) - 1, 0) * _cachedNumberOfVisibleColumns; 552 | const NSUInteger numberOfVisibleCells = _cachedNumberOfVisibleColumns * (_cachedNumberOfVisibleRows + 2); 553 | 554 | NSIndexSet *visibleCellsIndexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(firstVisibleIndex, MIN(numberOfVisibleCells, _cachedNumberOfItems - firstVisibleIndex))]; 555 | if ([_visibleCellsIndexes isEqualToIndexSet:visibleCellsIndexSet]) return; 556 | 557 | // Calculate which cells to remove from view 558 | if([_visibleCellsIndexes count] != 0) 559 | { 560 | NSMutableIndexSet *removeIndexSet = [_visibleCellsIndexes mutableCopy]; 561 | [removeIndexSet removeIndexes:visibleCellsIndexSet]; 562 | 563 | if([removeIndexSet count] != 0) [self OE_enqueueCellsAtIndexes:removeIndexSet]; 564 | } 565 | 566 | // Calculate which cells to add to view 567 | NSMutableIndexSet *addIndexSet = [visibleCellsIndexSet mutableCopy]; 568 | if([_visibleCellsIndexes count] != 0) 569 | { 570 | [addIndexSet removeIndexes:_visibleCellsIndexes]; 571 | [_visibleCellsIndexes removeAllIndexes]; 572 | } 573 | 574 | // Update the visible cells index set 575 | [_visibleCellsIndexes addIndexes:visibleCellsIndexSet]; 576 | 577 | if([addIndexSet count] != 0) [self reloadCellsAtIndexes:addIndexSet]; 578 | } 579 | 580 | - (void)OE_setNeedsReloadData 581 | { 582 | _needsReloadData = YES; 583 | [_rootLayer setNeedsLayout]; 584 | } 585 | 586 | - (void)OE_reloadDataIfNeeded 587 | { 588 | if(_needsReloadData) [self reloadData]; 589 | } 590 | 591 | - (void)noteNumberOfCellsChanged 592 | { 593 | [self OE_calculateCachedValuesAndQueryForDataChanges:YES]; 594 | } 595 | 596 | - (void)OE_removeNoItemsView 597 | { 598 | if(_noItemsView != nil) 599 | { 600 | [_noItemsView removeFromSuperview]; 601 | _noItemsView = nil; 602 | [[self enclosingScrollView] setVerticalScrollElasticity:_previousElasticity]; 603 | [self OE_setNeedsLayoutGridView]; 604 | } 605 | } 606 | 607 | - (void)OE_addNoItemsView 608 | { 609 | // Enqueue all the cells for later use and remove them from the view 610 | [self OE_enqueueCellsAtIndexes:_visibleCellsIndexes]; 611 | [_visibleCellsIndexes removeAllIndexes]; 612 | 613 | // Check to see if the dataSource has a view to display when there is nothing to display 614 | if(_dataSourceHas.viewForNoItemsInGridView) 615 | { 616 | _noItemsView = [_dataSource viewForNoItemsInGridView:self]; 617 | if(_noItemsView) 618 | { 619 | NSScrollView *enclosingScrollView = [self enclosingScrollView]; 620 | 621 | [self addSubview:_noItemsView]; 622 | [_noItemsView setHidden:NO]; 623 | [self OE_centerNoItemsView]; 624 | _previousElasticity = [enclosingScrollView verticalScrollElasticity]; 625 | [enclosingScrollView setVerticalScrollElasticity:NSScrollElasticityNone]; 626 | [self OE_setNeedsLayoutGridView]; 627 | } 628 | } 629 | } 630 | 631 | - (void)reloadData 632 | { 633 | [_selectionIndexes removeAllIndexes]; 634 | _indexOfKeyboardSelection = NSNotFound; 635 | 636 | [self OE_enqueueCellsAtIndexes:_visibleCellsIndexes]; 637 | [_visibleCellsIndexes removeAllIndexes]; 638 | [_reuseableCells removeAllObjects]; 639 | 640 | _cachedNumberOfVisibleColumns = 0; 641 | _cachedNumberOfVisibleRows = 0; 642 | _cachedNumberOfItems = 0; 643 | 644 | _cachedContentOffset = NSZeroPoint; 645 | _cachedViewSize = NSZeroSize; 646 | _cachedItemSize = NSZeroSize; 647 | _cachedColumnSpacing = 0.0; 648 | 649 | [self OE_removeNoItemsView]; 650 | 651 | // Recalculate all of the required cached values 652 | [self OE_calculateCachedValuesAndQueryForDataChanges:YES]; 653 | if(_cachedNumberOfItems == 0) [self OE_addNoItemsView]; 654 | 655 | _needsReloadData = NO; 656 | } 657 | 658 | - (void)reloadCellsAtIndexes:(NSIndexSet *)indexes 659 | { 660 | // If there is no index set or no items in the index set, then there is nothing to update 661 | if([indexes count] == 0) return; 662 | 663 | [indexes enumerateIndexesUsingBlock: 664 | ^ (NSUInteger idx, BOOL *stop) 665 | { 666 | // If the cell is not already visible, then there is nothing to reload 667 | if([_visibleCellsIndexes containsIndex:idx]) 668 | { 669 | OEGridViewCell *newCell = [_dataSource gridView:self cellForItemAtIndex:idx]; 670 | OEGridViewCell *oldCell = [self cellForItemAtIndex:idx makeIfNecessary:NO]; 671 | if(newCell != oldCell) 672 | { 673 | if(oldCell) [newCell setFrame:[oldCell frame]]; 674 | 675 | // Prepare the new cell for insertion 676 | if (newCell) 677 | { 678 | [newCell OE_setIndex:idx]; 679 | [newCell setSelected:[_selectionIndexes containsIndex:idx] animated:NO]; 680 | 681 | // Replace the old cell with the new cell 682 | if(oldCell) 683 | { 684 | [self OE_enqueueCellsAtIndexes:[NSIndexSet indexSetWithIndex:[oldCell OE_index]]]; 685 | } 686 | [newCell setOpacity:1.0]; 687 | [newCell setHidden:NO]; 688 | 689 | if(!oldCell) [newCell setFrame:[self rectForCellAtIndex:idx]]; 690 | 691 | [_visibleCellByIndex setObject:newCell forKey:[NSNumber numberWithUnsignedInteger:idx]]; 692 | [_rootLayer addSublayer:newCell]; 693 | } 694 | 695 | [self OE_setNeedsLayoutGridView]; 696 | } 697 | } 698 | }]; 699 | [self OE_reorderSublayers]; 700 | } 701 | 702 | #pragma mark - 703 | #pragma mark View Operations 704 | 705 | - (BOOL)isFlipped 706 | { 707 | return YES; 708 | } 709 | 710 | 711 | - (void)updateTrackingAreas 712 | { 713 | [super updateTrackingAreas]; 714 | if (_trackingArea) { [self removeTrackingArea:_trackingArea]; } 715 | NSTrackingAreaOptions options = (NSTrackingMouseMoved | NSTrackingMouseEnteredAndExited | NSTrackingActiveInKeyWindow); 716 | _trackingArea = [[NSTrackingArea alloc] initWithRect:[self bounds] options:options owner:self userInfo:nil]; 717 | [self addTrackingArea:_trackingArea]; 718 | } 719 | 720 | 721 | - (void)viewWillMoveToWindow:(NSWindow *)newWindow 722 | { 723 | NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; 724 | NSWindow *oldWindow = [self window]; 725 | 726 | if(oldWindow) 727 | { 728 | [notificationCenter removeObserver:self name:NSWindowDidBecomeKeyNotification object:oldWindow]; 729 | [notificationCenter removeObserver:self name:NSWindowDidResignKeyNotification object:oldWindow]; 730 | } 731 | 732 | if(newWindow) 733 | { 734 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(OE_windowChangedKey:) name:NSWindowDidBecomeKeyNotification object:[self window]]; 735 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(OE_windowChangedKey:) name:NSWindowDidResignKeyNotification object:[self window]]; 736 | } 737 | [self updateTrackingAreas]; 738 | } 739 | 740 | - (void)viewWillMoveToSuperview:(NSView *)newSuperview 741 | { 742 | NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; 743 | NSClipView *newClipView = ([newSuperview isKindOfClass:[NSClipView class]] ? (NSClipView *)newSuperview : nil); 744 | NSClipView *oldClipView = [[self enclosingScrollView] contentView]; 745 | 746 | if(oldClipView) 747 | { 748 | [notificationCenter removeObserver:self name:NSViewBoundsDidChangeNotification object:oldClipView]; 749 | [notificationCenter removeObserver:self name:NSViewFrameDidChangeNotification object:oldClipView]; 750 | } 751 | 752 | if(newClipView) 753 | { 754 | // TODO: I think there is some optimization we can do here 755 | [self setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; 756 | [notificationCenter addObserver:self selector:@selector(OE_clipViewBoundsChanged:) name:NSViewBoundsDidChangeNotification object:newClipView]; 757 | [notificationCenter addObserver:self selector:@selector(OE_clipViewFrameChanged:) name:NSViewFrameDidChangeNotification object:newClipView]; 758 | [newClipView setPostsBoundsChangedNotifications:YES]; 759 | [newClipView setPostsFrameChangedNotifications:YES]; 760 | } 761 | } 762 | 763 | - (void)OE_updateSelectedCellsActiveSelectorWithFocus:(BOOL)focus 764 | { 765 | if(([_selectionIndexes count] == 0) || ([_selectionIndexes lastIndex] < [_visibleCellsIndexes firstIndex]) || ([_selectionIndexes firstIndex] > [_visibleCellsIndexes lastIndex])) return; 766 | 767 | NSMutableIndexSet *visibleAndSelected = [_selectionIndexes mutableCopy]; 768 | [visibleAndSelected removeIndexesInRange:NSMakeRange([_selectionIndexes firstIndex], [_visibleCellsIndexes firstIndex] - [_selectionIndexes firstIndex])]; 769 | [visibleAndSelected removeIndexesInRange:NSMakeRange([_visibleCellsIndexes lastIndex] + 1, [_selectionIndexes lastIndex] - [_visibleCellsIndexes lastIndex])]; 770 | 771 | if([visibleAndSelected count] > 0) 772 | { 773 | [visibleAndSelected enumerateIndexesUsingBlock: 774 | ^ (NSUInteger idx, BOOL *stop) 775 | { 776 | OEGridViewCell *cell = [self cellForItemAtIndex:idx makeIfNecessary:NO]; 777 | if(cell) 778 | { 779 | if(focus) [cell didBecomeFocused]; 780 | else [cell willResignFocus]; 781 | } 782 | }]; 783 | } 784 | } 785 | 786 | - (void)OE_windowChangedKey:(NSNotification *)notification 787 | { 788 | if([notification name] == NSWindowDidBecomeKeyNotification) [self OE_updateSelectedCellsActiveSelectorWithFocus:YES]; 789 | else if([notification name] == NSWindowDidResignKeyNotification) [self OE_updateSelectedCellsActiveSelectorWithFocus:NO]; 790 | } 791 | 792 | - (void)OE_clipViewFrameChanged:(NSNotification *)notification 793 | { 794 | // Return immediately if this method is being surpressed. 795 | if(_supressFrameResize > 0) return; 796 | [self OE_updateDecorativeLayers]; 797 | 798 | if(_noItemsView) 799 | { 800 | [self setFrame:[[self enclosingScrollView] bounds]]; 801 | [self OE_centerNoItemsView]; 802 | } 803 | const NSRect visibleRect = [[self enclosingScrollView] documentVisibleRect]; 804 | if(!NSEqualSizes(_cachedViewSize, visibleRect.size)) 805 | { 806 | [self OE_cancelFieldEditor]; 807 | [self OE_calculateCachedValuesAndQueryForDataChanges:NO]; 808 | } 809 | } 810 | 811 | - (void)OE_clipViewBoundsChanged:(NSNotification *)notification 812 | { 813 | [self OE_updateDecorativeLayers]; 814 | const NSRect visibleRect = [[self enclosingScrollView] documentVisibleRect]; 815 | if(abs(_cachedContentOffset.y - visibleRect.origin.y) > _itemSize.height) 816 | { 817 | _cachedContentOffset = visibleRect.origin; 818 | [self OE_checkForDataReload]; 819 | } 820 | } 821 | 822 | - (void)OE_centerNoItemsView 823 | { 824 | if(!_noItemsView) return; 825 | 826 | const NSRect visibleRect = [[self enclosingScrollView] visibleRect]; 827 | const NSSize viewSize = [_noItemsView frame].size; 828 | const NSRect viewFrame = NSMakeRect(ceil((NSWidth(visibleRect) - viewSize.width) / 2.0), ceil((NSHeight(visibleRect) - viewSize.height) / 2.0), viewSize.width, viewSize.height); 829 | [_noItemsView setFrame:viewFrame]; 830 | } 831 | 832 | #pragma mark - 833 | #pragma mark Layer Operations 834 | 835 | - (id)actionForLayer:(CALayer *)layer forKey:(NSString *)event 836 | { 837 | return [NSNull null]; 838 | } 839 | 840 | - (void)OE_reorderSublayers 841 | { 842 | [_rootLayer insertSublayer:_backgroundLayer atIndex:0]; 843 | 844 | unsigned int index = (unsigned int)[[_rootLayer sublayers] count]; 845 | [_rootLayer insertSublayer:_foregroundLayer atIndex:index]; 846 | [_rootLayer insertSublayer:_selectionLayer atIndex:index]; 847 | [_rootLayer insertSublayer:_dragIndicationLayer atIndex:index]; 848 | } 849 | 850 | - (void)OE_updateDecorativeLayers 851 | { 852 | if(!_dragIndicationLayer && !_backgroundLayer && !_foregroundLayer) return; 853 | 854 | [CATransaction begin]; 855 | [CATransaction setDisableActions:YES]; 856 | const NSRect decorativeFrame = [[self enclosingScrollView] documentVisibleRect]; 857 | [_backgroundLayer setFrame:decorativeFrame]; 858 | [_foregroundLayer setFrame:decorativeFrame]; 859 | [_dragIndicationLayer setFrame:NSInsetRect(decorativeFrame, 1.0, 1.0)]; 860 | [CATransaction commit]; 861 | } 862 | 863 | - (void)OE_setNeedsLayoutGridView 864 | { 865 | _needsLayoutGridView = YES; 866 | [_rootLayer setNeedsLayout]; 867 | } 868 | 869 | - (void)OE_layoutGridViewIfNeeded 870 | { 871 | // -layoutSublayers is called for every little thing, this checks to see if we really intended to adjust the location of the cells. This value can 872 | // be set using OE_setNeedsLayoutGridView 873 | if(_needsLayoutGridView) [self OE_layoutGridView]; 874 | } 875 | 876 | - (void)OE_layoutGridView 877 | { 878 | if([_visibleCellByIndex count] == 0) return; 879 | 880 | [_visibleCellByIndex enumerateKeysAndObjectsUsingBlock: 881 | ^ (NSNumber *key, OEGridViewCell *obj, BOOL *stop) 882 | { 883 | [obj setFrame:[self rectForCellAtIndex:[key unsignedIntegerValue]]]; 884 | }]; 885 | 886 | _needsLayoutGridView = NO; 887 | } 888 | 889 | - (void)layoutSublayers 890 | { 891 | [self OE_reloadDataIfNeeded]; 892 | [self OE_updateDecorativeLayers]; 893 | [self OE_layoutGridViewIfNeeded]; 894 | } 895 | 896 | #pragma mark - 897 | #pragma mark Responder Chain 898 | 899 | - (BOOL)acceptsFirstResponder 900 | { 901 | return YES; 902 | } 903 | 904 | - (BOOL)becomeFirstResponder 905 | { 906 | [self OE_updateSelectedCellsActiveSelectorWithFocus:YES]; 907 | return YES; 908 | } 909 | 910 | - (BOOL)resignFirstResponder 911 | { 912 | [self OE_updateSelectedCellsActiveSelectorWithFocus:NO]; 913 | return YES; 914 | } 915 | 916 | #pragma mark - 917 | #pragma mark Mouse Handling Operations 918 | 919 | - (BOOL)acceptsFirstMouse:(NSEvent *)theEvent 920 | { 921 | return YES; 922 | } 923 | 924 | - (NSPoint)OE_pointInViewFromEvent:(NSEvent *)theEvent 925 | { 926 | return [self convertPoint:[theEvent locationInWindow] fromView:nil]; 927 | } 928 | 929 | - (OEGridLayer *)OE_gridLayerForPoint:(const NSPoint)point 930 | { 931 | CALayer *hitLayer = [_rootLayer hitTest:[self convertPointToLayer:point]]; 932 | return ([hitLayer isKindOfClass:[OEGridLayer class]] ? (OEGridLayer *)hitLayer : nil); 933 | } 934 | 935 | - (void)mouseDown:(NSEvent *)theEvent 936 | { 937 | const NSPoint pointInView = [self OE_pointInViewFromEvent:theEvent]; 938 | _trackingLayer = [self OE_gridLayerForPoint:pointInView]; 939 | 940 | if(![_trackingLayer isInteractive]) _trackingLayer = _rootLayer; 941 | 942 | OEGridViewCell *cell = nil; 943 | if ([_trackingLayer isKindOfClass:[OEGridViewCell class]]) cell = (OEGridViewCell *)_trackingLayer; 944 | 945 | if(cell == nil && _trackingLayer != nil && _trackingLayer != _rootLayer) 946 | { 947 | const NSPoint pointInLayer = [_rootLayer convertPoint:pointInView toLayer:_trackingLayer]; 948 | [_trackingLayer mouseDownAtPointInLayer:pointInLayer withEvent:theEvent]; 949 | if(![_trackingLayer isTracking]) _trackingLayer = nil; 950 | } 951 | 952 | const NSUInteger modifierFlags = [[NSApp currentEvent] modifierFlags]; 953 | const BOOL commandKeyDown = ((modifierFlags & NSCommandKeyMask) == NSCommandKeyMask); 954 | const BOOL shiftKeyDown = ((modifierFlags & NSShiftKeyMask) == NSShiftKeyMask); 955 | const BOOL invertSelection = commandKeyDown || shiftKeyDown; 956 | 957 | // Figure out which cell was touched, inverse it's selection... 958 | if(cell != nil) 959 | { 960 | if(!invertSelection && ![cell isSelected]) [self deselectAll:self]; 961 | 962 | NSUInteger idx = [cell OE_index]; 963 | if(![_selectionIndexes containsIndex:idx]) 964 | { 965 | [self selectCellAtIndex:idx]; 966 | _indexOfKeyboardSelection = idx; 967 | } 968 | else if(invertSelection) 969 | { 970 | [self deselectCellAtIndex:idx]; 971 | _indexOfKeyboardSelection = [_selectionIndexes lastIndex]; 972 | } 973 | } 974 | else if(_trackingLayer == nil || _trackingLayer == _rootLayer) 975 | { 976 | _trackingLayer = _rootLayer; 977 | 978 | if(!invertSelection) [self deselectAll:self]; 979 | 980 | // If the command key was pressed and there are already a list of selected indexes, then we may want to invert the items that are already selected 981 | if(invertSelection && [_selectionIndexes count] > 0) _originalSelectionIndexes = [_selectionIndexes copy]; 982 | } 983 | 984 | // Start tracking mouse 985 | NSEvent *lastMouseDragEvent = nil; 986 | const BOOL isTrackingRootLayer = (_trackingLayer == _rootLayer); 987 | const NSUInteger eventMask = NSLeftMouseUpMask | NSLeftMouseDraggedMask | NSKeyDownMask | (isTrackingRootLayer ? NSPeriodicMask : 0); 988 | _initialPoint = pointInView; 989 | 990 | // If we are tracking the root layer then we are dragging a selection box, fire off periodic events so that we can autoscroll the view 991 | if(isTrackingRootLayer) [NSEvent startPeriodicEventsAfterDelay:OEInitialPeriodicDelay withPeriod:OEPeriodicInterval]; 992 | 993 | // Keep tracking as long as we are tracking a layer and there are events in the queue 994 | while(_trackingLayer && (theEvent = [[self window] nextEventMatchingMask:eventMask])) 995 | { 996 | if(isTrackingRootLayer && [theEvent type] == NSPeriodic) 997 | { 998 | // Refire last mouse drag event when perioidc events are encountered 999 | if(lastMouseDragEvent) 1000 | { 1001 | [self mouseDragged:lastMouseDragEvent]; 1002 | 1003 | // Stop tracking last mouse drag event if we've reached the bottom or top of the scrollable area 1004 | const NSRect visibleRect = [[self enclosingScrollView] documentVisibleRect]; 1005 | const NSRect bounds = [self bounds]; 1006 | if (NSMinY(bounds) == NSMinY(visibleRect) || NSMaxY(bounds) == NSMaxY(visibleRect)) lastMouseDragEvent = nil; 1007 | } 1008 | } 1009 | else if([theEvent type] == NSLeftMouseDragged) 1010 | { 1011 | const NSPoint point = [self convertPoint:[theEvent locationInWindow] fromView:nil]; 1012 | lastMouseDragEvent = (NSPointInRect(point, [self visibleRect]) ? nil : theEvent); 1013 | [self mouseDragged:theEvent]; 1014 | } 1015 | else if([theEvent type] == NSLeftMouseUp) 1016 | { 1017 | [self mouseUp:theEvent]; 1018 | break; 1019 | } 1020 | else if([theEvent type] == NSKeyDown) 1021 | { 1022 | NSBeep(); 1023 | } 1024 | } 1025 | 1026 | lastMouseDragEvent = nil; 1027 | _trackingLayer = nil; 1028 | 1029 | if(isTrackingRootLayer) [NSEvent stopPeriodicEvents]; 1030 | } 1031 | 1032 | - (void)mouseDragged:(NSEvent *)theEvent 1033 | { 1034 | // Exit immediately if the noItemsView is visible or if we are not tracking anything 1035 | if(_trackingLayer == nil || _noItemsView != nil) return; 1036 | 1037 | if([_trackingLayer isKindOfClass:[OEGridViewCell class]]) 1038 | { 1039 | if(_dataSourceHas.pasteboardWriterForIndex && [_selectionIndexes count] > 0) 1040 | { 1041 | // Don't start dragging a cell until the mouse has traveled at least 5 pixels in any direction 1042 | const NSPoint pointInView = [self OE_pointInViewFromEvent:theEvent]; 1043 | const NSPoint draggedDistance = NSMakePoint(ABS(pointInView.x - _initialPoint.x), ABS(pointInView.y - _initialPoint.y)); 1044 | if(draggedDistance.x >= 5.0 || draggedDistance.y >= 5.0 || (draggedDistance.x * draggedDistance.x + draggedDistance.y * draggedDistance.y) >= 25) 1045 | { 1046 | __block NSMutableArray *draggingItems = [NSMutableArray array]; 1047 | [_selectionIndexes enumerateIndexesUsingBlock: 1048 | ^ (NSUInteger idx, BOOL *stop) 1049 | { 1050 | id item = [_dataSource gridView:self pasteboardWriterForIndex:idx]; 1051 | if(item != nil) 1052 | { 1053 | NSDraggingItem *dragItem = [[NSDraggingItem alloc] initWithPasteboardWriter:item]; 1054 | OEGridViewCell *cell = [self cellForItemAtIndex:idx makeIfNecessary:YES]; 1055 | [dragItem setDraggingFrame:NSOffsetRect([cell hitRect], NSMinX([cell frame]), NSMinY([cell frame])) contents:[cell draggingImage]]; 1056 | [draggingItems addObject:dragItem]; 1057 | } 1058 | }]; 1059 | 1060 | // If there are items being dragged, start a dragging session 1061 | if([draggingItems count] > 0) 1062 | { 1063 | _draggingSession = [self beginDraggingSessionWithItems:draggingItems event:theEvent source:self]; 1064 | [_draggingSession setDraggingFormation:NSDraggingFormationStack]; 1065 | } 1066 | 1067 | // Cacnel the tracking layer (which will cancel the event tracking loop). The dragging session has it's own mouse tracking loop. 1068 | _trackingLayer = nil; 1069 | } 1070 | } 1071 | else 1072 | { 1073 | _trackingLayer = nil; 1074 | } 1075 | } 1076 | else if(_trackingLayer != _rootLayer) 1077 | { 1078 | // Forward drag event to the OEGridLayer that is being tracked 1079 | const NSPoint pointInLayer = [_rootLayer convertPoint:[self OE_pointInViewFromEvent:theEvent] toLayer:_trackingLayer]; 1080 | [_trackingLayer mouseMovedAtPointInLayer:pointInLayer withEvent:theEvent]; 1081 | } 1082 | else 1083 | { 1084 | // Make sure that the view is scrolled 1085 | [self autoscroll:theEvent]; 1086 | 1087 | // Calculate the selection rect 1088 | const NSPoint pointInView = [self OE_pointInViewFromEvent:theEvent]; 1089 | const CGRect bounds = [self bounds]; 1090 | const CGPoint minPoint = CGPointMake(MAX(MIN(pointInView.x, _initialPoint.x), 0.0), MAX(MIN(pointInView.y, _initialPoint.y), 1.0)); 1091 | const CGPoint maxPoint = CGPointMake(MIN(MAX(pointInView.x, _initialPoint.x), CGRectGetMaxX(bounds)), MIN(MAX(pointInView.y, _initialPoint.y), CGRectGetMaxY(bounds))); 1092 | const CGRect selectionRect = { .origin = minPoint, .size = { maxPoint.x - minPoint.x, maxPoint.y - minPoint.y }}; 1093 | 1094 | [CATransaction begin]; 1095 | [CATransaction setDisableActions:YES]; 1096 | 1097 | // Create the selection view if it doesn't exisit...set the frame to the previous calculation 1098 | if(_selectionLayer == nil) 1099 | { 1100 | _selectionLayer = [[CALayer alloc] init]; 1101 | [_selectionLayer setBackgroundColor:[[NSColor colorWithCalibratedWhite:1.0 alpha:0.3] CGColor]]; 1102 | [_selectionLayer setBorderColor:[[NSColor whiteColor] CGColor]]; 1103 | [_selectionLayer setBorderWidth:1.0]; 1104 | [_rootLayer addSublayer:_selectionLayer]; 1105 | [self OE_reorderSublayers]; 1106 | } 1107 | 1108 | [_selectionLayer setFrame:CGRectIntegral(selectionRect)]; 1109 | 1110 | [CATransaction commit]; 1111 | 1112 | // Determine which cells to select and which ones to deselect 1113 | NSIndexSet *indexesUnderSelectionRect = [self indexesForCellsInRect:selectionRect]; 1114 | NSMutableIndexSet *indexesToSelect = nil; 1115 | NSMutableIndexSet *indexesToDeselect = nil; 1116 | 1117 | if(_originalSelectionIndexes) 1118 | { 1119 | /** Invert the selection */ 1120 | // Calculate the new indexes to select...it should be: 1121 | // indexesToSelect = (_originalSelectionIndexes + indexesUnderSelectionRect) - (_originalSelectionIndexes X indexesUnderSelectionRect). 1122 | indexesToSelect = [_originalSelectionIndexes mutableCopy]; 1123 | [indexesToSelect addIndexes:indexesUnderSelectionRect]; 1124 | 1125 | if([indexesUnderSelectionRect firstIndex] != NSNotFound) 1126 | { 1127 | const NSUInteger firstIndex = [indexesUnderSelectionRect firstIndex]; 1128 | const NSUInteger lastIndex = [indexesUnderSelectionRect lastIndex]; 1129 | NSIndexSet *intersection = [_originalSelectionIndexes indexesInRange:NSMakeRange(firstIndex, lastIndex - firstIndex + 1) 1130 | options:0 1131 | passingTest: 1132 | ^ BOOL (NSUInteger idx, BOOL *stop) 1133 | { 1134 | return [indexesUnderSelectionRect containsIndex:idx]; 1135 | }]; 1136 | 1137 | [indexesToSelect removeIndexes:intersection]; 1138 | } 1139 | } 1140 | else 1141 | { 1142 | /** Select the indexes under selection rect */ 1143 | indexesToSelect = [indexesUnderSelectionRect mutableCopy]; 1144 | } 1145 | 1146 | BOOL selectionChanged = NO; 1147 | if([indexesToDeselect firstIndex] != NSNotFound) 1148 | { 1149 | // Figure out which indexes that are currently selected that need to be deslected: indexesToDeselect = _selectionIndexes - indexesToSelect 1150 | indexesToDeselect = [_selectionIndexes mutableCopy]; 1151 | [indexesToDeselect removeIndexes:indexesToSelect]; 1152 | [indexesToDeselect enumerateIndexesUsingBlock: 1153 | ^ (NSUInteger idx, BOOL *stop) 1154 | { 1155 | [[self cellForItemAtIndex:idx makeIfNecessary:NO] setSelected:NO animated:YES]; 1156 | }]; 1157 | [_selectionIndexes removeIndexes:indexesToDeselect]; 1158 | selectionChanged = YES; 1159 | } 1160 | 1161 | if([indexesToSelect firstIndex] != NSNotFound) 1162 | { 1163 | // Figure out which indexes that are not selected that need to be selected: indexesToSelect = _selectionIndexes - indexesToDeselect 1164 | [indexesToSelect removeIndexes:_selectionIndexes]; 1165 | [indexesToSelect enumerateIndexesUsingBlock: 1166 | ^ (NSUInteger idx, BOOL *stop) 1167 | { 1168 | [[self cellForItemAtIndex:idx makeIfNecessary:NO] setSelected:YES animated:YES]; 1169 | }]; 1170 | [_selectionIndexes addIndexes:indexesToSelect]; 1171 | selectionChanged = YES; 1172 | } 1173 | 1174 | if(selectionChanged && _delegateHas.selectionChanged) [_delegate selectionChangedInGridView:self]; 1175 | 1176 | _indexOfKeyboardSelection = [_selectionIndexes lastIndex]; 1177 | } 1178 | } 1179 | 1180 | - (void)mouseUp:(NSEvent *)theEvent 1181 | { 1182 | if(_trackingLayer == nil) return; 1183 | 1184 | const NSPoint pointInView = [self OE_pointInViewFromEvent:theEvent]; 1185 | 1186 | if([_trackingLayer isKindOfClass:[OEGridViewCell class]]) 1187 | { 1188 | if([theEvent clickCount] == 2 && _delegateHas.doubleClickedCellForItemAtIndex) 1189 | { 1190 | OEGridViewCell *cell = (OEGridViewCell *)[self OE_gridLayerForPoint:pointInView]; 1191 | if ([cell isKindOfClass:[OEGridViewCell class]]) 1192 | [_delegate gridView:self doubleClickedCellForItemAtIndex:[cell OE_index]]; 1193 | } 1194 | } 1195 | else if(_trackingLayer != _rootLayer) 1196 | { 1197 | const NSPoint pointInLayer = [_rootLayer convertPoint:pointInView toLayer:_trackingLayer]; 1198 | [_trackingLayer mouseUpAtPointInLayer:pointInLayer withEvent:theEvent]; 1199 | } 1200 | else 1201 | { 1202 | [_selectionLayer removeFromSuperlayer]; 1203 | _selectionLayer = nil; 1204 | _originalSelectionIndexes = nil; 1205 | 1206 | if([theEvent clickCount] == 2) 1207 | { 1208 | CALayer *hitLayer = [_rootLayer hitTest:[self convertPointToLayer:pointInView]]; 1209 | if(hitLayer && [hitLayer isKindOfClass:[CATextLayer class]]) 1210 | { 1211 | CATextLayer *titleLayer = (CATextLayer *)hitLayer; 1212 | CALayer *superlayer = [titleLayer superlayer]; 1213 | while(superlayer) 1214 | { 1215 | if([superlayer isKindOfClass:[OEGridViewCell class]]) break; 1216 | 1217 | superlayer = [superlayer superlayer]; 1218 | } 1219 | 1220 | if(superlayer) [self OE_setupFieldEditorForCell:(OEGridViewCell *)superlayer titleLayer:titleLayer]; 1221 | } 1222 | } 1223 | } 1224 | _trackingLayer = nil; 1225 | } 1226 | 1227 | - (void)mouseMoved:(NSEvent *)theEvent 1228 | { 1229 | const NSPoint pointInView = [self OE_pointInViewFromEvent:theEvent]; 1230 | OEGridLayer *newLayer = [self OE_gridLayerForPoint:pointInView]; 1231 | while (newLayer && ![newLayer receivesHoverEvents]) { 1232 | newLayer = (OEGridLayer*)[newLayer superlayer]; 1233 | if (![newLayer isKindOfClass:[OEGridLayer class]]) { 1234 | newLayer = nil; 1235 | } 1236 | } 1237 | OEGridLayer *oldLayer = _hoveringLayer; 1238 | _hoveringLayer = newLayer; 1239 | const NSPoint pointInLayer = [_rootLayer convertPoint:pointInView toLayer:_hoveringLayer]; 1240 | if (oldLayer != _hoveringLayer) { 1241 | [oldLayer mouseExitedAtPointInLayer:pointInLayer withEvent:theEvent]; 1242 | [_hoveringLayer mouseEnteredAtPointInLayer:pointInLayer withEvent:theEvent]; 1243 | } else { 1244 | [_hoveringLayer mouseMovedAtPointInLayer:pointInLayer withEvent:theEvent]; 1245 | } 1246 | } 1247 | 1248 | - (void)mouseExited:(NSEvent *)theEvent 1249 | { 1250 | if (_hoveringLayer) { 1251 | const NSPoint pointInView = [self OE_pointInViewFromEvent:theEvent]; 1252 | const NSPoint pointInLayer = [_rootLayer convertPoint:pointInView toLayer:_hoveringLayer]; 1253 | [_hoveringLayer mouseExitedAtPointInLayer:pointInLayer withEvent:theEvent]; 1254 | _hoveringLayer = nil; 1255 | } 1256 | } 1257 | 1258 | - (void)OE_setupFieldEditorForCell:(OEGridViewCell *)cell titleLayer:(CATextLayer *)textLayer 1259 | { 1260 | NSRect fieldFrame = [_rootLayer convertRect:[textLayer bounds] fromLayer:textLayer]; 1261 | fieldFrame = NSOffsetRect(NSInsetRect(fieldFrame, 0.0, -1.0), 0.0, -1.0); 1262 | [_fieldEditor setFrame:fieldFrame]; 1263 | 1264 | NSString *title = [textLayer string]; 1265 | [_fieldEditor setString:title]; 1266 | [_fieldEditor setDelegate:cell]; 1267 | [_fieldEditor setHidden:NO]; 1268 | [[self window] makeFirstResponder:[[_fieldEditor subviews] objectAtIndex:0]]; 1269 | } 1270 | 1271 | - (void)OE_cancelFieldEditor 1272 | { 1273 | if([_fieldEditor isHidden]) return; 1274 | 1275 | OEGridViewCell *delegate = [_fieldEditor delegate]; 1276 | if([delegate isKindOfClass:[OEGridViewCell class]]) [delegate setEditing:NO]; 1277 | 1278 | [_fieldEditor setHidden:YES]; 1279 | [[self window] makeFirstResponder:self]; 1280 | } 1281 | 1282 | - (NSMenu*)menuForEvent:(NSEvent *)event 1283 | { 1284 | [[self window] makeFirstResponder:self]; 1285 | NSPoint mouseLocationInWindow = [event locationInWindow]; 1286 | NSPoint mouseLocationInView = [self convertPoint:mouseLocationInWindow fromView:nil]; 1287 | NSUInteger index = [self indexForCellAtPoint:mouseLocationInView]; 1288 | if(index != NSNotFound && _dataSourceHas.menuForItemsAtIndexes) 1289 | { 1290 | BOOL itemIsSelected = [[self selectionIndexes] containsIndex:index]; 1291 | NSIndexSet* indexes = itemIsSelected ? [self selectionIndexes] : [NSIndexSet indexSetWithIndex:index]; 1292 | if(!itemIsSelected) 1293 | [self setSelectionIndexes:[NSIndexSet indexSetWithIndex:index]]; 1294 | return [[self dataSource] gridView:self menuForItemsAtIndexes:indexes]; 1295 | } 1296 | return [self menu]; 1297 | } 1298 | 1299 | #pragma mark - 1300 | #pragma mark Touch Gestures 1301 | 1302 | - (void)magnifyWithEvent:(NSEvent *)event 1303 | { 1304 | if (_delegateHas.magnifiedWithEvent) { 1305 | [_delegate gridView:self magnifiedWithEvent:event]; 1306 | } 1307 | } 1308 | 1309 | - (void)endGestureWithEvent:(NSEvent *)event 1310 | { 1311 | if (_delegateHas.magnifyEndedWithEvent) { 1312 | [_delegate gridView:self magnifyEndedWithEvent:event]; 1313 | } 1314 | } 1315 | 1316 | #pragma mark - 1317 | #pragma mark Keyboard Handling Operations 1318 | 1319 | - (void)OE_moveKeyboardSelectionToIndex:(NSUInteger)index 1320 | { 1321 | NSUInteger modifierFlags = [[NSApp currentEvent] modifierFlags]; 1322 | BOOL multiSelect = ((modifierFlags & NSCommandKeyMask) == NSCommandKeyMask) || ((modifierFlags & NSShiftKeyMask) == NSShiftKeyMask); 1323 | 1324 | if(!multiSelect) [self deselectAll:self]; 1325 | 1326 | if(index != NSNotFound) 1327 | { 1328 | [self selectCellAtIndex:index]; 1329 | [self scrollRectToVisible:NSIntegralRect(NSInsetRect([self rectForCellAtIndex:index], 0.0, -_rowSpacing))]; 1330 | } 1331 | 1332 | _indexOfKeyboardSelection = index; 1333 | } 1334 | 1335 | - (void)cancelOperation:(id)sender 1336 | { 1337 | [self OE_moveKeyboardSelectionToIndex:NSNotFound]; 1338 | } 1339 | 1340 | - (void)moveUp:(id)sender 1341 | { 1342 | if(_cachedNumberOfItems == 0) return; 1343 | 1344 | NSUInteger index = 0; 1345 | if(_indexOfKeyboardSelection == NSNotFound) index = (_cachedNumberOfItems / _cachedNumberOfVisibleColumns) * _cachedNumberOfVisibleColumns; 1346 | else index = MIN(_indexOfKeyboardSelection, _indexOfKeyboardSelection - _cachedNumberOfVisibleColumns); 1347 | 1348 | [self OE_moveKeyboardSelectionToIndex:index]; 1349 | } 1350 | 1351 | - (void)moveDown:(id)sender 1352 | { 1353 | if(_cachedNumberOfItems == 0) return; 1354 | 1355 | NSUInteger index = 0; 1356 | if(_indexOfKeyboardSelection != NSNotFound) 1357 | { 1358 | index = _indexOfKeyboardSelection + _cachedNumberOfVisibleColumns; 1359 | if(index >= _cachedNumberOfItems) index = _indexOfKeyboardSelection; 1360 | } 1361 | 1362 | [self OE_moveKeyboardSelectionToIndex:index]; 1363 | } 1364 | 1365 | - (void)moveLeft:(id)sender 1366 | { 1367 | if(_cachedNumberOfItems == 0) return; 1368 | 1369 | NSUInteger index = 0; 1370 | if(_indexOfKeyboardSelection == NSNotFound) 1371 | { 1372 | index = MIN(_cachedNumberOfVisibleColumns, _cachedNumberOfItems) - 1; 1373 | } 1374 | else 1375 | { 1376 | if(_indexOfKeyboardSelection > 0) 1377 | { 1378 | const NSUInteger rowFirstIndex = (_indexOfKeyboardSelection / _cachedNumberOfVisibleColumns) * _cachedNumberOfVisibleColumns; 1379 | index = MAX(rowFirstIndex, _indexOfKeyboardSelection - 1); 1380 | } 1381 | } 1382 | 1383 | [self OE_moveKeyboardSelectionToIndex:index]; 1384 | } 1385 | 1386 | - (void)moveRight:(id)sender 1387 | { 1388 | if(_cachedNumberOfItems == 0) return; 1389 | 1390 | NSUInteger index = 0; 1391 | if(_indexOfKeyboardSelection != NSNotFound) 1392 | { 1393 | const NSUInteger rowLastIndex = MIN((((_indexOfKeyboardSelection / _cachedNumberOfVisibleColumns) + 1) * _cachedNumberOfVisibleColumns), _cachedNumberOfItems); 1394 | index = MIN(rowLastIndex - 1, _indexOfKeyboardSelection + 1); 1395 | } 1396 | 1397 | [self OE_moveKeyboardSelectionToIndex:index]; 1398 | } 1399 | 1400 | - (void)keyDown:(NSEvent *)theEvent 1401 | { 1402 | if ([theEvent keyCode] == kVK_Delete || [theEvent keyCode] == kVK_ForwardDelete) [NSApp sendAction:@selector(delete:) to:nil from:self]; 1403 | else [super keyDown:theEvent]; 1404 | } 1405 | 1406 | #pragma mark - 1407 | #pragma mark NSDraggingDestination 1408 | 1409 | - (NSDragOperation)OE_dragOperationForDestinationLayer:(id)sender 1410 | { 1411 | const NSPoint pointInView = [self convertPoint:[sender draggingLocation] fromView:nil]; 1412 | OEGridLayer *newDragDestinationLayer = [self OE_gridLayerForPoint:pointInView]; 1413 | 1414 | if(_dragDestinationLayer != newDragDestinationLayer) 1415 | { 1416 | if(newDragDestinationLayer == _rootLayer) 1417 | { 1418 | _prevDragDestinationLayer = nil; 1419 | _dragDestinationLayer = nil; 1420 | } 1421 | else if(newDragDestinationLayer != _prevDragDestinationLayer) 1422 | { 1423 | NSDragOperation result = [newDragDestinationLayer draggingEntered:sender]; 1424 | if(result != NSDragOperationNone) 1425 | { 1426 | [_dragIndicationLayer setHidden:YES]; 1427 | _dragDestinationLayer = newDragDestinationLayer; 1428 | return result; 1429 | } 1430 | _prevDragDestinationLayer = newDragDestinationLayer; 1431 | } 1432 | } 1433 | return NSDragOperationNone; 1434 | } 1435 | 1436 | - (NSDragOperation)draggingEntered:(id )sender 1437 | { 1438 | _lastDragOperation = NSDragOperationNone; 1439 | 1440 | NSDragOperation result = [self OE_dragOperationForDestinationLayer:sender]; 1441 | if(result != NSDragOperationNone) return result; 1442 | 1443 | // The delegate has to be able to validate and accept drops, if it can't do then then there is no need to drag anything around 1444 | if(_delegateHas.validateDrop && _delegateHas.acceptDrop) 1445 | { 1446 | _lastDragOperation = [_delegate gridView:self validateDrop:sender]; 1447 | [_dragIndicationLayer setHidden:(_lastDragOperation == NSDragOperationNone)]; 1448 | 1449 | if(![_dragIndicationLayer isHidden]) [_rootLayer setNeedsLayout]; 1450 | } 1451 | 1452 | return _lastDragOperation; 1453 | } 1454 | 1455 | - (NSDragOperation)draggingUpdated:(id )sender 1456 | { 1457 | const NSPoint pointInView = [self convertPoint:[sender draggingLocation] fromView:nil]; 1458 | BOOL hadDragDestinationLayer = (_dragDestinationLayer != nil); 1459 | 1460 | if(_dragDestinationLayer) 1461 | { 1462 | CGPoint pointInSuperlayer = [_rootLayer convertPoint:pointInView toLayer:[_dragDestinationLayer superlayer]]; 1463 | if([_dragDestinationLayer hitTest:pointInSuperlayer] == _dragDestinationLayer) 1464 | { 1465 | NSDragOperation result = [_dragDestinationLayer draggingUpdated:sender]; 1466 | if(result != NSDragOperationNone) return result; 1467 | } 1468 | [_dragDestinationLayer draggingExited:sender]; 1469 | _dragDestinationLayer = nil; 1470 | } 1471 | 1472 | NSDragOperation result = [self OE_dragOperationForDestinationLayer:sender]; 1473 | if(result != NSDragOperationNone) return result; 1474 | 1475 | if(hadDragDestinationLayer) 1476 | { 1477 | // If we were targeting the drag destination layer and now we are not, then its the same as running draggingEntered: 1478 | _lastDragOperation = [self draggingEntered:sender]; 1479 | } 1480 | else if(_delegateHas.draggingUpdated) 1481 | { 1482 | _lastDragOperation = [_delegate gridView:self draggingUpdated:sender]; 1483 | [_dragIndicationLayer setHidden:(_lastDragOperation == NSDragOperationNone)]; 1484 | 1485 | if(![_dragIndicationLayer isHidden]) [_rootLayer setNeedsLayout]; 1486 | } 1487 | 1488 | return _lastDragOperation; 1489 | } 1490 | 1491 | - (void)draggingExited:(id)sender 1492 | { 1493 | if(_dragDestinationLayer) 1494 | { 1495 | [_dragDestinationLayer draggingExited:sender]; 1496 | _dragDestinationLayer = nil; 1497 | } 1498 | else 1499 | { 1500 | [_dragIndicationLayer setHidden:YES]; 1501 | } 1502 | } 1503 | 1504 | - (BOOL)performDragOperation:(id)sender 1505 | { 1506 | [_dragIndicationLayer setHidden:YES]; 1507 | 1508 | if(_dragDestinationLayer != nil) return [_dragDestinationLayer performDragOperation:sender]; 1509 | else return _delegateHas.acceptDrop && [_delegate gridView:self acceptDrop:sender]; 1510 | } 1511 | 1512 | #pragma mark - 1513 | #pragma mark NSDraggingSource 1514 | 1515 | - (NSDragOperation)draggingSession:(NSDraggingSession *)session sourceOperationMaskForDraggingContext:(NSDraggingContext)context 1516 | { 1517 | return context == NSDraggingContextWithinApplication ? NSDragOperationCopy : NSDragOperationNone; 1518 | } 1519 | 1520 | - (void)draggingSession:(NSDraggingSession *)session endedAtPoint:(NSPoint)screenPoint operation:(NSDragOperation)operation 1521 | { 1522 | _draggingSession = nil; 1523 | } 1524 | 1525 | #pragma mark - 1526 | #pragma mark Properties 1527 | 1528 | - (void)setForegroundLayer:(CALayer *)foregroundLayer 1529 | { 1530 | if(_foregroundLayer == foregroundLayer) return; 1531 | 1532 | [_foregroundLayer removeFromSuperlayer]; 1533 | _foregroundLayer = foregroundLayer; 1534 | 1535 | if(_foregroundLayer) [self OE_reorderSublayers]; 1536 | } 1537 | 1538 | - (void)setBackgroundLayer:(CALayer *)backgroundLayer 1539 | { 1540 | if(_backgroundLayer == backgroundLayer) return; 1541 | 1542 | [_backgroundLayer removeFromSuperlayer]; 1543 | _backgroundLayer = backgroundLayer; 1544 | 1545 | if(_backgroundLayer) [self OE_reorderSublayers]; 1546 | } 1547 | 1548 | - (void)setMinimumColumnSpacing:(CGFloat)minimumColumnSpacing 1549 | { 1550 | if(_minimumColumnSpacing == minimumColumnSpacing) return; 1551 | 1552 | _minimumColumnSpacing = minimumColumnSpacing; 1553 | [self OE_calculateCachedValuesAndQueryForDataChanges:NO]; 1554 | } 1555 | 1556 | - (void)setRowSpacing:(CGFloat)rowSpacing 1557 | { 1558 | if(_rowSpacing == rowSpacing) return; 1559 | 1560 | _rowSpacing = rowSpacing; 1561 | [self OE_calculateCachedValuesAndQueryForDataChanges:NO]; 1562 | } 1563 | 1564 | - (void)setItemSize:(NSSize)itemSize 1565 | { 1566 | if(NSEqualSizes(_itemSize, itemSize)) return; 1567 | 1568 | [self OE_cancelFieldEditor]; 1569 | 1570 | _itemSize = itemSize; 1571 | [[self enclosingScrollView] flashScrollers]; 1572 | [self OE_calculateCachedValuesAndQueryForDataChanges:NO]; 1573 | } 1574 | 1575 | - (void)setDataSource:(id)dataSource 1576 | { 1577 | if(_dataSource != dataSource) 1578 | { 1579 | _dataSource = dataSource; 1580 | _dataSourceHas.viewForNoItemsInGridView = [_dataSource respondsToSelector:@selector(viewForNoItemsInGridView:)]; 1581 | _dataSourceHas.willBeginEditingCellForItemAtIndex = [_dataSource respondsToSelector:@selector(gridView:willBeginEditingCellForItemAtIndex:)]; 1582 | _dataSourceHas.didEndEditingCellForItemAtIndex = [_dataSource respondsToSelector:@selector(gridView:didEndEditingCellForItemAtIndex:)]; 1583 | _dataSourceHas.pasteboardWriterForIndex = [_dataSource respondsToSelector:@selector(gridView:pasteboardWriterForIndex:)]; 1584 | _dataSourceHas.menuForItemsAtIndexes = [_dataSource respondsToSelector:@selector(gridView:menuForItemsAtIndexes:)]; 1585 | 1586 | [self OE_setNeedsReloadData]; 1587 | } 1588 | } 1589 | 1590 | - (void)setDelegate:(id)delegate 1591 | { 1592 | if(_delegate != delegate) 1593 | { 1594 | _delegate = delegate; 1595 | _delegateHas.selectionChanged = [_delegate respondsToSelector:@selector(selectionChangedInGridView:)]; 1596 | _delegateHas.doubleClickedCellForItemAtIndex = [_delegate respondsToSelector:@selector(gridView:doubleClickedCellForItemAtIndex:)]; 1597 | _delegateHas.validateDrop = [_delegate respondsToSelector:@selector(gridView:validateDrop:)]; 1598 | _delegateHas.draggingUpdated = [_delegate respondsToSelector:@selector(gridView:draggingUpdated:)]; 1599 | _delegateHas.acceptDrop = [_delegate respondsToSelector:@selector(gridView:acceptDrop:)]; 1600 | _delegateHas.magnifiedWithEvent = [_delegate respondsToSelector:@selector(gridView:magnifiedWithEvent:)]; 1601 | _delegateHas.magnifyEndedWithEvent = [_delegate respondsToSelector:@selector(gridView:magnifyEndedWithEvent:)]; 1602 | } 1603 | } 1604 | 1605 | - (void)setSelectionIndexes:(NSIndexSet *)selectionIndexes 1606 | { 1607 | if([_selectionIndexes isEqualToIndexSet:selectionIndexes]) return; 1608 | 1609 | [_selectionIndexes removeAllIndexes]; 1610 | [_selectionIndexes addIndexes:selectionIndexes]; 1611 | [_visibleCellByIndex enumerateKeysAndObjectsUsingBlock: 1612 | ^ (NSNumber *key, OEGridViewCell *obj, BOOL *stop) 1613 | { 1614 | [obj setSelected:[_selectionIndexes containsIndex:[key unsignedIntegerValue]] animated:![CATransaction disableActions]]; 1615 | }]; 1616 | 1617 | if(_delegateHas.selectionChanged) [_delegate selectionChangedInGridView:self]; 1618 | } 1619 | 1620 | - (NSIndexSet *)selectionIndexes 1621 | { 1622 | // Make an immutable copy 1623 | return [_selectionIndexes copy]; 1624 | } 1625 | 1626 | @end 1627 | 1628 | @implementation OEGridView (OEGridViewCell) 1629 | 1630 | - (void)OE_willBeginEditingCell:(OEGridViewCell *)cell 1631 | { 1632 | if(_dataSourceHas.willBeginEditingCellForItemAtIndex) [_dataSource gridView:self willBeginEditingCellForItemAtIndex:[cell OE_index]]; 1633 | } 1634 | 1635 | - (void)OE_didEndEditingCell:(OEGridViewCell *)cell 1636 | { 1637 | if(_dataSourceHas.didEndEditingCellForItemAtIndex) [_dataSource gridView:self didEndEditingCellForItemAtIndex:[cell OE_index]]; 1638 | } 1639 | 1640 | @end 1641 | -------------------------------------------------------------------------------- /OEGridView.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'OEGridView' 3 | s.version = '0.0.1' 4 | s.summary = 'High performance Core Animation based grid view ' 5 | s.author = { 'Indragie Karunaratne' => 'indragiek@gmail.com' } 6 | s.source_files = '*.{h,m}' 7 | s.source = { :git => 'https://github.com/indragiek/OEGridView.git'} 8 | s.requires_arc = true 9 | s.license = 'OpenEmu' 10 | end -------------------------------------------------------------------------------- /OEGridViewCell+OEGridView.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2012, OpenEmu Team 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the OpenEmu Team nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY OpenEmu Team ''AS IS'' AND ANY 16 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL OpenEmu Team BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | */ 26 | 27 | #import "OEGridViewCell.h" 28 | 29 | @interface OEGridViewCell (OEGridView) 30 | 31 | @property(nonatomic, assign, setter = OE_setIndex:) NSUInteger OE_index; 32 | 33 | @end 34 | -------------------------------------------------------------------------------- /OEGridViewCell.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2012, OpenEmu Team 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the OpenEmu Team nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY OpenEmu Team ''AS IS'' AND ANY 16 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL OpenEmu Team BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | */ 26 | 27 | #import 28 | #import "OEGridLayer.h" 29 | 30 | @class OEGridView; 31 | 32 | @interface OEGridViewCell : OEGridLayer 33 | { 34 | @private 35 | NSUInteger _index; 36 | } 37 | 38 | - (void)prepareForReuse; 39 | - (void)didBecomeFocused; 40 | - (void)willResignFocus; 41 | 42 | #pragma mark - 43 | #pragma mark Properties 44 | 45 | @property(nonatomic, assign, getter=isEditing) BOOL editing; 46 | @property(nonatomic, assign, getter=isSelected) BOOL selected; 47 | - (void)setSelected:(BOOL)selected animated:(BOOL)animated; 48 | 49 | @property(nonatomic, strong) CALayer *foregroundLayer; 50 | @property(nonatomic, readonly) OEGridView *gridView; 51 | @property(nonatomic, readonly) NSRect hitRect; 52 | @property(nonatomic, readonly) id draggingImage; 53 | @property(nonatomic, assign) BOOL highlighted; 54 | @end 55 | -------------------------------------------------------------------------------- /OEGridViewCell.m: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2012, OpenEmu Team 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the OpenEmu Team nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY OpenEmu Team ''AS IS'' AND ANY 16 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL OpenEmu Team BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | */ 26 | 27 | #import "OEGridViewCell.h" 28 | #import "OEGridView.h" 29 | #import "OEGridView+OEGridViewCell.h" 30 | 31 | @interface OEGridViewCell () 32 | 33 | - (void)OE_reorderLayers; 34 | 35 | @end 36 | 37 | @implementation OEGridViewCell 38 | @synthesize highlighted = _highlighted; 39 | @synthesize selected=_selected; 40 | @synthesize editing=_editing; 41 | @synthesize foregroundLayer=_foregroundLayer; 42 | 43 | - (id)init 44 | { 45 | if((self = [super init])) 46 | { 47 | [self setNeedsDisplayOnBoundsChange:YES]; 48 | [self setLayoutManager:[OEGridViewLayoutManager layoutManager]]; 49 | [self setInteractive:YES]; 50 | } 51 | 52 | return self; 53 | } 54 | 55 | - (void)addSublayer:(CALayer *)layer 56 | { 57 | [super addSublayer:layer]; 58 | [self OE_reorderLayers]; 59 | } 60 | 61 | - (void)insertSublayer:(CALayer *)layer atIndex:(unsigned int)idx 62 | { 63 | [super insertSublayer:layer atIndex:idx]; 64 | [self OE_reorderLayers]; 65 | } 66 | 67 | - (void)layoutSublayers 68 | { 69 | [CATransaction begin]; 70 | [CATransaction setDisableActions:YES]; 71 | [_foregroundLayer setFrame:[self bounds]]; 72 | [CATransaction commit]; 73 | } 74 | 75 | - (void)prepareForReuse 76 | { 77 | [self setTracking:NO]; 78 | [self setEditing:NO]; 79 | [self setSelected:NO]; 80 | [self setHidden:NO]; 81 | [self setOpacity:1.0]; 82 | [self setShadowOpacity:0.0]; 83 | } 84 | 85 | - (void)didBecomeFocused 86 | { 87 | } 88 | 89 | - (void)willResignFocus 90 | { 91 | } 92 | 93 | #pragma mark - 94 | #pragma mark Properties 95 | 96 | - (id)draggingImage 97 | { 98 | const CGSize imageSize = [self bounds].size; 99 | NSBitmapImageRep *dragImageRep = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:NULL pixelsWide:imageSize.width pixelsHigh:imageSize.height bitsPerSample:8 samplesPerPixel:4 hasAlpha:YES isPlanar:NO colorSpaceName:NSCalibratedRGBColorSpace bytesPerRow:(NSInteger)ceil(imageSize.width) * 4 bitsPerPixel:32]; 100 | NSGraphicsContext *bitmapContext = [NSGraphicsContext graphicsContextWithBitmapImageRep:dragImageRep]; 101 | CGContextRef ctx = (CGContextRef)[bitmapContext graphicsPort]; 102 | 103 | if([self superlayer] == nil) CGContextConcatCTM(ctx, CGAffineTransformMake(1.0, 0.0, 0.0, -1.0, 0.0, imageSize.height)); 104 | 105 | CGContextClearRect(ctx, CGRectMake(0.0, 0.0, imageSize.width, imageSize.height)); 106 | CGContextSetAllowsAntialiasing(ctx, YES); 107 | [self renderInContext:ctx]; 108 | CGContextFlush(ctx); 109 | 110 | NSImage *dragImage = [[NSImage alloc] initWithSize:imageSize]; 111 | [dragImage addRepresentation:dragImageRep]; 112 | [dragImage setFlipped:YES]; 113 | 114 | return dragImage; 115 | } 116 | 117 | - (void)setSelected:(BOOL)selected 118 | { 119 | [self setSelected:selected animated:NO]; 120 | } 121 | 122 | - (void)setSelected:(BOOL)selected animated:(BOOL)animated 123 | { 124 | _selected = selected; 125 | } 126 | 127 | - (void)setEditing:(BOOL)editing 128 | { 129 | if(_editing != editing) 130 | { 131 | if(editing) [[self gridView] OE_willBeginEditingCell:self]; 132 | else [[self gridView] OE_didEndEditingCell:self]; 133 | _editing = editing; 134 | } 135 | } 136 | 137 | - (void)OE_reorderLayers 138 | { 139 | [super insertSublayer:_foregroundLayer atIndex:[[self sublayers] count]]; 140 | } 141 | 142 | - (void)setForegroundLayer:(CALayer *)foregroundLayer 143 | { 144 | if(_foregroundLayer != foregroundLayer) 145 | { 146 | [_foregroundLayer removeFromSuperlayer]; 147 | _foregroundLayer = foregroundLayer; 148 | 149 | [self OE_reorderLayers]; 150 | } 151 | } 152 | 153 | - (OEGridView *)gridView 154 | { 155 | OEGridView *superlayerDelegate = [[self superlayer] delegate]; 156 | return [superlayerDelegate isKindOfClass:[OEGridView class]] ? superlayerDelegate : nil; 157 | } 158 | 159 | - (NSRect)hitRect 160 | { 161 | return [self bounds]; 162 | } 163 | 164 | - (void)OE_setIndex:(NSUInteger)index 165 | { 166 | _index = index; 167 | } 168 | 169 | - (NSUInteger)OE_index 170 | { 171 | return _index; 172 | } 173 | 174 | @end 175 | -------------------------------------------------------------------------------- /OEGridViewFieldEditor.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2011, OpenEmu Team 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the OpenEmu Team nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY OpenEmu Team ''AS IS'' AND ANY 16 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL OpenEmu Team BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | */ 26 | 27 | #import 28 | 29 | @interface OEGridViewFieldEditor : NSView 30 | 31 | @property(nonatomic, assign) NSTextAlignment alignment; 32 | @property(nonatomic, weak) id delegate; 33 | @property(nonatomic, strong) NSString *string; 34 | @property(nonatomic, strong) NSFont *font; 35 | @property(nonatomic, strong) NSColor *borderColor; 36 | 37 | @end 38 | -------------------------------------------------------------------------------- /OEGridViewFieldEditor.m: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2011, OpenEmu Team 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the OpenEmu Team nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY OpenEmu Team ''AS IS'' AND ANY 16 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL OpenEmu Team BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | */ 26 | 27 | #import "OEGridViewFieldEditor.h" 28 | #import "NSColor+OEAdditions.h" 29 | 30 | #pragma mark - 31 | 32 | @implementation OEGridViewFieldEditor 33 | { 34 | NSTextField *textView; 35 | } 36 | 37 | @synthesize borderColor; 38 | 39 | - (id)initWithFrame:(NSRect)frame 40 | { 41 | if((self = [super initWithFrame:frame])) 42 | { 43 | textView = [[NSTextField alloc] initWithFrame:NSMakeRect(10, 10, 80, 80)]; 44 | [textView setBezeled:NO]; 45 | [textView setAllowsEditingTextAttributes:NO]; 46 | [self addSubview:textView]; 47 | 48 | [self setAutoresizesSubviews:NO]; 49 | [self setHidden:YES]; 50 | [self setWantsLayer:YES]; 51 | 52 | NSFont *fieldEditorFont = [[NSFontManager sharedFontManager] fontWithFamily:@"Lucida Grande" traits:NSBoldFontMask weight:9 size:12]; 53 | [self setAlignment:NSCenterTextAlignment]; 54 | [self setBorderColor:[NSColor blackColor]]; 55 | [self setFont:fieldEditorFont]; 56 | 57 | CALayer *layer = [self layer]; 58 | [layer setShouldRasterize:YES]; 59 | [layer setShadowOpacity:0.45]; 60 | [layer setShadowColor:[[NSColor blackColor] CGColor]]; 61 | [layer setShadowOffset:CGSizeMake(0.0, -6.0)]; 62 | [layer setShadowRadius:5]; 63 | } 64 | 65 | return self; 66 | } 67 | 68 | #pragma mark - 69 | 70 | - (void)drawRect:(NSRect)dirtyRect 71 | { 72 | [super drawRect:dirtyRect]; 73 | 74 | [[self borderColor] setStroke]; 75 | 76 | NSBezierPath *borderPath = [NSBezierPath bezierPathWithRect:NSInsetRect([self bounds], 0.5, 0.5)]; 77 | [borderPath stroke]; 78 | 79 | [[NSColor whiteColor] setStroke]; 80 | borderPath = [NSBezierPath bezierPathWithRect:NSInsetRect([self bounds], 1.5, 1.5)]; 81 | [borderPath stroke]; 82 | } 83 | 84 | #pragma mark - 85 | 86 | - (void)setFrameSize:(NSSize)newSize 87 | { 88 | [super setFrameSize:newSize]; 89 | 90 | if(newSize.width >= 2) newSize.width -= 2; 91 | if(newSize.height >= 2) newSize.height -= 2; 92 | 93 | [textView setFrameSize:newSize]; 94 | [textView setFrameOrigin:NSMakePoint(1, 1)]; 95 | } 96 | 97 | #pragma mark - Accessors 98 | 99 | - (NSString *)string { return [textView stringValue]; } 100 | - (void)setString:(NSString *)newString { [textView setStringValue:newString]; } 101 | 102 | - (NSTextAlignment)alignment { return [textView alignment]; } 103 | - (void)setAlignment:(NSTextAlignment)alignment { [textView setAlignment:alignment]; } 104 | 105 | - (NSFont *)font { return [textView font]; } 106 | - (void)setFont:(NSFont *)newFont { [textView setFont:newFont]; } 107 | 108 | - (id)delegate { return [textView delegate]; } 109 | - (void)setDelegate:(id)delegate { [textView setDelegate:delegate]; } 110 | 111 | @end 112 | -------------------------------------------------------------------------------- /OEGridViewLayoutManager.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2012, OpenEmu Team 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the OpenEmu Team nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY OpenEmu Team ''AS IS'' AND ANY 16 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL OpenEmu Team BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | */ 26 | 27 | #import 28 | 29 | @protocol OEGridViewLayoutManagerProtocol 30 | 31 | - (void)layoutSublayers; 32 | 33 | @end 34 | 35 | @interface OEGridViewLayoutManager : NSObject 36 | 37 | + (OEGridViewLayoutManager *)layoutManager; 38 | 39 | @end 40 | -------------------------------------------------------------------------------- /OEGridViewLayoutManager.m: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2012, OpenEmu Team 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the OpenEmu Team nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY OpenEmu Team ''AS IS'' AND ANY 16 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL OpenEmu Team BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | */ 26 | 27 | #import "OEGridViewLayoutManager.h" 28 | #import 29 | 30 | @implementation OEGridViewLayoutManager 31 | 32 | + (OEGridViewLayoutManager *)layoutManager 33 | { 34 | static OEGridViewLayoutManager *layoutManager = nil; 35 | 36 | static dispatch_once_t onceToken; 37 | dispatch_once(&onceToken, ^{ 38 | layoutManager = [[self alloc] init]; 39 | }); 40 | 41 | return layoutManager; 42 | } 43 | 44 | - (void)layoutSublayersOfLayer:(CALayer *)theLayer 45 | { 46 | if([theLayer conformsToProtocol:@protocol(OEGridViewLayoutManagerProtocol)]) [theLayer layoutSublayers]; 47 | else if([[theLayer delegate] conformsToProtocol:@protocol(OEGridViewLayoutManagerProtocol)]) [[theLayer delegate] layoutSublayers]; 48 | } 49 | 50 | @end 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### OEGridView (from the [OpenEmu project](https://github.com/OpenEmu/OpenEmu/)) 2 | #### Sonora 2.0 Fork 3 | 4 | This is the unofficial repo for my fork of OEGridView, the high performance Core Animation based grid view used in [OpenEmu](http://openemu.org) (which is a fantastic app, by the way). 5 | 6 | OEGridView doesn't have its own repository, so I copied the required source files and am hosting it in this repository with my own additions that I put in for use in the upcoming [Sonora 2.0](http://indragie.com). 7 | 8 | **The original source code was written mainly by [Enrique Osuna](https://github.com/riquedafreak) along with contributions from other members of the OpenEmu project** 9 | 10 | I'm also putting in some documentation in here to help other people use it in their projects. 11 | 12 | #### Requirements 13 | 14 | OEGridView requires ARC. If your project doesn't use ARC, simply add the `-fobjc-arc` flag to all of the files except for `NSColor+OEAdditions.m`, which can not be compiled with ARC enabled. 15 | 16 | #### Additions for Sonora 2 17 | 18 | This project has a few additions that were made due to the needs of Sonora 2: 19 | 20 | - Support for OS X 10.8 Mountain Lion and the Retina display 21 | - Support for receiving mouse entered, exited, and moved events inside layers 22 | 23 | 24 | #### How to use 25 | 26 | Basic usage of OEGridView is super simple: 27 | 28 | - Place the OEGridView inside an NSScrollView 29 | - Set the `dataSource` and `delegate` properties to your controller object 30 | - Set the `itemSize` property of the `OEGridView` to the size of each content cell 31 | - Implement the following two methods: 32 | 33 | ``` 34 | - (OEGridViewCell *)gridView:(OEGridView *)gridView cellForItemAtIndex:(NSUInteger)index; 35 | - (NSUInteger)numberOfItemsInGridView:(OEGridView *)gridView; 36 | ``` 37 | 38 | The `gridView:cellForItemAtIndex:` method is where you would return an instance of your `OEGridViewCell` subclass. A basic implementation of this method would look like this (from Sonora): 39 | 40 | ``` 41 | - (OEGridViewCell *)gridView:(OEGridView *)gridView cellForItemAtIndex:(NSUInteger)index 42 | { 43 | SNRAlbumGridViewCell *item = (SNRAlbumGridViewCell *)[gridView cellForItemAtIndex:index makeIfNecessary:NO]; 44 | if (!item) { 45 | item = (SNRAlbumGridViewCell *)[gridView dequeueReusableCell]; 46 | } 47 | if (!item) { 48 | item = [[SNRAlbumGridViewCell alloc] init]; 49 | } 50 | id object = [self.fetchedResultsController objectAtIndex:index]; 51 | item.representedObject = object; 52 | item.albumName = [object valueForKey:@"name"]; 53 | } 54 | ``` 55 | The key point here is to call OEGridView's `-cellForItemAtIndex:makeIfNecessary:` method first to see if the cell already exists, then call `-dequeueReusableCell`, and only then create a new cell if the previous two methods returned nil for maximum performance. 56 | 57 | Creating an `OEGridViewCell` subclass is also fairly simple. First of all, a few tips: 58 | 59 | - `OEGridViewCell` inherits from `OEGridLayer`, which is a `CALayer` subclass. Therefore, `OEGridViewCell`'s are to be treated like any other `CALayer`. 60 | - Avoid drawing directly into the layer, and utilize sublayers as much as possible. Use plain `CALayer` and `CATextLayer` sublayers for displaying images and text and use your own `CALayer` subclasses for custom drawn content. 61 | - If you want to use interactive controls, you must create a subclass of `OEGridLayer` with the `interactive` property set to `YES` in order to receive mouse events (check `OEGridLayer.h` for all the mouse event methods). 62 | - Set the `tracking` property of the `OEGridLayer` to `YES` when the `mouseDownAtPointInLayer:` method is called if you want to continue receiving additional events for mouseUp, etc. 63 | - `OEGridLayer`'s can also receive hover events (mouse entered, exited, and moved) when the `receivesHoverEvents` property is set to `YES`. 64 | 65 | A basic `OEGridViewCell` subclass might look like this: 66 | 67 | ``` 68 | - (id)init 69 | { 70 | if ((self = [super init])) { 71 | // Set the layer attributes 72 | self.receivesHoverEvents = YES; 73 | self.backgroundColor = [][NSColor redColor] CGColor]; 74 | // Create and configure any sublayers 75 | _imageLayer = [OEGridLayer layer]; 76 | _imageLayer.contentsGravity = kCAGravityResize; 77 | _textLayer = [CATextLayer layer]; 78 | // Add sublayers 79 | [self addSublayer:_imageLayer]; 80 | [self addSublayer:_textLayer]; 81 | } 82 | return self; 83 | } 84 | 85 | - (void)layoutSublayers 86 | { 87 | [super layoutSublayers]; 88 | // Do any sublayer layout 89 | [_imageLayer setFrame:[self bounds]]; 90 | } 91 | 92 | - (void)prepareForReuse 93 | { 94 | [super prepareForReuse]; 95 | // Clear any cached attributes 96 | self.image = nil; 97 | self.text = nil; 98 | } 99 | 100 | #pragma mark - Accessors 101 | 102 | - (void)setImage:(NSImage*)image 103 | { 104 | if (_image != image) { 105 | _image = image; 106 | [_imageLayer setImage:image]; 107 | } 108 | } 109 | 110 | - (void)setText:(NSString*)text 111 | { 112 | if (_text != text) { 113 | _text = text; 114 | [_textLayer setString:text]; 115 | } 116 | } 117 | ``` 118 | 119 | #### License 120 | 121 | OpenEmu is distributed under the following license, which also applies to OEGridView: 122 | 123 | ``` 124 | Copyright (c) 2012, OpenEmu Team 125 | 126 | Redistribution and use in source and binary forms, with or without 127 | modification, are permitted provided that the following conditions are met: 128 | * Redistributions of source code must retain the above copyright 129 | notice, this list of conditions and the following disclaimer. 130 | * Redistributions in binary form must reproduce the above copyright 131 | notice, this list of conditions and the following disclaimer in the 132 | documentation and/or other materials provided with the distribution. 133 | * Neither the name of the OpenEmu Team nor the 134 | names of its contributors may be used to endorse or promote products 135 | derived from this software without specific prior written permission. 136 | 137 | THIS SOFTWARE IS PROVIDED BY OpenEmu Team ''AS IS'' AND ANY 138 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 139 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 140 | DISCLAIMED. IN NO EVENT SHALL OpenEmu Team BE LIABLE FOR ANY 141 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 142 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 143 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 144 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 145 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 146 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 147 | ``` --------------------------------------------------------------------------------