// 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/cocoa/download_item_controller.h" #include "app/l10n_util_mac.h" #include "app/resource_bundle.h" #include "app/text_elider.h" #include "base/histogram.h" #include "base/mac_util.h" #include "base/string_util.h" #include "base/sys_string_conversions.h" #include "base/utf_string_conversions.h" #import "chrome/browser/browser_theme_provider.h" #import "chrome/browser/cocoa/download_item_button.h" #import "chrome/browser/cocoa/download_item_cell.h" #include "chrome/browser/cocoa/download_item_mac.h" #import "chrome/browser/cocoa/download_shelf_controller.h" #import "chrome/browser/cocoa/themed_window.h" #import "chrome/browser/cocoa/ui_localizer.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" #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.GetNSImageNamed(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 filepath(downloadModel->download()->original_name()); FilePath::StringType extension = filepath.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; ElideString(UTF8ToWide(extension), kFileNameMaxLength / 2, &wide_extension); extension = WideToUTF8(wide_extension); } // Rebuild the filename.extension. std::wstring rootname = UTF8ToWide(filepath.BaseName().RemoveExtension().value()); ElideString(rootname, kFileNameMaxLength - extension.length(), &rootname); std::string filename = WideToUTF8(rootname); if (extension.length()) filename += std::string(".") + extension; dangerousWarning = l10n_util::GetNSStringFWithFixup( IDS_PROMPT_DANGEROUS_DOWNLOAD, UTF8ToUTF16(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]]; // TODO(thakis): Make this prettier, by fading the items out or overlaying // the partial visible one with a horizontal alpha gradient -- crbug.com/17830 NSView* view = [self view]; NSRect containerFrame = [[view superview] frame]; [view setHidden:(NSMaxX([view frame]) > NSWidth(containerFrame))]; } - (IBAction)handleButtonClick:(id)sender { 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 { std::wstring elidedFilename = gfx::ElideFilename( [self download]->GetFileName(), gfx::Font(), kToolTipMaxWidth); [progressView_ setToolTip:base::SysWideToNSString(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([[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