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