blob: 61b0aa391e0e3113dd2583170c2111d51e3c875a [file] [log] [blame]
// Copyright (c) 2010 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "chrome/browser/cocoa/download_item_controller.h"
#include "app/l10n_util_mac.h"
#include "app/resource_bundle.h"
#include "app/text_elider.h"
#include "base/histogram.h"
#include "base/mac_util.h"
#include "base/sys_string_conversions.h"
#include "base/utf_string_conversions.h"
#import "chrome/browser/browser_theme_provider.h"
#import "chrome/browser/cocoa/download_item_button.h"
#import "chrome/browser/cocoa/download_item_cell.h"
#include "chrome/browser/cocoa/download_item_mac.h"
#import "chrome/browser/cocoa/download_shelf_controller.h"
#import "chrome/browser/cocoa/themed_window.h"
#import "chrome/browser/cocoa/ui_localizer.h"
#include "chrome/browser/download/download_item_model.h"
#include "chrome/browser/download/download_shelf.h"
#include "chrome/browser/download/download_util.h"
#include "grit/generated_resources.h"
#include "grit/theme_resources.h"
#include "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h"
namespace {
// NOTE: Mac currently doesn't use this like Windows does. Mac uses this to
// control the min size on the dangerous download text. TVL sent a query off to
// UX to fully spec all the the behaviors of download items and truncations
// rules so all platforms can get inline in the future.
const int kTextWidth = 140; // Pixels
// The maximum number of characters we show in a file name when displaying the
// dangerous download message.
const int kFileNameMaxLength = 20;
// The maximum width in pixels for the file name tooltip.
const int kToolTipMaxWidth = 900;
// Helper to widen a view.
void WidenView(NSView* view, CGFloat widthChange) {
// If it is an NSBox, the autoresize of the contentView is the issue.
NSView* contentView = view;
if ([view isKindOfClass:[NSBox class]]) {
contentView = [(NSBox*)view contentView];
}
BOOL autoresizesSubviews = [contentView autoresizesSubviews];
if (autoresizesSubviews) {
[contentView setAutoresizesSubviews:NO];
}
NSRect frame = [view frame];
frame.size.width += widthChange;
[view setFrame:frame];
if (autoresizesSubviews) {
[contentView setAutoresizesSubviews:YES];
}
}
} // namespace
// A class for the chromium-side part of the download shelf context menu.
class DownloadShelfContextMenuMac : public DownloadShelfContextMenu {
public:
DownloadShelfContextMenuMac(BaseDownloadItemModel* model)
: DownloadShelfContextMenu(model) { }
using DownloadShelfContextMenu::ExecuteCommand;
using DownloadShelfContextMenu::IsCommandIdChecked;
using DownloadShelfContextMenu::IsCommandIdEnabled;
using DownloadShelfContextMenu::SHOW_IN_FOLDER;
using DownloadShelfContextMenu::OPEN_WHEN_COMPLETE;
using DownloadShelfContextMenu::ALWAYS_OPEN_TYPE;
using DownloadShelfContextMenu::CANCEL;
using DownloadShelfContextMenu::TOGGLE_PAUSE;
};
@interface DownloadItemController (Private)
- (void)themeDidChangeNotification:(NSNotification*)aNotification;
- (void)updateTheme:(ThemeProvider*)themeProvider;
- (void)setState:(DownoadItemState)state;
@end
// Implementation of DownloadItemController
@implementation DownloadItemController
- (id)initWithModel:(BaseDownloadItemModel*)downloadModel
shelf:(DownloadShelfController*)shelf {
if ((self = [super initWithNibName:@"DownloadItem"
bundle:mac_util::MainAppBundle()])) {
// Must be called before [self view], so that bridge_ is set in awakeFromNib
bridge_.reset(new DownloadItemMac(downloadModel, self));
menuBridge_.reset(new DownloadShelfContextMenuMac(downloadModel));
NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
[defaultCenter addObserver:self
selector:@selector(themeDidChangeNotification:)
name:kBrowserThemeDidChangeNotification
object:nil];
shelf_ = shelf;
state_ = kNormal;
creationTime_ = base::Time::Now();
}
return self;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
[progressView_ setController:nil];
[[self view] removeFromSuperview];
[super dealloc];
}
- (void)awakeFromNib {
[progressView_ setController:self];
[self setStateFromDownload:bridge_->download_model()];
GTMUILocalizerAndLayoutTweaker* localizerAndLayoutTweaker =
[[[GTMUILocalizerAndLayoutTweaker alloc] init] autorelease];
[localizerAndLayoutTweaker applyLocalizer:localizer_ tweakingUI:[self view]];
// The strings are based on the download item's name, sizing tweaks have to be
// manually done.
DCHECK(buttonTweaker_ != nil);
CGFloat widthChange = [buttonTweaker_ changedWidth];
// If it's a dangerous download, size the two lines so the text/filename
// is always visible.
if ([self isDangerousMode]) {
widthChange +=
[GTMUILocalizerAndLayoutTweaker
sizeToFitFixedHeightTextField:dangerousDownloadLabel_
minWidth:kTextWidth];
}
// Grow the parent views
WidenView([self view], widthChange);
WidenView(dangerousDownloadView_, widthChange);
// Slide the two buttons over.
NSPoint frameOrigin = [buttonTweaker_ frame].origin;
frameOrigin.x += widthChange;
[buttonTweaker_ setFrameOrigin:frameOrigin];
ResourceBundle& rb = ResourceBundle::GetSharedInstance();
NSImage* alertIcon = rb.GetNSImageNamed(IDR_WARNING);
DCHECK(alertIcon);
[image_ setImage:alertIcon];
bridge_->LoadIcon();
[self updateToolTip];
}
- (void)setStateFromDownload:(BaseDownloadItemModel*)downloadModel {
DCHECK_EQ(bridge_->download_model(), downloadModel);
// Handle dangerous downloads.
if (downloadModel->download()->safety_state() == DownloadItem::DANGEROUS) {
[self setState:kDangerous];
NSString* dangerousWarning;
NSString* confirmButtonTitle;
// The dangerous download label and button text are different for an
// extension file.
if (downloadModel->download()->is_extension_install()) {
dangerousWarning = l10n_util::GetNSStringWithFixup(
IDS_PROMPT_DANGEROUS_DOWNLOAD_EXTENSION);
confirmButtonTitle = l10n_util::GetNSStringWithFixup(
IDS_CONTINUE_EXTENSION_DOWNLOAD);
} else {
// This basic fixup copies Windows DownloadItemView::DownloadItemView().
// Extract the file extension (if any).
FilePath filepath(downloadModel->download()->original_name());
FilePath::StringType extension = filepath.Extension();
// Remove leading '.' from the extension
if (extension.length() > 0)
extension = extension.substr(1);
// Elide giant extensions.
if (extension.length() > kFileNameMaxLength / 2) {
std::wstring wide_extension;
ElideString(UTF8ToWide(extension), kFileNameMaxLength / 2,
&wide_extension);
extension = WideToUTF8(wide_extension);
}
// Rebuild the filename.extension.
std::wstring rootname =
UTF8ToWide(filepath.BaseName().RemoveExtension().value());
ElideString(rootname, kFileNameMaxLength - extension.length(), &rootname);
std::string filename = WideToUTF8(rootname);
if (extension.length())
filename += std::string(".") + extension;
dangerousWarning = l10n_util::GetNSStringFWithFixup(
IDS_PROMPT_DANGEROUS_DOWNLOAD, UTF8ToUTF16(filename));
confirmButtonTitle = l10n_util::GetNSStringWithFixup(IDS_SAVE_DOWNLOAD);
}
[dangerousDownloadLabel_ setStringValue:dangerousWarning];
[dangerousDownloadConfirmButton_ setTitle:confirmButtonTitle];
return;
}
// Set correct popup menu. Also, set draggable download on completion.
if (downloadModel->download()->state() == DownloadItem::COMPLETE) {
[progressView_ setMenu:completeDownloadMenu_];
[progressView_ setDownload:downloadModel->download()->full_path()];
} else {
[progressView_ setMenu:activeDownloadMenu_];
}
[cell_ setStateFromDownload:downloadModel];
}
- (void)setIcon:(NSImage*)icon {
[cell_ setImage:icon];
}
- (void)remove {
// We are deleted after this!
[shelf_ remove:self];
}
- (void)updateVisibility:(id)sender {
if ([[self view] window])
[self updateTheme:[[[self view] window] themeProvider]];
// TODO(thakis): Make this prettier, by fading the items out or overlaying
// the partial visible one with a horizontal alpha gradient -- crbug.com/17830
NSView* view = [self view];
NSRect containerFrame = [[view superview] frame];
[view setHidden:(NSMaxX([view frame]) > NSWidth(containerFrame))];
}
- (IBAction)handleButtonClick:(id)sender {
DownloadItem* download = bridge_->download_model()->download();
if (download->state() == DownloadItem::IN_PROGRESS)
download->set_open_when_complete(!download->open_when_complete());
else if (download->state() == DownloadItem::COMPLETE)
download_util::OpenDownload(download);
}
- (NSSize)preferredSize {
if (state_ == kNormal)
return [progressView_ frame].size;
DCHECK_EQ(kDangerous, state_);
return [dangerousDownloadView_ frame].size;
}
- (DownloadItem*)download {
return bridge_->download_model()->download();
}
- (void)updateToolTip {
std::wstring elidedFilename = gfx::ElideFilename(
[self download]->GetFileName(),
gfx::Font(), kToolTipMaxWidth);
[progressView_ setToolTip:base::SysWideToNSString(elidedFilename)];
}
- (void)clearDangerousMode {
[self setState:kNormal];
// The state change hide the dangerouse download view and is now showing the
// download progress view. This means the view is likely to be a different
// size, so trigger a shelf layout to fix up spacing.
[shelf_ layoutItems];
}
- (BOOL)isDangerousMode {
return state_ == kDangerous;
}
- (void)setState:(DownoadItemState)state {
if (state_ == state)
return;
state_ = state;
if (state_ == kNormal) {
[progressView_ setHidden:NO];
[dangerousDownloadView_ setHidden:YES];
} else {
DCHECK_EQ(kDangerous, state_);
[progressView_ setHidden:YES];
[dangerousDownloadView_ setHidden:NO];
}
// NOTE: Do not relayout the shelf, as this could get called during initial
// setup of the the item, so the localized text and sizing might not have
// happened yet.
}
// Called after the current theme has changed.
- (void)themeDidChangeNotification:(NSNotification*)aNotification {
ThemeProvider* themeProvider =
static_cast<ThemeProvider*>([[aNotification object] pointerValue]);
[self updateTheme:themeProvider];
}
// Adapt appearance to the current theme. Called after theme changes and before
// this is shown for the first time.
- (void)updateTheme:(ThemeProvider*)themeProvider {
NSColor* color =
themeProvider->GetNSColor(BrowserThemeProvider::COLOR_TAB_TEXT, true);
[dangerousDownloadLabel_ setTextColor:color];
}
- (IBAction)saveDownload:(id)sender {
// The user has confirmed a dangerous download. We record how quickly the
// user did this to detect whether we're being clickjacked.
UMA_HISTOGRAM_LONG_TIMES("clickjacking.save_download",
base::Time::Now() - creationTime_);
// This will change the state and notify us.
bridge_->download_model()->download()->manager()->DangerousDownloadValidated(
bridge_->download_model()->download());
}
- (IBAction)discardDownload:(id)sender {
UMA_HISTOGRAM_LONG_TIMES("clickjacking.discard_download",
base::Time::Now() - creationTime_);
if (bridge_->download_model()->download()->state() ==
DownloadItem::IN_PROGRESS)
bridge_->download_model()->download()->Cancel(true);
bridge_->download_model()->download()->Remove(true);
// WARNING: we are deleted at this point. Don't access 'this'.
}
// Sets the enabled and checked state of a particular menu item for this
// download. We translate the NSMenuItem selection to menu selections understood
// by the non platform specific download context menu.
- (BOOL)validateMenuItem:(NSMenuItem *)item {
SEL action = [item action];
int actionId = 0;
if (action == @selector(handleOpen:)) {
actionId = DownloadShelfContextMenuMac::OPEN_WHEN_COMPLETE;
} else if (action == @selector(handleAlwaysOpen:)) {
actionId = DownloadShelfContextMenuMac::ALWAYS_OPEN_TYPE;
} else if (action == @selector(handleReveal:)) {
actionId = DownloadShelfContextMenuMac::SHOW_IN_FOLDER;
} else if (action == @selector(handleCancel:)) {
actionId = DownloadShelfContextMenuMac::CANCEL;
} else if (action == @selector(handleTogglePause:)) {
actionId = DownloadShelfContextMenuMac::TOGGLE_PAUSE;
} else {
NOTREACHED();
return YES;
}
if (menuBridge_->IsCommandIdChecked(actionId))
[item setState:NSOnState];
else
[item setState:NSOffState];
return menuBridge_->IsCommandIdEnabled(actionId) ? YES : NO;
}
- (IBAction)handleOpen:(id)sender {
menuBridge_->ExecuteCommand(
DownloadShelfContextMenuMac::OPEN_WHEN_COMPLETE);
}
- (IBAction)handleAlwaysOpen:(id)sender {
menuBridge_->ExecuteCommand(
DownloadShelfContextMenuMac::ALWAYS_OPEN_TYPE);
}
- (IBAction)handleReveal:(id)sender {
menuBridge_->ExecuteCommand(DownloadShelfContextMenuMac::SHOW_IN_FOLDER);
}
- (IBAction)handleCancel:(id)sender {
menuBridge_->ExecuteCommand(DownloadShelfContextMenuMac::CANCEL);
}
- (IBAction)handleTogglePause:(id)sender {
if([sender state] == NSOnState) {
[sender setTitle:l10n_util::GetNSStringWithFixup(
IDS_DOWNLOAD_MENU_PAUSE_ITEM)];
} else {
[sender setTitle:l10n_util::GetNSStringWithFixup(
IDS_DOWNLOAD_MENU_RESUME_ITEM)];
}
menuBridge_->ExecuteCommand(DownloadShelfContextMenuMac::TOGGLE_PAUSE);
}
@end