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 42 static 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 51 static const NSTimeInterval kAnimationHideDuration = 0.4; 52 53 static 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