1 // Copyright (c) 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/mac/mac_util.h"
9 #include "base/metrics/histogram.h"
10 #include "base/strings/string16.h"
11 #include "base/strings/string_util.h"
12 #include "base/strings/sys_string_conversions.h"
13 #include "base/strings/utf_string_conversions.h"
14 #include "chrome/browser/download/chrome_download_manager_delegate.h"
15 #include "chrome/browser/download/download_item_model.h"
16 #include "chrome/browser/download/download_shelf_context_menu.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/generated_resources.h"
29 #include "grit/theme_resources.h"
30 #include "third_party/google_toolbox_for_mac/src/AppKit/GTMUILocalizerAndLayoutTweaker.h"
31 #include "ui/base/l10n/l10n_util_mac.h"
32 #include "ui/base/resource/resource_bundle.h"
33 #include "ui/gfx/text_elider.h"
34 #include "ui/gfx/font.h"
35 #include "ui/gfx/image/image.h"
37 using content::DownloadItem;
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];
58 BOOL autoresizesSubviews = [contentView autoresizesSubviews];
59 if (autoresizesSubviews) {
60 [contentView setAutoresizesSubviews:NO];
63 NSRect frame = [view frame];
64 frame.size.width += widthChange;
65 [view setFrame:frame];
67 if (autoresizesSubviews) {
68 [contentView setAutoresizesSubviews:YES];
74 class DownloadShelfContextMenuMac : public DownloadShelfContextMenu {
76 DownloadShelfContextMenuMac(DownloadItem* downloadItem,
77 content::PageNavigator* navigator)
78 : DownloadShelfContextMenu(downloadItem, navigator) { }
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:(DownoadItemState)state;
90 // Implementation of DownloadItemController
92 @implementation DownloadItemController
94 - (id)initWithDownload:(DownloadItem*)downloadItem
95 shelf:(DownloadShelfController*)shelf
96 navigator:(content::PageNavigator*)navigator {
97 if ((self = [super initWithNibName:@"DownloadItem"
98 bundle:base::mac::FrameworkBundle()])) {
99 // Must be called before [self view], so that bridge_ is set in awakeFromNib
100 bridge_.reset(new DownloadItemMac(downloadItem, self));
101 menuBridge_.reset(new DownloadShelfContextMenuMac(downloadItem, navigator));
103 NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
104 [defaultCenter addObserver:self
105 selector:@selector(themeDidChangeNotification:)
106 name:kBrowserThemeDidChangeNotification
111 creationTime_ = base::Time::Now();
112 font_list_.reset(new gfx::FontList(
113 ui::ResourceBundle::GetSharedInstance().GetFontList(
114 ui::ResourceBundle::BaseFont)));
120 [[NSNotificationCenter defaultCenter] removeObserver:self];
121 [progressView_ setController:nil];
122 [[self view] removeFromSuperview];
126 - (void)awakeFromNib {
127 [progressView_ setController:self];
129 GTMUILocalizerAndLayoutTweaker* localizerAndLayoutTweaker =
130 [[[GTMUILocalizerAndLayoutTweaker alloc] init] autorelease];
131 [localizerAndLayoutTweaker applyLocalizer:localizer_ tweakingUI:[self view]];
133 [self setStateFromDownload:bridge_->download_model()];
136 [self updateToolTip];
139 - (void)showDangerousWarning:(DownloadItemModel*)downloadModel {
140 // The transition from safe -> dangerous should only happen once. The code
141 // assumes that the danger type of the download doesn't change once it's set.
142 if ([self isDangerousMode])
145 [self setState:kDangerous];
147 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
150 NSString* dangerousWarning = base::SysUTF16ToNSString(
151 downloadModel->GetWarningText(*font_list_, kTextWidth));
152 DCHECK(dangerousWarning);
153 [dangerousDownloadLabel_ setStringValue:dangerousWarning];
154 CGFloat labelWidthChange =
155 [GTMUILocalizerAndLayoutTweaker
156 sizeToFitFixedHeightTextField:dangerousDownloadLabel_
157 minWidth:kTextWidth];
158 CGFloat buttonWidthChange = 0.0;
160 if (downloadModel->MightBeMalicious()) {
161 alertIcon = rb.GetNativeImageNamed(IDR_SAFEBROWSING_WARNING).ToNSImage();
162 buttonWidthChange = [maliciousButtonTweaker_ changedWidth];
164 // Move the buttons to account for the change in label size.
165 NSPoint frameOrigin = [maliciousButtonTweaker_ frame].origin;
166 frameOrigin.x += labelWidthChange;
167 [maliciousButtonTweaker_ setFrameOrigin:frameOrigin];
169 [dangerousButtonTweaker_ setHidden:YES];
170 [maliciousButtonTweaker_ setHidden:NO];
172 alertIcon = rb.GetNativeImageNamed(IDR_WARNING).ToNSImage();
173 buttonWidthChange = [dangerousButtonTweaker_ changedWidth];
175 // The text on the confirm button can change depending on the type of the
177 NSString* confirmButtonTitle =
178 base::SysUTF16ToNSString(downloadModel->GetWarningConfirmButtonText());
179 DCHECK(confirmButtonTitle);
180 [dangerousDownloadConfirmButton_ setTitle:confirmButtonTitle];
182 // Since the text of the confirm button changed, dangerousButtonTweaker
183 // should be resized.
185 [GTMUILocalizerAndLayoutTweaker sizeToFitView:dangerousButtonTweaker_];
186 buttonWidthChange += sizeChange.width;
188 // Move the button to account for the change in label size.
189 NSPoint frameOrigin = [dangerousButtonTweaker_ frame].origin;
190 frameOrigin.x += labelWidthChange;
191 [dangerousButtonTweaker_ setFrameOrigin:frameOrigin];
193 [dangerousButtonTweaker_ setHidden:NO];
194 [maliciousButtonTweaker_ setHidden:YES];
197 [image_ setImage:alertIcon];
199 // Grow the parent views
200 WidenView([self view], labelWidthChange + buttonWidthChange);
201 WidenView(dangerousDownloadView_, labelWidthChange + buttonWidthChange);
204 - (void)setStateFromDownload:(DownloadItemModel*)downloadModel {
205 DCHECK_EQ([self download], downloadModel->download());
207 // Handle dangerous downloads.
208 if (downloadModel->IsDangerous()) {
209 [self showDangerousWarning:downloadModel];
213 // Set path to draggable download on completion.
214 if (downloadModel->download()->GetState() == DownloadItem::COMPLETE)
215 [progressView_ setDownload:downloadModel->download()->GetTargetFilePath()];
217 [cell_ setStateFromDownload:downloadModel];
220 - (void)setIcon:(NSImage*)icon {
221 [cell_ setImage:icon];
225 // We are deleted after this!
226 [shelf_ remove:self];
229 - (void)updateVisibility:(id)sender {
230 if ([[self view] window])
231 [self updateTheme:[[[self view] window] themeProvider]];
233 NSView* view = [self view];
234 NSRect containerFrame = [[view superview] frame];
235 [view setHidden:(NSMaxX([view frame]) > NSWidth(containerFrame))];
238 - (void)downloadWasOpened {
239 [shelf_ downloadWasOpened:self];
242 - (IBAction)handleButtonClick:(id)sender {
243 NSEvent* event = [NSApp currentEvent];
244 DownloadItem* download = [self download];
245 if ([event modifierFlags] & NSCommandKeyMask) {
246 // Let cmd-click show the file in Finder, like e.g. in Safari and Spotlight.
247 download->ShowDownloadInShell();
249 download->OpenDownload();
253 - (NSSize)preferredSize {
254 if (state_ == kNormal)
255 return [progressView_ frame].size;
256 DCHECK_EQ(kDangerous, state_);
257 return [dangerousDownloadView_ frame].size;
260 - (DownloadItem*)download {
261 return bridge_->download_model()->download();
264 - (ui::MenuModel*)contextMenuModel {
265 return menuBridge_->GetMenuModel();
268 - (void)updateToolTip {
269 base::string16 tooltip_text =
270 bridge_->download_model()->GetTooltipText(*font_list_, kToolTipMaxWidth);
271 [progressView_ setToolTip:base::SysUTF16ToNSString(tooltip_text)];
274 - (void)clearDangerousMode {
275 [self setState:kNormal];
276 // The state change hide the dangerouse download view and is now showing the
277 // download progress view. This means the view is likely to be a different
278 // size, so trigger a shelf layout to fix up spacing.
279 [shelf_ layoutItems];
282 - (BOOL)isDangerousMode {
283 return state_ == kDangerous;
286 - (void)setState:(DownoadItemState)state {
290 if (state_ == kNormal) {
291 [progressView_ setHidden:NO];
292 [dangerousDownloadView_ setHidden:YES];
294 DCHECK_EQ(kDangerous, state_);
295 [progressView_ setHidden:YES];
296 [dangerousDownloadView_ setHidden:NO];
298 // NOTE: Do not relayout the shelf, as this could get called during initial
299 // setup of the the item, so the localized text and sizing might not have
303 // Called after a theme change took place, possibly for a different profile.
304 - (void)themeDidChangeNotification:(NSNotification*)notification {
305 [self updateTheme:[[[self view] window] themeProvider]];
308 // Adapt appearance to the current theme. Called after theme changes and before
309 // this is shown for the first time.
310 - (void)updateTheme:(ui::ThemeProvider*)themeProvider {
314 NSColor* color = themeProvider->GetNSColor(ThemeProperties::COLOR_TAB_TEXT);
315 [dangerousDownloadLabel_ setTextColor:color];
318 - (IBAction)saveDownload:(id)sender {
319 // The user has confirmed a dangerous download. We record how quickly the
320 // user did this to detect whether we're being clickjacked.
321 UMA_HISTOGRAM_LONG_TIMES("clickjacking.save_download",
322 base::Time::Now() - creationTime_);
323 // This will change the state and notify us.
324 bridge_->download_model()->download()->ValidateDangerousDownload();
327 - (IBAction)discardDownload:(id)sender {
328 UMA_HISTOGRAM_LONG_TIMES("clickjacking.discard_download",
329 base::Time::Now() - creationTime_);
330 DownloadItem* download = bridge_->download_model()->download();
332 // WARNING: we are deleted at this point. Don't access 'this'.
335 - (IBAction)dismissMaliciousDownload:(id)sender {
337 // WARNING: we are deleted at this point.
340 - (IBAction)showContextMenu:(id)sender {
341 base::scoped_nsobject<DownloadShelfContextMenuController> menuController(
342 [[DownloadShelfContextMenuController alloc]
343 initWithItemController:self
345 [NSMenu popUpContextMenu:[menuController menu]
346 withEvent:[NSApp currentEvent]
347 forView:[self view]];