Home | History | Annotate | Download | only in download
      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/download/download_item_cell.h"
      6 
      7 #include "base/sys_string_conversions.h"
      8 #include "chrome/browser/download/download_item.h"
      9 #include "chrome/browser/download/download_item_model.h"
     10 #include "chrome/browser/download/download_manager.h"
     11 #include "chrome/browser/download/download_util.h"
     12 #import "chrome/browser/themes/theme_service.h"
     13 #import "chrome/browser/ui/cocoa/download/download_item_cell.h"
     14 #import "chrome/browser/ui/cocoa/image_utils.h"
     15 #import "chrome/browser/ui/cocoa/themed_window.h"
     16 #include "grit/theme_resources.h"
     17 #import "third_party/GTM/AppKit/GTMNSAnimation+Duration.h"
     18 #import "third_party/GTM/AppKit/GTMNSColor+Luminance.h"
     19 #include "ui/base/l10n/l10n_util.h"
     20 #include "ui/base/text/text_elider.h"
     21 #include "ui/gfx/canvas_skia_paint.h"
     22 
     23 namespace {
     24 
     25 // Distance from top border to icon.
     26 const CGFloat kImagePaddingTop = 7;
     27 
     28 // Distance from left border to icon.
     29 const CGFloat kImagePaddingLeft = 9;
     30 
     31 // Width of icon.
     32 const CGFloat kImageWidth = 16;
     33 
     34 // Height of icon.
     35 const CGFloat kImageHeight = 16;
     36 
     37 // x coordinate of download name string, in view coords.
     38 const CGFloat kTextPosLeft = kImagePaddingLeft +
     39     kImageWidth + download_util::kSmallProgressIconOffset;
     40 
     41 // Distance from end of download name string to dropdown area.
     42 const CGFloat kTextPaddingRight = 3;
     43 
     44 // y coordinate of download name string, in view coords, when status message
     45 // is visible.
     46 const CGFloat kPrimaryTextPosTop = 3;
     47 
     48 // y coordinate of download name string, in view coords, when status message
     49 // is not visible.
     50 const CGFloat kPrimaryTextOnlyPosTop = 10;
     51 
     52 // y coordinate of status message, in view coords.
     53 const CGFloat kSecondaryTextPosTop = 18;
     54 
     55 // Grey value of status text.
     56 const CGFloat kSecondaryTextColor = 0.5;
     57 
     58 // Width of dropdown area on the right (includes 1px for the border on each
     59 // side).
     60 const CGFloat kDropdownAreaWidth = 14;
     61 
     62 // Width of dropdown arrow.
     63 const CGFloat kDropdownArrowWidth = 5;
     64 
     65 // Height of dropdown arrow.
     66 const CGFloat kDropdownArrowHeight = 3;
     67 
     68 // Vertical displacement of dropdown area, relative to the "centered" position.
     69 const CGFloat kDropdownAreaY = -2;
     70 
     71 // Duration of the two-lines-to-one-line animation, in seconds.
     72 NSTimeInterval kHideStatusDuration = 0.3;
     73 
     74 // Duration of the 'download complete' animation, in seconds.
     75 const int kCompleteAnimationDuration = 2.5;
     76 
     77 // Duration of the 'download interrupted' animation, in seconds.
     78 const int kInterruptedAnimationDuration = 2.5;
     79 
     80 }
     81 
     82 // This is a helper class to animate the fading out of the status text.
     83 @interface DownloadItemCellAnimation : NSAnimation {
     84   DownloadItemCell* cell_;
     85 }
     86 - (id)initWithDownloadItemCell:(DownloadItemCell*)cell
     87                       duration:(NSTimeInterval)duration
     88                 animationCurve:(NSAnimationCurve)animationCurve;
     89 @end
     90 
     91 class BackgroundTheme : public ui::ThemeProvider {
     92 public:
     93   BackgroundTheme(ui::ThemeProvider* provider);
     94 
     95   virtual void Init(Profile* profile) { }
     96   virtual SkBitmap* GetBitmapNamed(int id) const { return nil; }
     97   virtual SkColor GetColor(int id) const { return SkColor(); }
     98   virtual bool GetDisplayProperty(int id, int* result) const { return false; }
     99   virtual bool ShouldUseNativeFrame() const { return false; }
    100   virtual bool HasCustomImage(int id) const { return false; }
    101   virtual RefCountedMemory* GetRawData(int id) const { return NULL; }
    102   virtual NSImage* GetNSImageNamed(int id, bool allow_default) const;
    103   virtual NSColor* GetNSImageColorNamed(int id, bool allow_default) const;
    104   virtual NSColor* GetNSColor(int id, bool allow_default) const;
    105   virtual NSColor* GetNSColorTint(int id, bool allow_default) const;
    106   virtual NSGradient* GetNSGradient(int id) const;
    107 
    108 private:
    109   ui::ThemeProvider* provider_;
    110   scoped_nsobject<NSGradient> buttonGradient_;
    111   scoped_nsobject<NSGradient> buttonPressedGradient_;
    112   scoped_nsobject<NSColor> borderColor_;
    113 };
    114 
    115 BackgroundTheme::BackgroundTheme(ui::ThemeProvider* provider) :
    116     provider_(provider) {
    117   NSColor* bgColor = [NSColor colorWithCalibratedRed:241/255.0
    118                                                green:245/255.0
    119                                                 blue:250/255.0
    120                                                alpha:77/255.0];
    121   NSColor* clickedColor = [NSColor colorWithCalibratedRed:239/255.0
    122                                                     green:245/255.0
    123                                                      blue:252/255.0
    124                                                     alpha:51/255.0];
    125 
    126   borderColor_.reset(
    127       [[NSColor colorWithCalibratedWhite:0 alpha:36/255.0] retain]);
    128   buttonGradient_.reset([[NSGradient alloc]
    129       initWithColors:[NSArray arrayWithObject:bgColor]]);
    130   buttonPressedGradient_.reset([[NSGradient alloc]
    131       initWithColors:[NSArray arrayWithObject:clickedColor]]);
    132 }
    133 
    134 NSImage* BackgroundTheme::GetNSImageNamed(int id, bool allow_default) const {
    135   return nil;
    136 }
    137 
    138 NSColor* BackgroundTheme::GetNSImageColorNamed(int id,
    139                                                bool allow_default) const {
    140   return nil;
    141 }
    142 
    143 NSColor* BackgroundTheme::GetNSColor(int id, bool allow_default) const {
    144   return provider_->GetNSColor(id, allow_default);
    145 }
    146 
    147 NSColor* BackgroundTheme::GetNSColorTint(int id, bool allow_default) const {
    148   if (id == ThemeService::TINT_BUTTONS)
    149     return borderColor_.get();
    150 
    151   return provider_->GetNSColorTint(id, allow_default);
    152 }
    153 
    154 NSGradient* BackgroundTheme::GetNSGradient(int id) const {
    155   switch (id) {
    156     case ThemeService::GRADIENT_TOOLBAR_BUTTON:
    157     case ThemeService::GRADIENT_TOOLBAR_BUTTON_INACTIVE:
    158       return buttonGradient_.get();
    159     case ThemeService::GRADIENT_TOOLBAR_BUTTON_PRESSED:
    160     case ThemeService::GRADIENT_TOOLBAR_BUTTON_PRESSED_INACTIVE:
    161       return buttonPressedGradient_.get();
    162     default:
    163       return provider_->GetNSGradient(id);
    164   }
    165 }
    166 
    167 @interface DownloadItemCell(Private)
    168 - (void)updateTrackingAreas:(id)sender;
    169 - (void)hideSecondaryTitle;
    170 - (void)animation:(NSAnimation*)animation
    171        progressed:(NSAnimationProgress)progress;
    172 - (NSString*)elideTitle:(int)availableWidth;
    173 - (NSString*)elideStatus:(int)availableWidth;
    174 - (ui::ThemeProvider*)backgroundThemeWrappingProvider:(ui::ThemeProvider*)provider;
    175 - (BOOL)pressedWithDefaultThemeOnPart:(DownloadItemMousePosition)part;
    176 - (NSColor*)titleColorForPart:(DownloadItemMousePosition)part;
    177 - (void)drawSecondaryTitleInRect:(NSRect)innerFrame;
    178 @end
    179 
    180 @implementation DownloadItemCell
    181 
    182 @synthesize secondaryTitle = secondaryTitle_;
    183 @synthesize secondaryFont = secondaryFont_;
    184 
    185 - (void)setInitialState {
    186   isStatusTextVisible_ = NO;
    187   titleY_ = kPrimaryTextPosTop;
    188   statusAlpha_ = 1.0;
    189 
    190   [self setFont:[NSFont systemFontOfSize:
    191       [NSFont systemFontSizeForControlSize:NSSmallControlSize]]];
    192   [self setSecondaryFont:[NSFont systemFontOfSize:
    193       [NSFont systemFontSizeForControlSize:NSSmallControlSize]]];
    194 
    195   [self updateTrackingAreas:self];
    196   [[NSNotificationCenter defaultCenter]
    197       addObserver:self
    198          selector:@selector(updateTrackingAreas:)
    199              name:NSViewFrameDidChangeNotification
    200            object:[self controlView]];
    201 }
    202 
    203 // For nib instantiations
    204 - (id)initWithCoder:(NSCoder*)decoder {
    205   if ((self = [super initWithCoder:decoder])) {
    206     [self setInitialState];
    207   }
    208   return self;
    209 }
    210 
    211 // For programmatic instantiations.
    212 - (id)initTextCell:(NSString *)string {
    213   if ((self = [super initTextCell:string])) {
    214     [self setInitialState];
    215   }
    216   return self;
    217 }
    218 
    219 - (void)dealloc {
    220   [[NSNotificationCenter defaultCenter] removeObserver:self];
    221   if ([completionAnimation_ isAnimating])
    222     [completionAnimation_ stopAnimation];
    223   if ([hideStatusAnimation_ isAnimating])
    224     [hideStatusAnimation_ stopAnimation];
    225   if (trackingAreaButton_) {
    226     [[self controlView] removeTrackingArea:trackingAreaButton_];
    227     trackingAreaButton_.reset();
    228   }
    229   if (trackingAreaDropdown_) {
    230     [[self controlView] removeTrackingArea:trackingAreaDropdown_];
    231     trackingAreaDropdown_.reset();
    232   }
    233   [secondaryTitle_ release];
    234   [secondaryFont_ release];
    235   [super dealloc];
    236 }
    237 
    238 - (void)setStateFromDownload:(BaseDownloadItemModel*)downloadModel {
    239   // Set the name of the download.
    240   downloadPath_ = downloadModel->download()->GetFileNameToReportUser();
    241 
    242   string16 statusText = downloadModel->GetStatusText();
    243   if (statusText.empty()) {
    244     // Remove the status text label.
    245     [self hideSecondaryTitle];
    246     isStatusTextVisible_ = NO;
    247   } else {
    248     // Set status text.
    249     NSString* statusString = base::SysUTF16ToNSString(statusText);
    250     [self setSecondaryTitle:statusString];
    251     isStatusTextVisible_ = YES;
    252   }
    253 
    254   switch (downloadModel->download()->state()) {
    255     case DownloadItem::COMPLETE:
    256       // Small downloads may start in a complete state due to asynchronous
    257       // notifications. In this case, we'll get a second complete notification
    258       // via the observers, so we ignore it and avoid creating a second complete
    259       // animation.
    260       if (completionAnimation_.get())
    261         break;
    262       completionAnimation_.reset([[DownloadItemCellAnimation alloc]
    263           initWithDownloadItemCell:self
    264                           duration:kCompleteAnimationDuration
    265                     animationCurve:NSAnimationLinear]);
    266       [completionAnimation_.get() setDelegate:self];
    267       [completionAnimation_.get() startAnimation];
    268       percentDone_ = -1;
    269       break;
    270     case DownloadItem::CANCELLED:
    271       percentDone_ = -1;
    272       break;
    273     case DownloadItem::INTERRUPTED:
    274       // Small downloads may start in an interrupted state due to asynchronous
    275       // notifications. In this case, we'll get a second complete notification
    276       // via the observers, so we ignore it and avoid creating a second complete
    277       // animation.
    278       if (completionAnimation_.get())
    279         break;
    280       completionAnimation_.reset([[DownloadItemCellAnimation alloc]
    281           initWithDownloadItemCell:self
    282                           duration:kInterruptedAnimationDuration
    283                     animationCurve:NSAnimationLinear]);
    284       [completionAnimation_.get() setDelegate:self];
    285       [completionAnimation_.get() startAnimation];
    286       percentDone_ = -2;
    287       break;
    288     case DownloadItem::IN_PROGRESS:
    289       percentDone_ = downloadModel->download()->is_paused() ?
    290           -1 : downloadModel->download()->PercentComplete();
    291       break;
    292     default:
    293       NOTREACHED();
    294   }
    295 
    296   [[self controlView] setNeedsDisplay:YES];
    297 }
    298 
    299 - (void)updateTrackingAreas:(id)sender {
    300   if (trackingAreaButton_) {
    301     [[self controlView] removeTrackingArea:trackingAreaButton_.get()];
    302       trackingAreaButton_.reset(nil);
    303   }
    304   if (trackingAreaDropdown_) {
    305     [[self controlView] removeTrackingArea:trackingAreaDropdown_.get()];
    306       trackingAreaDropdown_.reset(nil);
    307   }
    308 
    309   // Use two distinct tracking rects for left and right parts.
    310   // The tracking areas are also used to decide how to handle clicks. They must
    311   // always be active, so the click is handled correctly when a download item
    312   // is clicked while chrome is not the active app ( http://crbug.com/21916 ).
    313   NSRect bounds = [[self controlView] bounds];
    314   NSRect buttonRect, dropdownRect;
    315   NSDivideRect(bounds, &dropdownRect, &buttonRect,
    316       kDropdownAreaWidth, NSMaxXEdge);
    317 
    318   trackingAreaButton_.reset([[NSTrackingArea alloc]
    319                   initWithRect:buttonRect
    320                        options:(NSTrackingMouseEnteredAndExited |
    321                                 NSTrackingActiveAlways)
    322                          owner:self
    323                     userInfo:nil]);
    324   [[self controlView] addTrackingArea:trackingAreaButton_.get()];
    325 
    326   trackingAreaDropdown_.reset([[NSTrackingArea alloc]
    327                   initWithRect:dropdownRect
    328                        options:(NSTrackingMouseEnteredAndExited |
    329                                 NSTrackingActiveAlways)
    330                          owner:self
    331                     userInfo:nil]);
    332   [[self controlView] addTrackingArea:trackingAreaDropdown_.get()];
    333 }
    334 
    335 - (void)setShowsBorderOnlyWhileMouseInside:(BOOL)showOnly {
    336   // Override to make sure it doesn't do anything if it's called accidentally.
    337 }
    338 
    339 - (void)mouseEntered:(NSEvent*)theEvent {
    340   mouseInsideCount_++;
    341   if ([theEvent trackingArea] == trackingAreaButton_.get())
    342     mousePosition_ = kDownloadItemMouseOverButtonPart;
    343   else if ([theEvent trackingArea] == trackingAreaDropdown_.get())
    344     mousePosition_ = kDownloadItemMouseOverDropdownPart;
    345   [[self controlView] setNeedsDisplay:YES];
    346 }
    347 
    348 - (void)mouseExited:(NSEvent *)theEvent {
    349   mouseInsideCount_--;
    350   if (mouseInsideCount_ == 0)
    351     mousePosition_ = kDownloadItemMouseOutside;
    352   [[self controlView] setNeedsDisplay:YES];
    353 }
    354 
    355 - (BOOL)isMouseInside {
    356   return mousePosition_ != kDownloadItemMouseOutside;
    357 }
    358 
    359 - (BOOL)isMouseOverButtonPart {
    360   return mousePosition_ == kDownloadItemMouseOverButtonPart;
    361 }
    362 
    363 - (BOOL)isButtonPartPressed {
    364   return [self isHighlighted]
    365       && mousePosition_ == kDownloadItemMouseOverButtonPart;
    366 }
    367 
    368 - (BOOL)isMouseOverDropdownPart {
    369   return mousePosition_ == kDownloadItemMouseOverDropdownPart;
    370 }
    371 
    372 - (BOOL)isDropdownPartPressed {
    373   return [self isHighlighted]
    374       && mousePosition_ == kDownloadItemMouseOverDropdownPart;
    375 }
    376 
    377 - (NSBezierPath*)leftRoundedPath:(CGFloat)radius inRect:(NSRect)rect {
    378 
    379   NSPoint topLeft = NSMakePoint(NSMinX(rect), NSMaxY(rect));
    380   NSPoint topRight = NSMakePoint(NSMaxX(rect), NSMaxY(rect));
    381   NSPoint bottomRight = NSMakePoint(NSMaxX(rect) , NSMinY(rect));
    382 
    383   NSBezierPath* path = [NSBezierPath bezierPath];
    384   [path moveToPoint:topRight];
    385   [path appendBezierPathWithArcFromPoint:topLeft
    386                                  toPoint:rect.origin
    387                                   radius:radius];
    388   [path appendBezierPathWithArcFromPoint:rect.origin
    389                                  toPoint:bottomRight
    390                                  radius:radius];
    391   [path lineToPoint:bottomRight];
    392   return path;
    393 }
    394 
    395 - (NSBezierPath*)rightRoundedPath:(CGFloat)radius inRect:(NSRect)rect {
    396 
    397   NSPoint topLeft = NSMakePoint(NSMinX(rect), NSMaxY(rect));
    398   NSPoint topRight = NSMakePoint(NSMaxX(rect), NSMaxY(rect));
    399   NSPoint bottomRight = NSMakePoint(NSMaxX(rect), NSMinY(rect));
    400 
    401   NSBezierPath* path = [NSBezierPath bezierPath];
    402   [path moveToPoint:rect.origin];
    403   [path appendBezierPathWithArcFromPoint:bottomRight
    404                                 toPoint:topRight
    405                                   radius:radius];
    406   [path appendBezierPathWithArcFromPoint:topRight
    407                                 toPoint:topLeft
    408                                  radius:radius];
    409   [path lineToPoint:topLeft];
    410   return path;
    411 }
    412 
    413 - (NSString*)elideTitle:(int)availableWidth {
    414   NSFont* font = [self font];
    415   gfx::Font font_chr(base::SysNSStringToUTF16([font fontName]),
    416                      [font pointSize]);
    417 
    418   return base::SysUTF16ToNSString(
    419       ui::ElideFilename(downloadPath_, font_chr, availableWidth));
    420 }
    421 
    422 - (NSString*)elideStatus:(int)availableWidth {
    423   NSFont* font = [self secondaryFont];
    424   gfx::Font font_chr(base::SysNSStringToUTF16([font fontName]),
    425                      [font pointSize]);
    426 
    427   return base::SysUTF16ToNSString(ui::ElideText(
    428       base::SysNSStringToUTF16([self secondaryTitle]),
    429       font_chr,
    430       availableWidth,
    431       false));
    432 }
    433 
    434 - (ui::ThemeProvider*)backgroundThemeWrappingProvider:(ui::ThemeProvider*)provider {
    435   if (!themeProvider_.get()) {
    436     themeProvider_.reset(new BackgroundTheme(provider));
    437   }
    438 
    439   return themeProvider_.get();
    440 }
    441 
    442 // Returns if |part| was pressed while the default theme was active.
    443 - (BOOL)pressedWithDefaultThemeOnPart:(DownloadItemMousePosition)part {
    444   ui::ThemeProvider* themeProvider =
    445       [[[self controlView] window] themeProvider];
    446   bool isDefaultTheme =
    447       !themeProvider->HasCustomImage(IDR_THEME_BUTTON_BACKGROUND);
    448   return isDefaultTheme && [self isHighlighted] && mousePosition_ == part;
    449 }
    450 
    451 // Returns the text color that should be used to draw text on |part|.
    452 - (NSColor*)titleColorForPart:(DownloadItemMousePosition)part {
    453   ui::ThemeProvider* themeProvider =
    454       [[[self controlView] window] themeProvider];
    455   NSColor* themeTextColor =
    456       themeProvider->GetNSColor(ThemeService::COLOR_BOOKMARK_TEXT,
    457                                 true);
    458   return [self pressedWithDefaultThemeOnPart:part]
    459       ? [NSColor alternateSelectedControlTextColor] : themeTextColor;
    460 }
    461 
    462 - (void)drawSecondaryTitleInRect:(NSRect)innerFrame {
    463   if (![self secondaryTitle] || statusAlpha_ <= 0)
    464     return;
    465 
    466   CGFloat textWidth = innerFrame.size.width -
    467       (kTextPosLeft + kTextPaddingRight + kDropdownAreaWidth);
    468   NSString* secondaryText = [self elideStatus:textWidth];
    469   NSColor* secondaryColor =
    470       [self titleColorForPart:kDownloadItemMouseOverButtonPart];
    471 
    472   // If text is light-on-dark, lightening it alone will do nothing.
    473   // Therefore we mute luminance a wee bit before drawing in this case.
    474   if (![secondaryColor gtm_isDarkColor])
    475     secondaryColor = [secondaryColor gtm_colorByAdjustingLuminance:-0.2];
    476 
    477   NSDictionary* secondaryTextAttributes =
    478       [NSDictionary dictionaryWithObjectsAndKeys:
    479           secondaryColor, NSForegroundColorAttributeName,
    480           [self secondaryFont], NSFontAttributeName,
    481           nil];
    482   NSPoint secondaryPos =
    483       NSMakePoint(innerFrame.origin.x + kTextPosLeft, kSecondaryTextPosTop);
    484   [secondaryText drawAtPoint:secondaryPos
    485               withAttributes:secondaryTextAttributes];
    486 }
    487 
    488 - (void)drawWithFrame:(NSRect)cellFrame inView:(NSView*)controlView {
    489   // Constants from Cole.  Will kConstant them once the feedback loop
    490   // is complete.
    491   NSRect drawFrame = NSInsetRect(cellFrame, 1.5, 1.5);
    492   NSRect innerFrame = NSInsetRect(cellFrame, 2, 2);
    493 
    494   const float radius = 5;
    495   NSWindow* window = [controlView window];
    496   BOOL active = [window isKeyWindow] || [window isMainWindow];
    497 
    498   // In the default theme, draw download items with the bookmark button
    499   // gradient. For some themes, this leads to unreadable text, so draw the item
    500   // with a background that looks like windows (some transparent white) if a
    501   // theme is used. Use custom theme object with a white color gradient to trick
    502   // the superclass into drawing what we want.
    503   ui::ThemeProvider* themeProvider =
    504       [[[self controlView] window] themeProvider];
    505   bool isDefaultTheme =
    506       !themeProvider->HasCustomImage(IDR_THEME_BUTTON_BACKGROUND);
    507 
    508   NSGradient* bgGradient = nil;
    509   if (!isDefaultTheme) {
    510     themeProvider = [self backgroundThemeWrappingProvider:themeProvider];
    511     bgGradient = themeProvider->GetNSGradient(
    512         active ? ThemeService::GRADIENT_TOOLBAR_BUTTON :
    513                  ThemeService::GRADIENT_TOOLBAR_BUTTON_INACTIVE);
    514   }
    515 
    516   NSRect buttonDrawRect, dropdownDrawRect;
    517   NSDivideRect(drawFrame, &dropdownDrawRect, &buttonDrawRect,
    518       kDropdownAreaWidth, NSMaxXEdge);
    519 
    520   NSBezierPath* buttonInnerPath = [self
    521       leftRoundedPath:radius inRect:buttonDrawRect];
    522   NSBezierPath* dropdownInnerPath = [self
    523       rightRoundedPath:radius inRect:dropdownDrawRect];
    524 
    525   // Draw secondary title, if any. Do this before drawing the (transparent)
    526   // fill so that the text becomes a bit lighter. The default theme's "pressed"
    527   // gradient is not transparent, so only do this if a theme is active.
    528   bool drawStatusOnTop =
    529       [self pressedWithDefaultThemeOnPart:kDownloadItemMouseOverButtonPart];
    530   if (!drawStatusOnTop)
    531     [self drawSecondaryTitleInRect:innerFrame];
    532 
    533   // Stroke the borders and appropriate fill gradient.
    534   [self drawBorderAndFillForTheme:themeProvider
    535                       controlView:controlView
    536                         innerPath:buttonInnerPath
    537               showClickedGradient:[self isButtonPartPressed]
    538             showHighlightGradient:[self isMouseOverButtonPart]
    539                        hoverAlpha:0.0
    540                            active:active
    541                         cellFrame:cellFrame
    542                   defaultGradient:bgGradient];
    543 
    544   [self drawBorderAndFillForTheme:themeProvider
    545                       controlView:controlView
    546                         innerPath:dropdownInnerPath
    547               showClickedGradient:[self isDropdownPartPressed]
    548             showHighlightGradient:[self isMouseOverDropdownPart]
    549                        hoverAlpha:0.0
    550                            active:active
    551                         cellFrame:cellFrame
    552                   defaultGradient:bgGradient];
    553 
    554   [self drawInteriorWithFrame:innerFrame inView:controlView];
    555 
    556   // For the default theme, draw the status text on top of the (opaque) button
    557   // gradient.
    558   if (drawStatusOnTop)
    559     [self drawSecondaryTitleInRect:innerFrame];
    560 }
    561 
    562 - (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView*)controlView {
    563   // Draw title
    564   CGFloat textWidth = cellFrame.size.width -
    565       (kTextPosLeft + kTextPaddingRight + kDropdownAreaWidth);
    566   [self setTitle:[self elideTitle:textWidth]];
    567 
    568   NSColor* color = [self titleColorForPart:kDownloadItemMouseOverButtonPart];
    569   NSString* primaryText = [self title];
    570 
    571   NSDictionary* primaryTextAttributes =
    572       [NSDictionary dictionaryWithObjectsAndKeys:
    573           color, NSForegroundColorAttributeName,
    574           [self font], NSFontAttributeName,
    575           nil];
    576   NSPoint primaryPos = NSMakePoint(
    577       cellFrame.origin.x + kTextPosLeft,
    578       titleY_);
    579 
    580   [primaryText drawAtPoint:primaryPos withAttributes:primaryTextAttributes];
    581 
    582   // Draw progress disk
    583   {
    584     // CanvasSkiaPaint draws its content to the current NSGraphicsContext in its
    585     // destructor, which needs to be invoked before the icon is drawn below -
    586     // hence this nested block.
    587 
    588     // Always repaint the whole disk.
    589     NSPoint imagePosition = [self imageRectForBounds:cellFrame].origin;
    590     int x = imagePosition.x - download_util::kSmallProgressIconOffset;
    591     int y = imagePosition.y - download_util::kSmallProgressIconOffset;
    592     NSRect dirtyRect = NSMakeRect(
    593         x, y,
    594         download_util::kSmallProgressIconSize,
    595         download_util::kSmallProgressIconSize);
    596 
    597     gfx::CanvasSkiaPaint canvas(dirtyRect, false);
    598     canvas.set_composite_alpha(true);
    599     if (completionAnimation_.get()) {
    600       if ([completionAnimation_ isAnimating]) {
    601         if (percentDone_ == -1) {
    602           download_util::PaintDownloadComplete(&canvas,
    603               x, y,
    604               [completionAnimation_ currentValue],
    605               download_util::SMALL);
    606         } else {
    607           download_util::PaintDownloadInterrupted(&canvas,
    608               x, y,
    609               [completionAnimation_ currentValue],
    610               download_util::SMALL);
    611         }
    612       }
    613     } else if (percentDone_ >= 0) {
    614       download_util::PaintDownloadProgress(&canvas,
    615           x, y,
    616           download_util::kStartAngleDegrees,  // TODO(thakis): Animate
    617           percentDone_,
    618           download_util::SMALL);
    619     }
    620   }
    621 
    622   // Draw icon
    623   NSRect imageRect = NSZeroRect;
    624   imageRect.size = [[self image] size];
    625   [[self image] drawInRect:[self imageRectForBounds:cellFrame]
    626                   fromRect:imageRect
    627                  operation:NSCompositeSourceOver
    628                   fraction:[self isEnabled] ? 1.0 : 0.5
    629               neverFlipped:YES];
    630 
    631   // Separator between button and popup parts
    632   CGFloat lx = NSMaxX(cellFrame) - kDropdownAreaWidth + 0.5;
    633   [[NSColor colorWithDeviceWhite:0.0 alpha:0.1] set];
    634   [NSBezierPath strokeLineFromPoint:NSMakePoint(lx, NSMinY(cellFrame) + 1)
    635                             toPoint:NSMakePoint(lx, NSMaxY(cellFrame) - 1)];
    636   [[NSColor colorWithDeviceWhite:1.0 alpha:0.1] set];
    637   [NSBezierPath strokeLineFromPoint:NSMakePoint(lx + 1, NSMinY(cellFrame) + 1)
    638                             toPoint:NSMakePoint(lx + 1, NSMaxY(cellFrame) - 1)];
    639 
    640   // Popup arrow. Put center of mass of the arrow in the center of the
    641   // dropdown area.
    642   CGFloat cx = NSMaxX(cellFrame) - kDropdownAreaWidth/2 + 0.5;
    643   CGFloat cy = NSMidY(cellFrame);
    644   NSPoint p1 = NSMakePoint(cx - kDropdownArrowWidth/2,
    645                            cy - kDropdownArrowHeight/3 + kDropdownAreaY);
    646   NSPoint p2 = NSMakePoint(cx + kDropdownArrowWidth/2,
    647                            cy - kDropdownArrowHeight/3 + kDropdownAreaY);
    648   NSPoint p3 = NSMakePoint(cx, cy + kDropdownArrowHeight*2/3 + kDropdownAreaY);
    649   NSBezierPath *triangle = [NSBezierPath bezierPath];
    650   [triangle moveToPoint:p1];
    651   [triangle lineToPoint:p2];
    652   [triangle lineToPoint:p3];
    653   [triangle closePath];
    654 
    655   NSGraphicsContext* context = [NSGraphicsContext currentContext];
    656   [context saveGraphicsState];
    657 
    658   scoped_nsobject<NSShadow> shadow([[NSShadow alloc] init]);
    659   [shadow.get() setShadowColor:[NSColor whiteColor]];
    660   [shadow.get() setShadowOffset:NSMakeSize(0, -1)];
    661   [shadow setShadowBlurRadius:0.0];
    662   [shadow set];
    663 
    664   NSColor* fill = [self titleColorForPart:kDownloadItemMouseOverDropdownPart];
    665   [fill setFill];
    666 
    667   [triangle fill];
    668 
    669   [context restoreGraphicsState];
    670 }
    671 
    672 - (NSRect)imageRectForBounds:(NSRect)cellFrame {
    673   return NSMakeRect(cellFrame.origin.x + kImagePaddingLeft,
    674                     cellFrame.origin.y + kImagePaddingTop,
    675                     kImageWidth,
    676                     kImageHeight);
    677 }
    678 
    679 - (void)hideSecondaryTitle {
    680   if (isStatusTextVisible_) {
    681     // No core animation -- text in CA layers is not subpixel antialiased :-/
    682     hideStatusAnimation_.reset([[DownloadItemCellAnimation alloc]
    683         initWithDownloadItemCell:self
    684                         duration:kHideStatusDuration
    685                   animationCurve:NSAnimationEaseIn]);
    686     [hideStatusAnimation_.get() setDelegate:self];
    687     [hideStatusAnimation_.get() startAnimation];
    688   } else {
    689     // If the download is done so quickly that the status line is never visible,
    690     // don't show an animation
    691     [self animation:nil progressed:1.0];
    692   }
    693 }
    694 
    695 - (void)animation:(NSAnimation*)animation
    696       progressed:(NSAnimationProgress)progress {
    697   if (animation == hideStatusAnimation_ || animation == nil) {
    698     titleY_ = progress*kPrimaryTextOnlyPosTop +
    699         (1 - progress)*kPrimaryTextPosTop;
    700     statusAlpha_ = 1 - progress;
    701     [[self controlView] setNeedsDisplay:YES];
    702   } else if (animation == completionAnimation_) {
    703     [[self controlView] setNeedsDisplay:YES];
    704   }
    705 }
    706 
    707 - (void)animationDidEnd:(NSAnimation *)animation {
    708   if (animation == hideStatusAnimation_)
    709     hideStatusAnimation_.reset();
    710   else if (animation == completionAnimation_)
    711     completionAnimation_.reset();
    712 }
    713 
    714 @end
    715 
    716 @implementation DownloadItemCellAnimation
    717 
    718 - (id)initWithDownloadItemCell:(DownloadItemCell*)cell
    719                       duration:(NSTimeInterval)duration
    720                 animationCurve:(NSAnimationCurve)animationCurve {
    721   if ((self = [super gtm_initWithDuration:duration
    722                                 eventMask:NSLeftMouseDownMask
    723                            animationCurve:animationCurve])) {
    724     cell_ = cell;
    725     [self setAnimationBlockingMode:NSAnimationNonblocking];
    726   }
    727   return self;
    728 }
    729 
    730 - (void)setCurrentProgress:(NSAnimationProgress)progress {
    731   [super setCurrentProgress:progress];
    732   [cell_ animation:self progressed:progress];
    733 }
    734 
    735 @end
    736