blob: 4ef3578829404ff80cf5604e6a7a28c4492ee36b [file] [log] [blame]
// Copyright 2024 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/toolbar/pinned_action_toolbar_button.h"
#include <string>
#include <type_traits>
#include "base/auto_reset.h"
#include "base/metrics/user_metrics.h"
#include "base/notreached.h"
#include "base/strings/strcat.h"
#include "chrome/app/vector_icons/vector_icons.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser_actions.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
#include "chrome/browser/ui/customize_chrome/side_panel_controller.h"
#include "chrome/browser/ui/layout_constants.h"
#include "chrome/browser/ui/tabs/public/tab_features.h"
#include "chrome/browser/ui/views/event_utils.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/side_panel/side_panel_action_callback.h"
#include "chrome/browser/ui/views/toolbar/pinned_action_toolbar_button_menu_model.h"
#include "chrome/browser/ui/views/toolbar/pinned_toolbar_actions_container.h"
#include "chrome/browser/ui/views/toolbar/pinned_toolbar_actions_container_layout.h"
#include "chrome/browser/ui/views/toolbar/pinned_toolbar_button_status_indicator.h"
#include "chrome/browser/ui/views/toolbar/toolbar_button.h"
#include "chrome/browser/ui/views/toolbar/toolbar_ink_drop_util.h"
#include "chrome/browser/ui/views/toolbar/toolbar_view.h"
#include "chrome/browser/ui/web_applications/app_browser_controller.h"
#include "chrome/grit/generated_resources.h"
#include "ui/actions/action_id.h"
#include "ui/actions/actions.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/models/menu_separator_types.h"
#include "ui/color/color_id.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/button/button.h"
#include "ui/views/controls/button/button_controller.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/view_utils.h"
namespace {
// Width of the status indicator shown across the button.
constexpr int kStatusIndicatorWidth = 14;
// Height of the status indicator shown across the button.
constexpr int kStatusIndicatorHeight = 2;
// Spacing between the button's icon and the status indicator.
constexpr int kStatusIndicatorSpacing = 1;
} // namespace
DEFINE_UI_CLASS_PROPERTY_TYPE(PinnedToolbarActionFlexPriority)
DEFINE_UI_CLASS_PROPERTY_KEY(
std::underlying_type_t<PinnedToolbarActionFlexPriority>,
kToolbarButtonFlexPriorityKey,
std::underlying_type_t<PinnedToolbarActionFlexPriority>(
PinnedToolbarActionFlexPriority::kLow))
PinnedActionToolbarButton::PinnedActionToolbarButton(
Browser* browser,
actions::ActionId action_id,
base::WeakPtr<PinnedToolbarActionsContainer> container)
: ToolbarButton(
PressedCallback(),
std::make_unique<PinnedActionToolbarButtonMenuModel>(browser,
action_id),
nullptr,
false),
browser_(browser),
action_id_(action_id),
container_(container) {
SetProperty(views::kElementIdentifierKey,
kPinnedActionToolbarButtonElementId);
ConfigureInkDropForToolbar(this);
SetHorizontalAlignment(gfx::ALIGN_CENTER);
// Pinned action toolbar buttons have right margin and no left margin.
SetProperty(views::kMarginsKey,
gfx::Insets::TLBR(
0, 0, 0, GetLayoutConstant(TOOLBAR_ICON_DEFAULT_MARGIN)));
set_drag_controller(container_.get());
GetViewAccessibility().SetDescription(
std::u16string(), ax::mojom::DescriptionFrom::kAttributeExplicitlyEmpty);
// Normally, the notify action is determined by whether a view is draggable
// (and is set to press for non-draggable and release for draggable views).
// However, PinnedActionToolbarButton may be draggable or non-draggable
// depending on whether they are shown in an incognito window or unpinned and
// popped-out. We want to preserve the same trigger event to keep the UX
// (more) consistent. Set all PinnedActionToolbarButton to trigger on mouse
// release.
button_controller()->set_notify_action(
views::ButtonController::NotifyAction::kOnRelease);
// Do not flip the icon for RTL languages.
SetFlipCanvasOnPaintForRTLUI(false);
action_count_changed_subscription_ = AddAnchorCountChangedCallback(
base::BindRepeating(&PinnedActionToolbarButton::OnAnchorCountChanged,
base::Unretained(this)));
status_indicator_ =
PinnedToolbarButtonStatusIndicator::Install(image_container_view());
status_indicator_->SetColorId(kColorToolbarActionItemEngaged,
kColorToolbarButtonIconInactive);
// TODO(shibalik): Revisit since all pinned actions should not be toggle
// buttons.
GetViewAccessibility().SetRole(ax::mojom::Role::kToggleButton);
GetViewAccessibility().SetCheckedState(ax::mojom::CheckedState::kFalse);
if (web_app::AppBrowserController::IsWebApp(browser_)) {
SetLayoutInsets(gfx::Insets());
SetHorizontalAlignment(gfx::ALIGN_CENTER);
SetAppearDisabledInInactiveWidget(true);
}
}
PinnedActionToolbarButton::~PinnedActionToolbarButton() {
action_count_changed_subscription_ = {};
}
bool PinnedActionToolbarButton::IsActive() {
return anchor_higlight_.has_value();
}
base::AutoReset<bool> PinnedActionToolbarButton::SetNeedsDelayedDestruction(
bool needs_delayed_destruction) {
return base::AutoReset<bool>(&needs_delayed_destruction_,
needs_delayed_destruction);
}
void PinnedActionToolbarButton::SetIconVisibility(bool is_visible) {
is_icon_visible_ = is_visible;
NotifyViewControllerCallback();
}
void PinnedActionToolbarButton::AddHighlight() {
anchor_higlight_ = AddAnchorHighlight();
GetViewAccessibility().SetCheckedState(ax::mojom::CheckedState::kTrue);
}
void PinnedActionToolbarButton::ResetHighlight() {
anchor_higlight_.reset();
GetViewAccessibility().SetCheckedState(ax::mojom::CheckedState::kFalse);
}
void PinnedActionToolbarButton::SetPinned(bool pinned) {
if (pinned_ == pinned) {
return;
}
pinned_ = pinned;
NotifyViewControllerCallback();
}
bool PinnedActionToolbarButton::OnKeyPressed(const ui::KeyEvent& event) {
std::optional<event_utils::ReorderDirection> reorder_direction =
event_utils::GetReorderCommandForKeyboardEvent(event);
if (reorder_direction && pinned_ && browser_->profile()->IsRegularProfile()) {
int move_by = 0;
switch (*reorder_direction) {
case event_utils::ReorderDirection::kPrevious:
move_by = -1;
break;
case event_utils::ReorderDirection::kNext:
move_by = 1;
break;
}
container_->MovePinnedActionBy(action_id_, move_by);
return true;
}
return ToolbarButton::OnKeyPressed(event);
}
gfx::Size PinnedActionToolbarButton::CalculatePreferredSize(
const views::SizeBounds& available_size) const {
// This makes sure the buttons are at least the toolbar button sized width.
// The preferred size might be smaller when the button's icon is removed
// during drag/drop.
if (!container_) {
// Want to avoid this ever getting called during teardown.
return gfx::Size();
}
const gfx::Size toolbar_button_size = container_->GetDefaultButtonSize();
const gfx::Size preferred_size =
ToolbarButton::CalculatePreferredSize(available_size);
return std::max(preferred_size, toolbar_button_size,
[](const gfx::Size s1, const gfx::Size s2) {
return s1.width() < s2.width();
});
}
void PinnedActionToolbarButton::Layout(PassKey) {
LayoutSuperclass<ToolbarButton>(this);
gfx::Rect status_rect(kStatusIndicatorWidth, kStatusIndicatorHeight);
const gfx::Rect image_container_bounds =
image_container_view()->GetLocalBounds();
const int new_x = image_container_bounds.x() +
(image_container_bounds.width() - status_rect.width()) / 2;
const int new_y = image_container_bounds.bottom() + kStatusIndicatorSpacing;
// Set the new origin for status_rect
status_rect.set_origin(gfx::Point(new_x, new_y));
status_indicator_->SetBoundsRect(status_rect);
}
bool PinnedActionToolbarButton::OnMousePressed(const ui::MouseEvent& event) {
skip_execution_ = is_action_showing_bubble_;
return ToolbarButton::OnMousePressed(event);
}
void PinnedActionToolbarButton::OnMouseReleased(const ui::MouseEvent& event) {
if (!skip_execution_) {
ToolbarButton::OnMouseReleased(event);
} else {
OnClickCanceled(event);
}
skip_execution_ = false;
}
void PinnedActionToolbarButton::UpdateIcon() {
const std::optional<VectorIcons>& icons = GetVectorIcons();
// If the button is a cached permanent button the color provider will not be
// available.
if (!icons.has_value() || !GetColorProvider()) {
return;
}
const gfx::VectorIcon& icon = ui::TouchUiController::Get()->touch_ui()
? icons->touch_icon
: icons->icon;
if (is_icon_visible_ && action_engaged_) {
UpdateIconsWithColors(
icon, GetColorProvider()->GetColor(kColorToolbarActionItemEngaged),
GetColorProvider()->GetColor(kColorToolbarActionItemEngaged),
GetColorProvider()->GetColor(kColorToolbarActionItemEngaged),
GetForegroundColor(ButtonState::STATE_DISABLED));
} else {
UpdateIconsWithColors(icon, GetForegroundColor(ButtonState::STATE_NORMAL),
GetForegroundColor(ButtonState::STATE_HOVERED),
GetForegroundColor(ButtonState::STATE_PRESSED),
GetForegroundColor(ButtonState::STATE_DISABLED));
}
}
bool PinnedActionToolbarButton::ShouldShowEphemerallyInToolbar() {
return should_show_in_toolbar_ || has_anchor_;
}
void PinnedActionToolbarButton::SetActionEngaged(bool action_engaged) {
if (!IsActive()) {
SetProperty(
kToolbarButtonFlexPriorityKey,
action_engaged
? static_cast<
std::underlying_type_t<PinnedToolbarActionFlexPriority>>(
PinnedToolbarActionFlexPriority::kMedium)
: static_cast<
std::underlying_type_t<PinnedToolbarActionFlexPriority>>(
PinnedToolbarActionFlexPriority::kLow));
InvalidateLayout();
}
action_engaged_ = action_engaged;
}
void PinnedActionToolbarButton::HideStatusIndicator() {
status_indicator_->Hide();
}
void PinnedActionToolbarButton::UpdateStatusIndicator() {
if (action_engaged_ && is_icon_visible_) {
status_indicator_->Show();
} else {
status_indicator_->Hide();
}
}
void PinnedActionToolbarButton::OnAnchorCountChanged(size_t anchor_count) {
// If there is something anchored to the button we want to make sure the
// button will be visible in the toolbar in cases where the window might be
// small enough that icons must overflow. Update the
// kToolbarButtonFlexPriorityKey to make sure icons are forced visible or able
// to overflow.
if (anchor_count > 0) {
SetProperty(
kToolbarButtonFlexPriorityKey,
static_cast<std::underlying_type_t<PinnedToolbarActionFlexPriority>>(
PinnedToolbarActionFlexPriority::kHigh));
InvalidateLayout();
has_anchor_ = true;
} else {
SetProperty(
kToolbarButtonFlexPriorityKey,
action_engaged_
? static_cast<
std::underlying_type_t<PinnedToolbarActionFlexPriority>>(
PinnedToolbarActionFlexPriority::kMedium)
: static_cast<
std::underlying_type_t<PinnedToolbarActionFlexPriority>>(
PinnedToolbarActionFlexPriority::kLow));
InvalidateLayout();
has_anchor_ = false;
container_->MaybeRemovePoppedOutButtonFor(GetActionId());
}
}
std::unique_ptr<views::ActionViewInterface>
PinnedActionToolbarButton::GetActionViewInterface() {
return std::make_unique<PinnedActionToolbarButtonActionViewInterface>(this);
}
PinnedActionToolbarButtonActionViewInterface::
PinnedActionToolbarButtonActionViewInterface(
PinnedActionToolbarButton* action_view)
: ToolbarButtonActionViewInterface(action_view),
action_view_(action_view) {}
void PinnedActionToolbarButtonActionViewInterface::ActionItemChangedImpl(
actions::ActionItem* action_item) {
ButtonActionViewInterface::ActionItemChangedImpl(action_item);
if (action_view_->IsIconVisible() &&
actions::IsActionItemClass<actions::StatefulImageActionItem>(
action_item)) {
auto* stateful_action_item =
static_cast<actions::StatefulImageActionItem*>(action_item);
if (stateful_action_item->GetStatefulImage().IsVectorIcon()) {
action_view_->SetVectorIcon(*stateful_action_item->GetStatefulImage()
.GetVectorIcon()
.vector_icon());
}
}
// Update whether the action is engaged before updating the view.
action_view_->SetActionEngaged(
action_item->GetProperty(kActionItemUnderlineIndicatorKey));
bool is_pinnable = true;
switch (static_cast<actions::ActionPinnableState>(
action_item->GetProperty(actions::kActionItemPinnableKey))) {
case actions::ActionPinnableState::kNotPinnable:
is_pinnable = false;
break;
case actions::ActionPinnableState::kPinnable:
case actions::ActionPinnableState::kEnterpriseControlled:
is_pinnable = true;
break;
default:
NOTREACHED();
}
if (!is_pinnable && action_view_->IsPinned()) {
action_view_->SetVisible(false);
}
OnViewChangedImpl(action_item);
action_view_->SetIsActionShowingBubble(action_item->GetIsShowingBubble());
}
void PinnedActionToolbarButtonActionViewInterface::InvokeActionImpl(
actions::ActionItem* action_item) {
base::RecordAction(
base::UserMetricsAction("Actions.PinnedToolbarButtonActivation"));
std::optional<actions::ActionId> action_id = action_item->GetActionId();
CHECK(action_id.has_value());
const std::optional<std::string> metrics_name =
actions::ActionIdMap::ActionIdToString(action_id.value());
// ActionIdToStringMappings are not initialized in unit tests, therefore will
// not have a value. In the normal case, `metrics_name` should always have a
// value.
if (metrics_name.has_value()) {
base::RecordComputedAction(base::StrCat(
{"Actions.PinnedToolbarButtonActivation.", metrics_name.value()}));
}
base::AutoReset<bool> needs_delayed_destruction =
action_view_->SetNeedsDelayedDestruction(true);
action_item->InvokeAction(
actions::ActionInvocationContext::Builder()
.SetProperty(
kSidePanelOpenTriggerKey,
static_cast<std::underlying_type_t<SidePanelOpenTrigger>>(
SidePanelOpenTrigger::kPinnedEntryToolbarButton))
.Build());
}
void PinnedActionToolbarButtonActionViewInterface::OnViewChangedImpl(
actions::ActionItem* action_item) {
// Update the button's icon. If the action item is a stateful image action
// item, use the stateful image. Otherwise, use the action item's image.
ui::ImageModel image_model;
if (actions::IsActionItemClass<actions::StatefulImageActionItem>(
action_item)) {
image_model = static_cast<actions::StatefulImageActionItem*>(action_item)
->GetStatefulImage();
} else {
image_model = action_item->GetImage();
}
if (image_model.IsVectorIcon()) {
action_view_->SetVectorIcon(action_view_->IsIconVisible()
? *image_model.GetVectorIcon().vector_icon()
: gfx::VectorIcon::EmptyIcon());
} else {
action_view_->SetImageModel(
views::Button::STATE_NORMAL,
action_view_->IsIconVisible() ? image_model : ui::ImageModel());
}
// Set the accessible name. Fall back to the tooltip if one is not provided.
// If pinned, the pinned state is added to the accessible name.
const std::u16string accessible_name(action_item->GetAccessibleName().empty()
? action_view_->GetTooltipText()
: action_item->GetAccessibleName());
action_view_->GetViewAccessibility().SetName(
action_view_->IsPinned()
? l10n_util::GetStringFUTF16(
IDS_PINNED_ACTION_BUTTON_ACCESSIBLE_TITLE, accessible_name)
: accessible_name);
action_view_->UpdateStatusIndicator();
}
BEGIN_METADATA(PinnedActionToolbarButton)
END_METADATA