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/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 const CGFloat kControlPoint1Multiplier = 1.0/3.0;
     29 const 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.
     33 const NSTimeInterval kHoverShowDuration = 0.2;
     34 const NSTimeInterval kHoverHoldDuration = 0.02;
     35 const NSTimeInterval kHoverHideDuration = 0.4;
     36 const NSTimeInterval kAlertShowDuration = 0.4;
     37 const NSTimeInterval kAlertHoldDuration = 0.4;
     38 const NSTimeInterval kAlertHideDuration = 0.4;
     39 
     40 // The default time interval in seconds between glow updates (when
     41 // increasing/decreasing).
     42 const 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.
     46 const 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