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