| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/ui/views/download/download_shelf_view.h" |
| |
| #include <stddef.h> |
| |
| #include <algorithm> |
| #include <utility> |
| |
| #include "base/check.h" |
| #include "base/containers/adapters.h" |
| #include "base/time/time.h" |
| #include "chrome/browser/download/download_ui_model.h" |
| #include "chrome/browser/themes/theme_properties.h" |
| #include "chrome/browser/themes/theme_service.h" |
| #include "chrome/browser/themes/theme_service_factory.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/chrome_pages.h" |
| #include "chrome/browser/ui/view_ids.h" |
| #include "chrome/browser/ui/views/download/download_item_view.h" |
| #include "chrome/browser/ui/views/frame/browser_view.h" |
| #include "chrome/browser/ui/views/toolbar/toolbar_view.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "components/download/public/common/download_item.h" |
| #include "components/strings/grit/components_strings.h" |
| #include "components/vector_icons/vector_icons.h" |
| #include "third_party/abseil-cpp/absl/types/optional.h" |
| #include "ui/accessibility/ax_enums.mojom.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/metadata/metadata_impl_macros.h" |
| #include "ui/base/theme_provider.h" |
| #include "ui/compositor/compositor.h" |
| #include "ui/gfx/animation/animation.h" |
| #include "ui/gfx/animation/tween.h" |
| #include "ui/gfx/canvas.h" |
| #include "ui/gfx/color_utils.h" |
| #include "ui/gfx/geometry/rect.h" |
| #include "ui/gfx/geometry/size.h" |
| #include "ui/gfx/presentation_feedback.h" |
| #include "ui/views/accessibility/view_accessibility.h" |
| #include "ui/views/background.h" |
| #include "ui/views/controls/button/image_button.h" |
| #include "ui/views/controls/button/image_button_factory.h" |
| #include "ui/views/controls/button/md_text_button.h" |
| #include "ui/views/controls/webview/webview.h" |
| #include "ui/views/focus/focus_manager.h" |
| #include "ui/views/view.h" |
| #include "ui/views/widget/widget.h" |
| |
| namespace { |
| |
| // TODO(pkasting): Replace these with LayoutProvider constants |
| |
| // Padding above the content. |
| constexpr int kTopPadding = 1; |
| |
| // Padding from left edge and first download view. |
| constexpr int kStartPadding = 4; |
| |
| // Padding from right edge and close button/show downloads link. |
| constexpr int kEndPadding = 6; |
| |
| // Padding between the show all link and close button. |
| constexpr int kCloseAndLinkPadding = 6; |
| |
| } // namespace |
| |
| DownloadShelfView::DownloadShelfView(Browser* browser, BrowserView* parent) |
| : DownloadShelf(browser, browser->profile()), |
| AnimationDelegateViews(this), |
| parent_(parent) { |
| // Start out hidden: the shelf might be created but never shown in some |
| // cases, like when installing a theme. See DownloadShelf::AddDownload(). |
| SetVisible(false); |
| |
| show_all_view_ = AddChildView(std::make_unique<views::MdTextButton>( |
| base::BindRepeating(&chrome::ShowDownloads, browser), |
| l10n_util::GetStringUTF16(IDS_SHOW_ALL_DOWNLOADS))); |
| show_all_view_->SizeToPreferredSize(); |
| |
| close_button_ = AddChildView(views::CreateVectorImageButton( |
| base::BindRepeating(&DownloadShelf::Close, base::Unretained(this)))); |
| close_button_->SetAccessibleName( |
| l10n_util::GetStringUTF16(IDS_ACCNAME_CLOSE)); |
| close_button_->SizeToPreferredSize(); |
| |
| accessible_alert_ = AddChildView(std::make_unique<views::View>()); |
| |
| if (gfx::Animation::ShouldRenderRichAnimation()) { |
| new_item_animation_.SetSlideDuration( |
| base::TimeDelta::FromMilliseconds(800)); |
| shelf_animation_.SetSlideDuration(base::TimeDelta::FromMilliseconds(120)); |
| } else { |
| new_item_animation_.SetSlideDuration(base::TimeDelta()); |
| shelf_animation_.SetSlideDuration(base::TimeDelta()); |
| } |
| |
| views::ViewAccessibility& accessibility = GetViewAccessibility(); |
| accessibility.OverrideName( |
| l10n_util::GetStringUTF16(IDS_ACCNAME_DOWNLOADS_BAR)); |
| accessibility.OverrideRole(ax::mojom::Role::kGroup); |
| |
| // Delay 5 seconds if the mouse leaves the shelf by way of entering another |
| // window. This is much larger than the normal delay as opening a download is |
| // most likely going to trigger a new window to appear over the button. Delay |
| // a long time so that the user has a chance to quickly close the other app |
| // and return to chrome with the download shelf still open. |
| mouse_watcher_.set_notify_on_exit_time(base::TimeDelta::FromSeconds(5)); |
| SetID(VIEW_ID_DOWNLOAD_SHELF); |
| views::FocusRing::SetBackgroundColorIdForSubtree( |
| this, ThemeProperties::COLOR_TOOLBAR); |
| } |
| |
| DownloadShelfView::~DownloadShelfView() = default; |
| |
| bool DownloadShelfView::IsShowing() const { |
| return GetVisible() && shelf_animation_.IsShowing(); |
| } |
| |
| bool DownloadShelfView::IsClosing() const { |
| return shelf_animation_.IsClosing(); |
| } |
| |
| views::View* DownloadShelfView::GetView() { |
| return this; |
| } |
| |
| gfx::Size DownloadShelfView::CalculatePreferredSize() const { |
| gfx::Size prefsize(kEndPadding + kStartPadding + kCloseAndLinkPadding, 0); |
| |
| // Enlarge the preferred size enough to hold various other views side-by-side. |
| const auto adjust_size = [&prefsize](views::View* view) { |
| const gfx::Size size = view->GetPreferredSize(); |
| prefsize.Enlarge(size.width(), 0); |
| prefsize.set_height(std::max(size.height(), prefsize.height())); |
| }; |
| adjust_size(close_button_); |
| adjust_size(show_all_view_); |
| // Add one download view to the preferred size. |
| if (!download_views_.empty()) |
| adjust_size(download_views_.front()); |
| |
| prefsize.Enlarge(0, kTopPadding); |
| return gfx::Tween::SizeValueBetween(shelf_animation_.GetCurrentValue(), |
| gfx::Size(prefsize.width(), 0), prefsize); |
| } |
| |
| void DownloadShelfView::Layout() { |
| int x = kStartPadding; |
| const int download_items_end = |
| std::max(0, width() - kEndPadding - close_button_->width() - |
| kCloseAndLinkPadding - show_all_view_->width()); |
| const bool all_downloads_hidden = |
| !download_views_.empty() && |
| (download_views_.back()->GetPreferredSize().width() > |
| (download_items_end - x)); |
| |
| const auto center_y = [height = height()](int item_height) { |
| return std::max((height - item_height) / 2, kTopPadding); |
| }; |
| |
| show_all_view_->SetPosition( |
| {// If none of the download items can be shown, move the link to the left |
| // to make it more obvious that there is something to see. |
| all_downloads_hidden ? x : download_items_end, |
| center_y(show_all_view_->height())}); |
| close_button_->SetPosition( |
| {show_all_view_->bounds().right() + kCloseAndLinkPadding, |
| center_y(close_button_->height())}); |
| |
| if (all_downloads_hidden) { |
| for (auto* view : download_views_) |
| view->SetVisible(false); |
| return; |
| } |
| |
| for (auto* view : base::Reversed(download_views_)) { |
| gfx::Size view_size = view->GetPreferredSize(); |
| if (view == download_views_.back()) { |
| view_size = gfx::Tween::SizeValueBetween( |
| new_item_animation_.GetCurrentValue(), |
| gfx::Size(0, view_size.height()), view_size); |
| } |
| |
| const gfx::Rect bounds({x, center_y(view_size.height())}, view_size); |
| view->SetBoundsRect(bounds); |
| view->SetVisible(bounds.right() < download_items_end); |
| |
| x = bounds.right(); |
| } |
| } |
| |
| void DownloadShelfView::AnimationProgressed(const gfx::Animation* animation) { |
| if (animation == &new_item_animation_) { |
| InvalidateLayout(); |
| } else { |
| DCHECK_EQ(&shelf_animation_, animation); |
| // Force a re-layout of the parent, which will call back into |
| // GetPreferredSize(), where we will do our animation. In the case where the |
| // animation is hiding, we do a full resize - the fast resizing would |
| // otherwise leave blank white areas where the shelf was and where the |
| // user's eye is. Thankfully bottom-resizing is a lot faster than |
| // top-resizing. |
| parent_->ToolbarSizeChanged(shelf_animation_.IsShowing()); |
| } |
| } |
| |
| void DownloadShelfView::AnimationEnded(const gfx::Animation* animation) { |
| if (animation != &shelf_animation_) |
| return; |
| |
| const bool shown = shelf_animation_.IsShowing(); |
| parent_->SetDownloadShelfVisible(shown); |
| |
| // If the shelf was explicitly closed by the user, there are further steps to |
| // take to complete closing. |
| if (shown || is_hidden()) |
| return; |
| |
| // Remove all completed downloads. |
| for (size_t i = 0; i < download_views_.size();) { |
| DownloadItemView* const view = download_views_[i]; |
| DownloadUIModel* const model = view->model(); |
| if ((model->GetState() == download::DownloadItem::IN_PROGRESS) || |
| model->IsDangerous()) { |
| // Treat the item as opened when we close. This way if we get shown again |
| // the user need not open this item for the shelf to auto-close. |
| model->SetOpened(true); |
| ++i; |
| } else { |
| RemoveDownloadView(view); |
| } |
| } |
| |
| // Make the shelf non-visible. |
| // |
| // If we had keyboard focus, calling SetVisible(false) will cause keyboard |
| // focus to be completely lost. To prevent this, focus the web contents. |
| // TODO(crbug.com/846466): Fix AccessiblePaneView::SetVisible() or |
| // FocusManager to make this unnecessary. |
| auto* focus_manager = GetFocusManager(); |
| if (focus_manager && Contains(focus_manager->GetFocusedView())) |
| parent_->contents_web_view()->RequestFocus(); |
| SetVisible(false); |
| } |
| |
| void DownloadShelfView::MouseMovedOutOfHost() { |
| Close(); |
| } |
| |
| void DownloadShelfView::AutoClose() { |
| if (std::all_of(download_views_.cbegin(), download_views_.cend(), |
| [](const auto* view) { return view->model()->GetOpened(); })) |
| mouse_watcher_.Start(GetWidget()->GetNativeWindow()); |
| } |
| |
| void DownloadShelfView::RemoveDownloadView(View* view) { |
| DCHECK(view); |
| const auto i = |
| std::find(download_views_.begin(), download_views_.end(), view); |
| DCHECK(i != download_views_.end()); |
| download_views_.erase(i); |
| RemoveChildViewT(view); |
| if (download_views_.empty()) |
| Close(); |
| else |
| AutoClose(); |
| InvalidateLayout(); |
| } |
| |
| void DownloadShelfView::ConfigureButtonForTheme(views::MdTextButton* button) { |
| const auto* const tp = GetThemeProvider(); |
| DCHECK(tp); |
| |
| // If COLOR_DOWNLOAD_SHELF is not customized, just use the default button bg |
| // and text colors. |
| absl::optional<SkColor> bg_color; |
| absl::optional<SkColor> text_color; |
| if (tp->HasCustomColor(ThemeProperties::COLOR_DOWNLOAD_SHELF)) { |
| // For custom themes, we have to make up a background color for the |
| // button. Use a slight tint of the shelf background. |
| bg_color = color_utils::BlendTowardMaxContrast( |
| tp->GetColor(ThemeProperties::COLOR_DOWNLOAD_SHELF), 0x10); |
| // Text color should be set to an appropriate button color over the button |
| // background, COLOR_BOOKMARK_TEXT is currently used as a convenient hack. |
| text_color = tp->GetColor(ThemeProperties::COLOR_BOOKMARK_TEXT); |
| } |
| button->SetBgColorOverride(bg_color); |
| button->SetEnabledTextColors(text_color); |
| } |
| |
| void DownloadShelfView::DoShowDownload( |
| DownloadUIModel::DownloadUIModelPtr download) { |
| const base::TimeTicks show_download_start_time_ticks = base::TimeTicks::Now(); |
| mouse_watcher_.Stop(); |
| |
| const bool was_empty = download_views_.empty(); |
| |
| // Insert the new view as the first child, so the logical child order matches |
| // the visual order. This ensures that tabbing through downloads happens in |
| // the order users would expect. |
| download::DownloadItem* download_item = download->download(); |
| auto view = std::make_unique<DownloadItemView>(std::move(download), this, |
| accessible_alert_); |
| DownloadItemView* download_item_view = AddChildViewAt(std::move(view), 0); |
| download_views_.push_back(download_item_view); |
| |
| // Check download_item is not null, as it can be in some cases. See |
| // DownloadUIModel::download() description. |
| if (download_item) { |
| download_item_view->GetWidget() |
| ->GetCompositor() |
| ->RequestPresentationTimeForNextFrame(base::BindOnce( |
| [](base::TimeTicks start_time_ticks, int download_count, |
| const gfx::PresentationFeedback& feedback) { |
| base::UmaHistogramTimes( |
| download_count > 1 |
| ? "Download.Shelf.Views.NotFirstDownloadPaintTime" |
| : "Download.Shelf.Views.FirstDownloadPaintTime", |
| base::TimeTicks::Now() - start_time_ticks); |
| }, |
| show_download_start_time_ticks, download_views_.size())); |
| } |
| |
| // Max number of download views we'll contain. Any time a view is added and |
| // we already have this many download views, one is removed. |
| // TODO(pkasting): Maybe this should use a min width instead. |
| constexpr size_t kMaxDownloadViews = 15; |
| if (download_views_.size() > kMaxDownloadViews) |
| RemoveDownloadView(download_views_.front()); |
| |
| new_item_animation_.Reset(); |
| new_item_animation_.Show(); |
| |
| if (was_empty && !shelf_animation_.is_animating() && GetVisible()) { |
| // Force a re-layout of the parent to adjust height of shelf properly. |
| parent_->ToolbarSizeChanged(true); |
| } |
| } |
| |
| void DownloadShelfView::DoOpen() { |
| SetVisible(true); |
| shelf_animation_.Show(); |
| } |
| |
| void DownloadShelfView::DoClose() { |
| parent_->SetDownloadShelfVisible(false); |
| shelf_animation_.Hide(); |
| } |
| |
| void DownloadShelfView::DoHide() { |
| SetVisible(false); |
| parent_->ToolbarSizeChanged(false); |
| parent_->SetDownloadShelfVisible(false); |
| } |
| |
| void DownloadShelfView::DoUnhide() { |
| SetVisible(true); |
| parent_->ToolbarSizeChanged(true); |
| parent_->SetDownloadShelfVisible(true); |
| } |
| |
| void DownloadShelfView::OnPaintBorder(gfx::Canvas* canvas) { |
| canvas->FillRect(gfx::Rect(0, 0, width(), 1), |
| GetThemeProvider()->GetColor( |
| ThemeProperties::COLOR_TOOLBAR_CONTENT_AREA_SEPARATOR)); |
| } |
| |
| void DownloadShelfView::OnThemeChanged() { |
| views::AccessiblePaneView::OnThemeChanged(); |
| |
| ConfigureButtonForTheme(show_all_view_); |
| |
| SetBackground(views::CreateSolidBackground( |
| GetThemeProvider()->GetColor(ThemeProperties::COLOR_DOWNLOAD_SHELF))); |
| |
| views::SetImageFromVectorIcon( |
| close_button_, vector_icons::kCloseRoundedIcon, |
| GetThemeProvider()->GetColor(ThemeProperties::COLOR_BOOKMARK_TEXT)); |
| } |
| |
| views::View* DownloadShelfView::GetDefaultFocusableChild() { |
| return download_views_.empty() ? static_cast<views::View*>(show_all_view_) |
| : download_views_.back(); |
| } |
| |
| BEGIN_METADATA(DownloadShelfView, views::AccessiblePaneView) |
| END_METADATA |