Disable view source for Developer Tools.
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / download / download_item_cell.mm
blob8665c3fbc977d29189933b88c905fb8df407cd9b
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/strings/sys_string_conversions.h"
8 #include "chrome/browser/download/download_item_model.h"
9 #include "chrome/browser/download/download_shelf.h"
10 #import "chrome/browser/themes/theme_properties.h"
11 #import "chrome/browser/ui/cocoa/download/background_theme.h"
12 #import "chrome/browser/ui/cocoa/themed_window.h"
13 #include "content/public/browser/download_item.h"
14 #include "content/public/browser/download_manager.h"
15 #include "grit/theme_resources.h"
16 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMNSAnimation+Duration.h"
17 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMNSColor+Luminance.h"
18 #include "ui/base/l10n/l10n_util.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"
24 // Distance from top border to icon.
25 const CGFloat kImagePaddingTop = 7;
27 // Distance from left border to icon.
28 const CGFloat kImagePaddingLeft = 9;
30 // Width of icon.
31 const CGFloat kImageWidth = 16;
33 // Height of icon.
34 const CGFloat kImageHeight = 16;
36 // x coordinate of download name string, in view coords.
37 const CGFloat kTextPosLeft = kImagePaddingLeft +
38     kImageWidth + DownloadShelf::kSmallProgressIconOffset;
40 // Distance from end of download name string to dropdown area.
41 const CGFloat kTextPaddingRight = 3;
43 // y coordinate of download name string, in view coords, when status message
44 // is visible.
45 const CGFloat kPrimaryTextPosTop = 3;
47 // y coordinate of download name string, in view coords, when status message
48 // is not visible.
49 const CGFloat kPrimaryTextOnlyPosTop = 10;
51 // y coordinate of status message, in view coords.
52 const CGFloat kSecondaryTextPosTop = 18;
54 // Width of dropdown area on the right (includes 1px for the border on each
55 // side).
56 const CGFloat kDropdownAreaWidth = 14;
58 // Width of dropdown arrow.
59 const CGFloat kDropdownArrowWidth = 5;
61 // Height of dropdown arrow.
62 const CGFloat kDropdownArrowHeight = 3;
64 // Vertical displacement of dropdown area, relative to the "centered" position.
65 const CGFloat kDropdownAreaY = -2;
67 // Duration of the two-lines-to-one-line animation, in seconds.
68 NSTimeInterval kShowStatusDuration = 0.3;
69 NSTimeInterval kHideStatusDuration = 0.3;
71 // Duration of the 'download complete' animation, in seconds.
72 const CGFloat kCompleteAnimationDuration = 2.5;
74 // Duration of the 'download interrupted' animation, in seconds.
75 const CGFloat kInterruptedAnimationDuration = 2.5;
77 using content::DownloadItem;
79 // This is a helper class to animate the fading out of the status text.
80 @interface DownloadItemCellAnimation : NSAnimation {
81  @private
82   DownloadItemCell* cell_;
84 - (id)initWithDownloadItemCell:(DownloadItemCell*)cell
85                       duration:(NSTimeInterval)duration
86                 animationCurve:(NSAnimationCurve)animationCurve;
88 @end
90 // Timer used to animate indeterminate progress. An NSTimer retains its target.
91 // This means that the target must explicitly invalidate the timer before it
92 // can be deleted. This class keeps a weak reference to the target so the
93 // timer can be invalidated from the destructor.
94 @interface IndeterminateProgressTimer : NSObject {
95  @private
96   DownloadItemCell* cell_;
97   base::scoped_nsobject<NSTimer> timer_;
100 - (id)initWithDownloadItemCell:(DownloadItemCell*)cell;
101 - (void)invalidate;
103 @end
105 @interface DownloadItemCell(Private)
106 - (void)updateTrackingAreas:(id)sender;
107 - (void)setupToggleStatusVisibilityAnimation;
108 - (void)showSecondaryTitle;
109 - (void)hideSecondaryTitle;
110 - (void)animation:(NSAnimation*)animation
111        progressed:(NSAnimationProgress)progress;
112 - (void)updateIndeterminateDownload;
113 - (void)stopIndeterminateAnimation;
114 - (NSString*)elideTitle:(int)availableWidth;
115 - (NSString*)elideStatus:(int)availableWidth;
116 - (ui::ThemeProvider*)backgroundThemeWrappingProvider:
117     (ui::ThemeProvider*)provider;
118 - (BOOL)pressedWithDefaultThemeOnPart:(DownloadItemMousePosition)part;
119 - (NSColor*)titleColorForPart:(DownloadItemMousePosition)part;
120 - (void)drawSecondaryTitleInRect:(NSRect)innerFrame;
121 - (BOOL)isDefaultTheme;
122 @end
124 @implementation DownloadItemCell
126 @synthesize secondaryTitle = secondaryTitle_;
127 @synthesize secondaryFont = secondaryFont_;
129 - (void)setInitialState {
130   isStatusTextVisible_ = NO;
131   titleY_ = kPrimaryTextPosTop;
132   statusAlpha_ = 0.0;
133   indeterminateProgressAngle_ = DownloadShelf::kStartAngleDegrees;
135   [self setFont:[NSFont systemFontOfSize:
136       [NSFont systemFontSizeForControlSize:NSSmallControlSize]]];
137   [self setSecondaryFont:[NSFont systemFontOfSize:
138       [NSFont systemFontSizeForControlSize:NSSmallControlSize]]];
140   [self updateTrackingAreas:self];
141   [[NSNotificationCenter defaultCenter]
142       addObserver:self
143          selector:@selector(updateTrackingAreas:)
144              name:NSViewFrameDidChangeNotification
145            object:[self controlView]];
148 // For nib instantiations
149 - (id)initWithCoder:(NSCoder*)decoder {
150   if ((self = [super initWithCoder:decoder])) {
151     [self setInitialState];
152   }
153   return self;
156 // For programmatic instantiations.
157 - (id)initTextCell:(NSString *)string {
158   if ((self = [super initTextCell:string])) {
159     [self setInitialState];
160   }
161   return self;
164 - (void)dealloc {
165   [[NSNotificationCenter defaultCenter] removeObserver:self];
166   if ([completionAnimation_ isAnimating])
167     [completionAnimation_ stopAnimation];
168   if ([toggleStatusVisibilityAnimation_ isAnimating])
169     [toggleStatusVisibilityAnimation_ stopAnimation];
170   if (trackingAreaButton_) {
171     [[self controlView] removeTrackingArea:trackingAreaButton_];
172     trackingAreaButton_.reset();
173   }
174   if (trackingAreaDropdown_) {
175     [[self controlView] removeTrackingArea:trackingAreaDropdown_];
176     trackingAreaDropdown_.reset();
177   }
178   [self stopIndeterminateAnimation];
179   [secondaryTitle_ release];
180   [secondaryFont_ release];
181   [super dealloc];
184 - (void)setStateFromDownload:(DownloadItemModel*)downloadModel {
185   // Set the name of the download.
186   downloadPath_ = downloadModel->download()->GetFileNameToReportUser();
188   base::string16 statusText = downloadModel->GetStatusText();
189   if (statusText.empty()) {
190     // Remove the status text label.
191     [self hideSecondaryTitle];
192   } else {
193     // Set status text.
194     NSString* statusString = base::SysUTF16ToNSString(statusText);
195     [self setSecondaryTitle:statusString];
196     [self showSecondaryTitle];
197   }
199   switch (downloadModel->download()->GetState()) {
200     case DownloadItem::COMPLETE:
201       // Small downloads may start in a complete state due to asynchronous
202       // notifications. In this case, we'll get a second complete notification
203       // via the observers, so we ignore it and avoid creating a second complete
204       // animation.
205       if (completionAnimation_.get())
206         break;
207       completionAnimation_.reset([[DownloadItemCellAnimation alloc]
208           initWithDownloadItemCell:self
209                           duration:kCompleteAnimationDuration
210                     animationCurve:NSAnimationLinear]);
211       [completionAnimation_.get() setDelegate:self];
212       [completionAnimation_.get() startAnimation];
213       percentDone_ = -1;
214       [self stopIndeterminateAnimation];
215       break;
216     case DownloadItem::CANCELLED:
217       percentDone_ = -1;
218       [self stopIndeterminateAnimation];
219       break;
220     case DownloadItem::INTERRUPTED:
221       // Small downloads may start in an interrupted state due to asynchronous
222       // notifications. In this case, we'll get a second complete notification
223       // via the observers, so we ignore it and avoid creating a second complete
224       // animation.
225       if (completionAnimation_.get())
226         break;
227       completionAnimation_.reset([[DownloadItemCellAnimation alloc]
228           initWithDownloadItemCell:self
229                           duration:kInterruptedAnimationDuration
230                     animationCurve:NSAnimationLinear]);
231       [completionAnimation_.get() setDelegate:self];
232       [completionAnimation_.get() startAnimation];
233       percentDone_ = -2;
234       [self stopIndeterminateAnimation];
235       break;
236     case DownloadItem::IN_PROGRESS:
237       if (downloadModel->download()->IsPaused()) {
238         percentDone_ = -1;
239         [self stopIndeterminateAnimation];
240       } else if (downloadModel->PercentComplete() == -1) {
241         percentDone_ = -1;
242         if (!indeterminateProgressTimer_) {
243           indeterminateProgressTimer_.reset([[IndeterminateProgressTimer alloc]
244               initWithDownloadItemCell:self]);
245         }
246       } else {
247         percentDone_ = downloadModel->PercentComplete();
248         [self stopIndeterminateAnimation];
249       }
250       break;
251     default:
252       NOTREACHED();
253   }
255   [[self controlView] setNeedsDisplay:YES];
258 - (void)updateTrackingAreas:(id)sender {
259   if (trackingAreaButton_) {
260     [[self controlView] removeTrackingArea:trackingAreaButton_.get()];
261       trackingAreaButton_.reset(nil);
262   }
263   if (trackingAreaDropdown_) {
264     [[self controlView] removeTrackingArea:trackingAreaDropdown_.get()];
265       trackingAreaDropdown_.reset(nil);
266   }
268   // Use two distinct tracking rects for left and right parts.
269   // The tracking areas are also used to decide how to handle clicks. They must
270   // always be active, so the click is handled correctly when a download item
271   // is clicked while chrome is not the active app ( http://crbug.com/21916 ).
272   NSRect bounds = [[self controlView] bounds];
273   NSRect buttonRect, dropdownRect;
274   NSDivideRect(bounds, &dropdownRect, &buttonRect,
275       kDropdownAreaWidth, NSMaxXEdge);
277   trackingAreaButton_.reset([[NSTrackingArea alloc]
278                   initWithRect:buttonRect
279                        options:(NSTrackingMouseEnteredAndExited |
280                                 NSTrackingActiveAlways)
281                          owner:self
282                     userInfo:nil]);
283   [[self controlView] addTrackingArea:trackingAreaButton_.get()];
285   trackingAreaDropdown_.reset([[NSTrackingArea alloc]
286                   initWithRect:dropdownRect
287                        options:(NSTrackingMouseEnteredAndExited |
288                                 NSTrackingActiveAlways)
289                          owner:self
290                     userInfo:nil]);
291   [[self controlView] addTrackingArea:trackingAreaDropdown_.get()];
294 - (void)setShowsBorderOnlyWhileMouseInside:(BOOL)showOnly {
295   // Override to make sure it doesn't do anything if it's called accidentally.
298 - (void)mouseEntered:(NSEvent*)theEvent {
299   mouseInsideCount_++;
300   if ([theEvent trackingArea] == trackingAreaButton_.get())
301     mousePosition_ = kDownloadItemMouseOverButtonPart;
302   else if ([theEvent trackingArea] == trackingAreaDropdown_.get())
303     mousePosition_ = kDownloadItemMouseOverDropdownPart;
304   [[self controlView] setNeedsDisplay:YES];
307 - (void)mouseExited:(NSEvent *)theEvent {
308   mouseInsideCount_--;
309   if (mouseInsideCount_ == 0)
310     mousePosition_ = kDownloadItemMouseOutside;
311   [[self controlView] setNeedsDisplay:YES];
314 - (BOOL)isMouseInside {
315   return mousePosition_ != kDownloadItemMouseOutside;
318 - (BOOL)isMouseOverButtonPart {
319   return mousePosition_ == kDownloadItemMouseOverButtonPart;
322 - (BOOL)isButtonPartPressed {
323   return [self isHighlighted]
324       && mousePosition_ == kDownloadItemMouseOverButtonPart;
327 - (BOOL)isMouseOverDropdownPart {
328   return mousePosition_ == kDownloadItemMouseOverDropdownPart;
331 - (BOOL)isDropdownPartPressed {
332   return [self isHighlighted]
333       && mousePosition_ == kDownloadItemMouseOverDropdownPart;
336 - (NSBezierPath*)leftRoundedPath:(CGFloat)radius inRect:(NSRect)rect {
338   NSPoint topLeft = NSMakePoint(NSMinX(rect), NSMaxY(rect));
339   NSPoint topRight = NSMakePoint(NSMaxX(rect), NSMaxY(rect));
340   NSPoint bottomRight = NSMakePoint(NSMaxX(rect) , NSMinY(rect));
342   NSBezierPath* path = [NSBezierPath bezierPath];
343   [path moveToPoint:topRight];
344   [path appendBezierPathWithArcFromPoint:topLeft
345                                  toPoint:rect.origin
346                                   radius:radius];
347   [path appendBezierPathWithArcFromPoint:rect.origin
348                                  toPoint:bottomRight
349                                  radius:radius];
350   [path lineToPoint:bottomRight];
351   return path;
354 - (NSBezierPath*)rightRoundedPath:(CGFloat)radius inRect:(NSRect)rect {
356   NSPoint topLeft = NSMakePoint(NSMinX(rect), NSMaxY(rect));
357   NSPoint topRight = NSMakePoint(NSMaxX(rect), NSMaxY(rect));
358   NSPoint bottomRight = NSMakePoint(NSMaxX(rect), NSMinY(rect));
360   NSBezierPath* path = [NSBezierPath bezierPath];
361   [path moveToPoint:rect.origin];
362   [path appendBezierPathWithArcFromPoint:bottomRight
363                                 toPoint:topRight
364                                   radius:radius];
365   [path appendBezierPathWithArcFromPoint:topRight
366                                 toPoint:topLeft
367                                  radius:radius];
368   [path lineToPoint:topLeft];
369   return path;
372 - (NSString*)elideTitle:(int)availableWidth {
373   return base::SysUTF16ToNSString(gfx::ElideFilename(
374       downloadPath_, gfx::FontList(gfx::Font([self font])), availableWidth));
377 - (NSString*)elideStatus:(int)availableWidth {
378   return base::SysUTF16ToNSString(gfx::ElideText(
379       base::SysNSStringToUTF16([self secondaryTitle]),
380       gfx::FontList(gfx::Font([self secondaryFont])),
381       availableWidth,
382       gfx::ELIDE_AT_END));
385 - (ui::ThemeProvider*)backgroundThemeWrappingProvider:
386     (ui::ThemeProvider*)provider {
387   if (!themeProvider_.get()) {
388     themeProvider_.reset(new BackgroundTheme(provider));
389   }
391   return themeProvider_.get();
394 // Returns if |part| was pressed while the default theme was active.
395 - (BOOL)pressedWithDefaultThemeOnPart:(DownloadItemMousePosition)part {
396   return [self isDefaultTheme] && [self isHighlighted] &&
397           mousePosition_ == part;
400 // Returns the text color that should be used to draw text on |part|.
401 - (NSColor*)titleColorForPart:(DownloadItemMousePosition)part {
402   ui::ThemeProvider* themeProvider =
403       [[[self controlView] window] themeProvider];
404   if ([self pressedWithDefaultThemeOnPart:part] || !themeProvider)
405     return [NSColor alternateSelectedControlTextColor];
406   return themeProvider->GetNSColor(ThemeProperties::COLOR_BOOKMARK_TEXT);
409 - (void)drawSecondaryTitleInRect:(NSRect)innerFrame {
410   if (![self secondaryTitle] || statusAlpha_ <= 0)
411     return;
413   CGFloat textWidth = NSWidth(innerFrame) -
414       (kTextPosLeft + kTextPaddingRight + kDropdownAreaWidth);
415   NSString* secondaryText = [self elideStatus:textWidth];
416   NSColor* secondaryColor =
417       [self titleColorForPart:kDownloadItemMouseOverButtonPart];
419   // If text is light-on-dark, lightening it alone will do nothing.
420   // Therefore we mute luminance a wee bit before drawing in this case.
421   if (![secondaryColor gtm_isDarkColor])
422     secondaryColor = [secondaryColor gtm_colorByAdjustingLuminance:-0.2];
424   NSDictionary* secondaryTextAttributes =
425       [NSDictionary dictionaryWithObjectsAndKeys:
426           secondaryColor, NSForegroundColorAttributeName,
427           [self secondaryFont], NSFontAttributeName,
428           nil];
429   NSPoint secondaryPos =
430       NSMakePoint(innerFrame.origin.x + kTextPosLeft, kSecondaryTextPosTop);
432   gfx::ScopedNSGraphicsContextSaveGState contextSave;
433   NSGraphicsContext* nsContext = [NSGraphicsContext currentContext];
434   CGContextRef cgContext = (CGContextRef)[nsContext graphicsPort];
435   [nsContext setCompositingOperation:NSCompositeSourceOver];
436   CGContextSetAlpha(cgContext, statusAlpha_);
437   [secondaryText drawAtPoint:secondaryPos
438               withAttributes:secondaryTextAttributes];
441 - (BOOL)isDefaultTheme {
442   ui::ThemeProvider* themeProvider =
443       [[[self controlView] window] themeProvider];
444   if (!themeProvider)
445     return YES;
446   return !themeProvider->HasCustomImage(IDR_THEME_BUTTON_BACKGROUND);
449 - (void)drawWithFrame:(NSRect)cellFrame inView:(NSView*)controlView {
450   NSRect drawFrame = NSInsetRect(cellFrame, 1.5, 1.5);
451   NSRect innerFrame = NSInsetRect(cellFrame, 2, 2);
453   const float radius = 3;
454   NSWindow* window = [controlView window];
455   BOOL active = [window isKeyWindow] || [window isMainWindow];
457   // In the default theme, draw download items with the bookmark button
458   // gradient. For some themes, this leads to unreadable text, so draw the item
459   // with a background that looks like windows (some transparent white) if a
460   // theme is used. Use custom theme object with a white color gradient to trick
461   // the superclass into drawing what we want.
462   ui::ThemeProvider* themeProvider =
463       [[[self controlView] window] themeProvider];
465   NSGradient* bgGradient = nil;
466   if (![self isDefaultTheme]) {
467     themeProvider = [self backgroundThemeWrappingProvider:themeProvider];
468     bgGradient = themeProvider->GetNSGradient(
469         active ? ThemeProperties::GRADIENT_TOOLBAR_BUTTON :
470                  ThemeProperties::GRADIENT_TOOLBAR_BUTTON_INACTIVE);
471   }
473   NSRect buttonDrawRect, dropdownDrawRect;
474   NSDivideRect(drawFrame, &dropdownDrawRect, &buttonDrawRect,
475       kDropdownAreaWidth, NSMaxXEdge);
477   NSBezierPath* buttonInnerPath = [self
478       leftRoundedPath:radius inRect:buttonDrawRect];
479   NSBezierPath* dropdownInnerPath = [self
480       rightRoundedPath:radius inRect:dropdownDrawRect];
482   // Draw secondary title, if any. Do this before drawing the (transparent)
483   // fill so that the text becomes a bit lighter. The default theme's "pressed"
484   // gradient is not transparent, so only do this if a theme is active.
485   bool drawStatusOnTop =
486       [self pressedWithDefaultThemeOnPart:kDownloadItemMouseOverButtonPart];
487   if (!drawStatusOnTop)
488     [self drawSecondaryTitleInRect:innerFrame];
490   // Stroke the borders and appropriate fill gradient.
491   [self drawBorderAndFillForTheme:themeProvider
492                       controlView:controlView
493                         innerPath:buttonInnerPath
494               showClickedGradient:[self isButtonPartPressed]
495             showHighlightGradient:[self isMouseOverButtonPart]
496                        hoverAlpha:0.0
497                            active:active
498                         cellFrame:cellFrame
499                   defaultGradient:bgGradient];
501   [self drawBorderAndFillForTheme:themeProvider
502                       controlView:controlView
503                         innerPath:dropdownInnerPath
504               showClickedGradient:[self isDropdownPartPressed]
505             showHighlightGradient:[self isMouseOverDropdownPart]
506                        hoverAlpha:0.0
507                            active:active
508                         cellFrame:cellFrame
509                   defaultGradient:bgGradient];
511   [self drawInteriorWithFrame:innerFrame inView:controlView];
513   // For the default theme, draw the status text on top of the (opaque) button
514   // gradient.
515   if (drawStatusOnTop)
516     [self drawSecondaryTitleInRect:innerFrame];
519 - (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView*)controlView {
520   // Draw title
521   CGFloat textWidth = NSWidth(cellFrame) -
522       (kTextPosLeft + kTextPaddingRight + kDropdownAreaWidth);
523   [self setTitle:[self elideTitle:textWidth]];
525   NSColor* color = [self titleColorForPart:kDownloadItemMouseOverButtonPart];
526   NSString* primaryText = [self title];
528   NSDictionary* primaryTextAttributes =
529       [NSDictionary dictionaryWithObjectsAndKeys:
530           color, NSForegroundColorAttributeName,
531           [self font], NSFontAttributeName,
532           nil];
533   NSPoint primaryPos = NSMakePoint(
534       cellFrame.origin.x + kTextPosLeft,
535       titleY_);
537   [primaryText drawAtPoint:primaryPos withAttributes:primaryTextAttributes];
539   // Draw progress disk
540   {
541     // CanvasSkiaPaint draws its content to the current NSGraphicsContext in its
542     // destructor, which needs to be invoked before the icon is drawn below -
543     // hence this nested block.
545     // Always repaint the whole disk.
546     NSPoint imagePosition = [self imageRectForBounds:cellFrame].origin;
547     int x = imagePosition.x - DownloadShelf::kSmallProgressIconOffset;
548     int y = imagePosition.y - DownloadShelf::kSmallProgressIconOffset;
549     NSRect dirtyRect = NSMakeRect(
550         x, y,
551         DownloadShelf::kSmallProgressIconSize,
552         DownloadShelf::kSmallProgressIconSize);
554     gfx::CanvasSkiaPaint canvas(dirtyRect, false);
555     canvas.set_composite_alpha(true);
556     if (completionAnimation_.get()) {
557       if ([completionAnimation_ isAnimating]) {
558         if (percentDone_ == -1) {
559           DownloadShelf::PaintDownloadComplete(
560               &canvas,
561               x,
562               y,
563               [completionAnimation_ currentValue],
564               DownloadShelf::SMALL);
565         } else {
566           DownloadShelf::PaintDownloadInterrupted(
567               &canvas,
568               x,
569               y,
570               [completionAnimation_ currentValue],
571               DownloadShelf::SMALL);
572         }
573       }
574     } else if (percentDone_ >= 0 || indeterminateProgressTimer_) {
575       DownloadShelf::PaintDownloadProgress(&canvas,
576                                            x,
577                                            y,
578                                            indeterminateProgressAngle_,
579                                            percentDone_,
580                                            DownloadShelf::SMALL);
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   indeterminateProgressAngle_ =
693       (indeterminateProgressAngle_ + DownloadShelf::kUnknownIncrementDegrees) %
694       DownloadShelf::kMaxDegrees;
695   [[self controlView] setNeedsDisplay:YES];
698 - (void)stopIndeterminateAnimation {
699   [indeterminateProgressTimer_ invalidate];
700   indeterminateProgressTimer_.reset();
703 - (void)animationDidEnd:(NSAnimation *)animation {
704   if (animation == toggleStatusVisibilityAnimation_)
705     toggleStatusVisibilityAnimation_.reset();
706   else if (animation == completionAnimation_)
707     completionAnimation_.reset();
710 - (BOOL)isStatusTextVisible {
711   return isStatusTextVisible_;
714 - (CGFloat)statusTextAlpha {
715   return statusAlpha_;
718 - (void)skipVisibilityAnimation {
719   [toggleStatusVisibilityAnimation_ setCurrentProgress:1.0];
722 @end
724 @implementation DownloadItemCellAnimation
726 - (id)initWithDownloadItemCell:(DownloadItemCell*)cell
727                       duration:(NSTimeInterval)duration
728                 animationCurve:(NSAnimationCurve)animationCurve {
729   if ((self = [super gtm_initWithDuration:duration
730                                 eventMask:NSLeftMouseDownMask
731                            animationCurve:animationCurve])) {
732     cell_ = cell;
733     [self setAnimationBlockingMode:NSAnimationNonblocking];
734   }
735   return self;
738 - (void)setCurrentProgress:(NSAnimationProgress)progress {
739   [super setCurrentProgress:progress];
740   [cell_ animation:self progressed:progress];
743 @end
745 @implementation IndeterminateProgressTimer
747 - (id)initWithDownloadItemCell:(DownloadItemCell*)cell {
748   if ((self = [super init])) {
749     cell_ = cell;
750     timer_.reset([[NSTimer
751         scheduledTimerWithTimeInterval:DownloadShelf::kProgressRateMs / 1000.0
752                                 target:self
753                               selector:@selector(onTimer:)
754                               userInfo:nil
755                                repeats:YES] retain]);
756   }
757   return self;
760 - (void)invalidate {
761   [timer_ invalidate];
764 - (void)onTimer:(NSTimer*)timer {
765   [cell_ updateIndeterminateDownload];
768 @end