Home | History | Annotate | Download | only in download
      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