Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / ui / cocoa / tabs / tab_view.mm
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)