blob: 73dd1fcf31e5b799086ca974dd5b6076591197d8 [file] [log] [blame]
// Copyright (c) 2018 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/tabs/tab_hover_card_bubble_view.h"
#include <algorithm>
#include <memory>
#include "base/containers/mru_cache.h"
#include "base/metrics/field_trial_params.h"
#include "base/metrics/histogram_macros.h"
#include "build/build_config.h"
#include "chrome/app/vector_icons/vector_icons.h"
#include "chrome/browser/themes/theme_properties.h"
#include "chrome/browser/ui/tabs/tab_renderer_data.h"
#include "chrome/browser/ui/tabs/tab_style.h"
#include "chrome/browser/ui/thumbnails/thumbnail_image.h"
#include "chrome/browser/ui/views/chrome_layout_provider.h"
#include "chrome/browser/ui/views/chrome_typography.h"
#include "chrome/browser/ui/views/tabs/tab.h"
#include "chrome/browser/ui/views/tabs/tab_hover_card_controller.h"
#include "chrome/grit/generated_resources.h"
#include "components/url_formatter/url_formatter.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/theme_provider.h"
#include "ui/gfx/animation/linear_animation.h"
#include "ui/gfx/animation/slide_animation.h"
#include "ui/gfx/animation/tween.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/gfx/text_constants.h"
#include "ui/native_theme/native_theme.h"
#include "ui/resources/grit/ui_resources.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/background.h"
#include "ui/views/bubble/bubble_frame_view.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/layout/flex_layout.h"
#include "ui/views/layout/layout_provider.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/widget/widget.h"
#if defined(OS_WIN)
#include "ui/base/win/shell.h"
#endif
namespace {
// Maximum number of lines that a title label occupies.
constexpr int kHoverCardTitleMaxLines = 2;
bool CustomShadowsSupported() {
#if defined(OS_WIN)
return ui::win::IsAeroGlassEnabled();
#else
return true;
#endif
}
std::unique_ptr<views::View> CreateAlertView(const TabAlertState& state) {
auto alert_state_label = std::make_unique<views::Label>(
std::u16string(), views::style::CONTEXT_DIALOG_BODY_TEXT,
views::style::STYLE_PRIMARY);
alert_state_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
alert_state_label->SetMultiLine(true);
alert_state_label->SetVisible(true);
alert_state_label->SetText(chrome::GetTabAlertStateText(state));
return alert_state_label;
}
// Calculates an appropriate size to display a preview image in the hover card.
// For the vast majority of images, the |preferred_size| is used, but extremely
// tall or wide images use the image size instead, centering in the available
// space.
gfx::Size GetPreviewImageSize(gfx::Size preview_size,
gfx::Size preferred_size) {
DCHECK(!preferred_size.IsEmpty());
if (preview_size.IsEmpty())
return preview_size;
const float preview_aspect_ratio =
static_cast<float>(preview_size.width()) / preview_size.height();
const float preferred_aspect_ratio =
static_cast<float>(preferred_size.width()) / preferred_size.height();
const float ratio = preview_aspect_ratio / preferred_aspect_ratio;
// Images between 2/3 and 3/2 of the target aspect ratio use the preferred
// size, stretching the image. Only images outside this range get centered.
// Since this is a corner case most users will never see, the specific cutoffs
// just need to be reasonable and don't need to be precise values (that is,
// there is no "correct" value; if the results are not aesthetic they can be
// tuned).
constexpr float kMinStretchRatio = 0.667f;
constexpr float kMaxStretchRatio = 1.5f;
if (ratio >= kMinStretchRatio && ratio <= kMaxStretchRatio)
return preferred_size;
return preview_size;
}
} // namespace
// This is a label with two tweaks:
// - a solid background color, which can have alpha
// - a function to make the foreground and background color fade away (via
// alpha) to zero as an animation progresses
//
// It is used to overlay the old title and domain values as a hover card slide
// animation happens.
class TabHoverCardBubbleView::FadeLabel : public views::Label {
public:
using Label::Label;
METADATA_HEADER(FadeLabel);
FadeLabel() = default;
~FadeLabel() override = default;
// Sets the fade-out of the label as |percent| in the range [0, 1]. Since
// FadeLabel is designed to mask new text with the old and then fade away, the
// higher the percentage the less opaque the label.
void SetFade(double percent) {
if (percent >= 1.0)
SetText(std::u16string());
const SkAlpha alpha = base::saturated_cast<SkAlpha>(
std::numeric_limits<SkAlpha>::max() * (1.0 - percent));
SetBackgroundColor(SkColorSetA(GetBackgroundColor(), alpha));
SetEnabledColor(SkColorSetA(GetEnabledColor(), alpha));
}
protected:
// views::Label:
void OnPaintBackground(gfx::Canvas* canvas) override {
canvas->DrawColor(GetBackgroundColor());
}
};
BEGIN_METADATA(TabHoverCardBubbleView, FadeLabel, views::Label)
END_METADATA
TabHoverCardBubbleView::TabHoverCardBubbleView(Tab* tab)
: BubbleDialogDelegateView(tab,
views::BubbleBorder::TOP_LEFT,
views::BubbleBorder::STANDARD_SHADOW),
using_rounded_corners_(CustomShadowsSupported()) {
SetButtons(ui::DIALOG_BUTTON_NONE);
// We'll do all of our own layout inside the bubble, so no need to inset this
// view inside the client view.
set_margins(gfx::Insets());
// Set so that when hovering over a tab in a inactive window that window will
// not become active. Setting this to false creates the need to explicitly
// hide the hovercard on press, touch, and keyboard events.
SetCanActivate(false);
#if defined(OS_MAC)
set_accept_events(false);
#endif
// Set so that the tab hover card is not focus traversable when keyboard
// navigating through the tab strip.
set_focus_traversable_from_anchor_view(false);
title_label_ = AddChildView(std::make_unique<views::Label>(
std::u16string(), CONTEXT_TAB_HOVER_CARD_TITLE,
views::style::STYLE_PRIMARY));
title_label_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
title_label_->SetVerticalAlignment(gfx::ALIGN_TOP);
title_label_->SetMultiLine(true);
title_label_->SetMaxLines(kHoverCardTitleMaxLines);
title_fade_label_ = AddChildView(std::make_unique<FadeLabel>(
std::u16string(), CONTEXT_TAB_HOVER_CARD_TITLE,
views::style::STYLE_PRIMARY));
title_fade_label_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
title_fade_label_->SetVerticalAlignment(gfx::ALIGN_TOP);
title_fade_label_->SetMultiLine(true);
title_fade_label_->SetMaxLines(kHoverCardTitleMaxLines);
title_fade_label_->GetViewAccessibility().OverrideIsIgnored(true);
domain_label_ = AddChildView(std::make_unique<views::Label>(
std::u16string(), views::style::CONTEXT_DIALOG_BODY_TEXT,
views::style::STYLE_SECONDARY,
gfx::DirectionalityMode::DIRECTIONALITY_AS_URL));
domain_label_->SetElideBehavior(gfx::ELIDE_MIDDLE);
domain_label_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
domain_label_->SetMultiLine(false);
domain_fade_label_ = AddChildView(std::make_unique<FadeLabel>(
std::u16string(), views::style::CONTEXT_DIALOG_BODY_TEXT,
views::style::STYLE_SECONDARY,
gfx::DirectionalityMode::DIRECTIONALITY_AS_URL));
domain_fade_label_->SetElideBehavior(gfx::ELIDE_MIDDLE);
domain_fade_label_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
domain_fade_label_->SetMultiLine(false);
domain_fade_label_->GetViewAccessibility().OverrideIsIgnored(true);
if (TabHoverCardController::AreHoverCardImagesEnabled()) {
using Alignment = views::ImageView::Alignment;
const gfx::Size preview_size = TabStyle::GetPreviewImageSize();
preview_image_ = AddChildView(std::make_unique<views::ImageView>());
preview_image_->SetVisible(true);
preview_image_->SetHorizontalAlignment(Alignment::kCenter);
preview_image_->SetVerticalAlignment(Alignment::kCenter);
preview_image_->SetImageSize(preview_size);
preview_image_->SetPreferredSize(preview_size);
}
// Set up layout.
views::FlexLayout* const layout =
SetLayoutManager(std::make_unique<views::FlexLayout>());
layout->SetOrientation(views::LayoutOrientation::kVertical);
layout->SetMainAxisAlignment(views::LayoutAlignment::kStart);
layout->SetCrossAxisAlignment(views::LayoutAlignment::kStretch);
layout->SetCollapseMargins(true);
layout->SetChildViewIgnoredByLayout(title_fade_label_, true);
layout->SetChildViewIgnoredByLayout(domain_fade_label_, true);
constexpr int kHorizontalMargin = 18;
constexpr int kVerticalMargin = 10;
gfx::Insets title_margins(kVerticalMargin, kHorizontalMargin);
// In some browser types (e.g. ChromeOS terminal app) we hide the domain
// label. In those cases, we need to adjust the bottom margin of the title
// element because it is no longer above another text element and needs a
// bottom margin.
const bool show_domain = tab->controller()->ShowDomainInHoverCards();
domain_label_->SetVisible(show_domain);
if (show_domain) {
title_margins.set_bottom(0);
domain_label_->SetProperty(
views::kMarginsKey,
gfx::Insets(0, kHorizontalMargin, kVerticalMargin, kHorizontalMargin));
}
title_label_->SetProperty(views::kMarginsKey, title_margins);
title_label_->SetProperty(
views::kFlexBehaviorKey,
views::FlexSpecification(views::MinimumFlexSizeRule::kScaleToMinimum,
views::MaximumFlexSizeRule::kPreferred));
// Set up widget.
views::BubbleDialogDelegateView::CreateBubble(this);
set_adjust_if_offscreen(true);
constexpr int kFootnoteVerticalMargin = 8;
GetBubbleFrameView()->SetFootnoteMargins(
gfx::Insets(kFootnoteVerticalMargin, kHorizontalMargin,
kFootnoteVerticalMargin, kHorizontalMargin));
GetBubbleFrameView()->SetPreferredArrowAdjustment(
views::BubbleFrameView::PreferredArrowAdjustment::kOffset);
GetBubbleFrameView()->set_hit_test_transparent(true);
if (using_rounded_corners_) {
GetBubbleFrameView()->SetCornerRadius(
ChromeLayoutProvider::Get()->GetCornerRadiusMetric(
views::Emphasis::kHigh));
}
// Start in the fully "faded-in" position so that whatever text we initially
// display is visible.
SetTextFade(1.0);
}
TabHoverCardBubbleView::~TabHoverCardBubbleView() = default;
ax::mojom::Role TabHoverCardBubbleView::GetAccessibleWindowRole() {
// Override the role so that hover cards are not read when they appear because
// tabs handle accessibility text.
return ax::mojom::Role::kNone;
}
void TabHoverCardBubbleView::Layout() {
View::Layout();
title_fade_label_->SetBoundsRect(title_label_->bounds());
domain_fade_label_->SetBoundsRect(domain_label_->bounds());
}
void TabHoverCardBubbleView::UpdateCardContent(const Tab* tab) {
// Preview image is never visible for the active tab.
if (preview_image_)
preview_image_->SetVisible(!tab->IsActive());
std::u16string title;
absl::optional<TabAlertState> old_alert_state = alert_state_;
GURL domain_url;
// Use committed URL to determine if no page has yet loaded, since the title
// can be blank for some web pages.
if (tab->data().last_committed_url.is_empty()) {
domain_url = tab->data().visible_url;
title = tab->data().IsCrashed()
? l10n_util::GetStringUTF16(IDS_HOVER_CARD_CRASHED_TITLE)
: l10n_util::GetStringUTF16(IDS_TAB_LOADING_TITLE);
alert_state_ = absl::nullopt;
} else {
domain_url = tab->data().last_committed_url;
title = tab->data().title;
alert_state_ = Tab::GetAlertStateToShow(tab->data().alert_state);
}
std::u16string domain;
if (domain_url.SchemeIsFile()) {
title_label_->SetMultiLine(false);
title_label_->SetElideBehavior(gfx::ELIDE_MIDDLE);
domain = l10n_util::GetStringUTF16(IDS_HOVER_CARD_FILE_URL_SOURCE);
} else {
title_label_->SetElideBehavior(gfx::ELIDE_TAIL);
title_label_->SetMultiLine(true);
if (domain_url.SchemeIsBlob()) {
domain = l10n_util::GetStringUTF16(IDS_HOVER_CARD_BLOB_URL_SOURCE);
} else {
domain = url_formatter::FormatUrl(
domain_url,
url_formatter::kFormatUrlOmitDefaults |
url_formatter::kFormatUrlOmitHTTPS |
url_formatter::kFormatUrlOmitTrivialSubdomains |
url_formatter::kFormatUrlTrimAfterHost,
net::UnescapeRule::NORMAL, nullptr, nullptr, nullptr);
}
}
title_fade_label_->SetText(title_label_->GetText());
title_label_->SetText(title);
if (alert_state_ != old_alert_state) {
GetBubbleFrameView()->SetFootnoteView(
alert_state_.has_value() ? CreateAlertView(*alert_state_) : nullptr);
}
domain_fade_label_->SetText(domain_label_->GetText());
domain_label_->SetText(domain);
// Because we may have changed the card's contents, if the card has yet to be
// shown, ensure that it starts at the correct size.
if (!GetWidget()->IsVisible())
SizeToContents();
}
void TabHoverCardBubbleView::SetTextFade(double percent) {
title_fade_label_->SetFade(percent);
domain_fade_label_->SetFade(percent);
}
void TabHoverCardBubbleView::ClearPreviewImage() {
DCHECK(preview_image_)
<< "This method should only be called when preview images are enabled.";
// This can return null if there is no associated widget, etc. In that case
// there is nothing to render, and we can't get theme default colors to render
// with anyway, so bail out. This should hopefully address crbug.com/1070980
// (Null dereference
const ui::ThemeProvider* const theme_provider = GetThemeProvider();
if (!theme_provider)
return;
// Check the no-preview color and size to see if it needs to be
// regenerated. DPI or theme change can cause a regeneration.
const SkColor foreground_color = theme_provider->GetColor(
ThemeProperties::COLOR_HOVER_CARD_NO_PREVIEW_FOREGROUND);
// Set the no-preview placeholder image. All sizes are in DIPs.
// gfx::CreateVectorIcon() caches its result so there's no need to store
// images here; if a particular size/color combination has already been
// requested it will be low-cost to request it again.
constexpr gfx::Size kNoPreviewImageSize{64, 64};
const gfx::ImageSkia no_preview_image = gfx::CreateVectorIcon(
kGlobeIcon, kNoPreviewImageSize.width(), foreground_color);
preview_image_->SetImage(no_preview_image);
preview_image_->SetImageSize(kNoPreviewImageSize);
preview_image_->SetPreferredSize(TabStyle::GetPreviewImageSize());
// Also possibly regenerate the background if it has changed.
const SkColor background_color = theme_provider->GetColor(
ThemeProperties::COLOR_HOVER_CARD_NO_PREVIEW_BACKGROUND);
if (!preview_image_->background() ||
preview_image_->background()->get_color() != background_color) {
preview_image_->SetBackground(
views::CreateSolidBackground(background_color));
}
}
void TabHoverCardBubbleView::SetPreviewImage(gfx::ImageSkia preview_image) {
DCHECK(preview_image_)
<< "This method should only be called when preview images are enabled.";
const gfx::Size preview_size = TabStyle::GetPreviewImageSize();
preview_image_->SetImage(preview_image);
preview_image_->SetImageSize(
GetPreviewImageSize(preview_image.size(), preview_size));
preview_image_->SetPreferredSize(preview_size);
preview_image_->SetBackground(nullptr);
}
gfx::Size TabHoverCardBubbleView::CalculatePreferredSize() const {
gfx::Size preferred_size = GetLayoutManager()->GetPreferredSize(this);
preferred_size.set_width(TabStyle::GetPreviewImageSize().width());
DCHECK(!preferred_size.IsEmpty());
return preferred_size;
}
void TabHoverCardBubbleView::OnThemeChanged() {
BubbleDialogDelegateView::OnThemeChanged();
// Bubble closes if the theme changes to the point where the border has to be
// regenerated. See crbug.com/1140256
if (using_rounded_corners_ != CustomShadowsSupported()) {
GetWidget()->Close();
return;
}
// Update fade labels' background color to match that of the the original
// label since these child views are ignored by layout.
title_fade_label_->SetBackgroundColor(title_label_->GetBackgroundColor());
domain_fade_label_->SetBackgroundColor(domain_label_->GetBackgroundColor());
}
BEGIN_METADATA(TabHoverCardBubbleView, views::BubbleDialogDelegateView)
END_METADATA