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];
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];
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];
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,
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
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)];
133 // Draw and fill the arrow.
134 [[NSColor colorWithCalibratedWhite:0 alpha:0.67] set];
136 [[NSColor colorWithCalibratedWhite:1 alpha:0.67] setFill];
141 // YES if a double-click in the background of the tab strip minimizes the
143 - (BOOL)doubleClickMinimizesWindow {
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
150 - (BOOL)acceptsFirstMouse:(NSEvent*)event {
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];
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);
177 [super mouseUp:event];
180 // If clickCount is 0, the drag threshold was passed.
181 lastMouseUp_ = (clickCount == 1) ? timestamp : -1000.0;
184 // (URLDropTarget protocol)
185 - (id<URLDropTargetController>)urlDropController {
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 {
213 // Returns AX children (tabs and new tab button), sorted from left to right.
214 - (NSArray*)tabStripViewAccessibilityChildren {
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;
230 return NSOrderedSame;
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];
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];
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];
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];