• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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_strip_view.h"
6
7#include <cmath>  // floor
8
9#include "base/logging.h"
10#include "base/mac/mac_util.h"
11#include "chrome/browser/themes/theme_service.h"
12#import "chrome/browser/ui/cocoa/browser_window_controller.h"
13#import "chrome/browser/ui/cocoa/new_tab_button.h"
14#import "chrome/browser/ui/cocoa/tabs/tab_strip_controller.h"
15#import "chrome/browser/ui/cocoa/tabs/tab_view.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 "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"
23
24@implementation TabStripView
25
26@synthesize dropArrowShown = dropArrowShown_;
27@synthesize dropArrowPosition = dropArrowPosition_;
28
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)];
35
36    // Set lastMouseUp_ = -1000.0 so that timestamp-lastMouseUp_ is big unless
37    // lastMouseUp_ has been reset.
38    lastMouseUp_ = -1000.0;
39
40    // Register to be an URL drop target.
41    dropHandler_.reset([[URLDropTargetHandler alloc] initWithView:self]);
42
43    [self setWantsLayer:YES];
44  }
45  return self;
46}
47
48// Draw bottom border bitmap. Each tab is responsible for mimicking this bottom
49// border, unless it's the selected tab.
50- (void)drawBorder:(NSRect)dirtyRect {
51  ThemeService* themeProvider =
52      static_cast<ThemeService*>([[self window] themeProvider]);
53  if (!themeProvider)
54    return;
55
56  // First draw the toolbar bitmap, so that theme colors can shine through.
57  CGFloat backgroundHeight = 2 * [self cr_lineWidth];
58  if (NSMinY(dirtyRect) < backgroundHeight) {
59    gfx::ScopedNSGraphicsContextSaveGState scopedGState;
60    NSGraphicsContext *context = [NSGraphicsContext currentContext];
61    NSPoint position = [[self window] themeImagePositionForAlignment:
62        THEME_IMAGE_ALIGN_WITH_TAB_STRIP];
63    [context cr_setPatternPhase:position forView:self];
64
65    // Themes don't have an inactive image so only look for one if there's no
66    // theme.
67    bool active = [[self window] isKeyWindow] || [[self window] isMainWindow] ||
68                  !themeProvider->UsingDefaultTheme();
69    int resource_id = active ? IDR_THEME_TOOLBAR : IDR_THEME_TOOLBAR_INACTIVE;
70    [themeProvider->GetNSImageColorNamed(resource_id) set];
71    NSRectFill(
72        NSMakeRect(NSMinX(dirtyRect), 0, NSWidth(dirtyRect), backgroundHeight));
73  }
74
75  // Draw the border bitmap, which is partially transparent.
76  NSImage* image = themeProvider->GetNSImageNamed(IDR_TOOLBAR_SHADE_TOP);
77  if (NSMinY(dirtyRect) >= [image size].height)
78    return;
79
80  NSRect borderRect = dirtyRect;
81  borderRect.size.height = [image size].height;
82  borderRect.origin.y = 0;
83
84  BOOL focused = [[self window] isKeyWindow] || [[self window] isMainWindow];
85  NSDrawThreePartImage(borderRect, nil, image, nil, /*vertical=*/ NO,
86                       NSCompositeSourceOver,
87                       focused ?  1.0 : tabs::kImageNoFocusAlpha,
88                       /*flipped=*/ NO);
89}
90
91- (void)drawRect:(NSRect)rect {
92  NSRect boundsRect = [self bounds];
93
94  [self drawBorder:boundsRect];
95
96  // Draw drop-indicator arrow (if appropriate).
97  // TODO(viettrungluu): this is all a stop-gap measure.
98  if ([self dropArrowShown]) {
99    // Programmer art: an arrow parametrized by many knobs. Note that the arrow
100    // points downwards (so understand "width" and "height" accordingly).
101
102    // How many (pixels) to inset on the top/bottom.
103    const CGFloat kArrowTopInset = 1.5;
104    const CGFloat kArrowBottomInset = 1;
105
106    // What proportion of the vertical space is dedicated to the arrow tip,
107    // i.e., (arrow tip height)/(amount of vertical space).
108    const CGFloat kArrowTipProportion = 0.55;
109
110    // This is a slope, i.e., (arrow tip height)/(0.5 * arrow tip width).
111    const CGFloat kArrowTipSlope = 1.2;
112
113    // What proportion of the arrow tip width is the stem, i.e., (stem
114    // width)/(arrow tip width).
115    const CGFloat kArrowStemProportion = 0.33;
116
117    NSPoint arrowTipPos = [self dropArrowPosition];
118    arrowTipPos.x = std::floor(arrowTipPos.x);  // Draw on the pixel.
119    arrowTipPos.y += kArrowBottomInset;  // Inset on the bottom.
120
121    // Height we have to work with (insetting on the top).
122    CGFloat availableHeight =
123        NSMaxY(boundsRect) - arrowTipPos.y - kArrowTopInset;
124    DCHECK(availableHeight >= 5);
125
126    // Based on the knobs above, calculate actual dimensions which we'll need
127    // for drawing.
128    CGFloat arrowTipHeight = kArrowTipProportion * availableHeight;
129    CGFloat arrowTipWidth = 2 * arrowTipHeight / kArrowTipSlope;
130    CGFloat arrowStemHeight = availableHeight - arrowTipHeight;
131    CGFloat arrowStemWidth = kArrowStemProportion * arrowTipWidth;
132    CGFloat arrowStemInset = (arrowTipWidth - arrowStemWidth) / 2;
133
134    // The line width is arbitrary, but our path really should be mitered.
135    NSBezierPath* arrow = [NSBezierPath bezierPath];
136    [arrow setLineJoinStyle:NSMiterLineJoinStyle];
137    [arrow setLineWidth:1];
138
139    // Define the arrow's shape! We start from the tip and go clockwise.
140    [arrow moveToPoint:arrowTipPos];
141    [arrow relativeLineToPoint:NSMakePoint(-arrowTipWidth / 2, arrowTipHeight)];
142    [arrow relativeLineToPoint:NSMakePoint(arrowStemInset, 0)];
143    [arrow relativeLineToPoint:NSMakePoint(0, arrowStemHeight)];
144    [arrow relativeLineToPoint:NSMakePoint(arrowStemWidth, 0)];
145    [arrow relativeLineToPoint:NSMakePoint(0, -arrowStemHeight)];
146    [arrow relativeLineToPoint:NSMakePoint(arrowStemInset, 0)];
147    [arrow closePath];
148
149    // Draw and fill the arrow.
150    [[NSColor colorWithCalibratedWhite:0 alpha:0.67] set];
151    [arrow stroke];
152    [[NSColor colorWithCalibratedWhite:1 alpha:0.67] setFill];
153    [arrow fill];
154  }
155}
156
157// YES if a double-click in the background of the tab strip minimizes the
158// window.
159- (BOOL)doubleClickMinimizesWindow {
160  return YES;
161}
162
163// We accept first mouse so clicks onto close/zoom/miniaturize buttons and
164// title bar double-clicks are properly detected even when the window is in the
165// background.
166- (BOOL)acceptsFirstMouse:(NSEvent*)event {
167  return YES;
168}
169
170// Trap double-clicks and make them miniaturize the browser window.
171- (void)mouseUp:(NSEvent*)event {
172  // Bail early if double-clicks are disabled.
173  if (![self doubleClickMinimizesWindow]) {
174    [super mouseUp:event];
175    return;
176  }
177
178  NSInteger clickCount = [event clickCount];
179  NSTimeInterval timestamp = [event timestamp];
180
181  // Double-clicks on Zoom/Close/Mininiaturize buttons shouldn't cause
182  // miniaturization. For those, we miss the first click but get the second
183  // (with clickCount == 2!). We thus check that we got a first click shortly
184  // before (measured up-to-up) a double-click. Cocoa doesn't have a documented
185  // way of getting the proper interval (= (double-click-threshold) +
186  // (drag-threshold); the former is Carbon GetDblTime()/60.0 or
187  // com.apple.mouse.doubleClickThreshold [undocumented]). So we hard-code
188  // "short" as 0.8 seconds. (Measuring up-to-up isn't enough to properly
189  // detect double-clicks, but we're actually using Cocoa for that.)
190  if (clickCount == 2 && (timestamp - lastMouseUp_) < 0.8) {
191    if (base::mac::ShouldWindowsMiniaturizeOnDoubleClick())
192      [[self window] performMiniaturize:self];
193  } else {
194    [super mouseUp:event];
195  }
196
197  // If clickCount is 0, the drag threshold was passed.
198  lastMouseUp_ = (clickCount == 1) ? timestamp : -1000.0;
199}
200
201// (URLDropTarget protocol)
202- (id<URLDropTargetController>)urlDropController {
203  return controller_;
204}
205
206// (URLDropTarget protocol)
207- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender {
208  return [dropHandler_ draggingEntered:sender];
209}
210
211// (URLDropTarget protocol)
212- (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)sender {
213  return [dropHandler_ draggingUpdated:sender];
214}
215
216// (URLDropTarget protocol)
217- (void)draggingExited:(id<NSDraggingInfo>)sender {
218  return [dropHandler_ draggingExited:sender];
219}
220
221// (URLDropTarget protocol)
222- (BOOL)performDragOperation:(id<NSDraggingInfo>)sender {
223  return [dropHandler_ performDragOperation:sender];
224}
225
226- (BOOL)accessibilityIsIgnored {
227  return NO;
228}
229
230// Returns AX children (tabs and new tab button), sorted from left to right.
231- (NSArray*)accessibilityChildren {
232  NSArray* children =
233      [super accessibilityAttributeValue:NSAccessibilityChildrenAttribute];
234  return [children sortedArrayUsingComparator:
235      ^NSComparisonResult(id first, id second) {
236          NSPoint firstPosition =
237              [[first accessibilityAttributeValue:
238                          NSAccessibilityPositionAttribute] pointValue];
239          NSPoint secondPosition =
240              [[second accessibilityAttributeValue:
241                           NSAccessibilityPositionAttribute] pointValue];
242          if (firstPosition.x < secondPosition.x)
243            return NSOrderedAscending;
244          else if (firstPosition.x > secondPosition.x)
245            return NSOrderedDescending;
246          else
247            return NSOrderedSame;
248      }];
249}
250
251- (id)accessibilityAttributeValue:(NSString*)attribute {
252  if ([attribute isEqual:NSAccessibilityRoleAttribute]) {
253    return NSAccessibilityTabGroupRole;
254  } else if ([attribute isEqual:NSAccessibilityChildrenAttribute]) {
255    return [self accessibilityChildren];
256  } else if ([attribute isEqual:NSAccessibilityTabsAttribute]) {
257    NSArray* children = [self accessibilityChildren];
258    NSIndexSet* indexes = [children indexesOfObjectsPassingTest:
259        ^BOOL(id child, NSUInteger idx, BOOL* stop) {
260            NSString* role = [child
261                accessibilityAttributeValue:NSAccessibilityRoleAttribute];
262            return [role isEqualToString:NSAccessibilityRadioButtonRole];
263        }];
264    return [children objectsAtIndexes:indexes];
265  } else if ([attribute isEqual:NSAccessibilityContentsAttribute]) {
266    return [self accessibilityChildren];
267  } else if ([attribute isEqual:NSAccessibilityValueAttribute]) {
268    return [controller_ activeTabView];
269  }
270
271  return [super accessibilityAttributeValue:attribute];
272}
273
274- (NSArray*)accessibilityAttributeNames {
275  NSMutableArray* attributes =
276      [[super accessibilityAttributeNames] mutableCopy];
277  [attributes addObject:NSAccessibilityTabsAttribute];
278  [attributes addObject:NSAccessibilityContentsAttribute];
279  [attributes addObject:NSAccessibilityValueAttribute];
280
281  return [attributes autorelease];
282}
283
284- (ViewID)viewID {
285  return VIEW_ID_TAB_STRIP;
286}
287
288- (NewTabButton*)getNewTabButton {
289  return newTabButton_;
290}
291
292- (void)setNewTabButton:(NewTabButton*)button {
293  newTabButton_.reset([button retain]);
294}
295
296- (void)setController:(TabStripController*)controller {
297  controller_ = controller;
298}
299
300@end
301