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.
5 #include "chrome/browser/ui/views/download/download_shelf_view.h"
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"
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;
41 // Padding from left edge and first download view.
42 static const int kLeftPadding = 2;
44 // Padding from right edge and close button/show downloads link.
45 static const int kRightPadding = 10;
47 // Padding between the show all link and close button.
48 static const int kCloseAndLinkPadding = 14;
50 // Padding between the download views.
51 static const int kDownloadPadding = 10;
53 // Padding between the top/bottom and the content.
54 static const int kTopBottomPadding = 2;
56 // Padding between the icon and 'show all downloads' link
57 static const int kDownloadsTitlePadding = 4;
60 static const SkColor kBorderColor = SkColorSetRGB(214, 214, 214);
62 // New download item animation speed in milliseconds.
63 static const int kNewItemAnimationDurationMs = 800;
65 // Shelf show/hide speed.
66 static const int kShelfAnimationDurationMs = 120;
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;
75 using content::DownloadItem;
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
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()));
88 int CenterPosition(int size, int target_size) {
89 return std::max((target_size - size) / 2, kTopBottomPadding);
94 DownloadShelfView::DownloadShelfView(Browser* browser, BrowserView* parent)
100 mouse_watcher_(new views::MouseWatcherViewHost(this, gfx::Insets()),
102 mouse_watcher_.set_notify_on_exit_time(
103 base::TimeDelta::FromMilliseconds(kNotifyOnExitTimeMS));
104 set_id(VIEW_ID_DOWNLOAD_SHELF);
105 parent->AddChildView(this);
108 DownloadShelfView::~DownloadShelfView() {
109 parent_->RemoveChildView(this);
112 void DownloadShelfView::AddDownloadView(DownloadItemView* view) {
113 mouse_watcher_.Stop();
116 download_views_.push_back(view);
118 if (download_views_.size() > kMaxDownloadViews)
119 RemoveDownloadView(*download_views_.begin());
121 new_item_animation_->Reset();
122 new_item_animation_->Show();
125 void DownloadShelfView::DoAddDownload(DownloadItem* download) {
126 DownloadItemView* view = new DownloadItemView(download, this);
127 AddDownloadView(view);
130 void DownloadShelfView::MouseMovedOutOfHost() {
134 void DownloadShelfView::OnWillChangeFocus(views::View* focused_before,
135 views::View* focused_now) {
136 SchedulePaintForDownloadItem(focused_before);
137 SchedulePaintForDownloadItem(focused_now);
140 void DownloadShelfView::OnDidChangeFocus(views::View* focused_before,
141 views::View* focused_now) {
142 AccessiblePaneView::OnDidChangeFocus(focused_before, focused_now);
145 void DownloadShelfView::RemoveDownloadView(View* view) {
147 std::vector<DownloadItemView*>::iterator i =
148 find(download_views_.begin(), download_views_.end(), view);
149 DCHECK(i != download_views_.end());
150 download_views_.erase(i);
151 RemoveChildView(view);
153 if (download_views_.empty())
155 else if (CanAutoClose())
156 mouse_watcher_.Start();
161 views::View* DownloadShelfView::GetDefaultFocusableChild() {
162 return download_views_.empty() ?
163 static_cast<View*>(show_all_view_) : download_views_[0];
166 void DownloadShelfView::OnPaint(gfx::Canvas* canvas) {
167 OnPaintBackground(canvas);
168 OnPaintBorder(canvas);
170 // Draw the focus rect here, since it's outside the bounds of the item.
171 for (size_t i = 0; i < download_views_.size(); ++i) {
172 if (download_views_[i]->HasFocus()) {
173 gfx::Rect r = GetFocusRectBounds(download_views_[i]);
175 canvas->DrawFocusRect(r);
181 void DownloadShelfView::OnPaintBorder(gfx::Canvas* canvas) {
182 canvas->FillRect(gfx::Rect(0, 0, width(), 1), kBorderColor);
185 void DownloadShelfView::OpenedDownload(DownloadItemView* view) {
187 mouse_watcher_.Start();
190 content::PageNavigator* DownloadShelfView::GetNavigator() {
194 gfx::Size DownloadShelfView::GetPreferredSize() {
195 gfx::Size prefsize(kRightPadding + kLeftPadding + kCloseAndLinkPadding, 0);
196 AdjustSize(close_button_, &prefsize);
197 AdjustSize(show_all_view_, &prefsize);
198 // Add one download view to the preferred size.
199 if (!download_views_.empty()) {
200 AdjustSize(*download_views_.begin(), &prefsize);
201 prefsize.Enlarge(kDownloadPadding, 0);
203 prefsize.Enlarge(0, kTopBottomPadding + kTopBottomPadding);
204 if (shelf_animation_->is_animating()) {
205 prefsize.set_height(static_cast<int>(
206 static_cast<double>(prefsize.height()) *
207 shelf_animation_->GetCurrentValue()));
212 void DownloadShelfView::AnimationProgressed(const gfx::Animation *animation) {
213 if (animation == new_item_animation_.get()) {
216 } else if (animation == shelf_animation_.get()) {
217 // Force a re-layout of the parent, which will call back into
218 // GetPreferredSize, where we will do our animation. In the case where the
219 // animation is hiding, we do a full resize - the fast resizing would
220 // otherwise leave blank white areas where the shelf was and where the
221 // user's eye is. Thankfully bottom-resizing is a lot faster than
223 parent_->ToolbarSizeChanged(shelf_animation_->IsShowing());
227 void DownloadShelfView::AnimationEnded(const gfx::Animation *animation) {
228 if (animation == shelf_animation_.get()) {
229 parent_->SetDownloadShelfVisible(shelf_animation_->IsShowing());
230 if (!shelf_animation_->IsShowing())
235 void DownloadShelfView::Layout() {
236 // Let our base class layout our child views
237 views::View::Layout();
239 // If there is not enough room to show the first download item, show the
240 // "Show all downloads" link to the left to make it more visible that there is
242 bool show_link_only = !CanFitFirstDownloadItem();
244 gfx::Size image_size = arrow_image_->GetPreferredSize();
245 gfx::Size close_button_size = close_button_->GetPreferredSize();
246 gfx::Size show_all_size = show_all_view_->GetPreferredSize();
248 std::max<int>(0, width() - kRightPadding - close_button_size.width() -
249 kCloseAndLinkPadding - show_all_size.width() -
250 kDownloadsTitlePadding - image_size.width() -
252 int next_x = show_link_only ? kLeftPadding :
253 max_download_x + kDownloadPadding;
254 // Align vertically with show_all_view_.
255 arrow_image_->SetBounds(next_x,
256 CenterPosition(image_size.height(), height()),
257 image_size.width(), image_size.height());
258 next_x += image_size.width() + kDownloadsTitlePadding;
259 show_all_view_->SetBounds(next_x,
260 CenterPosition(show_all_size.height(), height()),
261 show_all_size.width(),
262 show_all_size.height());
263 next_x += show_all_size.width() + kCloseAndLinkPadding;
264 // If the window is maximized, we want to expand the hitbox of the close
265 // button to the right and bottom to make it easier to click.
266 bool is_maximized = browser_->window()->IsMaximized();
267 int y = CenterPosition(close_button_size.height(), height());
268 close_button_->SetBounds(next_x, y,
269 is_maximized ? width() - next_x : close_button_size.width(),
270 is_maximized ? height() - y : close_button_size.height());
271 if (show_link_only) {
272 // Let's hide all the items.
273 std::vector<DownloadItemView*>::reverse_iterator ri;
274 for (ri = download_views_.rbegin(); ri != download_views_.rend(); ++ri)
275 (*ri)->SetVisible(false);
279 next_x = kLeftPadding;
280 std::vector<DownloadItemView*>::reverse_iterator ri;
281 for (ri = download_views_.rbegin(); ri != download_views_.rend(); ++ri) {
282 gfx::Size view_size = (*ri)->GetPreferredSize();
286 // Figure out width of item.
287 int item_width = view_size.width();
288 if (new_item_animation_->is_animating() && ri == download_views_.rbegin()) {
289 item_width = static_cast<int>(static_cast<double>(view_size.width()) *
290 new_item_animation_->GetCurrentValue());
293 next_x += item_width;
295 // Make sure our item can be contained within the shelf.
296 if (next_x < max_download_x) {
297 (*ri)->SetVisible(true);
298 (*ri)->SetBounds(x, CenterPosition(view_size.height(), height()),
299 item_width, view_size.height());
301 (*ri)->SetVisible(false);
306 void DownloadShelfView::ViewHierarchyChanged(
307 const ViewHierarchyChangedDetails& details) {
308 View::ViewHierarchyChanged(details);
310 if (details.is_add && (details.child == this)) {
311 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
312 arrow_image_ = new views::ImageView();
313 arrow_image_->SetImage(rb.GetImageSkiaNamed(IDR_DOWNLOADS_FAVICON));
314 AddChildView(arrow_image_);
316 show_all_view_ = new views::Link(
317 l10n_util::GetStringUTF16(IDS_SHOW_ALL_DOWNLOADS));
318 show_all_view_->set_listener(this);
319 AddChildView(show_all_view_);
321 close_button_ = new views::ImageButton(this);
322 close_button_->SetImage(views::CustomButton::STATE_NORMAL,
323 rb.GetImageSkiaNamed(IDR_CLOSE_1));
324 close_button_->SetImage(views::CustomButton::STATE_HOVERED,
325 rb.GetImageSkiaNamed(IDR_CLOSE_1_H));
326 close_button_->SetImage(views::CustomButton::STATE_PRESSED,
327 rb.GetImageSkiaNamed(IDR_CLOSE_1_P));
328 close_button_->SetAccessibleName(
329 l10n_util::GetStringUTF16(IDS_ACCNAME_CLOSE));
330 AddChildView(close_button_);
332 UpdateColorsFromTheme();
334 new_item_animation_.reset(new gfx::SlideAnimation(this));
335 new_item_animation_->SetSlideDuration(kNewItemAnimationDurationMs);
337 shelf_animation_.reset(new gfx::SlideAnimation(this));
338 shelf_animation_->SetSlideDuration(kShelfAnimationDurationMs);
342 bool DownloadShelfView::CanFitFirstDownloadItem() {
343 if (download_views_.empty())
346 gfx::Size image_size = arrow_image_->GetPreferredSize();
347 gfx::Size close_button_size = close_button_->GetPreferredSize();
348 gfx::Size show_all_size = show_all_view_->GetPreferredSize();
350 // Let's compute the width available for download items, which is the width
351 // of the shelf minus the "Show all downloads" link, arrow and close button
353 int available_width = width() - kRightPadding - close_button_size.width() -
354 kCloseAndLinkPadding - show_all_size.width() - kDownloadsTitlePadding -
355 image_size.width() - kDownloadPadding - kLeftPadding;
356 if (available_width <= 0)
359 gfx::Size item_size = (*download_views_.rbegin())->GetPreferredSize();
360 return item_size.width() < available_width;
363 void DownloadShelfView::UpdateColorsFromTheme() {
364 if (show_all_view_ && close_button_ && GetThemeProvider()) {
365 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
366 set_background(views::Background::CreateSolidBackground(
367 GetThemeProvider()->GetColor(ThemeProperties::COLOR_TOOLBAR)));
368 show_all_view_->SetBackgroundColor(background()->get_color());
369 show_all_view_->SetEnabledColor(
370 GetThemeProvider()->GetColor(ThemeProperties::COLOR_BOOKMARK_TEXT));
371 close_button_->SetBackground(
372 GetThemeProvider()->GetColor(ThemeProperties::COLOR_TAB_TEXT),
373 rb.GetImageSkiaNamed(IDR_CLOSE_1),
374 rb.GetImageSkiaNamed(IDR_CLOSE_1_MASK));
378 void DownloadShelfView::OnThemeChanged() {
379 UpdateColorsFromTheme();
382 void DownloadShelfView::LinkClicked(views::Link* source, int event_flags) {
383 chrome::ShowDownloads(browser_);
386 void DownloadShelfView::ButtonPressed(
387 views::Button* button, const ui::Event& event) {
391 bool DownloadShelfView::IsShowing() const {
392 return shelf_animation_->IsShowing();
395 bool DownloadShelfView::IsClosing() const {
396 return shelf_animation_->IsClosing();
399 void DownloadShelfView::DoShow() {
400 shelf_animation_->Show();
403 void DownloadShelfView::DoClose(CloseReason reason) {
404 int num_in_progress = 0;
405 for (size_t i = 0; i < download_views_.size(); ++i) {
406 if (download_views_[i]->download()->GetState() == DownloadItem::IN_PROGRESS)
409 RecordDownloadShelfClose(
410 download_views_.size(), num_in_progress, reason == AUTOMATIC);
411 parent_->SetDownloadShelfVisible(false);
412 shelf_animation_->Hide();
415 Browser* DownloadShelfView::browser() const {
419 void DownloadShelfView::Closed() {
420 // Don't remove completed downloads if the shelf is just being auto-hidden
421 // rather than explicitly closed by the user.
424 // When the close animation is complete, remove all completed downloads.
426 while (i < download_views_.size()) {
427 DownloadItem* download = download_views_[i]->download();
428 DownloadItem::DownloadState state = download->GetState();
429 bool is_transfer_done = state == DownloadItem::COMPLETE ||
430 state == DownloadItem::CANCELLED ||
431 state == DownloadItem::INTERRUPTED;
432 if (is_transfer_done && !download->IsDangerous()) {
433 RemoveDownloadView(download_views_[i]);
435 // Treat the item as opened when we close. This way if we get shown again
436 // the user need not open this item for the shelf to auto-close.
437 download->SetOpened(true);
443 bool DownloadShelfView::CanAutoClose() {
444 for (size_t i = 0; i < download_views_.size(); ++i) {
445 if (!download_views_[i]->download()->GetOpened())
451 void DownloadShelfView::SchedulePaintForDownloadItem(views::View* view) {
452 // Make sure it's not NULL. (Focus sometimes changes to or from NULL.)
456 // Make sure it's one of our DownloadItemViews.
458 for (size_t i = 0; i < download_views_.size(); ++i) {
459 if (download_views_[i] == view)
466 gfx::Rect invalid_rect =
467 GetFocusRectBounds(static_cast<DownloadItemView*>(view));
468 SchedulePaintInRect(invalid_rect);
471 gfx::Rect DownloadShelfView::GetFocusRectBounds(
472 const DownloadItemView* download_item_view) {
473 gfx::Rect bounds = download_item_view->bounds();
475 #if defined(TOOLKIT_VIEWS)
476 bounds.set_height(bounds.height() - 1);