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