Popular sites on the NTP: check that experiment group StartsWith (rather than IS...
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / tabs / tab_view.mm
blob1e4d21d6df8c79e216795eb2492a3dd32106c407
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.
5 #import "chrome/browser/ui/cocoa/tabs/tab_view.h"
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/media_indicator_button_cocoa.h"
13 #import "chrome/browser/ui/cocoa/tabs/tab_controller.h"
14 #import "chrome/browser/ui/cocoa/tabs/tab_window_controller.h"
15 #import "chrome/browser/ui/cocoa/themed_window.h"
16 #import "chrome/browser/ui/cocoa/view_id_util.h"
17 #include "chrome/grit/generated_resources.h"
18 #include "grit/theme_resources.h"
19 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMFadeTruncatingTextFieldCell.h"
20 #import "ui/base/cocoa/nsgraphics_context_additions.h"
21 #import "ui/base/cocoa/nsview_additions.h"
22 #include "ui/base/cocoa/three_part_image.h"
23 #include "ui/base/l10n/l10n_util.h"
24 #include "ui/base/resource/resource_bundle.h"
25 #include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h"
28 const int kFillHeight = 25;  // Height of the "mask on" part of the mask bitmap.
30 // The amount of time in seconds during which each type of glow increases, holds
31 // steady, and decreases, respectively.
32 const NSTimeInterval kHoverShowDuration = 0.2;
33 const NSTimeInterval kHoverHoldDuration = 0.02;
34 const NSTimeInterval kHoverHideDuration = 0.4;
35 const NSTimeInterval kAlertShowDuration = 0.4;
36 const NSTimeInterval kAlertHoldDuration = 0.4;
37 const NSTimeInterval kAlertHideDuration = 0.4;
39 // The default time interval in seconds between glow updates (when
40 // increasing/decreasing).
41 const NSTimeInterval kGlowUpdateInterval = 0.025;
43 // This is used to judge whether the mouse has moved during rapid closure; if it
44 // has moved less than the threshold, we want to close the tab.
45 const CGFloat kRapidCloseDist = 2.5;
47 namespace {
49 ui::ThreePartImage& GetMaskImage() {
50   CR_DEFINE_STATIC_LOCAL(ui::ThreePartImage, mask,
51                          (IDR_TAB_ALPHA_LEFT, 0, IDR_TAB_ALPHA_RIGHT));
52   return mask;
55 ui::ThreePartImage& GetStrokeImage(bool active) {
56   CR_DEFINE_STATIC_LOCAL(
57       ui::ThreePartImage, activeStroke,
58       (IDR_TAB_ACTIVE_LEFT, IDR_TAB_ACTIVE_CENTER, IDR_TAB_ACTIVE_RIGHT));
59   CR_DEFINE_STATIC_LOCAL(
60       ui::ThreePartImage, inactiveStroke,
61       (IDR_TAB_INACTIVE_LEFT, IDR_TAB_INACTIVE_CENTER, IDR_TAB_INACTIVE_RIGHT));
63   return active ? activeStroke : inactiveStroke;
66 }  // namespace
68 @interface TabView(Private)
70 - (void)resetLastGlowUpdateTime;
71 - (NSTimeInterval)timeElapsedSinceLastGlowUpdate;
72 - (void)adjustGlowValue;
74 @end  // TabView(Private)
76 @implementation TabView
78 @synthesize state = state_;
79 @synthesize hoverAlpha = hoverAlpha_;
80 @synthesize alertAlpha = alertAlpha_;
81 @synthesize closing = closing_;
83 - (id)initWithFrame:(NSRect)frame
84          controller:(TabController*)controller
85         closeButton:(HoverCloseButton*)closeButton {
86   self = [super initWithFrame:frame];
87   if (self) {
88     controller_ = controller;
89     closeButton_ = closeButton;
91     // Make a text field for the title, but don't add it as a subview.
92     // We will use the cell to draw the text directly into our layer,
93     // so that we can get font smoothing enabled.
94     titleView_.reset([[NSTextField alloc] init]);
95     [titleView_ setAutoresizingMask:NSViewWidthSizable];
96     base::scoped_nsobject<GTMFadeTruncatingTextFieldCell> labelCell(
97         [[GTMFadeTruncatingTextFieldCell alloc] initTextCell:@"Label"]);
98     [labelCell setControlSize:NSSmallControlSize];
99     CGFloat fontSize = [NSFont systemFontSizeForControlSize:NSSmallControlSize];
100     [labelCell setFont:[NSFont systemFontOfSize:fontSize]];
101     [titleView_ setCell:labelCell];
102     titleViewCell_ = labelCell;
104     [self setWantsLayer:YES];  // -drawFill: needs a layer.
105   }
106   return self;
109 - (void)dealloc {
110   // Cancel any delayed requests that may still be pending (drags or hover).
111   [NSObject cancelPreviousPerformRequestsWithTarget:self];
112   [super dealloc];
115 // Called to obtain the context menu for when the user hits the right mouse
116 // button (or control-clicks). (Note that -rightMouseDown: is *not* called for
117 // control-click.)
118 - (NSMenu*)menu {
119   if ([self isClosing])
120     return nil;
122   // Sheets, being window-modal, should block contextual menus. For some reason
123   // they do not. Disallow them ourselves.
124   if ([[self window] attachedSheet])
125     return nil;
127   return [controller_ menu];
130 - (void)resizeSubviewsWithOldSize:(NSSize)oldBoundsSize {
131   [super resizeSubviewsWithOldSize:oldBoundsSize];
132   // Called when our view is resized. If it gets too small, start by hiding
133   // the close button and only show it if tab is selected. Eventually, hide the
134   // icon as well.
135   [controller_ updateVisibility];
138 // Overridden so that mouse clicks come to this view (the parent of the
139 // hierarchy) first. We want to handle clicks and drags in this class and
140 // leave the background button for display purposes only.
141 - (BOOL)acceptsFirstMouse:(NSEvent*)theEvent {
142   return YES;
145 - (void)mouseEntered:(NSEvent*)theEvent {
146   isMouseInside_ = YES;
147   [self resetLastGlowUpdateTime];
148   [self adjustGlowValue];
151 - (void)mouseMoved:(NSEvent*)theEvent {
152   if (state_ == NSOffState) {
153     hoverPoint_ = [self convertPoint:[theEvent locationInWindow] fromView:nil];
154     [self setNeedsDisplay:YES];
155   }
158 - (void)mouseExited:(NSEvent*)theEvent {
159   isMouseInside_ = NO;
160   hoverHoldEndTime_ =
161       [NSDate timeIntervalSinceReferenceDate] + kHoverHoldDuration;
162   [self resetLastGlowUpdateTime];
163   [self adjustGlowValue];
166 - (void)setTrackingEnabled:(BOOL)enabled {
167   if (![closeButton_ isHidden]) {
168     [closeButton_ setTrackingEnabled:enabled];
169   }
172 // Determines which view a click in our frame actually hit. It's either this
173 // view or one of the child buttons.
174 - (NSView*)hitTest:(NSPoint)aPoint {
175   NSView* const defaultHitTestResult = [super hitTest:aPoint];
176   if ([defaultHitTestResult isKindOfClass:[NSButton class]])
177     return defaultHitTestResult;
179   NSPoint viewPoint = [self convertPoint:aPoint fromView:[self superview]];
180   NSRect maskRect = [self bounds];
181   maskRect.size.height = kFillHeight;
182   return GetMaskImage().HitTest(viewPoint, maskRect) ? self : nil;
185 // Handle clicks and drags in this button. We get here because we have
186 // overridden acceptsFirstMouse: and the click is within our bounds.
187 - (void)mouseDown:(NSEvent*)theEvent {
188   if ([self isClosing])
189     return;
191   // Record the point at which this event happened. This is used by other mouse
192   // events that are dispatched from |-maybeStartDrag::|.
193   mouseDownPoint_ = [theEvent locationInWindow];
195   // Record the state of the close button here, because selecting the tab will
196   // unhide it.
197   BOOL closeButtonActive = ![closeButton_ isHidden];
199   // During the tab closure animation (in particular, during rapid tab closure),
200   // we may get incorrectly hit with a mouse down. If it should have gone to the
201   // close button, we send it there -- it should then track the mouse, so we
202   // don't have to worry about mouse ups.
203   if (closeButtonActive && [controller_ inRapidClosureMode]) {
204     NSPoint hitLocation = [[self superview] convertPoint:mouseDownPoint_
205                                                 fromView:nil];
206     if ([self hitTest:hitLocation] == closeButton_) {
207       [closeButton_ mouseDown:theEvent];
208       return;
209     }
210   }
212   // If the tab gets torn off, the tab controller will be removed from the tab
213   // strip and then deallocated. This will also result in *us* being
214   // deallocated. Both these are bad, so we prevent this by retaining the
215   // controller.
216   base::scoped_nsobject<TabController> controller([controller_ retain]);
218   // Try to initiate a drag. This will spin a custom event loop and may
219   // dispatch other mouse events.
220   [controller_ maybeStartDrag:theEvent forTab:controller];
222   // The custom loop has ended, so clear the point.
223   mouseDownPoint_ = NSZeroPoint;
226 - (void)mouseUp:(NSEvent*)theEvent {
227   // Check for rapid tab closure.
228   if ([theEvent type] == NSLeftMouseUp) {
229     NSPoint upLocation = [theEvent locationInWindow];
230     CGFloat dx = upLocation.x - mouseDownPoint_.x;
231     CGFloat dy = upLocation.y - mouseDownPoint_.y;
233     // During rapid tab closure (mashing tab close buttons), we may get hit
234     // with a mouse down. As long as the mouse up is over the close button,
235     // and the mouse hasn't moved too much, we close the tab.
236     if (![closeButton_ isHidden] &&
237         (dx*dx + dy*dy) <= kRapidCloseDist*kRapidCloseDist &&
238         [controller_ inRapidClosureMode]) {
239       NSPoint hitLocation =
240           [[self superview] convertPoint:[theEvent locationInWindow]
241                                 fromView:nil];
242       if ([self hitTest:hitLocation] == closeButton_) {
243         [controller_ closeTab:self];
244         return;
245       }
246     }
247   }
249   // Fire the action to select the tab.
250   [controller_ selectTab:self];
252   // Messaging the drag controller with |-endDrag:| would seem like the right
253   // thing to do here. But, when a tab has been detached, the controller's
254   // target is nil until the drag is finalized. Since |-mouseUp:| gets called
255   // via the manual event loop inside -[TabStripDragController
256   // maybeStartDrag:forTab:], the drag controller can end the dragging session
257   // itself directly after calling this.
260 - (void)otherMouseUp:(NSEvent*)theEvent {
261   if ([self isClosing])
262     return;
264   // Support middle-click-to-close.
265   if ([theEvent buttonNumber] == 2) {
266     // |-hitTest:| takes a location in the superview's coordinates.
267     NSPoint upLocation =
268         [[self superview] convertPoint:[theEvent locationInWindow]
269                               fromView:nil];
270     // If the mouse up occurred in our view or over the close button, then
271     // close.
272     if ([self hitTest:upLocation])
273       [controller_ closeTab:self];
274   }
277 // Returns the color used to draw the background of a tab. |selected| selects
278 // between the foreground and background tabs.
279 - (NSColor*)backgroundColorForSelected:(bool)selected {
280   ThemeService* themeProvider =
281       static_cast<ThemeService*>([[self window] themeProvider]);
282   if (!themeProvider)
283     return [[self window] backgroundColor];
285   int bitmapResources[2][2] = {
286     // Background window.
287     {
288       IDR_THEME_TAB_BACKGROUND_INACTIVE,  // Background tab.
289       IDR_THEME_TOOLBAR_INACTIVE,         // Active tab.
290     },
291     // Currently focused window.
292     {
293       IDR_THEME_TAB_BACKGROUND,  // Background tab.
294       IDR_THEME_TOOLBAR,         // Active tab.
295     },
296   };
298   // Themes don't have an inactive image so only look for one if there's no
299   // theme.
300   bool active =
301       [[self window] isMainWindow] || !themeProvider->UsingDefaultTheme();
302   return themeProvider->GetNSImageColorNamed(bitmapResources[active][selected]);
305 // Draws the tab background.
306 - (void)drawFill:(NSRect)dirtyRect {
307   gfx::ScopedNSGraphicsContextSaveGState scopedGState;
308   NSRect bounds = [self bounds];
310   NSRect clippingRect = bounds;
311   clippingRect.size.height = kFillHeight;
312   if (state_ != NSOnState) {
313     // Background tabs should not paint over the tab strip separator, which is
314     // two pixels high in both lodpi and hidpi.
315     clippingRect.origin.y = 2 * [self cr_lineWidth];
316     clippingRect.size.height -= clippingRect.origin.y;
317   }
318   NSRectClip(clippingRect);
320   NSPoint position = [[self window]
321       themeImagePositionForAlignment:THEME_IMAGE_ALIGN_WITH_TAB_STRIP];
322   [[NSGraphicsContext currentContext] cr_setPatternPhase:position forView:self];
324   [[self backgroundColorForSelected:(state_ != NSOffState)] set];
325   NSRectFill(dirtyRect);
327   if (state_ == NSOffState)
328     [self drawGlow:dirtyRect];
330   // If we filled outside the middle rect, we need to erase what we filled
331   // outside the tab's shape.
332   // This only works if we are drawing to our own backing layer.
333   if (!NSContainsRect(GetMaskImage().GetMiddleRect(bounds), dirtyRect)) {
334     DCHECK([self layer]);
335     GetMaskImage().DrawInRect(bounds, NSCompositeDestinationIn, 1.0);
336   }
339 // Draw the glow for hover and the overlay for alerts.
340 - (void)drawGlow:(NSRect)dirtyRect {
341   NSGraphicsContext* context = [NSGraphicsContext currentContext];
342   CGContextRef cgContext = static_cast<CGContextRef>([context graphicsPort]);
344   CGFloat hoverAlpha = [self hoverAlpha];
345   CGFloat alertAlpha = [self alertAlpha];
346   if (hoverAlpha > 0 || alertAlpha > 0) {
347     CGContextBeginTransparencyLayer(cgContext, 0);
349     // The alert glow overlay is like the selected state but at most at most 80%
350     // opaque. The hover glow brings up the overlay's opacity at most 50%.
351     CGFloat backgroundAlpha = 0.8 * alertAlpha;
352     backgroundAlpha += (1 - backgroundAlpha) * 0.5 * hoverAlpha;
353     CGContextSetAlpha(cgContext, backgroundAlpha);
355     [[self backgroundColorForSelected:YES] set];
356     NSRectFill(dirtyRect);
358     // ui::ThemeProvider::HasCustomImage is true only if the theme provides the
359     // image. However, even if the theme doesn't provide a tab background, the
360     // theme machinery will make one if given a frame image. See
361     // BrowserThemePack::GenerateTabBackgroundImages for details.
362     ui::ThemeProvider* themeProvider = [[self window] themeProvider];
363     BOOL hasCustomTheme = themeProvider &&
364         (themeProvider->HasCustomImage(IDR_THEME_TAB_BACKGROUND) ||
365          themeProvider->HasCustomImage(IDR_THEME_FRAME));
366     // Draw a mouse hover gradient for the default themes.
367     if (hoverAlpha > 0) {
368       if (themeProvider && !hasCustomTheme) {
369         base::scoped_nsobject<NSGradient> glow([NSGradient alloc]);
370         [glow initWithStartingColor:[NSColor colorWithCalibratedWhite:1.0
371                                         alpha:1.0 * hoverAlpha]
372                         endingColor:[NSColor colorWithCalibratedWhite:1.0
373                                                                 alpha:0.0]];
374         NSRect rect = [self bounds];
375         NSPoint point = hoverPoint_;
376         point.y = NSHeight(rect);
377         [glow drawFromCenter:point
378                       radius:0.0
379                     toCenter:point
380                       radius:NSWidth(rect) / 3.0
381                      options:NSGradientDrawsBeforeStartingLocation];
382       }
383     }
385     CGContextEndTransparencyLayer(cgContext);
386   }
389 // Draws the tab outline.
390 - (void)drawStroke:(NSRect)dirtyRect {
391   CGFloat alpha = [[self window] isMainWindow] ? 1.0 : tabs::kImageNoFocusAlpha;
392   GetStrokeImage(state_ == NSOnState)
393       .DrawInRect([self bounds], NSCompositeSourceOver, alpha);
396 - (void)drawRect:(NSRect)dirtyRect {
397   [self drawFill:dirtyRect];
398   [self drawStroke:dirtyRect];
400   // We draw the title string directly instead of using a NSTextField subview.
401   // This is so that we can get font smoothing to work on earlier OS, and even
402   // when the tab background is a pattern image (when using themes).
403   if (![titleView_ isHidden]) {
404     gfx::ScopedNSGraphicsContextSaveGState scopedGState;
405     NSGraphicsContext* context = [NSGraphicsContext currentContext];
406     CGContextRef cgContext = static_cast<CGContextRef>([context graphicsPort]);
407     CGContextSetShouldSmoothFonts(cgContext, true);
408     [[titleView_ cell] drawWithFrame:[titleView_ frame] inView:self];
409   }
412 - (void)setFrameOrigin:(NSPoint)origin {
413   // The background color depends on the view's vertical position.
414   if (NSMinY([self frame]) != origin.y)
415     [self setNeedsDisplay:YES];
416   [super setFrameOrigin:origin];
419 - (void)setToolTipText:(NSString*)string {
420   toolTipText_.reset([string copy]);
423 - (NSString*)toolTipText {
424   return toolTipText_;
427 - (void)viewDidMoveToWindow {
428   [super viewDidMoveToWindow];
429   if ([self window]) {
430     [controller_ updateTitleColor];
432     // The new window may have different main window status.
433     // This happens when the view is moved into a TabWindowOverlayWindow for
434     // tab dragging.
435     [self windowDidChangeActive];
436   }
439 - (NSString*)title {
440   return [titleView_ stringValue];
443 - (void)setTitle:(NSString*)title {
444   if ([title isEqualToString:[titleView_ stringValue]])
445     return;
447   [titleView_ setStringValue:title];
448   [closeButton_ setAccessibilityTitle:title];
450   base::string16 title16 = base::SysNSStringToUTF16(title);
451   bool isRTL = base::i18n::GetFirstStrongCharacterDirection(title16) ==
452                base::i18n::RIGHT_TO_LEFT;
453   titleViewCell_.truncateMode = isRTL ? GTMFadeTruncatingHead
454                                       : GTMFadeTruncatingTail;
456   [self setNeedsDisplayInRect:[titleView_ frame]];
459 - (NSRect)titleFrame {
460   return [titleView_ frame];
463 - (void)setTitleFrame:(NSRect)titleFrame {
464   NSRect oldTitleFrame = [titleView_ frame];
465   if (NSEqualRects(titleFrame, oldTitleFrame))
466     return;
467   [titleView_ setFrame:titleFrame];
468   [self setNeedsDisplayInRect:NSUnionRect(titleFrame, oldTitleFrame)];
471 - (NSColor*)titleColor {
472   return [titleView_ textColor];
475 - (void)setTitleColor:(NSColor*)titleColor {
476   if ([titleColor isEqual:[titleView_ textColor]])
477     return;
478   [titleView_ setTextColor:titleColor];
479   [self setNeedsDisplayInRect:[titleView_ frame]];
482 - (BOOL)titleHidden {
483   return [titleView_ isHidden];
486 - (void)setTitleHidden:(BOOL)titleHidden {
487   if (titleHidden == [titleView_ isHidden])
488     return;
489   [titleView_ setHidden:titleHidden];
490   [self setNeedsDisplayInRect:[titleView_ frame]];
493 - (void)setState:(NSCellStateValue)state {
494   if (state_ == state)
495     return;
496   state_ = state;
497   [self setNeedsDisplay:YES];
500 - (void)setClosing:(BOOL)closing {
501   closing_ = closing;  // Safe because the property is nonatomic.
502   // When closing, ensure clicks to the close button go nowhere.
503   if (closing) {
504     [closeButton_ setTarget:nil];
505     [closeButton_ setAction:nil];
506   }
509 - (void)startAlert {
510   // Do not start a new alert while already alerting or while in a decay cycle.
511   if (alertState_ == tabs::kAlertNone) {
512     alertState_ = tabs::kAlertRising;
513     [self resetLastGlowUpdateTime];
514     [self adjustGlowValue];
515   }
518 - (void)cancelAlert {
519   if (alertState_ != tabs::kAlertNone) {
520     alertState_ = tabs::kAlertFalling;
521     alertHoldEndTime_ =
522         [NSDate timeIntervalSinceReferenceDate] + kGlowUpdateInterval;
523     [self resetLastGlowUpdateTime];
524     [self adjustGlowValue];
525   }
528 - (int)widthOfLargestSelectableRegion {
529   // Assume the entire region to the left of the media indicator and/or close
530   // buttons is available for click-to-select.  If neither are visible, the
531   // entire tab region is available.
532   MediaIndicatorButton* const indicator = [controller_ mediaIndicatorButton];
533   const int indicatorLeft = (!indicator || [indicator isHidden]) ?
534       NSWidth([self frame]) : NSMinX([indicator frame]);
535   HoverCloseButton* const closeButton = [controller_ closeButton];
536   const int closeButtonLeft = (!closeButton || [closeButton isHidden]) ?
537       NSWidth([self frame]) : NSMinX([closeButton frame]);
538   return std::min(indicatorLeft, closeButtonLeft);
541 - (BOOL)accessibilityIsIgnored {
542   return NO;
545 - (NSArray*)accessibilityActionNames {
546   NSArray* parentActions = [super accessibilityActionNames];
548   return [parentActions arrayByAddingObject:NSAccessibilityPressAction];
551 - (NSArray*)accessibilityAttributeNames {
552   NSMutableArray* attributes =
553       [[super accessibilityAttributeNames] mutableCopy];
554   [attributes addObject:NSAccessibilityTitleAttribute];
555   [attributes addObject:NSAccessibilityEnabledAttribute];
556   [attributes addObject:NSAccessibilityValueAttribute];
558   return [attributes autorelease];
561 - (BOOL)accessibilityIsAttributeSettable:(NSString*)attribute {
562   if ([attribute isEqual:NSAccessibilityTitleAttribute])
563     return NO;
565   if ([attribute isEqual:NSAccessibilityEnabledAttribute])
566     return NO;
568   if ([attribute isEqual:NSAccessibilityValueAttribute])
569     return YES;
571   return [super accessibilityIsAttributeSettable:attribute];
574 - (void)accessibilityPerformAction:(NSString*)action {
575   if ([action isEqual:NSAccessibilityPressAction] &&
576       [[controller_ target] respondsToSelector:[controller_ action]]) {
577     [[controller_ target] performSelector:[controller_ action]
578         withObject:self];
579     NSAccessibilityPostNotification(self,
580                                     NSAccessibilityValueChangedNotification);
581   } else {
582     [super accessibilityPerformAction:action];
583   }
586 - (id)accessibilityAttributeValue:(NSString*)attribute {
587   if ([attribute isEqual:NSAccessibilityRoleAttribute])
588     return NSAccessibilityRadioButtonRole;
589   if ([attribute isEqual:NSAccessibilityRoleDescriptionAttribute])
590     return l10n_util::GetNSStringWithFixup(IDS_ACCNAME_TAB);
591   if ([attribute isEqual:NSAccessibilityTitleAttribute])
592     return [controller_ title];
593   if ([attribute isEqual:NSAccessibilityValueAttribute])
594     return [NSNumber numberWithInt:[controller_ selected]];
595   if ([attribute isEqual:NSAccessibilityEnabledAttribute])
596     return [NSNumber numberWithBool:YES];
598   return [super accessibilityAttributeValue:attribute];
601 - (ViewID)viewID {
602   return VIEW_ID_TAB;
605 // ThemedWindowDrawing implementation.
607 - (void)windowDidChangeTheme {
608   [self setNeedsDisplay:YES];
611 - (void)windowDidChangeActive {
612   [self setNeedsDisplay:YES];
615 @end  // @implementation TabView
617 @implementation TabView (TabControllerInterface)
619 - (void)setController:(TabController*)controller {
620   controller_ = controller;
623 @end  // @implementation TabView (TabControllerInterface)
625 @implementation TabView(Private)
627 - (void)resetLastGlowUpdateTime {
628   lastGlowUpdate_ = [NSDate timeIntervalSinceReferenceDate];
631 - (NSTimeInterval)timeElapsedSinceLastGlowUpdate {
632   return [NSDate timeIntervalSinceReferenceDate] - lastGlowUpdate_;
635 - (void)adjustGlowValue {
636   // A time interval long enough to represent no update.
637   const NSTimeInterval kNoUpdate = 1000000;
639   // Time until next update for either glow.
640   NSTimeInterval nextUpdate = kNoUpdate;
642   NSTimeInterval elapsed = [self timeElapsedSinceLastGlowUpdate];
643   NSTimeInterval currentTime = [NSDate timeIntervalSinceReferenceDate];
645   // TODO(viettrungluu): <http://crbug.com/30617> -- split off the stuff below
646   // into a pure function and add a unit test.
648   CGFloat hoverAlpha = [self hoverAlpha];
649   if (isMouseInside_) {
650     // Increase hover glow until it's 1.
651     if (hoverAlpha < 1) {
652       hoverAlpha = MIN(hoverAlpha + elapsed / kHoverShowDuration, 1);
653       [self setHoverAlpha:hoverAlpha];
654       nextUpdate = MIN(kGlowUpdateInterval, nextUpdate);
655     }  // Else already 1 (no update needed).
656   } else {
657     if (currentTime >= hoverHoldEndTime_) {
658       // No longer holding, so decrease hover glow until it's 0.
659       if (hoverAlpha > 0) {
660         hoverAlpha = MAX(hoverAlpha - elapsed / kHoverHideDuration, 0);
661         [self setHoverAlpha:hoverAlpha];
662         nextUpdate = MIN(kGlowUpdateInterval, nextUpdate);
663       }  // Else already 0 (no update needed).
664     } else {
665       // Schedule update for end of hold time.
666       nextUpdate = MIN(hoverHoldEndTime_ - currentTime, nextUpdate);
667     }
668   }
670   CGFloat alertAlpha = [self alertAlpha];
671   if (alertState_ == tabs::kAlertRising) {
672     // Increase alert glow until it's 1 ...
673     alertAlpha = MIN(alertAlpha + elapsed / kAlertShowDuration, 1);
674     [self setAlertAlpha:alertAlpha];
676     // ... and having reached 1, switch to holding.
677     if (alertAlpha >= 1) {
678       alertState_ = tabs::kAlertHolding;
679       alertHoldEndTime_ = currentTime + kAlertHoldDuration;
680       nextUpdate = MIN(kAlertHoldDuration, nextUpdate);
681     } else {
682       nextUpdate = MIN(kGlowUpdateInterval, nextUpdate);
683     }
684   } else if (alertState_ != tabs::kAlertNone) {
685     if (alertAlpha > 0) {
686       if (currentTime >= alertHoldEndTime_) {
687         // Stop holding, then decrease alert glow (until it's 0).
688         if (alertState_ == tabs::kAlertHolding) {
689           alertState_ = tabs::kAlertFalling;
690           nextUpdate = MIN(kGlowUpdateInterval, nextUpdate);
691         } else {
692           DCHECK_EQ(tabs::kAlertFalling, alertState_);
693           alertAlpha = MAX(alertAlpha - elapsed / kAlertHideDuration, 0);
694           [self setAlertAlpha:alertAlpha];
695           nextUpdate = MIN(kGlowUpdateInterval, nextUpdate);
696         }
697       } else {
698         // Schedule update for end of hold time.
699         nextUpdate = MIN(alertHoldEndTime_ - currentTime, nextUpdate);
700       }
701     } else {
702       // Done the alert decay cycle.
703       alertState_ = tabs::kAlertNone;
704     }
705   }
707   if (nextUpdate < kNoUpdate)
708     [self performSelector:_cmd withObject:nil afterDelay:nextUpdate];
710   [self resetLastGlowUpdateTime];
711   [self setNeedsDisplay:YES];
714 @end  // @implementation TabView(Private)