diff options
Diffstat (limited to 'chrome/browser/views/download_item_view.cc')
-rw-r--r-- | chrome/browser/views/download_item_view.cc | 1016 |
1 files changed, 1016 insertions, 0 deletions
diff --git a/chrome/browser/views/download_item_view.cc b/chrome/browser/views/download_item_view.cc new file mode 100644 index 0000000..348c86d --- /dev/null +++ b/chrome/browser/views/download_item_view.cc @@ -0,0 +1,1016 @@ +// 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. + +#include "chrome/browser/views/download_item_view.h" + +#include <vector> + +#include "app/l10n_util.h" +#include "app/resource_bundle.h" +#include "app/text_elider.h" +#include "app/theme_provider.h" +#include "base/callback.h" +#include "base/file_path.h" +#include "base/histogram.h" +#include "base/i18n/rtl.h" +#include "base/string_util.h" +#include "base/sys_string_conversions.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/browser_theme_provider.h" +#include "chrome/browser/download/download_item_model.h" +#include "chrome/browser/download/download_util.h" +#include "chrome/browser/views/download_shelf_view.h" +#include "gfx/canvas_skia.h" +#include "gfx/color_utils.h" +#include "grit/generated_resources.h" +#include "grit/theme_resources.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" + +#if defined(OS_WIN) +#include "app/win_util.h" +#endif + +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 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_->state() == DownloadItem::COMPLETE) + 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_(l10n_util::GetString(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)) { + 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_ = download_->GetFileName().ToWStringHack(); + + font_ = ResourceBundle::GetSharedInstance().GetFont(ResourceBundle::BaseFont); + box_height_ = std::max<int>(2 * kVerticalPadding + font_.height() + + kVerticalTextPadding + font_.height(), + 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 SlideAnimation(this)); + drop_hover_animation_.reset(new SlideAnimation(this)); + + if (download->safety_state() == DownloadItem::DANGEROUS) { + tooltip_text_.clear(); + body_state_ = DANGEROUS; + drop_down_state_ = DANGEROUS; + + warning_icon_ = rb.GetBitmapNamed(IDR_WARNING); + save_button_ = new views::NativeButton(this, l10n_util::GetString( + download->is_extension_install() ? + IDS_CONTINUE_EXTENSION_DOWNLOAD : IDS_SAVE_DOWNLOAD)); + save_button_->set_ignore_minimum_size(true); + discard_button_ = new views::NativeButton( + this, l10n_util::GetString(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 filepath(download->original_name()); +#if defined(OS_LINUX) + std::wstring extension = base::SysNativeMBToWide(filepath.Extension()); +#else + std::wstring extension = filepath.Extension(); +#endif + + // Remove leading '.' + if (extension.length() > 0) + extension = extension.substr(1); +#if defined(OS_LINUX) + std::wstring rootname = + base::SysNativeMBToWide(filepath.BaseName().RemoveExtension().value()); +#else + std::wstring rootname = filepath.BaseName().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) + ElideString(extension, kFileNameMaxLength / 2, &extension); + + // The dangerous download label text is different for an extension file. + if (download->is_extension_install()) { + dangerous_download_label_ = new views::Label( + l10n_util::GetString(IDS_PROMPT_DANGEROUS_DOWNLOAD_EXTENSION)); + } else { + ElideString(rootname, kFileNameMaxLength - extension.length(), &rootname); + std::wstring filename = rootname + L"." + extension; + base::i18n::GetDisplayStringInLTRDirectionality(&filename); + dangerous_download_label_ = new views::Label( + l10n_util::GetStringF(IDS_PROMPT_DANGEROUS_DOWNLOAD, filename)); + } + dangerous_download_label_->SetMultiLine(true); + dangerous_download_label_->SetHorizontalAlignment( + views::Label::ALIGN_LEFT); + AddChildView(dangerous_download_label_); + SizeLabelToMinWidth(); + } + + // Set up our animation. + StartDownloadProgress(); +} + +DownloadItemView::~DownloadItemView() { + if (context_menu_.get()) { + context_menu_->Stop(); + } + icon_consumer_.CancelAllRequests(); + StopDownloadProgress(); + download_->RemoveObserver(this); +} + +// 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(); +} + +// 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(); + } + + std::wstring status_text = model_->GetStatusText(); + switch (download_->state()) { + case DownloadItem::IN_PROGRESS: + download_->is_paused() ? StopDownloadProgress() : StartDownloadProgress(); + break; + case DownloadItem::COMPLETE: + if (download_->auto_opened()) { + parent_->RemoveDownloadView(this); // This will delete us! + return; + } + StopDownloadProgress(); + complete_animation_.reset(new SlideAnimation(this)); + complete_animation_->SetSlideDuration(kCompleteAnimationDurationMs); + complete_animation_->SetTweenType(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_ = status_text; + + // We use the parent's (DownloadShelfView's) SchedulePaint, since there + // are spaces between each DownloadItemView that the parent is responsible + // for painting. + GetParent()->SchedulePaint(); +} + +void DownloadItemView::OnDownloadOpened(DownloadItem* download) { + disabled_while_opening_ = true; + SetEnabled(false); + MessageLoop::current()->PostDelayedTask( + FROM_HERE, + reenable_method_factory_.NewRunnableMethod(&DownloadItemView::Reenable), + kDisabledOnOpenDuration); +} + +// View overrides + +// In dangerous mode we have to layout our buttons. +void DownloadItemView::Layout() { + if (IsDangerousMode()) { + dangerous_download_label_->SetColor( + GetThemeProvider()->GetColor(BrowserThemeProvider::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()); + } +} + +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_->state() == DownloadItem::IN_PROGRESS) + download_->Cancel(true); + download_->Remove(true); + // 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_->manager()->DangerousDownloadValidated(download_); + } +} + +// Load an icon for the file type we're downloading, and animate any in progress +// download state. +void DownloadItemView::Paint(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 = MirroredXWithWidthInsideView( + download_util::kSmallProgressIconSize, kTextWidth); + // Add font_.height() to compensate for title, which is drawn later. + int y = box_y_ + kVerticalPadding + font_.height() + + kVerticalTextPadding; + SkColor file_name_color = GetThemeProvider()->GetColor( + BrowserThemeProvider::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(status_text_, font_, file_name_color, + mirrored_x, y, kTextWidth, font_.height()); + } + } + + // Paint the background images. + int x = kLeftPadding; + bool rtl_ui = base::i18n::IsRTL(); + if (rtl_ui) { + // 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->Save(); + 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(); + if (rtl_ui) { + canvas->Restore(); + canvas->Save(); + // Flip it for drawing drop-down images for RTL locales. + canvas->TranslateInt(width(), 0); + canvas->ScaleInt(-1, 1); + } + } + + 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(); + } + } + + if (rtl_ui) { + // 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()) { + std::wstring filename; + if (!disabled_while_opening_) { + filename = gfx::ElideFilename(download_->GetFileName(), + font_, kTextWidth); + } else { + // First, Calculate the download status opening string width. + std::wstring empty_string; + std::wstring status_string = + l10n_util::GetStringF(IDS_DOWNLOAD_STATUS_OPENING, empty_string); + int status_string_width = font_.GetStringWidth(status_string); + // Then, elide the file name. + std::wstring filename_string = + gfx::ElideFilename(download_->GetFileName(), font_, + kTextWidth - status_string_width); + // Last, concat the whole string. + filename = l10n_util::GetStringF(IDS_DOWNLOAD_STATUS_OPENING, + filename_string); + } + + int mirrored_x = MirroredXWithWidthInsideView( + download_util::kSmallProgressIconSize, kTextWidth); + SkColor file_name_color = GetThemeProvider()->GetColor( + BrowserThemeProvider::COLOR_BOOKMARK_TEXT); + int y = box_y_ + (show_status_text_ ? kVerticalPadding : + (box_height_ - font_.height()) / 2); + + // Draw the file's name. + canvas->DrawStringInt(filename, font_, + IsEnabled() ? file_name_color : + kFileNameDisabledColor, + mirrored_x, y, kTextWidth, font_.height()); + } + + // Paint the icon. + IconManager* im = g_browser_process->icon_manager(); + SkBitmap* icon = IsDangerousMode() ? warning_icon_ : + im->LookupIcon(download_->full_path(), IconLoader::SMALL); + + // 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_->state() == DownloadItem::IN_PROGRESS) { + download_util::PaintDownloadProgress(canvas, this, 0, 0, + progress_angle_, + download_->PercentComplete(), + download_util::SMALL); + } else if (download_->state() == DownloadItem::COMPLETE && + complete_animation_.get() && + complete_animation_->is_animating()) { + download_util::PaintDownloadComplete(canvas, this, 0, 0, + complete_animation_->GetCurrentValue(), + download_util::SMALL); + } + } + + // Draw the icon image. + int mirrored_x = MirroredXWithWidthInsideView( + 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::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; + + // We need to load the icon now that the download_ has the real path. + LoadIcon(); + tooltip_text_ = download_->GetFileName().ToWStringHack(); + + // Force the shelf to layout again as our size has changed. + parent_->Layout(); + parent_->SchedulePaint(); +} + +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_.height() + 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); +} + +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(); +} + +// Display the context menu for this item. +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(); + + if (event.IsOnlyLeftMouseButton()) { + gfx::Point point(event.location()); + if (!InDropDownButtonXCoordinateRange(event.x())) { + SetState(PUSHED, NORMAL); + return true; + } + + drop_down_pressed_ = true; + SetState(NORMAL, PUSHED); + + // 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); + + // The menu's position is different depending on the UI layout. + // DownloadShelfContextMenu will take care of setting the right anchor for + // the menu depending on the locale. + 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())); + context_menu_->Run(point); + + // 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); + } + } + return true; +} + +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::OnMouseReleased(const views::MouseEvent& event, + bool canceled) { + // Mouse should not activate us in dangerous mode. + if (IsDangerousMode()) + return; + + if (dragging_) { + // Starting a drag results in a MouseReleased, we need to ignore it. + dragging_ = false; + starting_drag_ = false; + return; + } + if (event.IsOnlyLeftMouseButton() && + !InDropDownButtonXCoordinateRange(event.x())) + OpenDownload(); + + SetState(NORMAL, NORMAL); +} + +// 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_->state() == DownloadItem::COMPLETE) { + IconManager* im = g_browser_process->icon_manager(); + SkBitmap* icon = im->LookupIcon(download_->full_path(), + 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::AnimationProgressed(const 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::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_); + 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_); + } +} + +void DownloadItemView::OnExtractIconComplete(IconManager::Handle handle, + SkBitmap* icon_bitmap) { + if (icon_bitmap) + GetParent()->SchedulePaint(); +} + +void DownloadItemView::LoadIcon() { + IconManager* im = g_browser_process->icon_manager(); + im->LoadIcon(download_->full_path(), IconLoader::SMALL, &icon_consumer_, + NewCallback(this, &DownloadItemView::OnExtractIconComplete)); +} + +bool DownloadItemView::GetTooltipText(const gfx::Point& p, + std::wstring* tooltip) { + if (tooltip_text_.empty()) + return false; + + tooltip->assign(tooltip_text_); + return true; +} + +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; + size_t sp_index = text.find(L" "); + while (sp_index != std::wstring::npos) { + text.replace(sp_index, 1, L"\n"); + dangerous_download_label_->SetText(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) + break; + else + min_width = size.width(); + + // Restore the string. + text.replace(sp_index, 1, L" "); + + sp_index = text.find(L" ", sp_index + 1); + } + + // If we have a line with no space, 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; +} |