Popular sites on the NTP: check that experiment group StartsWith (rather than IS...
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / download / download_item_controller.mm
blobf562e199064b7b9a1a9d2f4b760aeaea890af5eb
1 // Copyright 2012 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.
5 #import "chrome/browser/ui/cocoa/download/download_item_controller.h"
7 #include "base/mac/bundle_locations.h"
8 #include "base/metrics/histogram.h"
9 #include "base/strings/string16.h"
10 #include "base/strings/string_util.h"
11 #include "base/strings/sys_string_conversions.h"
12 #include "base/strings/utf_string_conversions.h"
13 #include "chrome/browser/download/chrome_download_manager_delegate.h"
14 #include "chrome/browser/download/download_item_model.h"
15 #include "chrome/browser/download/download_shelf_context_menu.h"
16 #include "chrome/browser/extensions/api/experience_sampling_private/experience_sampling.h"
17 #import "chrome/browser/themes/theme_properties.h"
18 #import "chrome/browser/themes/theme_service.h"
19 #import "chrome/browser/ui/cocoa/download/download_item_button.h"
20 #import "chrome/browser/ui/cocoa/download/download_item_cell.h"
21 #include "chrome/browser/ui/cocoa/download/download_item_mac.h"
22 #import "chrome/browser/ui/cocoa/download/download_shelf_context_menu_controller.h"
23 #import "chrome/browser/ui/cocoa/download/download_shelf_controller.h"
24 #import "chrome/browser/ui/cocoa/themed_window.h"
25 #import "chrome/browser/ui/cocoa/ui_localizer.h"
26 #include "content/public/browser/download_item.h"
27 #include "content/public/browser/page_navigator.h"
28 #include "grit/theme_resources.h"
29 #include "third_party/google_toolbox_for_mac/src/AppKit/GTMUILocalizerAndLayoutTweaker.h"
30 #include "ui/base/l10n/l10n_util_mac.h"
31 #include "ui/base/resource/resource_bundle.h"
32 #include "ui/gfx/font.h"
33 #include "ui/gfx/image/image.h"
34 #include "ui/gfx/text_elider.h"
36 using content::DownloadItem;
37 using extensions::ExperienceSamplingEvent;
39 namespace {
41 // NOTE: Mac currently doesn't use this like Windows does.  Mac uses this to
42 // control the min size on the dangerous download text.  TVL sent a query off to
43 // UX to fully spec all the the behaviors of download items and truncations
44 // rules so all platforms can get inline in the future.
45 const int kTextWidth = 140;            // Pixels
47 // The maximum width in pixels for the file name tooltip.
48 const int kToolTipMaxWidth = 900;
51 // Helper to widen a view.
52 void WidenView(NSView* view, CGFloat widthChange) {
53   // If it is an NSBox, the autoresize of the contentView is the issue.
54   NSView* contentView = view;
55   if ([view isKindOfClass:[NSBox class]]) {
56     contentView = [(NSBox*)view contentView];
57   }
58   BOOL autoresizesSubviews = [contentView autoresizesSubviews];
59   if (autoresizesSubviews) {
60     [contentView setAutoresizesSubviews:NO];
61   }
63   NSRect frame = [view frame];
64   frame.size.width += widthChange;
65   [view setFrame:frame];
67   if (autoresizesSubviews) {
68     [contentView setAutoresizesSubviews:YES];
69   }
72 }  // namespace
74 class DownloadShelfContextMenuMac : public DownloadShelfContextMenu {
75  public:
76   DownloadShelfContextMenuMac(DownloadItem* downloadItem,
77                               content::PageNavigator* navigator)
78       : DownloadShelfContextMenu(downloadItem) { }
80   // DownloadShelfContextMenu::GetMenuModel is protected.
81   using DownloadShelfContextMenu::GetMenuModel;
84 @interface DownloadItemController (Private)
85 - (void)themeDidChangeNotification:(NSNotification*)aNotification;
86 - (void)updateTheme:(ui::ThemeProvider*)themeProvider;
87 - (void)setState:(DownloadItemState)state;
88 - (void)initExperienceSamplingEvent:(const char*)event;
89 - (void)updateExperienceSamplingEvent:(const char*)event;
90 @end
92 // Implementation of DownloadItemController
94 @implementation DownloadItemController
96 - (id)initWithDownload:(DownloadItem*)downloadItem
97                  shelf:(DownloadShelfController*)shelf
98              navigator:(content::PageNavigator*)navigator {
99   if ((self = [super initWithNibName:@"DownloadItem"
100                               bundle:base::mac::FrameworkBundle()])) {
101     // Must be called before [self view], so that bridge_ is set in awakeFromNib
102     bridge_.reset(new DownloadItemMac(downloadItem, self));
103     menuBridge_.reset(new DownloadShelfContextMenuMac(downloadItem, navigator));
105     NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
106     [defaultCenter addObserver:self
107                       selector:@selector(themeDidChangeNotification:)
108                           name:kBrowserThemeDidChangeNotification
109                         object:nil];
111     shelf_ = shelf;
112     state_ = kNormal;
113     creationTime_ = base::Time::Now();
114     font_list_.reset(new gfx::FontList(
115         ui::ResourceBundle::GetSharedInstance().GetFontList(
116             ui::ResourceBundle::BaseFont)));
117   }
118   return self;
121 - (void)dealloc {
122   [self updateExperienceSamplingEvent:ExperienceSamplingEvent::kIgnore];
123   [[NSNotificationCenter defaultCenter] removeObserver:self];
124   [progressView_ setController:nil];
125   [[self view] removeFromSuperview];
126   [super dealloc];
129 - (void)awakeFromNib {
130   [progressView_ setController:self];
132   GTMUILocalizerAndLayoutTweaker* localizerAndLayoutTweaker =
133       [[[GTMUILocalizerAndLayoutTweaker alloc] init] autorelease];
134   [localizerAndLayoutTweaker applyLocalizer:localizer_ tweakingUI:[self view]];
136   [self setStateFromDownload:bridge_->download_model()];
138   bridge_->LoadIcon();
139   [self updateToolTip];
142 - (void)showDangerousWarning:(DownloadItemModel*)downloadModel {
143   // The transition from safe -> dangerous should only happen once. The code
144   // assumes that the danger type of the download doesn't change once it's set.
145   if ([self isDangerousMode])
146     return;
148   [self setState:kDangerous];
150   // ExperienceSampling: Dangerous or malicious download warning is being shown
151   // to the user, so we start a new SamplingEvent and track it.
152   const char* event_name = downloadModel->MightBeMalicious()
153                                ? ExperienceSamplingEvent::kMaliciousDownload
154                                : ExperienceSamplingEvent::kDangerousDownload;
155   [self updateExperienceSamplingEvent:event_name];
157   ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
158   NSImage* alertIcon;
160   NSString* dangerousWarning = base::SysUTF16ToNSString(
161       downloadModel->GetWarningText(*font_list_, kTextWidth));
162   DCHECK(dangerousWarning);
163   [dangerousDownloadLabel_ setStringValue:dangerousWarning];
164   CGFloat labelWidthChange =
165       [GTMUILocalizerAndLayoutTweaker
166         sizeToFitFixedHeightTextField:dangerousDownloadLabel_
167                              minWidth:kTextWidth];
168   CGFloat buttonWidthChange = 0.0;
170   if (downloadModel->MightBeMalicious()) {
171     alertIcon = rb.GetNativeImageNamed(IDR_SAFEBROWSING_WARNING).ToNSImage();
172     buttonWidthChange = [maliciousButtonTweaker_ changedWidth];
174     // Move the buttons to account for the change in label size.
175     NSPoint frameOrigin = [maliciousButtonTweaker_ frame].origin;
176     frameOrigin.x += labelWidthChange;
177     [maliciousButtonTweaker_ setFrameOrigin:frameOrigin];
179     [dangerousButtonTweaker_ setHidden:YES];
180     [maliciousButtonTweaker_ setHidden:NO];
181   } else {
182     alertIcon = rb.GetNativeImageNamed(IDR_WARNING).ToNSImage();
183     buttonWidthChange = [dangerousButtonTweaker_ changedWidth];
185     // The text on the confirm button can change depending on the type of the
186     // download.
187     NSString* confirmButtonTitle =
188         base::SysUTF16ToNSString(downloadModel->GetWarningConfirmButtonText());
189     DCHECK(confirmButtonTitle);
190     [dangerousDownloadConfirmButton_ setTitle:confirmButtonTitle];
192     // Since the text of the confirm button changed, dangerousButtonTweaker
193     // should be resized.
194     NSSize sizeChange =
195         [GTMUILocalizerAndLayoutTweaker sizeToFitView:dangerousButtonTweaker_];
196     buttonWidthChange += sizeChange.width;
198     // Move the button to account for the change in label size.
199     NSPoint frameOrigin = [dangerousButtonTweaker_ frame].origin;
200     frameOrigin.x += labelWidthChange;
201     [dangerousButtonTweaker_ setFrameOrigin:frameOrigin];
203     [dangerousButtonTweaker_ setHidden:NO];
204     [maliciousButtonTweaker_ setHidden:YES];
205   }
206   DCHECK(alertIcon);
207   [image_ setImage:alertIcon];
209   WidenView(dangerousDownloadView_, labelWidthChange + buttonWidthChange);
210   [shelf_ layoutItems];
213 - (void)setStateFromDownload:(DownloadItemModel*)downloadModel {
214   DCHECK_EQ([self download], downloadModel->download());
216   // Handle dangerous downloads.
217   if (downloadModel->IsDangerous()) {
218     [self showDangerousWarning:downloadModel];
219     return;
220   }
222   // Set path to draggable download on completion.
223   if (downloadModel->download()->GetState() == DownloadItem::COMPLETE)
224     [progressView_ setDownload:downloadModel->download()->GetTargetFilePath()];
226   [cell_ setStateFromDownload:downloadModel];
229 - (void)setIcon:(NSImage*)icon {
230   [cell_ setImage:icon];
233 - (void)remove {
234   // We are deleted after this!
235   // If the download is destroyed before DownloadItemController, then we'd end
236   // up here. Reset the bridege_ so  that it can clean up after itself before
237   // the DownloadItemController is deallocd.
238   bridge_.reset();
239   [shelf_ remove:self];
242 - (void)updateVisibility:(id)sender {
243   if ([[self view] window])
244     [self updateTheme:[[[self view] window] themeProvider]];
246   NSView* view = [self view];
247   NSRect containerFrame = [[view superview] frame];
248   [view setHidden:(NSMaxX([view frame]) > NSWidth(containerFrame))];
251 - (void)downloadWasOpened {
252   [shelf_ downloadWasOpened:self];
255 - (IBAction)handleButtonClick:(id)sender {
256   NSEvent* event = [NSApp currentEvent];
257   DownloadItem* download = [self download];
258   if ([event modifierFlags] & NSCommandKeyMask) {
259     // Let cmd-click show the file in Finder, like e.g. in Safari and Spotlight.
260     download->ShowDownloadInShell();
261   } else {
262     download->OpenDownload();
263   }
266 - (NSSize)preferredSize {
267   if (state_ == kNormal)
268     return [progressView_ frame].size;
269   DCHECK_EQ(kDangerous, state_);
270   return [dangerousDownloadView_ frame].size;
273 - (DownloadItem*)download {
274   return bridge_->download_model()->download();
277 - (ui::MenuModel*)contextMenuModel {
278   return menuBridge_->GetMenuModel();
281 - (void)updateToolTip {
282   base::string16 tooltip_text =
283       bridge_->download_model()->GetTooltipText(*font_list_, kToolTipMaxWidth);
284   [progressView_ setToolTip:base::SysUTF16ToNSString(tooltip_text)];
287 - (void)clearDangerousMode {
288   [self setState:kNormal];
289   // The state change hide the dangerouse download view and is now showing the
290   // download progress view.  This means the view is likely to be a different
291   // size, so trigger a shelf layout to fix up spacing.
292   [shelf_ layoutItems];
295 - (BOOL)isDangerousMode {
296   return state_ == kDangerous;
299 - (void)setState:(DownloadItemState)state {
300   if (state_ == state)
301     return;
302   state_ = state;
303   if (state_ == kNormal) {
304     [progressView_ setHidden:NO];
305     [dangerousDownloadView_ setHidden:YES];
306   } else {
307     DCHECK_EQ(kDangerous, state_);
308     [progressView_ setHidden:YES];
309     [dangerousDownloadView_ setHidden:NO];
310   }
311   // NOTE: Do not relayout the shelf, as this could get called during initial
312   // setup of the the item, so the localized text and sizing might not have
313   // happened yet.
316 // Called after a theme change took place, possibly for a different profile.
317 - (void)themeDidChangeNotification:(NSNotification*)notification {
318   [self updateTheme:[[[self view] window] themeProvider]];
321 // Adapt appearance to the current theme. Called after theme changes and before
322 // this is shown for the first time.
323 - (void)updateTheme:(ui::ThemeProvider*)themeProvider {
324   if (!themeProvider)
325     return;
327   NSColor* color = themeProvider->GetNSColor(ThemeProperties::COLOR_TAB_TEXT);
328   [dangerousDownloadLabel_ setTextColor:color];
331 - (void)initExperienceSamplingEvent:(const char*)event {
332   sampling_event_.reset(new ExperienceSamplingEvent(
333       event,
334       bridge_->download_model()->download()->GetURL(),
335       bridge_->download_model()->download()->GetReferrerUrl(),
336       bridge_->download_model()->download()->GetBrowserContext()));
339 - (void)updateExperienceSamplingEvent:(const char*)event {
340   if (sampling_event_.get()) {
341     sampling_event_->CreateUserDecisionEvent(event);
342     sampling_event_.reset(NULL);
343   }
346 - (IBAction)saveDownload:(id)sender {
347   // The user has confirmed a dangerous download.  We record how quickly the
348   // user did this to detect whether we're being clickjacked.
349   UMA_HISTOGRAM_LONG_TIMES("clickjacking.save_download",
350                            base::Time::Now() - creationTime_);
351   // ExperienceSampling: User chose to proceed with dangerous download.
352   [self updateExperienceSamplingEvent:ExperienceSamplingEvent::kProceed];
353   // This will change the state and notify us.
354   bridge_->download_model()->download()->ValidateDangerousDownload();
357 - (IBAction)discardDownload:(id)sender {
358   UMA_HISTOGRAM_LONG_TIMES("clickjacking.discard_download",
359                            base::Time::Now() - creationTime_);
360   DownloadItem* download = bridge_->download_model()->download();
361   download->Remove();
362   // WARNING: we are deleted at this point.  Don't access 'this'.
365 - (IBAction)dismissMaliciousDownload:(id)sender {
366   // ExperienceSampling: User dismissed the dangerous download.
367   [self updateExperienceSamplingEvent:ExperienceSamplingEvent::kDeny];
368   [self remove];
369   // WARNING: we are deleted at this point.
372 - (IBAction)showContextMenu:(id)sender {
373   [progressView_ showContextMenu];
376 @end