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