Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 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" |
Ben Murdoch | eb525c5 | 2013-07-10 11:40:50 +0100 | [diff] [blame] | 10 | #import "base/mac/scoped_nsobject.h" |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 11 | #import "chrome/browser/themes/theme_properties.h" |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 12 | #import "chrome/browser/themes/theme_service.h" |
| 13 | #import "chrome/browser/ui/cocoa/nsview_additions.h" |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 14 | #import "chrome/browser/ui/cocoa/rect_path_utils.h" |
Ben Murdoch | eb525c5 | 2013-07-10 11:40:50 +0100 | [diff] [blame] | 15 | #import "chrome/browser/ui/cocoa/themed_window.h" |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 16 | #include "grit/theme_resources.h" |
| 17 | #import "third_party/GTM/AppKit/GTMNSColor+Luminance.h" |
Torne (Richard Coles) | 7d4cd47 | 2013-06-19 11:58:07 +0100 | [diff] [blame] | 18 | #import "ui/base/cocoa/nsgraphics_context_additions.h" |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 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 { |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 365 | backgroundImageColor = nil; |
| 366 | if (themeProvider && |
| 367 | themeProvider->HasCustomImage(IDR_THEME_BUTTON_BACKGROUND)) { |
| 368 | backgroundImageColor = |
| 369 | themeProvider->GetNSImageColorNamed(IDR_THEME_BUTTON_BACKGROUND); |
| 370 | } |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 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] |
Torne (Richard Coles) | 7d4cd47 | 2013-06-19 11:58:07 +0100 | [diff] [blame] | 391 | cr_setPatternPhase:NSMakePoint(NSMinX(trueRect), NSMaxY(trueRect)) |
| 392 | forView:controlView]; |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 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 ? |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 403 | ThemeProperties::GRADIENT_TOOLBAR_BUTTON_PRESSED : |
| 404 | ThemeProperties::GRADIENT_TOOLBAR_BUTTON_PRESSED_INACTIVE) : |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 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]; |
Ben Murdoch | eb525c5 | 2013-07-10 11:40:50 +0100 | [diff] [blame] | 426 | base::scoped_nsobject<NSBezierPath> highlightPath([innerPath copy]); |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 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( |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 447 | active ? ThemeProperties::COLOR_TOOLBAR_BUTTON_STROKE : |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 448 | ThemeProperties::COLOR_TOOLBAR_BUTTON_STROKE_INACTIVE) : |
| 449 | [NSColor colorWithCalibratedWhite:0.0 |
| 450 | alpha:0.3 * outerStrokeAlphaMult_]; |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 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); |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 472 | const CGFloat radius = 3; |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 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 | |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 544 | [self drawBorderAndFillForTheme:themeProvider |
| 545 | controlView:controlView |
| 546 | innerPath:innerPath |
| 547 | showClickedGradient:showClickedGradient |
| 548 | showHighlightGradient:[self isHighlighted] |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 549 | hoverAlpha:[self hoverAlpha] |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 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( |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 562 | active ? ThemeProperties::COLOR_TOOLBAR_BUTTON_STROKE : |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 563 | ThemeProperties::COLOR_TOOLBAR_BUTTON_STROKE_INACTIVE) : |
| 564 | [NSColor blackColor]; |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 565 | |
| 566 | [[stroke colorWithAlphaComponent:0.2] set]; |
| 567 | NSRectFillUsingOperation(NSInsetRect(borderRect, 0, 2), |
| 568 | NSCompositeSourceOver); |
| 569 | } |
| 570 | [self drawInteriorWithFrame:innerFrame inView:controlView]; |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 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 | } |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 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 ? |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 604 | themeProvider->GetNSColorTint(ThemeProperties::TINT_BUTTONS) : |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 605 | [NSColor blackColor]; |
| 606 | |
| 607 | if (isTemplate && themeProvider && themeProvider->UsingDefaultTheme()) { |
Ben Murdoch | eb525c5 | 2013-07-10 11:40:50 +0100 | [diff] [blame] | 608 | base::scoped_nsobject<NSShadow> shadow([[NSShadow alloc] init]); |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 609 | [shadow.get() setShadowColor:themeProvider->GetNSColor( |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 610 | ThemeProperties::COLOR_TOOLBAR_BEZEL)]; |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 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 |