• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • 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
23namespace {
24
25// Distance from top border to icon.
26const CGFloat kImagePaddingTop = 7;
27
28// Distance from left border to icon.
29const CGFloat kImagePaddingLeft = 9;
30
31// Width of icon.
32const CGFloat kImageWidth = 16;
33
34// Height of icon.
35const CGFloat kImageHeight = 16;
36
37// x coordinate of download name string, in view coords.
38const CGFloat kTextPosLeft = kImagePaddingLeft +
39    kImageWidth + download_util::kSmallProgressIconOffset;
40
41// Distance from end of download name string to dropdown area.
42const CGFloat kTextPaddingRight = 3;
43
44// y coordinate of download name string, in view coords, when status message
45// is visible.
46const CGFloat kPrimaryTextPosTop = 3;
47
48// y coordinate of download name string, in view coords, when status message
49// is not visible.
50const CGFloat kPrimaryTextOnlyPosTop = 10;
51
52// y coordinate of status message, in view coords.
53const CGFloat kSecondaryTextPosTop = 18;
54
55// Grey value of status text.
56const CGFloat kSecondaryTextColor = 0.5;
57
58// Width of dropdown area on the right (includes 1px for the border on each
59// side).
60const CGFloat kDropdownAreaWidth = 14;
61
62// Width of dropdown arrow.
63const CGFloat kDropdownArrowWidth = 5;
64
65// Height of dropdown arrow.
66const CGFloat kDropdownArrowHeight = 3;
67
68// Vertical displacement of dropdown area, relative to the "centered" position.
69const CGFloat kDropdownAreaY = -2;
70
71// Duration of the two-lines-to-one-line animation, in seconds.
72NSTimeInterval kHideStatusDuration = 0.3;
73
74// Duration of the 'download complete' animation, in seconds.
75const int kCompleteAnimationDuration = 2.5;
76
77// Duration of the 'download interrupted' animation, in seconds.
78const 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
91class BackgroundTheme : public ui::ThemeProvider {
92public:
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
108private:
109  ui::ThemeProvider* provider_;
110  scoped_nsobject<NSGradient> buttonGradient_;
111  scoped_nsobject<NSGradient> buttonPressedGradient_;
112  scoped_nsobject<NSColor> borderColor_;
113};
114
115BackgroundTheme::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
134NSImage* BackgroundTheme::GetNSImageNamed(int id, bool allow_default) const {
135  return nil;
136}
137
138NSColor* BackgroundTheme::GetNSImageColorNamed(int id,
139                                               bool allow_default) const {
140  return nil;
141}
142
143NSColor* BackgroundTheme::GetNSColor(int id, bool allow_default) const {
144  return provider_->GetNSColor(id, allow_default);
145}
146
147NSColor* 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
154NSGradient* 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