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