diff options
Diffstat (limited to 'chrome/browser/ui/cocoa/download')
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 |