// Copyright (c) 2011 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.
#include "chrome/browser/ui/views/download/download_item_view.h"
#include <vector>
#include "base/callback.h"
#include "base/file_path.h"
#include "base/i18n/break_iterator.h"
#include "base/i18n/rtl.h"
#include "base/metrics/histogram.h"
#include "base/string_util.h"
#include "base/sys_string_conversions.h"
#include "base/utf_string_conversions.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/download/download_item_model.h"
#include "chrome/browser/download/download_util.h"
#include "chrome/browser/themes/theme_service.h"
#include "chrome/browser/ui/views/download/download_shelf_view.h"
#include "grit/generated_resources.h"
#include "grit/theme_resources.h"
#include "ui/base/accessibility/accessible_view_state.h"
#include "ui/base/animation/slide_animation.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/base/text/text_elider.h"
#include "ui/gfx/canvas_skia.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/image.h"
#include "unicode/uchar.h"
#include "views/controls/button/native_button.h"
#include "views/controls/menu/menu_2.h"
#include "views/widget/root_view.h"
#include "views/widget/widget.h"
using base::TimeDelta;
// TODO(paulg): These may need to be adjusted when download progress
// animation is added, and also possibly to take into account
// different screen resolutions.
static const int kTextWidth = 140; // Pixels
static const int kDangerousTextWidth = 200; // Pixels
static const int kHorizontalTextPadding = 2; // Pixels
static const int kVerticalPadding = 3; // Pixels
static const int kVerticalTextSpacer = 2; // Pixels
static const int kVerticalTextPadding = 2; // Pixels
// The maximum number of characters we show in a file name when displaying the
// dangerous download message.
static const int kFileNameMaxLength = 20;
// We add some padding before the left image so that the progress animation icon
// hides the corners of the left image.
static const int kLeftPadding = 0; // Pixels.
// The space between the Save and Discard buttons when prompting for a dangerous
// download.
static const int kButtonPadding = 5; // Pixels.
// The space on the left and right side of the dangerous download label.
static const int kLabelPadding = 4; // Pixels.
static const SkColor kFileNameDisabledColor = SkColorSetRGB(171, 192, 212);
// How long the 'download complete' animation should last for.
static const int kCompleteAnimationDurationMs = 2500;
// How long the 'download interrupted' animation should last for.
static const int kInterruptedAnimationDurationMs = 2500;
// How long we keep the item disabled after the user clicked it to open the
// downloaded item.
static const int kDisabledOnOpenDuration = 3000;
// Darken light-on-dark download status text by 20% before drawing, thus
// creating a "muted" version of title text for both dark-on-light and
// light-on-dark themes.
static const double kDownloadItemLuminanceMod = 0.8;
// DownloadShelfContextMenuWin -------------------------------------------------
class DownloadShelfContextMenuWin : public DownloadShelfContextMenu {
public:
explicit DownloadShelfContextMenuWin(BaseDownloadItemModel* model)
: DownloadShelfContextMenu(model) {
DCHECK(model);
}
void Run(const gfx::Point& point) {
if (download_->IsComplete())
menu_.reset(new views::Menu2(GetFinishedMenuModel()));
else
menu_.reset(new views::Menu2(GetInProgressMenuModel()));
// The menu's alignment is determined based on the UI layout.
views::Menu2::Alignment alignment;
if (base::i18n::IsRTL())
alignment = views::Menu2::ALIGN_TOPRIGHT;
else
alignment = views::Menu2::ALIGN_TOPLEFT;
menu_->RunMenuAt(point, alignment);
}
// This method runs when the caller has been deleted and we should not attempt
// to access |download_|.
void Stop() {
download_ = NULL;
}
private:
scoped_ptr<views::Menu2> menu_;
};
// DownloadItemView ------------------------------------------------------------
DownloadItemView::DownloadItemView(DownloadItem* download,
DownloadShelfView* parent,
BaseDownloadItemModel* model)
: warning_icon_(NULL),
download_(download),
parent_(parent),
status_text_(UTF16ToWide(
l10n_util::GetStringUTF16(IDS_DOWNLOAD_STATUS_STARTING))),
show_status_text_(true),
body_state_(NORMAL),
drop_down_state_(NORMAL),
progress_angle_(download_util::kStartAngleDegrees),
drop_down_pressed_(false),
dragging_(false),
starting_drag_(false),
model_(model),
save_button_(NULL),
discard_button_(NULL),
dangerous_download_label_(NULL),
dangerous_download_label_sized_(false),
disabled_while_opening_(false),
creation_time_(base::Time::Now()),
ALLOW_THIS_IN_INITIALIZER_LIST(reenable_method_factory_(this)),
deleted_(NULL) {
DCHECK(download_);
download_->AddObserver(this);
ResourceBundle &rb = ResourceBundle::GetSharedInstance();
BodyImageSet normal_body_image_set = {
rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_LEFT_TOP),
rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_LEFT_MIDDLE),
rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_LEFT_BOTTOM),
rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_CENTER_TOP),
rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_CENTER_MIDDLE),
rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_CENTER_BOTTOM),
rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_RIGHT_TOP),
rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_RIGHT_MIDDLE),
rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_RIGHT_BOTTOM)
};
normal_body_image_set_ = normal_body_image_set;
DropDownImageSet normal_drop_down_image_set = {
rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_MENU_TOP),
rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_MENU_MIDDLE),
rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_MENU_BOTTOM)
};
normal_drop_down_image_set_ = normal_drop_down_image_set;
BodyImageSet hot_body_image_set = {
rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_LEFT_TOP_H),
rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_LEFT_MIDDLE_H),
rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_LEFT_BOTTOM_H),
rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_CENTER_TOP_H),
rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_CENTER_MIDDLE_H),
rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_CENTER_BOTTOM_H),
rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_RIGHT_TOP_H),
rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_RIGHT_MIDDLE_H),
rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_RIGHT_BOTTOM_H)
};
hot_body_image_set_ = hot_body_image_set;
DropDownImageSet hot_drop_down_image_set = {
rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_MENU_TOP_H),
rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_MENU_MIDDLE_H),
rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_MENU_BOTTOM_H)
};
hot_drop_down_image_set_ = hot_drop_down_image_set;
BodyImageSet pushed_body_image_set = {
rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_LEFT_TOP_P),
rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_LEFT_MIDDLE_P),
rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_LEFT_BOTTOM_P),
rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_CENTER_TOP_P),
rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_CENTER_MIDDLE_P),
rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_CENTER_BOTTOM_P),
rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_RIGHT_TOP_P),
rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_RIGHT_MIDDLE_P),
rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_RIGHT_BOTTOM_P)
};
pushed_body_image_set_ = pushed_body_image_set;
DropDownImageSet pushed_drop_down_image_set = {
rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_MENU_TOP_P),
rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_MENU_MIDDLE_P),
rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_MENU_BOTTOM_P)
};
pushed_drop_down_image_set_ = pushed_drop_down_image_set;
BodyImageSet dangerous_mode_body_image_set = {
rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_LEFT_TOP),
rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_LEFT_MIDDLE),
rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_LEFT_BOTTOM),
rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_CENTER_TOP),
rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_CENTER_MIDDLE),
rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_CENTER_BOTTOM),
rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_RIGHT_TOP_NO_DD),
rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_RIGHT_MIDDLE_NO_DD),
rb.GetBitmapNamed(IDR_DOWNLOAD_BUTTON_RIGHT_BOTTOM_NO_DD)
};
dangerous_mode_body_image_set_ = dangerous_mode_body_image_set;
LoadIcon();
tooltip_text_ =
UTF16ToWide(download_->GetFileNameToReportUser().LossyDisplayName());
font_ = ResourceBundle::GetSharedInstance().GetFont(ResourceBundle::BaseFont);
box_height_ = std::max<int>(2 * kVerticalPadding + font_.GetHeight() +
kVerticalTextPadding + font_.GetHeight(),
2 * kVerticalPadding +
normal_body_image_set_.top_left->height() +
normal_body_image_set_.bottom_left->height());
if (download_util::kSmallProgressIconSize > box_height_)
box_y_ = (download_util::kSmallProgressIconSize - box_height_) / 2;
else
box_y_ = kVerticalPadding;
gfx::Size size = GetPreferredSize();
if (base::i18n::IsRTL()) {
// Drop down button is glued to the left of the download shelf.
drop_down_x_left_ = 0;
drop_down_x_right_ = normal_drop_down_image_set_.top->width();
} else {
// Drop down button is glued to the right of the download shelf.
drop_down_x_left_ =
size.width() - normal_drop_down_image_set_.top->width();
drop_down_x_right_ = size.width();
}
body_hover_animation_.reset(new ui::SlideAnimation(this));
drop_hover_animation_.reset(new ui::SlideAnimation(this));
if (download->safety_state() == DownloadItem::DANGEROUS) {
tooltip_text_.clear();
body_state_ = DANGEROUS;
drop_down_state_ = DANGEROUS;
save_button_ = new views::NativeButton(this,
UTF16ToWide(l10n_util::GetStringUTF16(
download->is_extension_install() ?
IDS_CONTINUE_EXTENSION_DOWNLOAD : IDS_SAVE_DOWNLOAD)));
save_button_->set_ignore_minimum_size(true);
discard_button_ = new views::NativeButton(
this, UTF16ToWide(l10n_util::GetStringUTF16(IDS_DISCARD_DOWNLOAD)));
discard_button_->set_ignore_minimum_size(true);
AddChildView(save_button_);
AddChildView(discard_button_);
// Ensure the file name is not too long.
// Extract the file extension (if any).
FilePath filename(download->target_name());
#if defined(OS_LINUX)
string16 extension = WideToUTF16(base::SysNativeMBToWide(
filename.Extension()));
#else
string16 extension = filename.Extension();
#endif
// Remove leading '.'
if (extension.length() > 0)
extension = extension.substr(1);
#if defined(OS_LINUX)
string16 rootname = WideToUTF16(base::SysNativeMBToWide(
filename.RemoveExtension().value()));
#else
string16 rootname = filename.RemoveExtension().value();
#endif
// Elide giant extensions (this shouldn't currently be hit, but might
// in future, should we ever notice unsafe giant extensions).
if (extension.length() > kFileNameMaxLength / 2)
ui::ElideString(extension, kFileNameMaxLength / 2, &extension);
// The dangerous download label text and icon are different
// under different cases.
string16 dangerous_label;
if (download->danger_type() == DownloadItem::DANGEROUS_URL) {
// Safebrowsing shows the download URL leads to malicious file.
warning_icon_ = rb.GetBitmapNamed(IDR_SAFEBROWSING_WARNING);
dangerous_label =
l10n_util::GetStringUTF16(IDS_PROMPT_UNSAFE_DOWNLOAD_URL);
} else {
// The download file has dangerous file type (e.g.: an executable).
DCHECK(download->danger_type() == DownloadItem::DANGEROUS_FILE);
warning_icon_ = rb.GetBitmapNamed(IDR_WARNING);
if (download->is_extension_install()) {
dangerous_label =
l10n_util::GetStringUTF16(IDS_PROMPT_DANGEROUS_DOWNLOAD_EXTENSION);
} else {
ui::ElideString(rootname,
kFileNameMaxLength - extension.length(),
&rootname);
string16 filename = rootname + ASCIIToUTF16(".") + extension;
filename = base::i18n::GetDisplayStringInLTRDirectionality(filename);
dangerous_label =
l10n_util::GetStringFUTF16(IDS_PROMPT_DANGEROUS_DOWNLOAD, filename);
}
}
dangerous_download_label_ = new views::Label(UTF16ToWide(dangerous_label));
dangerous_download_label_->SetMultiLine(true);
dangerous_download_label_->SetHorizontalAlignment(
views::Label::ALIGN_LEFT);
AddChildView(dangerous_download_label_);
SizeLabelToMinWidth();
}
UpdateAccessibleName();
set_accessibility_focusable(true);
// Set up our animation.
StartDownloadProgress();
}
DownloadItemView::~DownloadItemView() {
if (context_menu_.get()) {
context_menu_->Stop();
}
icon_consumer_.CancelAllRequests();
StopDownloadProgress();
download_->RemoveObserver(this);
if (deleted_)
*deleted_ = true;
}
// Progress animation handlers.
void DownloadItemView::UpdateDownloadProgress() {
progress_angle_ = (progress_angle_ +
download_util::kUnknownIncrementDegrees) %
download_util::kMaxDegrees;
SchedulePaint();
}
void DownloadItemView::StartDownloadProgress() {
if (progress_timer_.IsRunning())
return;
progress_timer_.Start(
TimeDelta::FromMilliseconds(download_util::kProgressRateMs), this,
&DownloadItemView::UpdateDownloadProgress);
}
void DownloadItemView::StopDownloadProgress() {
progress_timer_.Stop();
}
void DownloadItemView::OnExtractIconComplete(IconManager::Handle handle,
gfx::Image* icon_bitmap) {
if (icon_bitmap)
parent()->SchedulePaint();
}
// DownloadObserver interface.
// Update the progress graphic on the icon and our text status label
// to reflect our current bytes downloaded, time remaining.
void DownloadItemView::OnDownloadUpdated(DownloadItem* download) {
DCHECK(download == download_);
if (body_state_ == DANGEROUS &&
download->safety_state() == DownloadItem::DANGEROUS_BUT_VALIDATED) {
// We have been approved.
ClearDangerousMode();
}
string16 status_text = model_->GetStatusText();
switch (download_->state()) {
case DownloadItem::IN_PROGRESS:
download_->is_paused() ? StopDownloadProgress() : StartDownloadProgress();
break;
case DownloadItem::INTERRUPTED:
StopDownloadProgress();
complete_animation_.reset(new ui::SlideAnimation(this));
complete_animation_->SetSlideDuration(kInterruptedAnimationDurationMs);
complete_animation_->SetTweenType(ui::Tween::LINEAR);
complete_animation_->Show();
if (status_text.empty())
show_status_text_ = false;
SchedulePaint();
LoadIcon();
break;
case DownloadItem::COMPLETE:
if (download_->auto_opened()) {
parent_->RemoveDownloadView(this); // This will delete us!
return;
}
StopDownloadProgress();
complete_animation_.reset(new ui::SlideAnimation(this));
complete_animation_->SetSlideDuration(kCompleteAnimationDurationMs);
complete_animation_->SetTweenType(ui::Tween::LINEAR);
complete_animation_->Show();
if (status_text.empty())
show_status_text_ = false;
SchedulePaint();
LoadIcon();
break;
case DownloadItem::CANCELLED:
StopDownloadProgress();
LoadIcon();
break;
case DownloadItem::REMOVING:
parent_->RemoveDownloadView(this); // This will delete us!
return;
default:
NOTREACHED();
}
status_text_ = UTF16ToWideHack(status_text);
UpdateAccessibleName();
// We use the parent's (DownloadShelfView's) SchedulePaint, since there
// are spaces between each DownloadItemView that the parent is responsible
// for painting.
parent()->SchedulePaint();
}
void DownloadItemView::OnDownloadOpened(DownloadItem* download) {
disabled_while_opening_ = true;
SetEnabled(false);
MessageLoop::current()->PostDelayedTask(
FROM_HERE,
reenable_method_factory_.NewRunnableMethod(&DownloadItemView::Reenable),
kDisabledOnOpenDuration);
// Notify our parent.
parent_->OpenedDownload(this);
}
// View overrides
// In dangerous mode we have to layout our buttons.
void DownloadItemView::Layout() {
if (IsDangerousMode()) {
dangerous_download_label_->SetColor(
GetThemeProvider()->GetColor(ThemeService::COLOR_BOOKMARK_TEXT));
int x = kLeftPadding + dangerous_mode_body_image_set_.top_left->width() +
warning_icon_->width() + kLabelPadding;
int y = (height() - dangerous_download_label_->height()) / 2;
dangerous_download_label_->SetBounds(x, y,
dangerous_download_label_->width(),
dangerous_download_label_->height());
gfx::Size button_size = GetButtonSize();
x += dangerous_download_label_->width() + kLabelPadding;
y = (height() - button_size.height()) / 2;
save_button_->SetBounds(x, y, button_size.width(), button_size.height());
x += button_size.width() + kButtonPadding;
discard_button_->SetBounds(x, y, button_size.width(), button_size.height());
}
}
gfx::Size DownloadItemView::GetPreferredSize() {
int width, height;
// First, we set the height to the height of two rows or text plus margins.
height = 2 * kVerticalPadding + 2 * font_.GetHeight() + kVerticalTextPadding;
// Then we increase the size if the progress icon doesn't fit.
height = std::max<int>(height, download_util::kSmallProgressIconSize);
if (IsDangerousMode()) {
width = kLeftPadding + dangerous_mode_body_image_set_.top_left->width();
width += warning_icon_->width() + kLabelPadding;
width += dangerous_download_label_->width() + kLabelPadding;
gfx::Size button_size = GetButtonSize();
// Make sure the button fits.
height = std::max<int>(height, 2 * kVerticalPadding + button_size.height());
// Then we make sure the warning icon fits.
height = std::max<int>(height, 2 * kVerticalPadding +
warning_icon_->height());
width += button_size.width() * 2 + kButtonPadding;
width += dangerous_mode_body_image_set_.top_right->width();
} else {
width = kLeftPadding + normal_body_image_set_.top_left->width();
width += download_util::kSmallProgressIconSize;
width += kTextWidth;
width += normal_body_image_set_.top_right->width();
width += normal_drop_down_image_set_.top->width();
}
return gfx::Size(width, height);
}
// Handle a mouse click and open the context menu if the mouse is
// over the drop-down region.
bool DownloadItemView::OnMousePressed(const views::MouseEvent& event) {
// Mouse should not activate us in dangerous mode.
if (IsDangerousMode())
return true;
// Stop any completion animation.
if (complete_animation_.get() && complete_animation_->is_animating())
complete_animation_->End();
gfx::Point menu_location(event.location());
if (event.IsOnlyLeftMouseButton()) {
if (!InDropDownButtonXCoordinateRange(event.x())) {
SetState(PUSHED, NORMAL);
return true;
}
// Anchor the menu below the dropmarker.
menu_location.SetPoint(base::i18n::IsRTL() ?
drop_down_x_right_ : drop_down_x_left_,
height());
drop_down_pressed_ = true;
SetState(NORMAL, PUSHED);
}
ShowContextMenu(menu_location, true);
return true;
}
// Handle drag (file copy) operations.
bool DownloadItemView::OnMouseDragged(const views::MouseEvent& event) {
// Mouse should not activate us in dangerous mode.
if (IsDangerousMode())
return true;
if (!starting_drag_) {
starting_drag_ = true;
drag_start_point_ = event.location();
}
if (dragging_) {
if (download_->IsComplete()) {
IconManager* im = g_browser_process->icon_manager();
gfx::Image* icon = im->LookupIcon(download_->GetUserVerifiedFilePath(),
IconLoader::SMALL);
if (icon) {
views::Widget* widget = GetWidget();
download_util::DragDownload(download_, icon,
widget ? widget->GetNativeView() : NULL);
}
}
} else if (ExceededDragThreshold(
event.location().x() - drag_start_point_.x(),
event.location().y() - drag_start_point_.y())) {
dragging_ = true;
}
return true;
}
void DownloadItemView::OnMouseReleased(const views::MouseEvent& event) {
// Mouse should not activate us in dangerous mode.
if (IsDangerousMode())
return;
if (event.IsOnlyLeftMouseButton() &&
!InDropDownButtonXCoordinateRange(event.x())) {
OpenDownload();
}
SetState(NORMAL, NORMAL);
}
void DownloadItemView::OnMouseCaptureLost() {
// Mouse should not activate us in dangerous mode.
if (IsDangerousMode())
return;
if (dragging_) {
// Starting a drag results in a MouseCaptureLost.
dragging_ = false;
starting_drag_ = false;
} else {
SetState(NORMAL, NORMAL);
}
}
void DownloadItemView::OnMouseMoved(const views::MouseEvent& event) {
// Mouse should not activate us in dangerous mode.
if (IsDangerousMode())
return;
bool on_body = !InDropDownButtonXCoordinateRange(event.x());
SetState(on_body ? HOT : NORMAL, on_body ? NORMAL : HOT);
if (on_body) {
body_hover_animation_->Show();
drop_hover_animation_->Hide();
} else {
body_hover_animation_->Hide();
drop_hover_animation_->Show();
}
}
void DownloadItemView::OnMouseExited(const views::MouseEvent& event) {
// Mouse should not activate us in dangerous mode.
if (IsDangerousMode())
return;
SetState(NORMAL, drop_down_pressed_ ? PUSHED : NORMAL);
body_hover_animation_->Hide();
drop_hover_animation_->Hide();
}
bool DownloadItemView::OnKeyPressed(const views::KeyEvent& event) {
// Key press should not activate us in dangerous mode.
if (IsDangerousMode())
return true;
if (event.key_code() == ui::VKEY_SPACE ||
event.key_code() == ui::VKEY_RETURN) {
OpenDownload();
return true;
}
return false;
}
bool DownloadItemView::GetTooltipText(const gfx::Point& p,
std::wstring* tooltip) {
if (tooltip_text_.empty())
return false;
tooltip->assign(tooltip_text_);
return true;
}
void DownloadItemView::ShowContextMenu(const gfx::Point& p,
bool is_mouse_gesture) {
gfx::Point point = p;
// Similar hack as in MenuButton.
// We're about to show the menu from a mouse press. By showing from the
// mouse press event we block RootView in mouse dispatching. This also
// appears to cause RootView to get a mouse pressed BEFORE the mouse
// release is seen, which means RootView sends us another mouse press no
// matter where the user pressed. To force RootView to recalculate the
// mouse target during the mouse press we explicitly set the mouse handler
// to NULL.
GetRootView()->SetMouseHandler(NULL);
// If |is_mouse_gesture| is false, |p| is ignored. The menu is shown aligned
// to drop down arrow button.
if (!is_mouse_gesture) {
drop_down_pressed_ = true;
SetState(NORMAL, PUSHED);
point.set_y(height());
if (base::i18n::IsRTL())
point.set_x(drop_down_x_right_);
else
point.set_x(drop_down_x_left_);
}
views::View::ConvertPointToScreen(this, &point);
if (!context_menu_.get())
context_menu_.reset(new DownloadShelfContextMenuWin(model_.get()));
// When we call the Run method on the menu, it runs an inner message loop
// that might causes us to be deleted.
bool deleted = false;
deleted_ = &deleted;
context_menu_->Run(point);
if (deleted)
return; // We have been deleted! Don't access 'this'.
deleted_ = NULL;
// If the menu action was to remove the download, this view will also be
// invalid so we must not access 'this' in this case.
if (context_menu_->download()) {
drop_down_pressed_ = false;
// Showing the menu blocks. Here we revert the state.
SetState(NORMAL, NORMAL);
}
}
void DownloadItemView::GetAccessibleState(ui::AccessibleViewState* state) {
state->name = accessible_name_;
state->role = ui::AccessibilityTypes::ROLE_PUSHBUTTON;
if (download_->safety_state() == DownloadItem::DANGEROUS) {
state->state = ui::AccessibilityTypes::STATE_UNAVAILABLE;
} else {
state->state = ui::AccessibilityTypes::STATE_HASPOPUP;
}
}
void DownloadItemView::ButtonPressed(
views::Button* sender, const views::Event& event) {
if (sender == discard_button_) {
UMA_HISTOGRAM_LONG_TIMES("clickjacking.discard_download",
base::Time::Now() - creation_time_);
if (download_->IsPartialDownload())
download_->Cancel(true);
download_->Delete(DownloadItem::DELETE_DUE_TO_USER_DISCARD);
// WARNING: we are deleted at this point. Don't access 'this'.
} else if (sender == save_button_) {
// The user has confirmed a dangerous download. We'd record how quickly the
// user did this to detect whether we're being clickjacked.
UMA_HISTOGRAM_LONG_TIMES("clickjacking.save_download",
base::Time::Now() - creation_time_);
// This will change the state and notify us.
download_->DangerousDownloadValidated();
}
}
void DownloadItemView::AnimationProgressed(const ui::Animation* animation) {
// We don't care if what animation (body button/drop button/complete),
// is calling back, as they all have to go through the same paint call.
SchedulePaint();
}
void DownloadItemView::OnPaint(gfx::Canvas* canvas) {
BodyImageSet* body_image_set = NULL;
switch (body_state_) {
case NORMAL:
case HOT:
body_image_set = &normal_body_image_set_;
break;
case PUSHED:
body_image_set = &pushed_body_image_set_;
break;
case DANGEROUS:
body_image_set = &dangerous_mode_body_image_set_;
break;
default:
NOTREACHED();
}
DropDownImageSet* drop_down_image_set = NULL;
switch (drop_down_state_) {
case NORMAL:
case HOT:
drop_down_image_set = &normal_drop_down_image_set_;
break;
case PUSHED:
drop_down_image_set = &pushed_drop_down_image_set_;
break;
case DANGEROUS:
drop_down_image_set = NULL; // No drop-down in dangerous mode.
break;
default:
NOTREACHED();
}
int center_width = width() - kLeftPadding -
body_image_set->left->width() -
body_image_set->right->width() -
(drop_down_image_set ?
normal_drop_down_image_set_.center->width() :
0);
// May be caused by animation.
if (center_width <= 0)
return;
// Draw status before button image to effectively lighten text.
if (!IsDangerousMode()) {
if (show_status_text_) {
int mirrored_x = GetMirroredXWithWidthInView(
download_util::kSmallProgressIconSize, kTextWidth);
// Add font_.height() to compensate for title, which is drawn later.
int y = box_y_ + kVerticalPadding + font_.GetHeight() +
kVerticalTextPadding;
SkColor file_name_color = GetThemeProvider()->GetColor(
ThemeService::COLOR_BOOKMARK_TEXT);
// If text is light-on-dark, lightening it alone will do nothing.
// Therefore we mute luminance a wee bit before drawing in this case.
if (color_utils::RelativeLuminance(file_name_color) > 0.5)
file_name_color = SkColorSetRGB(
static_cast<int>(kDownloadItemLuminanceMod *
SkColorGetR(file_name_color)),
static_cast<int>(kDownloadItemLuminanceMod *
SkColorGetG(file_name_color)),
static_cast<int>(kDownloadItemLuminanceMod *
SkColorGetB(file_name_color)));
canvas->DrawStringInt(WideToUTF16Hack(status_text_), font_,
file_name_color, mirrored_x, y, kTextWidth,
font_.GetHeight());
}
}
// Paint the background images.
int x = kLeftPadding;
canvas->Save();
if (base::i18n::IsRTL()) {
// Since we do not have the mirrored images for
// (hot_)body_image_set->top_left, (hot_)body_image_set->left,
// (hot_)body_image_set->bottom_left, and drop_down_image_set,
// for RTL UI, we flip the canvas to draw those images mirrored.
// Consequently, we do not need to mirror the x-axis of those images.
canvas->TranslateInt(width(), 0);
canvas->ScaleInt(-1, 1);
}
PaintBitmaps(canvas,
body_image_set->top_left, body_image_set->left,
body_image_set->bottom_left,
x, box_y_, box_height_, body_image_set->top_left->width());
x += body_image_set->top_left->width();
PaintBitmaps(canvas,
body_image_set->top, body_image_set->center,
body_image_set->bottom,
x, box_y_, box_height_, center_width);
x += center_width;
PaintBitmaps(canvas,
body_image_set->top_right, body_image_set->right,
body_image_set->bottom_right,
x, box_y_, box_height_, body_image_set->top_right->width());
// Overlay our body hot state.
if (body_hover_animation_->GetCurrentValue() > 0) {
canvas->SaveLayerAlpha(
static_cast<int>(body_hover_animation_->GetCurrentValue() * 255));
canvas->AsCanvasSkia()->drawARGB(0, 255, 255, 255, SkXfermode::kClear_Mode);
int x = kLeftPadding;
PaintBitmaps(canvas,
hot_body_image_set_.top_left, hot_body_image_set_.left,
hot_body_image_set_.bottom_left,
x, box_y_, box_height_, hot_body_image_set_.top_left->width());
x += body_image_set->top_left->width();
PaintBitmaps(canvas,
hot_body_image_set_.top, hot_body_image_set_.center,
hot_body_image_set_.bottom,
x, box_y_, box_height_, center_width);
x += center_width;
PaintBitmaps(canvas,
hot_body_image_set_.top_right, hot_body_image_set_.right,
hot_body_image_set_.bottom_right,
x, box_y_, box_height_,
hot_body_image_set_.top_right->width());
canvas->Restore();
}
x += body_image_set->top_right->width();
// Paint the drop-down.
if (drop_down_image_set) {
PaintBitmaps(canvas,
drop_down_image_set->top, drop_down_image_set->center,
drop_down_image_set->bottom,
x, box_y_, box_height_, drop_down_image_set->top->width());
// Overlay our drop-down hot state.
if (drop_hover_animation_->GetCurrentValue() > 0) {
canvas->SaveLayerAlpha(
static_cast<int>(drop_hover_animation_->GetCurrentValue() * 255));
canvas->AsCanvasSkia()->drawARGB(0, 255, 255, 255,
SkXfermode::kClear_Mode);
PaintBitmaps(canvas,
drop_down_image_set->top, drop_down_image_set->center,
drop_down_image_set->bottom,
x, box_y_, box_height_, drop_down_image_set->top->width());
canvas->Restore();
}
}
// Restore the canvas to avoid file name etc. text are drawn flipped.
// Consequently, the x-axis of following canvas->DrawXXX() method should be
// mirrored so the text and images are down in the right positions.
canvas->Restore();
// Print the text, left aligned and always print the file extension.
// Last value of x was the end of the right image, just before the button.
// Note that in dangerous mode we use a label (as the text is multi-line).
if (!IsDangerousMode()) {
string16 filename;
if (!disabled_while_opening_) {
filename = ui::ElideFilename(download_->GetFileNameToReportUser(),
font_, kTextWidth);
} else {
// First, Calculate the download status opening string width.
string16 status_string =
l10n_util::GetStringFUTF16(IDS_DOWNLOAD_STATUS_OPENING, string16());
int status_string_width = font_.GetStringWidth(status_string);
// Then, elide the file name.
string16 filename_string =
ui::ElideFilename(download_->GetFileNameToReportUser(), font_,
kTextWidth - status_string_width);
// Last, concat the whole string.
filename = l10n_util::GetStringFUTF16(IDS_DOWNLOAD_STATUS_OPENING,
filename_string);
}
int mirrored_x = GetMirroredXWithWidthInView(
download_util::kSmallProgressIconSize, kTextWidth);
SkColor file_name_color = GetThemeProvider()->GetColor(
ThemeService::COLOR_BOOKMARK_TEXT);
int y =
box_y_ + (show_status_text_ ? kVerticalPadding :
(box_height_ - font_.GetHeight()) / 2);
// Draw the file's name.
canvas->DrawStringInt(filename, font_,
IsEnabled() ? file_name_color :
kFileNameDisabledColor,
mirrored_x, y, kTextWidth, font_.GetHeight());
}
// Load the icon.
IconManager* im = g_browser_process->icon_manager();
gfx::Image* image = im->LookupIcon(download_->GetUserVerifiedFilePath(),
IconLoader::SMALL);
const SkBitmap* icon = NULL;
if (IsDangerousMode())
icon = warning_icon_;
else if (image)
icon = *image;
// We count on the fact that the icon manager will cache the icons and if one
// is available, it will be cached here. We *don't* want to request the icon
// to be loaded here, since this will also get called if the icon can't be
// loaded, in which case LookupIcon will always be NULL. The loading will be
// triggered only when we think the status might change.
if (icon) {
if (!IsDangerousMode()) {
if (download_->IsInProgress()) {
download_util::PaintDownloadProgress(canvas, this, 0, 0,
progress_angle_,
download_->PercentComplete(),
download_util::SMALL);
} else if (download_->IsComplete() &&
complete_animation_.get() &&
complete_animation_->is_animating()) {
if (download_->IsInterrupted()) {
download_util::PaintDownloadInterrupted(canvas, this, 0, 0,
complete_animation_->GetCurrentValue(),
download_util::SMALL);
} else {
download_util::PaintDownloadComplete(canvas, this, 0, 0,
complete_animation_->GetCurrentValue(),
download_util::SMALL);
}
}
}
// Draw the icon image.
int mirrored_x = GetMirroredXWithWidthInView(
download_util::kSmallProgressIconOffset, icon->width());
if (IsEnabled()) {
canvas->DrawBitmapInt(*icon, mirrored_x,
download_util::kSmallProgressIconOffset);
} else {
// Use an alpha to make the image look disabled.
SkPaint paint;
paint.setAlpha(120);
canvas->DrawBitmapInt(*icon, mirrored_x,
download_util::kSmallProgressIconOffset, paint);
}
}
}
void DownloadItemView::OpenDownload() {
// We're interested in how long it takes users to open downloads. If they
// open downloads super quickly, we should be concerned about clickjacking.
UMA_HISTOGRAM_LONG_TIMES("clickjacking.open_download",
base::Time::Now() - creation_time_);
download_->OpenDownload();
UpdateAccessibleName();
}
void DownloadItemView::LoadIcon() {
IconManager* im = g_browser_process->icon_manager();
im->LoadIcon(download_->GetUserVerifiedFilePath(),
IconLoader::SMALL, &icon_consumer_,
NewCallback(this, &DownloadItemView::OnExtractIconComplete));
}
// Load an icon for the file type we're downloading, and animate any in progress
// download state.
void DownloadItemView::PaintBitmaps(gfx::Canvas* canvas,
const SkBitmap* top_bitmap,
const SkBitmap* center_bitmap,
const SkBitmap* bottom_bitmap,
int x, int y, int height, int width) {
int middle_height = height - top_bitmap->height() - bottom_bitmap->height();
// Draw the top.
canvas->DrawBitmapInt(*top_bitmap,
0, 0, top_bitmap->width(), top_bitmap->height(),
x, y, width, top_bitmap->height(), false);
y += top_bitmap->height();
// Draw the center.
canvas->DrawBitmapInt(*center_bitmap,
0, 0, center_bitmap->width(), center_bitmap->height(),
x, y, width, middle_height, false);
y += middle_height;
// Draw the bottom.
canvas->DrawBitmapInt(*bottom_bitmap,
0, 0, bottom_bitmap->width(), bottom_bitmap->height(),
x, y, width, bottom_bitmap->height(), false);
}
void DownloadItemView::SetState(State body_state, State drop_down_state) {
if (body_state_ == body_state && drop_down_state_ == drop_down_state)
return;
body_state_ = body_state;
drop_down_state_ = drop_down_state;
SchedulePaint();
}
void DownloadItemView::ClearDangerousMode() {
DCHECK(download_->safety_state() == DownloadItem::DANGEROUS_BUT_VALIDATED &&
body_state_ == DANGEROUS && drop_down_state_ == DANGEROUS);
body_state_ = NORMAL;
drop_down_state_ = NORMAL;
// Remove the views used by the dangerous mode.
RemoveChildView(save_button_);
delete save_button_;
save_button_ = NULL;
RemoveChildView(discard_button_);
delete discard_button_;
discard_button_ = NULL;
RemoveChildView(dangerous_download_label_);
delete dangerous_download_label_;
dangerous_download_label_ = NULL;
// Set the accessible name back to the status and filename instead of the
// download warning.
UpdateAccessibleName();
// We need to load the icon now that the download_ has the real path.
LoadIcon();
tooltip_text_ =
UTF16ToWide(download_->GetFileNameToReportUser().LossyDisplayName());
// Force the shelf to layout again as our size has changed.
parent_->Layout();
parent_->SchedulePaint();
}
gfx::Size DownloadItemView::GetButtonSize() {
DCHECK(save_button_ && discard_button_);
gfx::Size size;
// We cache the size when successfully retrieved, not for performance reasons
// but because if this DownloadItemView is being animated while the tab is
// not showing, the native buttons are not parented and their preferred size
// is 0, messing-up the layout.
if (cached_button_size_.width() != 0)
return cached_button_size_;
size = save_button_->GetMinimumSize();
gfx::Size discard_size = discard_button_->GetMinimumSize();
size.SetSize(std::max(size.width(), discard_size.width()),
std::max(size.height(), discard_size.height()));
if (size.width() != 0)
cached_button_size_ = size;
return size;
}
// This method computes the minimum width of the label for displaying its text
// on 2 lines. It just breaks the string in 2 lines on the spaces and keeps the
// configuration with minimum width.
void DownloadItemView::SizeLabelToMinWidth() {
if (dangerous_download_label_sized_)
return;
std::wstring text = dangerous_download_label_->GetText();
TrimWhitespace(text, TRIM_ALL, &text);
DCHECK_EQ(std::wstring::npos, text.find(L"\n"));
// Make the label big so that GetPreferredSize() is not constrained by the
// current width.
dangerous_download_label_->SetBounds(0, 0, 1000, 1000);
gfx::Size size;
int min_width = -1;
string16 text16 = WideToUTF16(text);
// Using BREAK_WORD can work in most cases, but it can also break
// lines where it should not. Using BREAK_LINE is safer although
// slower for Chinese/Japanese. This is not perf-critical at all, though.
base::BreakIterator iter(&text16, base::BreakIterator::BREAK_LINE);
bool status = iter.Init();
DCHECK(status);
string16 current_text = text16;
string16 prev_text = text16;
while (iter.Advance()) {
size_t pos = iter.pos();
if (pos >= text16.length())
break;
// This can be a low surrogate codepoint, but u_isUWhiteSpace will
// return false and inserting a new line after a surrogate pair
// is perfectly ok.
char16 line_end_char = text16[pos - 1];
if (u_isUWhiteSpace(line_end_char))
current_text.replace(pos - 1, 1, 1, char16('\n'));
else
current_text.insert(pos, 1, char16('\n'));
dangerous_download_label_->SetText(UTF16ToWide(current_text));
size = dangerous_download_label_->GetPreferredSize();
if (min_width == -1)
min_width = size.width();
// If the width is growing again, it means we passed the optimal width spot.
if (size.width() > min_width) {
dangerous_download_label_->SetText(UTF16ToWide(prev_text));
break;
} else {
min_width = size.width();
}
// Restore the string.
prev_text = current_text;
current_text = text16;
}
// If we have a line with no line breaking opportunity (which is very
// unlikely), we won't cut it.
if (min_width == -1)
size = dangerous_download_label_->GetPreferredSize();
dangerous_download_label_->SetBounds(0, 0, size.width(), size.height());
dangerous_download_label_sized_ = true;
}
void DownloadItemView::Reenable() {
disabled_while_opening_ = false;
SetEnabled(true); // Triggers a repaint.
}
bool DownloadItemView::InDropDownButtonXCoordinateRange(int x) {
if (x > drop_down_x_left_ && x < drop_down_x_right_)
return true;
return false;
}
void DownloadItemView::UpdateAccessibleName() {
string16 new_name;
if (download_->safety_state() == DownloadItem::DANGEROUS) {
new_name = WideToUTF16Hack(dangerous_download_label_->GetText());
} else {
new_name = WideToUTF16Hack(status_text_) + char16(' ') +
download_->GetFileNameToReportUser().LossyDisplayName();
}
// If the name has changed, notify assistive technology that the name
// has changed so they can announce it immediately.
if (new_name != accessible_name_) {
accessible_name_ = new_name;
if (GetWidget()) {
GetWidget()->NotifyAccessibilityEvent(
this, ui::AccessibilityTypes::EVENT_NAME_CHANGED, true);
}
}
}