/* |
File: ImagePreviewCell.m |
Abstract: Provides a cell implementation that draws an image, title, sub-title, and has a |
custom trackable button that highlights when the mouse moves over it. |
|
Version: 1.7 |
|
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) 2014 Apple Inc. All Rights Reserved. |
|
*/ |
|
#import "ImagePreviewCell.h" |
|
// These defines should be on, and are simply for demo purposes |
#define HIT_TEST 1 |
#define EDIT_FRAME 1 |
#define TRACKING 1 |
#define TRACKING_AREA 1 |
#define EXPANSION_FRAME_SUPPORT 1 |
|
#pragma mark - |
|
@implementation ImagePreviewCell |
|
- (id)init { |
|
self = [super init]; |
if (self != nil) { |
[self setLineBreakMode:NSLineBreakByTruncatingTail]; |
} |
return self; |
} |
|
- (id)initWithCoder:(NSCoder *)aDecoder { |
|
self = [super initWithCoder:aDecoder]; |
return self; |
} |
|
// NSTableView likes to copy a cell before tracking -- |
// therefore we need to properly implement copyWithZone. |
// |
- (id)copyWithZone:(NSZone *)zone { |
|
ImagePreviewCell *result = [super copyWithZone:zone]; |
if (result != nil) { |
// We must clear out the image beforehand; otherwise, it would contain the previous |
// image (which wouldn't be retained), and doing the setImage: would be a nop since |
// it is the same image. This would eventually lead to a crash after you click on |
// the cell in a tableview, since it copies the cell at that time, and later releases it. |
// |
result->iImage = nil; |
result->iSubTitle = nil; |
[result setImage:[self image]]; |
[result setSubTitle:[self subTitle]]; |
} |
return result; |
} |
|
- (void)dealloc { |
|
[iImage release]; |
[iSubTitle release]; |
[super dealloc]; |
} |
|
- (NSImage *)image { |
|
return iImage; |
} |
|
- (void)setImage:(NSImage *)image { |
|
if (image != iImage) { |
[iImage release]; |
iImage = [image retain]; |
} |
} |
|
- (NSString *)subTitle { |
|
return iSubTitle; |
} |
|
- (void)setSubTitle:(NSString *)subTitle { |
|
if ((iSubTitle == nil) || ![iSubTitle isEqualToString:subTitle]) { |
[iSubTitle release]; |
iSubTitle = [subTitle retain]; |
} |
} |
|
- (SEL)infoButtonAction { |
|
return iInfoButtonAction; |
} |
|
- (void)setInfoButtonAction:(SEL)action { |
|
iInfoButtonAction = action; |
} |
|
- (NSImage *)infoButtonImage { |
|
// Construct an image name based on our current state |
NSString *imageName = [NSString stringWithFormat:@"info-%@%@", |
[self isHighlighted] ? @"selected" : @"normal", |
iMouseDownInInfoButton ? @"-mouse" : |
iMouseHoveredInInfoButton ? @"-hovered" : @""]; |
return [NSImage imageNamed:imageName]; |
} |
|
- (NSAttributedString *)attributedSubTitle { |
|
NSAttributedString *result = nil; |
if (iSubTitle) { |
// Make the text color gray, or light gray, depending on if we are highlighted (selected) or not |
NSColor *textColor = [self isHighlighted] ? [NSColor lightGrayColor] : [NSColor grayColor]; |
// Create a set of attributes to use |
NSDictionary *attrs = [NSDictionary dictionaryWithObjectsAndKeys: |
textColor, NSForegroundColorAttributeName, |
nil]; |
result = [[NSAttributedString alloc] initWithString:iSubTitle attributes:attrs]; |
} |
return [result autorelease]; |
} |
|
|
#pragma mark - |
|
#define PADDING_BEFORE_IMAGE 5.0 |
#define PADDING_BETWEEN_TITLE_AND_IMAGE 4.0 |
#define VERTICAL_PADDING_FOR_IMAGE 4.0 |
#define INFO_IMAGE_SIZE 13.0 |
#define PADDING_AROUND_INFO_IMAGE 2.0 |
#define IMAGE_SIZE 32.0 |
|
- (NSRect)rectForSubTitleBasedOnTitleRect:(NSRect)titleRect inBounds:(NSRect)bounds { |
|
NSAttributedString *subTitle = [self attributedSubTitle]; |
if (subTitle != nil) { |
titleRect.origin.y += titleRect.size.height; |
titleRect.size.width = [subTitle size].width; |
// Make sure it doesn't go past the bounds |
CGFloat amountPast = NSMaxX(titleRect) - NSMaxX(bounds); |
if (amountPast > 0) { |
titleRect.size.width -= amountPast; |
} |
return titleRect; |
} else { |
return NSZeroRect; |
} |
} |
|
- (NSRect)subTitleRectForBounds:(NSRect)bounds { |
|
NSRect titleRect = [self titleRectForBounds:bounds]; |
return [self rectForSubTitleBasedOnTitleRect:titleRect inBounds:bounds]; |
} |
|
- (NSRect)rectForInfoButtonBasedOnTitleRect:(NSRect)titleRect inBounds:(NSRect)bounds { |
|
NSRect buttonRect = titleRect; |
buttonRect.origin.x = NSMaxX(titleRect) + PADDING_BETWEEN_TITLE_AND_IMAGE; |
buttonRect.origin.y += 2.0; |
buttonRect.size.height = INFO_IMAGE_SIZE; |
buttonRect.size.width = INFO_IMAGE_SIZE; |
// Make sure it doesn't go past the bounds -- if so, we don't want to draw it. |
if (NSMaxX(buttonRect) - NSMaxX(bounds) > 0) { |
buttonRect = NSZeroRect; |
} |
buttonRect.origin.x = round(buttonRect.origin.x); |
return buttonRect; |
} |
|
- (NSRect)infoButtonRectForBounds:(NSRect)bounds { |
|
NSRect titleRect = [self titleRectForBounds:bounds]; |
return [self rectForInfoButtonBasedOnTitleRect:titleRect inBounds:bounds]; |
} |
|
- (NSRect)imageRectForBounds:(NSRect)bounds { |
|
NSRect result = bounds; |
result.origin.y += VERTICAL_PADDING_FOR_IMAGE; |
result.origin.x += PADDING_BEFORE_IMAGE; |
if (iImage != nil) { |
// Take the actual image and center it in the result |
result.size = [iImage size]; |
CGFloat widthCenter = IMAGE_SIZE - NSWidth(result); |
if (widthCenter > 0) { |
result.origin.x += round(widthCenter / 2.0); |
} |
CGFloat heightCenter = IMAGE_SIZE - NSHeight(result); |
if (heightCenter > 0) { |
result.origin.y += round(heightCenter / 2.0); |
} |
} else { |
result.size.width = result.size.height = IMAGE_SIZE; |
} |
return result; |
} |
|
- (NSRect)titleRectForBounds:(NSRect)bounds { |
|
NSAttributedString *title = [self attributedStringValue]; |
NSRect result = bounds; |
// The x origin is easy |
result.origin.x += PADDING_BEFORE_IMAGE + IMAGE_SIZE + PADDING_BETWEEN_TITLE_AND_IMAGE; |
// The y origin should be inline with the image |
result.origin.y += VERTICAL_PADDING_FOR_IMAGE; |
// Set the width and the height based on the texts real size. Notice the nil check! |
// Otherwise, the resulting NSSize could be undefined if we messaged a nil object. |
if (title != nil) { |
result.size = [title size]; |
} else { |
result.size = NSZeroSize; |
} |
// Now, we have to constrain us to the bounds. The max x we can go to has to be the |
// same as the bounds, but minus the info image location |
CGFloat maxX = NSMaxX(bounds) - (PADDING_AROUND_INFO_IMAGE + INFO_IMAGE_SIZE + PADDING_AROUND_INFO_IMAGE); |
CGFloat maxWidth = maxX - NSMinX(result); |
if (maxWidth < 0) maxWidth = 0; |
// Constrain us to these bounds |
result.size.width = MIN(NSWidth(result), maxWidth); |
return result; |
} |
|
- (NSSize)cellSizeForBounds:(NSRect)bounds { |
|
NSSize result; |
// Figure out the natural cell size and confine it to the bounds given |
NSRect titleRect = [self titleRectForBounds:bounds]; |
result.width = PADDING_BEFORE_IMAGE + IMAGE_SIZE + PADDING_BETWEEN_TITLE_AND_IMAGE + titleRect.size.width; |
// Add in spacing for the info image |
result.width += PADDING_AROUND_INFO_IMAGE + INFO_IMAGE_SIZE + PADDING_AROUND_INFO_IMAGE; |
result.height = VERTICAL_PADDING_FOR_IMAGE + IMAGE_SIZE + VERTICAL_PADDING_FOR_IMAGE; |
// Constrain it to the bounds passed in |
result.width = MIN(result.width, NSWidth(bounds)); |
result.height = MIN(result.height, NSHeight(bounds)); |
return result; |
} |
|
- (void)drawInteriorWithFrame:(NSRect)bounds inView:(NSView *)controlView { |
|
NSRect imageRect = [self imageRectForBounds:bounds]; |
if (iImage != nil) { |
[iImage setFlipped:[controlView isFlipped]]; |
[iImage drawInRect:imageRect fromRect:NSZeroRect operation:NSCompositeSourceIn fraction:1.0]; |
} else { |
NSBezierPath *path = [NSBezierPath bezierPathWithRect:imageRect]; |
CGFloat pattern[2] = { 4.0, 2.0 }; |
[path setLineDash:pattern count:2 phase:1.0]; |
[path setLineWidth:0]; |
[[NSColor grayColor] set]; |
[path stroke]; |
} |
|
NSRect titleRect = [self titleRectForBounds:bounds]; |
NSAttributedString *title = [self attributedStringValue]; |
if ([title length] > 0) { |
[title drawInRect:titleRect]; |
} |
|
NSAttributedString *attributedSubTitle = [self attributedSubTitle]; |
if ([attributedSubTitle length] > 0) { |
NSRect attributedSubTitleRect = [self rectForSubTitleBasedOnTitleRect:titleRect inBounds:bounds]; |
[attributedSubTitle drawInRect:attributedSubTitleRect]; |
} |
|
NSRect infoButtonRect = [self infoButtonRectForBounds:bounds]; |
NSImage *image = [self infoButtonImage]; |
[image setFlipped:[controlView isFlipped]]; |
[image drawInRect:infoButtonRect fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1.0]; |
} |
|
#if HIT_TEST |
|
- (NSUInteger)hitTestForEvent:(NSEvent *)event inRect:(NSRect)cellFrame ofView:(NSView *)controlView { |
|
NSPoint point = [controlView convertPoint:[event locationInWindow] fromView:nil]; |
|
NSRect titleRect = [self titleRectForBounds:cellFrame]; |
if (NSMouseInRect(point, titleRect, [controlView isFlipped])) { |
return NSCellHitContentArea | NSCellHitEditableTextArea; |
} |
|
NSRect imageRect = [self imageRectForBounds:cellFrame]; |
if (NSMouseInRect(point, imageRect, [controlView isFlipped])) { |
return NSCellHitContentArea; |
} |
|
// Did we hit the sub title? |
NSAttributedString *attributedSubTitle = [self attributedSubTitle]; |
if ([attributedSubTitle length] > 0) { |
NSRect attributedSubTitleRect = [self rectForSubTitleBasedOnTitleRect:titleRect inBounds:cellFrame]; |
if (NSMouseInRect(point, attributedSubTitleRect, [controlView isFlipped])) { |
// Notice that this text isn't an editable area. Clicking on it won't begin an editing session. |
return NSCellHitContentArea; |
} |
} |
|
// How about the info button? |
NSRect infoButtonRect = [self infoButtonRectForBounds:cellFrame]; |
if (NSMouseInRect(point, infoButtonRect, [controlView isFlipped])) { |
return NSCellHitContentArea | NSCellHitTrackableArea; |
} |
|
return NSCellHitNone; |
} |
|
#endif // HIT_TEST |
|
#if EDIT_FRAME |
|
- (void)editWithFrame:(NSRect)aRect inView:(NSView *)controlView editor:(NSText *)textObj delegate:(id)anObject event:(NSEvent *)theEvent { |
|
// Take advantaged of NSTextFieldCell's implementation of editWithFrame:, and just |
// adjust the frame for the area that really contains the text. |
NSRect titleRect = [self titleRectForBounds:aRect]; |
// Push the origin a little to the left so the new text appears directly over the |
// existing text in the cell |
titleRect.origin.x -= 2; |
// Since the NSText will not automatically grow, we should give it all the space that |
// is available for editing |
CGFloat sizeBeforeTitle = NSMinX(titleRect) - NSMinX(aRect); |
titleRect.size.width = NSWidth(aRect) - sizeBeforeTitle; |
[super editWithFrame:titleRect inView:controlView editor:textObj delegate:anObject event:theEvent]; |
} |
|
// NSTableView may call selectWithFrame: or editWithFrame: depending on how it is invoked. |
// This code should mirror the above method. selectWithFrame: differs by starting an editing |
// session and selecting all the text in the cell. |
- (void)selectWithFrame:(NSRect)aRect inView:(NSView *)controlView editor:(NSText *)textObj delegate:(id)anObject start:(NSInteger)selStart length:(NSInteger)selLength { |
|
NSRect titleRect = [self titleRectForBounds:aRect]; |
titleRect.origin.x -= 2; |
CGFloat sizeBeforeTitle = NSMinX(titleRect) - NSMinX(aRect); |
titleRect.size.width = NSWidth(aRect) - sizeBeforeTitle; |
[super selectWithFrame:titleRect inView:controlView editor:textObj delegate:anObject start:selStart length:selLength]; |
} |
|
#endif // EDIT_FRAME |
|
#if TRACKING |
|
+ (BOOL)prefersTrackingUntilMouseUp { |
// NSCell returns NO for this by default. If you want to have trackMouse:inRect:ofView:untilMouseUp: |
// always track until the mouse is up, then you MUST return YES. Otherwise, strange things will happen. |
return YES; |
} |
|
// Mouse tracking -- the only part we want to track is the "info" button |
- (BOOL)trackMouse:(NSEvent *)theEvent inRect:(NSRect)cellFrame ofView:(NSView *)controlView untilMouseUp:(BOOL)flag { |
|
[self setControlView:controlView]; |
|
NSRect infoButtonRect = [self infoButtonRectForBounds:cellFrame]; |
while ([theEvent type] != NSLeftMouseUp) { |
// This is VERY simple event tracking. We simply check to see if the mouse is in |
// the "i" button or not and dispatch entered/exited mouse events |
NSPoint point = [controlView convertPoint:[theEvent locationInWindow] fromView:nil]; |
BOOL mouseInButton = NSMouseInRect(point, infoButtonRect, [controlView isFlipped]); |
if (iMouseDownInInfoButton != mouseInButton) { |
iMouseDownInInfoButton = mouseInButton; |
[controlView setNeedsDisplayInRect:cellFrame]; |
} |
if ([theEvent type] == NSMouseEntered || [theEvent type] == NSMouseExited) { |
[NSApp sendEvent:theEvent]; |
} |
// Note that we process mouse entered and exited events and dispatch them to properly handle updates |
theEvent = [[controlView window] nextEventMatchingMask:(NSLeftMouseUpMask | NSLeftMouseDraggedMask | NSMouseEnteredMask | NSMouseExitedMask)]; |
} |
|
// Another way of implementing the above code would be to keep an NSButtonCell as an ivar, and simply call trackMouse:inRect:ofView:untilMouseUp: on it, if the tracking area was inside of it. |
|
if (iMouseDownInInfoButton) { |
// Send the action, and redisplay |
iMouseDownInInfoButton = NO; |
[controlView setNeedsDisplayInRect:cellFrame]; |
if (iInfoButtonAction) { |
[NSApp sendAction:iInfoButtonAction to:[self target] from:[self controlView]]; |
} |
} |
|
// We return YES since the mouse was released while we were tracking. |
// Not returning YES when you processed the mouse up is an easy way to introduce bugs! |
return YES; |
} |
|
#endif // TRACKING |
|
#if TRACKING_AREA |
|
// Mouse movement tracking -- we have a custom NSOutlineView subclass that automatically |
// lets us add mouseEntered:/mouseExited: support to any cell! |
// |
- (void)addTrackingAreasForView:(NSView *)controlView inRect:(NSRect)cellFrame withUserInfo:(NSDictionary *)userInfo mouseLocation:(NSPoint)mouseLocation { |
|
NSRect infoButtonRect = [self infoButtonRectForBounds:cellFrame]; |
|
NSTrackingAreaOptions options = NSTrackingEnabledDuringMouseDrag | NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways; |
|
BOOL mouseIsInside = NSMouseInRect(mouseLocation, infoButtonRect, [controlView isFlipped]); |
if (mouseIsInside) { |
options |= NSTrackingAssumeInside; |
[controlView setNeedsDisplayInRect:cellFrame]; |
} |
|
// We make the view the owner, and it delegates the calls back to the cell after it is |
// properly setup for the corresponding row/column in the outlineview |
NSTrackingArea *area = [[NSTrackingArea alloc] initWithRect:infoButtonRect options:options owner:controlView userInfo:userInfo]; |
[controlView addTrackingArea:area]; |
[area release]; |
} |
|
- (void)mouseEntered:(NSEvent *)event { |
|
iMouseHoveredInInfoButton = YES; |
[(NSControl *)[self controlView] updateCell:self]; |
} |
|
- (void)mouseExited:(NSEvent *)event { |
|
iMouseHoveredInInfoButton = NO; |
[(NSControl *)[self controlView] updateCell:self]; |
} |
|
#endif // TRACKING_AREA |
|
#if EXPANSION_FRAME_SUPPORT |
|
// Expansion tool tip support |
- (NSRect)expansionFrameWithFrame:(NSRect)cellFrame inView:(NSView *)view { |
|
// By default, for NSTextFieldCell, the cell is queried for the titleRectForBounds: |
// with a large rect. That value is returned, and is the correct implementation for us, |
// but we want a slightly larger rect |
// |
NSRect rect = [super expansionFrameWithFrame:cellFrame inView:view]; |
if (!NSIsEmptyRect(rect)) { |
// We want to make the cell *slightly* larger; it looks better when showing the expansion tool tip. |
rect.size.width += 4.0; |
rect.origin.x -= 2.0; |
} |
return rect; |
} |
|
- (void)drawWithExpansionFrame:(NSRect)cellFrame inView:(NSView *)view { |
|
// The drawing isn't correct; we ONLY want to draw the title rect, and do that here. |
NSAttributedString *title = [self attributedStringValue]; |
if ([title length] > 0) { |
cellFrame.origin.x += 2.0; |
cellFrame.size.width -= 2.0; |
[title drawInRect:cellFrame]; |
} |
} |
|
#endif // EXPANSION_FRAME_SUPPORT |
|
@end |