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::RemoveDownloadView(View
* view
) {
136 std::vector
<DownloadItemView
*>::iterator i
=
137 find(download_views_
.begin(), download_views_
.end(), view
);
138 DCHECK(i
!= download_views_
.end());
139 download_views_
.erase(i
);
140 RemoveChildView(view
);
142 if (download_views_
.empty())
144 else if (CanAutoClose())
145 mouse_watcher_
.Start();
150 views::View
* DownloadShelfView::GetDefaultFocusableChild() {
151 return download_views_
.empty() ?
152 static_cast<View
*>(show_all_view_
) : download_views_
[0];
155 void DownloadShelfView::OnPaintBorder(gfx::Canvas
* canvas
) {
156 canvas
->FillRect(gfx::Rect(0, 0, width(), 1), kBorderColor
);
159 void DownloadShelfView::OpenedDownload(DownloadItemView
* view
) {
161 mouse_watcher_
.Start();
164 content::PageNavigator
* DownloadShelfView::GetNavigator() {
168 gfx::Size
DownloadShelfView::GetPreferredSize() {
169 gfx::Size
prefsize(kRightPadding
+ kLeftPadding
+ kCloseAndLinkPadding
, 0);
170 AdjustSize(close_button_
, &prefsize
);
171 AdjustSize(show_all_view_
, &prefsize
);
172 // Add one download view to the preferred size.
173 if (!download_views_
.empty()) {
174 AdjustSize(*download_views_
.begin(), &prefsize
);
175 prefsize
.Enlarge(kDownloadPadding
, 0);
177 prefsize
.Enlarge(0, kTopBottomPadding
+ kTopBottomPadding
);
178 if (shelf_animation_
->is_animating()) {
179 prefsize
.set_height(static_cast<int>(
180 static_cast<double>(prefsize
.height()) *
181 shelf_animation_
->GetCurrentValue()));
186 void DownloadShelfView::AnimationProgressed(const gfx::Animation
*animation
) {
187 if (animation
== new_item_animation_
.get()) {
190 } else if (animation
== shelf_animation_
.get()) {
191 // Force a re-layout of the parent, which will call back into
192 // GetPreferredSize, where we will do our animation. In the case where the
193 // animation is hiding, we do a full resize - the fast resizing would
194 // otherwise leave blank white areas where the shelf was and where the
195 // user's eye is. Thankfully bottom-resizing is a lot faster than
197 parent_
->ToolbarSizeChanged(shelf_animation_
->IsShowing());
201 void DownloadShelfView::AnimationEnded(const gfx::Animation
*animation
) {
202 if (animation
== shelf_animation_
.get()) {
203 parent_
->SetDownloadShelfVisible(shelf_animation_
->IsShowing());
204 if (!shelf_animation_
->IsShowing())
209 void DownloadShelfView::Layout() {
210 // Let our base class layout our child views
211 views::View::Layout();
213 // If there is not enough room to show the first download item, show the
214 // "Show all downloads" link to the left to make it more visible that there is
216 bool show_link_only
= !CanFitFirstDownloadItem();
218 gfx::Size image_size
= arrow_image_
->GetPreferredSize();
219 gfx::Size close_button_size
= close_button_
->GetPreferredSize();
220 gfx::Size show_all_size
= show_all_view_
->GetPreferredSize();
222 std::max
<int>(0, width() - kRightPadding
- close_button_size
.width() -
223 kCloseAndLinkPadding
- show_all_size
.width() -
224 kDownloadsTitlePadding
- image_size
.width() -
226 int next_x
= show_link_only
? kLeftPadding
:
227 max_download_x
+ kDownloadPadding
;
228 // Align vertically with show_all_view_.
229 arrow_image_
->SetBounds(next_x
,
230 CenterPosition(image_size
.height(), height()),
231 image_size
.width(), image_size
.height());
232 next_x
+= image_size
.width() + kDownloadsTitlePadding
;
233 show_all_view_
->SetBounds(next_x
,
234 CenterPosition(show_all_size
.height(), height()),
235 show_all_size
.width(),
236 show_all_size
.height());
237 next_x
+= show_all_size
.width() + kCloseAndLinkPadding
;
238 // If the window is maximized, we want to expand the hitbox of the close
239 // button to the right and bottom to make it easier to click.
240 bool is_maximized
= browser_
->window()->IsMaximized();
241 int y
= CenterPosition(close_button_size
.height(), height());
242 close_button_
->SetBounds(next_x
, y
,
243 is_maximized
? width() - next_x
: close_button_size
.width(),
244 is_maximized
? height() - y
: close_button_size
.height());
245 if (show_link_only
) {
246 // Let's hide all the items.
247 std::vector
<DownloadItemView
*>::reverse_iterator ri
;
248 for (ri
= download_views_
.rbegin(); ri
!= download_views_
.rend(); ++ri
)
249 (*ri
)->SetVisible(false);
253 next_x
= kLeftPadding
;
254 std::vector
<DownloadItemView
*>::reverse_iterator ri
;
255 for (ri
= download_views_
.rbegin(); ri
!= download_views_
.rend(); ++ri
) {
256 gfx::Size view_size
= (*ri
)->GetPreferredSize();
260 // Figure out width of item.
261 int item_width
= view_size
.width();
262 if (new_item_animation_
->is_animating() && ri
== download_views_
.rbegin()) {
263 item_width
= static_cast<int>(static_cast<double>(view_size
.width()) *
264 new_item_animation_
->GetCurrentValue());
267 next_x
+= item_width
;
269 // Make sure our item can be contained within the shelf.
270 if (next_x
< max_download_x
) {
271 (*ri
)->SetVisible(true);
272 (*ri
)->SetBounds(x
, CenterPosition(view_size
.height(), height()),
273 item_width
, view_size
.height());
275 (*ri
)->SetVisible(false);
280 void DownloadShelfView::ViewHierarchyChanged(
281 const ViewHierarchyChangedDetails
& details
) {
282 View::ViewHierarchyChanged(details
);
284 if (details
.is_add
&& (details
.child
== this)) {
285 ui::ResourceBundle
& rb
= ui::ResourceBundle::GetSharedInstance();
286 arrow_image_
= new views::ImageView();
287 arrow_image_
->SetImage(rb
.GetImageSkiaNamed(IDR_DOWNLOADS_FAVICON
));
288 AddChildView(arrow_image_
);
290 show_all_view_
= new views::Link(
291 l10n_util::GetStringUTF16(IDS_SHOW_ALL_DOWNLOADS
));
292 show_all_view_
->set_listener(this);
293 AddChildView(show_all_view_
);
295 close_button_
= new views::ImageButton(this);
296 close_button_
->SetImage(views::CustomButton::STATE_NORMAL
,
297 rb
.GetImageSkiaNamed(IDR_CLOSE_1
));
298 close_button_
->SetImage(views::CustomButton::STATE_HOVERED
,
299 rb
.GetImageSkiaNamed(IDR_CLOSE_1_H
));
300 close_button_
->SetImage(views::CustomButton::STATE_PRESSED
,
301 rb
.GetImageSkiaNamed(IDR_CLOSE_1_P
));
302 close_button_
->SetAccessibleName(
303 l10n_util::GetStringUTF16(IDS_ACCNAME_CLOSE
));
304 AddChildView(close_button_
);
306 UpdateColorsFromTheme();
308 new_item_animation_
.reset(new gfx::SlideAnimation(this));
309 new_item_animation_
->SetSlideDuration(kNewItemAnimationDurationMs
);
311 shelf_animation_
.reset(new gfx::SlideAnimation(this));
312 shelf_animation_
->SetSlideDuration(kShelfAnimationDurationMs
);
316 bool DownloadShelfView::CanFitFirstDownloadItem() {
317 if (download_views_
.empty())
320 gfx::Size image_size
= arrow_image_
->GetPreferredSize();
321 gfx::Size close_button_size
= close_button_
->GetPreferredSize();
322 gfx::Size show_all_size
= show_all_view_
->GetPreferredSize();
324 // Let's compute the width available for download items, which is the width
325 // of the shelf minus the "Show all downloads" link, arrow and close button
327 int available_width
= width() - kRightPadding
- close_button_size
.width() -
328 kCloseAndLinkPadding
- show_all_size
.width() - kDownloadsTitlePadding
-
329 image_size
.width() - kDownloadPadding
- kLeftPadding
;
330 if (available_width
<= 0)
333 gfx::Size item_size
= (*download_views_
.rbegin())->GetPreferredSize();
334 return item_size
.width() < available_width
;
337 void DownloadShelfView::UpdateColorsFromTheme() {
338 if (show_all_view_
&& close_button_
&& GetThemeProvider()) {
339 ui::ResourceBundle
& rb
= ui::ResourceBundle::GetSharedInstance();
340 set_background(views::Background::CreateSolidBackground(
341 GetThemeProvider()->GetColor(ThemeProperties::COLOR_TOOLBAR
)));
342 show_all_view_
->SetBackgroundColor(background()->get_color());
343 show_all_view_
->SetEnabledColor(
344 GetThemeProvider()->GetColor(ThemeProperties::COLOR_BOOKMARK_TEXT
));
345 close_button_
->SetBackground(
346 GetThemeProvider()->GetColor(ThemeProperties::COLOR_TAB_TEXT
),
347 rb
.GetImageSkiaNamed(IDR_CLOSE_1
),
348 rb
.GetImageSkiaNamed(IDR_CLOSE_1_MASK
));
352 void DownloadShelfView::OnThemeChanged() {
353 UpdateColorsFromTheme();
356 void DownloadShelfView::LinkClicked(views::Link
* source
, int event_flags
) {
357 chrome::ShowDownloads(browser_
);
360 void DownloadShelfView::ButtonPressed(
361 views::Button
* button
, const ui::Event
& event
) {
365 bool DownloadShelfView::IsShowing() const {
366 return shelf_animation_
->IsShowing();
369 bool DownloadShelfView::IsClosing() const {
370 return shelf_animation_
->IsClosing();
373 void DownloadShelfView::DoShow() {
374 shelf_animation_
->Show();
377 void DownloadShelfView::DoClose(CloseReason reason
) {
378 int num_in_progress
= 0;
379 for (size_t i
= 0; i
< download_views_
.size(); ++i
) {
380 if (download_views_
[i
]->download()->GetState() == DownloadItem::IN_PROGRESS
)
383 RecordDownloadShelfClose(
384 download_views_
.size(), num_in_progress
, reason
== AUTOMATIC
);
385 parent_
->SetDownloadShelfVisible(false);
386 shelf_animation_
->Hide();
389 Browser
* DownloadShelfView::browser() const {
393 void DownloadShelfView::Closed() {
394 // Don't remove completed downloads if the shelf is just being auto-hidden
395 // rather than explicitly closed by the user.
398 // When the close animation is complete, remove all completed downloads.
400 while (i
< download_views_
.size()) {
401 DownloadItem
* download
= download_views_
[i
]->download();
402 DownloadItem::DownloadState state
= download
->GetState();
403 bool is_transfer_done
= state
== DownloadItem::COMPLETE
||
404 state
== DownloadItem::CANCELLED
||
405 state
== DownloadItem::INTERRUPTED
;
406 if (is_transfer_done
&& !download
->IsDangerous()) {
407 RemoveDownloadView(download_views_
[i
]);
409 // Treat the item as opened when we close. This way if we get shown again
410 // the user need not open this item for the shelf to auto-close.
411 download
->SetOpened(true);
417 bool DownloadShelfView::CanAutoClose() {
418 for (size_t i
= 0; i
< download_views_
.size(); ++i
) {
419 if (!download_views_
[i
]->download()->GetOpened())