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