blob: f786e3489a15ad2bd085e94b66416926908ef390 [file] [log] [blame]
// Copyright 2025 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/omnibox/omnibox_context_menu_controller.h"
#include <stddef.h>
#include <algorithm>
#include <memory>
#include <string>
#include <vector>
#include "base/memory/weak_ptr.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/app/vector_icons/vector_icons.h"
#include "chrome/browser/contextual_search/contextual_search_web_contents_helper.h"
#include "chrome/browser/favicon/favicon_service_factory.h"
#include "chrome/browser/favicon/favicon_utils.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/chrome_select_file_policy.h"
#include "chrome/browser/ui/contextual_search/searchbox_context_data.h"
#include "chrome/browser/ui/omnibox/omnibox_controller.h"
#include "chrome/browser/ui/omnibox/omnibox_edit_model.h"
#include "chrome/browser/ui/omnibox/omnibox_next_features.h"
#include "chrome/browser/ui/omnibox/omnibox_popup_state_manager.h"
#include "chrome/browser/ui/tabs/tab_renderer_data.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/views/location_bar/omnibox_popup_file_selector.h"
#include "chrome/browser/ui/webui/omnibox_popup/omnibox_popup_aim_handler.h"
#include "chrome/browser/ui/webui/omnibox_popup/omnibox_popup_ui.h"
#include "chrome/browser/ui/webui/omnibox_popup/omnibox_popup_web_contents_helper.h"
#include "chrome/browser/ui/webui/top_chrome/webui_contents_wrapper.h"
#include "chrome/browser/ui/webui/webui_embedding_context.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/grit/omnibox_popup_resources.h"
#include "components/favicon/core/favicon_service.h"
#include "components/favicon_base/favicon_types.h"
#include "components/lens/contextual_input.h"
#include "components/omnibox/browser/searchbox.mojom.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/url_constants.h"
#include "ui/base/models/image_model.h"
#include "ui/base/models/menu_model.h"
#include "ui/gfx/image/image.h"
namespace {
// TODO(crbug.com/457815342): Add this to config when available.
constexpr int kMaxRecentTabs = 5;
constexpr int kMinOmniboxContextMenuRecentTabsCommandId = 33000;
bool IsValidTab(GURL url) {
// Skip tabs that are still loading, and skip webui.
return url.is_valid() && !url.is_empty() &&
!url.SchemeIs(content::kChromeUIScheme) &&
!url.SchemeIs(content::kChromeUIUntrustedScheme) &&
!url.IsAboutBlank();
}
std::optional<lens::ImageEncodingOptions> CreateImageEncodingOptions() {
// TODO(crbug.com/457815342): Use omnibox fieldtrial when available.
auto image_upload_config =
omnibox::FeatureConfig::Get().config.composebox().image_upload();
return lens::ImageEncodingOptions{
.enable_webp_encoding = image_upload_config.enable_webp_encoding(),
.max_size = image_upload_config.downscale_max_image_size(),
.max_height = image_upload_config.downscale_max_image_height(),
.max_width = image_upload_config.downscale_max_image_width(),
.compression_quality = image_upload_config.image_compression_quality()};
}
} // namespace
OmniboxContextMenuController::OmniboxContextMenuController(
OmniboxPopupFileSelector* file_selector,
content::WebContents* web_contents)
: file_selector_(file_selector->GetWeakPtr()),
web_contents_(web_contents->GetWeakPtr()) {
menu_model_ = std::make_unique<ui::SimpleMenuModel>(this);
next_command_id_ = kMinOmniboxContextMenuRecentTabsCommandId;
BuildMenu();
}
OmniboxContextMenuController::~OmniboxContextMenuController() = default;
void OmniboxContextMenuController::BuildMenu() {
AddRecentTabItems();
AddStaticItems();
}
void OmniboxContextMenuController::AddItem(int id, const std::u16string str) {
menu_model_->AddItem(id, str);
}
void OmniboxContextMenuController::AddItemWithStringIdAndIcon(
int id,
int localization_id,
const ui::ImageModel& icon) {
menu_model_->AddItemWithStringIdAndIcon(id, localization_id, icon);
}
void OmniboxContextMenuController::AddItemWithIcon(int command_id,
const std::u16string& label,
const ui::ImageModel& icon) {
menu_model_->AddItemWithIcon(command_id, label, icon);
}
void OmniboxContextMenuController::AddSeparator() {
menu_model_->AddSeparator(ui::NORMAL_SEPARATOR);
}
void OmniboxContextMenuController::AddRecentTabItems() {
AddTitleWithStringId(IDS_NTP_COMPOSE_MOST_RECENT_TABS);
std::vector<OmniboxContextMenuController::TabInfo> tabs = GetRecentTabs();
for (const auto& tab : tabs) {
AddItemWithIcon(next_command_id_, tab.title,
favicon::GetDefaultFaviconModel());
AddTabFavicon(next_command_id_, tab.url, tab.title);
next_command_id_ += 1;
}
// Remove header if no tabs to show.
if (tabs.empty()) {
auto index = menu_model_->GetIndexOfCommandId(ui::MenuModel::kTitleId);
if (index) {
menu_model_->RemoveItemAt(index.value());
return;
}
}
AddSeparator();
}
void OmniboxContextMenuController::AddStaticItems() {
auto add_image_icon =
ui::ImageModel::FromVectorIcon(kAddPhotoAlternateIcon, ui::kColorMenuIcon,
ui::SimpleMenuModel::kDefaultIconSize);
AddItemWithStringIdAndIcon(IDC_OMNIBOX_CONTEXT_ADD_IMAGE,
IDS_NTP_COMPOSE_ADD_IMAGE, add_image_icon);
auto add_file_icon =
ui::ImageModel::FromVectorIcon(kAttachFileIcon, ui::kColorMenuIcon,
ui::SimpleMenuModel::kDefaultIconSize);
AddItemWithStringIdAndIcon(IDC_OMNIBOX_CONTEXT_ADD_FILE,
IDS_NTP_COMPOSE_ADD_FILE, add_file_icon);
auto* browser_window_interface =
webui::GetBrowserWindowInterface(web_contents_.get());
Profile* profile = browser_window_interface->GetProfile();
if (omnibox::IsDeepSearchEnabled(profile) ||
omnibox::IsCreateImagesEnabled(profile)) {
AddSeparator();
}
auto deep_search_icon =
ui::ImageModel::FromVectorIcon(kTravelExploreIcon, ui::kColorMenuIcon,
ui::SimpleMenuModel::kDefaultIconSize);
AddItemWithStringIdAndIcon(IDC_OMNIBOX_CONTEXT_DEEP_RESEARCH,
IDS_NTP_COMPOSE_DEEP_SEARCH, deep_search_icon);
auto create_images_icon = ui::ImageModel::FromResourceId(
IDR_OMNIBOX_POPUP_IMAGES_CREATE_IMAGES_PNG);
AddItemWithStringIdAndIcon(IDC_OMNIBOX_CONTEXT_CREATE_IMAGES,
IDS_NTP_COMPOSE_CREATE_IMAGES, create_images_icon);
}
std::vector<OmniboxContextMenuController::TabInfo>
OmniboxContextMenuController::GetRecentTabs() {
std::vector<OmniboxContextMenuController::TabInfo> tabs;
// Iterate through the tab strip model.
auto* browser_window_interface =
webui::GetBrowserWindowInterface(web_contents_.get());
auto* tab_strip_model = browser_window_interface->GetTabStripModel();
for (int i = 0; i < tab_strip_model->count(); i++) {
tabs::TabInterface* const tab = tab_strip_model->GetTabAtIndex(i);
TabRendererData tab_renderer_data =
TabRendererData::FromTabInModel(tab_strip_model, i);
const auto& last_committed_url = tab_renderer_data.last_committed_url;
if (!IsValidTab(last_committed_url)) {
continue;
}
OmniboxContextMenuController::TabInfo tab_data;
tab_data.tab_id = tab->GetHandle().raw_value();
tab_data.title = tab_renderer_data.title;
tab_data.url = last_committed_url;
content::WebContents* web_contents = tab_strip_model->GetWebContentsAt(i);
tab_data.last_active =
std::max(web_contents->GetLastActiveTimeTicks(),
web_contents->GetLastInteractionTimeTicks());
tabs.push_back(tab_data);
}
// Sort tabs by most recently active.
int max_tab_suggestions =
std::min(static_cast<int>(tabs.size()), kMaxRecentTabs);
std::partial_sort(tabs.begin(), tabs.begin() + max_tab_suggestions,
tabs.end(),
[](const OmniboxContextMenuController::TabInfo& a,
const OmniboxContextMenuController::TabInfo& b) {
return a.last_active > b.last_active;
});
tabs.resize(max_tab_suggestions);
return tabs;
}
void OmniboxContextMenuController::AddTabFavicon(int command_id,
const GURL& url,
const std::u16string& label) {
auto* browser_window_interface =
webui::GetBrowserWindowInterface(web_contents_.get());
Profile* profile = browser_window_interface->GetProfile();
if (!profile) {
return;
}
favicon::FaviconService* favicon_service =
FaviconServiceFactory::GetForProfile(profile,
ServiceAccessType::EXPLICIT_ACCESS);
if (!favicon_service) {
return;
}
favicon_service->GetFaviconImageForPageURL(
url,
base::BindOnce(static_cast<void (OmniboxContextMenuController::*)(
int, const favicon_base::FaviconImageResult&)>(
&OmniboxContextMenuController::OnFaviconDataAvailable),
weak_ptr_factory_.GetWeakPtr(), command_id),
&cancelable_task_tracker_);
}
void OmniboxContextMenuController::OnFaviconDataAvailable(
int command_id,
const favicon_base::FaviconImageResult& image_result) {
if (image_result.image.IsEmpty()) {
// Default icon has already been set.
return;
}
const std::optional<size_t> index_in_menu =
menu_model_->GetIndexOfCommandId(command_id);
DCHECK(index_in_menu.has_value());
menu_model_->SetIcon(index_in_menu.value(),
ui::ImageModel::FromImage(image_result.image));
if (menu_model_->menu_model_delegate()) {
menu_model_->menu_model_delegate()->OnIconChanged(command_id);
}
}
void OmniboxContextMenuController::AddTitleWithStringId(int localization_id) {
menu_model_->AddTitleWithStringId(localization_id);
}
void OmniboxContextMenuController::AddTabContext(const TabInfo& tab_info) {
UpdateSearchboxContext(/*tab_info=*/tab_info, /*tool_mode=*/std::nullopt);
GetEditModel()->OpenAiMode(/*via_keyboard=*/false, /*via_context_menu=*/true);
}
void OmniboxContextMenuController::UpdateSearchboxContext(
std::optional<TabInfo> tab_info,
std::optional<searchbox::mojom::ToolMode> tool_mode) {
auto* browser_window_interface =
webui::GetBrowserWindowInterface(web_contents_.get());
if (!browser_window_interface) {
return;
}
SearchboxContextData* searchbox_context_data =
browser_window_interface->GetFeatures().searchbox_context_data();
if (!searchbox_context_data) {
return;
}
auto context = searchbox_context_data->TakePendingContext();
if (!context) {
context = std::make_unique<SearchboxContextData::Context>();
}
if (tab_info) {
auto tab_attachment = searchbox::mojom::TabAttachmentStub::New();
tab_attachment->tab_id = tab_info->tab_id;
tab_attachment->title = base::UTF16ToUTF8(tab_info->title);
tab_attachment->url = tab_info->url;
context->file_infos.push_back(
searchbox::mojom::SearchContextAttachmentStub::NewTabAttachment(
std::move(tab_attachment)));
}
if (tool_mode) {
context->mode = *tool_mode;
}
OmniboxController* omnibox_controller = nullptr;
if (auto* helper =
OmniboxPopupWebContentsHelper::FromWebContents(web_contents_.get())) {
omnibox_controller = helper->get_omnibox_controller();
}
if (omnibox_controller &&
omnibox_controller->popup_state_manager()->popup_state() ==
OmniboxPopupState::kAim) {
if (auto* webui = web_contents_->GetWebUI()) {
if (auto* webui_controller = webui->GetController()) {
auto* omnibox_popup_ui = webui_controller->GetAs<OmniboxPopupUI>();
if (omnibox_popup_ui && omnibox_popup_ui->popup_aim_handler()) {
omnibox_popup_ui->popup_aim_handler()->AddContext(std::move(context));
}
}
}
} else {
searchbox_context_data->SetPendingContext(std::move(context));
}
}
raw_ptr<contextual_search::ContextualSearchContextController>
OmniboxContextMenuController::GetQueryController() {
return ContextualSearchWebContentsHelper::FromWebContents(web_contents_.get())
->session_handle()
->GetController();
}
raw_ptr<OmniboxEditModel> OmniboxContextMenuController::GetEditModel() {
return OmniboxPopupWebContentsHelper::FromWebContents(web_contents_.get())
->get_omnibox_controller()
->edit_model();
}
void OmniboxContextMenuController::ExecuteCommand(int id, int event_flags) {
// Add tab context if tab is selected.
if (id >= kMinOmniboxContextMenuRecentTabsCommandId &&
id < next_command_id_) {
std::vector<OmniboxContextMenuController::TabInfo> tabs = GetRecentTabs();
int tab_index_in_menu = id - kMinOmniboxContextMenuRecentTabsCommandId;
if (static_cast<size_t>(tab_index_in_menu) < tabs.size()) {
const auto& tab_info = tabs[tab_index_in_menu];
AddTabContext(tab_info);
}
} else {
switch (id) {
case IDC_OMNIBOX_CONTEXT_ADD_IMAGE: {
file_selector_->OpenFileUploadDialog(
web_contents_.get(),
/*is_image=*/true, GetQueryController(), GetEditModel(),
CreateImageEncodingOptions());
break;
}
case IDC_OMNIBOX_CONTEXT_ADD_FILE:
file_selector_->OpenFileUploadDialog(
web_contents_.get(),
/*is_image=*/false, GetQueryController(), GetEditModel(),
CreateImageEncodingOptions());
break;
case IDC_OMNIBOX_CONTEXT_DEEP_RESEARCH:
UpdateSearchboxContext(
/*tab_info=*/std::nullopt,
/*tool_mode=*/searchbox::mojom::ToolMode::kDeepSearch);
GetEditModel()->OpenAiMode(/*via_keyboard=*/false,
/*via_context_menu=*/true);
break;
case IDC_OMNIBOX_CONTEXT_CREATE_IMAGES:
UpdateSearchboxContext(
/*tab_info=*/std::nullopt,
/*tool_mode=*/searchbox::mojom::ToolMode::kCreateImage);
GetEditModel()->OpenAiMode(/*via_keyboard=*/false,
/*via_context_menu=*/true);
break;
default:
NOTREACHED();
}
}
}
bool OmniboxContextMenuController::IsCommandIdVisible(int command_id) const {
if (command_id == IDC_OMNIBOX_CONTEXT_DEEP_RESEARCH ||
command_id == IDC_OMNIBOX_CONTEXT_CREATE_IMAGES) {
auto* browser_window_interface =
webui::GetBrowserWindowInterface(web_contents_.get());
Profile* profile = browser_window_interface->GetProfile();
if (!profile) {
return false;
}
if (command_id == IDC_OMNIBOX_CONTEXT_DEEP_RESEARCH) {
return omnibox::IsDeepSearchEnabled(profile);
}
if (command_id == IDC_OMNIBOX_CONTEXT_CREATE_IMAGES) {
return omnibox::IsCreateImagesEnabled(profile);
}
}
return true;
}