1// Copyright (c) 2011 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#import "chrome/browser/ui/cocoa/download/download_item_controller.h" 6 7#include "base/mac/mac_util.h" 8#include "base/metrics/histogram.h" 9#include "base/string16.h" 10#include "base/string_util.h" 11#include "base/sys_string_conversions.h" 12#include "base/utf_string_conversions.h" 13#include "chrome/browser/download/download_item.h" 14#include "chrome/browser/download/download_item_model.h" 15#include "chrome/browser/download/download_shelf.h" 16#include "chrome/browser/download/download_util.h" 17#import "chrome/browser/themes/theme_service.h" 18#import "chrome/browser/ui/cocoa/download/download_item_button.h" 19#import "chrome/browser/ui/cocoa/download/download_item_cell.h" 20#include "chrome/browser/ui/cocoa/download/download_item_mac.h" 21#import "chrome/browser/ui/cocoa/download/download_shelf_controller.h" 22#import "chrome/browser/ui/cocoa/themed_window.h" 23#import "chrome/browser/ui/cocoa/ui_localizer.h" 24#include "grit/generated_resources.h" 25#include "grit/theme_resources.h" 26#include "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h" 27#include "ui/base/l10n/l10n_util_mac.h" 28#include "ui/base/resource/resource_bundle.h" 29#include "ui/base/text/text_elider.h" 30#include "ui/gfx/image.h" 31 32namespace { 33 34// NOTE: Mac currently doesn't use this like Windows does. Mac uses this to 35// control the min size on the dangerous download text. TVL sent a query off to 36// UX to fully spec all the the behaviors of download items and truncations 37// rules so all platforms can get inline in the future. 38const int kTextWidth = 140; // Pixels 39 40// The maximum number of characters we show in a file name when displaying the 41// dangerous download message. 42const int kFileNameMaxLength = 20; 43 44// The maximum width in pixels for the file name tooltip. 45const int kToolTipMaxWidth = 900; 46 47 48// Helper to widen a view. 49void WidenView(NSView* view, CGFloat widthChange) { 50 // If it is an NSBox, the autoresize of the contentView is the issue. 51 NSView* contentView = view; 52 if ([view isKindOfClass:[NSBox class]]) { 53 contentView = [(NSBox*)view contentView]; 54 } 55 BOOL autoresizesSubviews = [contentView autoresizesSubviews]; 56 if (autoresizesSubviews) { 57 [contentView setAutoresizesSubviews:NO]; 58 } 59 60 NSRect frame = [view frame]; 61 frame.size.width += widthChange; 62 [view setFrame:frame]; 63 64 if (autoresizesSubviews) { 65 [contentView setAutoresizesSubviews:YES]; 66 } 67} 68 69} // namespace 70 71// A class for the chromium-side part of the download shelf context menu. 72 73class DownloadShelfContextMenuMac : public DownloadShelfContextMenu { 74 public: 75 DownloadShelfContextMenuMac(BaseDownloadItemModel* model) 76 : DownloadShelfContextMenu(model) { } 77 78 using DownloadShelfContextMenu::ExecuteCommand; 79 using DownloadShelfContextMenu::IsCommandIdChecked; 80 using DownloadShelfContextMenu::IsCommandIdEnabled; 81 82 using DownloadShelfContextMenu::SHOW_IN_FOLDER; 83 using DownloadShelfContextMenu::OPEN_WHEN_COMPLETE; 84 using DownloadShelfContextMenu::ALWAYS_OPEN_TYPE; 85 using DownloadShelfContextMenu::CANCEL; 86 using DownloadShelfContextMenu::TOGGLE_PAUSE; 87}; 88 89@interface DownloadItemController (Private) 90- (void)themeDidChangeNotification:(NSNotification*)aNotification; 91- (void)updateTheme:(ui::ThemeProvider*)themeProvider; 92- (void)setState:(DownoadItemState)state; 93@end 94 95// Implementation of DownloadItemController 96 97@implementation DownloadItemController 98 99- (id)initWithModel:(BaseDownloadItemModel*)downloadModel 100 shelf:(DownloadShelfController*)shelf { 101 if ((self = [super initWithNibName:@"DownloadItem" 102 bundle:base::mac::MainAppBundle()])) { 103 // Must be called before [self view], so that bridge_ is set in awakeFromNib 104 bridge_.reset(new DownloadItemMac(downloadModel, self)); 105 menuBridge_.reset(new DownloadShelfContextMenuMac(downloadModel)); 106 107 NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter]; 108 [defaultCenter addObserver:self 109 selector:@selector(themeDidChangeNotification:) 110 name:kBrowserThemeDidChangeNotification 111 object:nil]; 112 113 shelf_ = shelf; 114 state_ = kNormal; 115 creationTime_ = base::Time::Now(); 116 } 117 return self; 118} 119 120- (void)dealloc { 121 [[NSNotificationCenter defaultCenter] removeObserver:self]; 122 [progressView_ setController:nil]; 123 [[self view] removeFromSuperview]; 124 [super dealloc]; 125} 126 127- (void)awakeFromNib { 128 [progressView_ setController:self]; 129 130 [self setStateFromDownload:bridge_->download_model()]; 131 132 GTMUILocalizerAndLayoutTweaker* localizerAndLayoutTweaker = 133 [[[GTMUILocalizerAndLayoutTweaker alloc] init] autorelease]; 134 [localizerAndLayoutTweaker applyLocalizer:localizer_ tweakingUI:[self view]]; 135 136 // The strings are based on the download item's name, sizing tweaks have to be 137 // manually done. 138 DCHECK(buttonTweaker_ != nil); 139 CGFloat widthChange = [buttonTweaker_ changedWidth]; 140 // If it's a dangerous download, size the two lines so the text/filename 141 // is always visible. 142 if ([self isDangerousMode]) { 143 widthChange += 144 [GTMUILocalizerAndLayoutTweaker 145 sizeToFitFixedHeightTextField:dangerousDownloadLabel_ 146 minWidth:kTextWidth]; 147 } 148 // Grow the parent views 149 WidenView([self view], widthChange); 150 WidenView(dangerousDownloadView_, widthChange); 151 // Slide the two buttons over. 152 NSPoint frameOrigin = [buttonTweaker_ frame].origin; 153 frameOrigin.x += widthChange; 154 [buttonTweaker_ setFrameOrigin:frameOrigin]; 155 156 bridge_->LoadIcon(); 157 [self updateToolTip]; 158} 159 160- (void)setStateFromDownload:(BaseDownloadItemModel*)downloadModel { 161 DCHECK_EQ(bridge_->download_model(), downloadModel); 162 163 // Handle dangerous downloads. 164 if (downloadModel->download()->safety_state() == DownloadItem::DANGEROUS) { 165 [self setState:kDangerous]; 166 167 ResourceBundle& rb = ResourceBundle::GetSharedInstance(); 168 NSString* dangerousWarning; 169 NSString* confirmButtonTitle; 170 NSImage* alertIcon; 171 172 // The dangerous download label, button text and icon are different under 173 // different cases. 174 if (downloadModel->download()->danger_type() == 175 DownloadItem::DANGEROUS_URL) { 176 // Safebrowsing shows the download URL leads to malicious file. 177 alertIcon = rb.GetNativeImageNamed(IDR_SAFEBROWSING_WARNING); 178 dangerousWarning = l10n_util::GetNSStringWithFixup( 179 IDS_PROMPT_UNSAFE_DOWNLOAD_URL); 180 confirmButtonTitle = l10n_util::GetNSStringWithFixup(IDS_SAVE_DOWNLOAD); 181 } else { 182 // It's a dangerous file type (e.g.: an executable). 183 DCHECK_EQ(downloadModel->download()->danger_type(), 184 DownloadItem::DANGEROUS_FILE); 185 alertIcon = rb.GetNativeImageNamed(IDR_WARNING); 186 if (downloadModel->download()->is_extension_install()) { 187 dangerousWarning = l10n_util::GetNSStringWithFixup( 188 IDS_PROMPT_DANGEROUS_DOWNLOAD_EXTENSION); 189 confirmButtonTitle = l10n_util::GetNSStringWithFixup( 190 IDS_CONTINUE_EXTENSION_DOWNLOAD); 191 } else { 192 // This basic fixup copies Windows DownloadItemView::DownloadItemView(). 193 194 // Extract the file extension (if any). 195 FilePath filename(downloadModel->download()->target_name()); 196 FilePath::StringType extension = filename.Extension(); 197 198 // Remove leading '.' from the extension 199 if (extension.length() > 0) 200 extension = extension.substr(1); 201 202 // Elide giant extensions. 203 if (extension.length() > kFileNameMaxLength / 2) { 204 string16 utf16_extension; 205 ui::ElideString(UTF8ToUTF16(extension), kFileNameMaxLength / 2, 206 &utf16_extension); 207 extension = UTF16ToUTF8(utf16_extension); 208 } 209 210 // Rebuild the filename.extension. 211 string16 rootname = UTF8ToUTF16(filename.RemoveExtension().value()); 212 ui::ElideString(rootname, kFileNameMaxLength - extension.length(), 213 &rootname); 214 std::string new_filename = UTF16ToUTF8(rootname); 215 if (extension.length()) 216 new_filename += std::string(".") + extension; 217 218 dangerousWarning = l10n_util::GetNSStringFWithFixup( 219 IDS_PROMPT_DANGEROUS_DOWNLOAD, UTF8ToUTF16(new_filename)); 220 confirmButtonTitle = 221 l10n_util::GetNSStringWithFixup(IDS_SAVE_DOWNLOAD); 222 } 223 } 224 DCHECK(alertIcon); 225 [image_ setImage:alertIcon]; 226 DCHECK(dangerousWarning); 227 [dangerousDownloadLabel_ setStringValue:dangerousWarning]; 228 DCHECK(confirmButtonTitle); 229 [dangerousDownloadConfirmButton_ setTitle:confirmButtonTitle]; 230 return; 231 } 232 233 // Set correct popup menu. Also, set draggable download on completion. 234 if (downloadModel->download()->IsComplete()) { 235 [progressView_ setMenu:completeDownloadMenu_]; 236 [progressView_ setDownload:downloadModel->download()->full_path()]; 237 } else { 238 [progressView_ setMenu:activeDownloadMenu_]; 239 } 240 241 [cell_ setStateFromDownload:downloadModel]; 242} 243 244- (void)setIcon:(NSImage*)icon { 245 [cell_ setImage:icon]; 246} 247 248- (void)remove { 249 // We are deleted after this! 250 [shelf_ remove:self]; 251} 252 253- (void)updateVisibility:(id)sender { 254 if ([[self view] window]) 255 [self updateTheme:[[[self view] window] themeProvider]]; 256 257 NSView* view = [self view]; 258 NSRect containerFrame = [[view superview] frame]; 259 [view setHidden:(NSMaxX([view frame]) > NSWidth(containerFrame))]; 260} 261 262- (void)downloadWasOpened { 263 [shelf_ downloadWasOpened:self]; 264} 265 266- (IBAction)handleButtonClick:(id)sender { 267 NSEvent* event = [NSApp currentEvent]; 268 if ([event modifierFlags] & NSCommandKeyMask) { 269 // Let cmd-click show the file in Finder, like e.g. in Safari and Spotlight. 270 menuBridge_->ExecuteCommand(DownloadShelfContextMenuMac::SHOW_IN_FOLDER); 271 } else { 272 DownloadItem* download = bridge_->download_model()->download(); 273 download->OpenDownload(); 274 } 275} 276 277- (NSSize)preferredSize { 278 if (state_ == kNormal) 279 return [progressView_ frame].size; 280 DCHECK_EQ(kDangerous, state_); 281 return [dangerousDownloadView_ frame].size; 282} 283 284- (DownloadItem*)download { 285 return bridge_->download_model()->download(); 286} 287 288- (void)updateToolTip { 289 string16 elidedFilename = ui::ElideFilename( 290 [self download]->GetFileNameToReportUser(), 291 gfx::Font(), kToolTipMaxWidth); 292 [progressView_ setToolTip:base::SysUTF16ToNSString(elidedFilename)]; 293} 294 295- (void)clearDangerousMode { 296 [self setState:kNormal]; 297 // The state change hide the dangerouse download view and is now showing the 298 // download progress view. This means the view is likely to be a different 299 // size, so trigger a shelf layout to fix up spacing. 300 [shelf_ layoutItems]; 301} 302 303- (BOOL)isDangerousMode { 304 return state_ == kDangerous; 305} 306 307- (void)setState:(DownoadItemState)state { 308 if (state_ == state) 309 return; 310 state_ = state; 311 if (state_ == kNormal) { 312 [progressView_ setHidden:NO]; 313 [dangerousDownloadView_ setHidden:YES]; 314 } else { 315 DCHECK_EQ(kDangerous, state_); 316 [progressView_ setHidden:YES]; 317 [dangerousDownloadView_ setHidden:NO]; 318 } 319 // NOTE: Do not relayout the shelf, as this could get called during initial 320 // setup of the the item, so the localized text and sizing might not have 321 // happened yet. 322} 323 324// Called after the current theme has changed. 325- (void)themeDidChangeNotification:(NSNotification*)aNotification { 326 ui::ThemeProvider* themeProvider = 327 static_cast<ThemeService*>([[aNotification object] pointerValue]); 328 [self updateTheme:themeProvider]; 329} 330 331// Adapt appearance to the current theme. Called after theme changes and before 332// this is shown for the first time. 333- (void)updateTheme:(ui::ThemeProvider*)themeProvider { 334 NSColor* color = 335 themeProvider->GetNSColor(ThemeService::COLOR_TAB_TEXT, true); 336 [dangerousDownloadLabel_ setTextColor:color]; 337} 338 339- (IBAction)saveDownload:(id)sender { 340 // The user has confirmed a dangerous download. We record how quickly the 341 // user did this to detect whether we're being clickjacked. 342 UMA_HISTOGRAM_LONG_TIMES("clickjacking.save_download", 343 base::Time::Now() - creationTime_); 344 // This will change the state and notify us. 345 bridge_->download_model()->download()->DangerousDownloadValidated(); 346} 347 348- (IBAction)discardDownload:(id)sender { 349 UMA_HISTOGRAM_LONG_TIMES("clickjacking.discard_download", 350 base::Time::Now() - creationTime_); 351 DownloadItem* download = bridge_->download_model()->download(); 352 if (download->IsPartialDownload()) 353 download->Cancel(true); 354 download->Delete(DownloadItem::DELETE_DUE_TO_USER_DISCARD); 355 // WARNING: we are deleted at this point. Don't access 'this'. 356} 357 358 359// Sets the enabled and checked state of a particular menu item for this 360// download. We translate the NSMenuItem selection to menu selections understood 361// by the non platform specific download context menu. 362- (BOOL)validateMenuItem:(NSMenuItem *)item { 363 SEL action = [item action]; 364 365 int actionId = 0; 366 if (action == @selector(handleOpen:)) { 367 actionId = DownloadShelfContextMenuMac::OPEN_WHEN_COMPLETE; 368 } else if (action == @selector(handleAlwaysOpen:)) { 369 actionId = DownloadShelfContextMenuMac::ALWAYS_OPEN_TYPE; 370 } else if (action == @selector(handleReveal:)) { 371 actionId = DownloadShelfContextMenuMac::SHOW_IN_FOLDER; 372 } else if (action == @selector(handleCancel:)) { 373 actionId = DownloadShelfContextMenuMac::CANCEL; 374 } else if (action == @selector(handleTogglePause:)) { 375 actionId = DownloadShelfContextMenuMac::TOGGLE_PAUSE; 376 } else { 377 NOTREACHED(); 378 return YES; 379 } 380 381 if (menuBridge_->IsCommandIdChecked(actionId)) 382 [item setState:NSOnState]; 383 else 384 [item setState:NSOffState]; 385 386 return menuBridge_->IsCommandIdEnabled(actionId) ? YES : NO; 387} 388 389- (IBAction)handleOpen:(id)sender { 390 menuBridge_->ExecuteCommand( 391 DownloadShelfContextMenuMac::OPEN_WHEN_COMPLETE); 392} 393 394- (IBAction)handleAlwaysOpen:(id)sender { 395 menuBridge_->ExecuteCommand( 396 DownloadShelfContextMenuMac::ALWAYS_OPEN_TYPE); 397} 398 399- (IBAction)handleReveal:(id)sender { 400 menuBridge_->ExecuteCommand(DownloadShelfContextMenuMac::SHOW_IN_FOLDER); 401} 402 403- (IBAction)handleCancel:(id)sender { 404 menuBridge_->ExecuteCommand(DownloadShelfContextMenuMac::CANCEL); 405} 406 407- (IBAction)handleTogglePause:(id)sender { 408 if([sender state] == NSOnState) { 409 [sender setTitle:l10n_util::GetNSStringWithFixup( 410 IDS_DOWNLOAD_MENU_PAUSE_ITEM)]; 411 } else { 412 [sender setTitle:l10n_util::GetNSStringWithFixup( 413 IDS_DOWNLOAD_MENU_RESUME_ITEM)]; 414 } 415 menuBridge_->ExecuteCommand(DownloadShelfContextMenuMac::TOGGLE_PAUSE); 416} 417 418@end 419