download_shelf_view.cc revision cedac228d2dd51db4b79ea1e72c7f249408ee061
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.
39static const size_t kMaxDownloadViews = 15;
40
41// Padding from left edge and first download view.
42static const int kLeftPadding = 2;
43
44// Padding from right edge and close button/show downloads link.
45static const int kRightPadding = 10;
46
47// Padding between the show all link and close button.
48static const int kCloseAndLinkPadding = 14;
49
50// Padding between the download views.
51static const int kDownloadPadding = 10;
52
53// Padding between the top/bottom and the content.
54static const int kTopBottomPadding = 2;
55
56// Padding between the icon and 'show all downloads' link
57static const int kDownloadsTitlePadding = 4;
58
59// Border color.
60static const SkColor kBorderColor = SkColorSetRGB(214, 214, 214);
61
62// New download item animation speed in milliseconds.
63static const int kNewItemAnimationDurationMs = 800;
64
65// Shelf show/hide speed.
66static 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.
73static const int kNotifyOnExitTimeMS = 5000;
74
75using content::DownloadItem;
76
77namespace {
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();
82void 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
88int CenterPosition(int size, int target_size) {
89  return std::max((target_size - size) / 2, kTopBottomPadding);
90}
91
92}  // namespace
93
94DownloadShelfView::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
108DownloadShelfView::~DownloadShelfView() {
109  parent_->RemoveChildView(this);
110}
111
112void 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
129void DownloadShelfView::DoAddDownload(DownloadItem* download) {
130  DownloadItemView* view = new DownloadItemView(download, this);
131  AddDownloadView(view);
132}
133
134void DownloadShelfView::MouseMovedOutOfHost() {
135  Close(AUTOMATIC);
136}
137
138void 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
154views::View* DownloadShelfView::GetDefaultFocusableChild() {
155  return download_views_.empty() ?
156      static_cast<View*>(show_all_view_) : download_views_.back();
157}
158
159void DownloadShelfView::OnPaintBorder(gfx::Canvas* canvas) {
160  canvas->FillRect(gfx::Rect(0, 0, width(), 1), kBorderColor);
161}
162
163void DownloadShelfView::OpenedDownload(DownloadItemView* view) {
164  if (CanAutoClose())
165    mouse_watcher_.Start();
166}
167
168content::PageNavigator* DownloadShelfView::GetNavigator() {
169  return browser_;
170}
171
172gfx::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
190void 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
205void 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
213void 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
284void 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
320bool 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
341void 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
356void DownloadShelfView::OnThemeChanged() {
357  UpdateColorsFromTheme();
358}
359
360void DownloadShelfView::LinkClicked(views::Link* source, int event_flags) {
361  chrome::ShowDownloads(browser_);
362}
363
364void DownloadShelfView::ButtonPressed(
365    views::Button* button, const ui::Event& event) {
366  Close(USER_ACTION);
367}
368
369bool DownloadShelfView::IsShowing() const {
370  return shelf_animation_->IsShowing();
371}
372
373bool DownloadShelfView::IsClosing() const {
374  return shelf_animation_->IsClosing();
375}
376
377void DownloadShelfView::DoShow() {
378  shelf_animation_->Show();
379}
380
381void 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
393Browser* DownloadShelfView::browser() const {
394  return browser_;
395}
396
397void 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
421bool 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