| // 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/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_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(); |
| if (download->state() == DownloadItem::IN_PROGRESS) |
| download->set_open_when_complete(!download->open_when_complete()); |
| else if (download->state() == DownloadItem::COMPLETE) |
| download_util::OpenDownload(download); |
| } |
| |
| - (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<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()->manager()->DangerousDownloadValidated( |
| bridge_->download_model()->download()); |
| } |
| |
| - (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 |