blob: e30d186a29d361b3d5344692fbd50141ee7953b4 [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/browser_actions.h"
#include <memory>
#include <optional>
#include <string>
#include "base/check_op.h"
#include "chrome/app/vector_icons/vector_icons.h"
#include "chrome/browser/devtools/devtools_window.h"
#include "chrome/browser/prefs/incognito_mode_prefs.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/search_engines/template_url_service_factory.h"
#include "chrome/browser/sharing_hub/sharing_hub_features.h"
#include "chrome/browser/ui/actions/chrome_action_id.h"
#include "chrome/browser/ui/actions/chrome_actions.h"
#include "chrome/browser/ui/autofill/address_bubbles_icon_controller.h"
#include "chrome/browser/ui/autofill/autofill_bubble_base.h"
#include "chrome/browser/ui/autofill/payments/save_payment_icon_controller.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_action_prefs_listener.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
#include "chrome/browser/ui/lens/lens_overlay_controller.h"
#include "chrome/browser/ui/passwords/manage_passwords_ui_controller.h"
#include "chrome/browser/ui/passwords/passwords_model_delegate.h"
#include "chrome/browser/ui/qrcode_generator/qrcode_generator_bubble_controller.h"
#include "chrome/browser/ui/send_tab_to_self/send_tab_to_self_bubble.h"
#include "chrome/browser/ui/tabs/public/tab_features.h"
#include "chrome/browser/ui/tabs/public/tab_interface.h"
#include "chrome/browser/ui/toolbar/chrome_labs/chrome_labs_utils.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/media_router/cast_browser_controller.h"
#include "chrome/browser/ui/views/page_info/page_info_view_factory.h"
#include "chrome/browser/ui/views/send_tab_to_self/send_tab_to_self_toolbar_bubble_controller.h"
#include "chrome/browser/ui/views/side_panel/history_clusters/history_clusters_side_panel_utils.h"
#include "chrome/browser/ui/views/side_panel/side_panel_action_callback.h"
#include "chrome/browser/ui/views/side_panel/side_panel_entry_id.h"
#include "chrome/browser/ui/views/side_panel/side_panel_entry_key.h"
#include "chrome/browser/ui/views/side_panel/side_panel_enums.h"
#include "chrome/browser/ui/views/side_panel/side_panel_ui.h"
#include "chrome/grit/branded_strings.h"
#include "chrome/grit/generated_resources.h"
#include "components/lens/lens_features.h"
#include "components/media_router/browser/media_router_dialog_controller.h"
#include "components/media_router/browser/media_router_metrics.h"
#include "components/omnibox/browser/vector_icons.h"
#include "components/policy/core/common/policy_pref_names.h"
#include "components/search_engines/template_url.h"
#include "components/search_engines/template_url_service.h"
#include "components/strings/grit/components_strings.h"
#include "components/user_prefs/user_prefs.h"
#include "components/vector_icons/vector_icons.h"
#include "ui/accessibility/accessibility_features.h"
#include "ui/actions/actions.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/ui_base_features.h"
#include "ui/gfx/text_utils.h"
#include "ui/gfx/vector_icon_types.h"
#include "ui/menus/simple_menu_model.h"
namespace {
actions::ActionItem::ActionItemBuilder ChromeMenuAction(
actions::ActionItem::InvokeActionCallback callback,
actions::ActionId action_id,
int title_id,
int tooltip_id,
const gfx::VectorIcon& icon) {
return actions::ActionItem::Builder(callback)
.SetActionId(action_id)
.SetText(BrowserActions::GetCleanTitleAndTooltipText(
l10n_util::GetStringUTF16(title_id)))
.SetTooltipText(BrowserActions::GetCleanTitleAndTooltipText(
l10n_util::GetStringUTF16(tooltip_id)))
.SetImage(ui::ImageModel::FromVectorIcon(icon, ui::kColorIcon))
.SetProperty(actions::kActionItemPinnableKey, true);
}
actions::StatefulImageActionItem::StatefulImageActionItemBuilder
StatefulChromeMenuAction(actions::ActionItem::InvokeActionCallback callback,
actions::ActionId action_id,
int title_id,
int tooltip_id,
const gfx::VectorIcon& icon) {
ui::ImageModel image = ui::ImageModel::FromVectorIcon(icon, ui::kColorIcon);
return actions::StatefulImageActionItem::Builder(callback)
.SetActionId(action_id)
.SetText(BrowserActions::GetCleanTitleAndTooltipText(
l10n_util::GetStringUTF16(title_id)))
.SetTooltipText(BrowserActions::GetCleanTitleAndTooltipText(
l10n_util::GetStringUTF16(tooltip_id)))
.SetImage(image)
.SetStatefulImage(image)
.SetProperty(actions::kActionItemPinnableKey, true);
}
actions::ActionItem::ActionItemBuilder SidePanelAction(
SidePanelEntryId id,
int title_id,
int tooltip_id,
const gfx::VectorIcon& icon,
actions::ActionId action_id,
Browser* browser,
bool is_pinnable) {
return actions::ActionItem::Builder(CreateToggleSidePanelActionCallback(
SidePanelEntryKey(id), browser))
.SetActionId(action_id)
.SetText(l10n_util::GetStringUTF16(title_id))
.SetTooltipText(l10n_util::GetStringUTF16(tooltip_id))
.SetImage(ui::ImageModel::FromVectorIcon(icon, ui::kColorIcon))
.SetProperty(actions::kActionItemPinnableKey, is_pinnable);
}
} // namespace
BrowserActions::BrowserActions(Browser& browser) : browser_(browser) {}
BrowserActions::~BrowserActions() {
// Extract the unique ptr and destruct it after the raw_ptr to avoid a
// dangling pointer scenario.
std::unique_ptr<actions::ActionItem> owned_root_action_item =
actions::ActionManager::Get().RemoveAction(root_action_item_);
root_action_item_ = nullptr;
}
// static
std::u16string BrowserActions::GetCleanTitleAndTooltipText(
std::u16string string) {
const std::u16string ellipsis_unicode = u"\u2026";
const std::u16string ellipsis_text = u"...";
auto remove_ellipsis = [&string](const std::u16string ellipsis) {
size_t ellipsis_pos = string.find(ellipsis);
if (ellipsis_pos != std::u16string::npos) {
string.erase(ellipsis_pos);
}
};
remove_ellipsis(ellipsis_unicode);
remove_ellipsis(ellipsis_text);
return gfx::RemoveAccelerator(string);
}
void BrowserActions::InitializeBrowserActions() {
Profile* profile = browser_->profile();
Browser* browser = &(browser_.get());
const bool is_guest_session = profile->IsGuestSession();
actions::ActionManager::Get().AddAction(
actions::ActionItem::Builder()
.CopyAddressTo(&root_action_item_)
.AddChildren(
SidePanelAction(
SidePanelEntryId::kBookmarks, IDS_BOOKMARK_MANAGER_TITLE,
IDS_BOOKMARK_MANAGER_TITLE, kBookmarksSidePanelRefreshIcon,
kActionSidePanelShowBookmarks, browser, true),
SidePanelAction(SidePanelEntryId::kReadingList,
IDS_READ_LATER_TITLE, IDS_READ_LATER_TITLE,
kReadingListIcon, kActionSidePanelShowReadingList,
browser, true),
SidePanelAction(SidePanelEntryId::kAboutThisSite,
IDS_PAGE_INFO_ABOUT_THIS_PAGE_TITLE,
IDS_PAGE_INFO_ABOUT_THIS_PAGE_TITLE,
PageInfoViewFactory::GetAboutThisSiteVectorIcon(),
kActionSidePanelShowAboutThisSite, browser,
false),
SidePanelAction(SidePanelEntryId::kCustomizeChrome,
IDS_SIDE_PANEL_CUSTOMIZE_CHROME_TITLE,
IDS_SIDE_PANEL_CUSTOMIZE_CHROME_TITLE,
vector_icons::kEditChromeRefreshIcon,
kActionSidePanelShowCustomizeChrome, browser,
false),
SidePanelAction(SidePanelEntryId::kShoppingInsights,
IDS_SHOPPING_INSIGHTS_SIDE_PANEL_TITLE,
IDS_SHOPPING_INSIGHTS_SIDE_PANEL_TITLE,
vector_icons::kShoppingBagIcon,
kActionSidePanelShowShoppingInsights, browser,
false),
SidePanelAction(SidePanelEntryId::kMerchantTrust,
IDS_MERCHANT_TRUST_SIDE_PANEL_TITLE,
IDS_MERCHANT_TRUST_SIDE_PANEL_TITLE,
vector_icons::kStorefrontIcon,
kActionSidePanelShowMerchantTrust, browser,
false))
.Build());
if (side_panel::history_clusters::
IsHistoryClustersSidePanelSupportedForProfile(profile)) {
root_action_item_->AddChild(
SidePanelAction(SidePanelEntryId::kHistoryClusters, IDS_HISTORY_TITLE,
IDS_HISTORY_CLUSTERS_SHOW_SIDE_PANEL,
vector_icons::kHistoryChromeRefreshIcon,
kActionSidePanelShowHistoryCluster, browser, true)
.Build());
}
root_action_item_->AddChild(
SidePanelAction(SidePanelEntryId::kReadAnything, IDS_READING_MODE_TITLE,
IDS_READING_MODE_TITLE, kMenuBookChromeRefreshIcon,
kActionSidePanelShowReadAnything, browser, true)
.Build());
if (lens::features::IsLensOverlayEnabled()) {
actions::ActionItem::InvokeActionCallback callback = base::BindRepeating(
[](base::WeakPtr<Browser> browser, actions::ActionItem* item,
actions::ActionInvocationContext context) {
if (!browser) {
return;
}
LensOverlayController* controller = browser->GetActiveTabInterface()
->GetTabFeatures()
->lens_overlay_controller();
// Toggle the Lens overlay. There's no need to show or hide the side
// panel as the overlay controller will handle that.
if (controller->IsOverlayActive()) {
controller->CloseUIAsync(
lens::LensOverlayDismissalSource::kToolbar);
} else {
controller->ShowUI(lens::LensOverlayInvocationSource::kToolbar);
}
},
browser->AsWeakPtr());
const gfx::VectorIcon& icon =
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
vector_icons::kGoogleLensMonochromeLogoIcon;
#else
vector_icons::kSearchChromeRefreshIcon;
#endif
root_action_item_->AddChild(
actions::ActionItem::Builder(callback)
.SetActionId(kActionSidePanelShowLensOverlayResults)
.SetText(l10n_util::GetStringUTF16(IDS_SHOW_LENS_OVERLAY))
.SetTooltipText(l10n_util::GetStringUTF16(
IDS_SIDE_PANEL_LENS_OVERLAY_TOOLBAR_TOOLTIP))
.SetImage(ui::ImageModel::FromVectorIcon(
icon, ui::kColorIcon, ui::SimpleMenuModel::kDefaultIconSize))
.SetProperty(actions::kActionItemPinnableKey, true)
.Build());
}
// Create the lens action item. The icon and text are set appropriately in the
// lens side panel coordinator. They have default values here.
root_action_item_->AddChild(
SidePanelAction(SidePanelEntryId::kLens, IDS_LENS_DEFAULT_TITLE,
IDS_LENS_DEFAULT_TITLE, vector_icons::kImageSearchIcon,
kActionSidePanelShowLens, browser, false)
.Build());
//------- Chrome Menu Actions --------//
if (features::IsToolbarPinningEnabled()) {
root_action_item_->AddChild(
ChromeMenuAction(base::BindRepeating(
[](Browser* browser, actions::ActionItem* item,
actions::ActionInvocationContext context) {
CHECK(IncognitoModePrefs::IsIncognitoAllowed(
browser->profile()));
chrome::NewIncognitoWindow(browser->profile());
},
base::Unretained(browser)),
kActionNewIncognitoWindow, IDS_NEW_INCOGNITO_WINDOW,
IDS_NEW_INCOGNITO_WINDOW, kIncognitoRefreshMenuIcon)
.SetEnabled(IncognitoModePrefs::IsIncognitoAllowed(profile))
.Build());
root_action_item_->AddChild(
ChromeMenuAction(base::BindRepeating(
[](Browser* browser, actions::ActionItem* item,
actions::ActionInvocationContext context) {
chrome::Print(browser);
},
base::Unretained(browser)),
kActionPrint, IDS_PRINT, IDS_PRINT, kPrintMenuIcon)
.SetEnabled(chrome::CanPrint(browser))
.Build());
root_action_item_->AddChild(
ChromeMenuAction(base::BindRepeating(
[](Browser* browser, actions::ActionItem* item,
actions::ActionInvocationContext context) {
if (browser->profile()->IsIncognitoProfile()) {
chrome::ShowIncognitoClearBrowsingDataDialog(
browser->GetBrowserForOpeningWebUi());
} else {
chrome::ShowClearBrowsingDataDialog(
browser->GetBrowserForOpeningWebUi());
}
},
base::Unretained(browser)),
kActionClearBrowsingData, IDS_CLEAR_BROWSING_DATA,
IDS_CLEAR_BROWSING_DATA, kTrashCanRefreshIcon)
.SetEnabled(
profile->IsIncognitoProfile() ||
(!profile->IsGuestSession() && !profile->IsSystemProfile()))
.Build());
if (chrome::CanOpenTaskManager()) {
root_action_item_->AddChild(
ChromeMenuAction(base::BindRepeating(
[](Browser* browser, actions::ActionItem* item,
actions::ActionInvocationContext context) {
chrome::OpenTaskManager(browser);
},
base::Unretained(browser)),
kActionTaskManager, IDS_TASK_MANAGER,
IDS_TASK_MANAGER, kTaskManagerIcon)
.Build());
}
root_action_item_->AddChild(
ChromeMenuAction(
base::BindRepeating(
[](Browser* browser, actions::ActionItem* item,
actions::ActionInvocationContext context) {
chrome::ToggleDevToolsWindow(
browser, DevToolsToggleAction::Show(),
DevToolsOpenedByAction::kPinnedToolbarButton);
},
base::Unretained(browser)),
kActionDevTools, IDS_DEV_TOOLS, IDS_DEV_TOOLS, kDeveloperToolsIcon)
.Build());
root_action_item_->AddChild(
ChromeMenuAction(
base::BindRepeating(
[](Browser* browser, actions::ActionItem* item,
actions::ActionInvocationContext context) {
auto* bubble_controller =
browser->browser_window_features()
->send_tab_to_self_toolbar_bubble_controller();
if (bubble_controller->IsBubbleShowing()) {
bubble_controller->HideBubble();
} else {
send_tab_to_self::ShowBubble(
browser->tab_strip_model()->GetActiveWebContents());
}
},
base::Unretained(browser)),
kActionSendTabToSelf, IDS_SEND_TAB_TO_SELF, IDS_SEND_TAB_TO_SELF,
kDevicesChromeRefreshIcon)
.SetEnabled(chrome::CanSendTabToSelf(browser))
.SetVisible(
!sharing_hub::SharingIsDisabledByPolicy(browser->profile()))
.Build());
root_action_item_->AddChild(
ChromeMenuAction(base::BindRepeating(
[](Browser* browser, actions::ActionItem* item,
actions::ActionInvocationContext context) {
chrome::ShowTranslateBubble(browser);
},
base::Unretained(browser)),
kActionShowTranslate, IDS_SHOW_TRANSLATE,
IDS_TOOLTIP_TRANSLATE, kTranslateIcon)
.Build());
root_action_item_->AddChild(
ChromeMenuAction(base::BindRepeating(
[](Browser* browser, actions::ActionItem* item,
actions::ActionInvocationContext context) {
chrome::GenerateQRCode(browser);
},
base::Unretained(browser)),
kActionQrCodeGenerator, IDS_APP_MENU_CREATE_QR_CODE,
IDS_APP_MENU_CREATE_QR_CODE, kQrCodeChromeRefreshIcon)
.SetEnabled(false)
.SetVisible(
!sharing_hub::SharingIsDisabledByPolicy(browser->profile()))
.Build());
root_action_item_->AddChild(
ChromeMenuAction(
base::BindRepeating(
[](Browser* browser, actions::ActionItem* item,
actions::ActionInvocationContext context) {
auto* controller =
autofill::AddressBubblesIconController::Get(
browser->tab_strip_model()->GetActiveWebContents());
if (controller && controller->GetBubbleView()) {
controller->GetBubbleView()->Hide();
} else {
chrome::ShowAddresses(browser);
}
},
base::Unretained(browser)),
kActionShowAddressesBubbleOrPage,
IDS_ADDRESSES_AND_MORE_SUBMENU_OPTION,
IDS_ADDRESSES_AND_MORE_SUBMENU_OPTION,
vector_icons::kLocationOnChromeRefreshIcon)
.SetEnabled(!is_guest_session)
.Build());
root_action_item_->AddChild(
ChromeMenuAction(
base::BindRepeating(
[](Browser* browser, actions::ActionItem* item,
actions::ActionInvocationContext context) {
auto hide_bubble = [&browser](int command_id) -> bool {
auto* controller = autofill::SavePaymentIconController::Get(
browser->tab_strip_model()->GetActiveWebContents(),
command_id);
if (controller && controller->GetPaymentBubbleView()) {
controller->GetPaymentBubbleView()->Hide();
return true;
}
return false;
};
const bool bubble_hidden =
hide_bubble(IDC_SAVE_CREDIT_CARD_FOR_PAGE) ||
hide_bubble(IDC_SAVE_IBAN_FOR_PAGE);
if (!bubble_hidden) {
chrome::ShowPaymentMethods(browser);
}
},
base::Unretained(browser)),
kActionShowPaymentsBubbleOrPage, IDS_PAYMENT_METHOD_SUBMENU_OPTION,
IDS_PAYMENT_METHOD_SUBMENU_OPTION, kCreditCardChromeRefreshIcon)
.SetEnabled(!is_guest_session)
.Build());
if (IsChromeLabsEnabled()) {
// TODO(b/354758327): Update `ShouldShowChromeLabsUI()` to not require
// `model` as a parameter, then use to set visibility of action item.
root_action_item_->AddChild(
ChromeMenuAction(base::BindRepeating(
[](Browser* browser, actions::ActionItem* item,
actions::ActionInvocationContext context) {
browser->window()->ShowChromeLabs();
},
base::Unretained(browser)),
kActionShowChromeLabs, IDS_CHROMELABS,
IDS_CHROMELABS, kScienceIcon)
.SetVisible(false)
.Build());
}
root_action_item_->AddChild(
ChromeMenuAction(
base::BindRepeating(
[](Browser* browser, actions::ActionItem* item,
actions::ActionInvocationContext context) {
if (PasswordsModelDelegateFromWebContents(
browser->tab_strip_model()->GetActiveWebContents())
->GetState() ==
password_manager::ui::INACTIVE_STATE) {
chrome::ShowPasswordManager(browser);
} else {
content::WebContents* web_contents =
browser->tab_strip_model()->GetActiveWebContents();
auto* controller =
ManagePasswordsUIController::FromWebContents(
web_contents);
if (controller->IsShowingBubble()) {
controller->HidePasswordBubble();
} else {
chrome::ManagePasswordsForPage(browser);
}
}
},
base::Unretained(browser)),
kActionShowPasswordsBubbleOrPage, IDS_VIEW_PASSWORDS,
IDS_VIEW_PASSWORDS, vector_icons::kPasswordManagerIcon)
.SetEnabled(!is_guest_session)
.Build());
root_action_item_->AddChild(
ChromeMenuAction(
base::BindRepeating(
[](Browser* browser, actions::ActionItem* item,
actions::ActionInvocationContext context) {
chrome::CopyURL(
browser,
browser->tab_strip_model()->GetActiveWebContents());
},
base::Unretained(browser)),
kActionCopyUrl, IDS_APP_MENU_COPY_LINK, IDS_APP_MENU_COPY_LINK,
kLinkChromeRefreshIcon)
.SetEnabled(chrome::CanCopyUrl(browser))
.SetVisible(
!sharing_hub::SharingIsDisabledByPolicy(browser->profile()))
.Build());
if (base::FeatureList::IsEnabled(features::kPinnedCastButton)) {
root_action_item_->AddChild(
StatefulChromeMenuAction(
base::BindRepeating(
[](Browser* browser, actions::ActionItem* item,
actions::ActionInvocationContext context) {
// TODO(crbug.com/356468503): Figure out how to capture
// action invocation location.
browser->browser_window_features()
->cast_browser_controller()
->ToggleDialog();
},
base::Unretained(browser)),
kActionRouteMedia, IDS_MEDIA_ROUTER_MENU_ITEM_TITLE,
IDS_MEDIA_ROUTER_ICON_TOOLTIP_TEXT, kCastChromeRefreshIcon)
.SetEnabled(chrome::CanRouteMedia(browser))
.Build());
}
AddListeners();
}
// Actions that do not directly show up in chrome UI.
root_action_item_->AddChild(
actions::ActionItem::Builder(
base::BindRepeating(
[](Browser* browser, actions::ActionItem* item,
actions::ActionInvocationContext context) {
browser->GetBrowserView().Cut();
},
base::Unretained(browser)))
.SetActionId(actions::kActionCut)
.Build());
root_action_item_->AddChild(
actions::ActionItem::Builder(
base::BindRepeating(
[](Browser* browser, actions::ActionItem* item,
actions::ActionInvocationContext context) {
browser->GetBrowserView().Copy();
},
base::Unretained(browser)))
.SetActionId(actions::kActionCopy)
.Build());
root_action_item_->AddChild(
actions::ActionItem::Builder(
base::BindRepeating(
[](Browser* browser, actions::ActionItem* item,
actions::ActionInvocationContext context) {
browser->GetBrowserView().Paste();
},
base::Unretained(browser)))
.SetActionId(actions::kActionPaste)
.Build());
}
void BrowserActions::RemoveListeners() {
browser_action_prefs_listener_.reset();
}
void BrowserActions::AddListeners() {
browser_action_prefs_listener_ =
std::make_unique<BrowserActionPrefsListener>(browser_.get());
}