1// Copyright (c) 2011 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#import "chrome/browser/ui/cocoa/download/download_item_cell.h" 6 7#include "base/sys_string_conversions.h" 8#include "chrome/browser/download/download_item.h" 9#include "chrome/browser/download/download_item_model.h" 10#include "chrome/browser/download/download_manager.h" 11#include "chrome/browser/download/download_util.h" 12#import "chrome/browser/themes/theme_service.h" 13#import "chrome/browser/ui/cocoa/download/download_item_cell.h" 14#import "chrome/browser/ui/cocoa/image_utils.h" 15#import "chrome/browser/ui/cocoa/themed_window.h" 16#include "grit/theme_resources.h" 17#import "third_party/GTM/AppKit/GTMNSAnimation+Duration.h" 18#import "third_party/GTM/AppKit/GTMNSColor+Luminance.h" 19#include "ui/base/l10n/l10n_util.h" 20#include "ui/base/text/text_elider.h" 21#include "ui/gfx/canvas_skia_paint.h" 22 23namespace { 24 25// Distance from top border to icon. 26const CGFloat kImagePaddingTop = 7; 27 28// Distance from left border to icon. 29const CGFloat kImagePaddingLeft = 9; 30 31// Width of icon. 32const CGFloat kImageWidth = 16; 33 34// Height of icon. 35const CGFloat kImageHeight = 16; 36 37// x coordinate of download name string, in view coords. 38const CGFloat kTextPosLeft = kImagePaddingLeft + 39 kImageWidth + download_util::kSmallProgressIconOffset; 40 41// Distance from end of download name string to dropdown area. 42const CGFloat kTextPaddingRight = 3; 43 44// y coordinate of download name string, in view coords, when status message 45// is visible. 46const CGFloat kPrimaryTextPosTop = 3; 47 48// y coordinate of download name string, in view coords, when status message 49// is not visible. 50const CGFloat kPrimaryTextOnlyPosTop = 10; 51 52// y coordinate of status message, in view coords. 53const CGFloat kSecondaryTextPosTop = 18; 54 55// Grey value of status text. 56const CGFloat kSecondaryTextColor = 0.5; 57 58// Width of dropdown area on the right (includes 1px for the border on each 59// side). 60const CGFloat kDropdownAreaWidth = 14; 61 62// Width of dropdown arrow. 63const CGFloat kDropdownArrowWidth = 5; 64 65// Height of dropdown arrow. 66const CGFloat kDropdownArrowHeight = 3; 67 68// Vertical displacement of dropdown area, relative to the "centered" position. 69const CGFloat kDropdownAreaY = -2; 70 71// Duration of the two-lines-to-one-line animation, in seconds. 72NSTimeInterval kHideStatusDuration = 0.3; 73 74// Duration of the 'download complete' animation, in seconds. 75const int kCompleteAnimationDuration = 2.5; 76 77// Duration of the 'download interrupted' animation, in seconds. 78const int kInterruptedAnimationDuration = 2.5; 79 80} 81 82// This is a helper class to animate the fading out of the status text. 83@interface DownloadItemCellAnimation : NSAnimation { 84 DownloadItemCell* cell_; 85} 86- (id)initWithDownloadItemCell:(DownloadItemCell*)cell 87 duration:(NSTimeInterval)duration 88 animationCurve:(NSAnimationCurve)animationCurve; 89@end 90 91class BackgroundTheme : public ui::ThemeProvider { 92public: 93 BackgroundTheme(ui::ThemeProvider* provider); 94 95 virtual void Init(Profile* profile) { } 96 virtual SkBitmap* GetBitmapNamed(int id) const { return nil; } 97 virtual SkColor GetColor(int id) const { return SkColor(); } 98 virtual bool GetDisplayProperty(int id, int* result) const { return false; } 99 virtual bool ShouldUseNativeFrame() const { return false; } 100 virtual bool HasCustomImage(int id) const { return false; } 101 virtual RefCountedMemory* GetRawData(int id) const { return NULL; } 102 virtual NSImage* GetNSImageNamed(int id, bool allow_default) const; 103 virtual NSColor* GetNSImageColorNamed(int id, bool allow_default) const; 104 virtual NSColor* GetNSColor(int id, bool allow_default) const; 105 virtual NSColor* GetNSColorTint(int id, bool allow_default) const; 106 virtual NSGradient* GetNSGradient(int id) const; 107 108private: 109 ui::ThemeProvider* provider_; 110 scoped_nsobject<NSGradient> buttonGradient_; 111 scoped_nsobject<NSGradient> buttonPressedGradient_; 112 scoped_nsobject<NSColor> borderColor_; 113}; 114 115BackgroundTheme::BackgroundTheme(ui::ThemeProvider* provider) : 116 provider_(provider) { 117 NSColor* bgColor = [NSColor colorWithCalibratedRed:241/255.0 118 green:245/255.0 119 blue:250/255.0 120 alpha:77/255.0]; 121 NSColor* clickedColor = [NSColor colorWithCalibratedRed:239/255.0 122 green:245/255.0 123 blue:252/255.0 124 alpha:51/255.0]; 125 126 borderColor_.reset( 127 [[NSColor colorWithCalibratedWhite:0 alpha:36/255.0] retain]); 128 buttonGradient_.reset([[NSGradient alloc] 129 initWithColors:[NSArray arrayWithObject:bgColor]]); 130 buttonPressedGradient_.reset([[NSGradient alloc] 131 initWithColors:[NSArray arrayWithObject:clickedColor]]); 132} 133 134NSImage* BackgroundTheme::GetNSImageNamed(int id, bool allow_default) const { 135 return nil; 136} 137 138NSColor* BackgroundTheme::GetNSImageColorNamed(int id, 139 bool allow_default) const { 140 return nil; 141} 142 143NSColor* BackgroundTheme::GetNSColor(int id, bool allow_default) const { 144 return provider_->GetNSColor(id, allow_default); 145} 146 147NSColor* BackgroundTheme::GetNSColorTint(int id, bool allow_default) const { 148 if (id == ThemeService::TINT_BUTTONS) 149 return borderColor_.get(); 150 151 return provider_->GetNSColorTint(id, allow_default); 152} 153 154NSGradient* BackgroundTheme::GetNSGradient(int id) const { 155 switch (id) { 156 case ThemeService::GRADIENT_TOOLBAR_BUTTON: 157 case ThemeService::GRADIENT_TOOLBAR_BUTTON_INACTIVE: 158 return buttonGradient_.get(); 159 case ThemeService::GRADIENT_TOOLBAR_BUTTON_PRESSED: 160 case ThemeService::GRADIENT_TOOLBAR_BUTTON_PRESSED_INACTIVE: 161 return buttonPressedGradient_.get(); 162 default: 163 return provider_->GetNSGradient(id); 164 } 165} 166 167@interface DownloadItemCell(Private) 168- (void)updateTrackingAreas:(id)sender; 169- (void)hideSecondaryTitle; 170- (void)animation:(NSAnimation*)animation 171 progressed:(NSAnimationProgress)progress; 172- (NSString*)elideTitle:(int)availableWidth; 173- (NSString*)elideStatus:(int)availableWidth; 174- (ui::ThemeProvider*)backgroundThemeWrappingProvider:(ui::ThemeProvider*)provider; 175- (BOOL)pressedWithDefaultThemeOnPart:(DownloadItemMousePosition)part; 176- (NSColor*)titleColorForPart:(DownloadItemMousePosition)part; 177- (void)drawSecondaryTitleInRect:(NSRect)innerFrame; 178@end 179 180@implementation DownloadItemCell 181 182@synthesize secondaryTitle = secondaryTitle_; 183@synthesize secondaryFont = secondaryFont_; 184 185- (void)setInitialState { 186 isStatusTextVisible_ = NO; 187 titleY_ = kPrimaryTextPosTop; 188 statusAlpha_ = 1.0; 189 190 [self setFont:[NSFont systemFontOfSize: 191 [NSFont systemFontSizeForControlSize:NSSmallControlSize]]]; 192 [self setSecondaryFont:[NSFont systemFontOfSize: 193 [NSFont systemFontSizeForControlSize:NSSmallControlSize]]]; 194 195 [self updateTrackingAreas:self]; 196 [[NSNotificationCenter defaultCenter] 197 addObserver:self 198 selector:@selector(updateTrackingAreas:) 199 name:NSViewFrameDidChangeNotification 200 object:[self controlView]]; 201} 202 203// For nib instantiations 204- (id)initWithCoder:(NSCoder*)decoder { 205 if ((self = [super initWithCoder:decoder])) { 206 [self setInitialState]; 207 } 208 return self; 209} 210 211// For programmatic instantiations. 212- (id)initTextCell:(NSString *)string { 213 if ((self = [super initTextCell:string])) { 214 [self setInitialState]; 215 } 216 return self; 217} 218 219- (void)dealloc { 220 [[NSNotificationCenter defaultCenter] removeObserver:self]; 221 if ([completionAnimation_ isAnimating]) 222 [completionAnimation_ stopAnimation]; 223 if ([hideStatusAnimation_ isAnimating]) 224 [hideStatusAnimation_ stopAnimation]; 225 if (trackingAreaButton_) { 226 [[self controlView] removeTrackingArea:trackingAreaButton_]; 227 trackingAreaButton_.reset(); 228 } 229 if (trackingAreaDropdown_) { 230 [[self controlView] removeTrackingArea:trackingAreaDropdown_]; 231 trackingAreaDropdown_.reset(); 232 } 233 [secondaryTitle_ release]; 234 [secondaryFont_ release]; 235 [super dealloc]; 236} 237 238- (void)setStateFromDownload:(BaseDownloadItemModel*)downloadModel { 239 // Set the name of the download. 240 downloadPath_ = downloadModel->download()->GetFileNameToReportUser(); 241 242 string16 statusText = downloadModel->GetStatusText(); 243 if (statusText.empty()) { 244 // Remove the status text label. 245 [self hideSecondaryTitle]; 246 isStatusTextVisible_ = NO; 247 } else { 248 // Set status text. 249 NSString* statusString = base::SysUTF16ToNSString(statusText); 250 [self setSecondaryTitle:statusString]; 251 isStatusTextVisible_ = YES; 252 } 253 254 switch (downloadModel->download()->state()) { 255 case DownloadItem::COMPLETE: 256 // Small downloads may start in a complete state due to asynchronous 257 // notifications. In this case, we'll get a second complete notification 258 // via the observers, so we ignore it and avoid creating a second complete 259 // animation. 260 if (completionAnimation_.get()) 261 break; 262 completionAnimation_.reset([[DownloadItemCellAnimation alloc] 263 initWithDownloadItemCell:self 264 duration:kCompleteAnimationDuration 265 animationCurve:NSAnimationLinear]); 266 [completionAnimation_.get() setDelegate:self]; 267 [completionAnimation_.get() startAnimation]; 268 percentDone_ = -1; 269 break; 270 case DownloadItem::CANCELLED: 271 percentDone_ = -1; 272 break; 273 case DownloadItem::INTERRUPTED: 274 // Small downloads may start in an interrupted state due to asynchronous 275 // notifications. In this case, we'll get a second complete notification 276 // via the observers, so we ignore it and avoid creating a second complete 277 // animation. 278 if (completionAnimation_.get()) 279 break; 280 completionAnimation_.reset([[DownloadItemCellAnimation alloc] 281 initWithDownloadItemCell:self 282 duration:kInterruptedAnimationDuration 283 animationCurve:NSAnimationLinear]); 284 [completionAnimation_.get() setDelegate:self]; 285 [completionAnimation_.get() startAnimation]; 286 percentDone_ = -2; 287 break; 288 case DownloadItem::IN_PROGRESS: 289 percentDone_ = downloadModel->download()->is_paused() ? 290 -1 : downloadModel->download()->PercentComplete(); 291 break; 292 default: 293 NOTREACHED(); 294 } 295 296 [[self controlView] setNeedsDisplay:YES]; 297} 298 299- (void)updateTrackingAreas:(id)sender { 300 if (trackingAreaButton_) { 301 [[self controlView] removeTrackingArea:trackingAreaButton_.get()]; 302 trackingAreaButton_.reset(nil); 303 } 304 if (trackingAreaDropdown_) { 305 [[self controlView] removeTrackingArea:trackingAreaDropdown_.get()]; 306 trackingAreaDropdown_.reset(nil); 307 } 308 309 // Use two distinct tracking rects for left and right parts. 310 // The tracking areas are also used to decide how to handle clicks. They must 311 // always be active, so the click is handled correctly when a download item 312 // is clicked while chrome is not the active app ( http://crbug.com/21916 ). 313 NSRect bounds = [[self controlView] bounds]; 314 NSRect buttonRect, dropdownRect; 315 NSDivideRect(bounds, &dropdownRect, &buttonRect, 316 kDropdownAreaWidth, NSMaxXEdge); 317 318 trackingAreaButton_.reset([[NSTrackingArea alloc] 319 initWithRect:buttonRect 320 options:(NSTrackingMouseEnteredAndExited | 321 NSTrackingActiveAlways) 322 owner:self 323 userInfo:nil]); 324 [[self controlView] addTrackingArea:trackingAreaButton_.get()]; 325 326 trackingAreaDropdown_.reset([[NSTrackingArea alloc] 327 initWithRect:dropdownRect 328 options:(NSTrackingMouseEnteredAndExited | 329 NSTrackingActiveAlways) 330 owner:self 331 userInfo:nil]); 332 [[self controlView] addTrackingArea:trackingAreaDropdown_.get()]; 333} 334 335- (void)setShowsBorderOnlyWhileMouseInside:(BOOL)showOnly { 336 // Override to make sure it doesn't do anything if it's called accidentally. 337} 338 339- (void)mouseEntered:(NSEvent*)theEvent { 340 mouseInsideCount_++; 341 if ([theEvent trackingArea] == trackingAreaButton_.get()) 342 mousePosition_ = kDownloadItemMouseOverButtonPart; 343 else if ([theEvent trackingArea] == trackingAreaDropdown_.get()) 344 mousePosition_ = kDownloadItemMouseOverDropdownPart; 345 [[self controlView] setNeedsDisplay:YES]; 346} 347 348- (void)mouseExited:(NSEvent *)theEvent { 349 mouseInsideCount_--; 350 if (mouseInsideCount_ == 0) 351 mousePosition_ = kDownloadItemMouseOutside; 352 [[self controlView] setNeedsDisplay:YES]; 353} 354 355- (BOOL)isMouseInside { 356 return mousePosition_ != kDownloadItemMouseOutside; 357} 358 359- (BOOL)isMouseOverButtonPart { 360 return mousePosition_ == kDownloadItemMouseOverButtonPart; 361} 362 363- (BOOL)isButtonPartPressed { 364 return [self isHighlighted] 365 && mousePosition_ == kDownloadItemMouseOverButtonPart; 366} 367 368- (BOOL)isMouseOverDropdownPart { 369 return mousePosition_ == kDownloadItemMouseOverDropdownPart; 370} 371 372- (BOOL)isDropdownPartPressed { 373 return [self isHighlighted] 374 && mousePosition_ == kDownloadItemMouseOverDropdownPart; 375} 376 377- (NSBezierPath*)leftRoundedPath:(CGFloat)radius inRect:(NSRect)rect { 378 379 NSPoint topLeft = NSMakePoint(NSMinX(rect), NSMaxY(rect)); 380 NSPoint topRight = NSMakePoint(NSMaxX(rect), NSMaxY(rect)); 381 NSPoint bottomRight = NSMakePoint(NSMaxX(rect) , NSMinY(rect)); 382 383 NSBezierPath* path = [NSBezierPath bezierPath]; 384 [path moveToPoint:topRight]; 385 [path appendBezierPathWithArcFromPoint:topLeft 386 toPoint:rect.origin 387 radius:radius]; 388 [path appendBezierPathWithArcFromPoint:rect.origin 389 toPoint:bottomRight 390 radius:radius]; 391 [path lineToPoint:bottomRight]; 392 return path; 393} 394 395- (NSBezierPath*)rightRoundedPath:(CGFloat)radius inRect:(NSRect)rect { 396 397 NSPoint topLeft = NSMakePoint(NSMinX(rect), NSMaxY(rect)); 398 NSPoint topRight = NSMakePoint(NSMaxX(rect), NSMaxY(rect)); 399 NSPoint bottomRight = NSMakePoint(NSMaxX(rect), NSMinY(rect)); 400 401 NSBezierPath* path = [NSBezierPath bezierPath]; 402 [path moveToPoint:rect.origin]; 403 [path appendBezierPathWithArcFromPoint:bottomRight 404 toPoint:topRight 405 radius:radius]; 406 [path appendBezierPathWithArcFromPoint:topRight 407 toPoint:topLeft 408 radius:radius]; 409 [path lineToPoint:topLeft]; 410 return path; 411} 412 413- (NSString*)elideTitle:(int)availableWidth { 414 NSFont* font = [self font]; 415 gfx::Font font_chr(base::SysNSStringToUTF16([font fontName]), 416 [font pointSize]); 417 418 return base::SysUTF16ToNSString( 419 ui::ElideFilename(downloadPath_, font_chr, availableWidth)); 420} 421 422- (NSString*)elideStatus:(int)availableWidth { 423 NSFont* font = [self secondaryFont]; 424 gfx::Font font_chr(base::SysNSStringToUTF16([font fontName]), 425 [font pointSize]); 426 427 return base::SysUTF16ToNSString(ui::ElideText( 428 base::SysNSStringToUTF16([self secondaryTitle]), 429 font_chr, 430 availableWidth, 431 false)); 432} 433 434- (ui::ThemeProvider*)backgroundThemeWrappingProvider:(ui::ThemeProvider*)provider { 435 if (!themeProvider_.get()) { 436 themeProvider_.reset(new BackgroundTheme(provider)); 437 } 438 439 return themeProvider_.get(); 440} 441 442// Returns if |part| was pressed while the default theme was active. 443- (BOOL)pressedWithDefaultThemeOnPart:(DownloadItemMousePosition)part { 444 ui::ThemeProvider* themeProvider = 445 [[[self controlView] window] themeProvider]; 446 bool isDefaultTheme = 447 !themeProvider->HasCustomImage(IDR_THEME_BUTTON_BACKGROUND); 448 return isDefaultTheme && [self isHighlighted] && mousePosition_ == part; 449} 450 451// Returns the text color that should be used to draw text on |part|. 452- (NSColor*)titleColorForPart:(DownloadItemMousePosition)part { 453 ui::ThemeProvider* themeProvider = 454 [[[self controlView] window] themeProvider]; 455 NSColor* themeTextColor = 456 themeProvider->GetNSColor(ThemeService::COLOR_BOOKMARK_TEXT, 457 true); 458 return [self pressedWithDefaultThemeOnPart:part] 459 ? [NSColor alternateSelectedControlTextColor] : themeTextColor; 460} 461 462- (void)drawSecondaryTitleInRect:(NSRect)innerFrame { 463 if (![self secondaryTitle] || statusAlpha_ <= 0) 464 return; 465 466 CGFloat textWidth = innerFrame.size.width - 467 (kTextPosLeft + kTextPaddingRight + kDropdownAreaWidth); 468 NSString* secondaryText = [self elideStatus:textWidth]; 469 NSColor* secondaryColor = 470 [self titleColorForPart:kDownloadItemMouseOverButtonPart]; 471 472 // If text is light-on-dark, lightening it alone will do nothing. 473 // Therefore we mute luminance a wee bit before drawing in this case. 474 if (![secondaryColor gtm_isDarkColor]) 475 secondaryColor = [secondaryColor gtm_colorByAdjustingLuminance:-0.2]; 476 477 NSDictionary* secondaryTextAttributes = 478 [NSDictionary dictionaryWithObjectsAndKeys: 479 secondaryColor, NSForegroundColorAttributeName, 480 [self secondaryFont], NSFontAttributeName, 481 nil]; 482 NSPoint secondaryPos = 483 NSMakePoint(innerFrame.origin.x + kTextPosLeft, kSecondaryTextPosTop); 484 [secondaryText drawAtPoint:secondaryPos 485 withAttributes:secondaryTextAttributes]; 486} 487 488- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView*)controlView { 489 // Constants from Cole. Will kConstant them once the feedback loop 490 // is complete. 491 NSRect drawFrame = NSInsetRect(cellFrame, 1.5, 1.5); 492 NSRect innerFrame = NSInsetRect(cellFrame, 2, 2); 493 494 const float radius = 5; 495 NSWindow* window = [controlView window]; 496 BOOL active = [window isKeyWindow] || [window isMainWindow]; 497 498 // In the default theme, draw download items with the bookmark button 499 // gradient. For some themes, this leads to unreadable text, so draw the item 500 // with a background that looks like windows (some transparent white) if a 501 // theme is used. Use custom theme object with a white color gradient to trick 502 // the superclass into drawing what we want. 503 ui::ThemeProvider* themeProvider = 504 [[[self controlView] window] themeProvider]; 505 bool isDefaultTheme = 506 !themeProvider->HasCustomImage(IDR_THEME_BUTTON_BACKGROUND); 507 508 NSGradient* bgGradient = nil; 509 if (!isDefaultTheme) { 510 themeProvider = [self backgroundThemeWrappingProvider:themeProvider]; 511 bgGradient = themeProvider->GetNSGradient( 512 active ? ThemeService::GRADIENT_TOOLBAR_BUTTON : 513 ThemeService::GRADIENT_TOOLBAR_BUTTON_INACTIVE); 514 } 515 516 NSRect buttonDrawRect, dropdownDrawRect; 517 NSDivideRect(drawFrame, &dropdownDrawRect, &buttonDrawRect, 518 kDropdownAreaWidth, NSMaxXEdge); 519 520 NSBezierPath* buttonInnerPath = [self 521 leftRoundedPath:radius inRect:buttonDrawRect]; 522 NSBezierPath* dropdownInnerPath = [self 523 rightRoundedPath:radius inRect:dropdownDrawRect]; 524 525 // Draw secondary title, if any. Do this before drawing the (transparent) 526 // fill so that the text becomes a bit lighter. The default theme's "pressed" 527 // gradient is not transparent, so only do this if a theme is active. 528 bool drawStatusOnTop = 529 [self pressedWithDefaultThemeOnPart:kDownloadItemMouseOverButtonPart]; 530 if (!drawStatusOnTop) 531 [self drawSecondaryTitleInRect:innerFrame]; 532 533 // Stroke the borders and appropriate fill gradient. 534 [self drawBorderAndFillForTheme:themeProvider 535 controlView:controlView 536 innerPath:buttonInnerPath 537 showClickedGradient:[self isButtonPartPressed] 538 showHighlightGradient:[self isMouseOverButtonPart] 539 hoverAlpha:0.0 540 active:active 541 cellFrame:cellFrame 542 defaultGradient:bgGradient]; 543 544 [self drawBorderAndFillForTheme:themeProvider 545 controlView:controlView 546 innerPath:dropdownInnerPath 547 showClickedGradient:[self isDropdownPartPressed] 548 showHighlightGradient:[self isMouseOverDropdownPart] 549 hoverAlpha:0.0 550 active:active 551 cellFrame:cellFrame 552 defaultGradient:bgGradient]; 553 554 [self drawInteriorWithFrame:innerFrame inView:controlView]; 555 556 // For the default theme, draw the status text on top of the (opaque) button 557 // gradient. 558 if (drawStatusOnTop) 559 [self drawSecondaryTitleInRect:innerFrame]; 560} 561 562- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView*)controlView { 563 // Draw title 564 CGFloat textWidth = cellFrame.size.width - 565 (kTextPosLeft + kTextPaddingRight + kDropdownAreaWidth); 566 [self setTitle:[self elideTitle:textWidth]]; 567 568 NSColor* color = [self titleColorForPart:kDownloadItemMouseOverButtonPart]; 569 NSString* primaryText = [self title]; 570 571 NSDictionary* primaryTextAttributes = 572 [NSDictionary dictionaryWithObjectsAndKeys: 573 color, NSForegroundColorAttributeName, 574 [self font], NSFontAttributeName, 575 nil]; 576 NSPoint primaryPos = NSMakePoint( 577 cellFrame.origin.x + kTextPosLeft, 578 titleY_); 579 580 [primaryText drawAtPoint:primaryPos withAttributes:primaryTextAttributes]; 581 582 // Draw progress disk 583 { 584 // CanvasSkiaPaint draws its content to the current NSGraphicsContext in its 585 // destructor, which needs to be invoked before the icon is drawn below - 586 // hence this nested block. 587 588 // Always repaint the whole disk. 589 NSPoint imagePosition = [self imageRectForBounds:cellFrame].origin; 590 int x = imagePosition.x - download_util::kSmallProgressIconOffset; 591 int y = imagePosition.y - download_util::kSmallProgressIconOffset; 592 NSRect dirtyRect = NSMakeRect( 593 x, y, 594 download_util::kSmallProgressIconSize, 595 download_util::kSmallProgressIconSize); 596 597 gfx::CanvasSkiaPaint canvas(dirtyRect, false); 598 canvas.set_composite_alpha(true); 599 if (completionAnimation_.get()) { 600 if ([completionAnimation_ isAnimating]) { 601 if (percentDone_ == -1) { 602 download_util::PaintDownloadComplete(&canvas, 603 x, y, 604 [completionAnimation_ currentValue], 605 download_util::SMALL); 606 } else { 607 download_util::PaintDownloadInterrupted(&canvas, 608 x, y, 609 [completionAnimation_ currentValue], 610 download_util::SMALL); 611 } 612 } 613 } else if (percentDone_ >= 0) { 614 download_util::PaintDownloadProgress(&canvas, 615 x, y, 616 download_util::kStartAngleDegrees, // TODO(thakis): Animate 617 percentDone_, 618 download_util::SMALL); 619 } 620 } 621 622 // Draw icon 623 NSRect imageRect = NSZeroRect; 624 imageRect.size = [[self image] size]; 625 [[self image] drawInRect:[self imageRectForBounds:cellFrame] 626 fromRect:imageRect 627 operation:NSCompositeSourceOver 628 fraction:[self isEnabled] ? 1.0 : 0.5 629 neverFlipped:YES]; 630 631 // Separator between button and popup parts 632 CGFloat lx = NSMaxX(cellFrame) - kDropdownAreaWidth + 0.5; 633 [[NSColor colorWithDeviceWhite:0.0 alpha:0.1] set]; 634 [NSBezierPath strokeLineFromPoint:NSMakePoint(lx, NSMinY(cellFrame) + 1) 635 toPoint:NSMakePoint(lx, NSMaxY(cellFrame) - 1)]; 636 [[NSColor colorWithDeviceWhite:1.0 alpha:0.1] set]; 637 [NSBezierPath strokeLineFromPoint:NSMakePoint(lx + 1, NSMinY(cellFrame) + 1) 638 toPoint:NSMakePoint(lx + 1, NSMaxY(cellFrame) - 1)]; 639 640 // Popup arrow. Put center of mass of the arrow in the center of the 641 // dropdown area. 642 CGFloat cx = NSMaxX(cellFrame) - kDropdownAreaWidth/2 + 0.5; 643 CGFloat cy = NSMidY(cellFrame); 644 NSPoint p1 = NSMakePoint(cx - kDropdownArrowWidth/2, 645 cy - kDropdownArrowHeight/3 + kDropdownAreaY); 646 NSPoint p2 = NSMakePoint(cx + kDropdownArrowWidth/2, 647 cy - kDropdownArrowHeight/3 + kDropdownAreaY); 648 NSPoint p3 = NSMakePoint(cx, cy + kDropdownArrowHeight*2/3 + kDropdownAreaY); 649 NSBezierPath *triangle = [NSBezierPath bezierPath]; 650 [triangle moveToPoint:p1]; 651 [triangle lineToPoint:p2]; 652 [triangle lineToPoint:p3]; 653 [triangle closePath]; 654 655 NSGraphicsContext* context = [NSGraphicsContext currentContext]; 656 [context saveGraphicsState]; 657 658 scoped_nsobject<NSShadow> shadow([[NSShadow alloc] init]); 659 [shadow.get() setShadowColor:[NSColor whiteColor]]; 660 [shadow.get() setShadowOffset:NSMakeSize(0, -1)]; 661 [shadow setShadowBlurRadius:0.0]; 662 [shadow set]; 663 664 NSColor* fill = [self titleColorForPart:kDownloadItemMouseOverDropdownPart]; 665 [fill setFill]; 666 667 [triangle fill]; 668 669 [context restoreGraphicsState]; 670} 671 672- (NSRect)imageRectForBounds:(NSRect)cellFrame { 673 return NSMakeRect(cellFrame.origin.x + kImagePaddingLeft, 674 cellFrame.origin.y + kImagePaddingTop, 675 kImageWidth, 676 kImageHeight); 677} 678 679- (void)hideSecondaryTitle { 680 if (isStatusTextVisible_) { 681 // No core animation -- text in CA layers is not subpixel antialiased :-/ 682 hideStatusAnimation_.reset([[DownloadItemCellAnimation alloc] 683 initWithDownloadItemCell:self 684 duration:kHideStatusDuration 685 animationCurve:NSAnimationEaseIn]); 686 [hideStatusAnimation_.get() setDelegate:self]; 687 [hideStatusAnimation_.get() startAnimation]; 688 } else { 689 // If the download is done so quickly that the status line is never visible, 690 // don't show an animation 691 [self animation:nil progressed:1.0]; 692 } 693} 694 695- (void)animation:(NSAnimation*)animation 696 progressed:(NSAnimationProgress)progress { 697 if (animation == hideStatusAnimation_ || animation == nil) { 698 titleY_ = progress*kPrimaryTextOnlyPosTop + 699 (1 - progress)*kPrimaryTextPosTop; 700 statusAlpha_ = 1 - progress; 701 [[self controlView] setNeedsDisplay:YES]; 702 } else if (animation == completionAnimation_) { 703 [[self controlView] setNeedsDisplay:YES]; 704 } 705} 706 707- (void)animationDidEnd:(NSAnimation *)animation { 708 if (animation == hideStatusAnimation_) 709 hideStatusAnimation_.reset(); 710 else if (animation == completionAnimation_) 711 completionAnimation_.reset(); 712} 713 714@end 715 716@implementation DownloadItemCellAnimation 717 718- (id)initWithDownloadItemCell:(DownloadItemCell*)cell 719 duration:(NSTimeInterval)duration 720 animationCurve:(NSAnimationCurve)animationCurve { 721 if ((self = [super gtm_initWithDuration:duration 722 eventMask:NSLeftMouseDownMask 723 animationCurve:animationCurve])) { 724 cell_ = cell; 725 [self setAnimationBlockingMode:NSAnimationNonblocking]; 726 } 727 return self; 728} 729 730- (void)setCurrentProgress:(NSAnimationProgress)progress { 731 [super setCurrentProgress:progress]; 732 [cell_ animation:self progressed:progress]; 733} 734 735@end 736