Popular sites on the NTP: check that experiment group StartsWith (rather than IS...
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / tabs / tab_strip_view.mm
blobdd7ba76a2ddd257ce54b089abc7facd2e4df0cfe
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_strip_view.h"
7 #include <cmath>  // floor
9 #include "base/logging.h"
10 #include "chrome/browser/themes/theme_service.h"
11 #import "chrome/browser/ui/cocoa/browser_window_controller.h"
12 #import "chrome/browser/ui/cocoa/new_tab_button.h"
13 #import "chrome/browser/ui/cocoa/tabs/tab_strip_controller.h"
14 #import "chrome/browser/ui/cocoa/tabs/tab_view.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 "ui/base/cocoa/appkit_utils.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_mac.h"
22 #include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h"
24 @implementation TabStripView
26 @synthesize dropArrowShown = dropArrowShown_;
27 @synthesize dropArrowPosition = dropArrowPosition_;
29 - (id)initWithFrame:(NSRect)frame {
30   self = [super initWithFrame:frame];
31   if (self) {
32     newTabButton_.reset([[NewTabButton alloc] initWithFrame:
33         NSMakeRect(295, 0, 40, 27)]);
34     [newTabButton_ setToolTip:l10n_util::GetNSString(IDS_TOOLTIP_NEW_TAB)];
36     // Set lastMouseUp_ = -1000.0 so that timestamp-lastMouseUp_ is big unless
37     // lastMouseUp_ has been reset.
38     lastMouseUp_ = -1000.0;
40     // Register to be an URL drop target.
41     dropHandler_.reset([[URLDropTargetHandler alloc] initWithView:self]);
43     [self setShowsDivider:NO];
45     [self setWantsLayer:YES];
46   }
47   return self;
50 // Draw bottom border bitmap. Each tab is responsible for mimicking this bottom
51 // border, unless it's the selected tab.
52 - (void)drawBorder:(NSRect)dirtyRect {
53   NSWindow* window = [self window];
54   ui::ThemeProvider* themeProvider = [window themeProvider];
55   if (!themeProvider)
56     return;
58   // First draw the toolbar bitmap, so that theme colors can shine through.
59   NSRect backgroundRect = [self bounds];
60   backgroundRect.size.height = 2 * [self cr_lineWidth];
61   if (NSIntersectsRect(backgroundRect, dirtyRect))
62     [self drawBackground:backgroundRect];
64   // Draw the border bitmap, which is partially transparent.
65   NSImage* image = themeProvider->GetNSImageNamed(IDR_TOOLBAR_SHADE_TOP);
66   NSRect borderRect = backgroundRect;
67   borderRect.size.height = [image size].height;
68   if (NSIntersectsRect(borderRect, dirtyRect)) {
69     BOOL focused = [window isMainWindow];
70     NSDrawThreePartImage(borderRect, nil, image, nil, /*vertical=*/ NO,
71                          NSCompositeSourceOver,
72                          focused ?  1.0 : tabs::kImageNoFocusAlpha,
73                          /*flipped=*/ NO);
74   }
77 - (void)drawRect:(NSRect)dirtyRect {
78   [self drawBorder:dirtyRect];
80   // Draw drop-indicator arrow (if appropriate).
81   // TODO(viettrungluu): this is all a stop-gap measure.
82   if ([self dropArrowShown]) {
83     // Programmer art: an arrow parametrized by many knobs. Note that the arrow
84     // points downwards (so understand "width" and "height" accordingly).
86     // How many (pixels) to inset on the top/bottom.
87     const CGFloat kArrowTopInset = 1.5;
88     const CGFloat kArrowBottomInset = 1;
90     // What proportion of the vertical space is dedicated to the arrow tip,
91     // i.e., (arrow tip height)/(amount of vertical space).
92     const CGFloat kArrowTipProportion = 0.55;
94     // This is a slope, i.e., (arrow tip height)/(0.5 * arrow tip width).
95     const CGFloat kArrowTipSlope = 1.2;
97     // What proportion of the arrow tip width is the stem, i.e., (stem
98     // width)/(arrow tip width).
99     const CGFloat kArrowStemProportion = 0.33;
101     NSPoint arrowTipPos = [self dropArrowPosition];
102     arrowTipPos.x = std::floor(arrowTipPos.x);  // Draw on the pixel.
103     arrowTipPos.y += kArrowBottomInset;  // Inset on the bottom.
105     // Height we have to work with (insetting on the top).
106     CGFloat availableHeight =
107         NSMaxY([self bounds]) - arrowTipPos.y - kArrowTopInset;
108     DCHECK(availableHeight >= 5);
110     // Based on the knobs above, calculate actual dimensions which we'll need
111     // for drawing.
112     CGFloat arrowTipHeight = kArrowTipProportion * availableHeight;
113     CGFloat arrowTipWidth = 2 * arrowTipHeight / kArrowTipSlope;
114     CGFloat arrowStemHeight = availableHeight - arrowTipHeight;
115     CGFloat arrowStemWidth = kArrowStemProportion * arrowTipWidth;
116     CGFloat arrowStemInset = (arrowTipWidth - arrowStemWidth) / 2;
118     // The line width is arbitrary, but our path really should be mitered.
119     NSBezierPath* arrow = [NSBezierPath bezierPath];
120     [arrow setLineJoinStyle:NSMiterLineJoinStyle];
121     [arrow setLineWidth:1];
123     // Define the arrow's shape! We start from the tip and go clockwise.
124     [arrow moveToPoint:arrowTipPos];
125     [arrow relativeLineToPoint:NSMakePoint(-arrowTipWidth / 2, arrowTipHeight)];
126     [arrow relativeLineToPoint:NSMakePoint(arrowStemInset, 0)];
127     [arrow relativeLineToPoint:NSMakePoint(0, arrowStemHeight)];
128     [arrow relativeLineToPoint:NSMakePoint(arrowStemWidth, 0)];
129     [arrow relativeLineToPoint:NSMakePoint(0, -arrowStemHeight)];
130     [arrow relativeLineToPoint:NSMakePoint(arrowStemInset, 0)];
131     [arrow closePath];
133     // Draw and fill the arrow.
134     [[NSColor colorWithCalibratedWhite:0 alpha:0.67] set];
135     [arrow stroke];
136     [[NSColor colorWithCalibratedWhite:1 alpha:0.67] setFill];
137     [arrow fill];
138   }
141 // YES if a double-click in the background of the tab strip minimizes the
142 // window.
143 - (BOOL)doubleClickMinimizesWindow {
144   return YES;
147 // We accept first mouse so clicks onto close/zoom/miniaturize buttons and
148 // title bar double-clicks are properly detected even when the window is in the
149 // background.
150 - (BOOL)acceptsFirstMouse:(NSEvent*)event {
151   return YES;
154 // Trap double-clicks and make them miniaturize the browser window.
155 - (void)mouseUp:(NSEvent*)event {
156   // Bail early if double-clicks are disabled.
157   if (![self doubleClickMinimizesWindow]) {
158     [super mouseUp:event];
159     return;
160   }
162   NSInteger clickCount = [event clickCount];
163   NSTimeInterval timestamp = [event timestamp];
165   // Double-clicks on Zoom/Close/Mininiaturize buttons shouldn't cause
166   // miniaturization. For those, we miss the first click but get the second
167   // (with clickCount == 2!). We thus check that we got a first click shortly
168   // before (measured up-to-up) a double-click. Cocoa doesn't have a documented
169   // way of getting the proper interval (= (double-click-threshold) +
170   // (drag-threshold); the former is Carbon GetDblTime()/60.0 or
171   // com.apple.mouse.doubleClickThreshold [undocumented]). So we hard-code
172   // "short" as 0.8 seconds. (Measuring up-to-up isn't enough to properly
173   // detect double-clicks, but we're actually using Cocoa for that.)
174   if (clickCount == 2 && (timestamp - lastMouseUp_) < 0.8) {
175     ui::WindowTitlebarReceivedDoubleClick([self window], self);
176   } else {
177     [super mouseUp:event];
178   }
180   // If clickCount is 0, the drag threshold was passed.
181   lastMouseUp_ = (clickCount == 1) ? timestamp : -1000.0;
184 // (URLDropTarget protocol)
185 - (id<URLDropTargetController>)urlDropController {
186   return controller_;
189 // (URLDropTarget protocol)
190 - (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender {
191   return [dropHandler_ draggingEntered:sender];
194 // (URLDropTarget protocol)
195 - (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)sender {
196   return [dropHandler_ draggingUpdated:sender];
199 // (URLDropTarget protocol)
200 - (void)draggingExited:(id<NSDraggingInfo>)sender {
201   return [dropHandler_ draggingExited:sender];
204 // (URLDropTarget protocol)
205 - (BOOL)performDragOperation:(id<NSDraggingInfo>)sender {
206   return [dropHandler_ performDragOperation:sender];
209 - (BOOL)accessibilityIsIgnored {
210   return NO;
213 // Returns AX children (tabs and new tab button), sorted from left to right.
214 - (NSArray*)tabStripViewAccessibilityChildren {
215   NSArray* children =
216       [super accessibilityAttributeValue:NSAccessibilityChildrenAttribute];
217   return [children sortedArrayUsingComparator:
218       ^NSComparisonResult(id first, id second) {
219           NSPoint firstPosition =
220               [[first accessibilityAttributeValue:
221                           NSAccessibilityPositionAttribute] pointValue];
222           NSPoint secondPosition =
223               [[second accessibilityAttributeValue:
224                            NSAccessibilityPositionAttribute] pointValue];
225           if (firstPosition.x < secondPosition.x)
226             return NSOrderedAscending;
227           else if (firstPosition.x > secondPosition.x)
228             return NSOrderedDescending;
229           else
230             return NSOrderedSame;
231       }];
234 - (id)accessibilityAttributeValue:(NSString*)attribute {
235   if ([attribute isEqual:NSAccessibilityRoleAttribute]) {
236     return NSAccessibilityTabGroupRole;
237   } else if ([attribute isEqual:NSAccessibilityChildrenAttribute]) {
238     return [self tabStripViewAccessibilityChildren];
239   } else if ([attribute isEqual:NSAccessibilityTabsAttribute]) {
240     NSArray* children = [self tabStripViewAccessibilityChildren];
241     NSIndexSet* indexes = [children indexesOfObjectsPassingTest:
242         ^BOOL(id child, NSUInteger idx, BOOL* stop) {
243             NSString* role = [child
244                 accessibilityAttributeValue:NSAccessibilityRoleAttribute];
245             return [role isEqualToString:NSAccessibilityRadioButtonRole];
246         }];
247     return [children objectsAtIndexes:indexes];
248   } else if ([attribute isEqual:NSAccessibilityContentsAttribute]) {
249     return [self tabStripViewAccessibilityChildren];
250   } else if ([attribute isEqual:NSAccessibilityValueAttribute]) {
251     return [controller_ activeTabView];
252   }
254   return [super accessibilityAttributeValue:attribute];
257 - (NSArray*)accessibilityAttributeNames {
258   NSMutableArray* attributes =
259       [[super accessibilityAttributeNames] mutableCopy];
260   [attributes addObject:NSAccessibilityTabsAttribute];
261   [attributes addObject:NSAccessibilityContentsAttribute];
262   [attributes addObject:NSAccessibilityValueAttribute];
264   return [attributes autorelease];
267 - (ViewID)viewID {
268   return VIEW_ID_TAB_STRIP;
271 - (NewTabButton*)getNewTabButton {
272   return newTabButton_;
275 - (void)setNewTabButton:(NewTabButton*)button {
276   newTabButton_.reset([button retain]);
279 - (void)setController:(TabStripController*)controller {
280   controller_ = controller;
283 // ThemedWindowDrawing implementation.
285 - (void)windowDidChangeTheme {
286   [self setNeedsDisplay:YES];
289 - (void)windowDidChangeActive {
290   [self setNeedsDisplay:YES];
293 @end