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