1 // Copyright (c) 2011 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/profile_menu_button.h" 6 7 #include <algorithm> 8 9 #include "base/logging.h" 10 #import "third_party/GTM/AppKit/GTMFadeTruncatingTextFieldCell.h" 11 12 namespace { 13 14 const CGFloat kTabWidth = 24; 15 const CGFloat kTabHeight = 13; 16 const CGFloat kTabArrowWidth = 7; 17 const CGFloat kTabArrowHeight = 4; 18 const CGFloat kTabRoundRectRadius = 5; 19 const CGFloat kTabDisplayNameMarginY = 3; 20 21 NSColor* GetWhiteWithAlpha(CGFloat alpha) { 22 return [NSColor colorWithCalibratedWhite:1.0 alpha:alpha]; 23 } 24 25 NSColor* GetBlackWithAlpha(CGFloat alpha) { 26 return [NSColor colorWithCalibratedWhite:0.0 alpha:alpha]; 27 } 28 29 } // namespace 30 31 @interface ProfileMenuButton (Private) 32 - (void)commonInit; 33 - (NSPoint)popUpMenuPosition; 34 - (NSImage*)tabImage; 35 - (NSRect)textFieldRect; 36 - (NSBezierPath*)tabPathWithRect:(NSRect)rect 37 radius:(CGFloat)radius; 38 - (NSBezierPath*)downArrowPathWithRect:(NSRect)rect; 39 - (NSImage*)tabImageWithSize:(NSSize)tabSize 40 fillColor:(NSColor*)fillColor 41 isPressed:(BOOL)isPressed; 42 @end 43 44 @implementation ProfileMenuButton 45 46 @synthesize shouldShowProfileDisplayName = shouldShowProfileDisplayName_; 47 48 - (void)commonInit { 49 textFieldCell_.reset( 50 [[GTMFadeTruncatingTextFieldCell alloc] initTextCell:@""]); 51 [textFieldCell_ setBackgroundStyle:NSBackgroundStyleRaised]; 52 [textFieldCell_ setAlignment:NSRightTextAlignment]; 53 [textFieldCell_ setFont:[NSFont systemFontOfSize: 54 [NSFont smallSystemFontSize]]]; 55 } 56 57 - (id)initWithFrame:(NSRect)frame 58 pullsDown:(BOOL)flag { 59 if ((self = [super initWithFrame:frame pullsDown:flag])) 60 [self commonInit]; 61 return self; 62 } 63 64 - (id)initWithCoder:(NSCoder*)decoder { 65 if ((self = [super initWithCoder:decoder])) 66 [self commonInit]; 67 return self; 68 } 69 70 - (void)dealloc { 71 [[NSNotificationCenter defaultCenter] removeObserver:self]; 72 [super dealloc]; 73 } 74 75 - (NSString*)profileDisplayName { 76 return [textFieldCell_ stringValue]; 77 } 78 79 - (void)setProfileDisplayName:(NSString*)name { 80 if (![[textFieldCell_ stringValue] isEqual:name]) { 81 [textFieldCell_ setStringValue:name]; 82 [self setNeedsDisplay:YES]; 83 } 84 } 85 86 - (void)setShouldShowProfileDisplayName:(BOOL)flag { 87 shouldShowProfileDisplayName_ = flag; 88 [self setNeedsDisplay:YES]; 89 } 90 91 - (BOOL)isFlipped { 92 return NO; 93 } 94 95 - (void)viewWillMoveToWindow:(NSWindow*)newWindow { 96 if ([self window] == newWindow) 97 return; 98 99 if ([self window]) { 100 [[NSNotificationCenter defaultCenter] 101 removeObserver:self 102 name:NSWindowDidBecomeMainNotification 103 object:[self window]]; 104 [[NSNotificationCenter defaultCenter] 105 removeObserver:self 106 name:NSWindowDidResignMainNotification 107 object:[self window]]; 108 } 109 110 if (newWindow) { 111 [[NSNotificationCenter defaultCenter] 112 addObserver:self 113 selector:@selector(onWindowFocusChanged:) 114 name:NSWindowDidBecomeMainNotification 115 object:newWindow]; 116 [[NSNotificationCenter defaultCenter] 117 addObserver:self 118 selector:@selector(onWindowFocusChanged:) 119 name:NSWindowDidResignMainNotification 120 object:newWindow]; 121 } 122 } 123 124 - (void)onWindowFocusChanged:(NSNotification*)note { 125 [self setNeedsDisplay:YES]; 126 } 127 128 - (NSRect)tabRect { 129 NSRect bounds = [self bounds]; 130 NSRect tabRect; 131 tabRect.size.width = kTabWidth; 132 tabRect.size.height = kTabHeight; 133 tabRect.origin.x = NSMaxX(bounds) - NSWidth(tabRect); 134 tabRect.origin.y = NSMaxY(bounds) - NSHeight(tabRect); 135 return tabRect; 136 } 137 138 - (NSRect)textFieldRect { 139 NSRect bounds = [self bounds]; 140 NSSize desiredSize = [textFieldCell_ cellSize]; 141 142 NSRect textRect = bounds; 143 textRect.size.height = std::min(desiredSize.height, NSHeight(bounds)); 144 145 // For some reason there's always a 2 pixel gap on the right side of the 146 // text field. Fix it by moving the text field to the right by 2 pixels. 147 textRect.origin.x += 2; 148 149 return textRect; 150 } 151 152 - (NSView*)hitTest:(NSPoint)aPoint { 153 NSView* probe = [super hitTest:aPoint]; 154 if (probe != self) 155 return probe; 156 157 NSPoint viewPoint = [self convertPoint:aPoint fromView:[self superview]]; 158 BOOL isFlipped = [self isFlipped]; 159 if (NSMouseInRect(viewPoint, [self tabRect], isFlipped)) 160 return self; 161 else 162 return nil; 163 } 164 165 - (NSBezierPath*)tabPathWithRect:(NSRect)rect 166 radius:(CGFloat)radius { 167 const NSRect innerRect = NSInsetRect(rect, radius, radius); 168 NSBezierPath* path = [NSBezierPath bezierPath]; 169 170 // Top left 171 [path moveToPoint:NSMakePoint(NSMinX(rect), NSMaxY(rect))]; 172 173 // Bottom left 174 [path lineToPoint:NSMakePoint(NSMinX(rect), NSMinY(innerRect))]; 175 [path appendBezierPathWithArcWithCenter:NSMakePoint(NSMinX(innerRect), 176 NSMinY(innerRect)) 177 radius:radius 178 startAngle:180 179 endAngle:270 180 clockwise:NO]; 181 182 // Bottom right 183 [path lineToPoint:NSMakePoint(NSMaxX(innerRect), NSMinY(rect))]; 184 [path appendBezierPathWithArcWithCenter:NSMakePoint(NSMaxX(innerRect), 185 NSMinY(innerRect)) 186 radius:radius 187 startAngle:270 188 endAngle:360 189 clockwise:NO]; 190 191 // Top right 192 [path lineToPoint:NSMakePoint(NSMaxX(rect), NSMaxY(rect))]; 193 194 [path closePath]; 195 return path; 196 } 197 198 - (NSBezierPath*)downArrowPathWithRect:(NSRect)rect { 199 NSBezierPath* path = [NSBezierPath bezierPath]; 200 201 // Top left 202 [path moveToPoint:NSMakePoint(NSMinX(rect), NSMaxY(rect))]; 203 204 // Bottom middle 205 [path lineToPoint:NSMakePoint(NSMidX(rect), NSMinY(rect))]; 206 207 // Top right 208 [path lineToPoint:NSMakePoint(NSMaxX(rect), NSMaxY(rect))]; 209 210 [path closePath]; 211 return path; 212 } 213 214 - (NSImage*)tabImageWithSize:(NSSize)tabSize 215 fillColor:(NSColor*)fillColor 216 isPressed:(BOOL)isPressed { 217 NSImage* image = [[[NSImage alloc] initWithSize:tabSize] autorelease]; 218 [image lockFocus]; 219 220 // White shadow for inset look 221 [[NSGraphicsContext currentContext] saveGraphicsState]; 222 scoped_nsobject<NSShadow> tabShadow([[NSShadow alloc] init]); 223 [tabShadow.get() setShadowOffset:NSMakeSize(0, -1)]; 224 [tabShadow setShadowBlurRadius:0]; 225 [tabShadow.get() setShadowColor:GetWhiteWithAlpha(0.6)]; 226 [tabShadow set]; 227 228 // Gray outline 229 NSRect tabRect = NSMakeRect(0, 1, tabSize.width, tabSize.height - 1); 230 NSBezierPath* outlinePath = [self tabPathWithRect:tabRect 231 radius:kTabRoundRectRadius]; 232 [[NSColor colorWithCalibratedWhite:0.44 alpha:1.0] set]; 233 [outlinePath fill]; 234 235 [[NSGraphicsContext currentContext] restoreGraphicsState]; 236 237 // Fill 238 NSRect fillRect = NSInsetRect(tabRect, 1, 0); 239 fillRect.size.height -= 1; 240 fillRect.origin.y += 1; 241 NSBezierPath* fillPath = [self tabPathWithRect:fillRect 242 radius:kTabRoundRectRadius - 1]; 243 [fillColor set]; 244 [fillPath fill]; 245 246 // Shading for fill to make the bottom of the tab slightly darker. 247 scoped_nsobject<NSGradient> gradient([[NSGradient alloc] 248 initWithStartingColor:GetBlackWithAlpha(isPressed ? 0.2 : 0.0) 249 endingColor:GetBlackWithAlpha(0.2)]); 250 [gradient drawInBezierPath:fillPath angle:270]; 251 252 // Highlight on top 253 NSRect highlightRect = NSInsetRect(tabRect, 1, 0); 254 highlightRect.size.height = 1; 255 highlightRect.origin.y = NSMaxY(tabRect) - highlightRect.size.height; 256 [GetWhiteWithAlpha(0.5) set]; 257 NSRectFillUsingOperation(highlightRect, NSCompositeSourceOver); 258 259 // Arrow shadow 260 [[NSGraphicsContext currentContext] saveGraphicsState]; 261 scoped_nsobject<NSShadow> arrowShadow([[NSShadow alloc] init]); 262 [arrowShadow.get() setShadowOffset:NSMakeSize(0, -1)]; 263 [arrowShadow setShadowBlurRadius:0]; 264 [arrowShadow.get() setShadowColor:GetBlackWithAlpha(0.6)]; 265 [arrowShadow set]; 266 267 // Down arrow 268 NSRect arrowRect; 269 arrowRect.size.width = kTabArrowWidth; 270 arrowRect.size.height = kTabArrowHeight; 271 arrowRect.origin.x = NSMinX(tabRect) + roundf((NSWidth(tabRect) - 272 NSWidth(arrowRect)) / 2.0); 273 arrowRect.origin.y = NSMinY(tabRect) + roundf((tabRect.size.height - 274 arrowRect.size.height) / 2.0); 275 NSBezierPath* arrowPath = [self downArrowPathWithRect:arrowRect]; 276 if (isPressed) 277 [[NSColor colorWithCalibratedWhite:0.8 alpha:1.0] set]; 278 else 279 [[NSColor whiteColor] set]; 280 [arrowPath fill]; 281 282 [[NSGraphicsContext currentContext] restoreGraphicsState]; 283 284 [image unlockFocus]; 285 return image; 286 } 287 288 - (NSImage*)tabImage { 289 BOOL isPressed = [[self cell] isHighlighted]; 290 291 // Invalidate the cached image if necessary. 292 if (cachedTabImageIsPressed_ != isPressed) { 293 cachedTabImageIsPressed_ = isPressed; 294 cachedTabImage_.reset(); 295 } 296 297 if (cachedTabImage_) 298 return cachedTabImage_; 299 300 // TODO: Use different colors for different profiles and tint for 301 // the current browser theme. 302 NSColor* fillColor = [NSColor colorWithCalibratedRed:122.0/255.0 303 green:177.0/255.0 304 blue:252.0/255.0 305 alpha:1.0]; 306 NSRect tabRect = [self tabRect]; 307 cachedTabImage_.reset([[self tabImageWithSize:tabRect.size 308 fillColor:fillColor 309 isPressed:isPressed] retain]); 310 return cachedTabImage_; 311 } 312 313 - (void)drawRect:(NSRect)rect { 314 CGFloat alpha = [[self window] isMainWindow] ? 1.0 : 0.5; 315 [[self tabImage] drawInRect:[self tabRect] 316 fromRect:NSZeroRect 317 operation:NSCompositeSourceOver 318 fraction:alpha]; 319 320 if (shouldShowProfileDisplayName_) { 321 NSColor* textColor = [[self window] isMainWindow] ? 322 GetBlackWithAlpha(0.6) : GetBlackWithAlpha(0.4); 323 if (![[textFieldCell_ textColor] isEqual:textColor]) 324 [textFieldCell_ setTextColor:textColor]; 325 [textFieldCell_ drawWithFrame:[self textFieldRect] inView:self]; 326 } 327 } 328 329 - (NSPoint)popUpMenuPosition { 330 NSPoint menuPos = [self tabRect].origin; 331 // By default popUpContextMenu: causes the menu to show up a few pixels above 332 // the point you give it. We need to shift it down a bit so that it lines up 333 // with the bottom of the tab. 334 menuPos.y -= 6; 335 return [self convertPoint:menuPos toView:nil]; 336 } 337 338 - (void) mouseDown:(NSEvent*)event 339 withShowMenuTarget:(id)target { 340 if (![self menu]) { 341 [super mouseDown:event]; 342 return; 343 } 344 345 NSPoint point = [[self superview] 346 convertPointFromBase:[event locationInWindow]]; 347 if (![[self hitTest:point] isEqual:self]) 348 return; 349 350 // Draw the control as depressed. 351 [self highlight:YES]; 352 353 NSEvent* fakeEvent = [NSEvent 354 mouseEventWithType:[event type] 355 location:[self popUpMenuPosition] 356 modifierFlags:[event modifierFlags] 357 timestamp:[event timestamp] 358 windowNumber:[event windowNumber] 359 context:[event context] 360 eventNumber:[event eventNumber] 361 clickCount:[event clickCount] 362 pressure:[event pressure]]; 363 DCHECK([target respondsToSelector: 364 @selector(popUpContextMenu:withEvent:forView:)]); 365 [target popUpContextMenu:[self menu] 366 withEvent:fakeEvent 367 forView:self]; 368 369 [self highlight:NO]; 370 } 371 372 - (void)mouseDown:(NSEvent*)event { 373 [self mouseDown:event 374 withShowMenuTarget:[NSMenu class]]; 375 } 376 377 - (NSSize)desiredControlSize { 378 NSSize size = [self tabRect].size; 379 380 if (shouldShowProfileDisplayName_) { 381 NSSize textFieldSize = [textFieldCell_ cellSize]; 382 size.width = std::max(size.width, textFieldSize.width); 383 size.height += textFieldSize.height + kTabDisplayNameMarginY; 384 } 385 386 size.width = ceil(size.width); 387 size.height = ceil(size.height); 388 return size; 389 } 390 391 - (NSSize)minControlSize { 392 return [self tabRect].size; 393 } 394 395 @end 396