blob: 0b5c28998441b231a842a8ff9c38cdd5b8a9e8b1 [file] [log] [blame]
// Copyright 2023 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/toolbar_controller.h"
#include <optional>
#include <ranges>
#include <string_view>
#include <variant>
#include <vector>
#include "base/containers/adapters.h"
#include "base/metrics/user_metrics.h"
#include "base/metrics/user_metrics_action.h"
#include "base/no_destructor.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.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/color/chrome_color_id.h"
#include "chrome/browser/ui/toolbar/pinned_toolbar/pinned_toolbar_actions_model.h"
#include "chrome/browser/ui/toolbar_controller_util.h"
#include "chrome/browser/ui/views/side_panel/side_panel_action_callback.h"
#include "chrome/browser/ui/views/side_panel/side_panel_enums.h"
#include "chrome/browser/ui/views/toolbar/overflow_button.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_view.h"
#include "chrome/grit/generated_resources.h"
#include "components/vector_icons/vector_icons.h"
#include "third_party/abseil-cpp/absl/functional/overload.h"
#include "ui/actions/actions.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/mojom/menu_source_type.mojom.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/menus/simple_menu_model.h"
#include "ui/views/controls/menu/menu_item_view.h"
#include "ui/views/controls/menu/menu_model_adapter.h"
#include "ui/views/controls/menu/submenu_view.h"
#include "ui/views/interaction/element_tracker_views.h"
#include "ui/views/layout/flex_layout.h"
#include "ui/views/layout/flex_layout_types.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/view_utils.h"
namespace {
// Status indicator of a menu item.
constexpr gfx::Rect kStatusRect(10, 2);
// Padding between the image container and the status indicator.
constexpr int kImageContainerLowerPadding = 1;
base::flat_map<ui::ElementIdentifier, int> CalculateFlexOrder(
const std::vector<ui::ElementIdentifier>& elements_in_overflow_order,
int element_flex_order_start) {
base::flat_map<ui::ElementIdentifier, int> id_to_order_map;
// Loop in reverse order to ensure the first element gets the largest flex
// order and overflows the first.
for (auto it : base::Reversed(elements_in_overflow_order)) {
id_to_order_map[it] = element_flex_order_start++;
}
return id_to_order_map;
}
} // namespace
ToolbarController::PopOutState::PopOutState() = default;
ToolbarController::PopOutState::~PopOutState() = default;
ToolbarController::PopOutHandler::PopOutHandler(
ToolbarController* controller,
ui::ElementContext context,
ui::ElementIdentifier identifier,
ui::ElementIdentifier observed_identifier)
: controller_(controller),
identifier_(identifier),
observed_identifier_(observed_identifier) {
shown_subscription_ =
ui::ElementTracker::GetElementTracker()->AddElementShownCallback(
observed_identifier_, context,
base::BindRepeating(&PopOutHandler::OnElementShown,
base::Unretained(this)));
hidden_subscription_ =
ui::ElementTracker::GetElementTracker()->AddElementHiddenCallback(
observed_identifier_, context,
base::BindRepeating(&PopOutHandler::OnElementHidden,
base::Unretained(this)));
}
ToolbarController::PopOutHandler::~PopOutHandler() = default;
void ToolbarController::PopOutHandler::OnElementShown(
ui::TrackedElement* element) {
controller_->PopOut(identifier_);
}
void ToolbarController::PopOutHandler::OnElementHidden(
ui::TrackedElement* element) {
controller_->EndPopOut(identifier_);
}
ToolbarController::ElementIdInfo::ElementIdInfo(
ui::ElementIdentifier overflow_identifier,
int menu_text_id,
raw_ptr<const gfx::VectorIcon> menu_icon,
ui::ElementIdentifier activate_identifier,
std::optional<ui::ElementIdentifier> observed_identifier)
: overflow_identifier(overflow_identifier),
menu_text_id(menu_text_id),
menu_icon(menu_icon),
activate_identifier(activate_identifier),
observed_identifier(observed_identifier) {}
ToolbarController::ResponsiveElementInfo::ResponsiveElementInfo(
std::variant<ElementIdInfo, actions::ActionId> overflow_id,
bool is_section_end)
: overflow_id(overflow_id), is_section_end(is_section_end) {}
ToolbarController::ResponsiveElementInfo::ResponsiveElementInfo(
const ResponsiveElementInfo& info) = default;
ToolbarController::ResponsiveElementInfo::~ResponsiveElementInfo() = default;
ToolbarController::ToolbarController(
const std::vector<ToolbarController::ResponsiveElementInfo>&
responsive_elements,
const std::vector<ui::ElementIdentifier>& elements_in_overflow_order,
int element_flex_order_start,
views::View* toolbar_container_view,
OverflowButton* overflow_button,
ToolbarController::PinnedActionsDelegate* pinned_actions_delegate,
PinnedToolbarActionsModel* pinned_actions_model)
: responsive_elements_(responsive_elements),
element_flex_order_start_(element_flex_order_start),
toolbar_container_view_(toolbar_container_view),
overflow_button_(overflow_button),
pinned_actions_delegate_(pinned_actions_delegate),
pinned_actions_model_(pinned_actions_model) {
if (ToolbarControllerUtil::PreventOverflow()) {
return;
}
for (auto& responsive_element : responsive_elements_) {
if (std::holds_alternative<actions::ActionId>(
responsive_element.overflow_id)) {
actions::ActionId action_id =
std::get<actions::ActionId>(responsive_element.overflow_id);
actions::ActionItem* action_item =
pinned_actions_delegate_->GetActionItemFor(action_id);
action_changed_subscription_.push_back(
action_item->AddActionChangedCallback(
base::BindRepeating(&ToolbarController::ActionItemChanged,
base::Unretained(this), action_item)));
}
}
const auto id_to_order_map =
CalculateFlexOrder(elements_in_overflow_order, element_flex_order_start);
for (const auto& element : responsive_elements) {
const auto& overflow_id = element.overflow_id;
std::visit(
absl::Overload{
[](actions::ActionId id) { return; },
[&](ToolbarController::ElementIdInfo id) {
auto* const toolbar_element = FindToolbarElementWithId(
toolbar_container_view_, id.overflow_identifier);
if (!toolbar_element) {
return;
}
views::FlexSpecification* original_spec =
toolbar_element->GetProperty(views::kFlexBehaviorKey);
views::FlexSpecification flex_spec;
if (!original_spec) {
flex_spec = views::FlexSpecification(
views::MinimumFlexSizeRule::kPreferredSnapToZero,
views::MaximumFlexSizeRule::kPreferred);
toolbar_element->SetProperty(views::kFlexBehaviorKey,
flex_spec);
}
flex_spec =
toolbar_element->GetProperty(views::kFlexBehaviorKey)
->WithOrder(id_to_order_map.at(id.overflow_identifier));
toolbar_element->SetProperty(views::kFlexBehaviorKey, flex_spec);
// Create pop out state and pop out handlers to support pop out.
if (id.observed_identifier.has_value()) {
auto state = std::make_unique<PopOutState>();
if (original_spec) {
state->original_spec =
std::optional<views::FlexSpecification>(*original_spec);
}
state->responsive_spec = flex_spec;
state->handler = std::make_unique<PopOutHandler>(
this,
views::ElementTrackerViews::GetContextForView(
toolbar_container_view),
id.overflow_identifier, id.observed_identifier.value());
pop_out_state_[id.overflow_identifier] = std::move(state);
}
}},
overflow_id);
}
responsive_elements_ = GetResponsiveElementsWithOrderedActions();
pinned_actions_model_->AddObserver(this);
}
ToolbarController::~ToolbarController() {
CloseMenu();
pinned_actions_model_->RemoveObserver(this);
}
std::vector<ToolbarController::ResponsiveElementInfo>
ToolbarController::GetDefaultResponsiveElements(Browser* browser) {
bool is_incognito = browser->profile()->IsIncognitoProfile();
// TODO(crbug.com/40912482): Fill in observed identifier.
// Order matters because it should match overflow menu order top to bottom.
std::vector<ToolbarController::ResponsiveElementInfo> elements = {
ToolbarController::ResponsiveElementInfo(
ToolbarController::ElementIdInfo{
kToolbarForwardButtonElementId,
IDS_OVERFLOW_MENU_ITEM_TEXT_FORWARD,
&vector_icons::kForwardArrowChromeRefreshIcon,
kToolbarForwardButtonElementId},
/*is_section_end=*/false),
ToolbarController::ResponsiveElementInfo(
ToolbarController::ElementIdInfo{
kToolbarHomeButtonElementId, IDS_OVERFLOW_MENU_ITEM_TEXT_HOME,
&kNavigateHomeChromeRefreshIcon, kToolbarHomeButtonElementId},
/*is_section_end=*/false)};
// Support actions items.
const auto* const browser_actions = browser->browser_actions();
if (browser_actions) {
auto* root_item = browser_actions->root_action_item();
if (root_item) {
for (const auto& item : root_item->GetChildren().children()) {
auto id = item->GetActionId();
if (item->GetProperty(actions::kActionItemPinnableKey) ==
std::underlying_type_t<actions::ActionPinnableState>(
actions::ActionPinnableState::kPinnable) &&
id.has_value()) {
elements.emplace_back(id.value());
}
}
auto& last_element = elements.back();
if (std::holds_alternative<actions::ActionId>(last_element.overflow_id)) {
last_element.is_section_end = true;
}
}
}
elements.insert(
elements.end(),
{ToolbarController::ResponsiveElementInfo(
ToolbarController::ElementIdInfo(kToolbarChromeLabsButtonElementId,
IDS_OVERFLOW_MENU_ITEM_TEXT_LABS,
&kScienceIcon,
kToolbarChromeLabsButtonElementId,
kToolbarChromeLabsBubbleElementId),
/*is_section_end=*/false),
ToolbarController::ResponsiveElementInfo(
ToolbarController::ElementIdInfo(
kToolbarMediaButtonElementId,
IDS_OVERFLOW_MENU_ITEM_TEXT_MEDIA_CONTROLS,
&kMediaToolbarButtonChromeRefreshIcon,
kToolbarMediaButtonElementId, kToolbarMediaBubbleElementId),
/*is_section_end=*/true),
ToolbarController::ResponsiveElementInfo(
ToolbarController::ElementIdInfo(kToolbarNewTabButtonElementId,
IDS_OVERFLOW_MENU_ITEM_TEXT_NEW_TAB,
#if BUILDFLAG(ENABLE_WEBUI_TAB_STRIP)
&kNewTabToolbarButtonIcon,
#else
nullptr,
#endif // BUILDFLAG(ENABLE_WEBUI_TAB_STRIP)
kToolbarNewTabButtonElementId),
/*is_section_end=*/true),
ToolbarController::ResponsiveElementInfo(
ToolbarController::ElementIdInfo(
kToolbarAvatarButtonElementId,
IDS_OVERFLOW_MENU_ITEM_TEXT_PROFILE,
is_incognito ? (&kIncognitoRefreshMenuIcon)
: (&kUserAccountAvatarRefreshIcon),
kToolbarAvatarButtonElementId, kToolbarAvatarBubbleElementId),
/*is_section_end=*/false)});
return elements;
}
std::vector<ui::ElementIdentifier>
ToolbarController::GetDefaultOverflowOrder() {
return std::vector<ui::ElementIdentifier>(
{kToolbarHomeButtonElementId, kToolbarChromeLabsButtonElementId,
kToolbarMediaButtonElementId, kToolbarNewTabButtonElementId,
kToolbarForwardButtonElementId, kToolbarAvatarButtonElementId});
}
// Every activate identifier should have an action name in order to emit
// metrics. Please update action names in actions.xml to match this map.
std::string ToolbarController::GetActionNameFromElementIdentifier(
std::variant<ui::ElementIdentifier, actions::ActionId> identifier) {
static const base::NoDestructor<base::flat_map<
std::variant<ui::ElementIdentifier, actions::ActionId>, std::string_view>>
identifier_to_action_name_map({
{kToolbarAvatarButtonElementId, "AvatarButton"},
{kToolbarChromeLabsButtonElementId, "ChromeLabsButton"},
{kExtensionsMenuButtonElementId, "ExtensionsMenuButton"},
{kToolbarForwardButtonElementId, "ForwardButton"},
{kToolbarHomeButtonElementId, "HomeButton"},
{kToolbarMediaButtonElementId, "MediaButton"},
{kToolbarNewTabButtonElementId, "NewTabButton"},
{kToolbarSidePanelButtonElementId, "SidePanelButton"},
{kActionClearBrowsingData, "PinnedClearBrowsingDataButton"},
{kActionCopyUrl, "PinnedCopyLinkButton"},
{kActionDevTools, "PinnedDeveloperToolsButton"},
{kActionNewIncognitoWindow, "PinnedNewIncognitoWindowButton"},
{kActionPrint, "PinnedPrintButton"},
{kActionQrCodeGenerator, "PinnedQrCodeGeneratorButton"},
{kActionRouteMedia, "PinnedCastButton"},
{kActionSendTabToSelf, "PinnedSendTabToSelfButton"},
{kActionShowAddressesBubbleOrPage,
"PinnedShowAddressesBubbleOrPageButton"},
{kActionShowChromeLabs, "PinnedShowChromeLabsButton"},
{kActionShowDownloads, "PinnedShowDownloadsButton"},
{kActionShowPasswordsBubbleOrPage,
"PinnedShowPasswordsBubbleOrPageButton"},
{kActionShowPaymentsBubbleOrPage,
"PinnedShowPaymentsBubbleOrPageButton"},
{kActionShowTranslate, "PinnedShowTranslateButton"},
{kActionSidePanelShowBookmarks, "PinnedShowBookmarkSidePanelButton"},
{kActionSidePanelShowReadAnything,
"PinnedShowReadAnythingSidePanelButton"},
{kActionSidePanelShowHistoryCluster,
"PinnedShowHistorySidePanelButton"},
{kActionSidePanelShowReadingList,
"PinnedShowReadingListSidePanelButton"},
{kActionSidePanelShowSearchCompanion,
"PinnedShowSearchCompanionSidePanelButton"},
{kActionTaskManager, "PinnedTaskManagerButton"},
{kActionSidePanelShowLensOverlayResults,
"PinnedShowLensOverlayResultsSidePanelButton"},
{kActionSendSharedTabGroupFeedback, "SharedTabGroupFeedbackButton"},
{kActionTabSearch, "PinnedTabSearchButton"},
{kActionSidePanelShowGlic, "PinnedGlicButton"},
});
const auto it = identifier_to_action_name_map->find(identifier);
return it == identifier_to_action_name_map->end()
? std::string()
: base::StrCat({"ResponsiveToolbar.OverflowMenuItemActivated.",
it->second});
}
bool ToolbarController::PopOut(ui::ElementIdentifier identifier) {
auto* const element =
FindToolbarElementWithId(toolbar_container_view_, identifier);
if (!element) {
LOG(ERROR) << "Cannot find toolbar element id: " << identifier;
return false;
}
const auto it = pop_out_state_.find(identifier);
if (it == pop_out_state_.end()) {
LOG(ERROR) << "Cannot find pop out state for id:" << identifier;
return false;
}
if (it->second->is_popped_out) {
return false;
}
it->second->is_popped_out = true;
auto& original = it->second->original_spec;
if (original.has_value()) {
element->SetProperty(views::kFlexBehaviorKey, original.value());
} else {
element->ClearProperty(views::kFlexBehaviorKey);
}
element->parent()->InvalidateLayout();
return true;
}
bool ToolbarController::EndPopOut(ui::ElementIdentifier identifier) {
auto* const element =
FindToolbarElementWithId(toolbar_container_view_, identifier);
if (!element) {
LOG(ERROR) << "Cannot find toolbar element id: " << identifier;
return false;
}
const auto it = pop_out_state_.find(identifier);
if (it == pop_out_state_.end()) {
LOG(ERROR) << "Cannot find pop out state for id:" << identifier;
return false;
}
if (!it->second->is_popped_out) {
return false;
}
it->second->is_popped_out = false;
element->SetProperty(views::kFlexBehaviorKey, it->second->responsive_spec);
element->parent()->InvalidateLayout();
return true;
}
bool ToolbarController::ShouldShowOverflowButton(gfx::Size available_size) {
if (ToolbarControllerUtil::PreventOverflow()) {
return false;
}
// Once at least one button has been dropped by layout manager show overflow
// button. Be sure to exclude the overflow button from the calculation.
views::ManualLayoutUtil manual_layout_util(
static_cast<views::LayoutManagerBase*>(
toolbar_container_view_->GetLayoutManager()));
const auto exclusion =
manual_layout_util.TemporarilyExcludeFromLayout(overflow_button());
views::ProposedLayout proposed_layout =
static_cast<views::LayoutManagerBase*>(
toolbar_container_view_->GetLayoutManager())
->GetProposedLayout(available_size);
// Check if any buttons should overflow from pinned action delegate given the
// available size.
if (pinned_actions_delegate_) {
if (views::ChildLayout* child_layout = proposed_layout.GetLayoutFor(
pinned_actions_delegate_->GetContainerView())) {
if (pinned_actions_delegate_->ShouldAnyButtonsOverflow(gfx::Size(
child_layout->bounds.width(), child_layout->bounds.height()))) {
return true;
}
}
}
for (const auto& element : responsive_elements_) {
// Skip if it's an ActionId because it's already checked.
if (std::holds_alternative<actions::ActionId>(element.overflow_id)) {
continue;
}
if (IsOverflowed(element, &proposed_layout)) {
return true;
}
}
return false;
}
bool ToolbarController::InOverflowMode() const {
return overflow_button_->GetVisible();
}
std::u16string ToolbarController::GetMenuText(
const ResponsiveElementInfo& element_info) const {
return std::visit(
absl::Overload{
[this](actions::ActionId id) {
return std::u16string(
pinned_actions_delegate_->GetActionItemFor(id)->GetText());
},
[](ToolbarController::ElementIdInfo id) {
return l10n_util::GetStringUTF16(id.menu_text_id);
}},
element_info.overflow_id);
}
std::optional<ui::ImageModel> ToolbarController::GetMenuIcon(
const ResponsiveElementInfo& element_info) const {
return std::visit(
absl::Overload{
[this](actions::ActionId id) {
// Resize the vector icon to `kDefaultIconSize`.
const ui::ImageModel& pinned_icon_image =
pinned_actions_delegate_->GetActionItemFor(id)->GetImage();
if (!pinned_icon_image.IsEmpty() &&
pinned_icon_image.IsVectorIcon()) {
ui::VectorIconModel vector_icon_model =
pinned_icon_image.GetVectorIcon();
return std::make_optional(ui::ImageModel::FromVectorIcon(
*vector_icon_model.vector_icon(), vector_icon_model.color(),
ui::SimpleMenuModel::kDefaultIconSize));
} else {
return std::make_optional(pinned_icon_image);
}
},
[&](ToolbarController::ElementIdInfo info)
-> std::optional<ui::ImageModel> {
if (!info.menu_icon) {
return std::nullopt;
}
return std::make_optional(ui::ImageModel::FromVectorIcon(
*info.menu_icon, ui::kColorMenuIcon,
ui::SimpleMenuModel::kDefaultIconSize));
}},
element_info.overflow_id);
}
views::View* ToolbarController::FindToolbarElementWithId(
views::View* view,
ui::ElementIdentifier id) {
if (!view) {
return nullptr;
}
if (view->GetProperty(views::kElementIdentifierKey) == id) {
return view;
}
for (views::View* child : view->children()) {
if (auto* result = FindToolbarElementWithId(child, id)) {
return result;
}
}
return nullptr;
}
std::vector<const ToolbarController::ResponsiveElementInfo*>
ToolbarController::GetOverflowedElements() {
std::vector<const ToolbarController::ResponsiveElementInfo*>
overflowed_buttons;
if (ToolbarControllerUtil::PreventOverflow()) {
return overflowed_buttons;
}
for (const auto& element : responsive_elements_) {
if (IsOverflowed(element)) {
overflowed_buttons.push_back(&element);
}
}
return overflowed_buttons;
}
bool ToolbarController::IsOverflowed(
const ResponsiveElementInfo& element,
const views::ProposedLayout* proposed_layout) const {
return std::visit(
absl::Overload{
[&](actions::ActionId id) {
CHECK(!proposed_layout);
return pinned_actions_delegate_ &&
pinned_actions_delegate_->IsOverflowed(id);
},
[&](ToolbarController::ElementIdInfo id) {
const auto* const toolbar_element = FindToolbarElementWithId(
toolbar_container_view_, id.overflow_identifier);
const views::FlexLayout* const flex_layout =
static_cast<views::FlexLayout*>(
toolbar_container_view_->GetLayoutManager());
return flex_layout->CanBeVisible(toolbar_element) &&
!(proposed_layout
? proposed_layout->GetLayoutFor(toolbar_element)
->visible
: toolbar_element->GetVisible());
}},
element.overflow_id);
}
void ToolbarController::OnActionsChanged() {
responsive_elements_ = GetResponsiveElementsWithOrderedActions();
}
// This function returns responsive_elements_ but with some portions reordered.
// It rearranges any consecutive sequence of elements that have overflow_id
// of type ActionId.
// Elements with overflow_id of type ElementIdInfo are left in their original
// position. pinned_actions_delegate_->PinnedActionIds() determines the new
// order for the ActionId elements. ActionId elements that aren't in
// PinnedActionIds() are sorted to the front (i.e. placed closer to index 0).
std::vector<ToolbarController::ResponsiveElementInfo>
ToolbarController::GetResponsiveElementsWithOrderedActions() const {
std::vector<ResponsiveElementInfo> ordered_responsive_elements(
responsive_elements_);
const std::vector<actions::ActionId>& ordered_pinned_action_ids =
pinned_actions_delegate_->PinnedActionIds();
auto actions_sorting_function =
[&ordered_pinned_action_ids](
const ToolbarController::ResponsiveElementInfo& a,
const ToolbarController::ResponsiveElementInfo& b) -> bool {
CHECK(std::holds_alternative<actions::ActionId>(a.overflow_id));
CHECK(std::holds_alternative<actions::ActionId>(b.overflow_id));
actions::ActionId a_action_id = std::get<actions::ActionId>(a.overflow_id);
actions::ActionId b_action_id = std::get<actions::ActionId>(b.overflow_id);
for (int ordered_pinned_action_id : ordered_pinned_action_ids) {
if (a_action_id == ordered_pinned_action_id) {
return true;
}
if (b_action_id == ordered_pinned_action_id) {
return false;
}
}
return false;
};
size_t element_index = 0;
while (element_index < ordered_responsive_elements.size()) {
// If the element is not an Action, continue
if (!std::holds_alternative<actions::ActionId>(
ordered_responsive_elements[element_index].overflow_id)) {
element_index++;
continue;
}
// If the current element is an Action, look at the next elements to find
// what's the next one that is not an Action.
// The elements in the [element_index, next_non_action_element_index) range
// will all be Actions and need to be sorted.
size_t next_non_action_element_index = element_index + 1;
while (next_non_action_element_index < ordered_responsive_elements.size() &&
std::holds_alternative<actions::ActionId>(
ordered_responsive_elements[next_non_action_element_index]
.overflow_id)) {
next_non_action_element_index++;
}
std::sort(
ordered_responsive_elements.begin() + element_index,
ordered_responsive_elements.begin() + next_non_action_element_index,
actions_sorting_function);
element_index = next_non_action_element_index;
}
return ordered_responsive_elements;
}
std::unique_ptr<ui::SimpleMenuModel>
ToolbarController::CreateOverflowMenuModel() {
CHECK(overflow_button_->GetVisible());
auto menu_model = std::make_unique<ui::SimpleMenuModel>(this);
// True if the separator belonging to previous section has not been added yet.
bool pre_separator_pending = false;
for (size_t i = 0; i < responsive_elements_.size(); ++i) {
const auto& element = responsive_elements_[i];
if (IsOverflowed(element)) {
if (pre_separator_pending && menu_model->GetItemCount() > 0) {
menu_model->AddSeparator(ui::NORMAL_SEPARATOR);
}
const auto image_model = GetMenuIcon(element);
if (image_model.has_value()) {
menu_model->AddItemWithIcon(i, GetMenuText(element),
image_model.value());
} else {
menu_model->AddItem(i, GetMenuText(element));
}
pre_separator_pending = false;
}
if (element.is_section_end) {
pre_separator_pending = true;
}
}
return menu_model;
}
bool ToolbarController::IsCommandIdEnabled(int command_id) const {
return std::visit(
absl::Overload{
[this](actions::ActionId id) {
return pinned_actions_delegate_->GetActionItemFor(id)->GetEnabled();
},
[this](ToolbarController::ElementIdInfo id) {
return FindToolbarElementWithId(toolbar_container_view_,
id.overflow_identifier)
->GetEnabled();
}},
responsive_elements_.at(command_id).overflow_id);
}
void ToolbarController::ExecuteCommand(int command_id, int event_flags) {
const auto& element_info = responsive_elements_.at(command_id);
std::variant<ui::ElementIdentifier, actions::ActionId> action_key;
std::visit(
absl::Overload{
[&, this](actions::ActionId id) {
pinned_actions_delegate_->GetActionItemFor(id)->InvokeAction(
actions::ActionInvocationContext::Builder()
.SetProperty(
kSidePanelOpenTriggerKey,
static_cast<
std::underlying_type_t<SidePanelOpenTrigger>>(
SidePanelOpenTrigger::kOverflowMenu))
.Build());
action_key.emplace<actions::ActionId>(id);
},
[&, this](ToolbarController::ElementIdInfo id) {
const auto& activate_identifier = id.activate_identifier;
const auto* const element = FindToolbarElementWithId(
toolbar_container_view_, activate_identifier);
CHECK(element);
const auto* button = AsViewClass<views::Button>(element);
button->button_controller()->NotifyClick();
action_key.emplace<ui::ElementIdentifier>(activate_identifier);
}},
element_info.overflow_id);
std::string action_name = GetActionNameFromElementIdentifier(action_key);
if (!action_name.empty()) {
base::RecordAction(
base::UserMetricsAction("ResponsiveToolbar.OverflowMenuItemActivated"));
base::RecordAction(base::UserMetricsAction(action_name.c_str()));
}
}
void ToolbarController::ShowStatusIndicator() {
views::SubmenuView* sub_menu = root_menu_item_->GetSubmenu();
// Install the status indicator and show it if it is active.
for (auto* menu_item : sub_menu->GetMenuItems()) {
if (!menu_item->icon_view()) {
continue;
}
// Layout of the status indicator.
PinnedToolbarButtonStatusIndicator* status_indicator =
PinnedToolbarButtonStatusIndicator::Install(menu_item->icon_view());
status_indicator->SetColorId(kColorToolbarActionItemEngaged,
kColorToolbarButtonIconInactive);
gfx::Rect status_rect = kStatusRect;
const gfx::Rect image_container_bounds =
menu_item->icon_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() + kImageContainerLowerPadding;
// Set the new origin for status_rect
status_rect.set_origin(gfx::Point(new_x, new_y));
status_indicator->SetBoundsRect(status_rect);
if (std::holds_alternative<actions::ActionId>(
responsive_elements_.at(menu_item->GetCommand()).overflow_id)) {
actions::ActionId action_id = std::get<actions::ActionId>(
responsive_elements_.at(menu_item->GetCommand()).overflow_id);
actions::ActionItem* action_item =
pinned_actions_delegate_->GetActionItemFor(action_id);
if (action_item &&
action_item->GetProperty(kActionItemUnderlineIndicatorKey)) {
const ui::ImageModel& pinned_icon_image = action_item->GetImage();
if (!pinned_icon_image.IsEmpty() && pinned_icon_image.IsVectorIcon()) {
ui::VectorIconModel vector_icon_model =
pinned_icon_image.GetVectorIcon();
menu_item->icon_view()->SetImage(ui::ImageModel::FromVectorIcon(
*vector_icon_model.vector_icon(), kColorToolbarActionItemEngaged,
ui::SimpleMenuModel::kDefaultIconSize));
}
status_indicator->Show();
}
}
}
}
void ToolbarController::ActionItemChanged(actions::ActionItem* action_item) {
if (!IsMenuRunning()) {
return;
}
std::optional<int> command_id = std::nullopt;
for (size_t i = 0; i < responsive_elements_.size(); ++i) {
const auto& element = responsive_elements_[i];
if (std::holds_alternative<actions::ActionId>(element.overflow_id)) {
actions::ActionId element_action_id =
std::get<actions::ActionId>(element.overflow_id);
if (element_action_id == action_item->GetActionId().value()) {
command_id = static_cast<int>(i);
break;
}
}
}
if (!IsOverflowed(responsive_elements_.at(command_id.value()))) {
return;
}
views::MenuItemView* menu_item =
root_menu_item_->GetMenuItemByID(command_id.value());
if (!menu_item || !menu_item->icon_view()) {
return;
}
PinnedToolbarButtonStatusIndicator* status_indicator =
PinnedToolbarButtonStatusIndicator::GetStatusIndicator(
menu_item->icon_view());
if (!status_indicator) {
return;
}
if (action_item->GetProperty(kActionItemUnderlineIndicatorKey)) {
const ui::ImageModel& pinned_icon_image = action_item->GetImage();
if (!pinned_icon_image.IsEmpty() && pinned_icon_image.IsVectorIcon()) {
ui::VectorIconModel vector_icon_model = pinned_icon_image.GetVectorIcon();
menu_item->icon_view()->SetImage(ui::ImageModel::FromVectorIcon(
*vector_icon_model.vector_icon(), kColorToolbarActionItemEngaged,
ui::SimpleMenuModel::kDefaultIconSize));
}
status_indicator->Show();
} else {
const ui::ImageModel& pinned_icon_image = action_item->GetImage();
if (!pinned_icon_image.IsEmpty() && pinned_icon_image.IsVectorIcon()) {
ui::VectorIconModel vector_icon_model = pinned_icon_image.GetVectorIcon();
menu_item->icon_view()->SetImage(ui::ImageModel::FromVectorIcon(
*vector_icon_model.vector_icon(), vector_icon_model.color(),
ui::SimpleMenuModel::kDefaultIconSize));
}
status_indicator->Hide();
}
}
void ToolbarController::PopulateMenu(views::MenuItemView* parent) {
if (parent->HasSubmenu()) {
parent->GetSubmenu()->RemoveAllChildViews();
}
if (menu_model_) {
menu_model_->Clear();
}
menu_model_ = CreateOverflowMenuModel();
CHECK(menu_model_);
for (size_t i = 0; i < menu_model_->GetItemCount(); ++i) {
views::MenuItemView* menu_item =
views::MenuModelAdapter::AppendMenuItemFromModel(
menu_model_.get(), i, parent, menu_model_->GetCommandIdAt(i));
// `menu_item` can be nullptr if it is a separator.
if (menu_item &&
menu_item->GetType() == views::MenuItemView::Type::kNormal) {
menu_item->SetEnabled(IsCommandIdEnabled(menu_item->GetCommand()));
}
}
parent->GetSubmenu()->InvalidateLayout();
}
void ToolbarController::ShowMenu() {
auto* button_controller = overflow_button_->menu_button_controller();
auto root = std::make_unique<views::MenuItemView>(this);
root_menu_item_ = root.get();
PopulateMenu(root_menu_item_);
menu_runner_ = std::make_unique<views::MenuRunner>(
std::move(root), views::MenuRunner::HAS_MNEMONICS);
menu_runner_->RunMenuAt(
button_controller->button()->GetWidget(), button_controller,
button_controller->button()->GetAnchorBoundsInScreen(),
views::MenuAnchorPosition::kTopRight, ui::mojom::MenuSourceType::kNone);
ShowStatusIndicator();
}
bool ToolbarController::IsMenuRunning() const {
return menu_runner_ && menu_runner_->IsRunning();
}
void ToolbarController::CloseMenu() {
root_menu_item_ = nullptr;
menu_model_.reset();
menu_runner_.reset();
}