download_item_controller.mm revision ddb351dbec246cf1fab5ec20d2d5520909041de1
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