- add sources.
[platform/framework/web/crosswalk.git] / src / chrome / browser / ui / cocoa / download / download_item_cell.mm
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #import "chrome/browser/ui/cocoa/download/download_item_cell.h"
6
7 #include "base/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/gfx/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"
23
24 // Distance from top border to icon.
25 const CGFloat kImagePaddingTop = 7;
26
27 // Distance from left border to icon.
28 const CGFloat kImagePaddingLeft = 9;
29
30 // Width of icon.
31 const CGFloat kImageWidth = 16;
32
33 // Height of icon.
34 const CGFloat kImageHeight = 16;
35
36 // x coordinate of download name string, in view coords.
37 const CGFloat kTextPosLeft = kImagePaddingLeft +
38     kImageWidth + DownloadShelf::kSmallProgressIconOffset;
39
40 // Distance from end of download name string to dropdown area.
41 const CGFloat kTextPaddingRight = 3;
42
43 // y coordinate of download name string, in view coords, when status message
44 // is visible.
45 const CGFloat kPrimaryTextPosTop = 3;
46
47 // y coordinate of download name string, in view coords, when status message
48 // is not visible.
49 const CGFloat kPrimaryTextOnlyPosTop = 10;
50
51 // y coordinate of status message, in view coords.
52 const CGFloat kSecondaryTextPosTop = 18;
53
54 // Width of dropdown area on the right (includes 1px for the border on each
55 // side).
56 const CGFloat kDropdownAreaWidth = 14;
57
58 // Width of dropdown arrow.
59 const CGFloat kDropdownArrowWidth = 5;
60
61 // Height of dropdown arrow.
62 const CGFloat kDropdownArrowHeight = 3;
63
64 // Vertical displacement of dropdown area, relative to the "centered" position.
65 const CGFloat kDropdownAreaY = -2;
66
67 // Duration of the two-lines-to-one-line animation, in seconds.
68 NSTimeInterval kShowStatusDuration = 0.3;
69 NSTimeInterval kHideStatusDuration = 0.3;
70
71 // Duration of the 'download complete' animation, in seconds.
72 const CGFloat kCompleteAnimationDuration = 2.5;
73
74 // Duration of the 'download interrupted' animation, in seconds.
75 const CGFloat kInterruptedAnimationDuration = 2.5;
76
77 using content::DownloadItem;
78
79 // This is a helper class to animate the fading out of the status text.
80 @interface DownloadItemCellAnimation : NSAnimation {
81  @private
82   DownloadItemCell* cell_;
83 }
84 - (id)initWithDownloadItemCell:(DownloadItemCell*)cell
85                       duration:(NSTimeInterval)duration
86                 animationCurve:(NSAnimationCurve)animationCurve;
87
88 @end
89
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_;
98 }
99
100 - (id)initWithDownloadItemCell:(DownloadItemCell*)cell;
101 - (void)invalidate;
102
103 @end
104
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
123
124 @implementation DownloadItemCell
125
126 @synthesize secondaryTitle = secondaryTitle_;
127 @synthesize secondaryFont = secondaryFont_;
128
129 - (void)setInitialState {
130   isStatusTextVisible_ = NO;
131   titleY_ = kPrimaryTextPosTop;
132   statusAlpha_ = 0.0;
133   indeterminateProgressAngle_ = DownloadShelf::kStartAngleDegrees;
134
135   [self setFont:[NSFont systemFontOfSize:
136       [NSFont systemFontSizeForControlSize:NSSmallControlSize]]];
137   [self setSecondaryFont:[NSFont systemFontOfSize:
138       [NSFont systemFontSizeForControlSize:NSSmallControlSize]]];
139
140   [self updateTrackingAreas:self];
141   [[NSNotificationCenter defaultCenter]
142       addObserver:self
143          selector:@selector(updateTrackingAreas:)
144              name:NSViewFrameDidChangeNotification
145            object:[self controlView]];
146 }
147
148 // For nib instantiations
149 - (id)initWithCoder:(NSCoder*)decoder {
150   if ((self = [super initWithCoder:decoder])) {
151     [self setInitialState];
152   }
153   return self;
154 }
155
156 // For programmatic instantiations.
157 - (id)initTextCell:(NSString *)string {
158   if ((self = [super initTextCell:string])) {
159     [self setInitialState];
160   }
161   return self;
162 }
163
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];
182 }
183
184 - (void)setStateFromDownload:(DownloadItemModel*)downloadModel {
185   // Set the name of the download.
186   downloadPath_ = downloadModel->download()->GetFileNameToReportUser();
187
188   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   }
198
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   }
254
255   [[self controlView] setNeedsDisplay:YES];
256 }
257
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   }
267
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);
276
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()];
284
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()];
292 }
293
294 - (void)setShowsBorderOnlyWhileMouseInside:(BOOL)showOnly {
295   // Override to make sure it doesn't do anything if it's called accidentally.
296 }
297
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];
305 }
306
307 - (void)mouseExited:(NSEvent *)theEvent {
308   mouseInsideCount_--;
309   if (mouseInsideCount_ == 0)
310     mousePosition_ = kDownloadItemMouseOutside;
311   [[self controlView] setNeedsDisplay:YES];
312 }
313
314 - (BOOL)isMouseInside {
315   return mousePosition_ != kDownloadItemMouseOutside;
316 }
317
318 - (BOOL)isMouseOverButtonPart {
319   return mousePosition_ == kDownloadItemMouseOverButtonPart;
320 }
321
322 - (BOOL)isButtonPartPressed {
323   return [self isHighlighted]
324       && mousePosition_ == kDownloadItemMouseOverButtonPart;
325 }
326
327 - (BOOL)isMouseOverDropdownPart {
328   return mousePosition_ == kDownloadItemMouseOverDropdownPart;
329 }
330
331 - (BOOL)isDropdownPartPressed {
332   return [self isHighlighted]
333       && mousePosition_ == kDownloadItemMouseOverDropdownPart;
334 }
335
336 - (NSBezierPath*)leftRoundedPath:(CGFloat)radius inRect:(NSRect)rect {
337
338   NSPoint topLeft = NSMakePoint(NSMinX(rect), NSMaxY(rect));
339   NSPoint topRight = NSMakePoint(NSMaxX(rect), NSMaxY(rect));
340   NSPoint bottomRight = NSMakePoint(NSMaxX(rect) , NSMinY(rect));
341
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;
352 }
353
354 - (NSBezierPath*)rightRoundedPath:(CGFloat)radius inRect:(NSRect)rect {
355
356   NSPoint topLeft = NSMakePoint(NSMinX(rect), NSMaxY(rect));
357   NSPoint topRight = NSMakePoint(NSMaxX(rect), NSMaxY(rect));
358   NSPoint bottomRight = NSMakePoint(NSMaxX(rect), NSMinY(rect));
359
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;
370 }
371
372 - (NSString*)elideTitle:(int)availableWidth {
373   NSFont* font = [self font];
374   gfx::Font font_chr(base::SysNSStringToUTF8([font fontName]),
375                      [font pointSize]);
376
377   return base::SysUTF16ToNSString(
378       gfx::ElideFilename(downloadPath_, font_chr, availableWidth));
379 }
380
381 - (NSString*)elideStatus:(int)availableWidth {
382   NSFont* font = [self secondaryFont];
383   gfx::Font font_chr(base::SysNSStringToUTF8([font fontName]),
384                      [font pointSize]);
385
386   return base::SysUTF16ToNSString(gfx::ElideText(
387       base::SysNSStringToUTF16([self secondaryTitle]),
388       font_chr,
389       availableWidth,
390       gfx::ELIDE_AT_END));
391 }
392
393 - (ui::ThemeProvider*)backgroundThemeWrappingProvider:
394     (ui::ThemeProvider*)provider {
395   if (!themeProvider_.get()) {
396     themeProvider_.reset(new BackgroundTheme(provider));
397   }
398
399   return themeProvider_.get();
400 }
401
402 // Returns if |part| was pressed while the default theme was active.
403 - (BOOL)pressedWithDefaultThemeOnPart:(DownloadItemMousePosition)part {
404   return [self isDefaultTheme] && [self isHighlighted] &&
405           mousePosition_ == part;
406 }
407
408 // Returns the text color that should be used to draw text on |part|.
409 - (NSColor*)titleColorForPart:(DownloadItemMousePosition)part {
410   ui::ThemeProvider* themeProvider =
411       [[[self controlView] window] themeProvider];
412   if ([self pressedWithDefaultThemeOnPart:part] || !themeProvider)
413     return [NSColor alternateSelectedControlTextColor];
414   return themeProvider->GetNSColor(ThemeProperties::COLOR_BOOKMARK_TEXT);
415 }
416
417 - (void)drawSecondaryTitleInRect:(NSRect)innerFrame {
418   if (![self secondaryTitle] || statusAlpha_ <= 0)
419     return;
420
421   CGFloat textWidth = NSWidth(innerFrame) -
422       (kTextPosLeft + kTextPaddingRight + kDropdownAreaWidth);
423   NSString* secondaryText = [self elideStatus:textWidth];
424   NSColor* secondaryColor =
425       [self titleColorForPart:kDownloadItemMouseOverButtonPart];
426
427   // If text is light-on-dark, lightening it alone will do nothing.
428   // Therefore we mute luminance a wee bit before drawing in this case.
429   if (![secondaryColor gtm_isDarkColor])
430     secondaryColor = [secondaryColor gtm_colorByAdjustingLuminance:-0.2];
431
432   NSDictionary* secondaryTextAttributes =
433       [NSDictionary dictionaryWithObjectsAndKeys:
434           secondaryColor, NSForegroundColorAttributeName,
435           [self secondaryFont], NSFontAttributeName,
436           nil];
437   NSPoint secondaryPos =
438       NSMakePoint(innerFrame.origin.x + kTextPosLeft, kSecondaryTextPosTop);
439
440   gfx::ScopedNSGraphicsContextSaveGState contextSave;
441   NSGraphicsContext* nsContext = [NSGraphicsContext currentContext];
442   CGContextRef cgContext = (CGContextRef)[nsContext graphicsPort];
443   [nsContext setCompositingOperation:NSCompositeSourceOver];
444   CGContextSetAlpha(cgContext, statusAlpha_);
445   [secondaryText drawAtPoint:secondaryPos
446               withAttributes:secondaryTextAttributes];
447 }
448
449 - (BOOL)isDefaultTheme {
450   ui::ThemeProvider* themeProvider =
451       [[[self controlView] window] themeProvider];
452   if (!themeProvider)
453     return YES;
454   return !themeProvider->HasCustomImage(IDR_THEME_BUTTON_BACKGROUND);
455 }
456
457 - (void)drawWithFrame:(NSRect)cellFrame inView:(NSView*)controlView {
458   NSRect drawFrame = NSInsetRect(cellFrame, 1.5, 1.5);
459   NSRect innerFrame = NSInsetRect(cellFrame, 2, 2);
460
461   const float radius = 3;
462   NSWindow* window = [controlView window];
463   BOOL active = [window isKeyWindow] || [window isMainWindow];
464
465   // In the default theme, draw download items with the bookmark button
466   // gradient. For some themes, this leads to unreadable text, so draw the item
467   // with a background that looks like windows (some transparent white) if a
468   // theme is used. Use custom theme object with a white color gradient to trick
469   // the superclass into drawing what we want.
470   ui::ThemeProvider* themeProvider =
471       [[[self controlView] window] themeProvider];
472
473   NSGradient* bgGradient = nil;
474   if (![self isDefaultTheme]) {
475     themeProvider = [self backgroundThemeWrappingProvider:themeProvider];
476     bgGradient = themeProvider->GetNSGradient(
477         active ? ThemeProperties::GRADIENT_TOOLBAR_BUTTON :
478                  ThemeProperties::GRADIENT_TOOLBAR_BUTTON_INACTIVE);
479   }
480
481   NSRect buttonDrawRect, dropdownDrawRect;
482   NSDivideRect(drawFrame, &dropdownDrawRect, &buttonDrawRect,
483       kDropdownAreaWidth, NSMaxXEdge);
484
485   NSBezierPath* buttonInnerPath = [self
486       leftRoundedPath:radius inRect:buttonDrawRect];
487   NSBezierPath* dropdownInnerPath = [self
488       rightRoundedPath:radius inRect:dropdownDrawRect];
489
490   // Draw secondary title, if any. Do this before drawing the (transparent)
491   // fill so that the text becomes a bit lighter. The default theme's "pressed"
492   // gradient is not transparent, so only do this if a theme is active.
493   bool drawStatusOnTop =
494       [self pressedWithDefaultThemeOnPart:kDownloadItemMouseOverButtonPart];
495   if (!drawStatusOnTop)
496     [self drawSecondaryTitleInRect:innerFrame];
497
498   // Stroke the borders and appropriate fill gradient.
499   [self drawBorderAndFillForTheme:themeProvider
500                       controlView:controlView
501                         innerPath:buttonInnerPath
502               showClickedGradient:[self isButtonPartPressed]
503             showHighlightGradient:[self isMouseOverButtonPart]
504                        hoverAlpha:0.0
505                            active:active
506                         cellFrame:cellFrame
507                   defaultGradient:bgGradient];
508
509   [self drawBorderAndFillForTheme:themeProvider
510                       controlView:controlView
511                         innerPath:dropdownInnerPath
512               showClickedGradient:[self isDropdownPartPressed]
513             showHighlightGradient:[self isMouseOverDropdownPart]
514                        hoverAlpha:0.0
515                            active:active
516                         cellFrame:cellFrame
517                   defaultGradient:bgGradient];
518
519   [self drawInteriorWithFrame:innerFrame inView:controlView];
520
521   // For the default theme, draw the status text on top of the (opaque) button
522   // gradient.
523   if (drawStatusOnTop)
524     [self drawSecondaryTitleInRect:innerFrame];
525 }
526
527 - (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView*)controlView {
528   // Draw title
529   CGFloat textWidth = NSWidth(cellFrame) -
530       (kTextPosLeft + kTextPaddingRight + kDropdownAreaWidth);
531   [self setTitle:[self elideTitle:textWidth]];
532
533   NSColor* color = [self titleColorForPart:kDownloadItemMouseOverButtonPart];
534   NSString* primaryText = [self title];
535
536   NSDictionary* primaryTextAttributes =
537       [NSDictionary dictionaryWithObjectsAndKeys:
538           color, NSForegroundColorAttributeName,
539           [self font], NSFontAttributeName,
540           nil];
541   NSPoint primaryPos = NSMakePoint(
542       cellFrame.origin.x + kTextPosLeft,
543       titleY_);
544
545   [primaryText drawAtPoint:primaryPos withAttributes:primaryTextAttributes];
546
547   // Draw progress disk
548   {
549     // CanvasSkiaPaint draws its content to the current NSGraphicsContext in its
550     // destructor, which needs to be invoked before the icon is drawn below -
551     // hence this nested block.
552
553     // Always repaint the whole disk.
554     NSPoint imagePosition = [self imageRectForBounds:cellFrame].origin;
555     int x = imagePosition.x - DownloadShelf::kSmallProgressIconOffset;
556     int y = imagePosition.y - DownloadShelf::kSmallProgressIconOffset;
557     NSRect dirtyRect = NSMakeRect(
558         x, y,
559         DownloadShelf::kSmallProgressIconSize,
560         DownloadShelf::kSmallProgressIconSize);
561
562     gfx::CanvasSkiaPaint canvas(dirtyRect, false);
563     canvas.set_composite_alpha(true);
564     if (completionAnimation_.get()) {
565       if ([completionAnimation_ isAnimating]) {
566         if (percentDone_ == -1) {
567           DownloadShelf::PaintDownloadComplete(
568               &canvas,
569               x,
570               y,
571               [completionAnimation_ currentValue],
572               DownloadShelf::SMALL);
573         } else {
574           DownloadShelf::PaintDownloadInterrupted(
575               &canvas,
576               x,
577               y,
578               [completionAnimation_ currentValue],
579               DownloadShelf::SMALL);
580         }
581       }
582     } else if (percentDone_ >= 0 || indeterminateProgressTimer_) {
583       DownloadShelf::PaintDownloadProgress(&canvas,
584                                            x,
585                                            y,
586                                            indeterminateProgressAngle_,
587                                            percentDone_,
588                                            DownloadShelf::SMALL);
589     }
590   }
591
592   // Draw icon
593   [[self image] drawInRect:[self imageRectForBounds:cellFrame]
594                   fromRect:NSZeroRect
595                  operation:NSCompositeSourceOver
596                   fraction:[self isEnabled] ? 1.0 : 0.5
597             respectFlipped:YES
598                      hints:nil];
599
600   // Separator between button and popup parts
601   CGFloat lx = NSMaxX(cellFrame) - kDropdownAreaWidth + 0.5;
602   [[NSColor colorWithDeviceWhite:0.0 alpha:0.1] set];
603   [NSBezierPath strokeLineFromPoint:NSMakePoint(lx, NSMinY(cellFrame) + 1)
604                             toPoint:NSMakePoint(lx, NSMaxY(cellFrame) - 1)];
605   [[NSColor colorWithDeviceWhite:1.0 alpha:0.1] set];
606   [NSBezierPath strokeLineFromPoint:NSMakePoint(lx + 1, NSMinY(cellFrame) + 1)
607                             toPoint:NSMakePoint(lx + 1, NSMaxY(cellFrame) - 1)];
608
609   // Popup arrow. Put center of mass of the arrow in the center of the
610   // dropdown area.
611   CGFloat cx = NSMaxX(cellFrame) - kDropdownAreaWidth/2 + 0.5;
612   CGFloat cy = NSMidY(cellFrame);
613   NSPoint p1 = NSMakePoint(cx - kDropdownArrowWidth/2,
614                            cy - kDropdownArrowHeight/3 + kDropdownAreaY);
615   NSPoint p2 = NSMakePoint(cx + kDropdownArrowWidth/2,
616                            cy - kDropdownArrowHeight/3 + kDropdownAreaY);
617   NSPoint p3 = NSMakePoint(cx, cy + kDropdownArrowHeight*2/3 + kDropdownAreaY);
618   NSBezierPath *triangle = [NSBezierPath bezierPath];
619   [triangle moveToPoint:p1];
620   [triangle lineToPoint:p2];
621   [triangle lineToPoint:p3];
622   [triangle closePath];
623
624   gfx::ScopedNSGraphicsContextSaveGState scopedGState;
625
626   base::scoped_nsobject<NSShadow> shadow([[NSShadow alloc] init]);
627   [shadow.get() setShadowColor:[NSColor whiteColor]];
628   [shadow.get() setShadowOffset:NSMakeSize(0, -1)];
629   [shadow setShadowBlurRadius:0.0];
630   [shadow set];
631
632   NSColor* fill = [self titleColorForPart:kDownloadItemMouseOverDropdownPart];
633   [fill setFill];
634
635   [triangle fill];
636 }
637
638 - (NSRect)imageRectForBounds:(NSRect)cellFrame {
639   return NSMakeRect(cellFrame.origin.x + kImagePaddingLeft,
640                     cellFrame.origin.y + kImagePaddingTop,
641                     kImageWidth,
642                     kImageHeight);
643 }
644
645 - (void)setupToggleStatusVisibilityAnimation {
646   if (toggleStatusVisibilityAnimation_ &&
647       [toggleStatusVisibilityAnimation_ isAnimating]) {
648     // If the animation is running, cancel the animation and show/hide the
649     // status text immediately.
650     [toggleStatusVisibilityAnimation_ stopAnimation];
651     [self animation:toggleStatusVisibilityAnimation_ progressed:1.0];
652     toggleStatusVisibilityAnimation_.reset();
653   } else {
654     // Don't use core animation -- text in CA layers is not subpixel antialiased
655     toggleStatusVisibilityAnimation_.reset([[DownloadItemCellAnimation alloc]
656         initWithDownloadItemCell:self
657                         duration:kShowStatusDuration
658                   animationCurve:NSAnimationEaseIn]);
659     [toggleStatusVisibilityAnimation_.get() setDelegate:self];
660     [toggleStatusVisibilityAnimation_.get() startAnimation];
661   }
662 }
663
664 - (void)showSecondaryTitle {
665   if (isStatusTextVisible_)
666     return;
667   isStatusTextVisible_ = YES;
668   [self setupToggleStatusVisibilityAnimation];
669 }
670
671 - (void)hideSecondaryTitle {
672   if (!isStatusTextVisible_)
673     return;
674   isStatusTextVisible_ = NO;
675   [self setupToggleStatusVisibilityAnimation];
676 }
677
678 - (IndeterminateProgressTimer*)indeterminateProgressTimer {
679   return indeterminateProgressTimer_;
680 }
681
682 - (void)animation:(NSAnimation*)animation
683    progressed:(NSAnimationProgress)progress {
684   if (animation == toggleStatusVisibilityAnimation_) {
685     if (isStatusTextVisible_) {
686       titleY_ = (1 - progress)*kPrimaryTextOnlyPosTop + kPrimaryTextPosTop;
687       statusAlpha_ = progress;
688     } else {
689       titleY_ = progress*kPrimaryTextOnlyPosTop +
690           (1 - progress)*kPrimaryTextPosTop;
691       statusAlpha_ = 1 - progress;
692     }
693     [[self controlView] setNeedsDisplay:YES];
694   } else if (animation == completionAnimation_) {
695     [[self controlView] setNeedsDisplay:YES];
696   }
697 }
698
699 - (void)updateIndeterminateDownload {
700   indeterminateProgressAngle_ =
701       (indeterminateProgressAngle_ + DownloadShelf::kUnknownIncrementDegrees) %
702       DownloadShelf::kMaxDegrees;
703   [[self controlView] setNeedsDisplay:YES];
704 }
705
706 - (void)stopIndeterminateAnimation {
707   [indeterminateProgressTimer_ invalidate];
708   indeterminateProgressTimer_.reset();
709 }
710
711 - (void)animationDidEnd:(NSAnimation *)animation {
712   if (animation == toggleStatusVisibilityAnimation_)
713     toggleStatusVisibilityAnimation_.reset();
714   else if (animation == completionAnimation_)
715     completionAnimation_.reset();
716 }
717
718 - (BOOL)isStatusTextVisible {
719   return isStatusTextVisible_;
720 }
721
722 - (CGFloat)statusTextAlpha {
723   return statusAlpha_;
724 }
725
726 - (void)skipVisibilityAnimation {
727   [toggleStatusVisibilityAnimation_ setCurrentProgress:1.0];
728 }
729
730 @end
731
732 @implementation DownloadItemCellAnimation
733
734 - (id)initWithDownloadItemCell:(DownloadItemCell*)cell
735                       duration:(NSTimeInterval)duration
736                 animationCurve:(NSAnimationCurve)animationCurve {
737   if ((self = [super gtm_initWithDuration:duration
738                                 eventMask:NSLeftMouseDownMask
739                            animationCurve:animationCurve])) {
740     cell_ = cell;
741     [self setAnimationBlockingMode:NSAnimationNonblocking];
742   }
743   return self;
744 }
745
746 - (void)setCurrentProgress:(NSAnimationProgress)progress {
747   [super setCurrentProgress:progress];
748   [cell_ animation:self progressed:progress];
749 }
750
751 @end
752
753 @implementation IndeterminateProgressTimer
754
755 - (id)initWithDownloadItemCell:(DownloadItemCell*)cell {
756   if ((self = [super init])) {
757     cell_ = cell;
758     timer_.reset([[NSTimer
759         scheduledTimerWithTimeInterval:DownloadShelf::kProgressRateMs / 1000.0
760                                 target:self
761                               selector:@selector(onTimer:)
762                               userInfo:nil
763                                repeats:YES] retain]);
764   }
765   return self;
766 }
767
768 - (void)invalidate {
769   [timer_ invalidate];
770 }
771
772 - (void)onTimer:(NSTimer*)timer {
773   [cell_ updateIndeterminateDownload];
774 }
775
776 @end