Home | History | Annotate | Download | only in tabs
      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