/* |
File: LTView.m |
Abstract: This custom view uses CALayers to arange and draw slides. Drag and Drop images onto this view to add them as slides. Double-click a slide to edit the masking of the image to the slide. This view tracks both mouse and touch events to modify the slide. Using two fingers on the trackpad will adjust the position and size of the slide under the cursor. |
|
Version: 1.0 |
|
Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple |
Inc. ("Apple") in consideration of your agreement to the following |
terms, and your use, installation, modification or redistribution of |
this Apple software constitutes acceptance of these terms. If you do |
not agree with these terms, please do not use, install, modify or |
redistribute this Apple software. |
|
In consideration of your agreement to abide by the following terms, and |
subject to these terms, Apple grants you a personal, non-exclusive |
license, under Apple's copyrights in this original Apple software (the |
"Apple Software"), to use, reproduce, modify and redistribute the Apple |
Software, with or without modifications, in source and/or binary forms; |
provided that if you redistribute the Apple Software in its entirety and |
without modifications, you must retain this notice and the following |
text and disclaimers in all such redistributions of the Apple Software. |
Neither the name, trademarks, service marks or logos of Apple Inc. may |
be used to endorse or promote products derived from the Apple Software |
without specific prior written permission from Apple. Except as |
expressly stated in this notice, no other rights or licenses, express or |
implied, are granted by Apple herein, including but not limited to any |
patent rights that may be infringed by your derivative works or by other |
works in which the Apple Software may be incorporated. |
|
The Apple Software is provided by Apple on an "AS IS" basis. APPLE |
MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION |
THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS |
FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND |
OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. |
|
IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL |
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, |
MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED |
AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), |
STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE |
POSSIBILITY OF SUCH DAMAGE. |
|
Copyright (C) 2009 Apple Inc. All Rights Reserved. |
|
*/ |
|
#import "InputTrackers.h" |
#import "LTView.h" |
#import "LTMaskLayer.h" |
|
// The _LTOverlayLayer class is used so that our overlay layer with the drag handles is not included in hit testing. Otherwise, hit testing would always return the overlay layer since it is the top layer and fills the entire view. I used an underbar in the classname and implemented it in this file because it is a private helper class of LTView. |
@interface _LTOverlayLayer : CALayer |
@end |
|
@implementation _LTOverlayLayer |
- (BOOL)containsPoint:(CGPoint)p { |
return NO; |
} |
@end |
|
|
#pragma mark Tracking Dictionary Keys |
// These are the keys for properties we store in the InputTracker dictionary. |
static NSString *kLayerKey = @"layer"; |
static NSString *kInitialFrameKey = @"initialFrame"; |
static NSString *kInitialPositionKey = @"initialPosition"; |
static NSString *kResizeIndexKey = @"resizeIndex"; |
|
// This is pointer value that we use as the binding context fot LTView |
NSString *kLTOberserverContext = @"LTView.context"; |
|
// In 64bit, NS and CG points/rects are interchangeable without compiler warnings. But for 32 bit, we have to do a whole bunch of conversions to make the compiler happy. There is no CG equivalent call for NSPointInRect. We use it a fair amount in this sample, so this macro will make reading the code easier later on. |
#define LTPointInRect(p,r) NSPointInRect(NSPointFromCGPoint(p), NSRectFromCGRect(r)) |
|
#pragma mark Resize Rect Helpers |
static void resizeRectsForFrame(CGRect *resizeRects, CGRect frame) { |
if (!resizeRects) return; |
CGFloat width = 5.0; |
|
//top left |
resizeRects[0] = CGRectMake(CGRectGetMinX(frame) - width, CGRectGetMaxY(frame), width, width); |
//top middle |
resizeRects[1] = CGRectMake(CGRectGetMidX(frame) - width/2.0, CGRectGetMaxY(frame), width, width); |
//top right |
resizeRects[2] = CGRectMake(CGRectGetMaxX(frame), CGRectGetMaxY(frame), width, width); |
//right middle |
resizeRects[3] = CGRectMake(CGRectGetMaxX(frame), CGRectGetMidY(frame) - width/2.0, width, width); |
//bottom right |
resizeRects[4] = CGRectMake(CGRectGetMaxX(frame), CGRectGetMinY(frame)- width, width, width); |
//bottom middle |
resizeRects[5] = CGRectMake(CGRectGetMidX(frame) - width/2.0, CGRectGetMinY(frame) - width, width, width); |
//bottom left |
resizeRects[6] = CGRectMake(CGRectGetMinX(frame) - width, CGRectGetMinY(frame) - width, width, width); |
//left middle |
resizeRects[7] = CGRectMake(CGRectGetMinX(frame) - width, CGRectGetMidY(frame) - width/2.0, width, width); |
} |
|
static NSInteger indexOfResizeRectForPoint(CGRect *resizeRects, CGPoint point) { |
if (!resizeRects) return -1; |
|
if (LTPointInRect(point, resizeRects[0])) return 0; |
if (LTPointInRect(point, resizeRects[1])) return 1; |
if (LTPointInRect(point, resizeRects[2])) return 2; |
if (LTPointInRect(point, resizeRects[3])) return 3; |
if (LTPointInRect(point, resizeRects[4])) return 4; |
if (LTPointInRect(point, resizeRects[5])) return 5; |
if (LTPointInRect(point, resizeRects[6])) return 6; |
if (LTPointInRect(point, resizeRects[7])) return 7; |
|
return -1; |
} |
|
@interface LTView () |
- (void)_initTrackers; |
- (void)drawDragHandleFrames:(CGRect *)handleFrames inContext:(CGContextRef)context; |
@end |
|
|
@implementation LTView |
|
+ (void)initialize { |
[self exposeBinding:kLTViewSlides]; |
[self exposeBinding:kLTViewSelectionIndexes]; |
} |
|
- (id)initWithFrame:(NSRect)frame { |
self = [super initWithFrame:frame]; |
if (self) { |
|
// setup the CALayer for the overall full-screen view |
CALayer *backingLayer = [CALayer layer]; |
_overlayLayer = [[_LTOverlayLayer layer] retain]; |
|
[self setLayer:backingLayer]; |
[self setWantsLayer:YES]; |
|
backingLayer.frame = NSRectToCGRect(frame); |
backingLayer.bounds = CGRectMake(0, 0, frame.size.width, frame.size.height); |
backingLayer.backgroundColor = CGColorCreateGenericRGB(1, 1, 1, 1.0); |
backingLayer.opaque = YES; |
|
// The overlay layer is used to draw any drag handles, so that they are always on top of all slides. We must take care to make sure this layer is always the last one. |
_overlayLayer.frame = backingLayer.frame; |
_overlayLayer.opaque = NO; |
_overlayLayer.delegate = self; // We want to be the delegate so we can do the drag handle drawing |
_overlayLayer.backgroundColor = CGColorCreateGenericRGB(0, 0, 0, 0.0); |
_overlayLayer.autoresizingMask = kCALayerWidthSizable | kCALayerHeightSizable; |
[backingLayer addSublayer:_overlayLayer]; |
|
// init ivars |
_slides = [[NSMutableArray array] retain]; |
self.selectionIndexes = [NSIndexSet indexSet]; |
|
// init the input trackers |
[self _initTrackers]; |
|
// register for dragging |
[self registerForDraggedTypes:[NSArray arrayWithObject:(NSString *)kUTTypeFileURL]]; |
|
// we want touch events |
[self setAcceptsTouchEvents:YES]; |
} |
|
return self; |
} |
|
- (void)dealloc { |
[_inputTrackers release]; |
[_selectionIndexes release]; |
[_overlayLayer release]; |
|
// Remove all objects via the KVO methods to make sure we remove our observers. |
[[self mutableArrayValueForKey:kLTViewSlides] removeAllObjects]; |
[_slides release]; |
|
[super dealloc]; |
} |
|
|
// Create the set of tracker objects the LTView needs. See InputTracker.h for more information. |
- (void)_initTrackers { |
_inputTrackers = [NSMutableArray new]; |
|
ClickTracker *clickTracker = [ClickTracker new]; |
clickTracker.action = @selector(clickAction:); |
clickTracker.doubleAction = @selector(doubleClickAction:); |
clickTracker.view = self; |
[_inputTrackers addObject:clickTracker]; |
[clickTracker release]; |
|
DragTracker *dragTracker = [DragTracker new]; |
dragTracker.beginTrackingAction = @selector(beginMouseDrag:); |
dragTracker.view = self; |
[_inputTrackers addObject:dragTracker]; |
[dragTracker release]; |
|
DualTouchTracker *touchTracker = [DualTouchTracker new]; |
touchTracker.beginTrackingAction = @selector(dualTouchesBegan:); |
touchTracker.view = self; |
[_inputTrackers addObject:touchTracker]; |
[touchTracker release]; |
} |
|
|
#pragma mark CALayerDelegate |
|
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)context { |
CGContextSetFillColorWithColor(context, layer.backgroundColor); |
CGContextFillRect(context, layer.bounds); |
|
if ([self.selectionIndexes count]) { |
CGContextSetRGBStrokeColor(context, 0, 0, 0, 1); |
CGContextSetRGBFillColor(context, 1, 1, 1, 1); |
|
for (CALayer *layer in [[self.layer sublayers] objectsAtIndexes:self.selectionIndexes]) { |
CGRect frame = layer.frame; |
CGRect handleFrames[8] = {0.0}; |
|
resizeRectsForFrame(handleFrames, frame); |
[self drawDragHandleFrames:handleFrames inContext:context]; |
} |
} |
|
if (_editingSlide) { |
CGContextSetRGBStrokeColor(context, 0, 0, 0, 1); |
CGContextSetRGBFillColor(context, 1, 1, 1, 1); |
|
CGRect frame = _editingSlide.photoFrame; |
CGRect handleFrames[8] = {0.0}; |
|
resizeRectsForFrame(handleFrames, frame); |
[self drawDragHandleFrames:handleFrames inContext:context]; |
} |
} |
|
- (void)drawDragHandleFrames:(CGRect *)handleFrames inContext:(CGContextRef)context { |
CGContextFillRects(context, handleFrames, 8); |
CGContextStrokeRectWithWidth(context, handleFrames[0], 1.0); |
CGContextStrokeRectWithWidth(context, handleFrames[1], 1.0); |
CGContextStrokeRectWithWidth(context, handleFrames[2], 1.0); |
CGContextStrokeRectWithWidth(context, handleFrames[3], 1.0); |
CGContextStrokeRectWithWidth(context, handleFrames[4], 1.0); |
CGContextStrokeRectWithWidth(context, handleFrames[5], 1.0); |
CGContextStrokeRectWithWidth(context, handleFrames[6], 1.0); |
CGContextStrokeRectWithWidth(context, handleFrames[7], 1.0); |
} |
|
|
#pragma mark NSDraggingDestination |
|
- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender { |
return NSDragOperationGeneric; |
} |
|
- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender { |
NSPasteboard *draggingPasteboard = [sender draggingPasteboard]; |
NSArray *classArray = [NSArray arrayWithObject:[NSURL class]]; |
NSDictionary *options = [NSDictionary dictionaryWithObject:[NSImage imageTypes] forKey:NSPasteboardURLReadingContentsConformToTypesKey]; |
NSArray *items = [draggingPasteboard readObjectsForClasses:classArray options:options]; |
NSPoint slideOrigin = [self convertPointFromBase:[sender draggingLocation]]; |
|
|
for (NSURL *fileURL in items) { |
id newObject = [_newObjectCreator newObject]; |
NSImage *image = [[[NSImage alloc] initWithContentsOfURL:fileURL] autorelease]; |
NSSize maxSize = self.bounds.size; |
maxSize.width /= 2.0; |
maxSize.height /= 2.0; |
NSRect slideFrame = {NSZeroPoint, [image size]}; |
|
// Reduce the size of the slide until it fits on no more than a quarter of the view. |
while(slideFrame.size.width > maxSize.width || slideFrame.size.height > maxSize.height) { |
slideFrame.size.width /= 2.0; |
slideFrame.size.height /= 2.0; |
} |
|
// Start the photo filling the entire slide. |
NSRect photoFrame = slideFrame; |
slideFrame.origin = slideOrigin; |
slideFrame.origin.y -= slideFrame.size.height / 2.0; |
|
[newObject setValue:[NSValue valueWithRect:slideFrame] forKey:kLTViewSlidePropertyFrame]; |
[newObject setValue:[NSValue valueWithRect:photoFrame] forKey:kLTViewSlidePropertyPhotoFrame]; |
[newObject setValue:[NSData dataWithContentsOfURL:fileURL] forKey:kLTViewSlidePropertyPhoto]; |
|
[_newObjectCreator insertObject:newObject atArrangedObjectIndex:[_slides count]]; |
|
// Shift the next image over a bit. |
slideOrigin.x += slideFrame.size.width + 5; |
} |
|
return YES; |
} |
|
|
#pragma mark NSKeyValueBindingCreation |
|
- (void)bind:(NSString *)binding toObject:(id)observable withKeyPath:(NSString *)keyPath options:(NSDictionary *)options { |
if ([binding isEqualToString:kLTViewSlides]) { |
_newObjectCreator = [observable retain]; |
_keyPath = [keyPath retain]; |
[_newObjectCreator addObserver:self forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:kLTOberserverContext]; |
return; |
} |
|
[super bind:binding toObject:observable withKeyPath:keyPath options:options]; |
} |
|
- (void)unbind:(NSString *)binding { |
if ([binding isEqualToString:kLTViewSlides]) { |
[_newObjectCreator removeObserver:self forKeyPath:_keyPath]; |
[_newObjectCreator release]; |
[_keyPath release]; |
} |
|
[super unbind:binding]; |
} |
|
|
#pragma mark NSKeyValueObserving |
|
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { |
if ([keyPath isEqualTo:_keyPath] && context == kLTOberserverContext) { |
switch([[change objectForKey:NSKeyValueChangeKindKey] integerValue]) { |
case NSKeyValueChangeSetting: |
{ |
NSMutableArray *slides = [self mutableArrayValueForKey:kLTViewSlides]; |
[slides removeAllObjects]; |
[slides addObjectsFromArray:[object valueForKey:_keyPath]]; |
[_overlayLayer setNeedsDisplay]; |
} |
break; |
|
default: |
break; |
|
} |
return; |
} |
|
if (context == kLTOberserverContext) { |
LTMaskLayer *layer = nil; |
for (LTMaskLayer *subLayer in [self.layer sublayers]) { |
if (subLayer.source == object) { |
layer = subLayer; |
break; |
} |
} |
|
if ([keyPath isEqualTo:kLTViewSlidePropertyCornerRadius]) { |
layer.cornerRadius = [[object valueForKeyPath:keyPath] floatValue]; |
return; |
} |
|
if ([keyPath isEqualTo:kLTViewSlidePropertyFrameThickness]) { |
layer.borderWidth = [[object valueForKeyPath:keyPath] floatValue]; |
return; |
} |
|
if ([keyPath isEqualTo:kLTViewSlidePropertyFrame]) { |
layer.frame = NSRectToCGRect([[object valueForKeyPath:keyPath] rectValue]); |
[_overlayLayer setNeedsDisplay]; //update drag handles |
return; |
} |
|
if ([keyPath isEqualTo:kLTViewSlidePropertyPhotoFrame]) { |
layer.photoLayer.frame = NSRectToCGRect([[object valueForKeyPath:keyPath] rectValue]); |
[_overlayLayer setNeedsDisplay]; //update drag handles |
return; |
} |
} |
|
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; |
} |
|
|
#pragma mark NSResponder |
|
// Route all events to the input tracker collection. See InputTracker.h. |
|
- (void)mouseDown:(NSEvent *)event { |
[_inputTrackers makeObjectsPerformSelector:_cmd withObject:event]; |
} |
|
- (void)mouseDragged:(NSEvent *)event { |
[_inputTrackers makeObjectsPerformSelector:_cmd withObject:event]; |
} |
|
- (void)mouseUp:(NSEvent *)event { |
[_inputTrackers makeObjectsPerformSelector:_cmd withObject:event]; |
} |
|
- (void)touchesBeganWithEvent:(NSEvent *)event { |
[_inputTrackers makeObjectsPerformSelector:_cmd withObject:event]; |
} |
|
- (void)touchesMovedWithEvent:(NSEvent *)event { |
[_inputTrackers makeObjectsPerformSelector:_cmd withObject:event]; |
} |
|
- (void)touchesEndedWithEvent:(NSEvent *)event { |
[_inputTrackers makeObjectsPerformSelector:_cmd withObject:event]; |
} |
|
- (void)touchesCancelledWithEvent:(NSEvent *)event { |
[_inputTrackers makeObjectsPerformSelector:_cmd withObject:event]; |
} |
|
|
#pragma mark API |
|
static BOOL gOldAnimationIsDisabled = FALSE; |
static double gOldAnimationDuration = 0.25; |
|
@synthesize selectionIndexes = _selectionIndexes; |
|
// These functions set up the Core Animation variables that we want and restore whatever was there |
+ (void)setupCAAnimationStack { |
gOldAnimationIsDisabled = [CATransaction animationDuration]; |
gOldAnimationDuration = [CATransaction disableActions]; |
|
[CATransaction setValue:[NSNumber numberWithBool:FALSE] forKey:kCATransactionDisableActions]; |
[CATransaction setValue:[NSNumber numberWithFloat:0.0] forKey:kCATransactionAnimationDuration]; |
} |
|
+ (void)restoreCAAnimationStack { |
[CATransaction setValue:[NSNumber numberWithBool:gOldAnimationIsDisabled] forKey:kCATransactionDisableActions]; |
[CATransaction setValue:[NSNumber numberWithFloat:gOldAnimationDuration] forKey:kCATransactionAnimationDuration]; |
} |
|
- (NSInteger)countOfSlides { |
return [_slides count]; |
} |
|
- (id)objectInSlidesAtIndex:(NSInteger)index { |
return [_slides objectAtIndex:index]; |
} |
|
- (NSArray *)slidesAtIndexes:(NSIndexSet *)indexes { |
return [_slides objectsAtIndexes:indexes]; |
} |
|
- (void)insertSlides:(NSArray *)array atIndexes:(NSIndexSet *)indexes { |
NSUInteger layerIndex = [indexes firstIndex]; |
|
for (id slide in array) { |
NSImage *image = [[NSImage alloc] initWithData:[slide valueForKey:kLTViewSlidePropertyPhoto]]; |
|
CGRect frame = NSRectToCGRect([[slide valueForKey:kLTViewSlidePropertyFrame] rectValue]); |
LTMaskLayer *slideLayer = [LTMaskLayer layer]; |
slideLayer.photo = image; |
slideLayer.frame = frame; |
slideLayer.bounds = CGRectMake(0, 0, frame.size.width, frame.size.height); |
slideLayer.cornerRadius = [[slide valueForKey:kLTViewSlidePropertyCornerRadius] floatValue]; |
slideLayer.borderWidth = [[slide valueForKey:kLTViewSlidePropertyFrameThickness] floatValue]; |
slideLayer.photoLayer.frame = NSRectToCGRect([[slide valueForKey:kLTViewSlidePropertyPhotoFrame] rectValue]); |
slideLayer.source = slide; |
|
// Finally insert the layer at the same index of the data source. Note: The data source is always one less than the number of sublayers because we have the overlay layer. This means that we always insert slide layers under the overlay layer (just like we want). |
[self.layer insertSublayer:slideLayer atIndex:layerIndex]; |
layerIndex = [indexes indexGreaterThanIndex:layerIndex]; |
|
// Add observers for the properties that LTView displays. Why not have the LTMaskLayer doing the observing? Mainly because, LTView needs to know about frame changes to update the resizeRects. So we do them all in the same place to keeps things simpler for the sample project. |
[slide addObserver:self forKeyPath:kLTViewSlidePropertyFrame options:0 context:kLTOberserverContext]; |
[slide addObserver:self forKeyPath:kLTViewSlidePropertyPhotoFrame options:0 context:kLTOberserverContext]; |
[slide addObserver:self forKeyPath:kLTViewSlidePropertyCornerRadius options:0 context:kLTOberserverContext]; |
[slide addObserver:self forKeyPath:kLTViewSlidePropertyFrameThickness options:0 context:kLTOberserverContext]; |
|
[image release]; |
} |
|
[_slides insertObjects:array atIndexes:indexes]; |
} |
|
- (void)removeSlidesAtIndexes:(NSIndexSet *)indexes { |
// Stop observing all the properties we started observing when we inserted the layer above. |
for (id slide in [_slides objectsAtIndexes:indexes]) { |
[slide removeObserver:self forKeyPath:kLTViewSlidePropertyFrame]; |
[slide removeObserver:self forKeyPath:kLTViewSlidePropertyPhotoFrame]; |
[slide removeObserver:self forKeyPath:kLTViewSlidePropertyCornerRadius]; |
[slide removeObserver:self forKeyPath:kLTViewSlidePropertyFrameThickness]; |
} |
|
// Finally remove all the layers at the same indexes of the data source. Note: The data source is always one less than the number of sublayers because we have the overlay layer. This means that we always remove slide layers under the overlay layer and never the overlay layer itself. |
[_slides removeObjectsAtIndexes:indexes]; |
|
for (CALayer *layer in [[self.layer sublayers] objectsAtIndexes:indexes]) { |
[layer removeFromSuperlayer]; |
} |
} |
|
// We commit the changes to the LTMaskLayer at the end of tracking so that we only hit the data source a minimal amount. Also, this way, Undo will undo the whole tracking action instead of just one small step of it. |
- (void)commitFrameChangeOfLayer:(LTMaskLayer *)layer { |
CGRect photoFrame; |
|
// This methods is called when either the mask layer's frame has changed, or when the photo layer's frame has changed. Note, changing the mask layer's frame implies a photo layer frame change, but not the other way around. |
if (layer.superlayer == self.layer) { |
photoFrame = layer.photoLayer.frame; |
[layer.source setValue:[NSValue valueWithRect:NSRectFromCGRect(layer.frame)] forKey:kLTViewSlidePropertyFrame]; |
} else { |
layer = (LTMaskLayer *)layer.superlayer; |
photoFrame = layer.photoLayer.frame; |
} |
|
[layer.source setValue:[NSValue valueWithRect:NSRectFromCGRect(photoFrame)] forKey:kLTViewSlidePropertyPhotoFrame]; |
|
[_overlayLayer setNeedsDisplay]; |
} |
|
|
#pragma mark Input Tracker Support and Actions |
|
- (void)disableTrackersExcluding:(InputTracker*)excluded { |
for (InputTracker *tracker in _inputTrackers) { |
if (tracker != excluded) tracker.isEnabled = NO; |
} |
} |
|
- (void)enableTrackers { |
for (InputTracker *tracker in _inputTrackers) { |
tracker.isEnabled = YES; |
} |
} |
|
// The user clicked and is not in the process of adjusting the photo masking. Modify the selection accordingly. A different set of tracker actions will manage dragging. |
- (void)clickAction:(ClickTracker*)tracker { |
CGPoint trackerLocation = NSPointToCGPoint([tracker location]); |
CGPoint layerLocation = [self.layer convertPoint:trackerLocation fromLayer:nil]; |
|
// Check for clicks in any existing resize handles first. |
for (CALayer *layer in [[self.layer sublayers] objectsAtIndexes:self.selectionIndexes]) { |
CGRect resizeRects[8]; |
resizeRectsForFrame(resizeRects, layer.frame); |
NSInteger resizeIndex = indexOfResizeRectForPoint(resizeRects, layerLocation); |
|
if (resizeIndex >= 0) return; // in resize handle, don't change selection |
} |
|
// Use layer hit testing to find the targeted layer, if any. |
CALayer *layer = [self.layer hitTest:trackerLocation]; |
layer = (layer == self.layer) ? nil : layer; |
|
if (layer) { |
NSUInteger layerIndex = [[self.layer sublayers] indexOfObject:layer]; |
if (layerIndex != NSNotFound) { |
BOOL isCommandDown = (([tracker modifiers] & NSCommandKeyMask) != 0); |
if (isCommandDown) { |
NSMutableIndexSet *newIndexSet = [self.selectionIndexes mutableCopy]; |
|
if ([newIndexSet containsIndex:layerIndex]) { |
[newIndexSet removeIndex:layerIndex]; |
} else { |
[newIndexSet addIndex:layerIndex]; |
self.selectionIndexes = [[NSIndexSet alloc] initWithIndexSet:newIndexSet]; |
} |
|
[newIndexSet release]; |
} else { |
if (![self.selectionIndexes containsIndex:layerIndex]) { |
self.selectionIndexes = [NSIndexSet indexSetWithIndex:layerIndex]; |
} |
} |
} |
} else { |
self.selectionIndexes = [NSIndexSet indexSet]; |
} |
|
// Redraw resize handles for new selection. |
[_overlayLayer setNeedsDisplay]; |
} |
|
// The user clicked while adjusting the photo masking of a slide. If the click is outside the photo's unmasked frame (and not in a resize handle either), then stop adjusting the photo masking. |
- (void)editingClickAction:(ClickTracker*)tracker { |
CGPoint layerLocation = [self.layer convertPoint:NSPointToCGPoint([tracker location]) fromLayer:nil]; |
if (!LTPointInRect(layerLocation, _editingSlide.photoFrame)){ |
CGRect resizeRects[8]; |
resizeRectsForFrame(resizeRects, _editingSlide.photoFrame); |
NSInteger resizeIndex = indexOfResizeRectForPoint(resizeRects, layerLocation); |
if (resizeIndex < 0) { |
_editingSlide.masksToBounds = YES; |
_editingSlide = nil; |
tracker.action = @selector(clickAction:); |
[self clickAction:tracker]; |
} |
} |
} |
|
// The user double cliked. If adjusting a photo mask, then stop. Otherwise, begin adjusting a photo mask if the double click occured on a slide. |
- (void)doubleClickAction:(ClickTracker*)tracker { |
if (_editingSlide) { |
_editingSlide.masksToBounds = YES; |
_editingSlide = nil; |
tracker.action = @selector(clickAction:); |
[self clickAction:tracker]; |
[_overlayLayer setNeedsDisplay]; |
return; |
} else { |
CGPoint trackerLocation = NSPointToCGPoint([tracker location]); |
|
CALayer *layer = [self.layer hitTest:trackerLocation]; |
layer = (layer == self.layer) ? nil : layer; |
|
if (layer) { |
// We don't allow selections during photo mask adjustments. |
self.selectionIndexes = [NSIndexSet indexSet]; |
|
// Begin photo mask adjustment mode. |
_editingSlide = (LTMaskLayer *)layer; |
layer.masksToBounds = NO; |
|
// Change the click action because selection modification is not valid in this mode. |
tracker.action = @selector(editingClickAction:); |
} |
} |
|
// Update the resize handle drawing. |
[_overlayLayer setNeedsDisplay]; |
} |
|
|
// The user has exceeded the drag threshold, and we are not currently tracking touches. Start drag tracking. |
- (void)beginMouseDrag:(DragTracker*)tracker { |
CGPoint layerLocation = [self.layer convertPoint:NSPointToCGPoint(tracker.initialPoint) fromLayer:nil]; |
|
if (_editingSlide) { |
// The user is adjusting a photo mask |
CALayer *layer = _editingSlide.photoLayer; |
|
// Check if the use is resizing via a drag handle |
CGRect resizeRects[8]; |
resizeRectsForFrame(resizeRects, _editingSlide.photoFrame); |
NSInteger resizeIndex = indexOfResizeRectForPoint(resizeRects, layerLocation); |
|
if (resizeIndex >= 0) { |
// Drag handle resize |
tracker.userInfo = [NSDictionary dictionaryWithObjectsAndKeys: |
layer, kLayerKey, |
[NSValue valueWithRect:NSRectFromCGRect(layer.frame)], kInitialFrameKey, |
[NSNumber numberWithInteger:resizeIndex], kResizeIndexKey, |
nil]; |
|
// Notice how we manage which type of tracking we are performing by simply changing the tracking action methods. |
tracker.updateTrackingAction = @selector(resizeSlide:); |
tracker.endTrackingAction = @selector(resizeSlideEnd:); |
|
[self disableTrackersExcluding:tracker]; // No other tracking allowed while dragging. |
} else { |
// The user is moving the photo inside the mask. Note: we don't need to confrim this by comparing the mouse location against the photo's frame, because the dragging threshold means the click action will have already fired, making the assessment for us and updated the _editingSlide value. |
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys: |
layer, kLayerKey, |
[NSValue valueWithPoint:NSPointFromCGPoint(layer.position)], kInitialPositionKey, |
nil]; |
tracker.userInfo = [NSArray arrayWithObject:userInfo]; |
|
// Notice how we manage which type of tracking we are performing by simply changing the tracking action methods. |
tracker.updateTrackingAction = @selector(dragSlides:); |
tracker.endTrackingAction = @selector(dragSlidesEnd:); |
|
[self disableTrackersExcluding:tracker]; // No other tracking allowed while dragging. |
} |
return; |
} |
|
// The user is not modifying a photo mask. Determine if the user is resizing via drag handle, moving the selection, or dragging in empty space. |
// Loop through every layer in the selection. |
for (CALayer *layer in [[self.layer sublayers] objectsAtIndexes:self.selectionIndexes]) { |
CGRect resizeRects[8]; |
resizeRectsForFrame(resizeRects, layer.frame); |
NSInteger resizeIndex = indexOfResizeRectForPoint(resizeRects, layerLocation); |
|
// Check the resize handles. |
if (resizeIndex >= 0) { |
// Resize only this layer. |
tracker.userInfo = [NSDictionary dictionaryWithObjectsAndKeys: |
layer, kLayerKey, |
[NSValue valueWithRect:NSRectFromCGRect(layer.frame)], kInitialFrameKey, |
[NSNumber numberWithInteger:resizeIndex], kResizeIndexKey, |
nil]; |
|
// Notice how we manage which type of tracking we are performing by simply changing the tracking action methods. |
tracker.updateTrackingAction = @selector(resizeSlide:); |
tracker.endTrackingAction = @selector(resizeSlideEnd:); |
|
[self disableTrackersExcluding:tracker]; // No other tracking allowed while dragging. |
return; |
} |
|
// Check if the cursor is within this slide. If so, move the entire selection. |
if ([layer containsPoint:[layer convertPoint:layerLocation fromLayer:self.layer]]){ |
NSMutableArray *array = [NSMutableArray arrayWithCapacity:[self.selectionIndexes count]]; |
for (CALayer *layer in [[self.layer sublayers] objectsAtIndexes:self.selectionIndexes]) { |
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys: |
layer, kLayerKey, |
[NSValue valueWithPoint:NSPointFromCGPoint(layer.position)], kInitialPositionKey, |
nil]; |
[array addObject:userInfo]; |
} |
tracker.userInfo = array; |
|
// Notice how we manage which type of tracking we are performing by simply changing the tracking action methods. |
tracker.updateTrackingAction = @selector(dragSlides:); |
tracker.endTrackingAction = @selector(dragSlidesEnd:); |
|
[self disableTrackersExcluding:tracker]; // No other tracking allowed while dragging. |
return; |
} |
} |
|
// If we get here, then the user has dragged in empty space. Notice how we manage which type of tracking we are performing by simply changing the tracking action methods. In this case, the actions are left as nil, so nothing will occur. |
|
// A rubber band selection is left as an exercise to the reader. |
} |
|
// Mouse dragging of either the selection or of an LTMaskLayer's photo sublayer. Though, this method only looks at an array of CALayers in the tracker userInfo, and does not need to distinguish between the two. |
- (void)dragSlides:(DragTracker*)tracker { |
NSPoint delta = tracker.delta; |
NSArray *array = tracker.userInfo; |
|
// Turn off animation so that each layer is moved immediately. |
[LTView setupCAAnimationStack]; |
for (NSDictionary *userInfo in array){ |
CALayer *layer = [userInfo objectForKey:kLayerKey]; |
NSPoint initialPosition = [[userInfo objectForKey:kInitialPositionKey] pointValue]; |
if (layer) { |
initialPosition.x += delta.x; |
initialPosition.y += delta.y; |
layer.position = NSPointToCGPoint(initialPosition); |
} |
} |
[LTView restoreCAAnimationStack]; |
|
// Update drag handles |
[_overlayLayer setNeedsDisplay]; |
} |
|
// Mouse dragging of either the selection or of an LTMaskLayer's photo sublayer has ended. Though, this method only looks at an array of CALayers in the tracker userInfo, and does not need to distinguish between the two. |
- (void)dragSlidesEnd:(DragTracker*)tracker { |
[self dragSlides:tracker]; |
|
// Commit the new CALayer frame values to the data source |
for (NSDictionary *userInfo in tracker.userInfo){ |
[self commitFrameChangeOfLayer:[userInfo objectForKey:kLayerKey]]; |
} |
|
// reset the tracker back to nil values |
tracker.userInfo = nil; |
tracker.updateTrackingAction = nil; |
tracker.endTrackingAction = nil; |
|
// Tracking over, re-enable all trackers. |
[self enableTrackers]; |
} |
|
// Mouse resizing of a layer via a drag handle. This may be an LTMaskLayer or its photo sublayer. Though, this method only looks at the CALayer in the tracker userInfo, and does not need to distinguish between the two. |
- (void)resizeSlide:(DragTracker*)tracker { |
NSPoint delta = tracker.delta; |
NSDictionary *userInfo = tracker.userInfo; |
CALayer *layer = [userInfo objectForKey:kLayerKey]; |
|
CGRect frame = NSRectToCGRect([[userInfo objectForKey:kInitialFrameKey] rectValue]); |
switch ([[userInfo objectForKey:kResizeIndexKey] integerValue]) { |
case 0: //top left |
frame.origin.x += delta.x; |
frame.size.width -= delta.x; |
frame.size.height += delta.y; |
break; |
|
case 1: //top middle |
frame.size.height += delta.y; |
break; |
|
case 2: //top right |
frame.size.width += delta.x; |
frame.size.height += delta.y; |
break; |
|
case 3: //right middle |
frame.size.width += delta.x; |
break; |
|
case 4: //bottom right |
frame.origin.y += delta.y; |
frame.size.width += delta.x; |
frame.size.height -= delta.y; |
break; |
|
case 5: //bottom middle |
frame.origin.y += delta.y; |
frame.size.height -= delta.y; |
break; |
|
case 6: //bottom left |
frame.origin.x += delta.x; |
frame.origin.y += delta.y; |
frame.size.width -= delta.x; |
frame.size.height -= delta.y; |
break; |
|
case 7: //left middle |
frame.origin.x += delta.x; |
frame.size.width -= delta.x; |
break; |
|
default: |
break; |
} |
|
// Turn off animation so that each layer is moved immeditaly. |
[LTView setupCAAnimationStack]; |
layer.frame = frame; |
[LTView restoreCAAnimationStack]; |
|
// Update resize handles |
[_overlayLayer setNeedsDisplay]; |
} |
|
- (void)resizeSlideEnd:(DragTracker*)tracker { |
[self resizeSlide:tracker]; |
|
// Commit the new CALayer frame values to the data source |
NSDictionary *userInfo = tracker.userInfo; |
[self commitFrameChangeOfLayer:[userInfo objectForKey:kLayerKey]]; |
|
// Reset the tracker back to nil values |
tracker.userInfo = nil; |
tracker.updateTrackingAction = nil; |
tracker.endTrackingAction = nil; |
|
// Tracking over, re-enable all trackers. |
[self enableTrackers]; |
} |
|
|
// The user has two fingers on the trackpad, has exceeded the movement threshold, and we are not currently tracking the mouse. Start dual-touch tracking. |
- (void)dualTouchesBegan:(DualTouchTracker*)tracker { |
CALayer *layer = nil; |
CGPoint trackerLocation = NSPointToCGPoint(tracker.initialPoint); |
|
if (_editingSlide) { |
// The user is adjusting a photo mask, use the photo sublayer if the cursor is over the unmasked photo |
CGPoint layerLocation = [self.layer convertPoint:trackerLocation fromLayer:nil]; |
if (LTPointInRect(layerLocation, _editingSlide.photoFrame)) { |
layer = _editingSlide.photoLayer; |
} |
} else { |
// The user is not adjusting a photo mask. Determine which LTMaskLayer is under the cursor, if any. |
layer = [self.layer hitTest:trackerLocation]; |
layer = (layer == self.layer) ? nil : layer; |
} |
|
if (layer) { |
tracker.userInfo = [NSDictionary dictionaryWithObjectsAndKeys: |
layer, kLayerKey, |
[NSValue valueWithRect:NSRectFromCGRect(layer.frame)], kInitialFrameKey, |
nil]; |
|
// Notice how we manage which type of tracking we are performing by simply changing the tracking action methods. |
tracker.updateTrackingAction = @selector(dualTouchesMoved:); |
tracker.endTrackingAction = @selector(dualTouchesEnded:); |
|
[self disableTrackersExcluding:tracker]; // No other tracking allowed while dragging. |
|
// Hide the cursor since the user is not moving the cursor. |
[NSCursor hide]; |
}/* else { |
The cursor is not over an appropriate layer. Notice how we manage which type of tracking we are performing by simply changing the tracking action methods. In this case, the actions are left as nil, so nothing will occur. |
} */ |
} |
|
- (void)dualTouchesMoved:(DualTouchTracker*)tracker { |
NSDictionary *userInfo = tracker.userInfo; |
CGPoint deltaOrigin = NSPointToCGPoint(tracker.deltaOrigin); |
CGSize deltaSize = NSSizeToCGSize(tracker.deltaSize); |
|
CGRect originalFrame = NSRectToCGRect([[userInfo objectForKey:kInitialFrameKey] rectValue]); |
CGRect newFrame = originalFrame; |
newFrame.origin.x += deltaOrigin.x; |
newFrame.origin.y += deltaOrigin.y; |
newFrame.size.width += deltaSize.width; |
newFrame.size.height += deltaSize.height; |
|
// Update the Layer's frame |
CALayer *layer = [userInfo objectForKey:kLayerKey]; |
[LTView setupCAAnimationStack]; |
layer.frame = newFrame; |
[LTView restoreCAAnimationStack]; |
|
// Update selection handles if needed |
[_overlayLayer setNeedsDisplay]; |
|
// Warp the cursor so that new touches are targeted to this Slide. |
NSPoint trackerLocation = NSPointFromCGPoint([layer.superlayer convertPoint:NSPointToCGPoint(tracker.initialPoint) fromLayer:nil]); |
|
// Calculate the original cursor offest. |
deltaOrigin.x = trackerLocation.x - CGRectGetMinX(originalFrame); |
deltaOrigin.y = trackerLocation.y - CGRectGetMinY(originalFrame); |
|
// Determine new cursor offest |
deltaOrigin.x = (deltaOrigin.x/CGRectGetWidth(originalFrame)) * CGRectGetWidth(newFrame); |
deltaOrigin.y = (deltaOrigin.y/CGRectGetHeight(originalFrame)) * CGRectGetHeight(newFrame); |
|
// Use new cursor offset to warp cursor in screen space |
CGPoint cgCursorLocation = newFrame.origin; |
cgCursorLocation.x += deltaOrigin.x; |
cgCursorLocation.y += deltaOrigin.y; |
cgCursorLocation = [layer.superlayer convertPoint:cgCursorLocation toLayer:nil]; |
|
NSPoint nsCursorLocation = NSPointFromCGPoint(cgCursorLocation); |
nsCursorLocation = [self convertPointToBase:nsCursorLocation]; |
nsCursorLocation = [self.window convertBaseToScreen:nsCursorLocation]; |
nsCursorLocation.y = [[NSScreen mainScreen] frame].size.height - nsCursorLocation.y; |
CGWarpMouseCursorPosition(NSPointToCGPoint(nsCursorLocation)); |
} |
|
- (void)dualTouchesEnded:(DualTouchTracker*)tracker { |
NSDictionary *userInfo = tracker.userInfo; |
[self commitFrameChangeOfLayer:[userInfo objectForKey:kLayerKey]]; |
|
tracker.updateTrackingAction = nil; |
tracker.endTrackingAction = nil; |
[self enableTrackers]; |
|
// We explicitly hide the cursor, so unhide it here. |
[NSCursor unhide]; |
[NSCursor setHiddenUntilMouseMoves:YES]; |
} |
|
@end |
|
// See LTView.h for definitions of these properties |
NSString *kLTViewSlides = @"slides"; |
NSString *kLTViewSelectionIndexes = @"selectionIndexes"; |
NSString *kLTViewSlidePropertyFrame = @"frame"; |
NSString *kLTViewSlidePropertyPhotoFrame = @"photoFrame"; |
NSString *kLTViewSlidePropertyPhoto = @"photo"; |
NSString *kLTViewSlidePropertyCornerRadius = @"cornerRadius"; |
NSString *kLTViewSlidePropertyFrameThickness = @"frameThickness"; |