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 32 namespace { 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. 38 const int kTextWidth = 140; // Pixels 39 40 // The maximum number of characters we show in a file name when displaying the 41 // dangerous download message. 42 const int kFileNameMaxLength = 20; 43 44 // The maximum width in pixels for the file name tooltip. 45 const int kToolTipMaxWidth = 900; 46 47 48 // Helper to widen a view. 49 void 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 73 class 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