Index: chrome/browser/ui/cocoa/hover_close_button.mm |
diff --git a/chrome/browser/ui/cocoa/hover_close_button.mm b/chrome/browser/ui/cocoa/hover_close_button.mm |
index 32a5c80d6ee00f6de82001982ef36add487749de..5dacaded56c589658e7a86275a7e6324179c24f0 100644 |
--- a/chrome/browser/ui/cocoa/hover_close_button.mm |
+++ b/chrome/browser/ui/cocoa/hover_close_button.mm |
@@ -4,26 +4,98 @@ |
#import "chrome/browser/ui/cocoa/hover_close_button.h" |
+#import <QuartzCore/QuartzCore.h> |
+ |
#include "base/memory/scoped_nsobject.h" |
+#include "base/memory/scoped_ptr.h" |
+#import "chrome/browser/ui/cocoa/animation_utils.h" |
#include "grit/generated_resources.h" |
#import "third_party/molokocacao/NSBezierPath+MCAdditions.h" |
#include "ui/base/l10n/l10n_util.h" |
+#include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h" |
namespace { |
-const CGFloat kCircleRadius = 0.415 * 16; |
+const CGFloat kButtonWidth = 16; |
+const CGFloat kCircleRadius = 0.415 * kButtonWidth; |
const CGFloat kCircleHoverWhite = 0.565; |
const CGFloat kCircleClickWhite = 0.396; |
const CGFloat kXShadowAlpha = 0.75; |
const CGFloat kXShadowCircleAlpha = 0.1; |
+ |
+// Images that are used for all close buttons. Set up in +initialize. |
+CGImageRef gHoverNoneImage = NULL; |
+CGImageRef gHoverMouseOverImage = NULL; |
+CGImageRef gHoverMouseDownImage = NULL; |
+ |
+// Strings that are used for all close buttons. Set up in +initialize. |
+NSString* gTooltip = nil; |
+NSString* gDescription = nil; |
} // namespace |
-@interface HoverCloseButton(Private) |
-- (void)updatePaths; |
-- (void)setUpDrawingPaths; |
+@interface HoverCloseButton () |
+// Sets up the button's tracking areas and accessibility info when instantiated |
+// via initWithFrame or awakeFromNib. |
+- (void)commonInit; |
+ |
+// Draws the close button into a CGImageRef in a given state. |
++ (CGImageRef)imageForBounds:(NSRect)bounds |
+ xPath:(NSBezierPath*)xPath |
+ circlePath:(NSBezierPath*)circlePath |
+ hoverState:(HoverState)hoverState; |
@end |
@implementation HoverCloseButton |
++ (void)initialize { |
+ if ([self class] == [HoverCloseButton class]) { |
+ // Set up the paths for our images. They are centered around the origin. |
+ NSRect bounds = NSMakeRect(0, 0, kButtonWidth, kButtonWidth); |
+ NSBezierPath* circlePath = [NSBezierPath bezierPath]; |
+ [circlePath appendBezierPathWithArcWithCenter:NSZeroPoint |
+ radius:kCircleRadius |
+ startAngle:0.0 |
+ endAngle:365.0]; |
+ |
+ // Construct an 'x' by drawing two intersecting rectangles in the shape of a |
+ // cross and then rotating the path by 45 degrees. |
+ NSBezierPath* xPath = [NSBezierPath bezierPath]; |
+ [xPath appendBezierPathWithRect:NSMakeRect(-4.5, -1.0, 9.0, 2.0)]; |
+ [xPath appendBezierPathWithRect:NSMakeRect(-1.0, -4.5, 2.0, 9.0)]; |
+ |
+ NSAffineTransform* transform = [NSAffineTransform transform]; |
+ [transform rotateByDegrees:45.0]; |
+ [xPath transformUsingAffineTransform:transform]; |
+ |
+ // Move the paths into the center of the given bounds rectangle. |
+ transform = [NSAffineTransform transform]; |
+ NSPoint xCenter = NSMakePoint(NSWidth(bounds) / 2.0, |
+ NSHeight(bounds) / 2.0); |
+ [transform translateXBy:xCenter.x yBy:xCenter.y]; |
+ [circlePath transformUsingAffineTransform:transform]; |
+ [xPath transformUsingAffineTransform:transform]; |
+ |
+ CGImageRef image = [self imageForBounds:bounds |
+ xPath:xPath |
+ circlePath:circlePath |
+ hoverState:kHoverStateNone]; |
+ gHoverNoneImage = CGImageRetain(image); |
+ image = [self imageForBounds:bounds |
+ xPath:xPath |
+ circlePath:circlePath |
+ hoverState:kHoverStateMouseOver]; |
+ gHoverMouseOverImage = CGImageRetain(image); |
+ image = [self imageForBounds:bounds |
+ xPath:xPath |
+ circlePath:circlePath |
+ hoverState:kHoverStateMouseDown]; |
+ gHoverMouseDownImage = CGImageRetain(image); |
+ |
+ // Grab some strings that are used by all close buttons. |
+ gDescription = [l10n_util::GetNSStringWithFixup(IDS_ACCNAME_CLOSE) copy]; |
+ gTooltip = [l10n_util::GetNSStringWithFixup(IDS_TOOLTIP_CLOSE_TAB) copy]; |
+ } |
+} |
+ |
- (id)initWithFrame:(NSRect)frameRect { |
if ((self = [super initWithFrame:frameRect])) { |
[self commonInit]; |
@@ -36,100 +108,128 @@ const CGFloat kXShadowCircleAlpha = 0.1; |
[self commonInit]; |
} |
-- (void)drawRect:(NSRect)rect { |
- if (!circlePath_.get() || !xPath_.get()) |
- [self setUpDrawingPaths]; |
+- (void)setHoverState:(HoverState)state { |
+ [super setHoverState:state]; |
- // Only call updatePaths if the size changed. |
- if (!NSEqualSizes(oldSize_, [self bounds].size)) |
- [self updatePaths]; |
+ // Only animate the HoverStateNone case. |
+ scoped_ptr<ScopedCAActionDisabler> disabler; |
+ if (state != kHoverStateNone) { |
+ disabler.reset(new ScopedCAActionDisabler); |
+ } |
+ [hoverNoneLayer_ setHidden:state != kHoverStateNone]; |
+ [hoverMouseDownLayer_ setHidden:state != kHoverStateMouseDown]; |
+ [hoverMouseOverLayer_ setHidden:state != kHoverStateMouseOver]; |
+} |
+ |
+- (void)commonInit { |
+ // Set accessibility description. |
+ NSCell* cell = [self cell]; |
+ [cell accessibilitySetOverrideValue:gDescription |
+ forAttribute:NSAccessibilityDescriptionAttribute]; |
+ |
+ // Add a tooltip. |
+ [self setToolTip:gTooltip]; |
+ |
+ // Set up layers |
+ CALayer* viewLayer = [self layer]; |
+ |
+ // Layers will be contrained to be 0 pixels from left edge horizontally, |
+ // and centered vertically. |
+ viewLayer.layoutManager = [CAConstraintLayoutManager layoutManager]; |
+ CAConstraint* xConstraint = |
+ [CAConstraint constraintWithAttribute:kCAConstraintMinX |
+ relativeTo:@"superlayer" |
+ attribute:kCAConstraintMinX |
+ offset:0]; |
+ CAConstraint* yConstraint = |
+ [CAConstraint constraintWithAttribute:kCAConstraintMidY |
+ relativeTo:@"superlayer" |
+ attribute:kCAConstraintMidY]; |
+ |
+ hoverNoneLayer_.reset([[CALayer alloc] init]); |
+ [hoverNoneLayer_ setDelegate:self]; |
+ |
+ // .get() is being used here (and below) because if it isn't used, the |
+ // compiler doesn't realize that the call to setFrame: is being performed |
+ // on a CALayer, and assumes that the call is being performed on a NSView. |
+ // setFrame: on NSView takes an NSRect, setFrame: on CALayer takes a CGRect. |
+ // The difference in arguments causes a compile error. |
+ [hoverNoneLayer_.get() setFrame:viewLayer.frame]; |
+ [viewLayer addSublayer:hoverNoneLayer_]; |
+ [hoverNoneLayer_ addConstraint:xConstraint]; |
+ [hoverNoneLayer_ addConstraint:yConstraint]; |
+ [hoverNoneLayer_ setContents:reinterpret_cast<id>(gHoverNoneImage)]; |
+ [hoverNoneLayer_ setHidden:NO]; |
+ |
+ hoverMouseOverLayer_.reset([[CALayer alloc] init]); |
+ [hoverMouseOverLayer_.get() setFrame:viewLayer.frame]; |
+ [viewLayer addSublayer:hoverMouseOverLayer_]; |
+ [hoverMouseOverLayer_ addConstraint:xConstraint]; |
+ [hoverMouseOverLayer_ addConstraint:yConstraint]; |
+ [hoverMouseOverLayer_ setContents:reinterpret_cast<id>(gHoverMouseOverImage)]; |
+ [hoverMouseOverLayer_ setHidden:YES]; |
+ |
+ hoverMouseDownLayer_.reset([[CALayer alloc] init]); |
+ [hoverMouseDownLayer_.get() setFrame:viewLayer.frame]; |
+ [viewLayer addSublayer:hoverMouseDownLayer_]; |
+ [hoverMouseDownLayer_ addConstraint:xConstraint]; |
+ [hoverMouseDownLayer_ addConstraint:yConstraint]; |
+ [hoverMouseDownLayer_ setContents:reinterpret_cast<id>(gHoverMouseDownImage)]; |
+ [hoverMouseDownLayer_ setHidden:YES]; |
+} |
+ |
++ (CGImageRef)imageForBounds:(NSRect)bounds |
+ xPath:(NSBezierPath*)xPath |
+ circlePath:(NSBezierPath*)circlePath |
+ hoverState:(HoverState)hoverState { |
+ gfx::ScopedNSGraphicsContextSaveGState graphicsStateSaver; |
+ |
+ scoped_nsobject<NSBitmapImageRep> imageRep( |
+ [[NSBitmapImageRep alloc] |
+ initWithBitmapDataPlanes:NULL |
+ pixelsWide:NSWidth(bounds) |
+ pixelsHigh:NSHeight(bounds) |
+ bitsPerSample:8 |
+ samplesPerPixel:4 |
+ hasAlpha:YES |
+ isPlanar:NO |
+ colorSpaceName:NSCalibratedRGBColorSpace |
+ bitmapFormat:0 |
+ bytesPerRow:kButtonWidth * 4 |
+ bitsPerPixel:32]); |
+ NSGraphicsContext* gc = |
+ [NSGraphicsContext graphicsContextWithBitmapImageRep:imageRep]; |
+ [NSGraphicsContext setCurrentContext:gc]; |
+ |
+ [[NSColor clearColor] set]; |
+ NSRectFill(bounds); |
// If the user is hovering over the button, a light/dark gray circle is drawn |
// behind the 'x'. |
- if (hoverState_ != kHoverStateNone) { |
+ if (hoverState != kHoverStateNone) { |
// Adjust the darkness of the circle depending on whether it is being |
// clicked. |
- CGFloat white = (hoverState_ == kHoverStateMouseOver) ? |
+ CGFloat white = (hoverState == kHoverStateMouseOver) ? |
kCircleHoverWhite : kCircleClickWhite; |
[[NSColor colorWithCalibratedWhite:white alpha:1.0] set]; |
- [circlePath_ fill]; |
+ [circlePath fill]; |
} |
[[NSColor whiteColor] set]; |
- [xPath_ fill]; |
+ [xPath fill]; |
// Give the 'x' an inner shadow for depth. If the button is in a hover state |
// (circle behind it), then adjust the shadow accordingly (not as harsh). |
NSShadow* shadow = [[[NSShadow alloc] init] autorelease]; |
- CGFloat alpha = (hoverState_ != kHoverStateNone) ? |
+ CGFloat alpha = (hoverState != kHoverStateNone) ? |
kXShadowCircleAlpha : kXShadowAlpha; |
- [shadow setShadowColor:[NSColor colorWithCalibratedWhite:0.15 |
- alpha:alpha]]; |
+ [shadow setShadowColor:[NSColor colorWithCalibratedWhite:0.15 alpha:alpha]]; |
[shadow setShadowOffset:NSMakeSize(0.0, 0.0)]; |
[shadow setShadowBlurRadius:2.5]; |
- [xPath_ fillWithInnerShadow:shadow]; |
-} |
- |
-- (void)commonInit { |
- // Set accessibility description. |
- NSString* description = l10n_util::GetNSStringWithFixup(IDS_ACCNAME_CLOSE); |
- [[self cell] |
- accessibilitySetOverrideValue:description |
- forAttribute:NSAccessibilityDescriptionAttribute]; |
- |
- // Add a tooltip. |
- [self setToolTip:l10n_util::GetNSStringWithFixup(IDS_TOOLTIP_CLOSE_TAB)]; |
-} |
- |
-- (void)setUpDrawingPaths { |
- // Keep the paths centered around the origin in this function. It is then |
- // translated in -updatePaths. |
- NSPoint xCenter = NSZeroPoint; |
- |
- circlePath_.reset([[NSBezierPath bezierPath] retain]); |
- [circlePath_ moveToPoint:xCenter]; |
- CGFloat radius = kCircleRadius; |
- [circlePath_ appendBezierPathWithArcWithCenter:xCenter |
- radius:radius |
- startAngle:0.0 |
- endAngle:365.0]; |
- |
- // Construct an 'x' by drawing two intersecting rectangles in the shape of a |
- // cross and then rotating the path by 45 degrees. |
- xPath_.reset([[NSBezierPath bezierPath] retain]); |
- [xPath_ appendBezierPathWithRect:NSMakeRect(3.5, 7.0, 9.0, 2.0)]; |
- [xPath_ appendBezierPathWithRect:NSMakeRect(7.0, 3.5, 2.0, 9.0)]; |
- |
- NSRect pathBounds = [xPath_ bounds]; |
- NSPoint pathCenter = NSMakePoint(NSMidX(pathBounds), NSMidY(pathBounds)); |
- |
- NSAffineTransform* transform = [NSAffineTransform transform]; |
- [transform translateXBy:xCenter.x yBy:xCenter.y]; |
- [transform rotateByDegrees:45.0]; |
- [transform translateXBy:-pathCenter.x yBy:-pathCenter.y]; |
- |
- [xPath_ transformUsingAffineTransform:transform]; |
-} |
- |
-- (void)updatePaths { |
- oldSize_ = [self bounds].size; |
- |
- // Revert the current transform for the two points. |
- if (transform_.get()) { |
- [transform_.get() invert]; |
- [circlePath_.get() transformUsingAffineTransform:transform_.get()]; |
- [xPath_.get() transformUsingAffineTransform:transform_.get()]; |
- } |
- |
- // Create the new transform. [self bounds] is prefered in case aRect wasn't |
- // literally taken as bounds (e.g. cropped). |
- NSPoint xCenter = NSMakePoint(8, oldSize_.height / 2.0f); |
+ [xPath fillWithInnerShadow:shadow]; |
- // Retain here, as scoped_* don't retain. |
- transform_.reset([[NSAffineTransform transform] retain]); |
- [transform_.get() translateXBy:xCenter.x yBy:xCenter.y]; |
- [circlePath_.get() transformUsingAffineTransform:transform_.get()]; |
- [xPath_.get() transformUsingAffineTransform:transform_.get()]; |
+ // CGImage returns an autoreleased CGImageRef. |
+ return [imageRep CGImage]; |
} |
@end |