blob: 13c032448cc7bf0fb8cedcd14affdae129604a1d [file] [log] [blame]
// Copyright 2017 The Chromium Authors
// 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/controls/hover_button.h"
#include <algorithm>
#include <string_view>
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/ui/color/chrome_color_id.h"
#include "chrome/browser/ui/views/chrome_typography.h"
#include "chrome/browser/ui/views/hover_button_controller.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/models/image_model.h"
#include "ui/color/color_id.h"
#include "ui/compositor/layer.h"
#include "ui/events/event_constants.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/animation/ink_drop.h"
#include "ui/views/animation/ink_drop_impl.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/controls/button/menu_button_controller.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/focus/focus_manager.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/flex_layout.h"
#include "ui/views/style/typography.h"
#include "ui/views/style/typography_provider.h"
#include "ui/views/view_class_properties.h"
namespace {
std::unique_ptr<views::Border> CreateBorderWithVerticalSpacing(
int vertical_spacing) {
const int horizontal_spacing = ChromeLayoutProvider::Get()->GetDistanceMetric(
views::DISTANCE_BUTTON_HORIZONTAL_PADDING);
return views::CreateEmptyBorder(
gfx::Insets::VH(vertical_spacing, horizontal_spacing));
}
int GetVerticalSpacing() {
return ChromeLayoutProvider::Get()->GetDistanceMetric(
views::DISTANCE_CONTROL_LIST_VERTICAL) /
2;
}
// Wrapper class for the icon that maintains consistent spacing for both badged
// and unbadged icons.
// Badging may make the icon slightly wider (but not taller). However, the
// layout should be the same whether or not the icon is badged, so allow the
// badged part of the icon to extend into the padding.
class IconWrapper : public views::View {
METADATA_HEADER(IconWrapper, views::View)
public:
explicit IconWrapper(std::unique_ptr<views::View> icon,
int vertical_spacing,
int icon_label_spacing)
: icon_(AddChildView(std::move(icon))),
icon_label_spacing_(icon_label_spacing) {
SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kHorizontal));
// Make sure hovering over the icon also hovers the |HoverButton|.
SetCanProcessEventsWithinSubtree(false);
// Don't cover |icon| when the ink drops are being painted.
// |MenuButton| already does this with its own image.
SetPaintToLayer();
layer()->SetFillsBoundsOpaquely(false);
SetProperty(views::kMarginsKey, gfx::Insets::VH(vertical_spacing, 0));
}
// views::View:
gfx::Size CalculatePreferredSize(
const views::SizeBounds& available_size) const override {
const gfx::Size icon_size = icon_->GetPreferredSize(available_size);
return gfx::Size(icon_size.width() + icon_label_spacing_,
icon_size.height());
}
views::View* icon() { return icon_; }
private:
raw_ptr<views::View> icon_;
int icon_label_spacing_;
};
BEGIN_METADATA(IconWrapper)
END_METADATA
} // namespace
HoverButton::HoverButton()
: views::LabelButton(base::BindRepeating(&HoverButton::OnPressed,
base::Unretained(this))) {
SetButtonController(std::make_unique<HoverButtonController>(
this,
std::make_unique<views::Button::DefaultButtonControllerDelegate>(this)));
views::InstallRectHighlightPathGenerator(this);
SetInstallFocusRingOnFocus(false);
SetFocusBehavior(FocusBehavior::ALWAYS);
SetBorder(CreateBorderWithVerticalSpacing(GetVerticalSpacing()));
views::InkDrop::Get(this)->SetMode(views::InkDropHost::InkDropMode::ON);
views::InkDrop::UseInkDropForFloodFillRipple(views::InkDrop::Get(this),
/*highlight_on_hover=*/false,
/*highlight_on_focus=*/true);
views::InkDrop::Get(this)->SetBaseColor(kColorHoverButtonBackgroundHovered);
// kColorHoverButtonBackgroundHovered has its own opacity.
// sets the opacity to 100% * opacity(kColorHoverButtonBackgroundHovered).
views::InkDrop::Get(this)->SetVisibleOpacity(1.0f);
views::InkDrop::Get(this)->SetHighlightOpacity(1.0f);
SetTriggerableEventFlags(ui::EF_LEFT_MOUSE_BUTTON |
ui::EF_RIGHT_MOUSE_BUTTON);
button_controller()->set_notify_action(
views::ButtonController::NotifyAction::kOnRelease);
}
HoverButton::HoverButton(PressedCallback callback, const std::u16string& text)
: HoverButton() {
SetCallback(std::move(callback));
SetText(text);
}
HoverButton::HoverButton(PressedCallback callback,
const ui::ImageModel& icon,
const std::u16string& text)
: HoverButton(std::move(callback), text) {
SetImageModel(STATE_NORMAL, icon);
}
HoverButton::HoverButton(PressedCallback callback,
std::unique_ptr<views::View> icon_view,
const std::u16string& title,
const std::u16string& subtitle,
std::unique_ptr<views::View> secondary_view,
bool add_vertical_label_spacing,
const std::u16string& footer,
int icon_label_spacing,
bool multiline_subtitle)
: HoverButton(std::move(callback), std::u16string()) {
label()->SetHandlesTooltips(false);
// Set the layout manager to ignore the ink_drop_container to ensure the ink
// drop tracks the bounds of its parent.
ink_drop_container()->SetProperty(views::kViewIgnoredByLayoutKey, true);
SetLayoutManager(std::make_unique<views::FlexLayout>())
->SetCrossAxisAlignment(views::LayoutAlignment::kCenter);
// The vertical space that must exist on the top and the bottom of the item
// to ensure the proper spacing is maintained between items when stacking
// vertically.
const int vertical_spacing = GetVerticalSpacing();
if (icon_view) {
icon_wrapper_ = AddChildView(std::make_unique<IconWrapper>(
std::move(icon_view), vertical_spacing, icon_label_spacing));
icon_view_ = static_cast<IconWrapper*>(icon_wrapper_)->icon();
}
// |label_wrapper| will hold the title as well as subtitle and footer, if
// present.
auto label_wrapper = std::make_unique<views::View>();
title_ = label_wrapper->AddChildView(std::make_unique<views::Label>());
title_->SetText(title);
title_->SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_LEFT);
// Hover the whole button when hovering |title_|. This is OK because |title_|
// will never have a link in it.
title_->SetCanProcessEventsWithinSubtree(false);
// A title text update may result in the same label size and not trigger any
// observers. Thus, we need to add a callback that updates tooltip and
// accessible name when title text changes.
text_changed_subscriptions_.push_back(title_->AddTextChangedCallback(
base::BindRepeating(&HoverButton::UpdateTooltipAndAccessibleName,
base::Unretained(this))));
if (!subtitle.empty()) {
std::unique_ptr<views::Label> subtitle_label =
CreateSecondaryLabel(subtitle);
subtitle_label->SetMultiLine(multiline_subtitle);
subtitle_ = label_wrapper->AddChildView(std::move(subtitle_label));
}
if (!footer.empty()) {
std::unique_ptr<views::Label> footer_label = CreateSecondaryLabel(footer);
footer_ = label_wrapper->AddChildView(std::move(footer_label));
}
label_wrapper->SetLayoutManager(std::make_unique<views::FlexLayout>())
->SetOrientation(views::LayoutOrientation::kVertical)
.SetMainAxisAlignment(views::LayoutAlignment::kCenter);
label_wrapper->SetProperty(
views::kFlexBehaviorKey,
views::FlexSpecification(views::LayoutOrientation::kHorizontal,
views::MinimumFlexSizeRule::kScaleToZero,
views::MaximumFlexSizeRule::kUnbounded, true));
label_wrapper->SetCanProcessEventsWithinSubtree(false);
label_wrapper->SetProperty(
views::kMarginsKey,
gfx::Insets::VH(add_vertical_label_spacing ? vertical_spacing : 0, 0));
label_wrapper_ = AddChildView(std::move(label_wrapper));
// Observe |label_wrapper_| bounds changes to ensure the HoverButton tooltip
// is kept in sync with the size.
label_observation_.Observe(label_wrapper_.get());
if (secondary_view) {
secondary_view->SetCanProcessEventsWithinSubtree(false);
// |secondary_view| needs a layer otherwise it's obscured by the layer
// used in drawing ink drops.
secondary_view->SetPaintToLayer();
secondary_view->layer()->SetFillsBoundsOpaquely(false);
const int secondary_icon_label_spacing = icon_label_spacing;
// Set vertical margins such that the vertical distance between HoverButtons
// is maintained.
secondary_view->SetProperty(
views::kMarginsKey,
gfx::Insets::TLBR(vertical_spacing, secondary_icon_label_spacing,
vertical_spacing, 0));
secondary_view_ = AddChildView(std::move(secondary_view));
}
// Create the appropriate border with no vertical insets. The required spacing
// will be met via margins set on the containing views.
SetBorder(CreateBorderWithVerticalSpacing(0));
}
HoverButton::~HoverButton() = default;
void HoverButton::SetCallback(PressedCallback callback) {
// TODO(pkasting): Why does HoverButton have its own callback -- to disable
// special handling of the label? Can we remove this member and override?
callback_ = std::move(callback);
}
gfx::Size HoverButton::CalculatePreferredSize(
const views::SizeBounds& available_size) const {
if (label_wrapper_) {
return GetLayoutManager()->GetPreferredSize(this, available_size);
}
return views::LabelButton::CalculatePreferredSize(available_size);
}
void HoverButton::SetBorder(std::unique_ptr<views::Border> b) {
LabelButton::SetBorder(std::move(b));
PreferredSizeChanged();
}
void HoverButton::PreferredSizeChanged() {
LabelButton::PreferredSizeChanged();
if (GetLayoutManager()) {
SetMinSize(GetLayoutManager()->GetPreferredSize(this));
}
}
void HoverButton::OnViewBoundsChanged(View* observed_view) {
LabelButton::OnViewBoundsChanged(observed_view);
if (observed_view == label_wrapper_) {
UpdateTooltipAndAccessibleName();
}
}
void HoverButton::SetTitleTextStyle(views::style::TextStyle text_style,
SkColor background_color,
std::optional<ui::ColorId> color_id) {
if (!title()) {
return;
}
title_->SetTextStyle(text_style);
title_->SetBackgroundColor(background_color);
if (color_id) {
title_->SetEnabledColor(color_id.value());
}
}
void HoverButton::SetSubtitleTextStyle(int text_context,
views::style::TextStyle text_style) {
if (!subtitle()) {
return;
}
subtitle()->SetTextContext(text_context);
subtitle()->SetTextStyle(text_style);
subtitle()->SetAutoColorReadabilityEnabled(true);
// `subtitle_`'s preferred size may have changed. Notify the view because
// `subtitle_` is an indirect child and thus
// HoverButton::ChildPreferredSizeChanged() is not called.
PreferredSizeChanged();
}
void HoverButton::SetFooterTextStyle(int text_content,
views::style::TextStyle text_style) {
if (!footer()) {
return;
}
footer()->SetTextContext(text_content);
footer()->SetTextStyle(text_style);
footer()->SetAutoColorReadabilityEnabled(true);
// `footer_`'s preferred size may have changed. Notify the view because
// `footer_` is an indirect child and thus
// HoverButton::ChildPreferredSizeChanged() is not called.
PreferredSizeChanged();
}
void HoverButton::AddExtraAccessibleText(const std::u16string& text) {
additional_accessible_text_ = text;
}
void HoverButton::SetIconHorizontalMargins(int left, int right) {
int vertical_spacing = GetVerticalSpacing();
icon_wrapper_->SetProperty(
views::kMarginsKey,
gfx::Insets::TLBR(vertical_spacing, left, vertical_spacing, right));
}
void HoverButton::UpdateTooltipAndAccessibleName() {
std::vector<std::u16string_view> texts = {title_->GetText()};
if (subtitle_) {
texts.push_back(subtitle_->GetText());
}
if (footer_) {
texts.push_back(footer_->GetText());
}
if (!additional_accessible_text_.empty()) {
texts.push_back(additional_accessible_text_);
}
const std::u16string accessible_name = base::JoinString(texts, u"\n");
// Only use a tooltip if the available space is smaller than its preferred
// size.
const bool needs_tooltip =
label_wrapper_->GetPreferredSize().width() > label_wrapper_->width();
SetTooltipText(needs_tooltip ? accessible_name : std::u16string());
GetViewAccessibility().SetName(accessible_name);
}
views::Button::KeyClickAction HoverButton::GetKeyClickActionForEvent(
const ui::KeyEvent& event) {
if (event.key_code() == ui::VKEY_RETURN) {
// As the hover button is presented in the user menu, it triggers a
// kOnKeyPress action every time the user clicks on enter on all platforms.
// (it ignores the value of PlatformStyle::kReturnClicksFocusedControl)
return KeyClickAction::kOnKeyPress;
}
return LabelButton::GetKeyClickActionForEvent(event);
}
void HoverButton::StateChanged(ButtonState old_state) {
LabelButton::StateChanged(old_state);
// |HoverButtons| are designed for use in a list, so ensure only one button
// can have a hover background at any time by requesting focus on hover.
if (GetState() == STATE_HOVERED && old_state != STATE_PRESSED) {
RequestFocus();
} else if (GetState() == STATE_NORMAL && HasFocus()) {
GetFocusManager()->SetFocusedView(nullptr);
}
}
views::View* HoverButton::GetTooltipHandlerForPoint(const gfx::Point& point) {
if (!HitTestPoint(point)) {
return nullptr;
}
// Let the secondary control handle it if it has a tooltip.
if (secondary_view_) {
gfx::Point point_in_secondary_view(point);
ConvertPointToTarget(this, secondary_view_, &point_in_secondary_view);
View* handler =
secondary_view_->GetTooltipHandlerForPoint(point_in_secondary_view);
if (handler) {
gfx::Point point_in_handler_view(point);
ConvertPointToTarget(this, handler, &point_in_handler_view);
if (!handler->GetRenderedTooltipText(point_in_secondary_view).empty()) {
return handler;
}
}
}
return this;
}
void HoverButton::OnPressed(const ui::Event& event) {
if (callback_) {
callback_.Run(event);
}
}
std::unique_ptr<views::Label> HoverButton::CreateSecondaryLabel(
const std::u16string& text) {
auto label = std::make_unique<views::Label>(
text, views::style::CONTEXT_BUTTON, views::style::STYLE_SECONDARY);
label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
label->SetAutoColorReadabilityEnabled(false);
// A subtitle text update may result in the same label size and not trigger
// any observers. Thus, we need to add a callback that updates tooltip and
// accessible name when subtitle text changes.
text_changed_subscriptions_.push_back(label->AddTextChangedCallback(
base::BindRepeating(&HoverButton::UpdateTooltipAndAccessibleName,
base::Unretained(this))));
return label;
}
BEGIN_METADATA(HoverButton)
END_METADATA