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