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 "chrome/grit/generated_resources.h" 21 #include "content/public/browser/download_item.h" 22 #include "content/public/browser/download_manager.h" 23 #include "content/public/browser/page_navigator.h" 24 #include "grit/theme_resources.h" 25 #include "ui/base/l10n/l10n_util.h" 26 #include "ui/base/resource/resource_bundle.h" 27 #include "ui/base/theme_provider.h" 28 #include "ui/gfx/animation/slide_animation.h" 29 #include "ui/gfx/canvas.h" 30 #include "ui/resources/grit/ui_resources.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(); 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 88 int CenterPosition(int size, int target_size) { 89 return std::max((target_size - size) / 2, kTopBottomPadding); 90 } 91 92 } // namespace 93 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 108 DownloadShelfView::~DownloadShelfView() { 109 parent_->RemoveChildView(this); 110 } 111 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 129 void DownloadShelfView::DoAddDownload(DownloadItem* download) { 130 DownloadItemView* view = new DownloadItemView(download, this); 131 AddDownloadView(view); 132 } 133 134 void DownloadShelfView::MouseMovedOutOfHost() { 135 Close(AUTOMATIC); 136 } 137 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 154 views::View* DownloadShelfView::GetDefaultFocusableChild() { 155 return download_views_.empty() ? 156 static_cast<View*>(show_all_view_) : download_views_.back(); 157 } 158 159 void DownloadShelfView::OnPaintBorder(gfx::Canvas* canvas) { 160 canvas->FillRect(gfx::Rect(0, 0, width(), 1), kBorderColor); 161 } 162 163 void DownloadShelfView::OpenedDownload(DownloadItemView* view) { 164 if (CanAutoClose()) 165 mouse_watcher_.Start(); 166 } 167 168 content::PageNavigator* DownloadShelfView::GetNavigator() { 169 return browser_; 170 } 171 172 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 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 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 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 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 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 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 356 void DownloadShelfView::OnThemeChanged() { 357 UpdateColorsFromTheme(); 358 } 359 360 void DownloadShelfView::LinkClicked(views::Link* source, int event_flags) { 361 chrome::ShowDownloads(browser_); 362 } 363 364 void DownloadShelfView::ButtonPressed( 365 views::Button* button, const ui::Event& event) { 366 Close(USER_ACTION); 367 } 368 369 bool DownloadShelfView::IsShowing() const { 370 return shelf_animation_->IsShowing(); 371 } 372 373 bool DownloadShelfView::IsClosing() const { 374 return shelf_animation_->IsClosing(); 375 } 376 377 void DownloadShelfView::DoShow() { 378 shelf_animation_->Show(); 379 } 380 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 393 Browser* DownloadShelfView::browser() const { 394 return browser_; 395 } 396 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 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