//
// PSCoverFlow.m
// CoverFlow
//
// Created by Adam Czubernat on 23.12.2013.
// Copyright (c) 2013 Perforce Software, Inc. All rights reserved.
//
#import "PSCoverFlow.h"
#import <QuartzCore/QuartzCore.h>
@interface PSCoverFlow () {
CAScrollLayer *scrollLayer;
CATransform3D perspective;
CATransform3D leftTransform;
CATransform3D rightTransform;
CGSize size;
NSInteger selectedIndex;
}
- (void)selectIndex:(NSInteger)index;
- (void)postSelectionChange;
- (void)layoutLayers;
- (void)layoutShadowLayer:(CALayer *)layer;
@end
@implementation PSCoverFlow
@synthesize delegate;
- (id)initWithFrame:(NSRect)frameRect {
self = [super initWithFrame:frameRect];
if (self) {
self.layer = [CALayer layer];
self.wantsLayer = YES;
scrollLayer = [CAScrollLayer layer];
scrollLayer.frame = (CGRect) { CGPointZero, frameRect.size };
scrollLayer.scrollMode = kCAScrollHorizontally;
scrollLayer.autoresizingMask = kCALayerWidthSizable | kCALayerHeightSizable;
scrollLayer.layoutManager = self;
[self.layer addSublayer:scrollLayer];
perspective = CATransform3DIdentity;
perspective.m34 = - 1.0f / 800.0f;
// Gradient mask
CAGradientLayer *gradient = [CAGradientLayer layer];
gradient.frame = (CGRect) { CGPointZero, frameRect.size };
gradient.autoresizingMask = kCALayerWidthSizable | kCALayerHeightSizable;
id clearColor = (__bridge id)CGColorGetConstantColor(kCGColorClear);
id opaqueColor = (__bridge id)CGColorGetConstantColor(kCGColorBlack);
gradient.colors = @[ clearColor, opaqueColor, opaqueColor, clearColor ];
gradient.startPoint = CGPointMake(-0.1f, 0);
gradient.endPoint = CGPointMake(1.1f, 0);
self.layer.mask = gradient;
}
return self;
}
- (void)setFrame:(NSRect)frameRect {
[CATransaction begin];
[CATransaction setDisableActions:YES];
[super setFrame:frameRect];
[self layoutLayers];
[CATransaction commit];
}
- (void)viewDidMoveToWindow {
if (self.window)
[self reload];
}
#pragma mark - CALayoutManager
- (void)layoutSublayersOfLayer:(CALayer *)layer {
CGFloat spacing = size.width * 0.8f;
[scrollLayer scrollToPoint:(CGPoint) {
size.width * selectedIndex - (self.frame.size.width - size.width) / 2.0f, 0.0f
}];
[layer.sublayers enumerateObjectsUsingBlock:^(CALayer *sublayer, NSUInteger idx, BOOL *stop) {
CATransform3D transform = CATransform3DIdentity;
if (idx < selectedIndex) {
CGFloat x = spacing * (selectedIndex - idx);
transform = CATransform3DTranslate(transform, x, 0, 0);
transform = CATransform3DConcat(leftTransform, transform);
} else if (idx > selectedIndex) {
CGFloat x = -spacing * (idx - selectedIndex);
transform = CATransform3DTranslate(transform, x, 0, 0);
transform = CATransform3DConcat(rightTransform, transform);
}
sublayer.transform = transform;
}];
}
#pragma mark - Private
- (void)postSelectionChange {
if (!self.superview)
return;
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(postSelectionChange) object:nil];
if ([delegate respondsToSelector:@selector(coverFlow:didSelectIndex:)])
[delegate coverFlow:self didSelectIndex:selectedIndex];
}
- (void)selectIndex:(NSInteger)index {
if (index >= scrollLayer.sublayers.count || index < 0 || index == selectedIndex)
return;
selectedIndex = index;
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(postSelectionChange) object:nil];
[self performSelector:@selector(postSelectionChange) withObject:nil afterDelay:0.2f];
[CATransaction begin];
[CATransaction setAnimationTimingFunction:
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]];
[scrollLayer setNeedsLayout];
[CATransaction commit];
}
- (void)layoutLayers {
CGFloat angle = 60.0f * M_PI/180;
CGFloat recess = -200.0f;
CGFloat sizeScale = 0.85f;
size = self.frame.size;
size.width = size.height *= sizeScale;
CGFloat margin = size.width * 0.6f;
CGFloat frameMargin = self.frame.size.height * (1.0f - sizeScale) / 2.0f;
leftTransform = CATransform3DTranslate(perspective, -margin, 0, recess);
leftTransform = CATransform3DRotate(leftTransform, angle, 0, 1, 0);
rightTransform = CATransform3DTranslate(perspective, margin, 0, recess);
rightTransform = CATransform3DRotate(rightTransform, -angle, 0, 1, 0);
[scrollLayer.sublayers enumerateObjectsUsingBlock:^(CALayer *layer, NSUInteger idx, BOOL *stop) {
// Resize layers
layer.frame = (CGRect) { idx * size.width, frameMargin, size };
[self layoutShadowLayer:layer];
}];
}
- (void)layoutShadowLayer:(CALayer *)layer {
NSImage *image = layer.contents;
if (!image) {
layer.shadowOpacity = 0;
return;
}
// Shadow aspect fit
CGRect rect = { .size = [layer.contents size] };
CGFloat ratio = fminf(size.width / rect.size.width, size.height / rect.size.height);
rect.size = (CGSize) { rect.size.width * ratio, rect.size.height * ratio };
rect.origin = (CGPoint) { (size.width - rect.size.width) / 2.0f, (size.height - rect.size.height) / 2.0f };
// Resize shadow
CGPathRef shadowPath = CGPathCreateWithRect(rect, NULL);
layer.shadowRadius = size.height * 0.02f;
layer.shadowPath = shadowPath;
layer.shadowOpacity = 1.0f;
CGPathRelease(shadowPath);
}
#pragma mark - Public
- (void)reload {
[scrollLayer.sublayers makeObjectsPerformSelector:@selector(removeFromSuperlayer)];
NSInteger imagesCount = 0;
if ([delegate respondsToSelector:@selector(numberOfImagesInCoverFlow:)])
imagesCount = [delegate numberOfImagesInCoverFlow:self];
for (NSInteger index = 0; index < imagesCount; index++) {
CALayer *layer = [CALayer layer];
layer.shouldRasterize = YES;
layer.contentsGravity = kCAGravityResizeAspect;
CGColorRef color = CGColorCreateGenericGray(0, 0.2f);
layer.borderColor = color;
CGColorRelease(color);
layer.shadowOffset = CGSizeZero;
layer.shadowColor = CGColorGetConstantColor(kCGColorBlack);
[scrollLayer addSublayer:layer];
}
[self reloadImagesInRange:(NSRange) { 0, imagesCount }];
[self selectIndex:0];
[self layoutLayers];
}
- (void)reloadImagesInRange:(NSRange)range {
NSInteger count = MIN(range.location + range.length, scrollLayer.sublayers.count);
for (NSInteger index = range.location; index < count; index++) {
NSImage *image = nil;
if ([delegate respondsToSelector:@selector(coverFlow:imageForIndex:)])
image = [delegate coverFlow:self imageForIndex:index];
// image = image ?: [NSImage imageNamed:NSImageNameQuickLookTemplate];
CALayer *layer = [scrollLayer.sublayers objectAtIndex:index];
layer.contents = image;
CGColorRef color = image ? nil : CGColorCreateGenericGray(0.7, 0.9f);
layer.backgroundColor = color;
CGColorRelease(color);
layer.borderWidth = image ? 0 : 2.0f;
[self layoutShadowLayer:layer];
}
}
- (NSInteger)selectedIndex {
return selectedIndex;
}
- (void)setSelectedIndex:(NSInteger)index {
if (index >= scrollLayer.sublayers.count || index < 0 || index == selectedIndex)
return;
selectedIndex = index;
[CATransaction begin];
[CATransaction setAnimationTimingFunction:
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]];
[scrollLayer setNeedsLayout];
[CATransaction commit];
}
#pragma mark - NSResponder
- (BOOL)acceptsFirstResponder {
return YES;
}
- (void)mouseDown:(NSEvent *)theEvent {
CGPoint point = [self convertPoint:theEvent.locationInWindow fromView:nil];
CALayer *layer = [scrollLayer hitTest:point];
NSInteger index = [scrollLayer.sublayers indexOfObjectIdenticalTo:layer];
if (index != NSNotFound)
[self selectIndex:index];
}
- (void)scrollWheel:(NSEvent *)theEvent {
CGFloat delta = theEvent.deltaX ?: theEvent.deltaY;
if (fabs(delta) > 1.0f)
[self selectIndex:selectedIndex + (delta > 0 ? -1 : 1)];
}
- (void)keyDown:(NSEvent *)event {
unichar keyChar = [[event charactersIgnoringModifiers] characterAtIndex:0];
if (keyChar == NSLeftArrowFunctionKey)
[self moveLeft:nil];
else if (keyChar == NSRightArrowFunctionKey)
[self moveRight:nil];
else if (keyChar == NSUpArrowFunctionKey)
[self moveUp:nil];
else if (keyChar == NSDownArrowFunctionKey)
[self moveDown:nil];
else
[super keyDown:event];
}
- (void)moveUp:(id)sender {
[self moveLeft:sender];
}
- (void)moveDown:(id)sender {
[self moveRight:sender];
}
- (void)moveLeft:(id)sender {
[self selectIndex:selectedIndex - 1];
}
- (void)moveRight:(id)sender {
[self selectIndex:selectedIndex + 1];
}
@end
| # | Change | User | Description | Committed | |
|---|---|---|---|---|---|
| #1 | 15071 | alan_petersen |
Populate -o //guest/perforce_software/piper/... //guest/alan_petersen/piper/.... |
||
| //guest/perforce_software/piper/mac/R1.0/Perforce/Classes/Views/PSCoverFlow.m | |||||
| #1 | 11254 | alan_petersen |
Populate //guest/perforce_software/piper/mac/R1.0/... from //guest/perforce_software/piper/mac/main/.... |
||
| //guest/perforce_software/piper/mac/main/Perforce/Classes/Views/PSCoverFlow.m | |||||
| #1 | 11252 | alan_petersen | Rename/move file(s) | ||
| //guest/perforce_software/piper/mac/Perforce/Classes/Views/PSCoverFlow.m | |||||
| #1 | 10744 | alan_petersen | Rename/move file(s) | ||
| //guest/perforce_software/piper/Perforce/Classes/Views/PSCoverFlow.m | |||||
| #2 | 8985 | Matt Attaway |
Update the copyright on a couple files No functional change. |
||
| #1 | 8919 | Matt Attaway | Initial add of Piper, a lightweight Perforce client for artists and designers. | ||