summaryrefslogtreecommitdiffstats
path: root/chrome/browser/ui/cocoa/download
diff options
context:
space:
mode:
Diffstat (limited to 'chrome/browser/ui/cocoa/download')
-rw-r--r--chrome/browser/ui/cocoa/download/download_item_button.h27
-rw-r--r--chrome/browser/ui/cocoa/download/download_item_button.mm50
-rw-r--r--chrome/browser/ui/cocoa/download/download_item_button_unittest.mm21
-rw-r--r--chrome/browser/ui/cocoa/download/download_item_cell.h61
-rw-r--r--chrome/browser/ui/cocoa/download/download_item_cell.mm708
-rw-r--r--chrome/browser/ui/cocoa/download/download_item_controller.h108
-rw-r--r--chrome/browser/ui/cocoa/download/download_item_controller.mm401
-rw-r--r--chrome/browser/ui/cocoa/download/download_item_mac.h63
-rw-r--r--chrome/browser/ui/cocoa/download/download_item_mac.mm101
-rw-r--r--chrome/browser/ui/cocoa/download/download_shelf_controller.h103
-rw-r--r--chrome/browser/ui/cocoa/download/download_shelf_controller.mm426
-rw-r--r--chrome/browser/ui/cocoa/download/download_shelf_mac.h43
-rw-r--r--chrome/browser/ui/cocoa/download/download_shelf_mac.mm40
-rw-r--r--chrome/browser/ui/cocoa/download/download_shelf_mac_unittest.mm91
-rw-r--r--chrome/browser/ui/cocoa/download/download_shelf_view.h20
-rw-r--r--chrome/browser/ui/cocoa/download/download_shelf_view.mm71
-rw-r--r--chrome/browser/ui/cocoa/download/download_shelf_view_unittest.mm23
-rw-r--r--chrome/browser/ui/cocoa/download/download_started_animation_mac.mm196
-rw-r--r--chrome/browser/ui/cocoa/download/download_util_mac.h25
-rw-r--r--chrome/browser/ui/cocoa/download/download_util_mac.mm83
-rw-r--r--chrome/browser/ui/cocoa/download/download_util_mac_unittest.mm58
21 files changed, 2719 insertions, 0 deletions
diff --git a/chrome/browser/ui/cocoa/download/download_item_button.h b/chrome/browser/ui/cocoa/download/download_item_button.h
new file mode 100644
index 0000000..c064cd8
--- /dev/null
+++ b/chrome/browser/ui/cocoa/download/download_item_button.h
@@ -0,0 +1,27 @@
+// Copyright (c) 2010 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#import "base/mac/cocoa_protocols.h"
+#include "base/file_path.h"
+#import "chrome/browser/ui/cocoa/draggable_button.h"
+
+@class DownloadItemController;
+
+// A button that is a drag source for a file and that displays a context menu
+// instead of firing an action when clicked in a certain area.
+@interface DownloadItemButton : DraggableButton<NSMenuDelegate> {
+ @private
+ FilePath downloadPath_;
+ DownloadItemController* controller_; // weak
+}
+
+@property(assign, nonatomic) FilePath download;
+@property(assign, nonatomic) DownloadItemController* controller;
+
+// Overridden from DraggableButton.
+- (void)beginDrag:(NSEvent*)event;
+
+@end
diff --git a/chrome/browser/ui/cocoa/download/download_item_button.mm b/chrome/browser/ui/cocoa/download/download_item_button.mm
new file mode 100644
index 0000000..da9f6b4
--- /dev/null
+++ b/chrome/browser/ui/cocoa/download/download_item_button.mm
@@ -0,0 +1,50 @@
+// Copyright (c) 2010 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "chrome/browser/ui/cocoa/download/download_item_button.h"
+
+#include "base/logging.h"
+#include "base/sys_string_conversions.h"
+#import "chrome/browser/ui/cocoa/download/download_item_cell.h"
+#import "chrome/browser/ui/cocoa/download/download_item_controller.h"
+
+@implementation DownloadItemButton
+
+@synthesize download = downloadPath_;
+@synthesize controller = controller_;
+
+// Overridden from DraggableButton.
+- (void)beginDrag:(NSEvent*)event {
+ if (!downloadPath_.empty()) {
+ NSString* filename = base::SysUTF8ToNSString(downloadPath_.value());
+ [self dragFile:filename fromRect:[self bounds] slideBack:YES event:event];
+ }
+}
+
+// Override to show a context menu on mouse down if clicked over the context
+// menu area.
+- (void)mouseDown:(NSEvent*)event {
+ DCHECK(controller_);
+ // Override so that we can pop up a context menu on mouse down.
+ NSCell* cell = [self cell];
+ DCHECK([cell respondsToSelector:@selector(isMouseOverButtonPart)]);
+ if ([reinterpret_cast<DownloadItemCell*>(cell) isMouseOverButtonPart]) {
+ [super mouseDown:event];
+ } else {
+ // Hold a reference to our controller in case the download completes and we
+ // represent a file that's auto-removed (e.g. a theme).
+ scoped_nsobject<DownloadItemController> ref([controller_ retain]);
+ [cell setHighlighted:YES];
+ [[self menu] setDelegate:self];
+ [NSMenu popUpContextMenu:[self menu]
+ withEvent:[NSApp currentEvent]
+ forView:self];
+ }
+}
+
+- (void)menuDidClose:(NSMenu*)menu {
+ [[self cell] setHighlighted:NO];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/download/download_item_button_unittest.mm b/chrome/browser/ui/cocoa/download/download_item_button_unittest.mm
new file mode 100644
index 0000000..bb0279d
--- /dev/null
+++ b/chrome/browser/ui/cocoa/download/download_item_button_unittest.mm
@@ -0,0 +1,21 @@
+// Copyright (c) 2010 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/download/download_item_button.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+// Make sure nothing leaks.
+TEST(DownloadItemButtonTest, Create) {
+ scoped_nsobject<DownloadItemButton> button;
+ button.reset([[DownloadItemButton alloc]
+ initWithFrame:NSMakeRect(0,0,500,500)]);
+
+ // Test setter
+ FilePath path("foo");
+ [button.get() setDownload:path];
+ EXPECT_EQ(path.value(), [button.get() download].value());
+}
diff --git a/chrome/browser/ui/cocoa/download/download_item_cell.h b/chrome/browser/ui/cocoa/download/download_item_cell.h
new file mode 100644
index 0000000..a5ffaeb
--- /dev/null
+++ b/chrome/browser/ui/cocoa/download/download_item_cell.h
@@ -0,0 +1,61 @@
+// Copyright (c) 2010 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_DOWNLOAD_DOWNLOAD_ITEM_CELL_H_
+#define CHROME_BROWSER_UI_COCOA_DOWNLOAD_DOWNLOAD_ITEM_CELL_H_
+#pragma once
+
+#import "base/mac/cocoa_protocols.h"
+#include "base/scoped_ptr.h"
+#import "chrome/browser/ui/cocoa/gradient_button_cell.h"
+
+#include "base/file_path.h"
+
+class BaseDownloadItemModel;
+
+// A button cell that implements the weird button/popup button hybrid that is
+// used by the download items.
+
+// The button represented by this cell consists of a button part on the left
+// and a dropdown-menu part on the right. This enum describes which part the
+// mouse cursor is over currently.
+enum DownloadItemMousePosition {
+ kDownloadItemMouseOutside,
+ kDownloadItemMouseOverButtonPart,
+ kDownloadItemMouseOverDropdownPart
+};
+
+@interface DownloadItemCell : GradientButtonCell<NSAnimationDelegate> {
+ @private
+ // Track which part of the button the mouse is over
+ DownloadItemMousePosition mousePosition_;
+ int mouseInsideCount_;
+ scoped_nsobject<NSTrackingArea> trackingAreaButton_;
+ scoped_nsobject<NSTrackingArea> trackingAreaDropdown_;
+
+ FilePath downloadPath_; // stored unelided
+ NSString* secondaryTitle_;
+ NSFont* secondaryFont_;
+ int percentDone_;
+ scoped_nsobject<NSAnimation> completionAnimation_;
+
+ BOOL isStatusTextVisible_;
+ CGFloat titleY_;
+ CGFloat statusAlpha_;
+ scoped_nsobject<NSAnimation> hideStatusAnimation_;
+
+ scoped_ptr<ThemeProvider> themeProvider_;
+}
+
+- (void)setStateFromDownload:(BaseDownloadItemModel*)downloadModel;
+
+@property (nonatomic, copy) NSString* secondaryTitle;
+@property (nonatomic, retain) NSFont* secondaryFont;
+
+// Returns if the mouse is over the button part of the cell.
+- (BOOL)isMouseOverButtonPart;
+
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_DOWNLOAD_DOWNLOAD_ITEM_CELL_H_
diff --git a/chrome/browser/ui/cocoa/download/download_item_cell.mm b/chrome/browser/ui/cocoa/download/download_item_cell.mm
new file mode 100644
index 0000000..b83d6f5
--- /dev/null
+++ b/chrome/browser/ui/cocoa/download/download_item_cell.mm
@@ -0,0 +1,708 @@
+// Copyright (c) 2010 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "chrome/browser/ui/cocoa/download/download_item_cell.h"
+
+#include "app/l10n_util.h"
+#include "app/text_elider.h"
+#include "base/mac_util.h"
+#include "base/sys_string_conversions.h"
+#include "chrome/browser/download/download_item.h"
+#include "chrome/browser/download/download_item_model.h"
+#include "chrome/browser/download/download_manager.h"
+#include "chrome/browser/download/download_util.h"
+#import "chrome/browser/themes/browser_theme_provider.h"
+#import "chrome/browser/ui/cocoa/download/download_item_cell.h"
+#import "chrome/browser/ui/cocoa/image_utils.h"
+#import "chrome/browser/ui/cocoa/themed_window.h"
+#include "gfx/canvas_skia_paint.h"
+#include "grit/theme_resources.h"
+#import "third_party/GTM/AppKit/GTMNSAnimation+Duration.h"
+#import "third_party/GTM/AppKit/GTMNSColor+Luminance.h"
+
+namespace {
+
+// Distance from top border to icon
+const CGFloat kImagePaddingTop = 7;
+
+// Distance from left border to icon
+const CGFloat kImagePaddingLeft = 9;
+
+// Width of icon
+const CGFloat kImageWidth = 16;
+
+// Height of icon
+const CGFloat kImageHeight = 16;
+
+// x coordinate of download name string, in view coords
+const CGFloat kTextPosLeft = kImagePaddingLeft +
+ kImageWidth + download_util::kSmallProgressIconOffset;
+
+// Distance from end of download name string to dropdown area
+const CGFloat kTextPaddingRight = 3;
+
+// y coordinate of download name string, in view coords, when status message
+// is visible
+const CGFloat kPrimaryTextPosTop = 3;
+
+// y coordinate of download name string, in view coords, when status message
+// is not visible
+const CGFloat kPrimaryTextOnlyPosTop = 10;
+
+// y coordinate of status message, in view coords
+const CGFloat kSecondaryTextPosTop = 18;
+
+// Grey value of status text
+const CGFloat kSecondaryTextColor = 0.5;
+
+// Width of dropdown area on the right (includes 1px for the border on each
+// side).
+const CGFloat kDropdownAreaWidth = 14;
+
+// Width of dropdown arrow
+const CGFloat kDropdownArrowWidth = 5;
+
+// Height of dropdown arrow
+const CGFloat kDropdownArrowHeight = 3;
+
+// Vertical displacement of dropdown area, relative to the "centered" position.
+const CGFloat kDropdownAreaY = -2;
+
+// Duration of the two-lines-to-one-line animation, in seconds
+NSTimeInterval kHideStatusDuration = 0.3;
+
+// Duration of the 'download complete' animation, in seconds
+const int kCompleteAnimationDuration = 2.5;
+
+}
+
+// This is a helper class to animate the fading out of the status text.
+@interface DownloadItemCellAnimation : NSAnimation {
+ DownloadItemCell* cell_;
+}
+- (id)initWithDownloadItemCell:(DownloadItemCell*)cell
+ duration:(NSTimeInterval)duration
+ animationCurve:(NSAnimationCurve)animationCurve;
+@end
+
+class BackgroundTheme : public ThemeProvider {
+public:
+ BackgroundTheme(ThemeProvider* provider);
+
+ virtual void Init(Profile* profile) { }
+ virtual SkBitmap* GetBitmapNamed(int id) const { return nil; }
+ virtual SkColor GetColor(int id) const { return SkColor(); }
+ virtual bool GetDisplayProperty(int id, int* result) const { return false; }
+ virtual bool ShouldUseNativeFrame() const { return false; }
+ virtual bool HasCustomImage(int id) const { return false; }
+ virtual RefCountedMemory* GetRawData(int id) const { return NULL; }
+ virtual NSImage* GetNSImageNamed(int id, bool allow_default) const;
+ virtual NSColor* GetNSImageColorNamed(int id, bool allow_default) const;
+ virtual NSColor* GetNSColor(int id, bool allow_default) const;
+ virtual NSColor* GetNSColorTint(int id, bool allow_default) const;
+ virtual NSGradient* GetNSGradient(int id) const;
+
+private:
+ ThemeProvider* provider_;
+ scoped_nsobject<NSGradient> buttonGradient_;
+ scoped_nsobject<NSGradient> buttonPressedGradient_;
+ scoped_nsobject<NSColor> borderColor_;
+};
+
+BackgroundTheme::BackgroundTheme(ThemeProvider* provider) :
+ provider_(provider) {
+ NSColor* bgColor = [NSColor colorWithCalibratedRed:241/255.0
+ green:245/255.0
+ blue:250/255.0
+ alpha:77/255.0];
+ NSColor* clickedColor = [NSColor colorWithCalibratedRed:239/255.0
+ green:245/255.0
+ blue:252/255.0
+ alpha:51/255.0];
+
+ borderColor_.reset(
+ [[NSColor colorWithCalibratedWhite:0 alpha:36/255.0] retain]);
+ buttonGradient_.reset([[NSGradient alloc]
+ initWithColors:[NSArray arrayWithObject:bgColor]]);
+ buttonPressedGradient_.reset([[NSGradient alloc]
+ initWithColors:[NSArray arrayWithObject:clickedColor]]);
+}
+
+NSImage* BackgroundTheme::GetNSImageNamed(int id, bool allow_default) const {
+ return nil;
+}
+
+NSColor* BackgroundTheme::GetNSImageColorNamed(int id,
+ bool allow_default) const {
+ return nil;
+}
+
+NSColor* BackgroundTheme::GetNSColor(int id, bool allow_default) const {
+ return provider_->GetNSColor(id, allow_default);
+}
+
+NSColor* BackgroundTheme::GetNSColorTint(int id, bool allow_default) const {
+ if (id == BrowserThemeProvider::TINT_BUTTONS)
+ return borderColor_.get();
+
+ return provider_->GetNSColorTint(id, allow_default);
+}
+
+NSGradient* BackgroundTheme::GetNSGradient(int id) const {
+ switch (id) {
+ case BrowserThemeProvider::GRADIENT_TOOLBAR_BUTTON:
+ case BrowserThemeProvider::GRADIENT_TOOLBAR_BUTTON_INACTIVE:
+ return buttonGradient_.get();
+ case BrowserThemeProvider::GRADIENT_TOOLBAR_BUTTON_PRESSED:
+ case BrowserThemeProvider::GRADIENT_TOOLBAR_BUTTON_PRESSED_INACTIVE:
+ return buttonPressedGradient_.get();
+ default:
+ return provider_->GetNSGradient(id);
+ }
+}
+
+@interface DownloadItemCell(Private)
+- (void)updateTrackingAreas:(id)sender;
+- (void)hideSecondaryTitle;
+- (void)animation:(NSAnimation*)animation
+ progressed:(NSAnimationProgress)progress;
+- (NSString*)elideTitle:(int)availableWidth;
+- (NSString*)elideStatus:(int)availableWidth;
+- (ThemeProvider*)backgroundThemeWrappingProvider:(ThemeProvider*)provider;
+- (BOOL)pressedWithDefaultThemeOnPart:(DownloadItemMousePosition)part;
+- (NSColor*)titleColorForPart:(DownloadItemMousePosition)part;
+- (void)drawSecondaryTitleInRect:(NSRect)innerFrame;
+@end
+
+@implementation DownloadItemCell
+
+@synthesize secondaryTitle = secondaryTitle_;
+@synthesize secondaryFont = secondaryFont_;
+
+- (void)setInitialState {
+ isStatusTextVisible_ = NO;
+ titleY_ = kPrimaryTextPosTop;
+ statusAlpha_ = 1.0;
+
+ [self setFont:[NSFont systemFontOfSize:
+ [NSFont systemFontSizeForControlSize:NSSmallControlSize]]];
+ [self setSecondaryFont:[NSFont systemFontOfSize:
+ [NSFont systemFontSizeForControlSize:NSSmallControlSize]]];
+
+ [self updateTrackingAreas:self];
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(updateTrackingAreas:)
+ name:NSViewFrameDidChangeNotification
+ object:[self controlView]];
+}
+
+// For nib instantiations
+- (id)initWithCoder:(NSCoder*)decoder {
+ if ((self = [super initWithCoder:decoder])) {
+ [self setInitialState];
+ }
+ return self;
+}
+
+// For programmatic instantiations.
+- (id)initTextCell:(NSString *)string {
+ if ((self = [super initTextCell:string])) {
+ [self setInitialState];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ if ([completionAnimation_ isAnimating])
+ [completionAnimation_ stopAnimation];
+ if ([hideStatusAnimation_ isAnimating])
+ [hideStatusAnimation_ stopAnimation];
+ if (trackingAreaButton_) {
+ [[self controlView] removeTrackingArea:trackingAreaButton_];
+ trackingAreaButton_.reset();
+ }
+ if (trackingAreaDropdown_) {
+ [[self controlView] removeTrackingArea:trackingAreaDropdown_];
+ trackingAreaDropdown_.reset();
+ }
+ [secondaryTitle_ release];
+ [secondaryFont_ release];
+ [super dealloc];
+}
+
+- (void)setStateFromDownload:(BaseDownloadItemModel*)downloadModel {
+ // Set the name of the download.
+ downloadPath_ = downloadModel->download()->GetFileNameToReportUser();
+
+ std::wstring statusText = downloadModel->GetStatusText();
+ if (statusText.empty()) {
+ // Remove the status text label.
+ [self hideSecondaryTitle];
+ isStatusTextVisible_ = NO;
+ } else {
+ // Set status text.
+ NSString* statusString = base::SysWideToNSString(statusText);
+ [self setSecondaryTitle:statusString];
+ isStatusTextVisible_ = YES;
+ }
+
+ switch (downloadModel->download()->state()) {
+ case DownloadItem::COMPLETE:
+ // Small downloads may start in a complete state due to asynchronous
+ // notifications. In this case, we'll get a second complete notification
+ // via the observers, so we ignore it and avoid creating a second complete
+ // animation.
+ if (completionAnimation_.get())
+ break;
+ completionAnimation_.reset([[DownloadItemCellAnimation alloc]
+ initWithDownloadItemCell:self
+ duration:kCompleteAnimationDuration
+ animationCurve:NSAnimationLinear]);
+ [completionAnimation_.get() setDelegate:self];
+ [completionAnimation_.get() startAnimation];
+ percentDone_ = -1;
+ break;
+ case DownloadItem::CANCELLED:
+ percentDone_ = -1;
+ break;
+ case DownloadItem::IN_PROGRESS:
+ percentDone_ = downloadModel->download()->is_paused() ?
+ -1 : downloadModel->download()->PercentComplete();
+ break;
+ default:
+ NOTREACHED();
+ }
+
+ [[self controlView] setNeedsDisplay:YES];
+}
+
+- (void)updateTrackingAreas:(id)sender {
+ if (trackingAreaButton_) {
+ [[self controlView] removeTrackingArea:trackingAreaButton_.get()];
+ trackingAreaButton_.reset(nil);
+ }
+ if (trackingAreaDropdown_) {
+ [[self controlView] removeTrackingArea:trackingAreaDropdown_.get()];
+ trackingAreaDropdown_.reset(nil);
+ }
+
+ // Use two distinct tracking rects for left and right parts.
+ // The tracking areas are also used to decide how to handle clicks. They must
+ // always be active, so the click is handled correctly when a download item
+ // is clicked while chrome is not the active app ( http://crbug.com/21916 ).
+ NSRect bounds = [[self controlView] bounds];
+ NSRect buttonRect, dropdownRect;
+ NSDivideRect(bounds, &dropdownRect, &buttonRect,
+ kDropdownAreaWidth, NSMaxXEdge);
+
+ trackingAreaButton_.reset([[NSTrackingArea alloc]
+ initWithRect:buttonRect
+ options:(NSTrackingMouseEnteredAndExited |
+ NSTrackingActiveAlways)
+ owner:self
+ userInfo:nil]);
+ [[self controlView] addTrackingArea:trackingAreaButton_.get()];
+
+ trackingAreaDropdown_.reset([[NSTrackingArea alloc]
+ initWithRect:dropdownRect
+ options:(NSTrackingMouseEnteredAndExited |
+ NSTrackingActiveAlways)
+ owner:self
+ userInfo:nil]);
+ [[self controlView] addTrackingArea:trackingAreaDropdown_.get()];
+}
+
+- (void)setShowsBorderOnlyWhileMouseInside:(BOOL)showOnly {
+ // Override to make sure it doesn't do anything if it's called accidentally.
+}
+
+- (void)mouseEntered:(NSEvent*)theEvent {
+ mouseInsideCount_++;
+ if ([theEvent trackingArea] == trackingAreaButton_.get())
+ mousePosition_ = kDownloadItemMouseOverButtonPart;
+ else if ([theEvent trackingArea] == trackingAreaDropdown_.get())
+ mousePosition_ = kDownloadItemMouseOverDropdownPart;
+ [[self controlView] setNeedsDisplay:YES];
+}
+
+- (void)mouseExited:(NSEvent *)theEvent {
+ mouseInsideCount_--;
+ if (mouseInsideCount_ == 0)
+ mousePosition_ = kDownloadItemMouseOutside;
+ [[self controlView] setNeedsDisplay:YES];
+}
+
+- (BOOL)isMouseInside {
+ return mousePosition_ != kDownloadItemMouseOutside;
+}
+
+- (BOOL)isMouseOverButtonPart {
+ return mousePosition_ == kDownloadItemMouseOverButtonPart;
+}
+
+- (BOOL)isButtonPartPressed {
+ return [self isHighlighted]
+ && mousePosition_ == kDownloadItemMouseOverButtonPart;
+}
+
+- (BOOL)isMouseOverDropdownPart {
+ return mousePosition_ == kDownloadItemMouseOverDropdownPart;
+}
+
+- (BOOL)isDropdownPartPressed {
+ return [self isHighlighted]
+ && mousePosition_ == kDownloadItemMouseOverDropdownPart;
+}
+
+- (NSBezierPath*)leftRoundedPath:(CGFloat)radius inRect:(NSRect)rect {
+
+ NSPoint topLeft = NSMakePoint(NSMinX(rect), NSMaxY(rect));
+ NSPoint topRight = NSMakePoint(NSMaxX(rect), NSMaxY(rect));
+ NSPoint bottomRight = NSMakePoint(NSMaxX(rect) , NSMinY(rect));
+
+ NSBezierPath* path = [NSBezierPath bezierPath];
+ [path moveToPoint:topRight];
+ [path appendBezierPathWithArcFromPoint:topLeft
+ toPoint:rect.origin
+ radius:radius];
+ [path appendBezierPathWithArcFromPoint:rect.origin
+ toPoint:bottomRight
+ radius:radius];
+ [path lineToPoint:bottomRight];
+ return path;
+}
+
+- (NSBezierPath*)rightRoundedPath:(CGFloat)radius inRect:(NSRect)rect {
+
+ NSPoint topLeft = NSMakePoint(NSMinX(rect), NSMaxY(rect));
+ NSPoint topRight = NSMakePoint(NSMaxX(rect), NSMaxY(rect));
+ NSPoint bottomRight = NSMakePoint(NSMaxX(rect), NSMinY(rect));
+
+ NSBezierPath* path = [NSBezierPath bezierPath];
+ [path moveToPoint:rect.origin];
+ [path appendBezierPathWithArcFromPoint:bottomRight
+ toPoint:topRight
+ radius:radius];
+ [path appendBezierPathWithArcFromPoint:topRight
+ toPoint:topLeft
+ radius:radius];
+ [path lineToPoint:topLeft];
+ return path;
+}
+
+- (NSString*)elideTitle:(int)availableWidth {
+ NSFont* font = [self font];
+ gfx::Font font_chr(base::SysNSStringToWide([font fontName]),
+ [font pointSize]);
+
+ return base::SysUTF16ToNSString(
+ ElideFilename(downloadPath_, font_chr, availableWidth));
+}
+
+- (NSString*)elideStatus:(int)availableWidth {
+ NSFont* font = [self secondaryFont];
+ gfx::Font font_chr(base::SysNSStringToWide([font fontName]),
+ [font pointSize]);
+
+ return base::SysUTF16ToNSString(ElideText(
+ base::SysNSStringToUTF16([self secondaryTitle]),
+ font_chr,
+ availableWidth,
+ false));
+}
+
+- (ThemeProvider*)backgroundThemeWrappingProvider:(ThemeProvider*)provider {
+ if (!themeProvider_.get()) {
+ themeProvider_.reset(new BackgroundTheme(provider));
+ }
+
+ return themeProvider_.get();
+}
+
+// Returns if |part| was pressed while the default theme was active.
+- (BOOL)pressedWithDefaultThemeOnPart:(DownloadItemMousePosition)part {
+ ThemeProvider* themeProvider = [[[self controlView] window] themeProvider];
+ bool isDefaultTheme =
+ !themeProvider->HasCustomImage(IDR_THEME_BUTTON_BACKGROUND);
+ return isDefaultTheme && [self isHighlighted] && mousePosition_ == part;
+}
+
+// Returns the text color that should be used to draw text on |part|.
+- (NSColor*)titleColorForPart:(DownloadItemMousePosition)part {
+ ThemeProvider* themeProvider = [[[self controlView] window] themeProvider];
+ NSColor* themeTextColor =
+ themeProvider->GetNSColor(BrowserThemeProvider::COLOR_BOOKMARK_TEXT,
+ true);
+ return [self pressedWithDefaultThemeOnPart:part]
+ ? [NSColor alternateSelectedControlTextColor] : themeTextColor;
+}
+
+- (void)drawSecondaryTitleInRect:(NSRect)innerFrame {
+ if (![self secondaryTitle] || statusAlpha_ <= 0)
+ return;
+
+ CGFloat textWidth = innerFrame.size.width -
+ (kTextPosLeft + kTextPaddingRight + kDropdownAreaWidth);
+ NSString* secondaryText = [self elideStatus:textWidth];
+ NSColor* secondaryColor =
+ [self titleColorForPart:kDownloadItemMouseOverButtonPart];
+
+ // If text is light-on-dark, lightening it alone will do nothing.
+ // Therefore we mute luminance a wee bit before drawing in this case.
+ if (![secondaryColor gtm_isDarkColor])
+ secondaryColor = [secondaryColor gtm_colorByAdjustingLuminance:-0.2];
+
+ NSDictionary* secondaryTextAttributes =
+ [NSDictionary dictionaryWithObjectsAndKeys:
+ secondaryColor, NSForegroundColorAttributeName,
+ [self secondaryFont], NSFontAttributeName,
+ nil];
+ NSPoint secondaryPos =
+ NSMakePoint(innerFrame.origin.x + kTextPosLeft, kSecondaryTextPosTop);
+ [secondaryText drawAtPoint:secondaryPos
+ withAttributes:secondaryTextAttributes];
+}
+
+- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView*)controlView {
+ // Constants from Cole. Will kConstant them once the feedback loop
+ // is complete.
+ NSRect drawFrame = NSInsetRect(cellFrame, 1.5, 1.5);
+ NSRect innerFrame = NSInsetRect(cellFrame, 2, 2);
+
+ const float radius = 5;
+ NSWindow* window = [controlView window];
+ BOOL active = [window isKeyWindow] || [window isMainWindow];
+
+ // In the default theme, draw download items with the bookmark button
+ // gradient. For some themes, this leads to unreadable text, so draw the item
+ // with a background that looks like windows (some transparent white) if a
+ // theme is used. Use custom theme object with a white color gradient to trick
+ // the superclass into drawing what we want.
+ ThemeProvider* themeProvider = [[[self controlView] window] themeProvider];
+ bool isDefaultTheme =
+ !themeProvider->HasCustomImage(IDR_THEME_BUTTON_BACKGROUND);
+
+ NSGradient* bgGradient = nil;
+ if (!isDefaultTheme) {
+ themeProvider = [self backgroundThemeWrappingProvider:themeProvider];
+ bgGradient = themeProvider->GetNSGradient(
+ active ? BrowserThemeProvider::GRADIENT_TOOLBAR_BUTTON :
+ BrowserThemeProvider::GRADIENT_TOOLBAR_BUTTON_INACTIVE);
+ }
+
+ NSRect buttonDrawRect, dropdownDrawRect;
+ NSDivideRect(drawFrame, &dropdownDrawRect, &buttonDrawRect,
+ kDropdownAreaWidth, NSMaxXEdge);
+
+ NSBezierPath* buttonInnerPath = [self
+ leftRoundedPath:radius inRect:buttonDrawRect];
+ NSBezierPath* dropdownInnerPath = [self
+ rightRoundedPath:radius inRect:dropdownDrawRect];
+
+ // Draw secondary title, if any. Do this before drawing the (transparent)
+ // fill so that the text becomes a bit lighter. The default theme's "pressed"
+ // gradient is not transparent, so only do this if a theme is active.
+ bool drawStatusOnTop =
+ [self pressedWithDefaultThemeOnPart:kDownloadItemMouseOverButtonPart];
+ if (!drawStatusOnTop)
+ [self drawSecondaryTitleInRect:innerFrame];
+
+ // Stroke the borders and appropriate fill gradient.
+ [self drawBorderAndFillForTheme:themeProvider
+ controlView:controlView
+ innerPath:buttonInnerPath
+ showClickedGradient:[self isButtonPartPressed]
+ showHighlightGradient:[self isMouseOverButtonPart]
+ hoverAlpha:0.0
+ active:active
+ cellFrame:cellFrame
+ defaultGradient:bgGradient];
+
+ [self drawBorderAndFillForTheme:themeProvider
+ controlView:controlView
+ innerPath:dropdownInnerPath
+ showClickedGradient:[self isDropdownPartPressed]
+ showHighlightGradient:[self isMouseOverDropdownPart]
+ hoverAlpha:0.0
+ active:active
+ cellFrame:cellFrame
+ defaultGradient:bgGradient];
+
+ [self drawInteriorWithFrame:innerFrame inView:controlView];
+
+ // For the default theme, draw the status text on top of the (opaque) button
+ // gradient.
+ if (drawStatusOnTop)
+ [self drawSecondaryTitleInRect:innerFrame];
+}
+
+- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView*)controlView {
+ // Draw title
+ CGFloat textWidth = cellFrame.size.width -
+ (kTextPosLeft + kTextPaddingRight + kDropdownAreaWidth);
+ [self setTitle:[self elideTitle:textWidth]];
+
+ NSColor* color = [self titleColorForPart:kDownloadItemMouseOverButtonPart];
+ NSString* primaryText = [self title];
+
+ NSDictionary* primaryTextAttributes =
+ [NSDictionary dictionaryWithObjectsAndKeys:
+ color, NSForegroundColorAttributeName,
+ [self font], NSFontAttributeName,
+ nil];
+ NSPoint primaryPos = NSMakePoint(
+ cellFrame.origin.x + kTextPosLeft,
+ titleY_);
+
+ [primaryText drawAtPoint:primaryPos withAttributes:primaryTextAttributes];
+
+ // Draw progress disk
+ {
+ // CanvasSkiaPaint draws its content to the current NSGraphicsContext in its
+ // destructor, which needs to be invoked before the icon is drawn below -
+ // hence this nested block.
+
+ // Always repaint the whole disk.
+ NSPoint imagePosition = [self imageRectForBounds:cellFrame].origin;
+ int x = imagePosition.x - download_util::kSmallProgressIconOffset;
+ int y = imagePosition.y - download_util::kSmallProgressIconOffset;
+ NSRect dirtyRect = NSMakeRect(
+ x, y,
+ download_util::kSmallProgressIconSize,
+ download_util::kSmallProgressIconSize);
+
+ gfx::CanvasSkiaPaint canvas(dirtyRect, false);
+ canvas.set_composite_alpha(true);
+ if (completionAnimation_.get()) {
+ if ([completionAnimation_ isAnimating]) {
+ download_util::PaintDownloadComplete(&canvas,
+ x, y,
+ [completionAnimation_ currentValue],
+ download_util::SMALL);
+ }
+ } else if (percentDone_ >= 0) {
+ download_util::PaintDownloadProgress(&canvas,
+ x, y,
+ download_util::kStartAngleDegrees, // TODO(thakis): Animate
+ percentDone_,
+ download_util::SMALL);
+ }
+ }
+
+ // Draw icon
+ NSRect imageRect = NSZeroRect;
+ imageRect.size = [[self image] size];
+ [[self image] drawInRect:[self imageRectForBounds:cellFrame]
+ fromRect:imageRect
+ operation:NSCompositeSourceOver
+ fraction:[self isEnabled] ? 1.0 : 0.5
+ neverFlipped:YES];
+
+ // Separator between button and popup parts
+ CGFloat lx = NSMaxX(cellFrame) - kDropdownAreaWidth + 0.5;
+ [[NSColor colorWithDeviceWhite:0.0 alpha:0.1] set];
+ [NSBezierPath strokeLineFromPoint:NSMakePoint(lx, NSMinY(cellFrame) + 1)
+ toPoint:NSMakePoint(lx, NSMaxY(cellFrame) - 1)];
+ [[NSColor colorWithDeviceWhite:1.0 alpha:0.1] set];
+ [NSBezierPath strokeLineFromPoint:NSMakePoint(lx + 1, NSMinY(cellFrame) + 1)
+ toPoint:NSMakePoint(lx + 1, NSMaxY(cellFrame) - 1)];
+
+ // Popup arrow. Put center of mass of the arrow in the center of the
+ // dropdown area.
+ CGFloat cx = NSMaxX(cellFrame) - kDropdownAreaWidth/2 + 0.5;
+ CGFloat cy = NSMidY(cellFrame);
+ NSPoint p1 = NSMakePoint(cx - kDropdownArrowWidth/2,
+ cy - kDropdownArrowHeight/3 + kDropdownAreaY);
+ NSPoint p2 = NSMakePoint(cx + kDropdownArrowWidth/2,
+ cy - kDropdownArrowHeight/3 + kDropdownAreaY);
+ NSPoint p3 = NSMakePoint(cx, cy + kDropdownArrowHeight*2/3 + kDropdownAreaY);
+ NSBezierPath *triangle = [NSBezierPath bezierPath];
+ [triangle moveToPoint:p1];
+ [triangle lineToPoint:p2];
+ [triangle lineToPoint:p3];
+ [triangle closePath];
+
+ NSGraphicsContext* context = [NSGraphicsContext currentContext];
+ [context saveGraphicsState];
+
+ scoped_nsobject<NSShadow> shadow([[NSShadow alloc] init]);
+ [shadow.get() setShadowColor:[NSColor whiteColor]];
+ [shadow.get() setShadowOffset:NSMakeSize(0, -1)];
+ [shadow setShadowBlurRadius:0.0];
+ [shadow set];
+
+ NSColor* fill = [self titleColorForPart:kDownloadItemMouseOverDropdownPart];
+ [fill setFill];
+
+ [triangle fill];
+
+ [context restoreGraphicsState];
+}
+
+- (NSRect)imageRectForBounds:(NSRect)cellFrame {
+ return NSMakeRect(cellFrame.origin.x + kImagePaddingLeft,
+ cellFrame.origin.y + kImagePaddingTop,
+ kImageWidth,
+ kImageHeight);
+}
+
+- (void)hideSecondaryTitle {
+ if (isStatusTextVisible_) {
+ // No core animation -- text in CA layers is not subpixel antialiased :-/
+ hideStatusAnimation_.reset([[DownloadItemCellAnimation alloc]
+ initWithDownloadItemCell:self
+ duration:kHideStatusDuration
+ animationCurve:NSAnimationEaseIn]);
+ [hideStatusAnimation_.get() setDelegate:self];
+ [hideStatusAnimation_.get() startAnimation];
+ } else {
+ // If the download is done so quickly that the status line is never visible,
+ // don't show an animation
+ [self animation:nil progressed:1.0];
+ }
+}
+
+- (void)animation:(NSAnimation*)animation
+ progressed:(NSAnimationProgress)progress {
+ if (animation == hideStatusAnimation_ || animation == nil) {
+ titleY_ = progress*kPrimaryTextOnlyPosTop +
+ (1 - progress)*kPrimaryTextPosTop;
+ statusAlpha_ = 1 - progress;
+ [[self controlView] setNeedsDisplay:YES];
+ } else if (animation == completionAnimation_) {
+ [[self controlView] setNeedsDisplay:YES];
+ }
+}
+
+- (void)animationDidEnd:(NSAnimation *)animation {
+ if (animation == hideStatusAnimation_)
+ hideStatusAnimation_.reset();
+ else if (animation == completionAnimation_)
+ completionAnimation_.reset();
+}
+
+@end
+
+@implementation DownloadItemCellAnimation
+
+- (id)initWithDownloadItemCell:(DownloadItemCell*)cell
+ duration:(NSTimeInterval)duration
+ animationCurve:(NSAnimationCurve)animationCurve {
+ if ((self = [super gtm_initWithDuration:duration
+ eventMask:NSLeftMouseDownMask
+ animationCurve:animationCurve])) {
+ cell_ = cell;
+ [self setAnimationBlockingMode:NSAnimationNonblocking];
+ }
+ return self;
+}
+
+- (void)setCurrentProgress:(NSAnimationProgress)progress {
+ [super setCurrentProgress:progress];
+ [cell_ animation:self progressed:progress];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/download/download_item_controller.h b/chrome/browser/ui/cocoa/download/download_item_controller.h
new file mode 100644
index 0000000..c41ff43
--- /dev/null
+++ b/chrome/browser/ui/cocoa/download/download_item_controller.h
@@ -0,0 +1,108 @@
+// Copyright (c) 2010 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_ptr.h"
+#include "base/time.h"
+
+class BaseDownloadItemModel;
+@class ChromeUILocalizer;
+@class DownloadItemCell;
+class DownloadItem;
+@class DownloadItemButton;
+class DownloadItemMac;
+class DownloadShelfContextMenuMac;
+@class DownloadShelfController;
+@class GTMWidthBasedTweaker;
+
+// A controller class that manages one download item.
+
+@interface DownloadItemController : NSViewController {
+ @private
+ IBOutlet DownloadItemButton* progressView_;
+ IBOutlet DownloadItemCell* cell_;
+
+ IBOutlet NSMenu* activeDownloadMenu_;
+ IBOutlet NSMenu* completeDownloadMenu_;
+
+ // This is shown instead of progressView_ for dangerous downloads.
+ IBOutlet NSView* dangerousDownloadView_;
+ IBOutlet NSTextField* dangerousDownloadLabel_;
+ IBOutlet NSButton* dangerousDownloadConfirmButton_;
+
+ // Needed to find out how much the tweaker changed sizes to update the
+ // other views.
+ IBOutlet GTMWidthBasedTweaker* buttonTweaker_;
+
+ // Because the confirm text and button for dangerous downloads are determined
+ // at runtime, an outlet to the localizer is needed to construct the layout
+ // tweaker in awakeFromNib in order to adjust the UI after all strings are
+ // determined.
+ IBOutlet ChromeUILocalizer* localizer_;
+
+ IBOutlet NSImageView* image_;
+
+ scoped_ptr<DownloadItemMac> bridge_;
+ scoped_ptr<DownloadShelfContextMenuMac> menuBridge_;
+
+ // Weak pointer to the shelf that owns us.
+ DownloadShelfController* shelf_;
+
+ // The time at which this view was created.
+ base::Time creationTime_;
+
+ // The state of this item.
+ enum DownoadItemState {
+ kNormal,
+ kDangerous
+ } state_;
+};
+
+// Takes ownership of |downloadModel|.
+- (id)initWithModel:(BaseDownloadItemModel*)downloadModel
+ shelf:(DownloadShelfController*)shelf;
+
+// Updates the UI and menu state from |downloadModel|.
+- (void)setStateFromDownload:(BaseDownloadItemModel*)downloadModel;
+
+// Remove ourself from the download UI.
+- (void)remove;
+
+// Update item's visibility depending on if the item is still completely
+// contained in its parent.
+- (void)updateVisibility:(id)sender;
+
+// Called after a download is opened.
+- (void)downloadWasOpened;
+
+// Asynchronous icon loading callback.
+- (void)setIcon:(NSImage*)icon;
+
+// Download item button clicked
+- (IBAction)handleButtonClick:(id)sender;
+
+// Returns the size this item wants to have.
+- (NSSize)preferredSize;
+
+// Returns the DownloadItem model object belonging to this item.
+- (DownloadItem*)download;
+
+// Updates the tooltip with the download's path.
+- (void)updateToolTip;
+
+// Handling of dangerous downloads
+- (void)clearDangerousMode;
+- (BOOL)isDangerousMode;
+- (IBAction)saveDownload:(id)sender;
+- (IBAction)discardDownload:(id)sender;
+
+// Context menu handlers.
+- (IBAction)handleOpen:(id)sender;
+- (IBAction)handleAlwaysOpen:(id)sender;
+- (IBAction)handleReveal:(id)sender;
+- (IBAction)handleCancel:(id)sender;
+- (IBAction)handleTogglePause:(id)sender;
+
+@end
diff --git a/chrome/browser/ui/cocoa/download/download_item_controller.mm b/chrome/browser/ui/cocoa/download/download_item_controller.mm
new file mode 100644
index 0000000..be0be80
--- /dev/null
+++ b/chrome/browser/ui/cocoa/download/download_item_controller.mm
@@ -0,0 +1,401 @@
+// Copyright (c) 2010 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "chrome/browser/ui/cocoa/download/download_item_controller.h"
+
+#include "app/l10n_util_mac.h"
+#include "app/resource_bundle.h"
+#include "app/text_elider.h"
+#include "base/mac_util.h"
+#include "base/metrics/histogram.h"
+#include "base/string16.h"
+#include "base/string_util.h"
+#include "base/sys_string_conversions.h"
+#include "base/utf_string_conversions.h"
+#include "chrome/browser/download/download_item.h"
+#include "chrome/browser/download/download_item_model.h"
+#include "chrome/browser/download/download_shelf.h"
+#include "chrome/browser/download/download_util.h"
+#import "chrome/browser/themes/browser_theme_provider.h"
+#import "chrome/browser/ui/cocoa/download/download_item_button.h"
+#import "chrome/browser/ui/cocoa/download/download_item_cell.h"
+#include "chrome/browser/ui/cocoa/download/download_item_mac.h"
+#import "chrome/browser/ui/cocoa/download/download_shelf_controller.h"
+#import "chrome/browser/ui/cocoa/themed_window.h"
+#import "chrome/browser/ui/cocoa/ui_localizer.h"
+#include "grit/generated_resources.h"
+#include "grit/theme_resources.h"
+#include "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h"
+
+namespace {
+
+// NOTE: Mac currently doesn't use this like Windows does. Mac uses this to
+// control the min size on the dangerous download text. TVL sent a query off to
+// UX to fully spec all the the behaviors of download items and truncations
+// rules so all platforms can get inline in the future.
+const int kTextWidth = 140; // Pixels
+
+// The maximum number of characters we show in a file name when displaying the
+// dangerous download message.
+const int kFileNameMaxLength = 20;
+
+// The maximum width in pixels for the file name tooltip.
+const int kToolTipMaxWidth = 900;
+
+
+// Helper to widen a view.
+void WidenView(NSView* view, CGFloat widthChange) {
+ // If it is an NSBox, the autoresize of the contentView is the issue.
+ NSView* contentView = view;
+ if ([view isKindOfClass:[NSBox class]]) {
+ contentView = [(NSBox*)view contentView];
+ }
+ BOOL autoresizesSubviews = [contentView autoresizesSubviews];
+ if (autoresizesSubviews) {
+ [contentView setAutoresizesSubviews:NO];
+ }
+
+ NSRect frame = [view frame];
+ frame.size.width += widthChange;
+ [view setFrame:frame];
+
+ if (autoresizesSubviews) {
+ [contentView setAutoresizesSubviews:YES];
+ }
+}
+
+} // namespace
+
+// A class for the chromium-side part of the download shelf context menu.
+
+class DownloadShelfContextMenuMac : public DownloadShelfContextMenu {
+ public:
+ DownloadShelfContextMenuMac(BaseDownloadItemModel* model)
+ : DownloadShelfContextMenu(model) { }
+
+ using DownloadShelfContextMenu::ExecuteCommand;
+ using DownloadShelfContextMenu::IsCommandIdChecked;
+ using DownloadShelfContextMenu::IsCommandIdEnabled;
+
+ using DownloadShelfContextMenu::SHOW_IN_FOLDER;
+ using DownloadShelfContextMenu::OPEN_WHEN_COMPLETE;
+ using DownloadShelfContextMenu::ALWAYS_OPEN_TYPE;
+ using DownloadShelfContextMenu::CANCEL;
+ using DownloadShelfContextMenu::TOGGLE_PAUSE;
+};
+
+@interface DownloadItemController (Private)
+- (void)themeDidChangeNotification:(NSNotification*)aNotification;
+- (void)updateTheme:(ThemeProvider*)themeProvider;
+- (void)setState:(DownoadItemState)state;
+@end
+
+// Implementation of DownloadItemController
+
+@implementation DownloadItemController
+
+- (id)initWithModel:(BaseDownloadItemModel*)downloadModel
+ shelf:(DownloadShelfController*)shelf {
+ if ((self = [super initWithNibName:@"DownloadItem"
+ bundle:mac_util::MainAppBundle()])) {
+ // Must be called before [self view], so that bridge_ is set in awakeFromNib
+ bridge_.reset(new DownloadItemMac(downloadModel, self));
+ menuBridge_.reset(new DownloadShelfContextMenuMac(downloadModel));
+
+ NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
+ [defaultCenter addObserver:self
+ selector:@selector(themeDidChangeNotification:)
+ name:kBrowserThemeDidChangeNotification
+ object:nil];
+
+ shelf_ = shelf;
+ state_ = kNormal;
+ creationTime_ = base::Time::Now();
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [progressView_ setController:nil];
+ [[self view] removeFromSuperview];
+ [super dealloc];
+}
+
+- (void)awakeFromNib {
+ [progressView_ setController:self];
+
+ [self setStateFromDownload:bridge_->download_model()];
+
+ GTMUILocalizerAndLayoutTweaker* localizerAndLayoutTweaker =
+ [[[GTMUILocalizerAndLayoutTweaker alloc] init] autorelease];
+ [localizerAndLayoutTweaker applyLocalizer:localizer_ tweakingUI:[self view]];
+
+ // The strings are based on the download item's name, sizing tweaks have to be
+ // manually done.
+ DCHECK(buttonTweaker_ != nil);
+ CGFloat widthChange = [buttonTweaker_ changedWidth];
+ // If it's a dangerous download, size the two lines so the text/filename
+ // is always visible.
+ if ([self isDangerousMode]) {
+ widthChange +=
+ [GTMUILocalizerAndLayoutTweaker
+ sizeToFitFixedHeightTextField:dangerousDownloadLabel_
+ minWidth:kTextWidth];
+ }
+ // Grow the parent views
+ WidenView([self view], widthChange);
+ WidenView(dangerousDownloadView_, widthChange);
+ // Slide the two buttons over.
+ NSPoint frameOrigin = [buttonTweaker_ frame].origin;
+ frameOrigin.x += widthChange;
+ [buttonTweaker_ setFrameOrigin:frameOrigin];
+
+ ResourceBundle& rb = ResourceBundle::GetSharedInstance();
+ NSImage* alertIcon = rb.GetNativeImageNamed(IDR_WARNING);
+ DCHECK(alertIcon);
+ [image_ setImage:alertIcon];
+
+ bridge_->LoadIcon();
+ [self updateToolTip];
+}
+
+- (void)setStateFromDownload:(BaseDownloadItemModel*)downloadModel {
+ DCHECK_EQ(bridge_->download_model(), downloadModel);
+
+ // Handle dangerous downloads.
+ if (downloadModel->download()->safety_state() == DownloadItem::DANGEROUS) {
+ [self setState:kDangerous];
+
+ NSString* dangerousWarning;
+ NSString* confirmButtonTitle;
+ // The dangerous download label and button text are different for an
+ // extension file.
+ if (downloadModel->download()->is_extension_install()) {
+ dangerousWarning = l10n_util::GetNSStringWithFixup(
+ IDS_PROMPT_DANGEROUS_DOWNLOAD_EXTENSION);
+ confirmButtonTitle = l10n_util::GetNSStringWithFixup(
+ IDS_CONTINUE_EXTENSION_DOWNLOAD);
+ } else {
+ // This basic fixup copies Windows DownloadItemView::DownloadItemView().
+
+ // Extract the file extension (if any).
+ FilePath filename(downloadModel->download()->target_name());
+ FilePath::StringType extension = filename.Extension();
+
+ // Remove leading '.' from the extension
+ if (extension.length() > 0)
+ extension = extension.substr(1);
+
+ // Elide giant extensions.
+ if (extension.length() > kFileNameMaxLength / 2) {
+ std::wstring wide_extension;
+ gfx::ElideString(UTF8ToWide(extension), kFileNameMaxLength / 2,
+ &wide_extension);
+ extension = WideToUTF8(wide_extension);
+ }
+
+ // Rebuild the filename.extension.
+ std::wstring rootname = UTF8ToWide(filename.RemoveExtension().value());
+ gfx::ElideString(rootname, kFileNameMaxLength - extension.length(),
+ &rootname);
+ std::string new_filename = WideToUTF8(rootname);
+ if (extension.length())
+ new_filename += std::string(".") + extension;
+
+ dangerousWarning = l10n_util::GetNSStringFWithFixup(
+ IDS_PROMPT_DANGEROUS_DOWNLOAD, UTF8ToUTF16(new_filename));
+ confirmButtonTitle = l10n_util::GetNSStringWithFixup(IDS_SAVE_DOWNLOAD);
+ }
+ [dangerousDownloadLabel_ setStringValue:dangerousWarning];
+ [dangerousDownloadConfirmButton_ setTitle:confirmButtonTitle];
+ return;
+ }
+
+ // Set correct popup menu. Also, set draggable download on completion.
+ if (downloadModel->download()->state() == DownloadItem::COMPLETE) {
+ [progressView_ setMenu:completeDownloadMenu_];
+ [progressView_ setDownload:downloadModel->download()->full_path()];
+ } else {
+ [progressView_ setMenu:activeDownloadMenu_];
+ }
+
+ [cell_ setStateFromDownload:downloadModel];
+}
+
+- (void)setIcon:(NSImage*)icon {
+ [cell_ setImage:icon];
+}
+
+- (void)remove {
+ // We are deleted after this!
+ [shelf_ remove:self];
+}
+
+- (void)updateVisibility:(id)sender {
+ if ([[self view] window])
+ [self updateTheme:[[[self view] window] themeProvider]];
+
+ NSView* view = [self view];
+ NSRect containerFrame = [[view superview] frame];
+ [view setHidden:(NSMaxX([view frame]) > NSWidth(containerFrame))];
+}
+
+- (void)downloadWasOpened {
+ [shelf_ downloadWasOpened:self];
+}
+
+- (IBAction)handleButtonClick:(id)sender {
+ NSEvent* event = [NSApp currentEvent];
+ if ([event modifierFlags] & NSCommandKeyMask) {
+ // Let cmd-click show the file in Finder, like e.g. in Safari and Spotlight.
+ menuBridge_->ExecuteCommand(DownloadShelfContextMenuMac::SHOW_IN_FOLDER);
+ } else {
+ DownloadItem* download = bridge_->download_model()->download();
+ download->OpenDownload();
+ }
+}
+
+- (NSSize)preferredSize {
+ if (state_ == kNormal)
+ return [progressView_ frame].size;
+ DCHECK_EQ(kDangerous, state_);
+ return [dangerousDownloadView_ frame].size;
+}
+
+- (DownloadItem*)download {
+ return bridge_->download_model()->download();
+}
+
+- (void)updateToolTip {
+ string16 elidedFilename = gfx::ElideFilename(
+ [self download]->GetFileNameToReportUser(),
+ gfx::Font(), kToolTipMaxWidth);
+ [progressView_ setToolTip:base::SysUTF16ToNSString(elidedFilename)];
+}
+
+- (void)clearDangerousMode {
+ [self setState:kNormal];
+ // The state change hide the dangerouse download view and is now showing the
+ // download progress view. This means the view is likely to be a different
+ // size, so trigger a shelf layout to fix up spacing.
+ [shelf_ layoutItems];
+}
+
+- (BOOL)isDangerousMode {
+ return state_ == kDangerous;
+}
+
+- (void)setState:(DownoadItemState)state {
+ if (state_ == state)
+ return;
+ state_ = state;
+ if (state_ == kNormal) {
+ [progressView_ setHidden:NO];
+ [dangerousDownloadView_ setHidden:YES];
+ } else {
+ DCHECK_EQ(kDangerous, state_);
+ [progressView_ setHidden:YES];
+ [dangerousDownloadView_ setHidden:NO];
+ }
+ // NOTE: Do not relayout the shelf, as this could get called during initial
+ // setup of the the item, so the localized text and sizing might not have
+ // happened yet.
+}
+
+// Called after the current theme has changed.
+- (void)themeDidChangeNotification:(NSNotification*)aNotification {
+ ThemeProvider* themeProvider =
+ static_cast<ThemeProvider*>([[aNotification object] pointerValue]);
+ [self updateTheme:themeProvider];
+}
+
+// Adapt appearance to the current theme. Called after theme changes and before
+// this is shown for the first time.
+- (void)updateTheme:(ThemeProvider*)themeProvider {
+ NSColor* color =
+ themeProvider->GetNSColor(BrowserThemeProvider::COLOR_TAB_TEXT, true);
+ [dangerousDownloadLabel_ setTextColor:color];
+}
+
+- (IBAction)saveDownload:(id)sender {
+ // The user has confirmed a dangerous download. We record how quickly the
+ // user did this to detect whether we're being clickjacked.
+ UMA_HISTOGRAM_LONG_TIMES("clickjacking.save_download",
+ base::Time::Now() - creationTime_);
+ // This will change the state and notify us.
+ bridge_->download_model()->download()->DangerousDownloadValidated();
+}
+
+- (IBAction)discardDownload:(id)sender {
+ UMA_HISTOGRAM_LONG_TIMES("clickjacking.discard_download",
+ base::Time::Now() - creationTime_);
+ if (bridge_->download_model()->download()->state() ==
+ DownloadItem::IN_PROGRESS)
+ bridge_->download_model()->download()->Cancel(true);
+ bridge_->download_model()->download()->Remove(true);
+ // WARNING: we are deleted at this point. Don't access 'this'.
+}
+
+
+// Sets the enabled and checked state of a particular menu item for this
+// download. We translate the NSMenuItem selection to menu selections understood
+// by the non platform specific download context menu.
+- (BOOL)validateMenuItem:(NSMenuItem *)item {
+ SEL action = [item action];
+
+ int actionId = 0;
+ if (action == @selector(handleOpen:)) {
+ actionId = DownloadShelfContextMenuMac::OPEN_WHEN_COMPLETE;
+ } else if (action == @selector(handleAlwaysOpen:)) {
+ actionId = DownloadShelfContextMenuMac::ALWAYS_OPEN_TYPE;
+ } else if (action == @selector(handleReveal:)) {
+ actionId = DownloadShelfContextMenuMac::SHOW_IN_FOLDER;
+ } else if (action == @selector(handleCancel:)) {
+ actionId = DownloadShelfContextMenuMac::CANCEL;
+ } else if (action == @selector(handleTogglePause:)) {
+ actionId = DownloadShelfContextMenuMac::TOGGLE_PAUSE;
+ } else {
+ NOTREACHED();
+ return YES;
+ }
+
+ if (menuBridge_->IsCommandIdChecked(actionId))
+ [item setState:NSOnState];
+ else
+ [item setState:NSOffState];
+
+ return menuBridge_->IsCommandIdEnabled(actionId) ? YES : NO;
+}
+
+- (IBAction)handleOpen:(id)sender {
+ menuBridge_->ExecuteCommand(
+ DownloadShelfContextMenuMac::OPEN_WHEN_COMPLETE);
+}
+
+- (IBAction)handleAlwaysOpen:(id)sender {
+ menuBridge_->ExecuteCommand(
+ DownloadShelfContextMenuMac::ALWAYS_OPEN_TYPE);
+}
+
+- (IBAction)handleReveal:(id)sender {
+ menuBridge_->ExecuteCommand(DownloadShelfContextMenuMac::SHOW_IN_FOLDER);
+}
+
+- (IBAction)handleCancel:(id)sender {
+ menuBridge_->ExecuteCommand(DownloadShelfContextMenuMac::CANCEL);
+}
+
+- (IBAction)handleTogglePause:(id)sender {
+ if([sender state] == NSOnState) {
+ [sender setTitle:l10n_util::GetNSStringWithFixup(
+ IDS_DOWNLOAD_MENU_PAUSE_ITEM)];
+ } else {
+ [sender setTitle:l10n_util::GetNSStringWithFixup(
+ IDS_DOWNLOAD_MENU_RESUME_ITEM)];
+ }
+ menuBridge_->ExecuteCommand(DownloadShelfContextMenuMac::TOGGLE_PAUSE);
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/download/download_item_mac.h b/chrome/browser/ui/cocoa/download/download_item_mac.h
new file mode 100644
index 0000000..6ad83a3
--- /dev/null
+++ b/chrome/browser/ui/cocoa/download/download_item_mac.h
@@ -0,0 +1,63 @@
+// Copyright (c) 2010 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_DOWNLOAD_DOWNLOAD_ITEM_MAC_H_
+#define CHROME_BROWSER_UI_COCOA_DOWNLOAD_DOWNLOAD_ITEM_MAC_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+#include "base/scoped_ptr.h"
+#include "chrome/browser/cancelable_request.h"
+#include "chrome/browser/download/download_item.h"
+#include "chrome/browser/download/download_manager.h"
+#include "chrome/browser/icon_manager.h"
+
+class BaseDownloadItemModel;
+@class DownloadItemController;
+
+// A class that bridges the visible mac download items to chromium's download
+// model. The owning object (DownloadItemController) must explicitly call
+// |LoadIcon| if it wants to display the icon associated with this download.
+
+class DownloadItemMac : DownloadItem::Observer {
+ public:
+ // DownloadItemMac takes ownership of |download_model|.
+ DownloadItemMac(BaseDownloadItemModel* download_model,
+ DownloadItemController* controller);
+
+ // Destructor.
+ ~DownloadItemMac();
+
+ // DownloadItem::Observer implementation
+ virtual void OnDownloadUpdated(DownloadItem* download);
+ virtual void OnDownloadOpened(DownloadItem* download);
+ virtual void OnDownloadFileCompleted(DownloadItem* download) { }
+
+ BaseDownloadItemModel* download_model() { return download_model_.get(); }
+
+ // Asynchronous icon loading support.
+ void LoadIcon();
+
+ private:
+ // Callback for asynchronous icon loading.
+ void OnExtractIconComplete(IconManager::Handle handle, SkBitmap* icon_bitmap);
+
+ // The download item model we represent.
+ scoped_ptr<BaseDownloadItemModel> download_model_;
+
+ // The objective-c controller object.
+ DownloadItemController* item_controller_; // weak, owns us.
+
+ // For canceling an in progress icon request.
+ CancelableRequestConsumerT<int, 0> icon_consumer_;
+
+ // Stores the last known path where the file will be saved.
+ FilePath lastFilePath_;
+
+ DISALLOW_COPY_AND_ASSIGN(DownloadItemMac);
+};
+
+#endif // CHROME_BROWSER_UI_COCOA_DOWNLOAD_DOWNLOAD_ITEM_MAC_H_
diff --git a/chrome/browser/ui/cocoa/download/download_item_mac.mm b/chrome/browser/ui/cocoa/download/download_item_mac.mm
new file mode 100644
index 0000000..f39cb96
--- /dev/null
+++ b/chrome/browser/ui/cocoa/download/download_item_mac.mm
@@ -0,0 +1,101 @@
+// Copyright (c) 2010 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/ui/cocoa/download/download_item_mac.h"
+
+#include "base/callback.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/download/download_item.h"
+#include "chrome/browser/download/download_item_model.h"
+#import "chrome/browser/ui/cocoa/download/download_item_controller.h"
+#include "chrome/browser/ui/cocoa/download/download_util_mac.h"
+#include "skia/ext/skia_utils_mac.h"
+
+// DownloadItemMac -------------------------------------------------------------
+
+DownloadItemMac::DownloadItemMac(BaseDownloadItemModel* download_model,
+ DownloadItemController* controller)
+ : download_model_(download_model), item_controller_(controller) {
+ download_model_->download()->AddObserver(this);
+}
+
+DownloadItemMac::~DownloadItemMac() {
+ download_model_->download()->RemoveObserver(this);
+ icon_consumer_.CancelAllRequests();
+}
+
+void DownloadItemMac::OnDownloadUpdated(DownloadItem* download) {
+ DCHECK_EQ(download, download_model_->download());
+
+ if ([item_controller_ isDangerousMode] &&
+ download->safety_state() == DownloadItem::DANGEROUS_BUT_VALIDATED) {
+ // We have been approved.
+ [item_controller_ clearDangerousMode];
+ }
+
+ if (download->GetUserVerifiedFilePath() != lastFilePath_) {
+ // Turns out the file path is "unconfirmed %d.crdownload" for dangerous
+ // downloads. When the download is confirmed, the file is renamed on
+ // another thread, so reload the icon if the download filename changes.
+ LoadIcon();
+ lastFilePath_ = download->GetUserVerifiedFilePath();
+
+ [item_controller_ updateToolTip];
+ }
+
+ switch (download->state()) {
+ case DownloadItem::REMOVING:
+ [item_controller_ remove]; // We're deleted now!
+ break;
+ case DownloadItem::COMPLETE:
+ if (download->auto_opened()) {
+ [item_controller_ remove]; // We're deleted now!
+ return;
+ }
+ download_util::NotifySystemOfDownloadComplete(download->full_path());
+ // fall through
+ case DownloadItem::IN_PROGRESS:
+ case DownloadItem::CANCELLED:
+ [item_controller_ setStateFromDownload:download_model_.get()];
+ break;
+ default:
+ NOTREACHED();
+ }
+}
+
+void DownloadItemMac::OnDownloadOpened(DownloadItem* download) {
+ DCHECK_EQ(download, download_model_->download());
+ [item_controller_ downloadWasOpened];
+}
+
+void DownloadItemMac::LoadIcon() {
+ IconManager* icon_manager = g_browser_process->icon_manager();
+ if (!icon_manager) {
+ NOTREACHED();
+ return;
+ }
+
+ // We may already have this particular image cached.
+ FilePath file = download_model_->download()->GetUserVerifiedFilePath();
+ SkBitmap* icon_bitmap = icon_manager->LookupIcon(file, IconLoader::SMALL);
+ if (icon_bitmap) {
+ NSImage* icon = gfx::SkBitmapToNSImage(*icon_bitmap);
+ [item_controller_ setIcon:icon];
+ return;
+ }
+
+ // The icon isn't cached, load it asynchronously.
+ icon_manager->LoadIcon(file, IconLoader::SMALL, &icon_consumer_,
+ NewCallback(this,
+ &DownloadItemMac::OnExtractIconComplete));
+}
+
+void DownloadItemMac::OnExtractIconComplete(IconManager::Handle handle,
+ SkBitmap* icon_bitmap) {
+ if (!icon_bitmap)
+ return;
+
+ NSImage* icon = gfx::SkBitmapToNSImage(*icon_bitmap);
+ [item_controller_ setIcon:icon];
+}
diff --git a/chrome/browser/ui/cocoa/download/download_shelf_controller.h b/chrome/browser/ui/cocoa/download/download_shelf_controller.h
new file mode 100644
index 0000000..e75911f
--- /dev/null
+++ b/chrome/browser/ui/cocoa/download/download_shelf_controller.h
@@ -0,0 +1,103 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <Cocoa/Cocoa.h>
+
+#import "base/mac/cocoa_protocols.h"
+#include "base/scoped_nsobject.h"
+#include "base/scoped_ptr.h"
+#import "chrome/browser/ui/cocoa/view_resizer.h"
+
+@class AnimatableView;
+class BaseDownloadItemModel;
+class Browser;
+@class BrowserWindowController;
+@class DownloadItemController;
+class DownloadShelf;
+@class DownloadShelfView;
+@class HyperlinkButtonCell;
+
+// A controller class that manages the download shelf for one window. It is
+// responsible for the behavior of the shelf itself (showing/hiding, handling
+// the link, layout) as well as for managing the download items it contains.
+//
+// All the files in cocoa/downloads_* are related as follows:
+//
+// download_shelf_mac bridges calls from chromium's c++ world to the objc
+// download_shelf_controller for the shelf (this file). The shelf's background
+// is drawn by download_shelf_view. Every item in a shelf is controlled by a
+// download_item_controller.
+//
+// download_item_mac bridges calls from chromium's c++ world to the objc
+// download_item_controller, which is responsible for managing a single item
+// on the shelf. The item controller loads its UI from a xib file, where the
+// UI of an item itself is represented by a button that is drawn by
+// download_item_cell.
+
+@interface DownloadShelfController : NSViewController<NSTextViewDelegate> {
+ @private
+ IBOutlet HyperlinkButtonCell* showAllDownloadsCell_;
+
+ IBOutlet NSImageView* image_;
+
+ BOOL barIsVisible_;
+
+ scoped_ptr<DownloadShelf> bridge_;
+
+ // Height of the shelf when it's fully visible.
+ CGFloat maxShelfHeight_;
+
+ // Current height of the shelf. Changes while the shelf is animating in or
+ // out.
+ CGFloat currentShelfHeight_;
+
+ // Used to autoclose the shelf when the mouse is moved off it. Is non-nil
+ // only when a subsequent mouseExited event can trigger autoclose or when a
+ // subsequent mouseEntered event will cancel autoclose. Is nil otherwise.
+ scoped_nsobject<NSTrackingArea> trackingArea_;
+
+ // The download items we have added to our shelf.
+ scoped_nsobject<NSMutableArray> downloadItemControllers_;
+
+ // The container that contains (and clamps) all the download items.
+ IBOutlet NSView* itemContainerView_;
+
+ // Delegate that handles resizing our view.
+ id<ViewResizer> resizeDelegate_;
+};
+
+- (id)initWithBrowser:(Browser*)browser
+ resizeDelegate:(id<ViewResizer>)resizeDelegate;
+
+- (IBAction)showDownloadsTab:(id)sender;
+
+// Returns our view cast as an AnimatableView.
+- (AnimatableView*)animatableView;
+
+- (DownloadShelf*)bridge;
+- (BOOL)isVisible;
+
+- (IBAction)show:(id)sender;
+
+// Run when the user clicks the close button on the right side of the shelf.
+- (IBAction)hide:(id)sender;
+
+- (void)addDownloadItem:(BaseDownloadItemModel*)model;
+
+// Remove a download, possibly via clearing browser data.
+- (void)remove:(DownloadItemController*)download;
+
+// Called by individual item controllers when their downloads are opened.
+- (void)downloadWasOpened:(DownloadItemController*)download;
+
+// Notification that we are closing and should release our downloads.
+- (void)exiting;
+
+// Return the height of the download shelf.
+- (float)height;
+
+// Re-layouts all download items based on their current state.
+- (void)layoutItems;
+
+@end
diff --git a/chrome/browser/ui/cocoa/download/download_shelf_controller.mm b/chrome/browser/ui/cocoa/download/download_shelf_controller.mm
new file mode 100644
index 0000000..4238b98
--- /dev/null
+++ b/chrome/browser/ui/cocoa/download/download_shelf_controller.mm
@@ -0,0 +1,426 @@
+// Copyright (c) 2010 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "chrome/browser/ui/cocoa/download/download_shelf_controller.h"
+
+#include "app/l10n_util.h"
+#include "app/resource_bundle.h"
+#include "base/mac_util.h"
+#include "base/sys_string_conversions.h"
+#include "chrome/browser/download/download_item.h"
+#include "chrome/browser/download/download_manager.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/themes/browser_theme_provider.h"
+#include "chrome/browser/ui/browser.h"
+#import "chrome/browser/ui/cocoa/animatable_view.h"
+#include "chrome/browser/ui/cocoa/browser_window_cocoa.h"
+#import "chrome/browser/ui/cocoa/browser_window_controller.h"
+#include "chrome/browser/ui/cocoa/download/download_item_controller.h"
+#include "chrome/browser/ui/cocoa/download/download_shelf_mac.h"
+#import "chrome/browser/ui/cocoa/download/download_shelf_view.h"
+#import "chrome/browser/ui/cocoa/hyperlink_button_cell.h"
+#include "grit/generated_resources.h"
+#include "grit/theme_resources.h"
+#import "third_party/GTM/AppKit/GTMNSAnimation+Duration.h"
+
+// Download shelf autoclose behavior:
+//
+// The download shelf autocloses if all of this is true:
+// 1) An item on the shelf has just been opened.
+// 2) All remaining items on the shelf have been opened in the past.
+// 3) The mouse leaves the shelf and remains off the shelf for 5 seconds.
+//
+// If the mouse re-enters the shelf within the 5 second grace period, the
+// autoclose is canceled. An autoclose can only be scheduled in response to a
+// shelf item being opened or removed. If an item is opened and then the
+// resulting autoclose is canceled, subsequent mouse exited events will NOT
+// trigger an autoclose.
+//
+// If the shelf is manually closed while a download is still in progress, that
+// download is marked as "opened" for these purposes. If the shelf is later
+// reopened, these previously-in-progress download will not block autoclose,
+// even if that download was never actually clicked on and opened.
+
+namespace {
+
+// Max number of download views we'll contain. Any time a view is added and
+// we already have this many download views, one is removed.
+const size_t kMaxDownloadItemCount = 16;
+
+// Horizontal padding between two download items.
+const int kDownloadItemPadding = 0;
+
+// Duration for the open-new-leftmost-item animation, in seconds.
+const NSTimeInterval kDownloadItemOpenDuration = 0.8;
+
+// Duration for download shelf closing animation, in seconds.
+const NSTimeInterval kDownloadShelfCloseDuration = 0.12;
+
+// Amount of time between when the mouse is moved off the shelf and the shelf is
+// autoclosed, in seconds.
+const NSTimeInterval kAutoCloseDelaySeconds = 5;
+
+} // namespace
+
+@interface DownloadShelfController(Private)
+- (void)showDownloadShelf:(BOOL)enable;
+- (void)layoutItems:(BOOL)skipFirst;
+- (void)closed;
+- (BOOL)canAutoClose;
+
+- (void)updateTheme;
+- (void)themeDidChangeNotification:(NSNotification*)notification;
+- (void)viewFrameDidChange:(NSNotification*)notification;
+
+- (void)installTrackingArea;
+- (void)cancelAutoCloseAndRemoveTrackingArea;
+@end
+
+
+@implementation DownloadShelfController
+
+- (id)initWithBrowser:(Browser*)browser
+ resizeDelegate:(id<ViewResizer>)resizeDelegate {
+ if ((self = [super initWithNibName:@"DownloadShelf"
+ bundle:mac_util::MainAppBundle()])) {
+ resizeDelegate_ = resizeDelegate;
+ maxShelfHeight_ = NSHeight([[self view] bounds]);
+ currentShelfHeight_ = maxShelfHeight_;
+
+ // Reset the download shelf's frame height to zero. It will be properly
+ // positioned and sized the first time we try to set its height. (Just
+ // setting the rect to NSZeroRect does not work: it confuses Cocoa's view
+ // layout logic. If the shelf's width is too small, cocoa makes the download
+ // item container view wider than the browser window).
+ NSRect frame = [[self view] frame];
+ frame.size.height = 0;
+ [[self view] setFrame:frame];
+
+ downloadItemControllers_.reset([[NSMutableArray alloc] init]);
+
+ bridge_.reset(new DownloadShelfMac(browser, self));
+ }
+ return self;
+}
+
+- (void)awakeFromNib {
+ NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
+ [defaultCenter addObserver:self
+ selector:@selector(themeDidChangeNotification:)
+ name:kBrowserThemeDidChangeNotification
+ object:nil];
+
+ [[self animatableView] setResizeDelegate:resizeDelegate_];
+ [[self view] setPostsFrameChangedNotifications:YES];
+ [defaultCenter addObserver:self
+ selector:@selector(viewFrameDidChange:)
+ name:NSViewFrameDidChangeNotification
+ object:[self view]];
+
+ ResourceBundle& rb = ResourceBundle::GetSharedInstance();
+ NSImage* favicon = rb.GetNativeImageNamed(IDR_DOWNLOADS_FAVICON);
+ DCHECK(favicon);
+ [image_ setImage:favicon];
+}
+
+- (void)dealloc {
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [self cancelAutoCloseAndRemoveTrackingArea];
+
+ // The controllers will unregister themselves as observers when they are
+ // deallocated. No need to do that here.
+ [super dealloc];
+}
+
+// Called after the current theme has changed.
+- (void)themeDidChangeNotification:(NSNotification*)notification {
+ [self updateTheme];
+}
+
+// Called after the frame's rect has changed; usually when the height is
+// animated.
+- (void)viewFrameDidChange:(NSNotification*)notification {
+ // Anchor subviews at the top of |view|, so that it looks like the shelf
+ // is sliding out.
+ CGFloat newShelfHeight = NSHeight([[self view] frame]);
+ if (newShelfHeight == currentShelfHeight_)
+ return;
+
+ for (NSView* view in [[self view] subviews]) {
+ NSRect frame = [view frame];
+ frame.origin.y -= currentShelfHeight_ - newShelfHeight;
+ [view setFrame:frame];
+ }
+ currentShelfHeight_ = newShelfHeight;
+}
+
+// Adapt appearance to the current theme. Called after theme changes and before
+// this is shown for the first time.
+- (void)updateTheme {
+ NSColor* color = nil;
+
+ if (bridge_.get() && bridge_->browser() && bridge_->browser()->profile()) {
+ ThemeProvider* provider = bridge_->browser()->profile()->GetThemeProvider();
+
+ color =
+ provider->GetNSColor(BrowserThemeProvider::COLOR_BOOKMARK_TEXT, false);
+ }
+
+ if (!color)
+ color = [HyperlinkButtonCell defaultTextColor];
+
+ [showAllDownloadsCell_ setTextColor:color];
+}
+
+- (AnimatableView*)animatableView {
+ return static_cast<AnimatableView*>([self view]);
+}
+
+- (void)showDownloadsTab:(id)sender {
+ bridge_->browser()->ShowDownloadsTab();
+}
+
+- (void)remove:(DownloadItemController*)download {
+ // Look for the download in our controller array and remove it. This will
+ // explicity release it so that it removes itself as an Observer of the
+ // DownloadItem. We don't want to wait for autorelease since the DownloadItem
+ // we are observing will likely be gone by then.
+ [[NSNotificationCenter defaultCenter] removeObserver:download];
+
+ // TODO(dmaclach): Remove -- http://crbug.com/25845
+ [[download view] removeFromSuperview];
+
+ [downloadItemControllers_ removeObject:download];
+
+ [self layoutItems];
+
+ // Check to see if we have any downloads remaining and if not, hide the shelf.
+ if (![downloadItemControllers_ count])
+ [self showDownloadShelf:NO];
+}
+
+- (void)downloadWasOpened:(DownloadItemController*)item_controller {
+ // This should only be called on the main thead.
+ DCHECK([NSThread isMainThread]);
+
+ if ([self canAutoClose])
+ [self installTrackingArea];
+}
+
+// We need to explicitly release our download controllers here since they need
+// to remove themselves as observers before the remaining shutdown happens.
+- (void)exiting {
+ [[self animatableView] stopAnimation];
+ [self cancelAutoCloseAndRemoveTrackingArea];
+ downloadItemControllers_.reset();
+}
+
+// Show or hide the bar based on the value of |enable|. Handles animating the
+// resize of the content view.
+- (void)showDownloadShelf:(BOOL)enable {
+ if ([self isVisible] == enable)
+ return;
+
+ if ([[self view] window])
+ [self updateTheme];
+
+ // Animate the shelf out, but not in.
+ // TODO(rohitrao): We do not animate on the way in because Cocoa is already
+ // doing a lot of work to set up the download arrow animation. I've chosen to
+ // do no animation over janky animation. Find a way to make animating in
+ // smoother.
+ AnimatableView* view = [self animatableView];
+ if (enable)
+ [view setHeight:maxShelfHeight_];
+ else
+ [view animateToNewHeight:0 duration:kDownloadShelfCloseDuration];
+
+ barIsVisible_ = enable;
+}
+
+- (DownloadShelf*)bridge {
+ return bridge_.get();
+}
+
+- (BOOL)isVisible {
+ return barIsVisible_;
+}
+
+- (void)show:(id)sender {
+ [self showDownloadShelf:YES];
+}
+
+- (void)hide:(id)sender {
+ [self cancelAutoCloseAndRemoveTrackingArea];
+
+ // If |sender| isn't nil, then we're being closed from the UI by the user and
+ // we need to tell our shelf implementation to close. Otherwise, we're being
+ // closed programmatically by our shelf implementation.
+ if (sender)
+ bridge_->Close();
+ else
+ [self showDownloadShelf:NO];
+}
+
+- (void)animationDidEnd:(NSAnimation*)animation {
+ if (![self isVisible])
+ [self closed];
+}
+
+- (float)height {
+ return maxShelfHeight_;
+}
+
+// If |skipFirst| is true, the frame of the leftmost item is not set.
+- (void)layoutItems:(BOOL)skipFirst {
+ CGFloat currentX = 0;
+ for (DownloadItemController* itemController
+ in downloadItemControllers_.get()) {
+ NSRect frame = [[itemController view] frame];
+ frame.origin.x = currentX;
+ frame.size.width = [itemController preferredSize].width;
+ if (!skipFirst)
+ [[[itemController view] animator] setFrame:frame];
+ currentX += frame.size.width + kDownloadItemPadding;
+ skipFirst = NO;
+ }
+}
+
+- (void)layoutItems {
+ [self layoutItems:NO];
+}
+
+- (void)addDownloadItem:(BaseDownloadItemModel*)model {
+ DCHECK([NSThread isMainThread]);
+ [self cancelAutoCloseAndRemoveTrackingArea];
+
+ // Insert new item at the left.
+ scoped_nsobject<DownloadItemController> controller(
+ [[DownloadItemController alloc] initWithModel:model shelf:self]);
+
+ // Adding at index 0 in NSMutableArrays is O(1).
+ [downloadItemControllers_ insertObject:controller.get() atIndex:0];
+
+ [itemContainerView_ addSubview:[controller view]];
+
+ // The controller is in charge of removing itself as an observer in its
+ // dealloc.
+ [[NSNotificationCenter defaultCenter]
+ addObserver:controller
+ selector:@selector(updateVisibility:)
+ name:NSViewFrameDidChangeNotification
+ object:[controller view]];
+ [[NSNotificationCenter defaultCenter]
+ addObserver:controller
+ selector:@selector(updateVisibility:)
+ name:NSViewFrameDidChangeNotification
+ object:itemContainerView_];
+
+ // Start at width 0...
+ NSSize size = [controller preferredSize];
+ NSRect frame = NSMakeRect(0, 0, 0, size.height);
+ [[controller view] setFrame:frame];
+
+ // ...then animate in
+ frame.size.width = size.width;
+ [NSAnimationContext beginGrouping];
+ [[NSAnimationContext currentContext]
+ gtm_setDuration:kDownloadItemOpenDuration
+ eventMask:NSLeftMouseUpMask];
+ [[[controller view] animator] setFrame:frame];
+ [NSAnimationContext endGrouping];
+
+ // Keep only a limited number of items in the shelf.
+ if ([downloadItemControllers_ count] > kMaxDownloadItemCount) {
+ DCHECK(kMaxDownloadItemCount > 0);
+
+ // Since no user will ever see the item being removed (needs a horizontal
+ // screen resolution greater than 3200 at 16 items at 200 pixels each),
+ // there's no point in animating the removal.
+ [self remove:[downloadItemControllers_ lastObject]];
+ }
+
+ // Finally, move the remaining items to the right. Skip the first item when
+ // laying out the items, so that the longer animation duration we set up above
+ // is not overwritten.
+ [self layoutItems:YES];
+}
+
+- (void)closed {
+ NSUInteger i = 0;
+ while (i < [downloadItemControllers_ count]) {
+ DownloadItemController* itemController =
+ [downloadItemControllers_ objectAtIndex:i];
+ DownloadItem* download = [itemController download];
+ bool isTransferDone =
+ download->state() == DownloadItem::COMPLETE ||
+ download->state() == DownloadItem::CANCELLED;
+ if (isTransferDone &&
+ download->safety_state() != DownloadItem::DANGEROUS) {
+ [self remove:itemController];
+ } else {
+ // Treat the item as opened when we close. This way if we get shown again
+ // the user need not open this item for the shelf to auto-close.
+ download->set_opened(true);
+ ++i;
+ }
+ }
+}
+
+- (void)mouseEntered:(NSEvent*)event {
+ // If the mouse re-enters the download shelf, cancel the auto-close. Further
+ // mouse exits should not trigger autoclose, so also remove the tracking area.
+ [self cancelAutoCloseAndRemoveTrackingArea];
+}
+
+- (void)mouseExited:(NSEvent*)event {
+ // Cancel any previous hide requests, just to be safe.
+ [NSObject cancelPreviousPerformRequestsWithTarget:self
+ selector:@selector(hide:)
+ object:self];
+
+ // Schedule an autoclose after a delay. If the mouse is moved back into the
+ // view, or if an item is added to the shelf, the timer will be canceled.
+ [self performSelector:@selector(hide:)
+ withObject:self
+ afterDelay:kAutoCloseDelaySeconds];
+}
+
+- (BOOL)canAutoClose {
+ for (NSUInteger i = 0; i < [downloadItemControllers_ count]; ++i) {
+ DownloadItemController* itemController =
+ [downloadItemControllers_ objectAtIndex:i];
+ if (![itemController download]->opened())
+ return NO;
+ }
+ return YES;
+}
+
+- (void)installTrackingArea {
+ // Install the tracking area to listen for mouseExited messages and trigger
+ // the shelf autoclose.
+ if (trackingArea_.get())
+ return;
+
+ trackingArea_.reset([[NSTrackingArea alloc]
+ initWithRect:[[self view] bounds]
+ options:NSTrackingMouseEnteredAndExited |
+ NSTrackingActiveAlways
+ owner:self
+ userInfo:nil]);
+ [[self view] addTrackingArea:trackingArea_];
+}
+
+- (void)cancelAutoCloseAndRemoveTrackingArea {
+ [NSObject cancelPreviousPerformRequestsWithTarget:self
+ selector:@selector(hide:)
+ object:self];
+
+ if (trackingArea_.get()) {
+ [[self view] removeTrackingArea:trackingArea_];
+ trackingArea_.reset(nil);
+ }
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/download/download_shelf_mac.h b/chrome/browser/ui/cocoa/download/download_shelf_mac.h
new file mode 100644
index 0000000..ddfc6f8
--- /dev/null
+++ b/chrome/browser/ui/cocoa/download/download_shelf_mac.h
@@ -0,0 +1,43 @@
+// Copyright (c) 2010 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_DOWNLOAD_DOWNLOAD_SHELF_MAC_H_
+#define CHROME_BROWSER_UI_COCOA_DOWNLOAD_DOWNLOAD_SHELF_MAC_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#include "chrome/browser/download/download_shelf.h"
+
+class BaseDownloadItemModel;
+class CustomDrawButton;
+class DownloadItemMac;
+
+@class ShelfView;
+@class DownloadShelfController;
+
+// A class to bridge the chromium download shelf to mac gui. This is just a
+// wrapper class that forward everything to DownloadShelfController.
+
+class DownloadShelfMac : public DownloadShelf {
+ public:
+ explicit DownloadShelfMac(Browser* browser,
+ DownloadShelfController* controller);
+
+ // DownloadShelf implementation.
+ virtual void AddDownload(BaseDownloadItemModel* download_model);
+ virtual bool IsShowing() const;
+ virtual bool IsClosing() const;
+ virtual void Show();
+ virtual void Close();
+ virtual Browser* browser() const { return browser_; }
+
+ private:
+ // The browser that owns this shelf.
+ Browser* browser_;
+
+ DownloadShelfController* shelf_controller_; // weak, owns us
+};
+
+#endif // CHROME_BROWSER_UI_COCOA_DOWNLOAD_DOWNLOAD_SHELF_MAC_H_
diff --git a/chrome/browser/ui/cocoa/download/download_shelf_mac.mm b/chrome/browser/ui/cocoa/download/download_shelf_mac.mm
new file mode 100644
index 0000000..53c13f4
--- /dev/null
+++ b/chrome/browser/ui/cocoa/download/download_shelf_mac.mm
@@ -0,0 +1,40 @@
+// Copyright (c) 2010 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/ui/cocoa/download/download_shelf_mac.h"
+
+#include "chrome/browser/download/download_item_model.h"
+#include "chrome/browser/ui/browser.h"
+#import "chrome/browser/ui/cocoa/download/download_shelf_controller.h"
+#include "chrome/browser/ui/cocoa/download/download_item_mac.h"
+
+DownloadShelfMac::DownloadShelfMac(Browser* browser,
+ DownloadShelfController* controller)
+ : browser_(browser),
+ shelf_controller_(controller) {
+}
+
+void DownloadShelfMac::AddDownload(BaseDownloadItemModel* download_model) {
+ [shelf_controller_ addDownloadItem:download_model];
+ Show();
+}
+
+bool DownloadShelfMac::IsShowing() const {
+ return [shelf_controller_ isVisible] == YES;
+}
+
+bool DownloadShelfMac::IsClosing() const {
+ // TODO(estade): This is never called. For now just return false.
+ return false;
+}
+
+void DownloadShelfMac::Show() {
+ [shelf_controller_ show:nil];
+ browser_->UpdateDownloadShelfVisibility(true);
+}
+
+void DownloadShelfMac::Close() {
+ [shelf_controller_ hide:nil];
+ browser_->UpdateDownloadShelfVisibility(false);
+}
diff --git a/chrome/browser/ui/cocoa/download/download_shelf_mac_unittest.mm b/chrome/browser/ui/cocoa/download/download_shelf_mac_unittest.mm
new file mode 100644
index 0000000..961d2db
--- /dev/null
+++ b/chrome/browser/ui/cocoa/download/download_shelf_mac_unittest.mm
@@ -0,0 +1,91 @@
+// Copyright (c) 2010 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/ui/cocoa/browser_test_helper.h"
+#include "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "chrome/browser/ui/cocoa/download/download_shelf_mac.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+// A fake implementation of DownloadShelfController. It implements only the
+// methods that DownloadShelfMac call during the tests in this file. We get this
+// class into the DownloadShelfMac constructor by some questionable casting --
+// Objective C is a dynamic language, so we pretend that's ok.
+
+@interface FakeDownloadShelfController : NSObject {
+ @public
+ int callCountIsVisible;
+ int callCountShow;
+ int callCountHide;
+}
+
+- (BOOL)isVisible;
+- (IBAction)show:(id)sender;
+- (IBAction)hide:(id)sender;
+@end
+
+@implementation FakeDownloadShelfController
+
+- (BOOL)isVisible {
+ ++callCountIsVisible;
+ return YES;
+}
+
+- (IBAction)show:(id)sender {
+ ++callCountShow;
+}
+
+- (IBAction)hide:(id)sender {
+ ++callCountHide;
+}
+
+@end
+
+
+namespace {
+
+class DownloadShelfMacTest : public CocoaTest {
+
+ virtual void SetUp() {
+ CocoaTest::SetUp();
+ shelf_controller_.reset([[FakeDownloadShelfController alloc] init]);
+ }
+
+ protected:
+ scoped_nsobject<FakeDownloadShelfController> shelf_controller_;
+ BrowserTestHelper browser_helper_;
+};
+
+TEST_F(DownloadShelfMacTest, CreationDoesNotCallShow) {
+ // Also make sure the DownloadShelfMacTest constructor doesn't crash.
+ DownloadShelfMac shelf(browser_helper_.browser(),
+ (DownloadShelfController*)shelf_controller_.get());
+ EXPECT_EQ(0, shelf_controller_.get()->callCountShow);
+}
+
+TEST_F(DownloadShelfMacTest, ForwardsShow) {
+ DownloadShelfMac shelf(browser_helper_.browser(),
+ (DownloadShelfController*)shelf_controller_.get());
+ EXPECT_EQ(0, shelf_controller_.get()->callCountShow);
+ shelf.Show();
+ EXPECT_EQ(1, shelf_controller_.get()->callCountShow);
+}
+
+TEST_F(DownloadShelfMacTest, ForwardsHide) {
+ DownloadShelfMac shelf(browser_helper_.browser(),
+ (DownloadShelfController*)shelf_controller_.get());
+ EXPECT_EQ(0, shelf_controller_.get()->callCountHide);
+ shelf.Close();
+ EXPECT_EQ(1, shelf_controller_.get()->callCountHide);
+}
+
+TEST_F(DownloadShelfMacTest, ForwardsIsShowing) {
+ DownloadShelfMac shelf(browser_helper_.browser(),
+ (DownloadShelfController*)shelf_controller_.get());
+ EXPECT_EQ(0, shelf_controller_.get()->callCountIsVisible);
+ shelf.IsShowing();
+ EXPECT_EQ(1, shelf_controller_.get()->callCountIsVisible);
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/download/download_shelf_view.h b/chrome/browser/ui/cocoa/download/download_shelf_view.h
new file mode 100644
index 0000000..bcd949c
--- /dev/null
+++ b/chrome/browser/ui/cocoa/download/download_shelf_view.h
@@ -0,0 +1,20 @@
+// Copyright (c) 2010 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_COCOA_DOWNLOAD_DOWNLOAD_SHELF_VIEW_H_
+#define CHROME_BROWSER_UI_COCOA_DOWNLOAD_DOWNLOAD_SHELF_VIEW_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+#import "chrome/browser/ui/cocoa/animatable_view.h"
+
+// A view that handles any special rendering for the download shelf, painting
+// a gradient and managing a set of DownloadItemViews.
+
+@interface DownloadShelfView : AnimatableView {
+}
+@end
+
+#endif // CHROME_BROWSER_UI_COCOA_DOWNLOAD_DOWNLOAD_SHELF_VIEW_H_
diff --git a/chrome/browser/ui/cocoa/download/download_shelf_view.mm b/chrome/browser/ui/cocoa/download/download_shelf_view.mm
new file mode 100644
index 0000000..f3840ef
--- /dev/null
+++ b/chrome/browser/ui/cocoa/download/download_shelf_view.mm
@@ -0,0 +1,71 @@
+// Copyright (c) 2010 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "chrome/browser/ui/cocoa/download/download_shelf_view.h"
+
+#include "base/scoped_nsobject.h"
+#include "chrome/browser/themes/browser_theme_provider.h"
+#import "chrome/browser/ui/cocoa/themed_window.h"
+#import "chrome/browser/ui/cocoa/view_id_util.h"
+#include "grit/theme_resources.h"
+
+@implementation DownloadShelfView
+
+- (NSColor*)strokeColor {
+ BOOL isKey = [[self window] isKeyWindow];
+ ThemeProvider* themeProvider = [[self window] themeProvider];
+ return themeProvider ? themeProvider->GetNSColor(
+ isKey ? BrowserThemeProvider::COLOR_TOOLBAR_STROKE :
+ BrowserThemeProvider::COLOR_TOOLBAR_STROKE_INACTIVE, true) :
+ [NSColor blackColor];
+}
+
+- (void)drawRect:(NSRect)rect {
+ BOOL isKey = [[self window] isKeyWindow];
+ ThemeProvider* themeProvider = [[self window] themeProvider];
+ if (!themeProvider)
+ return;
+
+ NSColor* backgroundImageColor =
+ themeProvider->GetNSImageColorNamed(IDR_THEME_TOOLBAR, false);
+ if (backgroundImageColor) {
+ // We want our backgrounds for the shelf to be phased from the upper
+ // left hand corner of the view.
+ NSPoint phase = NSMakePoint(0, NSHeight([self bounds]));
+ [[NSGraphicsContext currentContext] setPatternPhase:phase];
+ [backgroundImageColor set];
+ NSRectFill([self bounds]);
+ } else {
+ NSGradient* gradient = themeProvider->GetNSGradient(
+ isKey ? BrowserThemeProvider::GRADIENT_TOOLBAR :
+ BrowserThemeProvider::GRADIENT_TOOLBAR_INACTIVE);
+ NSPoint startPoint = [self convertPoint:NSMakePoint(0, 0) fromView:nil];
+ NSPoint endPoint =
+ [self convertPoint:NSMakePoint(0, [self frame].size.height)
+ fromView:nil];
+
+ [gradient drawFromPoint:startPoint
+ toPoint:endPoint
+ options:NSGradientDrawsBeforeStartingLocation |
+ NSGradientDrawsAfterEndingLocation];
+ }
+
+ // Draw top stroke
+ [[self strokeColor] set];
+ NSRect borderRect, contentRect;
+ NSDivideRect([self bounds], &borderRect, &contentRect, 1, NSMaxYEdge);
+ NSRectFillUsingOperation(borderRect, NSCompositeSourceOver);
+}
+
+// Mouse down events on the download shelf should not allow dragging the parent
+// window around.
+- (BOOL)mouseDownCanMoveWindow {
+ return NO;
+}
+
+- (ViewID)viewID {
+ return VIEW_ID_DOWNLOAD_SHELF;
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/download/download_shelf_view_unittest.mm b/chrome/browser/ui/cocoa/download/download_shelf_view_unittest.mm
new file mode 100644
index 0000000..926593f
--- /dev/null
+++ b/chrome/browser/ui/cocoa/download/download_shelf_view_unittest.mm
@@ -0,0 +1,23 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/download/download_shelf_view.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+namespace {
+
+class DownloadShelfViewTest : public CocoaTest {
+};
+
+// This class only needs to do one thing: prevent mouse down events from moving
+// the parent window around.
+TEST_F(DownloadShelfViewTest, CanDragWindow) {
+ scoped_nsobject<DownloadShelfView> view([[DownloadShelfView alloc] init]);
+ EXPECT_FALSE([view mouseDownCanMoveWindow]);
+}
+
+} // namespace
diff --git a/chrome/browser/ui/cocoa/download/download_started_animation_mac.mm b/chrome/browser/ui/cocoa/download/download_started_animation_mac.mm
new file mode 100644
index 0000000..d95b732
--- /dev/null
+++ b/chrome/browser/ui/cocoa/download/download_started_animation_mac.mm
@@ -0,0 +1,196 @@
+// Copyright (c) 2010 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+//
+// This file contains the Mac implementation the download animation, displayed
+// at the start of a download. The animation produces an arrow pointing
+// downwards and animates towards the bottom of the window where the new
+// download appears in the download shelf.
+
+#include "chrome/browser/download/download_started_animation.h"
+
+#import <QuartzCore/QuartzCore.h>
+
+#include "app/resource_bundle.h"
+#include "chrome/browser/tab_contents/tab_contents.h"
+#include "chrome/browser/tab_contents/tab_contents_view_mac.h"
+#include "chrome/common/notification_registrar.h"
+#import "chrome/browser/ui/cocoa/animatable_image.h"
+#include "chrome/common/notification_details.h"
+#include "chrome/common/notification_source.h"
+#include "grit/theme_resources.h"
+#import "third_party/GTM/AppKit/GTMNSAnimation+Duration.h"
+#include "third_party/skia/include/utils/mac/SkCGUtils.h"
+
+class DownloadAnimationTabObserver;
+
+// A class for managing the Core Animation download animation.
+// Should be instantiated using +startAnimationWithTabContents:.
+@interface DownloadStartedAnimationMac : NSObject {
+ @private
+ // The observer for the TabContents we are drawing on.
+ scoped_ptr<DownloadAnimationTabObserver> observer_;
+ CGFloat imageWidth_;
+ AnimatableImage* animation_;
+};
+
++ (void)startAnimationWithTabContents:(TabContents*)tabContents;
+
+// Called by the Observer if the tab is hidden or closed.
+- (void)closeAnimation;
+
+@end
+
+// A helper class to monitor tab hidden and closed notifications. If we receive
+// such a notification, we stop the animation.
+class DownloadAnimationTabObserver : public NotificationObserver {
+ public:
+ DownloadAnimationTabObserver(DownloadStartedAnimationMac* owner,
+ TabContents* tab_contents)
+ : owner_(owner),
+ tab_contents_(tab_contents) {
+ registrar_.Add(this,
+ NotificationType::TAB_CONTENTS_HIDDEN,
+ Source<TabContents>(tab_contents_));
+ registrar_.Add(this,
+ NotificationType::TAB_CONTENTS_DESTROYED,
+ Source<TabContents>(tab_contents_));
+ }
+
+ // Runs when a tab is hidden or destroyed. Let our owner know we should end
+ // the animation.
+ void Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details) {
+ // This ends up deleting us.
+ [owner_ closeAnimation];
+ }
+
+ private:
+ // The object we need to inform when we get a notification. Weak.
+ DownloadStartedAnimationMac* owner_;
+
+ // The tab we are observing. Weak.
+ TabContents* tab_contents_;
+
+ // Used for registering to receive notifications and automatic clean up.
+ NotificationRegistrar registrar_;
+
+ DISALLOW_COPY_AND_ASSIGN(DownloadAnimationTabObserver);
+};
+
+@implementation DownloadStartedAnimationMac
+
+- (id)initWithTabContents:(TabContents*)tabContents {
+ if ((self = [super init])) {
+ // Load the image of the download arrow.
+ ResourceBundle& bundle = ResourceBundle::GetSharedInstance();
+ NSImage* image = bundle.GetNativeImageNamed(IDR_DOWNLOAD_ANIMATION_BEGIN);
+
+ // Figure out the positioning in the current tab. Try to position the layer
+ // against the left edge, and three times the download image's height from
+ // the bottom of the tab, assuming there is enough room. If there isn't
+ // enough, don't show the animation and let the shelf speak for itself.
+ gfx::Rect bounds;
+ tabContents->GetContainerBounds(&bounds);
+ imageWidth_ = [image size].width;
+ CGFloat imageHeight = [image size].height;
+
+ // Sanity check the size in case there's no room to display the animation.
+ if (bounds.height() < imageHeight) {
+ [self release];
+ return nil;
+ }
+
+ NSView* tabContentsView = tabContents->GetNativeView();
+ NSWindow* parentWindow = [tabContentsView window];
+ if (!parentWindow) {
+ // The tab is no longer frontmost.
+ [self release];
+ return nil;
+ }
+
+ NSPoint origin = [tabContentsView frame].origin;
+ origin = [tabContentsView convertPoint:origin toView:nil];
+ origin = [parentWindow convertBaseToScreen:origin];
+
+ // Create the animation object to assist in animating and fading.
+ CGFloat animationHeight = MIN(bounds.height(), 4 * imageHeight);
+ NSRect frame = NSMakeRect(origin.x, origin.y, imageWidth_, animationHeight);
+ animation_ = [[AnimatableImage alloc] initWithImage:image
+ animationFrame:frame];
+ [parentWindow addChildWindow:animation_ ordered:NSWindowAbove];
+
+ animationHeight = MIN(bounds.height(), 3 * imageHeight);
+ [animation_ setStartFrame:CGRectMake(0, animationHeight,
+ imageWidth_, imageHeight)];
+ [animation_ setEndFrame:CGRectMake(0, imageHeight,
+ imageWidth_, imageHeight)];
+ [animation_ setStartOpacity:1.0];
+ [animation_ setEndOpacity:0.4];
+ [animation_ setDuration:0.6];
+
+ observer_.reset(new DownloadAnimationTabObserver(self, tabContents));
+
+ // Set up to get notified about resize events on the parent window.
+ NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
+ [center addObserver:self
+ selector:@selector(parentWindowChanged:)
+ name:NSWindowDidResizeNotification
+ object:parentWindow];
+ // When the animation window closes, it needs to be removed from the
+ // parent window.
+ [center addObserver:self
+ selector:@selector(windowWillClose:)
+ name:NSWindowWillCloseNotification
+ object:animation_];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [super dealloc];
+}
+
+// Called when the parent window is resized.
+- (void)parentWindowChanged:(NSNotification*)notification {
+ NSWindow* parentWindow = [animation_ parentWindow];
+ DCHECK([[notification object] isEqual:parentWindow]);
+ NSRect parentFrame = [parentWindow frame];
+ NSRect frame = parentFrame;
+ frame.size.width = MIN(imageWidth_, NSWidth(parentFrame));
+ [animation_ setFrame:frame display:YES];
+}
+
+- (void)closeAnimation {
+ [animation_ close];
+}
+
+// When the animation closes, release self.
+- (void)windowWillClose:(NSNotification*)notification {
+ DCHECK([[notification object] isEqual:animation_]);
+ [[animation_ parentWindow] removeChildWindow:animation_];
+ [self release];
+}
+
++ (void)startAnimationWithTabContents:(TabContents*)contents {
+ // Will be deleted when the animation window closes.
+ DownloadStartedAnimationMac* controller =
+ [[self alloc] initWithTabContents:contents];
+ // The initializer can return nil.
+ if (!controller)
+ return;
+
+ // The |animation_| releases itself when done.
+ [controller->animation_ startAnimation];
+}
+
+@end
+
+void DownloadStartedAnimation::Show(TabContents* tab_contents) {
+ DCHECK(tab_contents);
+
+ // Will be deleted when the animation is complete.
+ [DownloadStartedAnimationMac startAnimationWithTabContents:tab_contents];
+}
diff --git a/chrome/browser/ui/cocoa/download/download_util_mac.h b/chrome/browser/ui/cocoa/download/download_util_mac.h
new file mode 100644
index 0000000..8f99c8b
--- /dev/null
+++ b/chrome/browser/ui/cocoa/download/download_util_mac.h
@@ -0,0 +1,25 @@
+// Copyright (c) 2010 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+//
+// Download utility functions for Mac OS X.
+
+#ifndef CHROME_BROWSER_UI_COCOA_DOWNLOAD_DOWNLOAD_UTIL_MAC_H_
+#define CHROME_BROWSER_UI_COCOA_DOWNLOAD_DOWNLOAD_UTIL_MAC_H_
+#pragma once
+
+#import <Cocoa/Cocoa.h>
+
+class FilePath;
+
+namespace download_util {
+
+void AddFileToPasteboard(NSPasteboard* pasteboard, const FilePath& path);
+
+// Notify the system that a download completed. This will cause the download
+// folder in the dock to bounce.
+void NotifySystemOfDownloadComplete(const FilePath& path);
+
+} // namespace download_util
+
+#endif // CHROME_BROWSER_UI_COCOA_DOWNLOAD_DOWNLOAD_UTIL_MAC_H_
diff --git a/chrome/browser/ui/cocoa/download/download_util_mac.mm b/chrome/browser/ui/cocoa/download/download_util_mac.mm
new file mode 100644
index 0000000..baafbbf
--- /dev/null
+++ b/chrome/browser/ui/cocoa/download/download_util_mac.mm
@@ -0,0 +1,83 @@
+// Copyright (c) 2010 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+//
+// Download utility implementation for Mac OS X.
+
+#include "chrome/browser/ui/cocoa/download/download_util_mac.h"
+
+#include "base/sys_string_conversions.h"
+#include "chrome/browser/download/download_item.h"
+#include "chrome/browser/download/download_manager.h"
+#import "chrome/browser/ui/cocoa/dock_icon.h"
+#include "gfx/native_widget_types.h"
+#include "skia/ext/skia_utils_mac.h"
+
+namespace download_util {
+
+void AddFileToPasteboard(NSPasteboard* pasteboard, const FilePath& path) {
+ // Write information about the file being dragged to the pasteboard.
+ NSString* file = base::SysUTF8ToNSString(path.value());
+ NSArray* fileList = [NSArray arrayWithObject:file];
+ [pasteboard declareTypes:[NSArray arrayWithObject:NSFilenamesPboardType]
+ owner:nil];
+ [pasteboard setPropertyList:fileList forType:NSFilenamesPboardType];
+}
+
+void NotifySystemOfDownloadComplete(const FilePath& path) {
+ NSString* filePath = base::SysUTF8ToNSString(path.value());
+ [[NSDistributedNotificationCenter defaultCenter]
+ postNotificationName:@"com.apple.DownloadFileFinished"
+ object:filePath];
+
+ NSString* parentPath = [filePath stringByDeletingLastPathComponent];
+ FNNotifyByPath(
+ reinterpret_cast<const UInt8*>([parentPath fileSystemRepresentation]),
+ kFNDirectoryModifiedMessage,
+ kNilOptions);
+}
+
+void DragDownload(const DownloadItem* download,
+ SkBitmap* icon,
+ gfx::NativeView view) {
+ NSPasteboard* pasteboard = [NSPasteboard pasteboardWithName:NSDragPboard];
+ AddFileToPasteboard(pasteboard, download->full_path());
+
+ // Convert to an NSImage.
+ NSImage* dragImage = gfx::SkBitmapToNSImage(*icon);
+
+ // Synthesize a drag event, since we don't have access to the actual event
+ // that initiated a drag (possibly consumed by the DOM UI, for example).
+ NSPoint position = [[view window] mouseLocationOutsideOfEventStream];
+ NSTimeInterval eventTime = [[NSApp currentEvent] timestamp];
+ NSEvent* dragEvent = [NSEvent mouseEventWithType:NSLeftMouseDragged
+ location:position
+ modifierFlags:NSLeftMouseDraggedMask
+ timestamp:eventTime
+ windowNumber:[[view window] windowNumber]
+ context:nil
+ eventNumber:0
+ clickCount:1
+ pressure:1.0];
+
+ // Run the drag operation.
+ [[view window] dragImage:dragImage
+ at:position
+ offset:NSZeroSize
+ event:dragEvent
+ pasteboard:pasteboard
+ source:view
+ slideBack:YES];
+}
+
+void UpdateAppIconDownloadProgress(int download_count,
+ bool progress_known,
+ float progress) {
+ DockIcon* dock_icon = [DockIcon sharedDockIcon];
+ [dock_icon setDownloads:download_count];
+ [dock_icon setIndeterminate:!progress_known];
+ [dock_icon setProgress:progress];
+ [dock_icon updateIcon];
+}
+
+} // namespace download_util
diff --git a/chrome/browser/ui/cocoa/download/download_util_mac_unittest.mm b/chrome/browser/ui/cocoa/download/download_util_mac_unittest.mm
new file mode 100644
index 0000000..bd99e02
--- /dev/null
+++ b/chrome/browser/ui/cocoa/download/download_util_mac_unittest.mm
@@ -0,0 +1,58 @@
+// Copyright (c) 2010 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Download utility test for Mac OS X.
+
+#include "base/path_service.h"
+#include "base/sys_string_conversions.h"
+#import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#import "chrome/browser/ui/cocoa/download/download_util_mac.h"
+#include "chrome/common/chrome_paths.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#import "testing/gtest_mac.h"
+#include "testing/platform_test.h"
+
+namespace {
+
+class DownloadUtilMacTest : public CocoaTest {
+ public:
+ DownloadUtilMacTest() {
+ pasteboard_ = [NSPasteboard pasteboardWithUniqueName];
+ }
+
+ virtual ~DownloadUtilMacTest() {
+ [pasteboard_ releaseGlobally];
+ }
+
+ NSPasteboard* const pasteboard() { return pasteboard_; }
+
+ private:
+ NSPasteboard* pasteboard_;
+};
+
+// Ensure adding files to the pasteboard methods works as expected.
+TEST_F(DownloadUtilMacTest, AddFileToPasteboardTest) {
+ // Get a download test file for addition to the pasteboard.
+ FilePath testPath;
+ ASSERT_TRUE(PathService::Get(chrome::DIR_TEST_DATA, &testPath));
+ FilePath testFile(FILE_PATH_LITERAL("download-test1.lib"));
+ testPath = testPath.Append(testFile);
+
+ // Add a test file to the pasteboard via the download_util method.
+ download_util::AddFileToPasteboard(pasteboard(), testPath);
+
+ // Test to see that the object type for dragging files is available.
+ NSArray* types = [NSArray arrayWithObject:NSFilenamesPboardType];
+ NSString* available = [pasteboard() availableTypeFromArray:types];
+ EXPECT_TRUE(available != nil);
+
+ // Ensure the path is what we expect.
+ NSArray* files = [pasteboard() propertyListForType:NSFilenamesPboardType];
+ ASSERT_TRUE(files != nil);
+ NSString* expectedPath = [files objectAtIndex:0];
+ NSString* realPath = base::SysWideToNSString(testPath.ToWStringHack());
+ EXPECT_NSEQ(expectedPath, realPath);
+}
+
+} // namespace