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