blob: b81c7ddc1fc6febeae8f22969bb135d089face57 [file] [log] [blame]
// 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/infobars/infobar_view.h"
#include <algorithm>
#include <memory>
#include <utility>
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/themes/theme_properties.h"
#include "chrome/browser/ui/views/chrome_layout_provider.h"
#include "chrome/browser/ui/views/chrome_typography.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/grit/generated_resources.h"
#include "components/infobars/core/infobar_delegate.h"
#include "components/strings/grit/components_strings.h"
#include "components/vector_icons/vector_icons.h"
#include "third_party/skia/include/effects/SkGradientShader.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/base/class_property.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/theme_provider.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/native_theme/common_theme.h"
#include "ui/native_theme/native_theme.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/label_button_border.h"
#include "ui/views/controls/button/md_text_button.h"
#include "ui/views/controls/button/menu_button.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/link.h"
#include "ui/views/controls/menu/menu_runner.h"
#include "ui/views/view_properties.h"
#include "ui/views/widget/widget.h"
#include "ui/views/window/non_client_view.h"
// Helpers --------------------------------------------------------------------
// Used to mark children that are Labels, so we can update their background
// colors on a theme change.
enum class LabelType {
kNone,
kLabel,
kLink,
};
DEFINE_UI_CLASS_PROPERTY_TYPE(LabelType)
namespace {
DEFINE_UI_CLASS_PROPERTY_KEY(LabelType, kLabelType, LabelType::kNone)
// IDs of the colors to use for infobar elements.
constexpr int kInfoBarLabelBackgroundColor = ThemeProperties::COLOR_INFOBAR;
constexpr int kInfoBarLabelTextColor = ThemeProperties::COLOR_BOOKMARK_TEXT;
bool SortLabelsByDecreasingWidth(views::Label* label_1, views::Label* label_2) {
return label_1->GetPreferredSize().width() >
label_2->GetPreferredSize().width();
}
int GetElementSpacing() {
return ChromeLayoutProvider::Get()->GetDistanceMetric(
DISTANCE_UNRELATED_CONTROL_HORIZONTAL);
}
gfx::Insets GetCloseButtonSpacing() {
auto* provider = ChromeLayoutProvider::Get();
const gfx::Insets vector_button_insets =
provider->GetInsetsMetric(views::INSETS_VECTOR_IMAGE_BUTTON);
return gfx::Insets(
provider->GetDistanceMetric(DISTANCE_TOAST_CONTROL_VERTICAL),
GetElementSpacing()) -
vector_button_insets;
}
} // namespace
// InfoBarView ----------------------------------------------------------------
InfoBarView::InfoBarView(std::unique_ptr<infobars::InfoBarDelegate> delegate)
: infobars::InfoBar(std::move(delegate)),
views::ExternalFocusTracker(this, nullptr) {
set_owned_by_client(); // InfoBar deletes itself at the appropriate time.
// Clip child layers; without this, buttons won't look correct during
// animation.
SetPaintToLayer();
layer()->SetMasksToBounds(true);
gfx::Image image = this->delegate()->GetIcon();
if (!image.IsEmpty()) {
icon_ = new views::ImageView;
icon_->SetImage(image.ToImageSkia());
icon_->SizeToPreferredSize();
icon_->SetProperty(
views::kMarginsKey,
new gfx::Insets(ChromeLayoutProvider::Get()->GetDistanceMetric(
DISTANCE_TOAST_LABEL_VERTICAL),
0));
AddChildView(icon_);
}
close_button_ = views::CreateVectorImageButton(this);
// This is the wrong color, but allows the button's size to be computed
// correctly. We'll reset this with the correct color in OnThemeChanged().
views::SetImageFromVectorIcon(close_button_, vector_icons::kCloseRoundedIcon,
gfx::kPlaceholderColor);
close_button_->SetAccessibleName(
l10n_util::GetStringUTF16(IDS_ACCNAME_CLOSE));
close_button_->SetFocusForPlatform();
gfx::Insets close_button_spacing = GetCloseButtonSpacing();
close_button_->SetProperty(views::kMarginsKey,
new gfx::Insets(close_button_spacing.top(), 0,
close_button_spacing.bottom(), 0));
AddChildView(close_button_);
}
InfoBarView::~InfoBarView() {
// We should have closed any open menus in PlatformSpecificHide(), then
// subclasses' RunMenu() functions should have prevented opening any new ones
// once we became unowned.
DCHECK(!menu_runner_.get());
}
void InfoBarView::RecalculateHeight() {
// Ensure the infobar is tall enough to display its contents.
int height = 0;
for (int i = 0; i < child_count(); ++i) {
View* child = child_at(i);
const gfx::Insets* const margins = child->GetProperty(views::kMarginsKey);
const int margin_height = margins ? margins->height() : 0;
height = std::max(height, child->height() + margin_height);
}
SetTargetHeight(height + GetSeparatorHeightDip());
}
void InfoBarView::Layout() {
const int spacing = GetElementSpacing();
int start_x = 0;
if (icon_) {
icon_->SetPosition(gfx::Point(spacing, OffsetY(icon_)));
start_x = icon_->bounds().right();
}
const int content_minimum_width = ContentMinimumWidth();
if (content_minimum_width > 0)
start_x += spacing + content_minimum_width;
const gfx::Insets close_button_spacing = GetCloseButtonSpacing();
close_button_->SizeToPreferredSize();
close_button_->SetPosition(gfx::Point(
std::max(start_x + close_button_spacing.left(),
width() - close_button_spacing.right() - close_button_->width()),
OffsetY(close_button_)));
// For accessibility reasons, the close button should come last.
DCHECK_EQ(close_button_->parent()->child_count() - 1,
close_button_->parent()->GetIndexOf(close_button_));
}
void InfoBarView::GetAccessibleNodeData(ui::AXNodeData* node_data) {
node_data->SetName(l10n_util::GetStringUTF8(IDS_ACCNAME_INFOBAR));
node_data->role = ax::mojom::Role::kAlert;
node_data->AddStringAttribute(ax::mojom::StringAttribute::kKeyShortcuts,
"Alt+Shift+A");
}
gfx::Size InfoBarView::CalculatePreferredSize() const {
int width = 0;
const int spacing = GetElementSpacing();
if (icon_)
width += spacing + icon_->width();
const int content_width = ContentMinimumWidth();
if (content_width)
width += spacing + content_width;
return gfx::Size(
width + GetCloseButtonSpacing().width() + close_button_->width(),
computed_height());
}
void InfoBarView::ViewHierarchyChanged(
const ViewHierarchyChangedDetails& details) {
View::ViewHierarchyChanged(details);
// Anything that needs to happen once after all subclasses add their children.
if (details.is_add && (details.child == this)) {
ReorderChildView(close_button_, -1);
RecalculateHeight();
}
}
void InfoBarView::OnPaint(gfx::Canvas* canvas) {
views::View::OnPaint(canvas);
if (ShouldDrawSeparator()) {
const SkColor color =
GetColor(ThemeProperties::COLOR_DETACHED_BOOKMARK_BAR_SEPARATOR);
BrowserView::Paint1pxHorizontalLine(canvas, color, GetLocalBounds(), false);
}
}
void InfoBarView::OnThemeChanged() {
const SkColor background_color = GetColor(kInfoBarLabelBackgroundColor);
SetBackground(views::CreateSolidBackground(background_color));
const SkColor text_color = GetColor(kInfoBarLabelTextColor);
views::SetImageFromVectorIcon(close_button_, vector_icons::kCloseRoundedIcon,
text_color);
for (int i = 0; i < child_count(); ++i) {
View* child = child_at(i);
LabelType label_type = child->GetProperty(kLabelType);
if (label_type != LabelType::kNone) {
auto* label = static_cast<views::Label*>(child);
label->SetBackgroundColor(background_color);
if (label_type == LabelType::kLabel)
label->SetEnabledColor(text_color);
}
}
}
void InfoBarView::OnNativeThemeChanged(const ui::NativeTheme* theme) {
// The constructor could not set initial colors correctly, since the
// ThemeProvider wasn't available yet. When this function is called, the view
// has been added to a Widget, so that ThemeProvider is now present.
OnThemeChanged();
// Native theme changes can affect font sizes.
RecalculateHeight();
}
void InfoBarView::ButtonPressed(views::Button* sender,
const ui::Event& event) {
if (!owner())
return; // We're closing; don't call anything, it might access the owner.
if (sender == close_button_) {
delegate()->InfoBarDismissed();
RemoveSelf();
}
}
void InfoBarView::OnWillChangeFocus(View* focused_before, View* focused_now) {
views::ExternalFocusTracker::OnWillChangeFocus(focused_before, focused_now);
// This will trigger some screen readers to read the entire contents of this
// infobar.
if (focused_before && focused_now && !Contains(focused_before) &&
Contains(focused_now)) {
NotifyAccessibilityEvent(ax::mojom::Event::kAlert, true);
}
}
views::Label* InfoBarView::CreateLabel(const base::string16& text) const {
views::Label* label = new views::Label(text, CONTEXT_BODY_TEXT_LARGE);
SetLabelDetails(label);
label->SetEnabledColor(GetColor(kInfoBarLabelTextColor));
label->SetProperty(kLabelType, LabelType::kLabel);
return label;
}
views::Link* InfoBarView::CreateLink(const base::string16& text,
views::LinkListener* listener) const {
views::Link* link = new views::Link(text, CONTEXT_BODY_TEXT_LARGE);
SetLabelDetails(link);
link->set_listener(listener);
link->SetProperty(kLabelType, LabelType::kLink);
return link;
}
// static
void InfoBarView::AssignWidths(Labels* labels, int available_width) {
std::sort(labels->begin(), labels->end(), SortLabelsByDecreasingWidth);
AssignWidthsSorted(labels, available_width);
}
int InfoBarView::ContentMinimumWidth() const {
return 0;
}
int InfoBarView::StartX() const {
// Ensure we don't return a value greater than EndX(), so children can safely
// set something's width to "EndX() - StartX()" without risking that being
// negative.
return std::min((icon_ ? icon_->bounds().right() : 0) + GetElementSpacing(),
EndX());
}
int InfoBarView::EndX() const {
return close_button_->x() - GetCloseButtonSpacing().left();
}
int InfoBarView::OffsetY(views::View* view) const {
return GetSeparatorHeightDip() +
std::max((target_height() - view->height()) / 2, 0) -
(target_height() - height());
}
void InfoBarView::PlatformSpecificShow(bool animate) {
// If we gain focus, we want to restore it to the previously-focused element
// when we're hidden. So when we're in a Widget, create a focus tracker so
// that if we gain focus we'll know what the previously-focused element was.
SetFocusManager(GetFocusManager());
NotifyAccessibilityEvent(ax::mojom::Event::kAlert, true);
}
void InfoBarView::PlatformSpecificHide(bool animate) {
// Cancel any menus we may have open. It doesn't make sense to leave them
// open while we're hidden, and if we're going to become unowned, we can't
// allow the user to choose any options and potentially call functions that
// try to access the owner.
menu_runner_.reset();
// It's possible to be called twice (once with |animate| true and once with it
// false); in this case the second SetFocusManager() call will silently no-op.
SetFocusManager(NULL);
if (!animate)
return;
// Do not restore focus (and active state with it) if some other top-level
// window became active.
views::Widget* widget = GetWidget();
if (!widget || widget->IsActive())
FocusLastFocusedExternalView();
}
void InfoBarView::PlatformSpecificOnHeightRecalculated() {
// Ensure that notifying our container of our size change will result in a
// re-layout.
InvalidateLayout();
}
// static
void InfoBarView::AssignWidthsSorted(Labels* labels, int available_width) {
if (labels->empty())
return;
gfx::Size back_label_size(labels->back()->GetPreferredSize());
back_label_size.set_width(
std::min(back_label_size.width(),
available_width / static_cast<int>(labels->size())));
labels->back()->SetSize(back_label_size);
labels->pop_back();
AssignWidthsSorted(labels, available_width - back_label_size.width());
}
bool InfoBarView::ShouldDrawSeparator() const {
// There will be no parent when this infobar is not in a container, e.g. if
// it's in a background tab. It's still possible to reach here in that case,
// e.g. if ElevationIconSetter triggers a Layout().
return parent() && parent()->GetIndexOf(this) != 0;
}
int InfoBarView::GetSeparatorHeightDip() const {
// We only need a separator for infobars after the first; the topmost infobar
// uses the toolbar as its top separator.
//
// Ideally the separator would take out 1 px in layout, but since we lay out
// in DIPs, we reserve 1 DIP below scale factor 2x, and 0 DIPs at 2 or above.
// This way the padding above the infobar content will never be more than 1 px
// from its ideal value.
//
// This only works because all infobars have padding at the top; if we
// actually draw all the way to the top, we'd risk drawing a separator atop
// some infobar content.
auto scale_factor = [this]() {
auto* widget = GetWidget();
// There may be no widget in tests.
return widget ? widget->GetCompositor()->device_scale_factor() : 1;
};
return (ShouldDrawSeparator() && (scale_factor() < 2)) ? 1 : 0;
}
SkColor InfoBarView::GetColor(int id) const {
const auto* theme_provider = GetThemeProvider();
// When there's no theme provider, this color will never be used; it will be
// reset due to the OnNativeThemeChanged() override.
return theme_provider ? theme_provider->GetColor(id) : gfx::kPlaceholderColor;
}
void InfoBarView::SetLabelDetails(views::Label* label) const {
label->SizeToPreferredSize();
label->SetBackgroundColor(GetColor(kInfoBarLabelBackgroundColor));
label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
label->SetProperty(
views::kMarginsKey,
new gfx::Insets(ChromeLayoutProvider::Get()->GetDistanceMetric(
DISTANCE_TOAST_LABEL_VERTICAL),
0));
}