├── .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 | 
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 |
--------------------------------------------------------------------------------