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.
4
5#include "chrome/browser/ui/cocoa/gradient_button_cell.h"
6
7#include <cmath>
8
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/rect_path_utils.h"
14#import "chrome/browser/ui/cocoa/themed_window.h"
15#include "grit/theme_resources.h"
16#import "third_party/google_toolbox_for_mac/src/AppKit/GTMNSColor+Luminance.h"
17#import "ui/base/cocoa/nsgraphics_context_additions.h"
18#import "ui/base/cocoa/nsview_additions.h"
19#include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h"
20
21@interface GradientButtonCell (Private)
22- (void)sharedInit;
23
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;
36
37- (void)updateTrackingAreas;
38
39@end
40
41
42static const NSTimeInterval kAnimationShowDuration = 0.2;
43
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
51static const NSTimeInterval kAnimationHideDuration = 0.4;
52
53static const NSTimeInterval kAnimationContinuousCycleDuration = 0.4;
54
55@implementation GradientButtonCell
56
57@synthesize hoverAlpha = hoverAlpha_;
58
59// For nib instantiations
60- (id)initWithCoder:(NSCoder*)decoder {
61  if ((self = [super initWithCoder:decoder])) {
62    [self sharedInit];
63  }
64  return self;
65}
66
67// For programmatic instantiations
68- (id)initTextCell:(NSString*)string {
69  if ((self = [super initTextCell:string])) {
70    [self sharedInit];
71  }
72  return self;
73}
74
75- (void)dealloc {
76  if (trackingArea_) {
77    [[self controlView] removeTrackingArea:trackingArea_];
78    trackingArea_.reset();
79  }
80  [super dealloc];
81}
82
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;
90}
91
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];
97
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  }
129
130  // Update our control.
131  lastHoverUpdate_ = thisUpdate;
132  [self setHoverAlpha:opacity];
133  [[self controlView] setNeedsDisplay:YES];
134
135  // If our state needs it, keep going.
136  if ([self pulsing]) {
137    [self performSelector:_cmd withObject:nil afterDelay:0.02];
138  }
139}
140
141- (gradient_button_cell::PulseState)pulseState {
142  return pulseState_;
143}
144
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];
153
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  }
180}
181
182- (void)safelyStopPulsing {
183  [NSObject cancelPreviousPerformRequestsWithTarget:self];
184}
185
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  }
195}
196
197- (BOOL)isContinuousPulsing {
198  return (pulseState_ == gradient_button_cell::kPulsingContinuous) ?
199      YES : NO;
200}
201
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  }
216}
217#else
218
219- (void)adjustHoverValue {
220  NSTimeInterval thisUpdate = [NSDate timeIntervalSinceReferenceDate];
221
222  NSTimeInterval elapsed = thisUpdate - lastHoverUpdate_;
223
224  CGFloat opacity = [self hoverAlpha];
225  if (isMouseInside_) {
226    opacity += elapsed / kAnimationShowDuration;
227  } else {
228    opacity -= elapsed / kAnimationHideDuration;
229  }
230
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];
240
241  [[self controlView] setNeedsDisplay:YES];
242}
243
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];
254}
255
256
257
258#endif
259
260- (NSGradient*)gradientForHoverAlpha:(CGFloat)hoverAlpha
261                            isThemed:(BOOL)themed {
262  CGFloat startAlpha = 0.6 + 0.3 * hoverAlpha;
263  CGFloat endAlpha = 0.333 * hoverAlpha;
264
265  if (themed) {
266    startAlpha = 0.2 + 0.35 * hoverAlpha;
267    endAlpha = 0.333 * hoverAlpha;
268  }
269
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];
279
280  return [gradient autorelease];
281}
282
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]);
289}
290
291- (void)setShouldTheme:(BOOL)shouldTheme {
292  shouldTheme_ = shouldTheme;
293}
294
295- (NSImage*)overlayImage {
296  return overlayImage_.get();
297}
298
299- (void)setOverlayImage:(NSImage*)image {
300  overlayImage_.reset([image retain]);
301  [[self controlView] setNeedsDisplay:YES];
302}
303
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;
308}
309
310- (void)mouseEntered:(NSEvent*)theEvent {
311  [self setMouseInside:YES animate:YES];
312}
313
314- (void)mouseExited:(NSEvent*)theEvent {
315  [self setMouseInside:NO animate:YES];
316}
317
318- (BOOL)isMouseInside {
319  return trackingArea_ && isMouseInside_;
320}
321
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  }
339}
340
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];
352
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  }
373
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  }
383
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  }
410
411  // Visually indicate unclicked, enabled buttons.
412  if (!showClickedGradient && [self isEnabled]) {
413    gfx::ScopedNSGraphicsContextSaveGState scopedGState;
414    [innerPath addClip];
415
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    }
422
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];
430
431    // Draw the gradient inside.
432    [gradient drawInBezierPath:innerPath angle:90.0];
433  }
434
435  // Don't draw anything else for disabled flat buttons.
436  if (isFlatButton && ![self isEnabled])
437    return;
438
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];
453
454  [innerPath setLineWidth:1];
455  [innerPath stroke];
456}
457
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;
467
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;
473
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;
493
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  }
513}
514
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];
524
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];
530
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]) {
539
540    // When pulsing we want the bookmark to stand out a little more.
541    BOOL showClickedGradient = pressed ||
542        (pulseState_ == gradient_button_cell::kPulsingContinuous);
543
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  }
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 ? ThemeProperties::COLOR_TOOLBAR_BUTTON_STROKE :
563                 ThemeProperties::COLOR_TOOLBAR_BUTTON_STROKE_INACTIVE) :
564        [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  // 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  }
588}
589
590- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView*)controlView {
591  const CGFloat lineWidth = [controlView cr_lineWidth];
592
593  if (shouldTheme_) {
594    BOOL isTemplate = [[self image] isTemplate];
595
596    gfx::ScopedNSGraphicsContextSaveGState scopedGState;
597
598    CGContextRef context =
599        (CGContextRef)([[NSGraphicsContext currentContext] graphicsPort]);
600
601    ThemeService* themeProvider = static_cast<ThemeService*>(
602        [[controlView window] themeProvider]);
603    NSColor* color = themeProvider ?
604        themeProvider->GetNSColorTint(ThemeProperties::TINT_BUTTONS) :
605        [NSColor blackColor];
606
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    }
615
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  }
637
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  }
648}
649
650- (int)verticalTextOffset {
651  return 1;
652}
653
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];
666
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  }
675
676  // Gradient is about twice our line height long.
677  CGFloat gradientWidth = MIN(size.height * 2, NSWidth(cellFrame) / 4);
678
679  NSRect solidPart, gradientPart;
680  NSDivideRect(cellFrame, &gradientPart, &solidPart, gradientWidth, NSMaxXEdge);
681
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];
689
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;
693
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];
698
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  }
705
706  if (!clipping)
707    return cellFrame;
708
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];
718
719  NSColor *color = [NSColor textColor];
720  NSColor *alphaColor = [color colorWithAlphaComponent:0.0];
721  NSGradient *mask = [[NSGradient alloc] initWithStartingColor:color
722                                                   endingColor:alphaColor];
723
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);
733
734  return cellFrame;
735}
736
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;
746}
747
748- (void)resetCursorRect:(NSRect)cellFrame inView:(NSView*)controlView {
749  [super resetCursorRect:cellFrame inView:controlView];
750  if (trackingArea_)
751    [self updateTrackingAreas];
752}
753
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;
765}
766
767- (void)updateTrackingAreas {
768  NSView* controlView = [self controlView];
769  BOOL mouseInView = [self isMouseReallyInside];
770
771  if (trackingArea_.get())
772    [controlView removeTrackingArea:trackingArea_];
773
774  NSTrackingAreaOptions options = NSTrackingMouseEnteredAndExited |
775                                  NSTrackingActiveInActiveApp;
776  if (mouseInView)
777    options |= NSTrackingAssumeInside;
778
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  }
788}
789
790@end
791