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