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