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