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.
32static const size_t kMaxDownloadViews = 15;
33
34// Padding from left edge and first download view.
35static const int kLeftPadding = 2;
36
37// Padding from right edge and close button/show downloads link.
38static const int kRightPadding = 10;
39
40// Padding between the show all link and close button.
41static const int kCloseAndLinkPadding = 14;
42
43// Padding between the download views.
44static const int kDownloadPadding = 10;
45
46// Padding between the top/bottom and the content.
47static const int kTopBottomPadding = 2;
48
49// Padding between the icon and 'show all downloads' link
50static const int kDownloadsTitlePadding = 4;
51
52// Border color.
53static const SkColor kBorderColor = SkColorSetRGB(214, 214, 214);
54
55// New download item animation speed in milliseconds.
56static const int kNewItemAnimationDurationMs = 800;
57
58// Shelf show/hide speed.
59static 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.
66static const int kNotifyOnExitTimeMS = 5000;
67
68namespace {
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();
73void 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
79int CenterPosition(int size, int target_size) {
80  return std::max((target_size - size) / 2, kTopBottomPadding);
81}
82
83}  // namespace
84
85DownloadShelfView::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
96DownloadShelfView::~DownloadShelfView() {
97  parent_->RemoveChildView(this);
98}
99
100void 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
131void 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
146void DownloadShelfView::AddDownload(BaseDownloadItemModel* download_model) {
147  DownloadItemView* view = new DownloadItemView(
148      download_model->download(), this, download_model);
149  AddDownloadView(view);
150}
151
152void DownloadShelfView::MouseMovedOutOfView() {
153  Close();
154}
155
156void 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
163void 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
179views::View* DownloadShelfView::GetDefaultFocusableChild() {
180  if (!download_views_.empty())
181    return download_views_[0];
182  else
183    return show_all_view_;
184}
185
186void 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
200void DownloadShelfView::OnPaintBorder(gfx::Canvas* canvas) {
201  canvas->FillRectInt(kBorderColor, 0, 0, width(), 1);
202}
203
204void DownloadShelfView::OpenedDownload(DownloadItemView* view) {
205  if (CanAutoClose())
206    mouse_watcher_.Start();
207}
208
209gfx::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
227void 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
242void 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
250void 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
327bool 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
348void 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
358void DownloadShelfView::OnThemeChanged() {
359  UpdateButtonColors();
360}
361
362void DownloadShelfView::LinkActivated(views::Link* source, int event_flags) {
363  browser_->ShowDownloadsTab();
364}
365
366void DownloadShelfView::ButtonPressed(
367    views::Button* button, const views::Event& event) {
368  Close();
369}
370
371bool DownloadShelfView::IsShowing() const {
372  return shelf_animation_->IsShowing();
373}
374
375bool DownloadShelfView::IsClosing() const {
376  return shelf_animation_->IsClosing();
377}
378
379void DownloadShelfView::Show() {
380  shelf_animation_->Show();
381}
382
383void DownloadShelfView::Close() {
384  parent_->SetDownloadShelfVisible(false);
385  shelf_animation_->Hide();
386}
387
388Browser* DownloadShelfView::browser() const {
389  return browser_;
390}
391
392void 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
412bool 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
420void 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
440gfx::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