├── .gitignore ├── English.lproj ├── InfoPlist.strings └── MainMenu.xib ├── Info.plist ├── LICENSE ├── MBCoverFlowScroller.h ├── MBCoverFlowScroller.m ├── MBCoverFlowView.h ├── MBCoverFlowView.m ├── MBCoverFlowView.xcodeproj ├── TemplateIcon.icns └── project.pbxproj ├── MBCoverFlowViewController.h ├── MBCoverFlowViewController.m ├── MBCoverFlowView_Prefix.pch ├── NSImage+MBCoverFlowAdditions.h ├── NSImage+MBCoverFlowAdditions.m ├── README.mdown └── main.m /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | *.mode1v3 3 | *.pbxuser 4 | 5 | -------------------------------------------------------------------------------- /English.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattball/MBCoverFlowView/083842e25220bbada56f3bdf7295d05adeb63fcf/English.lproj/InfoPlist.strings -------------------------------------------------------------------------------- /Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | English 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | com.yourcompany.${PRODUCT_NAME:identifier} 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | ${PRODUCT_NAME} 17 | CFBundlePackageType 18 | APPL 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | NSMainNibFile 24 | MainMenu 25 | NSPrincipalClass 26 | NSApplication 27 | 28 | 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2009 Matthew Ball 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /MBCoverFlowScroller.h: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | The MIT License 4 | 5 | Copyright (c) 2009 Matthew Ball 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | 25 | */ 26 | 27 | #import 28 | 29 | /** 30 | * @brief A scroller which has an appearance appropriate for a 31 | * CoverFlow-style environment. 32 | */ 33 | @interface MBCoverFlowScroller : NSScroller { 34 | NSUInteger _numberOfIncrements; 35 | } 36 | 37 | /** 38 | * @brief The number of non-visible tick marks present between 39 | * the left and right ends of the scroller. 40 | * @details The scroll knob will "snap" to these increments. 41 | */ 42 | @property (nonatomic, assign) NSUInteger numberOfIncrements; 43 | 44 | @end 45 | -------------------------------------------------------------------------------- /MBCoverFlowScroller.m: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | The MIT License 4 | 5 | Copyright (c) 2009 Matthew Ball 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | 25 | */ 26 | 27 | #import "MBCoverFlowScroller.h" 28 | 29 | // Constants 30 | static NSColor *MBCoverFlowScrollerOutlineColor, *MBCoverFlowScrollerInactiveOutlineColor, *MBCoverFlowScrollerPressedOutlineColor; 31 | static NSColor *MBCoverFlowScrollerBackgroundTopColor, *MBCoverFlowScrollerInactiveBackgroundTopColor, *MBCoverFlowScrollerPressedBackgroundTopColor; 32 | static NSColor *MBCoverFlowScrollerBackgroundBottomColor, *MBCoverFlowScrollerInactiveBackgroundBottomColor, *MBCoverFlowScrollerPressedBackgroundBottomColor; 33 | static NSColor *MBCoverFlowScrollerGlossTopColor, *MBCoverFlowScrollerInactiveGlossTopColor, *MBCoverFlowScrollerPressedGlossTopColor; 34 | static NSColor *MBCoverFlowScrollerGlossBottomColor, *MBCoverFlowScrollerInactiveGlossBottomColor, *MBCoverFlowScrollerPressedGlossBottomColor; 35 | static NSColor *MBCoverFlowScrollerSlotBackgroundColor, *MBCoverFlowScrollerInactiveSlotBackgroundColor, *MBCoverFlowScrollerSlotInsetColor; 36 | 37 | @interface MBCoverFlowScroller () 38 | - (NSBezierPath *)_leftArrowPath; 39 | - (NSBezierPath *)_rightArrowPath; 40 | @end 41 | 42 | @interface NSScroller (Private) 43 | - (NSRect)_drawingRectForPart:(NSScrollerPart)aPart; 44 | @end 45 | 46 | const float MBCoverFlowScrollerKnobMinimumWidth = 30.0; 47 | 48 | @implementation MBCoverFlowScroller 49 | 50 | @synthesize numberOfIncrements=_numberOfIncrements; 51 | 52 | + (void)initialize 53 | { 54 | MBCoverFlowScrollerOutlineColor = [[NSColor colorWithCalibratedWhite:1.0 alpha:0.5] retain]; 55 | MBCoverFlowScrollerInactiveOutlineColor = [[NSColor colorWithCalibratedWhite:1.0 alpha:0.3] retain]; 56 | MBCoverFlowScrollerPressedOutlineColor = [[NSColor colorWithCalibratedWhite:1.0 alpha:1.0] retain]; 57 | MBCoverFlowScrollerBackgroundTopColor = [[NSColor colorWithCalibratedWhite:0.1 alpha:1.0] retain]; 58 | MBCoverFlowScrollerInactiveBackgroundTopColor = [[NSColor colorWithCalibratedWhite:0.1 alpha:1.0] retain]; 59 | MBCoverFlowScrollerPressedBackgroundTopColor = [[NSColor colorWithCalibratedWhite:0.8 alpha:1.0] retain]; 60 | MBCoverFlowScrollerBackgroundBottomColor = [[NSColor colorWithCalibratedWhite:0.0 alpha:1.0] retain]; 61 | MBCoverFlowScrollerInactiveBackgroundBottomColor = [[NSColor colorWithCalibratedWhite:0.0 alpha:1.0] retain]; 62 | MBCoverFlowScrollerPressedBackgroundBottomColor = [[NSColor colorWithCalibratedWhite:0.4 alpha:1.0] retain]; 63 | MBCoverFlowScrollerGlossTopColor = [[NSColor colorWithCalibratedWhite:1.0 alpha:0.3] retain]; 64 | MBCoverFlowScrollerInactiveGlossTopColor = [[NSColor colorWithCalibratedWhite:1.0 alpha:0.3] retain]; 65 | MBCoverFlowScrollerPressedGlossTopColor = [[NSColor colorWithCalibratedWhite:1.0 alpha:0.3] retain]; 66 | MBCoverFlowScrollerGlossBottomColor = [[NSColor colorWithCalibratedWhite:1.0 alpha:0.1] retain]; 67 | MBCoverFlowScrollerInactiveGlossBottomColor = [[NSColor colorWithCalibratedWhite:1.0 alpha:0.0] retain]; 68 | MBCoverFlowScrollerPressedGlossBottomColor = [[NSColor colorWithCalibratedWhite:1.0 alpha:0.1] retain]; 69 | MBCoverFlowScrollerSlotBackgroundColor = [[NSColor colorWithCalibratedWhite:1.0 alpha:0.3] retain]; 70 | MBCoverFlowScrollerInactiveSlotBackgroundColor = [[NSColor colorWithCalibratedWhite:1.0 alpha:0.2] retain]; 71 | MBCoverFlowScrollerSlotInsetColor = [[NSColor colorWithCalibratedWhite:0.0 alpha:0.2] retain]; 72 | } 73 | 74 | - (void)drawRect:(NSRect)rect 75 | { 76 | // Don't draw the scroller if it can't be scrolled 77 | if ([self knobProportion] >= 1.0) { 78 | return; 79 | } 80 | 81 | [self drawKnobSlotInRect:[self rectForPart:NSScrollerKnobSlot] highlight:NO] ; 82 | [self drawArrow:NSScrollerIncrementArrow highlight:( [self hitPart] == NSScrollerIncrementLine )] ; 83 | [self drawArrow:NSScrollerDecrementArrow highlight:( [self hitPart] == NSScrollerDecrementLine )] ; 84 | [self drawKnob]; 85 | } 86 | 87 | - (NSScrollerPart)testPart:(NSPoint)aPoint 88 | { 89 | [super testPart:aPoint]; 90 | 91 | aPoint = [self convertPoint:aPoint fromView:nil]; 92 | 93 | if ([[self _leftArrowPath] containsPoint:aPoint]) { 94 | return NSScrollerDecrementLine; 95 | } else if ([[self _rightArrowPath] containsPoint:aPoint]) { 96 | return NSScrollerIncrementLine; 97 | } else if (NSPointInRect(aPoint, [self rectForPart:NSScrollerKnob])) { 98 | return NSScrollerKnob; 99 | } 100 | return NSScrollerNoPart; 101 | } 102 | 103 | - (NSRect)rectForPart:(NSScrollerPart)aPart 104 | { 105 | if (aPart == NSScrollerDecrementLine) { 106 | NSRect rect = [self rectForPart:NSScrollerKnobSlot]; 107 | rect.origin.x = 0; 108 | rect.size.width = 30.0; 109 | return rect; 110 | } else if (aPart == NSScrollerIncrementLine) { 111 | NSRect rect = [self rectForPart:NSScrollerKnobSlot]; 112 | rect.size.width = 30.0; 113 | rect.origin.x = [self frame].size.width - rect.size.width; 114 | return rect; 115 | } else if (aPart == NSScrollerKnobSlot) { 116 | NSRect rect; 117 | rect.size.height = 16.0; 118 | rect.origin.x = 15.0; 119 | rect.size.width = [self frame].size.width - 2*rect.origin.x; 120 | rect.origin.y = [self frame].size.height - rect.size.height; 121 | return rect; 122 | } else if (aPart == NSScrollerKnob) { 123 | NSRect rect = [self rectForPart:NSScrollerKnobSlot]; 124 | float maxWidth = [self frame].size.width - ([self rectForPart:NSScrollerDecrementLine].size.width - 8.0 - 1.0) - ([self rectForPart:NSScrollerIncrementLine].size.width - 8.0 - 1.0); 125 | float minWidth = MBCoverFlowScrollerKnobMinimumWidth; 126 | rect.size.width = fmax(maxWidth * [self knobProportion], minWidth); 127 | 128 | rect.origin.x = NSMaxX([self rectForPart:NSScrollerDecrementLine]) - 8.0 - 1.0; 129 | 130 | float incrementWidth = (maxWidth - rect.size.width) / (self.numberOfIncrements); 131 | rect.origin.x += [self integerValue] * incrementWidth; 132 | 133 | return rect; 134 | } else if (aPart == NSScrollerDecrementPage) { 135 | 136 | } else if (aPart == NSScrollerIncrementPage) { 137 | 138 | } 139 | 140 | return NSZeroRect; 141 | } 142 | 143 | - (NSInteger)integerValue 144 | { 145 | return floor([self floatValue] * (self.numberOfIncrements)); 146 | } 147 | 148 | - (void)setIntegerValue:(NSInteger)value 149 | { 150 | [self setFloatValue:((float)value / (float)self.numberOfIncrements)+0.01]; 151 | } 152 | 153 | - (void)setNumberOfIncrements:(NSUInteger)newIncrements 154 | { 155 | _numberOfIncrements = newIncrements; 156 | if (newIncrements > 0) { 157 | [self setKnobProportion:(1.0/(self.numberOfIncrements+1))]; 158 | } else { 159 | [self setKnobProportion:1.0]; 160 | } 161 | [self setNeedsDisplay:YES]; 162 | } 163 | 164 | /* The documentation for NSScroller says to use -drawArrow:highlight:, but 165 | * that's never called. -drawArrow:highlightPart: is. 166 | */ 167 | - (void)drawArrow:(NSScrollerArrow)arrow highlightPart:(BOOL)flag 168 | { 169 | [self drawArrow:arrow highlight:flag]; 170 | } 171 | 172 | /* Since we've repositioned the arrows, NSScroller doesn't want to redisplay 173 | * the left one when it's clicked. Thus, we should just always redisplay the 174 | * entire view */ 175 | - (void)setNeedsDisplayInRect:(NSRect)rect 176 | { 177 | if (!NSEqualRects(rect, [self bounds])) { 178 | rect = [self bounds]; 179 | } 180 | 181 | [super setNeedsDisplayInRect:rect]; 182 | } 183 | 184 | - (void)drawArrow:(NSScrollerArrow)arrow highlight:(BOOL)flag 185 | { 186 | if (arrow == NSScrollerDecrementArrow) { 187 | NSBezierPath *arrowPath = [self _leftArrowPath]; 188 | [[NSGraphicsContext currentContext] saveGraphicsState]; 189 | [arrowPath addClip]; 190 | 191 | // Determine the proper colors 192 | NSColor *outlineColor = MBCoverFlowScrollerOutlineColor; 193 | NSColor *bgTop = MBCoverFlowScrollerBackgroundTopColor; 194 | NSColor *bgBottom = MBCoverFlowScrollerBackgroundBottomColor; 195 | NSColor *glossTop = MBCoverFlowScrollerGlossTopColor; 196 | NSColor *glossBottom = MBCoverFlowScrollerGlossBottomColor; 197 | 198 | if (flag) { 199 | outlineColor = MBCoverFlowScrollerPressedOutlineColor; 200 | bgTop = MBCoverFlowScrollerPressedBackgroundTopColor; 201 | bgBottom = MBCoverFlowScrollerPressedBackgroundBottomColor; 202 | glossTop = MBCoverFlowScrollerPressedGlossTopColor; 203 | glossBottom = MBCoverFlowScrollerPressedGlossBottomColor; 204 | } else if (![[self window] isKeyWindow]) { 205 | outlineColor = MBCoverFlowScrollerInactiveOutlineColor; 206 | bgTop = MBCoverFlowScrollerInactiveBackgroundTopColor; 207 | bgBottom = MBCoverFlowScrollerInactiveBackgroundBottomColor; 208 | glossTop = MBCoverFlowScrollerInactiveGlossTopColor; 209 | glossBottom = MBCoverFlowScrollerInactiveGlossBottomColor; 210 | } 211 | 212 | // Draw the background 213 | NSGradient *bgGradient = [[NSGradient alloc] initWithStartingColor:bgTop endingColor:bgBottom]; 214 | [bgGradient drawInBezierPath:arrowPath angle:90.0]; 215 | [bgGradient release]; 216 | 217 | // Draw the gloss 218 | NSGradient *glossGradient = [[NSGradient alloc] initWithStartingColor:glossTop endingColor:glossBottom]; 219 | NSRect glossRect = [self rectForPart:NSScrollerDecrementLine]; 220 | glossRect.origin.x += 4.0; 221 | glossRect.size.width += 20.0; 222 | glossRect.size.height /= 2.0; 223 | NSBezierPath *glossPath = [NSBezierPath bezierPathWithRoundedRect:glossRect xRadius:8.0 yRadius:4.0]; 224 | [glossGradient drawInBezierPath:glossPath angle:90.0]; 225 | [glossGradient release]; 226 | 227 | [arrowPath setLineWidth:2.0]; 228 | 229 | [outlineColor set]; 230 | [arrowPath stroke]; 231 | 232 | [[NSGraphicsContext currentContext] restoreGraphicsState]; 233 | 234 | // Draw the arrow 235 | NSRect arrowRect = [self rectForPart:NSScrollerDecrementLine]; 236 | NSPoint glyphTip = NSMakePoint(arrowRect.origin.x + 9.0, NSMidY(arrowRect)); 237 | NSPoint glyphTop = NSMakePoint(glyphTip.x + 6.0, NSMinY(arrowRect) + 5.0); 238 | NSPoint glyphBottom = NSMakePoint(glyphTop.x, NSMaxY(arrowRect) - 5.0); 239 | NSBezierPath *glyphPath = [NSBezierPath bezierPath]; 240 | [glyphPath moveToPoint:glyphTip]; 241 | [glyphPath lineToPoint:glyphTop]; 242 | [glyphPath lineToPoint:glyphBottom]; 243 | [glyphPath lineToPoint:glyphTip]; 244 | [glyphPath closePath]; 245 | [outlineColor set]; 246 | [glyphPath fill]; 247 | 248 | } else if (arrow == NSScrollerIncrementArrow) { 249 | NSBezierPath *arrowPath = [self _rightArrowPath]; 250 | [[NSGraphicsContext currentContext] saveGraphicsState]; 251 | [arrowPath addClip]; 252 | 253 | // Determine the proper colors 254 | NSColor *outlineColor = MBCoverFlowScrollerOutlineColor; 255 | NSColor *bgTop = MBCoverFlowScrollerBackgroundTopColor; 256 | NSColor *bgBottom = MBCoverFlowScrollerBackgroundBottomColor; 257 | NSColor *glossTop = MBCoverFlowScrollerGlossTopColor; 258 | NSColor *glossBottom = MBCoverFlowScrollerGlossBottomColor; 259 | 260 | if (flag) { 261 | outlineColor = MBCoverFlowScrollerPressedOutlineColor; 262 | bgTop = MBCoverFlowScrollerPressedBackgroundTopColor; 263 | bgBottom = MBCoverFlowScrollerPressedBackgroundBottomColor; 264 | glossTop = MBCoverFlowScrollerPressedGlossTopColor; 265 | glossBottom = MBCoverFlowScrollerPressedGlossBottomColor; 266 | } else if (![[self window] isKeyWindow]) { 267 | outlineColor = MBCoverFlowScrollerInactiveOutlineColor; 268 | bgTop = MBCoverFlowScrollerInactiveBackgroundTopColor; 269 | bgBottom = MBCoverFlowScrollerInactiveBackgroundBottomColor; 270 | glossTop = MBCoverFlowScrollerInactiveGlossTopColor; 271 | glossBottom = MBCoverFlowScrollerInactiveGlossBottomColor; 272 | } 273 | 274 | // Draw the background 275 | NSGradient *bgGradient = [[NSGradient alloc] initWithStartingColor:bgTop endingColor:bgBottom]; 276 | [bgGradient drawInBezierPath:arrowPath angle:90.0]; 277 | [bgGradient release]; 278 | 279 | // Draw the gloss 280 | NSGradient *glossGradient = [[NSGradient alloc] initWithStartingColor:glossTop endingColor:glossBottom]; 281 | NSRect glossRect = [self rectForPart:NSScrollerIncrementLine]; 282 | glossRect.origin.x -= 24.0; 283 | glossRect.size.width += 20.0; 284 | glossRect.size.height /= 2.0; 285 | NSBezierPath *glossPath = [NSBezierPath bezierPathWithRoundedRect:glossRect xRadius:8.0 yRadius:4.0]; 286 | [glossGradient drawInBezierPath:glossPath angle:90.0]; 287 | [glossGradient release]; 288 | 289 | [arrowPath setLineWidth:2.0]; 290 | 291 | [outlineColor set]; 292 | [arrowPath stroke]; 293 | 294 | [[NSGraphicsContext currentContext] restoreGraphicsState]; 295 | 296 | // Draw the arrow 297 | NSRect arrowRect = [self rectForPart:NSScrollerIncrementLine]; 298 | NSPoint glyphTip = NSMakePoint(NSMaxX(arrowRect) - 9.0, NSMidY(arrowRect)); 299 | NSPoint glyphTop = NSMakePoint(glyphTip.x - 6.0, NSMinY(arrowRect) + 5.0); 300 | NSPoint glyphBottom = NSMakePoint(glyphTop.x, NSMaxY(arrowRect) - 5.0); 301 | NSBezierPath *glyphPath = [NSBezierPath bezierPath]; 302 | [glyphPath moveToPoint:glyphTip]; 303 | [glyphPath lineToPoint:glyphTop]; 304 | [glyphPath lineToPoint:glyphBottom]; 305 | [glyphPath lineToPoint:glyphTip]; 306 | [glyphPath closePath]; 307 | [outlineColor set]; 308 | [glyphPath fill]; 309 | } 310 | } 311 | 312 | - (void)drawKnobSlotInRect:(NSRect)slotRect highlight:(BOOL)flag 313 | { 314 | NSBezierPath *slotPath = [NSBezierPath bezierPathWithRect:NSInsetRect(slotRect, 0.5, 0.5)]; 315 | 316 | // Determine the proper colors 317 | NSColor *bgColor = MBCoverFlowScrollerSlotBackgroundColor; 318 | NSColor *outlineColor = MBCoverFlowScrollerOutlineColor; 319 | if (![[self window] isKeyWindow]) { 320 | bgColor = MBCoverFlowScrollerInactiveSlotBackgroundColor; 321 | outlineColor = MBCoverFlowScrollerInactiveOutlineColor; 322 | } 323 | 324 | [bgColor set]; 325 | [slotPath fill]; 326 | [outlineColor set]; 327 | [slotPath setLineWidth:1.0]; 328 | [slotPath stroke]; 329 | 330 | NSRect insetRect = NSMakeRect(slotRect.origin.x, slotRect.origin.y+1.0, slotRect.size.width, 1.0); 331 | [MBCoverFlowScrollerSlotInsetColor set]; 332 | [NSBezierPath fillRect:insetRect]; 333 | } 334 | 335 | - (void)drawKnob 336 | { 337 | NSRect knobRect = [self rectForPart:NSScrollerKnob]; 338 | NSBezierPath *path = [NSBezierPath bezierPath]; 339 | 340 | NSPoint topLeft = NSMakePoint(NSMinX(knobRect) + 8.0, NSMinY(knobRect)); 341 | NSPoint bottomLeft = NSMakePoint(topLeft.x, NSMaxY(knobRect)); 342 | NSPoint topRight = NSMakePoint(NSMaxX(knobRect) - 8.0, topLeft.y); 343 | NSPoint bottomRight = NSMakePoint(topRight.x, bottomLeft.y); 344 | 345 | [path appendBezierPathWithArcWithCenter:NSMakePoint(topLeft.x, (topLeft.y + bottomLeft.y)/2) radius:(bottomLeft.y - topLeft.y)/2 startAngle:90 endAngle:270]; 346 | [path appendBezierPathWithArcWithCenter:NSMakePoint(topRight.x, (topRight.y + bottomRight.y)/2) radius:(bottomRight.y - topRight.y)/2 startAngle:-90 endAngle:90]; 347 | [path moveToPoint:bottomLeft]; 348 | [path lineToPoint:bottomRight]; 349 | 350 | NSBezierPath *knobPath = path; 351 | 352 | [[NSGraphicsContext currentContext] saveGraphicsState]; 353 | [knobPath addClip]; 354 | 355 | // Determine the proper colors 356 | NSColor *outlineColor = MBCoverFlowScrollerOutlineColor; 357 | NSColor *bgTop = MBCoverFlowScrollerBackgroundTopColor; 358 | NSColor *bgBottom = MBCoverFlowScrollerBackgroundBottomColor; 359 | NSColor *glossTop = MBCoverFlowScrollerGlossTopColor; 360 | NSColor *glossBottom = MBCoverFlowScrollerGlossBottomColor; 361 | 362 | if (![[self window] isKeyWindow]) { 363 | outlineColor = MBCoverFlowScrollerInactiveOutlineColor; 364 | bgTop = MBCoverFlowScrollerInactiveBackgroundTopColor; 365 | bgBottom = MBCoverFlowScrollerInactiveBackgroundBottomColor; 366 | glossTop = MBCoverFlowScrollerInactiveGlossTopColor; 367 | glossBottom = MBCoverFlowScrollerInactiveGlossBottomColor; 368 | } 369 | 370 | // Draw the background 371 | NSGradient *bgGradient = [[NSGradient alloc] initWithStartingColor:bgTop endingColor:bgBottom]; 372 | [bgGradient drawInBezierPath:knobPath angle:90.0]; 373 | [bgGradient release]; 374 | 375 | // Draw the gloss 376 | NSGradient *glossGradient = [[NSGradient alloc] initWithStartingColor:glossTop endingColor:glossBottom]; 377 | NSRect glossRect = [self rectForPart:NSScrollerKnob]; 378 | glossRect.origin.x += 4.0; 379 | glossRect.size.width -= 8.0; 380 | glossRect.size.height /= 2.0; 381 | NSBezierPath *glossPath = [NSBezierPath bezierPathWithRoundedRect:glossRect xRadius:8.0 yRadius:4.0]; 382 | [glossGradient drawInBezierPath:glossPath angle:90.0]; 383 | [glossGradient release]; 384 | 385 | [knobPath setLineWidth:2.0]; 386 | 387 | [outlineColor set]; 388 | [knobPath stroke]; 389 | 390 | [[NSGraphicsContext currentContext] restoreGraphicsState]; 391 | } 392 | 393 | - (NSRect)_drawingRectForPart:(NSScrollerPart)aPart 394 | { 395 | // Super's implementation has some side effects 396 | [super _drawingRectForPart:aPart]; 397 | 398 | // Return the appropriate rectangle 399 | return [self rectForPart:aPart]; 400 | } 401 | 402 | - (NSBezierPath *)_leftArrowPath 403 | { 404 | NSRect arrowRect = [self rectForPart:NSScrollerDecrementLine]; 405 | NSBezierPath *path = [NSBezierPath bezierPath]; 406 | 407 | NSPoint topLeft = NSMakePoint(NSMinX(arrowRect) + 8.0, NSMinY(arrowRect)); 408 | NSPoint bottomLeft = NSMakePoint(topLeft.x, NSMaxY(arrowRect)); 409 | NSPoint topRight = NSMakePoint(NSMaxX(arrowRect), topLeft.y); 410 | NSPoint bottomRight = NSMakePoint(topRight.x, bottomLeft.y); 411 | 412 | [path appendBezierPathWithArcWithCenter:NSMakePoint(topLeft.x, (topLeft.y + bottomLeft.y)/2) radius:(bottomLeft.y - topLeft.y)/2 startAngle:90 endAngle:270]; 413 | [path lineToPoint:topRight]; 414 | [path lineToPoint:bottomRight]; 415 | [path moveToPoint:topRight]; 416 | [path appendBezierPathWithArcWithCenter:NSMakePoint(NSMaxX(arrowRect), (topRight.y + bottomRight.y)/2) radius:(bottomRight.y - topRight.y)/2 startAngle:90 endAngle:270]; 417 | [path moveToPoint:bottomRight]; 418 | [path lineToPoint:bottomLeft]; 419 | [path setWindingRule:NSEvenOddWindingRule]; 420 | [path closePath]; 421 | 422 | return path; 423 | } 424 | 425 | - (NSBezierPath *)_rightArrowPath 426 | { 427 | NSRect arrowRect = [self rectForPart:NSScrollerIncrementLine]; 428 | NSBezierPath *path = [NSBezierPath bezierPath]; 429 | 430 | NSPoint topLeft = NSMakePoint(NSMinX(arrowRect), NSMinY(arrowRect)); 431 | NSPoint bottomLeft = NSMakePoint(topLeft.x, NSMaxY(arrowRect)); 432 | NSPoint topRight = NSMakePoint(NSMaxX(arrowRect)-8.0, topLeft.y); 433 | NSPoint bottomRight = NSMakePoint(topRight.x, bottomLeft.y); 434 | 435 | [path appendBezierPathWithArcWithCenter:NSMakePoint(topRight.x, (topRight.y + bottomRight.y)/2) radius:(bottomRight.y - topRight.y)/2 startAngle:-90 endAngle:90]; 436 | [path lineToPoint:bottomLeft]; 437 | [path lineToPoint:topLeft]; 438 | [path moveToPoint:bottomLeft]; 439 | [path appendBezierPathWithArcWithCenter:NSMakePoint(NSMinX(arrowRect), (topLeft.y + bottomLeft.y)/2) radius:(bottomLeft.y - topLeft.y)/2 startAngle:270 endAngle:90]; 440 | [path moveToPoint:topLeft]; 441 | [path lineToPoint:topRight]; 442 | [path setWindingRule:NSEvenOddWindingRule]; 443 | [path closePath]; 444 | 445 | return path; 446 | } 447 | 448 | /*- (NSScrollerPart)hitPart 449 | { 450 | return [self testPart:[[NSApp currentEvent] locationInWindow]]; 451 | }*/ 452 | 453 | @end 454 | -------------------------------------------------------------------------------- /MBCoverFlowView.h: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | The MIT License 4 | 5 | Copyright (c) 2009 Matthew Ball 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | 25 | */ 26 | 27 | #import 28 | #import 29 | 30 | @class MBCoverFlowScroller; 31 | 32 | /** 33 | * @brief An NSView subclass which displays a collection of 34 | * items using the Cover Flow style. 35 | */ 36 | @interface MBCoverFlowView : NSView { 37 | NSInteger _selectionIndex; 38 | 39 | // Layers 40 | CAScrollLayer *_scrollLayer; 41 | CALayer *_containerLayer; 42 | CALayer *_leftGradientLayer; 43 | CALayer *_rightGradientLayer; 44 | CALayer *_bottomGradientLayer; 45 | 46 | // Appearance 47 | CGImageRef _shadowImage; 48 | CATransform3D _leftTransform; 49 | CATransform3D _rightTransform; 50 | 51 | // Display Attributes 52 | NSSize _itemSize; 53 | NSViewController *_accessoryController; 54 | MBCoverFlowScroller *_scroller; 55 | BOOL _showsScrollbar; 56 | BOOL _autoresizesItems; 57 | CGImageRef _placeholderRef; 58 | NSImage *_placeholderIcon; 59 | 60 | // Data 61 | NSArray *_content; 62 | NSString *_imageKeyPath; 63 | NSOperationQueue *_imageLoadQueue; 64 | 65 | // Bindings 66 | NSMutableDictionary *_bindingInfo; 67 | 68 | // Actions 69 | id _target; 70 | SEL _action; 71 | } 72 | 73 | /** 74 | * @name Loading Data 75 | */ 76 | 77 | /** 78 | * @brief The receiver's content object. 79 | * 80 | * @see imageKeyPath 81 | */ 82 | @property (nonatomic, copy) NSArray *content; 83 | 84 | /** 85 | * @brief The key path which returns the image for an item 86 | * in the receiver's \c content array. 87 | * 88 | * @see content 89 | */ 90 | @property (nonatomic, copy) NSString *imageKeyPath; 91 | 92 | /** 93 | * @name Setting Display Attributes 94 | */ 95 | 96 | /** 97 | * @brief Whether or not the receiver should resize items to fit 98 | * the available vertical space. Defaults to \c YES. 99 | */ 100 | @property (nonatomic, assign) BOOL autoresizesItems; 101 | 102 | /** 103 | * @brief The size of the flow items. 104 | */ 105 | @property (nonatomic, assign) NSSize itemSize; 106 | 107 | /** 108 | * @brief Whether or not the receiver should display a scrollbar at 109 | * the bottom of the view. 110 | */ 111 | @property (nonatomic, assign) BOOL showsScrollbar; 112 | 113 | /** 114 | * @brief The controller which manages the receiver's accessory view. 115 | * @details The accessory controller's representedObject will be bound 116 | * to the receiver's selectedObject. The accessory controller's 117 | * view will be displayed below the flow images. 118 | */ 119 | @property (nonatomic, retain) NSViewController *accessoryController; 120 | 121 | /** 122 | * @brief The icon which will be displayed for items which have not had 123 | image data loaded. 124 | * @details This image should preferably be a template icon (using NSImage's 125 | * \c -setTemplate: method), so that the view can color the icon 126 | * appropriately. 127 | */ 128 | @property (nonatomic, retain) NSImage *placeholderIcon; 129 | 130 | /** 131 | * @name Managing the Selection 132 | */ 133 | 134 | /** 135 | * @brief The index of the receiver's front-most item. 136 | * 137 | * @see selectedObject 138 | */ 139 | @property (nonatomic, assign) NSInteger selectionIndex; 140 | 141 | /** 142 | * @brief The receiver's front-most item. 143 | * 144 | * @see selectionIndex 145 | */ 146 | @property (nonatomic, assign) id selectedObject; 147 | 148 | /** 149 | * @name The Target/Action Mechanism 150 | */ 151 | 152 | /** 153 | * @brief The target object that receives action messages from the view. 154 | * 155 | * @see action 156 | */ 157 | @property (nonatomic, assign) id target; 158 | 159 | /** 160 | * @brief The selector associated with the view. 161 | * @details The action will be called when the user double-clicks an item 162 | * or presses the Return key. 163 | * 164 | * @see target 165 | */ 166 | @property (nonatomic, assign) SEL action; 167 | 168 | /** 169 | * @name Layout Support 170 | */ 171 | 172 | /** 173 | * @brief Returns the area occupied by the flow item at the 174 | * specified index. 175 | * 176 | * @param index The index of the item 177 | * 178 | * @return A rectangle defining the area in which the view draws the 179 | * item at \c index, or \c NSZeroRect if the index is invalid. 180 | * 181 | * @see indexOfItemAtPoint: 182 | */ 183 | - (NSRect)rectForItemAtIndex:(NSInteger)index; 184 | 185 | /** 186 | * @brief Returns the index of the flow item a given point lies in. 187 | * 188 | * @param aPoint A point in the coordinate system of the receiver. 189 | * 190 | * @return The index of the flow item \c aPoint lies in, or \c NSNotFound 191 | * is \c aPoint does not lie inside an item. 192 | * 193 | * @see rectForItemAtIndex: 194 | */ 195 | - (NSInteger)indexOfItemAtPoint:(NSPoint)aPoint; 196 | 197 | @end 198 | -------------------------------------------------------------------------------- /MBCoverFlowView.m: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | The MIT License 4 | 5 | Copyright (c) 2009 Matthew Ball 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | 25 | */ 26 | 27 | #import "MBCoverFlowView.h" 28 | 29 | #import "MBCoverFlowScroller.h" 30 | #import "NSImage+MBCoverFlowAdditions.h" 31 | 32 | #import 33 | 34 | // Constants 35 | #define MBCoverFlowViewCellSpacing ([self itemSize].width/10) 36 | 37 | const float MBCoverFlowViewPlaceholderHeight = 600; 38 | 39 | const float MBCoverFlowViewTopMargin = 30.0; 40 | const float MBCoverFlowViewBottomMargin = 20.0; 41 | const float MBCoverFlowViewHorizontalMargin = 12.0; 42 | #define MBCoverFlowViewContainerMinY (NSMaxY([self.accessoryController.view frame]) - 3*[self itemSize].height/4) 43 | 44 | const float MBCoverFlowScrollerHorizontalMargin = 80.0; 45 | const float MBCoverFlowScrollerVerticalSpacing = 16.0; 46 | 47 | const float MBCoverFlowViewDefaultItemWidth = 140.0; 48 | const float MBCoverFlowViewDefaultItemHeight = 100.0; 49 | 50 | const float MBCoverFlowScrollMinimumDeltaThreshold = 0.4; 51 | 52 | // Perspective parameters 53 | const float MBCoverFlowViewPerspectiveCenterPosition = 100.0; 54 | const float MBCoverFlowViewPerspectiveSidePosition = 0.0; 55 | const float MBCoverFlowViewPerspectiveSideSpacingFactor = 0.75; 56 | const float MBCoverFlowViewPerspectiveRowScaleFactor = 0.85; 57 | const float MBCoverFlowViewPerspectiveAngle = 0.79; 58 | 59 | // KVO 60 | static NSString *MBCoverFlowViewImagePathContext; 61 | 62 | // Key Codes 63 | #define MBLeftArrowKeyCode 123 64 | #define MBRightArrowKeyCode 124 65 | #define MBReturnKeyCode 36 66 | 67 | @interface MBCoverFlowView () 68 | - (float)_positionOfSelectedItem; 69 | - (CALayer *)_insertLayerInScrollLayer; 70 | - (void)_scrollerChange:(MBCoverFlowScroller *)scroller; 71 | - (void)_refreshLayer:(CALayer *)layer; 72 | - (void)_loadImageForLayer:(CALayer *)layer; 73 | - (CALayer *)_layerForObject:(id)object; 74 | - (void)_recachePlaceholder; 75 | - (void)_setSelectionIndex:(NSInteger)index; // For two-way bindings 76 | @end 77 | 78 | 79 | @implementation MBCoverFlowView 80 | 81 | @synthesize accessoryController=_accessoryController, selectionIndex=_selectionIndex, 82 | itemSize=_itemSize, content=_content, showsScrollbar=_showsScrollbar, 83 | autoresizesItems=_autoresizesItems, imageKeyPath=_imageKeyPath, 84 | placeholderIcon=_placeholderIcon, target=_target, action=_action; 85 | 86 | @dynamic selectedObject; 87 | 88 | #pragma mark - 89 | #pragma mark Life Cycle 90 | 91 | + (void)initialize 92 | { 93 | [self exposeBinding:@"content"]; 94 | [self exposeBinding:@"selectionIndex"]; 95 | } 96 | 97 | - (id)initWithFrame:(NSRect)frameRect 98 | { 99 | if (self = [super initWithFrame:frameRect]) { 100 | _bindingInfo = [[NSMutableDictionary alloc] init]; 101 | 102 | _imageLoadQueue = [[NSOperationQueue alloc] init]; 103 | [_imageLoadQueue setMaxConcurrentOperationCount:1]; 104 | 105 | _placeholderIcon = [[NSImage imageNamed:NSImageNameQuickLookTemplate] retain]; 106 | 107 | _autoresizesItems = YES; 108 | 109 | [self setAutoresizesSubviews:YES]; 110 | 111 | // Create the scroller 112 | _scroller = [[MBCoverFlowScroller alloc] initWithFrame:NSMakeRect(10, 10, 400, 16)]; 113 | [_scroller setEnabled:YES]; 114 | [_scroller setTarget:self]; 115 | [_scroller setHidden:YES]; 116 | [_scroller setKnobProportion:1.0]; 117 | [_scroller setAction:@selector(_scrollerChange:)]; 118 | [self addSubview:_scroller]; 119 | 120 | _leftTransform = CATransform3DMakeRotation(-0.79, 0, -1, 0); 121 | _rightTransform = CATransform3DMakeRotation(MBCoverFlowViewPerspectiveAngle, 0, -1, 0); 122 | 123 | _itemSize = NSMakeSize(MBCoverFlowViewDefaultItemWidth, MBCoverFlowViewDefaultItemHeight); 124 | 125 | CALayer *rootLayer = [CALayer layer]; 126 | rootLayer.layoutManager = [CAConstraintLayoutManager layoutManager]; 127 | rootLayer.backgroundColor = CGColorGetConstantColor(kCGColorBlack); 128 | [self setLayer:rootLayer]; 129 | 130 | _containerLayer = [CALayer layer]; 131 | _containerLayer.name = @"body"; 132 | [_containerLayer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMidX relativeTo:@"superlayer" attribute:kCAConstraintMidX]]; 133 | [_containerLayer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintWidth relativeTo:@"superlayer" attribute:kCAConstraintWidth offset:-20]]; 134 | [_containerLayer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMinY relativeTo:@"superlayer" attribute:kCAConstraintMinY offset:MBCoverFlowViewContainerMinY]]; 135 | [_containerLayer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMaxY relativeTo:@"superlayer" attribute:kCAConstraintMaxY offset:-10]]; 136 | [rootLayer addSublayer:_containerLayer]; 137 | 138 | _scrollLayer = [CAScrollLayer layer]; 139 | _scrollLayer.scrollMode = kCAScrollHorizontally; 140 | _scrollLayer.autoresizingMask = kCALayerWidthSizable | kCALayerHeightSizable; 141 | _scrollLayer.layoutManager = self; 142 | [_containerLayer addSublayer:_scrollLayer]; 143 | 144 | // Create a gradient image to use for image shadows 145 | CGRect gradientRect; 146 | gradientRect.origin = CGPointZero; 147 | gradientRect.size = NSSizeToCGSize([self itemSize]); 148 | size_t bytesPerRow = 4*gradientRect.size.width; 149 | void* bitmapData = malloc(bytesPerRow * gradientRect.size.height); 150 | CGContextRef context = CGBitmapContextCreate(bitmapData, gradientRect.size.width, 151 | gradientRect.size.height, 8, bytesPerRow, 152 | CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB), kCGImageAlphaPremultipliedFirst); 153 | NSGradient *gradient = [[NSGradient alloc] initWithStartingColor:[NSColor colorWithDeviceWhite:0 alpha:0.6] endingColor:[NSColor colorWithDeviceWhite:0 alpha:1.0]]; 154 | NSGraphicsContext *nsContext = [NSGraphicsContext graphicsContextWithGraphicsPort:context flipped:YES]; 155 | [NSGraphicsContext saveGraphicsState]; 156 | [NSGraphicsContext setCurrentContext:nsContext]; 157 | [gradient drawInRect:NSMakeRect(0, 0, gradientRect.size.width, gradientRect.size.height) angle:90]; 158 | [NSGraphicsContext restoreGraphicsState]; 159 | _shadowImage = CGBitmapContextCreateImage(context); 160 | CGContextRelease(context); 161 | free(bitmapData); 162 | [gradient release]; 163 | 164 | 165 | /* create a pleasant gradient mask around our central layer. 166 | We don't have to worry about re-creating these when the window 167 | size changes because the images will be automatically interpolated 168 | to their new sizes; and as gradients, they are very well suited to 169 | interpolation. */ 170 | CALayer *maskLayer = [CALayer layer]; 171 | _leftGradientLayer = [CALayer layer]; 172 | _rightGradientLayer = [CALayer layer]; 173 | _bottomGradientLayer = [CALayer layer]; 174 | 175 | // left 176 | gradientRect.origin = CGPointZero; 177 | gradientRect.size.width = [self frame].size.width; 178 | gradientRect.size.height = [self frame].size.height; 179 | bytesPerRow = 4*gradientRect.size.width; 180 | bitmapData = malloc(bytesPerRow * gradientRect.size.height); 181 | context = CGBitmapContextCreate(bitmapData, gradientRect.size.width, 182 | gradientRect.size.height, 8, bytesPerRow, 183 | CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB), kCGImageAlphaPremultipliedFirst); 184 | gradient = [[NSGradient alloc] initWithStartingColor:[NSColor colorWithDeviceWhite:0. alpha:1.] endingColor:[NSColor colorWithDeviceWhite:0. alpha:0]]; 185 | nsContext = [NSGraphicsContext graphicsContextWithGraphicsPort:context flipped:YES]; 186 | [NSGraphicsContext saveGraphicsState]; 187 | [NSGraphicsContext setCurrentContext:nsContext]; 188 | [gradient drawInRect:NSMakeRect(0, 0, gradientRect.size.width, gradientRect.size.height) angle:0]; 189 | [NSGraphicsContext restoreGraphicsState]; 190 | CGImageRef gradientImage = CGBitmapContextCreateImage(context); 191 | _leftGradientLayer.contents = (id)gradientImage; 192 | CGContextRelease(context); 193 | CGImageRelease(gradientImage); 194 | free(bitmapData); 195 | 196 | // right 197 | bitmapData = malloc(bytesPerRow * gradientRect.size.height); 198 | context = CGBitmapContextCreate(bitmapData, gradientRect.size.width, 199 | gradientRect.size.height, 8, bytesPerRow, 200 | CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB), kCGImageAlphaPremultipliedFirst); 201 | nsContext = [NSGraphicsContext graphicsContextWithGraphicsPort:context flipped:YES]; 202 | [NSGraphicsContext saveGraphicsState]; 203 | [NSGraphicsContext setCurrentContext:nsContext]; 204 | [gradient drawInRect:NSMakeRect(0, 0, gradientRect.size.width, gradientRect.size.height) angle:180]; 205 | [NSGraphicsContext restoreGraphicsState]; 206 | gradientImage = CGBitmapContextCreateImage(context); 207 | _rightGradientLayer.contents = (id)gradientImage; 208 | CGContextRelease(context); 209 | CGImageRelease(gradientImage); 210 | free(bitmapData); 211 | 212 | // bottom 213 | gradientRect.size.width = [self frame].size.width; 214 | gradientRect.size.height = 32; 215 | bytesPerRow = 4*gradientRect.size.width; 216 | bitmapData = malloc(bytesPerRow * gradientRect.size.height); 217 | context = CGBitmapContextCreate(bitmapData, gradientRect.size.width, 218 | gradientRect.size.height, 8, bytesPerRow, 219 | CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB), kCGImageAlphaPremultipliedFirst); 220 | nsContext = [NSGraphicsContext graphicsContextWithGraphicsPort:context flipped:YES]; 221 | [NSGraphicsContext saveGraphicsState]; 222 | [NSGraphicsContext setCurrentContext:nsContext]; 223 | [gradient drawInRect:NSMakeRect(0, 0, gradientRect.size.width, gradientRect.size.height) angle:90]; 224 | [NSGraphicsContext restoreGraphicsState]; 225 | gradientImage = CGBitmapContextCreateImage(context); 226 | _bottomGradientLayer.contents = (id)gradientImage; 227 | CGContextRelease(context); 228 | CGImageRelease(gradientImage); 229 | free(bitmapData); 230 | [gradient release]; 231 | 232 | // the autoresizing mask allows it to change shape with the parent layer 233 | maskLayer.autoresizingMask = kCALayerWidthSizable | kCALayerHeightSizable; 234 | maskLayer.layoutManager = [CAConstraintLayoutManager layoutManager]; 235 | [_leftGradientLayer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMinX relativeTo:@"superlayer" attribute:kCAConstraintMinX]]; 236 | [_leftGradientLayer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMinY relativeTo:@"superlayer" attribute:kCAConstraintMinY]]; 237 | [_leftGradientLayer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMaxY relativeTo:@"superlayer" attribute:kCAConstraintMaxY]]; 238 | [_leftGradientLayer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMaxX relativeTo:@"superlayer" attribute:kCAConstraintMaxX scale:.5 offset:-[self itemSize].width / 2]]; 239 | [_rightGradientLayer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMaxX relativeTo:@"superlayer" attribute:kCAConstraintMaxX]]; 240 | [_rightGradientLayer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMinY relativeTo:@"superlayer" attribute:kCAConstraintMinY]]; 241 | [_rightGradientLayer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMaxY relativeTo:@"superlayer" attribute:kCAConstraintMaxY]]; 242 | [_rightGradientLayer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMinX relativeTo:@"superlayer" attribute:kCAConstraintMaxX scale:.5 offset:[self itemSize].width / 2]]; 243 | [_bottomGradientLayer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMaxX relativeTo:@"superlayer" attribute:kCAConstraintMaxX]]; 244 | [_bottomGradientLayer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMinY relativeTo:@"superlayer" attribute:kCAConstraintMinY]]; 245 | [_bottomGradientLayer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMinX relativeTo:@"superlayer" attribute:kCAConstraintMinX]]; 246 | [_bottomGradientLayer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMaxY relativeTo:@"superlayer" attribute:kCAConstraintMinY offset:32]]; 247 | 248 | _bottomGradientLayer.masksToBounds = YES; 249 | 250 | [maskLayer addSublayer:_rightGradientLayer]; 251 | [maskLayer addSublayer:_leftGradientLayer]; 252 | [maskLayer addSublayer:_bottomGradientLayer]; 253 | // we make it a sublayer rather than a mask so that the overlapping alpha will work correctly 254 | // without the use of a compositing filter 255 | [_containerLayer addSublayer:maskLayer]; 256 | } 257 | return self; 258 | } 259 | 260 | - (void)dealloc 261 | { 262 | [_bindingInfo release]; 263 | [_scroller release]; 264 | [_scrollLayer release]; 265 | [_containerLayer release]; 266 | self.accessoryController = nil; 267 | self.content = nil; 268 | self.imageKeyPath = nil; 269 | self.placeholderIcon = nil; 270 | CGImageRelease(_placeholderRef); 271 | CGImageRelease(_shadowImage); 272 | [_imageLoadQueue release]; 273 | _imageLoadQueue = nil; 274 | [super dealloc]; 275 | } 276 | 277 | - (void)awakeFromNib 278 | { 279 | [self setWantsLayer:YES]; 280 | [self _recachePlaceholder]; 281 | } 282 | 283 | #pragma mark - 284 | #pragma mark Superclass Overrides 285 | 286 | #pragma mark NSResponder 287 | 288 | - (BOOL)acceptsFirstResponder 289 | { 290 | return YES; 291 | } 292 | 293 | - (void)keyDown:(NSEvent *)theEvent 294 | { 295 | switch ([theEvent keyCode]) { 296 | case MBLeftArrowKeyCode: 297 | [self _setSelectionIndex:(self.selectionIndex - 1)]; 298 | break; 299 | case MBRightArrowKeyCode: 300 | [self _setSelectionIndex:(self.selectionIndex + 1)]; 301 | break; 302 | case MBReturnKeyCode: 303 | if (self.target && self.action) { 304 | [self.target performSelector:self.action withObject:self]; 305 | break; 306 | } 307 | default: 308 | [super keyDown:theEvent]; 309 | break; 310 | } 311 | } 312 | 313 | - (void)mouseDown:(NSEvent *)theEvent 314 | { 315 | if ([theEvent clickCount] == 2 && self.target && self.action) { 316 | [self.target performSelector:self.action withObject:self]; 317 | } 318 | 319 | NSPoint mouseLocation = [self convertPoint:[theEvent locationInWindow] fromView:nil]; 320 | NSInteger clickedIndex = [self indexOfItemAtPoint:mouseLocation]; 321 | if (clickedIndex != NSNotFound) { 322 | [self _setSelectionIndex:clickedIndex]; 323 | } 324 | } 325 | 326 | - (void)scrollWheel:(NSEvent *)theEvent 327 | { 328 | if (fabs([theEvent deltaY]) > MBCoverFlowScrollMinimumDeltaThreshold) { 329 | if ([theEvent deltaY] > 0) { 330 | [self _setSelectionIndex:(self.selectionIndex - 1)]; 331 | } else { 332 | [self _setSelectionIndex:(self.selectionIndex + 1)]; 333 | } 334 | } else if (fabs([theEvent deltaX]) > MBCoverFlowScrollMinimumDeltaThreshold) { 335 | if ([theEvent deltaX] > 0) { 336 | [self _setSelectionIndex:(self.selectionIndex - 1)]; 337 | } else { 338 | [self _setSelectionIndex:(self.selectionIndex + 1)]; 339 | } 340 | } 341 | } 342 | 343 | #pragma mark NSView 344 | 345 | - (void)viewWillMoveToSuperview:(NSView *)newSuperview 346 | { 347 | [self resizeSubviewsWithOldSize:[self frame].size]; 348 | } 349 | 350 | - (void)resizeSubviewsWithOldSize:(NSSize)oldSize 351 | { 352 | float accessoryY = MBCoverFlowScrollerVerticalSpacing; 353 | 354 | // Reposition the scroller 355 | if (self.showsScrollbar) { 356 | NSRect scrollerFrame = [_scroller frame]; 357 | scrollerFrame.size.width = [self frame].size.width - 2*MBCoverFlowScrollerHorizontalMargin; 358 | scrollerFrame.origin.x = ([self frame].size.width - scrollerFrame.size.width)/2; 359 | scrollerFrame.origin.y = MBCoverFlowViewBottomMargin; 360 | [_scroller setFrame:scrollerFrame]; 361 | accessoryY += NSMaxY([_scroller frame]); 362 | } 363 | 364 | if (self.accessoryController.view) { 365 | NSRect accessoryFrame = [self.accessoryController.view frame]; 366 | accessoryFrame.origin.x = floor(([self frame].size.width - accessoryFrame.size.width)/2); 367 | accessoryFrame.origin.y = accessoryY; 368 | [self.accessoryController.view setFrame:accessoryFrame]; 369 | } 370 | 371 | _containerLayer.constraints = nil; 372 | [_containerLayer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMidX relativeTo:@"superlayer" attribute:kCAConstraintMidX]]; 373 | [_containerLayer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintWidth relativeTo:@"superlayer" attribute:kCAConstraintWidth offset:-20]]; 374 | [_containerLayer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMinY relativeTo:@"superlayer" attribute:kCAConstraintMinY offset:MBCoverFlowViewContainerMinY]]; 375 | [_containerLayer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMaxY relativeTo:@"superlayer" attribute:kCAConstraintMaxY offset:-10]]; 376 | 377 | self.selectionIndex = self.selectionIndex; 378 | } 379 | 380 | - (BOOL)mouseDownCanMoveWindow 381 | { 382 | return NO; 383 | } 384 | 385 | #pragma mark - 386 | #pragma mark Subclass Methods 387 | 388 | #pragma mark Loading Data 389 | 390 | - (void)setContent:(NSArray *)newContents 391 | { 392 | if ([newContents isEqualToArray:self.content]) { 393 | return; 394 | } 395 | 396 | NSArray *oldContent = [self.content retain]; 397 | 398 | if (_content) { 399 | [_content release]; 400 | _content = nil; 401 | } 402 | 403 | if (newContents != nil) { 404 | _content = [newContents copy]; 405 | } 406 | 407 | // Add any new items 408 | NSMutableArray *itemsToAdd = [self.content mutableCopy]; 409 | [itemsToAdd removeObjectsInArray:oldContent]; 410 | 411 | for (NSObject *object in itemsToAdd) { 412 | CALayer *layer = [self _insertLayerInScrollLayer]; 413 | [layer setValue:object forKey:@"representedObject"]; 414 | if (self.imageKeyPath) { 415 | [object addObserver:self forKeyPath:self.imageKeyPath options:0 context:&MBCoverFlowViewImagePathContext]; 416 | } 417 | [self _refreshLayer:layer]; 418 | } 419 | [itemsToAdd release]; 420 | 421 | // Remove any items which are no longer present 422 | NSMutableArray *itemsToRemove = [oldContent mutableCopy]; 423 | [itemsToRemove removeObjectsInArray:self.content]; 424 | for (NSObject *object in itemsToRemove) { 425 | CALayer *layer = [self _layerForObject:object]; 426 | if (self.imageKeyPath) { 427 | [[layer valueForKey:@"representedObject"] removeObserver:self forKeyPath:self.imageKeyPath]; 428 | } 429 | [layer removeFromSuperlayer]; 430 | } 431 | [itemsToRemove release]; 432 | 433 | [oldContent release]; 434 | 435 | // Update the layer indices 436 | for (CALayer *layer in [_scrollLayer sublayers]) { 437 | [layer setValue:[NSNumber numberWithInteger:[self.content indexOfObject:[layer valueForKey:@"representedObject"]]] forKey:@"index"]; 438 | } 439 | 440 | [_scroller setNumberOfIncrements:fmax([self.content count]-1, 0)]; 441 | self.selectionIndex = self.selectionIndex; 442 | } 443 | 444 | - (void)setImageKeyPath:(NSString *)keyPath 445 | { 446 | if (_imageKeyPath) { 447 | // Remove any observations for the existing key path 448 | for (NSObject *object in self.content) { 449 | [object removeObserver:self forKeyPath:self.imageKeyPath]; 450 | } 451 | 452 | [_imageKeyPath release]; 453 | _imageKeyPath = nil; 454 | } 455 | 456 | if (keyPath) { 457 | _imageKeyPath = [keyPath copy]; 458 | } 459 | 460 | // Refresh all the layers with images at the new key path 461 | for (CALayer *layer in [_scrollLayer sublayers]) { 462 | if (self.imageKeyPath) { 463 | [[layer valueForKey:@"representedObject"] addObserver:self forKeyPath:self.imageKeyPath options:0 context:&MBCoverFlowViewImagePathContext]; 464 | } 465 | [self _refreshLayer:layer]; 466 | } 467 | } 468 | 469 | #pragma mark Setting Display Attributes 470 | 471 | - (void)setAutoresizesItems:(BOOL)flag 472 | { 473 | _autoresizesItems = flag; 474 | [self resizeSubviewsWithOldSize:[self frame].size]; 475 | } 476 | 477 | - (NSSize)itemSize 478 | { 479 | if (!self.autoresizesItems) { 480 | return _itemSize; 481 | } 482 | 483 | float origin = MBCoverFlowViewBottomMargin; 484 | 485 | if (self.showsScrollbar) { 486 | origin += [_scroller frame].size.height + MBCoverFlowScrollerVerticalSpacing; 487 | } 488 | 489 | if (self.accessoryController.view) { 490 | NSRect accessoryFrame = [self.accessoryController.view frame]; 491 | origin += accessoryFrame.size.height; 492 | } 493 | 494 | NSSize size; 495 | size.height = fmax(([self frame].size.height - origin) - [self frame].size.height/3, 1.0f); 496 | size.width = size.height * _itemSize.width / _itemSize.height; 497 | 498 | // Make sure it's integral 499 | size.height = floor(size.height); 500 | size.width = floor(size.width); 501 | 502 | return size; 503 | } 504 | 505 | - (void)setItemSize:(NSSize)newSize 506 | { 507 | if (newSize.width <= 0) { 508 | newSize.width = MBCoverFlowViewDefaultItemWidth; 509 | } 510 | 511 | if (newSize.height <= 0) { 512 | newSize.height = MBCoverFlowViewDefaultItemHeight; 513 | } 514 | 515 | _itemSize = newSize; 516 | 517 | // Update all the various constraints which depend on the item size 518 | _containerLayer.constraints = nil; 519 | [_containerLayer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMidX relativeTo:@"superlayer" attribute:kCAConstraintMidX]]; 520 | [_containerLayer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintWidth relativeTo:@"superlayer" attribute:kCAConstraintWidth offset:-20]]; 521 | [_containerLayer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMinY relativeTo:@"superlayer" attribute:kCAConstraintMinY offset:MBCoverFlowViewContainerMinY]]; 522 | [_containerLayer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMaxY relativeTo:@"superlayer" attribute:kCAConstraintMaxY offset:-10]]; 523 | 524 | _leftGradientLayer.constraints = nil; 525 | [_leftGradientLayer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMinX relativeTo:@"superlayer" attribute:kCAConstraintMinX]]; 526 | [_leftGradientLayer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMinY relativeTo:@"superlayer" attribute:kCAConstraintMinY]]; 527 | [_leftGradientLayer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMaxY relativeTo:@"superlayer" attribute:kCAConstraintMaxY]]; 528 | [_leftGradientLayer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMaxX relativeTo:@"superlayer" attribute:kCAConstraintMaxX scale:.5 offset:-[self itemSize].width / 2]]; 529 | _rightGradientLayer.constraints = nil; 530 | [_rightGradientLayer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMaxX relativeTo:@"superlayer" attribute:kCAConstraintMaxX]]; 531 | [_rightGradientLayer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMinY relativeTo:@"superlayer" attribute:kCAConstraintMinY]]; 532 | [_rightGradientLayer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMaxY relativeTo:@"superlayer" attribute:kCAConstraintMaxY]]; 533 | [_rightGradientLayer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMinX relativeTo:@"superlayer" attribute:kCAConstraintMaxX scale:.5 offset:[self itemSize].width / 2]]; 534 | 535 | // Update the view 536 | [self _recachePlaceholder]; 537 | [self.layer setNeedsLayout]; 538 | 539 | CALayer *layer = [[_scrollLayer sublayers] objectAtIndex:self.selectionIndex]; 540 | CGRect layerFrame = [layer frame]; 541 | 542 | // Scroll so the selected item is centered 543 | [_scrollLayer scrollToPoint:CGPointMake([self _positionOfSelectedItem], layerFrame.origin.y)]; 544 | 545 | } 546 | 547 | - (void)setShowsScrollbar:(BOOL)flag 548 | { 549 | _showsScrollbar = flag; 550 | [_scroller setHidden:!flag]; 551 | [self resizeSubviewsWithOldSize:[self frame].size]; 552 | } 553 | 554 | - (void)setAccessoryController:(NSViewController *)aController 555 | { 556 | if (aController == self.accessoryController) 557 | return; 558 | 559 | if (self.accessoryController != nil) { 560 | [self.accessoryController.view removeFromSuperview]; 561 | [self.accessoryController unbind:@"representedObject"]; 562 | [_accessoryController release]; 563 | _accessoryController = nil; 564 | [self setNextResponder:nil]; 565 | } 566 | 567 | if (aController != nil) { 568 | _accessoryController = [aController retain]; 569 | [self addSubview:self.accessoryController.view]; 570 | [self.accessoryController setNextResponder:[self nextResponder]]; 571 | [self setNextResponder:self.accessoryController]; 572 | [self.accessoryController bind:@"representedObject" toObject:self withKeyPath:@"selectedObject" options:nil]; 573 | } 574 | 575 | [self resizeSubviewsWithOldSize:[self frame].size]; 576 | } 577 | 578 | #pragma mark Managing the Selection 579 | 580 | - (void)setSelectionIndex:(NSInteger)newIndex 581 | { 582 | if (newIndex >= [[_scrollLayer sublayers] count] || newIndex < 0) { 583 | return; 584 | } 585 | 586 | if ([[NSApp currentEvent] modifierFlags] & (NSAlphaShiftKeyMask|NSShiftKeyMask)) 587 | [CATransaction setValue:[NSNumber numberWithFloat:2.1f] forKey:@"animationDuration"]; 588 | else 589 | [CATransaction setValue:[NSNumber numberWithFloat:0.7f] forKey:@"animationDuration"]; 590 | 591 | _selectionIndex = newIndex; 592 | [_scrollLayer layoutIfNeeded]; 593 | 594 | CALayer *layer = [[_scrollLayer sublayers] objectAtIndex:self.selectionIndex]; 595 | CGRect layerFrame = [layer frame]; 596 | 597 | // Scroll so the selected item is centered 598 | [_scrollLayer scrollToPoint:CGPointMake([self _positionOfSelectedItem], layerFrame.origin.y)]; 599 | [_scroller setIntegerValue:self.selectionIndex]; 600 | } 601 | 602 | - (id)selectedObject 603 | { 604 | if ([self.content count] == 0 || self.selectionIndex >= [self.content count]) { 605 | return nil; 606 | } 607 | 608 | return [self.content objectAtIndex:self.selectionIndex]; 609 | } 610 | 611 | - (void)setSelectedObject:(id)anObject 612 | { 613 | if (![self.content containsObject:anObject]) { 614 | NSLog(@"[MBCoverFlowView setSelectedObject:] -- The view does not contain the specified object."); 615 | return; 616 | } 617 | 618 | [self _setSelectionIndex:[self.content indexOfObject:anObject]]; 619 | } 620 | 621 | #pragma mark Layout Support 622 | 623 | - (NSInteger)indexOfItemAtPoint:(NSPoint)aPoint 624 | { 625 | // Check the selected item first 626 | if (NSPointInRect(aPoint, [self rectForItemAtIndex:self.selectionIndex])) { 627 | return self.selectionIndex; 628 | } 629 | 630 | // Check the items to the left, in descending order 631 | NSInteger index = self.selectionIndex-1; 632 | while (index >= 0) { 633 | NSRect layerRect = [self rectForItemAtIndex:index]; 634 | if (NSPointInRect(aPoint, layerRect)) { 635 | return index; 636 | } 637 | index--; 638 | } 639 | 640 | // Check the items to the right, in ascending order 641 | index = self.selectionIndex+1; 642 | while (index < [self.content count]) { 643 | NSRect layerRect = [self rectForItemAtIndex:index]; 644 | if (NSPointInRect(aPoint, layerRect)) { 645 | return index; 646 | } 647 | index++; 648 | } 649 | 650 | return NSNotFound; 651 | } 652 | 653 | // FIXME: The frame returned is not quite wide enough. Don't know why -- probably due to the transforms 654 | - (NSRect)rectForItemAtIndex:(NSInteger)index 655 | { 656 | if (index < 0 || index >= [self.content count]) { 657 | return NSZeroRect; 658 | } 659 | 660 | CALayer *layer = [self _layerForObject:[self.content objectAtIndex:index]]; 661 | CALayer *imageLayer = [[layer sublayers] objectAtIndex:0]; 662 | 663 | CGRect frame = [imageLayer convertRect:[imageLayer frame] toLayer:self.layer]; 664 | return NSRectFromCGRect(frame); 665 | } 666 | 667 | #pragma mark - 668 | #pragma mark Private Methods 669 | 670 | - (CALayer *)_insertLayerInScrollLayer 671 | { 672 | /* this enables a perspective transform. The value of zDistance 673 | affects the sharpness of the transform */ 674 | float zDistance = 420.; 675 | CATransform3D sublayerTransform = CATransform3DIdentity; 676 | sublayerTransform.m34 = 1. / -zDistance; 677 | 678 | CALayer *layer = [CALayer layer]; 679 | CALayer *imageLayer = [CALayer layer]; 680 | 681 | CGRect frame; 682 | frame.origin = CGPointZero; 683 | frame.size = NSSizeToCGSize([self itemSize]); 684 | 685 | [imageLayer setBounds:frame]; 686 | imageLayer.contents = (id)_placeholderRef; 687 | imageLayer.name = @"image"; 688 | 689 | [layer setBounds:frame]; 690 | [layer setValue:[NSNumber numberWithInteger:[[_scrollLayer sublayers] count]] forKey:@"index"]; 691 | [layer setSublayers:[NSArray arrayWithObject:imageLayer]]; 692 | [layer setSublayerTransform:sublayerTransform]; 693 | [layer setValue:[NSNumber numberWithBool:NO] forKey:@"hasImage"]; 694 | 695 | CALayer *reflectionLayer = [CALayer layer]; 696 | frame.origin.y = -frame.size.height; 697 | [reflectionLayer setFrame:frame]; 698 | reflectionLayer.name = @"reflection"; 699 | reflectionLayer.transform = CATransform3DMakeScale(1, -1, 1); 700 | reflectionLayer.contents = (id)_placeholderRef; 701 | [imageLayer addSublayer:reflectionLayer]; 702 | 703 | CALayer *gradientLayer = [CALayer layer]; 704 | frame.origin.y += frame.size.height; 705 | frame.origin.x -= 1.0; 706 | frame.size.height += 2.0; 707 | frame.size.width += 2.0; 708 | [gradientLayer setFrame:frame]; 709 | [gradientLayer setContents:(id)_shadowImage]; 710 | gradientLayer.autoresizingMask = kCALayerWidthSizable | kCALayerHeightSizable; 711 | [reflectionLayer addSublayer:gradientLayer]; 712 | 713 | [_scrollLayer addSublayer:layer]; 714 | 715 | return layer; 716 | } 717 | 718 | - (float)_positionOfSelectedItem 719 | { 720 | // this is the same math used in layoutSublayersOfLayer:, before tweaking 721 | return floor(MBCoverFlowViewHorizontalMargin + .5*([_scrollLayer bounds].size.width - [self itemSize].width * [[_scrollLayer sublayers] count] - MBCoverFlowViewCellSpacing * ([[_scrollLayer sublayers] count] - 1))) + self.selectionIndex * ([self itemSize].width + MBCoverFlowViewCellSpacing) - .5 * [_scrollLayer bounds].size.width + .5 * [self itemSize].width; 722 | } 723 | 724 | - (void)_scrollerChange:(MBCoverFlowScroller *)sender 725 | { 726 | NSScrollerPart clickedPart = [sender hitPart]; 727 | if (clickedPart == NSScrollerIncrementLine) { 728 | [self _setSelectionIndex:(self.selectionIndex + 1)]; 729 | } else if (clickedPart == NSScrollerDecrementLine) { 730 | [self _setSelectionIndex:(self.selectionIndex - 1)]; 731 | } else if (clickedPart == NSScrollerKnob) { 732 | [self _setSelectionIndex:[sender integerValue]]; 733 | } 734 | } 735 | 736 | - (void)_refreshLayer:(CALayer *)layer 737 | { 738 | NSObject *object = [layer valueForKey:@"representedObject"]; 739 | NSInteger index = [self.content indexOfObject:object]; 740 | 741 | [layer setValue:[NSNumber numberWithInteger:index] forKey:@"index"]; 742 | [layer setValue:[NSNumber numberWithBool:NO] forKey:@"hasImage"]; 743 | 744 | // Create the operation 745 | NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(_loadImageForLayer:) object:layer]; 746 | [_imageLoadQueue addOperation:operation]; 747 | [operation release]; 748 | } 749 | 750 | - (void)_loadImageForLayer:(CALayer *)layer 751 | { 752 | @try { 753 | NSImage *image; 754 | NSObject *object = [layer valueForKey:@"representedObject"]; 755 | 756 | if (self.imageKeyPath != nil) { 757 | image = [object valueForKeyPath:self.imageKeyPath]; 758 | } else if ([object isKindOfClass:[NSImage class]]) { 759 | image = (NSImage *)object; 760 | } 761 | 762 | if ([image isKindOfClass:[NSData class]]) { 763 | image = [[[NSImage alloc] initWithData:(NSData *)image] autorelease]; 764 | } 765 | 766 | CGImageRef imageRef; 767 | 768 | if (!image) { 769 | imageRef = CGImageRetain(_placeholderRef); 770 | [layer setValue:[NSNumber numberWithBool:NO] forKey:@"hasImage"]; 771 | } else { 772 | imageRef = [image imageRefCopy]; 773 | [layer setValue:[NSNumber numberWithBool:YES] forKey:@"hasImage"]; 774 | } 775 | 776 | CALayer *imageLayer = [[layer sublayers] objectAtIndex:0]; 777 | CALayer *reflectionLayer = [[imageLayer sublayers] objectAtIndex:0]; 778 | 779 | imageLayer.contents = (id)imageRef; 780 | reflectionLayer.contents = (id)imageRef; 781 | imageLayer.backgroundColor = NULL; 782 | reflectionLayer.backgroundColor = NULL; 783 | CGImageRelease(imageRef); 784 | } @catch (NSException *e) { 785 | // If the key path isn't valid, do nothing 786 | } 787 | } 788 | 789 | - (CALayer *)_layerForObject:(id)object 790 | { 791 | for (CALayer *layer in [_scrollLayer sublayers]) { 792 | if ([object isEqual:[layer valueForKey:@"representedObject"]]) { 793 | return layer; 794 | } 795 | } 796 | return nil; 797 | } 798 | 799 | - (void)_recachePlaceholder 800 | { 801 | CGImageRelease(_placeholderRef); 802 | 803 | NSSize itemSize = self.itemSize; 804 | NSSize placeholderSize; 805 | placeholderSize.height = MBCoverFlowViewPlaceholderHeight; 806 | placeholderSize.width = itemSize.width * placeholderSize.height/itemSize.height; 807 | 808 | NSImage *placeholder = [[NSImage alloc] initWithSize:placeholderSize]; 809 | [placeholder lockFocus]; 810 | NSColor *topColor = [NSColor colorWithCalibratedWhite:0.15 alpha:1.0]; 811 | NSColor *bottomColor = [NSColor colorWithCalibratedWhite:0.0 alpha:1.0]; 812 | NSGradient *gradient = [[NSGradient alloc] initWithStartingColor:topColor endingColor:bottomColor]; 813 | [gradient drawInRect:NSMakeRect(0, 0, placeholderSize.width, placeholderSize.height) relativeCenterPosition:NSMakePoint(0, 1)]; 814 | [gradient release]; 815 | 816 | // Draw the top bevel line 817 | NSColor *bevelColor = [NSColor colorWithCalibratedWhite:0.3 alpha:1.0]; 818 | [bevelColor set]; 819 | NSRectFill(NSMakeRect(0, placeholderSize.height-5.0, placeholderSize.width, 5.0)); 820 | 821 | NSColor *bottomBevelColor = [NSColor colorWithCalibratedWhite:0.1 alpha:1.0]; 822 | [bottomBevelColor set]; 823 | NSRectFill(NSMakeRect(0, 0, placeholderSize.width, 5.0)); 824 | 825 | // Draw the placeholder icon 826 | if (self.placeholderIcon) { 827 | NSRect iconRect; 828 | iconRect.size.height = placeholderSize.height/2; 829 | iconRect.size.width = iconRect.size.height * [self placeholderIcon].size.width/[self placeholderIcon].size.height; 830 | 831 | if (iconRect.size.width > placeholderSize.width * 0.666) { 832 | iconRect.size.width = placeholderSize.width/2; 833 | iconRect.size.height = iconRect.size.width * [self placeholderIcon].size.height/[self placeholderIcon].size.width; 834 | } 835 | 836 | iconRect.origin.x = (placeholderSize.width - iconRect.size.width)/2; 837 | iconRect.origin.y = (placeholderSize.height - iconRect.size.height)/2; 838 | 839 | NSImage *icon = [[NSImage alloc] initWithSize:iconRect.size]; 840 | [icon lockFocus]; 841 | NSColor *iconTopColor = [NSColor colorWithCalibratedRed:0.380 green:0.400 blue:0.427 alpha:1.0]; 842 | NSColor *iconBottomColor = [NSColor colorWithCalibratedRed:0.224 green:0.255 blue:0.302 alpha:1.0]; 843 | NSGradient *iconGradient = [[NSGradient alloc] initWithStartingColor:iconTopColor endingColor:iconBottomColor]; 844 | [iconGradient drawInRect:NSMakeRect(0, 0, iconRect.size.width, iconRect.size.width) angle:-90.0]; 845 | [iconGradient release]; 846 | [self.placeholderIcon drawInRect:NSMakeRect(0, 0, iconRect.size.width, iconRect.size.height) fromRect:NSZeroRect operation:NSCompositeDestinationIn fraction:1.0]; 847 | [icon unlockFocus]; 848 | 849 | [icon drawInRect:iconRect fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1.0]; 850 | [icon release]; 851 | } 852 | 853 | [placeholder unlockFocus]; 854 | 855 | _placeholderRef = [placeholder imageRefCopy]; 856 | 857 | // Update the placeholder for all necessary items 858 | for (CALayer *layer in [_scrollLayer sublayers]) { 859 | if (![[layer valueForKey:@"hasImage"] boolValue]) { 860 | CALayer *imageLayer = [[self.layer sublayers] objectAtIndex:0]; 861 | CALayer *reflectionLayer = [[imageLayer sublayers] objectAtIndex:0]; 862 | imageLayer.contents = (id)_placeholderRef; 863 | reflectionLayer.contents = (id)_placeholderRef; 864 | } 865 | } 866 | 867 | [placeholder release]; 868 | } 869 | 870 | - (void)_setSelectionIndex:(NSInteger)index 871 | { 872 | if (index < 0) { 873 | index = 0; 874 | } else if (index >= [self.content count]) { 875 | index = [self.content count] - 1; 876 | } 877 | 878 | if ([self infoForBinding:@"selectionIndex"]) { 879 | id container = [[self infoForBinding:@"selectionIndex"] objectForKey:NSObservedObjectKey]; 880 | NSString *keyPath = [[self infoForBinding:@"selectionIndex"] objectForKey:NSObservedKeyPathKey]; 881 | [container setValue:[NSNumber numberWithInteger:index] forKey:keyPath]; 882 | return; 883 | } 884 | 885 | self.selectionIndex = index; 886 | } 887 | 888 | #pragma mark - 889 | #pragma mark Protocol Methods 890 | 891 | #pragma mark CALayoutManager 892 | 893 | - (void)layoutSublayersOfLayer:(CALayer *)layer 894 | { 895 | float margin = floor(MBCoverFlowViewHorizontalMargin + ([layer bounds].size.width - [self itemSize].width * [[layer sublayers] count] - MBCoverFlowViewCellSpacing * ([[layer sublayers] count]-1)) * 0.5); 896 | 897 | for (CALayer *sublayer in [layer sublayers]) { 898 | CALayer *imageLayer = [[sublayer sublayers] objectAtIndex:0]; 899 | CALayer *reflectionLayer = [[imageLayer sublayers] objectAtIndex:0]; 900 | 901 | NSUInteger index = [[sublayer valueForKey:@"index"] integerValue]; 902 | CGRect frame; 903 | frame.size = NSSizeToCGSize([self itemSize]); 904 | frame.origin.x = margin + index * ([self itemSize].width + MBCoverFlowViewCellSpacing); 905 | frame.origin.y = frame.size.height; 906 | 907 | CGRect imageFrame = frame; 908 | imageFrame.origin = CGPointZero; 909 | 910 | CGRect reflectionFrame = imageFrame; 911 | reflectionFrame.origin.y = -frame.size.height; 912 | 913 | CGRect gradientFrame = reflectionFrame; 914 | gradientFrame.origin.y = 0; 915 | 916 | // Create the perspective effect 917 | if (index < self.selectionIndex) { 918 | // Left 919 | frame.origin.x += [self itemSize].width * MBCoverFlowViewPerspectiveSideSpacingFactor * (float)(self.selectionIndex - index - MBCoverFlowViewPerspectiveRowScaleFactor); 920 | imageLayer.transform = _leftTransform; 921 | imageLayer.zPosition = MBCoverFlowViewPerspectiveSidePosition; 922 | sublayer.zPosition = MBCoverFlowViewPerspectiveSidePosition - 0.1 * (self.selectionIndex - index); 923 | } else if (index > self.selectionIndex) { 924 | // Right 925 | frame.origin.x -= [self itemSize].width * MBCoverFlowViewPerspectiveSideSpacingFactor * (float)(index - self.selectionIndex - MBCoverFlowViewPerspectiveRowScaleFactor); 926 | imageLayer.transform = _rightTransform; 927 | imageLayer.zPosition = MBCoverFlowViewPerspectiveSidePosition; 928 | sublayer.zPosition = MBCoverFlowViewPerspectiveSidePosition - 0.1 * (index - self.selectionIndex); 929 | } else { 930 | // Center 931 | imageLayer.transform = CATransform3DIdentity; 932 | imageLayer.zPosition = MBCoverFlowViewPerspectiveCenterPosition; 933 | sublayer.zPosition = MBCoverFlowViewPerspectiveSidePosition; 934 | } 935 | 936 | [sublayer setFrame:frame]; 937 | [imageLayer setFrame:imageFrame]; 938 | [reflectionLayer setFrame:reflectionFrame]; 939 | [reflectionLayer setBounds:CGRectMake(0, 0, [reflectionLayer bounds].size.width, [reflectionLayer bounds].size.height)]; 940 | } 941 | } 942 | 943 | #pragma mark NSKeyValueObserving 944 | 945 | + (NSSet *)keyPathsForValuesAffectingSelectedObject 946 | { 947 | return [NSSet setWithObjects:@"selectionIndex", nil]; 948 | } 949 | 950 | - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context 951 | { 952 | if (context == &MBCoverFlowViewImagePathContext) { 953 | [self _refreshLayer:[self _layerForObject:object]]; 954 | } else { 955 | [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; 956 | } 957 | } 958 | 959 | @end 960 | -------------------------------------------------------------------------------- /MBCoverFlowView.xcodeproj/TemplateIcon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattball/MBCoverFlowView/083842e25220bbada56f3bdf7295d05adeb63fcf/MBCoverFlowView.xcodeproj/TemplateIcon.icns -------------------------------------------------------------------------------- /MBCoverFlowView.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 45; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1DDD58160DA1D0A300B32029 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1DDD58140DA1D0A300B32029 /* MainMenu.xib */; }; 11 | 8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */; }; 12 | 8D11072D0486CEB800E47090 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 29B97316FDCFA39411CA2CEA /* main.m */; settings = {ATTRIBUTES = (); }; }; 13 | 8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */; }; 14 | C91E73FC0F79F75C00FD319E /* MBCoverFlowScroller.m in Sources */ = {isa = PBXBuildFile; fileRef = C91E73FB0F79F75C00FD319E /* MBCoverFlowScroller.m */; }; 15 | C9D1735A0F6B38CD0097827F /* MBCoverFlowView.m in Sources */ = {isa = PBXBuildFile; fileRef = C9D173590F6B38CD0097827F /* MBCoverFlowView.m */; }; 16 | C9D1737A0F6B3ADC0097827F /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C9D173790F6B3ADC0097827F /* QuartzCore.framework */; }; 17 | C9D178230F6F97D60097827F /* MBCoverFlowViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = C9D178220F6F97D60097827F /* MBCoverFlowViewController.m */; }; 18 | C9E9ACB60F78B188004DDC0A /* NSImage+MBCoverFlowAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = C9E9ACB50F78B188004DDC0A /* NSImage+MBCoverFlowAdditions.m */; }; 19 | /* End PBXBuildFile section */ 20 | 21 | /* Begin PBXFileReference section */ 22 | 089C165DFE840E0CC02AAC07 /* English */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = English; path = English.lproj/InfoPlist.strings; sourceTree = ""; }; 23 | 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = /System/Library/Frameworks/Cocoa.framework; sourceTree = ""; }; 24 | 13E42FB307B3F0F600E4EEF1 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = /System/Library/Frameworks/CoreData.framework; sourceTree = ""; }; 25 | 1DDD58150DA1D0A300B32029 /* English */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = English; path = English.lproj/MainMenu.xib; sourceTree = ""; }; 26 | 29B97316FDCFA39411CA2CEA /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 27 | 29B97324FDCFA39411CA2CEA /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = /System/Library/Frameworks/AppKit.framework; sourceTree = ""; }; 28 | 29B97325FDCFA39411CA2CEA /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = ""; }; 29 | 32CA4F630368D1EE00C91783 /* MBCoverFlowView_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MBCoverFlowView_Prefix.pch; sourceTree = ""; }; 30 | 8D1107310486CEB800E47090 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 31 | 8D1107320486CEB800E47090 /* MBCoverFlowView.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MBCoverFlowView.app; sourceTree = BUILT_PRODUCTS_DIR; }; 32 | C91E73FA0F79F75C00FD319E /* MBCoverFlowScroller.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MBCoverFlowScroller.h; sourceTree = ""; }; 33 | C91E73FB0F79F75C00FD319E /* MBCoverFlowScroller.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MBCoverFlowScroller.m; sourceTree = ""; }; 34 | C9D173580F6B38CD0097827F /* MBCoverFlowView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MBCoverFlowView.h; sourceTree = ""; }; 35 | C9D173590F6B38CD0097827F /* MBCoverFlowView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MBCoverFlowView.m; sourceTree = ""; }; 36 | C9D173790F6B3ADC0097827F /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = /System/Library/Frameworks/QuartzCore.framework; sourceTree = ""; }; 37 | C9D178210F6F97D60097827F /* MBCoverFlowViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MBCoverFlowViewController.h; sourceTree = ""; }; 38 | C9D178220F6F97D60097827F /* MBCoverFlowViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MBCoverFlowViewController.m; sourceTree = ""; }; 39 | C9E9ACB40F78B188004DDC0A /* NSImage+MBCoverFlowAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSImage+MBCoverFlowAdditions.h"; sourceTree = ""; }; 40 | C9E9ACB50F78B188004DDC0A /* NSImage+MBCoverFlowAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSImage+MBCoverFlowAdditions.m"; sourceTree = ""; }; 41 | /* End PBXFileReference section */ 42 | 43 | /* Begin PBXFrameworksBuildPhase section */ 44 | 8D11072E0486CEB800E47090 /* Frameworks */ = { 45 | isa = PBXFrameworksBuildPhase; 46 | buildActionMask = 2147483647; 47 | files = ( 48 | 8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */, 49 | C9D1737A0F6B3ADC0097827F /* QuartzCore.framework in Frameworks */, 50 | ); 51 | runOnlyForDeploymentPostprocessing = 0; 52 | }; 53 | /* End PBXFrameworksBuildPhase section */ 54 | 55 | /* Begin PBXGroup section */ 56 | 080E96DDFE201D6D7F000001 /* Classes */ = { 57 | isa = PBXGroup; 58 | children = ( 59 | C9D178210F6F97D60097827F /* MBCoverFlowViewController.h */, 60 | C9D178220F6F97D60097827F /* MBCoverFlowViewController.m */, 61 | C9D173580F6B38CD0097827F /* MBCoverFlowView.h */, 62 | C9D173590F6B38CD0097827F /* MBCoverFlowView.m */, 63 | C91E73FA0F79F75C00FD319E /* MBCoverFlowScroller.h */, 64 | C91E73FB0F79F75C00FD319E /* MBCoverFlowScroller.m */, 65 | C9E9ACB40F78B188004DDC0A /* NSImage+MBCoverFlowAdditions.h */, 66 | C9E9ACB50F78B188004DDC0A /* NSImage+MBCoverFlowAdditions.m */, 67 | ); 68 | name = Classes; 69 | sourceTree = ""; 70 | }; 71 | 1058C7A0FEA54F0111CA2CBB /* Linked Frameworks */ = { 72 | isa = PBXGroup; 73 | children = ( 74 | C9D173790F6B3ADC0097827F /* QuartzCore.framework */, 75 | 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */, 76 | ); 77 | name = "Linked Frameworks"; 78 | sourceTree = ""; 79 | }; 80 | 1058C7A2FEA54F0111CA2CBB /* Other Frameworks */ = { 81 | isa = PBXGroup; 82 | children = ( 83 | 29B97324FDCFA39411CA2CEA /* AppKit.framework */, 84 | 13E42FB307B3F0F600E4EEF1 /* CoreData.framework */, 85 | 29B97325FDCFA39411CA2CEA /* Foundation.framework */, 86 | ); 87 | name = "Other Frameworks"; 88 | sourceTree = ""; 89 | }; 90 | 19C28FACFE9D520D11CA2CBB /* Products */ = { 91 | isa = PBXGroup; 92 | children = ( 93 | 8D1107320486CEB800E47090 /* MBCoverFlowView.app */, 94 | ); 95 | name = Products; 96 | sourceTree = ""; 97 | }; 98 | 29B97314FDCFA39411CA2CEA /* MBCoverFlowView */ = { 99 | isa = PBXGroup; 100 | children = ( 101 | 080E96DDFE201D6D7F000001 /* Classes */, 102 | 29B97315FDCFA39411CA2CEA /* Other Sources */, 103 | 29B97317FDCFA39411CA2CEA /* Resources */, 104 | 29B97323FDCFA39411CA2CEA /* Frameworks */, 105 | 19C28FACFE9D520D11CA2CBB /* Products */, 106 | ); 107 | name = MBCoverFlowView; 108 | sourceTree = ""; 109 | }; 110 | 29B97315FDCFA39411CA2CEA /* Other Sources */ = { 111 | isa = PBXGroup; 112 | children = ( 113 | 32CA4F630368D1EE00C91783 /* MBCoverFlowView_Prefix.pch */, 114 | 29B97316FDCFA39411CA2CEA /* main.m */, 115 | ); 116 | name = "Other Sources"; 117 | sourceTree = ""; 118 | }; 119 | 29B97317FDCFA39411CA2CEA /* Resources */ = { 120 | isa = PBXGroup; 121 | children = ( 122 | 8D1107310486CEB800E47090 /* Info.plist */, 123 | 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */, 124 | 1DDD58140DA1D0A300B32029 /* MainMenu.xib */, 125 | ); 126 | name = Resources; 127 | sourceTree = ""; 128 | }; 129 | 29B97323FDCFA39411CA2CEA /* Frameworks */ = { 130 | isa = PBXGroup; 131 | children = ( 132 | 1058C7A0FEA54F0111CA2CBB /* Linked Frameworks */, 133 | 1058C7A2FEA54F0111CA2CBB /* Other Frameworks */, 134 | ); 135 | name = Frameworks; 136 | sourceTree = ""; 137 | }; 138 | /* End PBXGroup section */ 139 | 140 | /* Begin PBXNativeTarget section */ 141 | 8D1107260486CEB800E47090 /* MBCoverFlowView */ = { 142 | isa = PBXNativeTarget; 143 | buildConfigurationList = C01FCF4A08A954540054247B /* Build configuration list for PBXNativeTarget "MBCoverFlowView" */; 144 | buildPhases = ( 145 | 8D1107290486CEB800E47090 /* Resources */, 146 | 8D11072C0486CEB800E47090 /* Sources */, 147 | 8D11072E0486CEB800E47090 /* Frameworks */, 148 | ); 149 | buildRules = ( 150 | ); 151 | dependencies = ( 152 | ); 153 | name = MBCoverFlowView; 154 | productInstallPath = "$(HOME)/Applications"; 155 | productName = MBCoverFlowView; 156 | productReference = 8D1107320486CEB800E47090 /* MBCoverFlowView.app */; 157 | productType = "com.apple.product-type.application"; 158 | }; 159 | /* End PBXNativeTarget section */ 160 | 161 | /* Begin PBXProject section */ 162 | 29B97313FDCFA39411CA2CEA /* Project object */ = { 163 | isa = PBXProject; 164 | buildConfigurationList = C01FCF4E08A954540054247B /* Build configuration list for PBXProject "MBCoverFlowView" */; 165 | compatibilityVersion = "Xcode 3.1"; 166 | hasScannedForEncodings = 1; 167 | mainGroup = 29B97314FDCFA39411CA2CEA /* MBCoverFlowView */; 168 | projectDirPath = ""; 169 | projectRoot = ""; 170 | targets = ( 171 | 8D1107260486CEB800E47090 /* MBCoverFlowView */, 172 | ); 173 | }; 174 | /* End PBXProject section */ 175 | 176 | /* Begin PBXResourcesBuildPhase section */ 177 | 8D1107290486CEB800E47090 /* Resources */ = { 178 | isa = PBXResourcesBuildPhase; 179 | buildActionMask = 2147483647; 180 | files = ( 181 | 8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */, 182 | 1DDD58160DA1D0A300B32029 /* MainMenu.xib in Resources */, 183 | ); 184 | runOnlyForDeploymentPostprocessing = 0; 185 | }; 186 | /* End PBXResourcesBuildPhase section */ 187 | 188 | /* Begin PBXSourcesBuildPhase section */ 189 | 8D11072C0486CEB800E47090 /* Sources */ = { 190 | isa = PBXSourcesBuildPhase; 191 | buildActionMask = 2147483647; 192 | files = ( 193 | 8D11072D0486CEB800E47090 /* main.m in Sources */, 194 | C9D1735A0F6B38CD0097827F /* MBCoverFlowView.m in Sources */, 195 | C9D178230F6F97D60097827F /* MBCoverFlowViewController.m in Sources */, 196 | C9E9ACB60F78B188004DDC0A /* NSImage+MBCoverFlowAdditions.m in Sources */, 197 | C91E73FC0F79F75C00FD319E /* MBCoverFlowScroller.m in Sources */, 198 | ); 199 | runOnlyForDeploymentPostprocessing = 0; 200 | }; 201 | /* End PBXSourcesBuildPhase section */ 202 | 203 | /* Begin PBXVariantGroup section */ 204 | 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */ = { 205 | isa = PBXVariantGroup; 206 | children = ( 207 | 089C165DFE840E0CC02AAC07 /* English */, 208 | ); 209 | name = InfoPlist.strings; 210 | sourceTree = ""; 211 | }; 212 | 1DDD58140DA1D0A300B32029 /* MainMenu.xib */ = { 213 | isa = PBXVariantGroup; 214 | children = ( 215 | 1DDD58150DA1D0A300B32029 /* English */, 216 | ); 217 | name = MainMenu.xib; 218 | sourceTree = ""; 219 | }; 220 | /* End PBXVariantGroup section */ 221 | 222 | /* Begin XCBuildConfiguration section */ 223 | C01FCF4B08A954540054247B /* Debug */ = { 224 | isa = XCBuildConfiguration; 225 | buildSettings = { 226 | ALWAYS_SEARCH_USER_PATHS = NO; 227 | COPY_PHASE_STRIP = NO; 228 | GCC_DYNAMIC_NO_PIC = NO; 229 | GCC_ENABLE_FIX_AND_CONTINUE = YES; 230 | GCC_MODEL_TUNING = G5; 231 | GCC_OPTIMIZATION_LEVEL = 0; 232 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 233 | GCC_PREFIX_HEADER = MBCoverFlowView_Prefix.pch; 234 | INFOPLIST_FILE = Info.plist; 235 | INSTALL_PATH = "$(HOME)/Applications"; 236 | PRODUCT_NAME = MBCoverFlowView; 237 | }; 238 | name = Debug; 239 | }; 240 | C01FCF4C08A954540054247B /* Release */ = { 241 | isa = XCBuildConfiguration; 242 | buildSettings = { 243 | ALWAYS_SEARCH_USER_PATHS = NO; 244 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 245 | GCC_MODEL_TUNING = G5; 246 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 247 | GCC_PREFIX_HEADER = MBCoverFlowView_Prefix.pch; 248 | INFOPLIST_FILE = Info.plist; 249 | INSTALL_PATH = "$(HOME)/Applications"; 250 | PRODUCT_NAME = MBCoverFlowView; 251 | }; 252 | name = Release; 253 | }; 254 | C01FCF4F08A954540054247B /* Debug */ = { 255 | isa = XCBuildConfiguration; 256 | buildSettings = { 257 | ARCHS = "$(ARCHS_STANDARD_32_BIT)"; 258 | GCC_C_LANGUAGE_STANDARD = c99; 259 | GCC_OPTIMIZATION_LEVEL = 0; 260 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 261 | GCC_WARN_UNUSED_VARIABLE = YES; 262 | ONLY_ACTIVE_ARCH = YES; 263 | PREBINDING = NO; 264 | SDKROOT = macosx10.5; 265 | }; 266 | name = Debug; 267 | }; 268 | C01FCF5008A954540054247B /* Release */ = { 269 | isa = XCBuildConfiguration; 270 | buildSettings = { 271 | ARCHS = "$(ARCHS_STANDARD_32_BIT)"; 272 | GCC_C_LANGUAGE_STANDARD = c99; 273 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 274 | GCC_WARN_UNUSED_VARIABLE = YES; 275 | PREBINDING = NO; 276 | SDKROOT = macosx10.5; 277 | }; 278 | name = Release; 279 | }; 280 | /* End XCBuildConfiguration section */ 281 | 282 | /* Begin XCConfigurationList section */ 283 | C01FCF4A08A954540054247B /* Build configuration list for PBXNativeTarget "MBCoverFlowView" */ = { 284 | isa = XCConfigurationList; 285 | buildConfigurations = ( 286 | C01FCF4B08A954540054247B /* Debug */, 287 | C01FCF4C08A954540054247B /* Release */, 288 | ); 289 | defaultConfigurationIsVisible = 0; 290 | defaultConfigurationName = Release; 291 | }; 292 | C01FCF4E08A954540054247B /* Build configuration list for PBXProject "MBCoverFlowView" */ = { 293 | isa = XCConfigurationList; 294 | buildConfigurations = ( 295 | C01FCF4F08A954540054247B /* Debug */, 296 | C01FCF5008A954540054247B /* Release */, 297 | ); 298 | defaultConfigurationIsVisible = 0; 299 | defaultConfigurationName = Release; 300 | }; 301 | /* End XCConfigurationList section */ 302 | }; 303 | rootObject = 29B97313FDCFA39411CA2CEA /* Project object */; 304 | } 305 | -------------------------------------------------------------------------------- /MBCoverFlowViewController.h: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | The MIT License 4 | 5 | Copyright (c) 2009 Matthew Ball 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | 25 | */ 26 | 27 | #import 28 | 29 | 30 | @interface MBCoverFlowViewController : NSViewController { 31 | 32 | } 33 | 34 | @end 35 | -------------------------------------------------------------------------------- /MBCoverFlowViewController.m: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | The MIT License 4 | 5 | Copyright (c) 2009 Matthew Ball 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | 25 | */ 26 | 27 | #import "MBCoverFlowViewController.h" 28 | 29 | #import "MBCoverFlowView.h" 30 | 31 | @implementation MBCoverFlowViewController 32 | 33 | - (void)awakeFromNib 34 | { 35 | NSViewController *labelViewController = [[NSViewController alloc] initWithNibName:nil bundle:nil]; 36 | NSTextField *label = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 10, 10)]; 37 | [label setBordered:NO]; 38 | [label setBezeled:NO]; 39 | [label setEditable:NO]; 40 | [label setSelectable:NO]; 41 | [label setDrawsBackground:NO]; 42 | [label setTextColor:[NSColor whiteColor]]; 43 | [label setFont:[NSFont boldSystemFontOfSize:12.0]]; 44 | [label setAutoresizingMask:NSViewWidthSizable]; 45 | [label setAlignment:NSCenterTextAlignment]; 46 | [label sizeToFit]; 47 | NSRect labelFrame = [label frame]; 48 | labelFrame.size.width = 400; 49 | [label setFrame:labelFrame]; 50 | [labelViewController setView:label]; 51 | [label bind:@"value" toObject:labelViewController withKeyPath:@"representedObject.name" options:nil]; 52 | [label release]; 53 | [(MBCoverFlowView *)self.view setAccessoryController:labelViewController]; 54 | [labelViewController release]; 55 | 56 | [(MBCoverFlowView *)self.view setImageKeyPath:@"image"]; 57 | [(MBCoverFlowView *)self.view setShowsScrollbar:YES]; 58 | 59 | [NSThread detachNewThreadSelector:@selector(loadImages) toTarget:self withObject:nil]; 60 | } 61 | 62 | - (void)loadImages 63 | { 64 | NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; 65 | NSMutableArray *images = [NSMutableArray array]; 66 | 67 | NSString *file; 68 | NSDirectoryEnumerator *dirEnum = [[NSFileManager defaultManager] enumeratorAtPath:@"/Library/Desktop Pictures/Nature"]; 69 | 70 | int count = 0; 71 | while ((file = [dirEnum nextObject])) 72 | { 73 | NSImage *image = [[NSImage alloc] initWithContentsOfFile:[@"/Library/Desktop Pictures/Nature" stringByAppendingPathComponent:file]]; 74 | if (image != nil) { 75 | // Scale down the image -- CoreAnimation doesn't like huge images 76 | [image setSize:NSMakeSize([image size].width/2, [image size].height/2)]; 77 | NSDictionary *imageInfo = [NSDictionary dictionaryWithObjectsAndKeys:image, @"image", file, @"name", nil]; 78 | [images addObject:imageInfo]; 79 | } 80 | [image release]; 81 | 82 | [(MBCoverFlowView *)self.view performSelectorOnMainThread:@selector(setContent:) withObject:images waitUntilDone:NO]; 83 | 84 | count++; 85 | } 86 | 87 | [pool release]; 88 | } 89 | 90 | @end 91 | -------------------------------------------------------------------------------- /MBCoverFlowView_Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header for all source files of the 'MBCoverFlowView' target in the 'MBCoverFlowView' project 3 | // 4 | 5 | #ifdef __OBJC__ 6 | #import 7 | #endif 8 | -------------------------------------------------------------------------------- /NSImage+MBCoverFlowAdditions.h: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | The MIT License 4 | 5 | Copyright (c) 2009 Matthew Ball 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | 25 | */ 26 | 27 | #import 28 | 29 | /** 30 | * @category NSImage(MBCoverFlowAdditions) 31 | * 32 | * @brief Additions to NSImage which are used by MBCoverFlowView. 33 | */ 34 | @interface NSImage (MBCoverFlowAdditions) 35 | 36 | /** 37 | * @brief Returns a CGImageRef for the image. 38 | * 39 | * @return A CGImageRef representation for the image. 40 | */ 41 | - (CGImageRef)imageRefCopy; 42 | 43 | @end 44 | -------------------------------------------------------------------------------- /NSImage+MBCoverFlowAdditions.m: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | The MIT License 4 | 5 | Copyright (c) 2009 Matthew Ball 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | 25 | */ 26 | 27 | #import "NSImage+MBCoverFlowAdditions.h" 28 | 29 | 30 | @implementation NSImage (MBCoverFlowAdditions) 31 | 32 | - (CGImageRef)imageRefCopy 33 | { 34 | CGContextRef context = CGBitmapContextCreate(NULL/*data - pass NULL to let CG allocate the memory*/, 35 | [self size].width, 36 | [self size].height, 37 | 8, 38 | 0, 39 | [[NSColorSpace genericRGBColorSpace] CGColorSpace], 40 | kCGBitmapByteOrder32Host|kCGImageAlphaPremultipliedFirst); 41 | 42 | [NSGraphicsContext saveGraphicsState]; 43 | [NSGraphicsContext setCurrentContext:[NSGraphicsContext graphicsContextWithGraphicsPort:context flipped:NO]]; 44 | [self drawInRect:NSMakeRect(0,0, [self size].width, [self size].height) fromRect:NSZeroRect operation:NSCompositeCopy fraction:1.0]; 45 | [NSGraphicsContext restoreGraphicsState]; 46 | 47 | CGImageRef cgImage = CGBitmapContextCreateImage(context); 48 | CGContextRelease(context); 49 | 50 | return cgImage; 51 | } 52 | 53 | @end 54 | -------------------------------------------------------------------------------- /README.mdown: -------------------------------------------------------------------------------- 1 | What Is It? 2 | =========== 3 | 4 | MBCoverFlowView is an open-source implementation of the Cover Flow interface found in iTunes, Finder, etc. 5 | 6 | ![MBCoverFlowView screenshot](http://farm4.static.flickr.com/3349/3486406907_30490b7970_o.png) 7 | 8 | How Do I Use It? 9 | ================ 10 | 11 | To use MBCoverFlowView in your app, the minimum requirement is that you set both the ``imageKeyPath`` and ``content`` properties of the view. The ``imageKeyPath`` property should be set to the key path which will access the image to display for each item in the ``content`` array. 12 | 13 | Bindings Are Cool! Can I Use Them? 14 | ================================== 15 | 16 | Of course! Currently, MBCoverFlowView has bindings for the ``@"content"`` and ``@"selectionIndex"`` keys. -------------------------------------------------------------------------------- /main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // MBCoverFlowView 4 | // 5 | // Created by Matt Ball on 3/13/09. 6 | // Copyright Daybreak Apps 2009. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | int main(int argc, char *argv[]) 12 | { 13 | return NSApplicationMain(argc, (const char **) argv); 14 | } 15 | --------------------------------------------------------------------------------