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