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