Home | History | Annotate | Download | only in cocoa
      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