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