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