Disable view source for Developer Tools.
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / gradient_button_cell.mm
blob4cd6f376d98511e6c0bd53d849843bc029764aa7
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "chrome/browser/ui/cocoa/gradient_button_cell.h"
7 #include <cmath>
9 #include "base/logging.h"
10 #import "base/mac/scoped_nsobject.h"
11 #import "chrome/browser/themes/theme_properties.h"
12 #import "chrome/browser/themes/theme_service.h"
13 #import "chrome/browser/ui/cocoa/nsview_additions.h"
14 #import "chrome/browser/ui/cocoa/rect_path_utils.h"
15 #import "chrome/browser/ui/cocoa/themed_window.h"
16 #include "grit/theme_resources.h"
17 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMNSColor+Luminance.h"
18 #import "ui/base/cocoa/nsgraphics_context_additions.h"
19 #include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h"
21 @interface GradientButtonCell (Private)
22 - (void)sharedInit;
24 // Get drawing parameters for a given cell frame in a given view. The inner
25 // frame is the one required by |-drawInteriorWithFrame:inView:|. The inner and
26 // outer paths are the ones required by |-drawBorderAndFillForTheme:...|. The
27 // outer path also gives the area in which to clip. Any of the |return...|
28 // arguments may be NULL (in which case the given parameter won't be returned).
29 // If |returnInnerPath| or |returnOuterPath|, |*returnInnerPath| or
30 // |*returnOuterPath| should be nil, respectively.
31 - (void)getDrawParamsForFrame:(NSRect)cellFrame
32                        inView:(NSView*)controlView
33                    innerFrame:(NSRect*)returnInnerFrame
34                     innerPath:(NSBezierPath**)returnInnerPath
35                      clipPath:(NSBezierPath**)returnClipPath;
37 - (void)updateTrackingAreas;
39 @end
42 static const NSTimeInterval kAnimationShowDuration = 0.2;
44 // Note: due to a bug (?), drawWithFrame:inView: does not call
45 // drawBorderAndFillForTheme::::: unless the mouse is inside.  The net
46 // effect is that our "fade out" when the mouse leaves becaumes
47 // instantaneous.  When I "fixed" it things looked horrible; the
48 // hover-overed bookmark button would stay highlit for 0.4 seconds
49 // which felt like latency/lag.  I'm leaving the "bug" in place for
50 // now so we don't suck.  -jrg
51 static const NSTimeInterval kAnimationHideDuration = 0.4;
53 static const NSTimeInterval kAnimationContinuousCycleDuration = 0.4;
55 @implementation GradientButtonCell
57 @synthesize hoverAlpha = hoverAlpha_;
59 // For nib instantiations
60 - (id)initWithCoder:(NSCoder*)decoder {
61   if ((self = [super initWithCoder:decoder])) {
62     [self sharedInit];
63   }
64   return self;
67 // For programmatic instantiations
68 - (id)initTextCell:(NSString*)string {
69   if ((self = [super initTextCell:string])) {
70     [self sharedInit];
71   }
72   return self;
75 - (void)dealloc {
76   if (trackingArea_) {
77     [[self controlView] removeTrackingArea:trackingArea_];
78     trackingArea_.reset();
79   }
80   [super dealloc];
83 // Return YES if we are pulsing (towards another state or continuously).
84 - (BOOL)pulsing {
85   if ((pulseState_ == gradient_button_cell::kPulsingOn) ||
86       (pulseState_ == gradient_button_cell::kPulsingOff) ||
87       (pulseState_ == gradient_button_cell::kPulsingContinuous))
88     return YES;
89   return NO;
92 // Perform one pulse step when animating a pulse.
93 - (void)performOnePulseStep {
94   NSTimeInterval thisUpdate = [NSDate timeIntervalSinceReferenceDate];
95   NSTimeInterval elapsed = thisUpdate - lastHoverUpdate_;
96   CGFloat opacity = [self hoverAlpha];
98   // Update opacity based on state.
99   // Adjust state if we have finished.
100   switch (pulseState_) {
101   case gradient_button_cell::kPulsingOn:
102     opacity += elapsed / kAnimationShowDuration;
103     if (opacity > 1.0) {
104       [self setPulseState:gradient_button_cell::kPulsedOn];
105       return;
106     }
107     break;
108   case gradient_button_cell::kPulsingOff:
109     opacity -= elapsed / kAnimationHideDuration;
110     if (opacity < 0.0) {
111       [self setPulseState:gradient_button_cell::kPulsedOff];
112       return;
113     }
114     break;
115   case gradient_button_cell::kPulsingContinuous:
116     opacity += elapsed / kAnimationContinuousCycleDuration * pulseMultiplier_;
117     if (opacity > 1.0) {
118       opacity = 1.0;
119       pulseMultiplier_ *= -1.0;
120     } else if (opacity < 0.0) {
121       opacity = 0.0;
122       pulseMultiplier_ *= -1.0;
123     }
124     outerStrokeAlphaMult_ = opacity;
125     break;
126   default:
127     NOTREACHED() << "unknown pulse state";
128   }
130   // Update our control.
131   lastHoverUpdate_ = thisUpdate;
132   [self setHoverAlpha:opacity];
133   [[self controlView] setNeedsDisplay:YES];
135   // If our state needs it, keep going.
136   if ([self pulsing]) {
137     [self performSelector:_cmd withObject:nil afterDelay:0.02];
138   }
141 - (gradient_button_cell::PulseState)pulseState {
142   return pulseState_;
145 // Set the pulsing state.  This can either set the pulse to on or off
146 // immediately (e.g. kPulsedOn, kPulsedOff) or initiate an animated
147 // state change.
148 - (void)setPulseState:(gradient_button_cell::PulseState)pstate {
149   pulseState_ = pstate;
150   pulseMultiplier_ = 0.0;
151   [NSObject cancelPreviousPerformRequestsWithTarget:self];
152   lastHoverUpdate_ = [NSDate timeIntervalSinceReferenceDate];
154   switch (pstate) {
155   case gradient_button_cell::kPulsedOn:
156   case gradient_button_cell::kPulsedOff:
157     outerStrokeAlphaMult_ = 1.0;
158     [self setHoverAlpha:((pulseState_ == gradient_button_cell::kPulsedOn) ?
159                          1.0 : 0.0)];
160     [[self controlView] setNeedsDisplay:YES];
161     break;
162   case gradient_button_cell::kPulsingOn:
163   case gradient_button_cell::kPulsingOff:
164     outerStrokeAlphaMult_ = 1.0;
165     // Set initial value then engage timer.
166     [self setHoverAlpha:((pulseState_ == gradient_button_cell::kPulsingOn) ?
167                          0.0 : 1.0)];
168     [self performOnePulseStep];
169     break;
170   case gradient_button_cell::kPulsingContinuous:
171     // Semantics of continuous pulsing are that we pulse independent
172     // of mouse position.
173     pulseMultiplier_ = 1.0;
174     [self performOnePulseStep];
175     break;
176   default:
177     CHECK(0);
178     break;
179   }
182 - (void)safelyStopPulsing {
183   [NSObject cancelPreviousPerformRequestsWithTarget:self];
186 - (void)setIsContinuousPulsing:(BOOL)continuous {
187   if (!continuous && pulseState_ != gradient_button_cell::kPulsingContinuous)
188     return;
189   if (continuous) {
190     [self setPulseState:gradient_button_cell::kPulsingContinuous];
191   } else {
192     [self setPulseState:(isMouseInside_ ? gradient_button_cell::kPulsedOn :
193                          gradient_button_cell::kPulsedOff)];
194   }
197 - (BOOL)isContinuousPulsing {
198   return (pulseState_ == gradient_button_cell::kPulsingContinuous) ?
199       YES : NO;
202 #if 1
203 // If we are not continuously pulsing, perform a pulse animation to
204 // reflect our new state.
205 - (void)setMouseInside:(BOOL)flag animate:(BOOL)animated {
206   isMouseInside_ = flag;
207   if (pulseState_ != gradient_button_cell::kPulsingContinuous) {
208     if (animated) {
209       [self setPulseState:(isMouseInside_ ? gradient_button_cell::kPulsingOn :
210                            gradient_button_cell::kPulsingOff)];
211     } else {
212       [self setPulseState:(isMouseInside_ ? gradient_button_cell::kPulsedOn :
213                            gradient_button_cell::kPulsedOff)];
214     }
215   }
217 #else
219 - (void)adjustHoverValue {
220   NSTimeInterval thisUpdate = [NSDate timeIntervalSinceReferenceDate];
222   NSTimeInterval elapsed = thisUpdate - lastHoverUpdate_;
224   CGFloat opacity = [self hoverAlpha];
225   if (isMouseInside_) {
226     opacity += elapsed / kAnimationShowDuration;
227   } else {
228     opacity -= elapsed / kAnimationHideDuration;
229   }
231   if (!isMouseInside_ && opacity < 0) {
232     opacity = 0;
233   } else if (isMouseInside_ && opacity > 1) {
234     opacity = 1;
235   } else {
236     [self performSelector:_cmd withObject:nil afterDelay:0.02];
237   }
238   lastHoverUpdate_ = thisUpdate;
239   [self setHoverAlpha:opacity];
241   [[self controlView] setNeedsDisplay:YES];
244 - (void)setMouseInside:(BOOL)flag animate:(BOOL)animated {
245   isMouseInside_ = flag;
246   if (animated) {
247     lastHoverUpdate_ = [NSDate timeIntervalSinceReferenceDate];
248     [self adjustHoverValue];
249   } else {
250     [NSObject cancelPreviousPerformRequestsWithTarget:self];
251     [self setHoverAlpha:flag ? 1.0 : 0.0];
252   }
253   [[self controlView] setNeedsDisplay:YES];
258 #endif
260 - (NSGradient*)gradientForHoverAlpha:(CGFloat)hoverAlpha
261                             isThemed:(BOOL)themed {
262   CGFloat startAlpha = 0.6 + 0.3 * hoverAlpha;
263   CGFloat endAlpha = 0.333 * hoverAlpha;
265   if (themed) {
266     startAlpha = 0.2 + 0.35 * hoverAlpha;
267     endAlpha = 0.333 * hoverAlpha;
268   }
270   NSColor* startColor =
271       [NSColor colorWithCalibratedWhite:1.0
272                                   alpha:startAlpha];
273   NSColor* endColor =
274       [NSColor colorWithCalibratedWhite:1.0 - 0.15 * hoverAlpha
275                                   alpha:endAlpha];
276   NSGradient* gradient = [[NSGradient alloc] initWithColorsAndLocations:
277                           startColor, hoverAlpha * 0.33,
278                           endColor, 1.0, nil];
280   return [gradient autorelease];
283 - (void)sharedInit {
284   shouldTheme_ = YES;
285   pulseState_ = gradient_button_cell::kPulsedOff;
286   pulseMultiplier_ = 1.0;
287   outerStrokeAlphaMult_ = 1.0;
288   gradient_.reset([[self gradientForHoverAlpha:0.0 isThemed:NO] retain]);
291 - (void)setShouldTheme:(BOOL)shouldTheme {
292   shouldTheme_ = shouldTheme;
295 - (NSImage*)overlayImage {
296   return overlayImage_.get();
299 - (void)setOverlayImage:(NSImage*)image {
300   overlayImage_.reset([image retain]);
301   [[self controlView] setNeedsDisplay:YES];
304 - (NSBackgroundStyle)interiorBackgroundStyle {
305   // Never lower the interior, since that just leads to a weird shadow which can
306   // often interact badly with the theme.
307   return NSBackgroundStyleRaised;
310 - (void)mouseEntered:(NSEvent*)theEvent {
311   [self setMouseInside:YES animate:YES];
314 - (void)mouseExited:(NSEvent*)theEvent {
315   [self setMouseInside:NO animate:YES];
318 - (BOOL)isMouseInside {
319   return trackingArea_ && isMouseInside_;
322 // Since we have our own drawWithFrame:, we need to also have our own
323 // logic for determining when the mouse is inside for honoring this
324 // request.
325 - (void)setShowsBorderOnlyWhileMouseInside:(BOOL)showOnly {
326   [super setShowsBorderOnlyWhileMouseInside:showOnly];
327   if (showOnly) {
328     [self updateTrackingAreas];
329   } else {
330     if (trackingArea_) {
331       [[self controlView] removeTrackingArea:trackingArea_];
332       trackingArea_.reset(nil);
333       if (isMouseInside_) {
334         isMouseInside_ = NO;
335         [[self controlView] setNeedsDisplay:YES];
336       }
337     }
338   }
341 // TODO(viettrungluu): clean up/reorganize.
342 - (void)drawBorderAndFillForTheme:(ui::ThemeProvider*)themeProvider
343                       controlView:(NSView*)controlView
344                         innerPath:(NSBezierPath*)innerPath
345               showClickedGradient:(BOOL)showClickedGradient
346             showHighlightGradient:(BOOL)showHighlightGradient
347                        hoverAlpha:(CGFloat)hoverAlpha
348                            active:(BOOL)active
349                         cellFrame:(NSRect)cellFrame
350                   defaultGradient:(NSGradient*)defaultGradient {
351   BOOL isFlatButton = [self showsBorderOnlyWhileMouseInside];
353   // For flat (unbordered when not hovered) buttons, never use the toolbar
354   // button background image, but the modest gradient used for themed buttons.
355   // To make things even more modest, scale the hover alpha down by 40 percent
356   // unless clicked.
357   NSColor* backgroundImageColor;
358   BOOL useThemeGradient;
359   if (isFlatButton) {
360     backgroundImageColor = nil;
361     useThemeGradient = YES;
362     if (!showClickedGradient)
363       hoverAlpha *= 0.6;
364   } else {
365     backgroundImageColor = nil;
366     if (themeProvider &&
367         themeProvider->HasCustomImage(IDR_THEME_BUTTON_BACKGROUND)) {
368       backgroundImageColor =
369           themeProvider->GetNSImageColorNamed(IDR_THEME_BUTTON_BACKGROUND);
370     }
371     useThemeGradient = backgroundImageColor ? YES : NO;
372   }
374   // The basic gradient shown inside; see above.
375   NSGradient* gradient;
376   if (hoverAlpha == 0 && !useThemeGradient) {
377     gradient = defaultGradient ? defaultGradient
378                                : gradient_;
379   } else {
380     gradient = [self gradientForHoverAlpha:hoverAlpha
381                                   isThemed:useThemeGradient];
382   }
384   // If we're drawing a background image, show that; else possibly show the
385   // clicked gradient.
386   if (backgroundImageColor) {
387     [backgroundImageColor set];
388     // Set the phase to match window.
389     NSRect trueRect = [controlView convertRect:cellFrame toView:nil];
390     [[NSGraphicsContext currentContext]
391         cr_setPatternPhase:NSMakePoint(NSMinX(trueRect), NSMaxY(trueRect))
392                    forView:controlView];
393     [innerPath fill];
394   } else {
395     if (showClickedGradient) {
396       NSGradient* clickedGradient = nil;
397       if (isFlatButton &&
398           [self tag] == kStandardButtonTypeWithLimitedClickFeedback) {
399         clickedGradient = gradient;
400       } else {
401         clickedGradient = themeProvider ? themeProvider->GetNSGradient(
402             active ?
403                 ThemeProperties::GRADIENT_TOOLBAR_BUTTON_PRESSED :
404                 ThemeProperties::GRADIENT_TOOLBAR_BUTTON_PRESSED_INACTIVE) :
405             nil;
406       }
407       [clickedGradient drawInBezierPath:innerPath angle:90.0];
408     }
409   }
411   // Visually indicate unclicked, enabled buttons.
412   if (!showClickedGradient && [self isEnabled]) {
413     gfx::ScopedNSGraphicsContextSaveGState scopedGState;
414     [innerPath addClip];
416     // Draw the inner glow.
417     if (hoverAlpha > 0) {
418       [innerPath setLineWidth:2];
419       [[NSColor colorWithCalibratedWhite:1.0 alpha:0.2 * hoverAlpha] setStroke];
420       [innerPath stroke];
421     }
423     // Draw the top inner highlight.
424     NSAffineTransform* highlightTransform = [NSAffineTransform transform];
425     [highlightTransform translateXBy:1 yBy:1];
426     base::scoped_nsobject<NSBezierPath> highlightPath([innerPath copy]);
427     [highlightPath transformUsingAffineTransform:highlightTransform];
428     [[NSColor colorWithCalibratedWhite:1.0 alpha:0.2] setStroke];
429     [highlightPath stroke];
431     // Draw the gradient inside.
432     [gradient drawInBezierPath:innerPath angle:90.0];
433   }
435   // Don't draw anything else for disabled flat buttons.
436   if (isFlatButton && ![self isEnabled])
437     return;
439   // Draw the outer stroke.
440   NSColor* strokeColor = nil;
441   if (showClickedGradient) {
442     strokeColor = [NSColor
443                     colorWithCalibratedWhite:0.0
444                                        alpha:0.3 * outerStrokeAlphaMult_];
445   } else {
446     strokeColor = themeProvider ? themeProvider->GetNSColor(
447         active ? ThemeProperties::COLOR_TOOLBAR_BUTTON_STROKE :
448                  ThemeProperties::COLOR_TOOLBAR_BUTTON_STROKE_INACTIVE) :
449         [NSColor colorWithCalibratedWhite:0.0
450                                     alpha:0.3 * outerStrokeAlphaMult_];
451   }
452   [strokeColor setStroke];
454   [innerPath setLineWidth:1];
455   [innerPath stroke];
458 // TODO(viettrungluu): clean this up.
459 // (Private)
460 - (void)getDrawParamsForFrame:(NSRect)cellFrame
461                        inView:(NSView*)controlView
462                    innerFrame:(NSRect*)returnInnerFrame
463                     innerPath:(NSBezierPath**)returnInnerPath
464                      clipPath:(NSBezierPath**)returnClipPath {
465   const CGFloat lineWidth = [controlView cr_lineWidth];
466   const CGFloat halfLineWidth = lineWidth / 2.0;
468   // Constants from Cole.  Will kConstant them once the feedback loop
469   // is complete.
470   NSRect drawFrame = NSInsetRect(cellFrame, 1.5 * lineWidth, 1.5 * lineWidth);
471   NSRect innerFrame = NSInsetRect(cellFrame, lineWidth, lineWidth);
472   const CGFloat radius = 3;
474   ButtonType type = [[(NSControl*)controlView cell] tag];
475   switch (type) {
476     case kMiddleButtonType:
477       drawFrame.size.width += 20;
478       innerFrame.size.width += 2;
479       // Fallthrough
480     case kRightButtonType:
481       drawFrame.origin.x -= 20;
482       innerFrame.origin.x -= 2;
483       // Fallthrough
484     case kLeftButtonType:
485     case kLeftButtonWithShadowType:
486       drawFrame.size.width += 20;
487       innerFrame.size.width += 2;
488     default:
489       break;
490   }
491   if (type == kLeftButtonWithShadowType)
492     innerFrame.size.width -= 1.0;
494   // Return results if |return...| not null.
495   if (returnInnerFrame)
496     *returnInnerFrame = innerFrame;
497   if (returnInnerPath) {
498     DCHECK(*returnInnerPath == nil);
499     *returnInnerPath = [NSBezierPath bezierPathWithRoundedRect:drawFrame
500                                                        xRadius:radius
501                                                        yRadius:radius];
502     [*returnInnerPath setLineWidth:lineWidth];
503   }
504   if (returnClipPath) {
505     DCHECK(*returnClipPath == nil);
506     NSRect clipPathRect =
507         NSInsetRect(drawFrame, -halfLineWidth, -halfLineWidth);
508     *returnClipPath = [NSBezierPath
509         bezierPathWithRoundedRect:clipPathRect
510                           xRadius:radius + halfLineWidth
511                           yRadius:radius + halfLineWidth];
512   }
515 // TODO(viettrungluu): clean this up.
516 - (void)drawWithFrame:(NSRect)cellFrame inView:(NSView*)controlView {
517   NSRect innerFrame;
518   NSBezierPath* innerPath = nil;
519   [self getDrawParamsForFrame:cellFrame
520                        inView:controlView
521                    innerFrame:&innerFrame
522                     innerPath:&innerPath
523                      clipPath:NULL];
525   BOOL pressed = ([((NSControl*)[self controlView]) isEnabled] &&
526                   [self isHighlighted]);
527   NSWindow* window = [controlView window];
528   ui::ThemeProvider* themeProvider = [window themeProvider];
529   BOOL active = [window isKeyWindow] || [window isMainWindow];
531   // Stroke the borders and appropriate fill gradient. If we're borderless, the
532   // only time we want to draw the inner gradient is if we're highlighted or if
533   // we're the first responder (when "Full Keyboard Access" is turned on).
534   if (([self isBordered] && ![self showsBorderOnlyWhileMouseInside]) ||
535       pressed ||
536       [self isMouseInside] ||
537       [self isContinuousPulsing] ||
538       [self showsFirstResponder]) {
540     // When pulsing we want the bookmark to stand out a little more.
541     BOOL showClickedGradient = pressed ||
542         (pulseState_ == gradient_button_cell::kPulsingContinuous);
544     [self drawBorderAndFillForTheme:themeProvider
545                         controlView:controlView
546                           innerPath:innerPath
547                 showClickedGradient:showClickedGradient
548               showHighlightGradient:[self isHighlighted]
549                          hoverAlpha:[self hoverAlpha]
550                              active:active
551                           cellFrame:cellFrame
552                     defaultGradient:nil];
553   }
555   // If this is the left side of a segmented button, draw a slight shadow.
556   ButtonType type = [[(NSControl*)controlView cell] tag];
557   if (type == kLeftButtonWithShadowType) {
558     const CGFloat lineWidth = [controlView cr_lineWidth];
559     NSRect borderRect, contentRect;
560     NSDivideRect(cellFrame, &borderRect, &contentRect, lineWidth, NSMaxXEdge);
561     NSColor* stroke = themeProvider ? themeProvider->GetNSColor(
562         active ? ThemeProperties::COLOR_TOOLBAR_BUTTON_STROKE :
563                  ThemeProperties::COLOR_TOOLBAR_BUTTON_STROKE_INACTIVE) :
564         [NSColor blackColor];
566     [[stroke colorWithAlphaComponent:0.2] set];
567     NSRectFillUsingOperation(NSInsetRect(borderRect, 0, 2),
568                              NSCompositeSourceOver);
569   }
570   [self drawInteriorWithFrame:innerFrame inView:controlView];
572   // Draws the blue focus ring.
573   if ([self showsFirstResponder]) {
574     gfx::ScopedNSGraphicsContextSaveGState scoped_state;
575     const CGFloat lineWidth = [controlView cr_lineWidth];
576     // insetX = 1.0 is used for the drawing of blue highlight so that this
577     // highlight won't be too near the bookmark toolbar itself, in case we
578     // draw bookmark buttons in bookmark toolbar.
579     rect_path_utils::FrameRectWithInset(rect_path_utils::RoundedCornerAll,
580                                         NSInsetRect(cellFrame, 0, lineWidth),
581                                         1.0,            // insetX
582                                         0.0,            // insetY
583                                         3.0,            // outerRadius
584                                         lineWidth * 2,  // lineWidth
585                                         [controlView
586                                             cr_keyboardFocusIndicatorColor]);
587   }
590 - (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView*)controlView {
591   const CGFloat lineWidth = [controlView cr_lineWidth];
593   if (shouldTheme_) {
594     BOOL isTemplate = [[self image] isTemplate];
596     gfx::ScopedNSGraphicsContextSaveGState scopedGState;
598     CGContextRef context =
599         (CGContextRef)([[NSGraphicsContext currentContext] graphicsPort]);
601     ThemeService* themeProvider = static_cast<ThemeService*>(
602         [[controlView window] themeProvider]);
603     NSColor* color = themeProvider ?
604         themeProvider->GetNSColorTint(ThemeProperties::TINT_BUTTONS) :
605         [NSColor blackColor];
607     if (isTemplate && themeProvider && themeProvider->UsingDefaultTheme()) {
608       base::scoped_nsobject<NSShadow> shadow([[NSShadow alloc] init]);
609       [shadow.get() setShadowColor:themeProvider->GetNSColor(
610           ThemeProperties::COLOR_TOOLBAR_BEZEL)];
611       [shadow.get() setShadowOffset:NSMakeSize(0.0, -lineWidth)];
612       [shadow setShadowBlurRadius:lineWidth];
613       [shadow set];
614     }
616     CGContextBeginTransparencyLayer(context, 0);
617     NSRect imageRect = NSZeroRect;
618     imageRect.size = [[self image] size];
619     NSRect drawRect = [self imageRectForBounds:cellFrame];
620     [[self image] drawInRect:drawRect
621                     fromRect:imageRect
622                    operation:NSCompositeSourceOver
623                     fraction:[self isEnabled] ? 1.0 : 0.5
624               respectFlipped:YES
625                        hints:nil];
626     if (isTemplate && color) {
627       [color set];
628       NSRectFillUsingOperation(cellFrame, NSCompositeSourceAtop);
629     }
630     CGContextEndTransparencyLayer(context);
631   } else {
632     // NSCell draws these off-center for some reason, probably because of the
633     // positioning of the control in the xib.
634     [super drawInteriorWithFrame:NSOffsetRect(cellFrame, 0, lineWidth)
635                           inView:controlView];
636   }
638   if (overlayImage_) {
639     NSRect imageRect = NSZeroRect;
640     imageRect.size = [overlayImage_ size];
641     [overlayImage_ drawInRect:[self imageRectForBounds:cellFrame]
642                      fromRect:imageRect
643                     operation:NSCompositeSourceOver
644                      fraction:[self isEnabled] ? 1.0 : 0.5
645                respectFlipped:YES
646                         hints:nil];
647   }
650 - (int)verticalTextOffset {
651   return 1;
654 // Overriden from NSButtonCell so we can display a nice fadeout effect for
655 // button titles that overflow.
656 // This method is copied in the most part from GTMFadeTruncatingTextFieldCell,
657 // the only difference is that here we draw the text ourselves rather than
658 // calling the super to do the work.
659 // We can't use GTMFadeTruncatingTextFieldCell because there's no easy way to
660 // get it to work with NSButtonCell.
661 // TODO(jeremy): Move this to GTM.
662 - (NSRect)drawTitle:(NSAttributedString*)title
663           withFrame:(NSRect)cellFrame
664              inView:(NSView*)controlView {
665   NSSize size = [title size];
667   // Empirically, Cocoa will draw an extra 2 pixels past NSWidth(cellFrame)
668   // before it clips the text.
669   const CGFloat kOverflowBeforeClip = 2;
670   BOOL clipping = YES;
671   if (std::floor(size.width) <= (NSWidth(cellFrame) + kOverflowBeforeClip)) {
672     cellFrame.origin.y += ([self verticalTextOffset] - 1);
673     clipping = NO;
674   }
676   // Gradient is about twice our line height long.
677   CGFloat gradientWidth = MIN(size.height * 2, NSWidth(cellFrame) / 4);
679   NSRect solidPart, gradientPart;
680   NSDivideRect(cellFrame, &gradientPart, &solidPart, gradientWidth, NSMaxXEdge);
682   // Draw non-gradient part without transparency layer, as light text on a dark
683   // background looks bad with a gradient layer.
684   NSPoint textOffset = NSZeroPoint;
685   {
686     gfx::ScopedNSGraphicsContextSaveGState scopedGState;
687     if (clipping)
688       [NSBezierPath clipRect:solidPart];
690     // 11 is the magic number needed to make this match the native
691     // NSButtonCell's label display.
692     CGFloat textLeft = [[self image] size].width + 11;
694     // For some reason, the height of cellFrame as passed in is totally bogus.
695     // For vertical centering purposes, we need the bounds of the containing
696     // view.
697     NSRect buttonFrame = [[self controlView] frame];
699     // Call the vertical offset to match native NSButtonCell's version.
700     textOffset = NSMakePoint(textLeft,
701                              (NSHeight(buttonFrame) - size.height) / 2 +
702                              [self verticalTextOffset]);
703     [title drawAtPoint:textOffset];
704   }
706   if (!clipping)
707     return cellFrame;
709   // Draw the gradient part with a transparency layer. This makes the text look
710   // suboptimal, but since it fades out, that's ok.
711   gfx::ScopedNSGraphicsContextSaveGState scopedGState;
712   [NSBezierPath clipRect:gradientPart];
713   CGContextRef context = static_cast<CGContextRef>(
714       [[NSGraphicsContext currentContext] graphicsPort]);
715   CGContextBeginTransparencyLayerWithRect(context,
716                                           NSRectToCGRect(gradientPart), 0);
717   [title drawAtPoint:textOffset];
719   NSColor *color = [NSColor textColor];
720   NSColor *alphaColor = [color colorWithAlphaComponent:0.0];
721   NSGradient *mask = [[NSGradient alloc] initWithStartingColor:color
722                                                    endingColor:alphaColor];
724   // Draw the gradient mask
725   CGContextSetBlendMode(context, kCGBlendModeDestinationIn);
726   [mask drawFromPoint:NSMakePoint(NSMaxX(cellFrame) - gradientWidth,
727                                   NSMinY(cellFrame))
728               toPoint:NSMakePoint(NSMaxX(cellFrame),
729                                   NSMinY(cellFrame))
730               options:NSGradientDrawsBeforeStartingLocation];
731   [mask release];
732   CGContextEndTransparencyLayer(context);
734   return cellFrame;
737 - (NSBezierPath*)clipPathForFrame:(NSRect)cellFrame
738                            inView:(NSView*)controlView {
739   NSBezierPath* boundingPath = nil;
740   [self getDrawParamsForFrame:cellFrame
741                        inView:controlView
742                    innerFrame:NULL
743                     innerPath:NULL
744                      clipPath:&boundingPath];
745   return boundingPath;
748 - (void)resetCursorRect:(NSRect)cellFrame inView:(NSView*)controlView {
749   [super resetCursorRect:cellFrame inView:controlView];
750   if (trackingArea_)
751     [self updateTrackingAreas];
754 - (BOOL)isMouseReallyInside {
755   BOOL mouseInView = NO;
756   NSView* controlView = [self controlView];
757   NSWindow* window = [controlView window];
758   NSRect bounds = [controlView bounds];
759   if (window) {
760     NSPoint mousePoint = [window mouseLocationOutsideOfEventStream];
761     mousePoint = [controlView convertPoint:mousePoint fromView:nil];
762     mouseInView = [controlView mouse:mousePoint inRect:bounds];
763   }
764   return mouseInView;
767 - (void)updateTrackingAreas {
768   NSView* controlView = [self controlView];
769   BOOL mouseInView = [self isMouseReallyInside];
771   if (trackingArea_.get())
772     [controlView removeTrackingArea:trackingArea_];
774   NSTrackingAreaOptions options = NSTrackingMouseEnteredAndExited |
775                                   NSTrackingActiveInActiveApp;
776   if (mouseInView)
777     options |= NSTrackingAssumeInside;
779   trackingArea_.reset([[NSTrackingArea alloc]
780                         initWithRect:[controlView bounds]
781                              options:options
782                                owner:self
783                             userInfo:nil]);
784   if (isMouseInside_ != mouseInView) {
785     [self setMouseInside:mouseInView animate:NO];
786     [controlView setNeedsDisplay:YES];
787   }
790 @end