Home | History | Annotate | Download | only in download
      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.
      4 
      5 #include "chrome/browser/ui/views/download/download_shelf_view.h"
      6 
      7 #include <algorithm>
      8 #include <vector>
      9 
     10 #include "base/logging.h"
     11 #include "base/strings/utf_string_conversions.h"
     12 #include "chrome/browser/download/download_item_model.h"
     13 #include "chrome/browser/download/download_stats.h"
     14 #include "chrome/browser/themes/theme_properties.h"
     15 #include "chrome/browser/ui/browser.h"
     16 #include "chrome/browser/ui/chrome_pages.h"
     17 #include "chrome/browser/ui/view_ids.h"
     18 #include "chrome/browser/ui/views/download/download_item_view.h"
     19 #include "chrome/browser/ui/views/frame/browser_view.h"
     20 #include "content/public/browser/download_item.h"
     21 #include "content/public/browser/download_manager.h"
     22 #include "content/public/browser/page_navigator.h"
     23 #include "grit/generated_resources.h"
     24 #include "grit/theme_resources.h"
     25 #include "grit/ui_resources.h"
     26 #include "ui/base/l10n/l10n_util.h"
     27 #include "ui/base/resource/resource_bundle.h"
     28 #include "ui/base/theme_provider.h"
     29 #include "ui/gfx/animation/slide_animation.h"
     30 #include "ui/gfx/canvas.h"
     31 #include "ui/views/background.h"
     32 #include "ui/views/controls/button/image_button.h"
     33 #include "ui/views/controls/image_view.h"
     34 #include "ui/views/controls/link.h"
     35 #include "ui/views/mouse_watcher_view_host.h"
     36 
     37 // Max number of download views we'll contain. Any time a view is added and
     38 // we already have this many download views, one is removed.
     39 static const size_t kMaxDownloadViews = 15;
     40 
     41 // Padding from left edge and first download view.
     42 static const int kLeftPadding = 2;
     43 
     44 // Padding from right edge and close button/show downloads link.
     45 static const int kRightPadding = 10;
     46 
     47 // Padding between the show all link and close button.
     48 static const int kCloseAndLinkPadding = 14;
     49 
     50 // Padding between the download views.
     51 static const int kDownloadPadding = 10;
     52 
     53 // Padding between the top/bottom and the content.
     54 static const int kTopBottomPadding = 2;
     55 
     56 // Padding between the icon and 'show all downloads' link
     57 static const int kDownloadsTitlePadding = 4;
     58 
     59 // Border color.
     60 static const SkColor kBorderColor = SkColorSetRGB(214, 214, 214);
     61 
     62 // New download item animation speed in milliseconds.
     63 static const int kNewItemAnimationDurationMs = 800;
     64 
     65 // Shelf show/hide speed.
     66 static const int kShelfAnimationDurationMs = 120;
     67 
     68 // Amount of time to delay if the mouse leaves the shelf by way of entering
     69 // another window. This is much larger than the normal delay as openning a
     70 // download is most likely going to trigger a new window to appear over the
     71 // button. Delay the time so that the user has a chance to quickly close the
     72 // other app and return to chrome with the download shelf still open.
     73 static const int kNotifyOnExitTimeMS = 5000;
     74 
     75 using content::DownloadItem;
     76 
     77 namespace {
     78 
     79 // Sets size->width() to view's preferred width + size->width().s
     80 // Sets size->height() to the max of the view's preferred height and
     81 // size->height();
     82 void AdjustSize(views::View* view, gfx::Size* size) {
     83   gfx::Size view_preferred = view->GetPreferredSize();
     84   size->Enlarge(view_preferred.width(), 0);
     85   size->set_height(std::max(view_preferred.height(), size->height()));
     86 }
     87 
     88 int CenterPosition(int size, int target_size) {
     89   return std::max((target_size - size) / 2, kTopBottomPadding);
     90 }
     91 
     92 }  // namespace
     93 
     94 DownloadShelfView::DownloadShelfView(Browser* browser, BrowserView* parent)
     95     : browser_(browser),
     96       arrow_image_(NULL),
     97       show_all_view_(NULL),
     98       close_button_(NULL),
     99       parent_(parent),
    100       mouse_watcher_(new views::MouseWatcherViewHost(this, gfx::Insets()),
    101                      this) {
    102   mouse_watcher_.set_notify_on_exit_time(
    103       base::TimeDelta::FromMilliseconds(kNotifyOnExitTimeMS));
    104   set_id(VIEW_ID_DOWNLOAD_SHELF);
    105   parent->AddChildView(this);
    106 }
    107 
    108 DownloadShelfView::~DownloadShelfView() {
    109   parent_->RemoveChildView(this);
    110 }
    111 
    112 void DownloadShelfView::AddDownloadView(DownloadItemView* view) {
    113   mouse_watcher_.Stop();
    114 
    115   DCHECK(view);
    116   download_views_.push_back(view);
    117 
    118   // Insert the new view as the first child, so the logical child order matches
    119   // the visual order.  This ensures that tabbing through downloads happens in
    120   // the order users would expect.
    121   AddChildViewAt(view, 0);
    122   if (download_views_.size() > kMaxDownloadViews)
    123     RemoveDownloadView(*download_views_.begin());
    124 
    125   new_item_animation_->Reset();
    126   new_item_animation_->Show();
    127 }
    128 
    129 void DownloadShelfView::DoAddDownload(DownloadItem* download) {
    130   DownloadItemView* view = new DownloadItemView(download, this);
    131   AddDownloadView(view);
    132 }
    133 
    134 void DownloadShelfView::MouseMovedOutOfHost() {
    135   Close(AUTOMATIC);
    136 }
    137 
    138 void DownloadShelfView::RemoveDownloadView(View* view) {
    139   DCHECK(view);
    140   std::vector<DownloadItemView*>::iterator i =
    141       find(download_views_.begin(), download_views_.end(), view);
    142   DCHECK(i != download_views_.end());
    143   download_views_.erase(i);
    144   RemoveChildView(view);
    145   delete view;
    146   if (download_views_.empty())
    147     Close(AUTOMATIC);
    148   else if (CanAutoClose())
    149     mouse_watcher_.Start();
    150   Layout();
    151   SchedulePaint();
    152 }
    153 
    154 views::View* DownloadShelfView::GetDefaultFocusableChild() {
    155   return download_views_.empty() ?
    156       static_cast<View*>(show_all_view_) : download_views_.back();
    157 }
    158 
    159 void DownloadShelfView::OnPaintBorder(gfx::Canvas* canvas) {
    160   canvas->FillRect(gfx::Rect(0, 0, width(), 1), kBorderColor);
    161 }
    162 
    163 void DownloadShelfView::OpenedDownload(DownloadItemView* view) {
    164   if (CanAutoClose())
    165     mouse_watcher_.Start();
    166 }
    167 
    168 content::PageNavigator* DownloadShelfView::GetNavigator() {
    169   return browser_;
    170 }
    171 
    172 gfx::Size DownloadShelfView::GetPreferredSize() const {
    173   gfx::Size prefsize(kRightPadding + kLeftPadding + kCloseAndLinkPadding, 0);
    174   AdjustSize(close_button_, &prefsize);
    175   AdjustSize(show_all_view_, &prefsize);
    176   // Add one download view to the preferred size.
    177   if (!download_views_.empty()) {
    178     AdjustSize(*download_views_.begin(), &prefsize);
    179     prefsize.Enlarge(kDownloadPadding, 0);
    180   }
    181   prefsize.Enlarge(0, kTopBottomPadding + kTopBottomPadding);
    182   if (shelf_animation_->is_animating()) {
    183     prefsize.set_height(static_cast<int>(
    184         static_cast<double>(prefsize.height()) *
    185                             shelf_animation_->GetCurrentValue()));
    186   }
    187   return prefsize;
    188 }
    189 
    190 void DownloadShelfView::AnimationProgressed(const gfx::Animation *animation) {
    191   if (animation == new_item_animation_.get()) {
    192     Layout();
    193     SchedulePaint();
    194   } else if (animation == shelf_animation_.get()) {
    195     // Force a re-layout of the parent, which will call back into
    196     // GetPreferredSize, where we will do our animation. In the case where the
    197     // animation is hiding, we do a full resize - the fast resizing would
    198     // otherwise leave blank white areas where the shelf was and where the
    199     // user's eye is. Thankfully bottom-resizing is a lot faster than
    200     // top-resizing.
    201     parent_->ToolbarSizeChanged(shelf_animation_->IsShowing());
    202   }
    203 }
    204 
    205 void DownloadShelfView::AnimationEnded(const gfx::Animation *animation) {
    206   if (animation == shelf_animation_.get()) {
    207     parent_->SetDownloadShelfVisible(shelf_animation_->IsShowing());
    208     if (!shelf_animation_->IsShowing())
    209       Closed();
    210   }
    211 }
    212 
    213 void DownloadShelfView::Layout() {
    214   // Let our base class layout our child views
    215   views::View::Layout();
    216 
    217   // If there is not enough room to show the first download item, show the
    218   // "Show all downloads" link to the left to make it more visible that there is
    219   // something to see.
    220   bool show_link_only = !CanFitFirstDownloadItem();
    221 
    222   gfx::Size image_size = arrow_image_->GetPreferredSize();
    223   gfx::Size close_button_size = close_button_->GetPreferredSize();
    224   gfx::Size show_all_size = show_all_view_->GetPreferredSize();
    225   int max_download_x =
    226       std::max<int>(0, width() - kRightPadding - close_button_size.width() -
    227                        kCloseAndLinkPadding - show_all_size.width() -
    228                        kDownloadsTitlePadding - image_size.width() -
    229                        kDownloadPadding);
    230   int next_x = show_link_only ? kLeftPadding :
    231                                 max_download_x + kDownloadPadding;
    232   // Align vertically with show_all_view_.
    233   arrow_image_->SetBounds(next_x,
    234                           CenterPosition(image_size.height(), height()),
    235                           image_size.width(), image_size.height());
    236   next_x += image_size.width() + kDownloadsTitlePadding;
    237   show_all_view_->SetBounds(next_x,
    238                             CenterPosition(show_all_size.height(), height()),
    239                             show_all_size.width(),
    240                             show_all_size.height());
    241   next_x += show_all_size.width() + kCloseAndLinkPadding;
    242   // If the window is maximized, we want to expand the hitbox of the close
    243   // button to the right and bottom to make it easier to click.
    244   bool is_maximized = browser_->window()->IsMaximized();
    245   int y = CenterPosition(close_button_size.height(), height());
    246   close_button_->SetBounds(next_x, y,
    247       is_maximized ? width() - next_x : close_button_size.width(),
    248       is_maximized ? height() - y : close_button_size.height());
    249   if (show_link_only) {
    250     // Let's hide all the items.
    251     std::vector<DownloadItemView*>::reverse_iterator ri;
    252     for (ri = download_views_.rbegin(); ri != download_views_.rend(); ++ri)
    253       (*ri)->SetVisible(false);
    254     return;
    255   }
    256 
    257   next_x = kLeftPadding;
    258   std::vector<DownloadItemView*>::reverse_iterator ri;
    259   for (ri = download_views_.rbegin(); ri != download_views_.rend(); ++ri) {
    260     gfx::Size view_size = (*ri)->GetPreferredSize();
    261 
    262     int x = next_x;
    263 
    264     // Figure out width of item.
    265     int item_width = view_size.width();
    266     if (new_item_animation_->is_animating() && ri == download_views_.rbegin()) {
    267        item_width = static_cast<int>(static_cast<double>(view_size.width()) *
    268                      new_item_animation_->GetCurrentValue());
    269     }
    270 
    271     next_x += item_width;
    272 
    273     // Make sure our item can be contained within the shelf.
    274     if (next_x < max_download_x) {
    275       (*ri)->SetVisible(true);
    276       (*ri)->SetBounds(x, CenterPosition(view_size.height(), height()),
    277                        item_width, view_size.height());
    278     } else {
    279       (*ri)->SetVisible(false);
    280     }
    281   }
    282 }
    283 
    284 void DownloadShelfView::ViewHierarchyChanged(
    285     const ViewHierarchyChangedDetails& details) {
    286   View::ViewHierarchyChanged(details);
    287 
    288   if (details.is_add && (details.child == this)) {
    289     ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
    290     arrow_image_ = new views::ImageView();
    291     arrow_image_->SetImage(rb.GetImageSkiaNamed(IDR_DOWNLOADS_FAVICON));
    292     AddChildView(arrow_image_);
    293 
    294     show_all_view_ = new views::Link(
    295         l10n_util::GetStringUTF16(IDS_SHOW_ALL_DOWNLOADS));
    296     show_all_view_->set_listener(this);
    297     AddChildView(show_all_view_);
    298 
    299     close_button_ = new views::ImageButton(this);
    300     close_button_->SetImage(views::CustomButton::STATE_NORMAL,
    301                             rb.GetImageSkiaNamed(IDR_CLOSE_1));
    302     close_button_->SetImage(views::CustomButton::STATE_HOVERED,
    303                             rb.GetImageSkiaNamed(IDR_CLOSE_1_H));
    304     close_button_->SetImage(views::CustomButton::STATE_PRESSED,
    305                             rb.GetImageSkiaNamed(IDR_CLOSE_1_P));
    306     close_button_->SetAccessibleName(
    307         l10n_util::GetStringUTF16(IDS_ACCNAME_CLOSE));
    308     AddChildView(close_button_);
    309 
    310     UpdateColorsFromTheme();
    311 
    312     new_item_animation_.reset(new gfx::SlideAnimation(this));
    313     new_item_animation_->SetSlideDuration(kNewItemAnimationDurationMs);
    314 
    315     shelf_animation_.reset(new gfx::SlideAnimation(this));
    316     shelf_animation_->SetSlideDuration(kShelfAnimationDurationMs);
    317   }
    318 }
    319 
    320 bool DownloadShelfView::CanFitFirstDownloadItem() {
    321   if (download_views_.empty())
    322     return true;
    323 
    324   gfx::Size image_size = arrow_image_->GetPreferredSize();
    325   gfx::Size close_button_size = close_button_->GetPreferredSize();
    326   gfx::Size show_all_size = show_all_view_->GetPreferredSize();
    327 
    328   // Let's compute the width available for download items, which is the width
    329   // of the shelf minus the "Show all downloads" link, arrow and close button
    330   // and the padding.
    331   int available_width = width() - kRightPadding - close_button_size.width() -
    332       kCloseAndLinkPadding - show_all_size.width() - kDownloadsTitlePadding -
    333       image_size.width() - kDownloadPadding - kLeftPadding;
    334   if (available_width <= 0)
    335     return false;
    336 
    337   gfx::Size item_size = (*download_views_.rbegin())->GetPreferredSize();
    338   return item_size.width() < available_width;
    339 }
    340 
    341 void DownloadShelfView::UpdateColorsFromTheme() {
    342   if (show_all_view_ && close_button_ && GetThemeProvider()) {
    343     ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
    344     set_background(views::Background::CreateSolidBackground(
    345         GetThemeProvider()->GetColor(ThemeProperties::COLOR_TOOLBAR)));
    346     show_all_view_->SetBackgroundColor(background()->get_color());
    347     show_all_view_->SetEnabledColor(
    348         GetThemeProvider()->GetColor(ThemeProperties::COLOR_BOOKMARK_TEXT));
    349     close_button_->SetBackground(
    350         GetThemeProvider()->GetColor(ThemeProperties::COLOR_TAB_TEXT),
    351         rb.GetImageSkiaNamed(IDR_CLOSE_1),
    352         rb.GetImageSkiaNamed(IDR_CLOSE_1_MASK));
    353   }
    354 }
    355 
    356 void DownloadShelfView::OnThemeChanged() {
    357   UpdateColorsFromTheme();
    358 }
    359 
    360 void DownloadShelfView::LinkClicked(views::Link* source, int event_flags) {
    361   chrome::ShowDownloads(browser_);
    362 }
    363 
    364 void DownloadShelfView::ButtonPressed(
    365     views::Button* button, const ui::Event& event) {
    366   Close(USER_ACTION);
    367 }
    368 
    369 bool DownloadShelfView::IsShowing() const {
    370   return shelf_animation_->IsShowing();
    371 }
    372 
    373 bool DownloadShelfView::IsClosing() const {
    374   return shelf_animation_->IsClosing();
    375 }
    376 
    377 void DownloadShelfView::DoShow() {
    378   shelf_animation_->Show();
    379 }
    380 
    381 void DownloadShelfView::DoClose(CloseReason reason) {
    382   int num_in_progress = 0;
    383   for (size_t i = 0; i < download_views_.size(); ++i) {
    384     if (download_views_[i]->download()->GetState() == DownloadItem::IN_PROGRESS)
    385       ++num_in_progress;
    386   }
    387   RecordDownloadShelfClose(
    388       download_views_.size(), num_in_progress, reason == AUTOMATIC);
    389   parent_->SetDownloadShelfVisible(false);
    390   shelf_animation_->Hide();
    391 }
    392 
    393 Browser* DownloadShelfView::browser() const {
    394   return browser_;
    395 }
    396 
    397 void DownloadShelfView::Closed() {
    398   // Don't remove completed downloads if the shelf is just being auto-hidden
    399   // rather than explicitly closed by the user.
    400   if (is_hidden())
    401     return;
    402   // When the close animation is complete, remove all completed downloads.
    403   size_t i = 0;
    404   while (i < download_views_.size()) {
    405     DownloadItem* download = download_views_[i]->download();
    406     DownloadItem::DownloadState state = download->GetState();
    407     bool is_transfer_done = state == DownloadItem::COMPLETE ||
    408                             state == DownloadItem::CANCELLED ||
    409                             state == DownloadItem::INTERRUPTED;
    410     if (is_transfer_done && !download->IsDangerous()) {
    411       RemoveDownloadView(download_views_[i]);
    412     } else {
    413       // Treat the item as opened when we close. This way if we get shown again
    414       // the user need not open this item for the shelf to auto-close.
    415       download->SetOpened(true);
    416       ++i;
    417     }
    418   }
    419 }
    420 
    421 bool DownloadShelfView::CanAutoClose() {
    422   for (size_t i = 0; i < download_views_.size(); ++i) {
    423     if (!download_views_[i]->download()->GetOpened())
    424       return false;
    425   }
    426   return true;
    427 }
    428