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 #import "chrome/browser/ui/cocoa/tabs/tab_view.h" 6 7 #include "base/logging.h" 8 #include "base/mac/sdk_forward_declarations.h" 9 #include "chrome/browser/themes/theme_service.h" 10 #import "chrome/browser/ui/cocoa/nsview_additions.h" 11 #import "chrome/browser/ui/cocoa/tabs/tab_controller.h" 12 #import "chrome/browser/ui/cocoa/tabs/tab_window_controller.h" 13 #import "chrome/browser/ui/cocoa/themed_window.h" 14 #import "chrome/browser/ui/cocoa/view_id_util.h" 15 #include "grit/generated_resources.h" 16 #include "grit/theme_resources.h" 17 #import "ui/base/cocoa/nsgraphics_context_additions.h" 18 #include "ui/base/l10n/l10n_util.h" 19 #include "ui/base/resource/resource_bundle.h" 20 #include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h" 21 22 23 const int kMaskHeight = 29; // Height of the mask bitmap. 24 const int kFillHeight = 25; // Height of the "mask on" part of the mask bitmap. 25 26 // Constants for inset and control points for tab shape. 27 const CGFloat kInsetMultiplier = 2.0/3.0; 28 29 // The amount of time in seconds during which each type of glow increases, holds 30 // steady, and decreases, respectively. 31 const NSTimeInterval kHoverShowDuration = 0.2; 32 const NSTimeInterval kHoverHoldDuration = 0.02; 33 const NSTimeInterval kHoverHideDuration = 0.4; 34 const NSTimeInterval kAlertShowDuration = 0.4; 35 const NSTimeInterval kAlertHoldDuration = 0.4; 36 const NSTimeInterval kAlertHideDuration = 0.4; 37 38 // The default time interval in seconds between glow updates (when 39 // increasing/decreasing). 40 const NSTimeInterval kGlowUpdateInterval = 0.025; 41 42 // This is used to judge whether the mouse has moved during rapid closure; if it 43 // has moved less than the threshold, we want to close the tab. 44 const CGFloat kRapidCloseDist = 2.5; 45 46 @interface TabView(Private) 47 48 - (void)resetLastGlowUpdateTime; 49 - (NSTimeInterval)timeElapsedSinceLastGlowUpdate; 50 - (void)adjustGlowValue; 51 - (CGImageRef)tabClippingMask; 52 53 @end // TabView(Private) 54 55 @implementation TabView 56 57 @synthesize state = state_; 58 @synthesize hoverAlpha = hoverAlpha_; 59 @synthesize alertAlpha = alertAlpha_; 60 @synthesize closing = closing_; 61 62 + (CGFloat)insetMultiplier { 63 return kInsetMultiplier; 64 } 65 66 - (id)initWithFrame:(NSRect)frame 67 controller:(TabController*)controller 68 closeButton:(HoverCloseButton*)closeButton { 69 self = [super initWithFrame:frame]; 70 if (self) { 71 controller_ = controller; 72 closeButton_ = closeButton; 73 } 74 return self; 75 } 76 77 - (void)dealloc { 78 // Cancel any delayed requests that may still be pending (drags or hover). 79 [NSObject cancelPreviousPerformRequestsWithTarget:self]; 80 [super dealloc]; 81 } 82 83 // Called to obtain the context menu for when the user hits the right mouse 84 // button (or control-clicks). (Note that -rightMouseDown: is *not* called for 85 // control-click.) 86 - (NSMenu*)menu { 87 if ([self isClosing]) 88 return nil; 89 90 // Sheets, being window-modal, should block contextual menus. For some reason 91 // they do not. Disallow them ourselves. 92 if ([[self window] attachedSheet]) 93 return nil; 94 95 return [controller_ menu]; 96 } 97 98 - (void)resizeSubviewsWithOldSize:(NSSize)oldBoundsSize { 99 [super resizeSubviewsWithOldSize:oldBoundsSize]; 100 // Called when our view is resized. If it gets too small, start by hiding 101 // the close button and only show it if tab is selected. Eventually, hide the 102 // icon as well. 103 [controller_ updateVisibility]; 104 } 105 106 // Overridden so that mouse clicks come to this view (the parent of the 107 // hierarchy) first. We want to handle clicks and drags in this class and 108 // leave the background button for display purposes only. 109 - (BOOL)acceptsFirstMouse:(NSEvent*)theEvent { 110 return YES; 111 } 112 113 - (void)mouseEntered:(NSEvent*)theEvent { 114 isMouseInside_ = YES; 115 [self resetLastGlowUpdateTime]; 116 [self adjustGlowValue]; 117 } 118 119 - (void)mouseMoved:(NSEvent*)theEvent { 120 hoverPoint_ = [self convertPoint:[theEvent locationInWindow] 121 fromView:nil]; 122 [self setNeedsDisplay:YES]; 123 } 124 125 - (void)mouseExited:(NSEvent*)theEvent { 126 isMouseInside_ = NO; 127 hoverHoldEndTime_ = 128 [NSDate timeIntervalSinceReferenceDate] + kHoverHoldDuration; 129 [self resetLastGlowUpdateTime]; 130 [self adjustGlowValue]; 131 } 132 133 - (void)setTrackingEnabled:(BOOL)enabled { 134 if (![closeButton_ isHidden]) { 135 [closeButton_ setTrackingEnabled:enabled]; 136 } 137 } 138 139 // Determines which view a click in our frame actually hit. It's either this 140 // view or our child close button. 141 - (NSView*)hitTest:(NSPoint)aPoint { 142 NSPoint viewPoint = [self convertPoint:aPoint fromView:[self superview]]; 143 if (![closeButton_ isHidden]) 144 if (NSPointInRect(viewPoint, [closeButton_ frame])) return closeButton_; 145 146 NSRect pointRect = NSMakeRect(viewPoint.x, viewPoint.y, 1, 1); 147 148 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); 149 NSImage* left = rb.GetNativeImageNamed(IDR_TAB_ALPHA_LEFT).ToNSImage(); 150 if (viewPoint.x < [left size].width) { 151 NSRect imageRect = NSMakeRect(0, 0, [left size].width, [left size].height); 152 if ([left hitTestRect:pointRect withImageDestinationRect:imageRect 153 context:nil hints:nil flipped:NO]) { 154 return self; 155 } 156 return nil; 157 } 158 159 NSImage* right = rb.GetNativeImageNamed(IDR_TAB_ALPHA_RIGHT).ToNSImage(); 160 CGFloat rightX = NSWidth([self bounds]) - [right size].width; 161 if (viewPoint.x > rightX) { 162 NSRect imageRect = NSMakeRect( 163 rightX, 0, [right size].width, [right size].height); 164 if ([right hitTestRect:pointRect withImageDestinationRect:imageRect 165 context:nil hints:nil flipped:NO]) { 166 return self; 167 } 168 return nil; 169 } 170 171 if (viewPoint.y < kFillHeight) 172 return self; 173 return nil; 174 } 175 176 // Returns |YES| if this tab can be torn away into a new window. 177 - (BOOL)canBeDragged { 178 return [controller_ tabCanBeDragged:controller_]; 179 } 180 181 // Handle clicks and drags in this button. We get here because we have 182 // overridden acceptsFirstMouse: and the click is within our bounds. 183 - (void)mouseDown:(NSEvent*)theEvent { 184 if ([self isClosing]) 185 return; 186 187 // Record the point at which this event happened. This is used by other mouse 188 // events that are dispatched from |-maybeStartDrag::|. 189 mouseDownPoint_ = [theEvent locationInWindow]; 190 191 // Record the state of the close button here, because selecting the tab will 192 // unhide it. 193 BOOL closeButtonActive = ![closeButton_ isHidden]; 194 195 // During the tab closure animation (in particular, during rapid tab closure), 196 // we may get incorrectly hit with a mouse down. If it should have gone to the 197 // close button, we send it there -- it should then track the mouse, so we 198 // don't have to worry about mouse ups. 199 if (closeButtonActive && [controller_ inRapidClosureMode]) { 200 NSPoint hitLocation = [[self superview] convertPoint:mouseDownPoint_ 201 fromView:nil]; 202 if ([self hitTest:hitLocation] == closeButton_) { 203 [closeButton_ mouseDown:theEvent]; 204 return; 205 } 206 } 207 208 // If the tab gets torn off, the tab controller will be removed from the tab 209 // strip and then deallocated. This will also result in *us* being 210 // deallocated. Both these are bad, so we prevent this by retaining the 211 // controller. 212 base::scoped_nsobject<TabController> controller([controller_ retain]); 213 214 // Try to initiate a drag. This will spin a custom event loop and may 215 // dispatch other mouse events. 216 [controller_ maybeStartDrag:theEvent forTab:controller]; 217 218 // The custom loop has ended, so clear the point. 219 mouseDownPoint_ = NSZeroPoint; 220 } 221 222 - (void)mouseUp:(NSEvent*)theEvent { 223 // Check for rapid tab closure. 224 if ([theEvent type] == NSLeftMouseUp) { 225 NSPoint upLocation = [theEvent locationInWindow]; 226 CGFloat dx = upLocation.x - mouseDownPoint_.x; 227 CGFloat dy = upLocation.y - mouseDownPoint_.y; 228 229 // During rapid tab closure (mashing tab close buttons), we may get hit 230 // with a mouse down. As long as the mouse up is over the close button, 231 // and the mouse hasn't moved too much, we close the tab. 232 if (![closeButton_ isHidden] && 233 (dx*dx + dy*dy) <= kRapidCloseDist*kRapidCloseDist && 234 [controller_ inRapidClosureMode]) { 235 NSPoint hitLocation = 236 [[self superview] convertPoint:[theEvent locationInWindow] 237 fromView:nil]; 238 if ([self hitTest:hitLocation] == closeButton_) { 239 [controller_ closeTab:self]; 240 return; 241 } 242 } 243 } 244 245 // Fire the action to select the tab. 246 [controller_ selectTab:self]; 247 248 // Messaging the drag controller with |-endDrag:| would seem like the right 249 // thing to do here. But, when a tab has been detached, the controller's 250 // target is nil until the drag is finalized. Since |-mouseUp:| gets called 251 // via the manual event loop inside -[TabStripDragController 252 // maybeStartDrag:forTab:], the drag controller can end the dragging session 253 // itself directly after calling this. 254 } 255 256 - (void)otherMouseUp:(NSEvent*)theEvent { 257 if ([self isClosing]) 258 return; 259 260 // Support middle-click-to-close. 261 if ([theEvent buttonNumber] == 2) { 262 // |-hitTest:| takes a location in the superview's coordinates. 263 NSPoint upLocation = 264 [[self superview] convertPoint:[theEvent locationInWindow] 265 fromView:nil]; 266 // If the mouse up occurred in our view or over the close button, then 267 // close. 268 if ([self hitTest:upLocation]) 269 [controller_ closeTab:self]; 270 } 271 } 272 273 // Returns the color used to draw the background of a tab. |selected| selects 274 // between the foreground and background tabs. 275 - (NSColor*)backgroundColorForSelected:(bool)selected { 276 ThemeService* themeProvider = 277 static_cast<ThemeService*>([[self window] themeProvider]); 278 if (!themeProvider) 279 return [[self window] backgroundColor]; 280 281 int bitmapResources[2][2] = { 282 // Background window. 283 { 284 IDR_THEME_TAB_BACKGROUND_INACTIVE, // Background tab. 285 IDR_THEME_TOOLBAR_INACTIVE, // Active tab. 286 }, 287 // Currently focused window. 288 { 289 IDR_THEME_TAB_BACKGROUND, // Background tab. 290 IDR_THEME_TOOLBAR, // Active tab. 291 }, 292 }; 293 294 // Themes don't have an inactive image so only look for one if there's no 295 // theme. 296 bool active = [[self window] isKeyWindow] || [[self window] isMainWindow] || 297 !themeProvider->UsingDefaultTheme(); 298 return themeProvider->GetNSImageColorNamed(bitmapResources[active][selected]); 299 } 300 301 // Draws the active tab background. 302 - (void)drawFillForActiveTab:(NSRect)dirtyRect { 303 NSColor* backgroundImageColor = [self backgroundColorForSelected:YES]; 304 [backgroundImageColor set]; 305 306 // Themes can have partially transparent images. NSRectFill() is measurably 307 // faster though, so call it for the known-safe default theme. 308 ThemeService* themeProvider = 309 static_cast<ThemeService*>([[self window] themeProvider]); 310 if (themeProvider && themeProvider->UsingDefaultTheme()) 311 NSRectFill(dirtyRect); 312 else 313 NSRectFillUsingOperation(dirtyRect, NSCompositeSourceOver); 314 } 315 316 // Draws the tab background. 317 - (void)drawFill:(NSRect)dirtyRect { 318 gfx::ScopedNSGraphicsContextSaveGState scopedGState; 319 NSGraphicsContext* context = [NSGraphicsContext currentContext]; 320 CGContextRef cgContext = static_cast<CGContextRef>([context graphicsPort]); 321 322 ThemeService* themeProvider = 323 static_cast<ThemeService*>([[self window] themeProvider]); 324 NSPoint position = [[self window] 325 themeImagePositionForAlignment: THEME_IMAGE_ALIGN_WITH_TAB_STRIP]; 326 [context cr_setPatternPhase:position forView:self]; 327 328 CGImageRef mask([self tabClippingMask]); 329 CGRect maskBounds = CGRectMake(0, 0, maskCacheWidth_, kMaskHeight); 330 CGContextClipToMask(cgContext, maskBounds, mask); 331 332 bool selected = [self state]; 333 if (selected) { 334 [self drawFillForActiveTab:dirtyRect]; 335 return; 336 } 337 338 // Background tabs should not paint over the tab strip separator, which is 339 // two pixels high in both lodpi and hidpi. 340 if (dirtyRect.origin.y < 1) 341 dirtyRect.origin.y = 2 * [self cr_lineWidth]; 342 343 // Draw the tab background. 344 NSColor* backgroundImageColor = [self backgroundColorForSelected:NO]; 345 [backgroundImageColor set]; 346 347 // Themes can have partially transparent images. NSRectFill() is measurably 348 // faster though, so call it for the known-safe default theme. 349 bool usingDefaultTheme = themeProvider && themeProvider->UsingDefaultTheme(); 350 if (usingDefaultTheme) 351 NSRectFill(dirtyRect); 352 else 353 NSRectFillUsingOperation(dirtyRect, NSCompositeSourceOver); 354 355 // Draw the glow for hover and the overlay for alerts. 356 CGFloat hoverAlpha = [self hoverAlpha]; 357 CGFloat alertAlpha = [self alertAlpha]; 358 if (hoverAlpha > 0 || alertAlpha > 0) { 359 gfx::ScopedNSGraphicsContextSaveGState contextSave; 360 CGContextBeginTransparencyLayer(cgContext, 0); 361 362 // The alert glow overlay is like the selected state but at most at most 80% 363 // opaque. The hover glow brings up the overlay's opacity at most 50%. 364 CGFloat backgroundAlpha = 0.8 * alertAlpha; 365 backgroundAlpha += (1 - backgroundAlpha) * 0.5 * hoverAlpha; 366 CGContextSetAlpha(cgContext, backgroundAlpha); 367 368 [self drawFillForActiveTab:dirtyRect]; 369 370 // ui::ThemeProvider::HasCustomImage is true only if the theme provides the 371 // image. However, even if the theme doesn't provide a tab background, the 372 // theme machinery will make one if given a frame image. See 373 // BrowserThemePack::GenerateTabBackgroundImages for details. 374 BOOL hasCustomTheme = themeProvider && 375 (themeProvider->HasCustomImage(IDR_THEME_TAB_BACKGROUND) || 376 themeProvider->HasCustomImage(IDR_THEME_FRAME)); 377 // Draw a mouse hover gradient for the default themes. 378 if (hoverAlpha > 0) { 379 if (themeProvider && !hasCustomTheme) { 380 base::scoped_nsobject<NSGradient> glow([NSGradient alloc]); 381 [glow initWithStartingColor:[NSColor colorWithCalibratedWhite:1.0 382 alpha:1.0 * hoverAlpha] 383 endingColor:[NSColor colorWithCalibratedWhite:1.0 384 alpha:0.0]]; 385 NSRect rect = [self bounds]; 386 NSPoint point = hoverPoint_; 387 point.y = NSHeight(rect); 388 [glow drawFromCenter:point 389 radius:0.0 390 toCenter:point 391 radius:NSWidth(rect) / 3.0 392 options:NSGradientDrawsBeforeStartingLocation]; 393 } 394 } 395 396 CGContextEndTransparencyLayer(cgContext); 397 } 398 } 399 400 // Draws the tab outline. 401 - (void)drawStroke:(NSRect)dirtyRect { 402 BOOL focused = [[self window] isKeyWindow] || [[self window] isMainWindow]; 403 CGFloat alpha = focused ? 1.0 : tabs::kImageNoFocusAlpha; 404 405 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); 406 float height = 407 [rb.GetNativeImageNamed(IDR_TAB_ACTIVE_LEFT).ToNSImage() size].height; 408 if ([controller_ active]) { 409 NSDrawThreePartImage(NSMakeRect(0, 0, NSWidth([self bounds]), height), 410 rb.GetNativeImageNamed(IDR_TAB_ACTIVE_LEFT).ToNSImage(), 411 rb.GetNativeImageNamed(IDR_TAB_ACTIVE_CENTER).ToNSImage(), 412 rb.GetNativeImageNamed(IDR_TAB_ACTIVE_RIGHT).ToNSImage(), 413 /*vertical=*/NO, 414 NSCompositeSourceOver, 415 alpha, 416 /*flipped=*/NO); 417 } else { 418 NSDrawThreePartImage(NSMakeRect(0, 0, NSWidth([self bounds]), height), 419 rb.GetNativeImageNamed(IDR_TAB_INACTIVE_LEFT).ToNSImage(), 420 rb.GetNativeImageNamed(IDR_TAB_INACTIVE_CENTER).ToNSImage(), 421 rb.GetNativeImageNamed(IDR_TAB_INACTIVE_RIGHT).ToNSImage(), 422 /*vertical=*/NO, 423 NSCompositeSourceOver, 424 alpha, 425 /*flipped=*/NO); 426 } 427 } 428 429 - (void)drawRect:(NSRect)dirtyRect { 430 // Text, close button, and image are drawn by subviews. 431 [self drawFill:dirtyRect]; 432 [self drawStroke:dirtyRect]; 433 } 434 435 - (void)setFrameOrigin:(NSPoint)origin { 436 // The background color depends on the view's vertical position. 437 if (NSMinY([self frame]) != origin.y) 438 [self setNeedsDisplay:YES]; 439 [super setFrameOrigin:origin]; 440 } 441 442 // Override this to catch the text so that we can choose when to display it. 443 - (void)setToolTip:(NSString*)string { 444 toolTipText_.reset([string retain]); 445 } 446 447 - (NSString*)toolTipText { 448 if (!toolTipText_.get()) { 449 return @""; 450 } 451 return toolTipText_.get(); 452 } 453 454 - (void)viewDidMoveToWindow { 455 [super viewDidMoveToWindow]; 456 if ([self window]) { 457 [controller_ updateTitleColor]; 458 } 459 } 460 461 - (void)setState:(NSCellStateValue)state { 462 if (state_ == state) 463 return; 464 state_ = state; 465 [self setNeedsDisplay:YES]; 466 } 467 468 - (void)setClosing:(BOOL)closing { 469 closing_ = closing; // Safe because the property is nonatomic. 470 // When closing, ensure clicks to the close button go nowhere. 471 if (closing) { 472 [closeButton_ setTarget:nil]; 473 [closeButton_ setAction:nil]; 474 } 475 } 476 477 - (void)startAlert { 478 // Do not start a new alert while already alerting or while in a decay cycle. 479 if (alertState_ == tabs::kAlertNone) { 480 alertState_ = tabs::kAlertRising; 481 [self resetLastGlowUpdateTime]; 482 [self adjustGlowValue]; 483 } 484 } 485 486 - (void)cancelAlert { 487 if (alertState_ != tabs::kAlertNone) { 488 alertState_ = tabs::kAlertFalling; 489 alertHoldEndTime_ = 490 [NSDate timeIntervalSinceReferenceDate] + kGlowUpdateInterval; 491 [self resetLastGlowUpdateTime]; 492 [self adjustGlowValue]; 493 } 494 } 495 496 - (BOOL)accessibilityIsIgnored { 497 return NO; 498 } 499 500 - (NSArray*)accessibilityActionNames { 501 NSArray* parentActions = [super accessibilityActionNames]; 502 503 return [parentActions arrayByAddingObject:NSAccessibilityPressAction]; 504 } 505 506 - (NSArray*)accessibilityAttributeNames { 507 NSMutableArray* attributes = 508 [[super accessibilityAttributeNames] mutableCopy]; 509 [attributes addObject:NSAccessibilityTitleAttribute]; 510 [attributes addObject:NSAccessibilityEnabledAttribute]; 511 [attributes addObject:NSAccessibilityValueAttribute]; 512 513 return [attributes autorelease]; 514 } 515 516 - (BOOL)accessibilityIsAttributeSettable:(NSString*)attribute { 517 if ([attribute isEqual:NSAccessibilityTitleAttribute]) 518 return NO; 519 520 if ([attribute isEqual:NSAccessibilityEnabledAttribute]) 521 return NO; 522 523 if ([attribute isEqual:NSAccessibilityValueAttribute]) 524 return YES; 525 526 return [super accessibilityIsAttributeSettable:attribute]; 527 } 528 529 - (void)accessibilityPerformAction:(NSString*)action { 530 if ([action isEqual:NSAccessibilityPressAction] && 531 [[controller_ target] respondsToSelector:[controller_ action]]) { 532 [[controller_ target] performSelector:[controller_ action] 533 withObject:self]; 534 NSAccessibilityPostNotification(self, 535 NSAccessibilityValueChangedNotification); 536 } else { 537 [super accessibilityPerformAction:action]; 538 } 539 } 540 541 - (id)accessibilityAttributeValue:(NSString*)attribute { 542 if ([attribute isEqual:NSAccessibilityRoleAttribute]) 543 return NSAccessibilityRadioButtonRole; 544 if ([attribute isEqual:NSAccessibilityRoleDescriptionAttribute]) 545 return l10n_util::GetNSStringWithFixup(IDS_ACCNAME_TAB); 546 if ([attribute isEqual:NSAccessibilityTitleAttribute]) 547 return [controller_ title]; 548 if ([attribute isEqual:NSAccessibilityValueAttribute]) 549 return [NSNumber numberWithInt:[controller_ selected]]; 550 if ([attribute isEqual:NSAccessibilityEnabledAttribute]) 551 return [NSNumber numberWithBool:YES]; 552 553 return [super accessibilityAttributeValue:attribute]; 554 } 555 556 - (ViewID)viewID { 557 return VIEW_ID_TAB; 558 } 559 560 @end // @implementation TabView 561 562 @implementation TabView (TabControllerInterface) 563 564 - (void)setController:(TabController*)controller { 565 controller_ = controller; 566 } 567 568 @end // @implementation TabView (TabControllerInterface) 569 570 @implementation TabView(Private) 571 572 - (void)resetLastGlowUpdateTime { 573 lastGlowUpdate_ = [NSDate timeIntervalSinceReferenceDate]; 574 } 575 576 - (NSTimeInterval)timeElapsedSinceLastGlowUpdate { 577 return [NSDate timeIntervalSinceReferenceDate] - lastGlowUpdate_; 578 } 579 580 - (void)adjustGlowValue { 581 // A time interval long enough to represent no update. 582 const NSTimeInterval kNoUpdate = 1000000; 583 584 // Time until next update for either glow. 585 NSTimeInterval nextUpdate = kNoUpdate; 586 587 NSTimeInterval elapsed = [self timeElapsedSinceLastGlowUpdate]; 588 NSTimeInterval currentTime = [NSDate timeIntervalSinceReferenceDate]; 589 590 // TODO(viettrungluu): <http://crbug.com/30617> -- split off the stuff below 591 // into a pure function and add a unit test. 592 593 CGFloat hoverAlpha = [self hoverAlpha]; 594 if (isMouseInside_) { 595 // Increase hover glow until it's 1. 596 if (hoverAlpha < 1) { 597 hoverAlpha = MIN(hoverAlpha + elapsed / kHoverShowDuration, 1); 598 [self setHoverAlpha:hoverAlpha]; 599 nextUpdate = MIN(kGlowUpdateInterval, nextUpdate); 600 } // Else already 1 (no update needed). 601 } else { 602 if (currentTime >= hoverHoldEndTime_) { 603 // No longer holding, so decrease hover glow until it's 0. 604 if (hoverAlpha > 0) { 605 hoverAlpha = MAX(hoverAlpha - elapsed / kHoverHideDuration, 0); 606 [self setHoverAlpha:hoverAlpha]; 607 nextUpdate = MIN(kGlowUpdateInterval, nextUpdate); 608 } // Else already 0 (no update needed). 609 } else { 610 // Schedule update for end of hold time. 611 nextUpdate = MIN(hoverHoldEndTime_ - currentTime, nextUpdate); 612 } 613 } 614 615 CGFloat alertAlpha = [self alertAlpha]; 616 if (alertState_ == tabs::kAlertRising) { 617 // Increase alert glow until it's 1 ... 618 alertAlpha = MIN(alertAlpha + elapsed / kAlertShowDuration, 1); 619 [self setAlertAlpha:alertAlpha]; 620 621 // ... and having reached 1, switch to holding. 622 if (alertAlpha >= 1) { 623 alertState_ = tabs::kAlertHolding; 624 alertHoldEndTime_ = currentTime + kAlertHoldDuration; 625 nextUpdate = MIN(kAlertHoldDuration, nextUpdate); 626 } else { 627 nextUpdate = MIN(kGlowUpdateInterval, nextUpdate); 628 } 629 } else if (alertState_ != tabs::kAlertNone) { 630 if (alertAlpha > 0) { 631 if (currentTime >= alertHoldEndTime_) { 632 // Stop holding, then decrease alert glow (until it's 0). 633 if (alertState_ == tabs::kAlertHolding) { 634 alertState_ = tabs::kAlertFalling; 635 nextUpdate = MIN(kGlowUpdateInterval, nextUpdate); 636 } else { 637 DCHECK_EQ(tabs::kAlertFalling, alertState_); 638 alertAlpha = MAX(alertAlpha - elapsed / kAlertHideDuration, 0); 639 [self setAlertAlpha:alertAlpha]; 640 nextUpdate = MIN(kGlowUpdateInterval, nextUpdate); 641 } 642 } else { 643 // Schedule update for end of hold time. 644 nextUpdate = MIN(alertHoldEndTime_ - currentTime, nextUpdate); 645 } 646 } else { 647 // Done the alert decay cycle. 648 alertState_ = tabs::kAlertNone; 649 } 650 } 651 652 if (nextUpdate < kNoUpdate) 653 [self performSelector:_cmd withObject:nil afterDelay:nextUpdate]; 654 655 [self resetLastGlowUpdateTime]; 656 [self setNeedsDisplay:YES]; 657 } 658 659 - (CGImageRef)tabClippingMask { 660 // NOTE: NSHeight([self bounds]) doesn't match the height of the bitmaps. 661 CGFloat scale = 1; 662 if ([[self window] respondsToSelector:@selector(backingScaleFactor)]) 663 scale = [[self window] backingScaleFactor]; 664 665 NSRect bounds = [self bounds]; 666 CGFloat tabWidth = NSWidth(bounds); 667 if (tabWidth == maskCacheWidth_ && scale == maskCacheScale_) 668 return maskCache_.get(); 669 670 maskCacheWidth_ = tabWidth; 671 maskCacheScale_ = scale; 672 673 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); 674 NSImage* leftMask = rb.GetNativeImageNamed(IDR_TAB_ALPHA_LEFT).ToNSImage(); 675 NSImage* rightMask = rb.GetNativeImageNamed(IDR_TAB_ALPHA_RIGHT).ToNSImage(); 676 677 CGFloat leftWidth = leftMask.size.width; 678 CGFloat rightWidth = rightMask.size.width; 679 680 // Image masks must be in the DeviceGray colorspace. Create a context and 681 // draw the mask into it. 682 base::ScopedCFTypeRef<CGColorSpaceRef> colorspace( 683 CGColorSpaceCreateDeviceGray()); 684 base::ScopedCFTypeRef<CGContextRef> maskContext( 685 CGBitmapContextCreate(NULL, tabWidth * scale, kMaskHeight * scale, 686 8, tabWidth * scale, colorspace, 0)); 687 CGContextScaleCTM(maskContext, scale, scale); 688 NSGraphicsContext* maskGraphicsContext = 689 [NSGraphicsContext graphicsContextWithGraphicsPort:maskContext 690 flipped:NO]; 691 692 gfx::ScopedNSGraphicsContextSaveGState scopedGState; 693 [NSGraphicsContext setCurrentContext:maskGraphicsContext]; 694 695 // Draw mask image. 696 [[NSColor blackColor] setFill]; 697 CGContextFillRect(maskContext, CGRectMake(0, 0, tabWidth, kMaskHeight)); 698 699 NSDrawThreePartImage(NSMakeRect(0, 0, tabWidth, kMaskHeight), 700 leftMask, nil, rightMask, /*vertical=*/NO, NSCompositeSourceOver, 1.0, 701 /*flipped=*/NO); 702 703 CGFloat middleWidth = tabWidth - leftWidth - rightWidth; 704 NSRect middleRect = NSMakeRect(leftWidth, 0, middleWidth, kFillHeight); 705 [[NSColor whiteColor] setFill]; 706 NSRectFill(middleRect); 707 708 maskCache_.reset(CGBitmapContextCreateImage(maskContext)); 709 return maskCache_; 710 } 711 712 @end // @implementation TabView(Private) 713