blob: f11059a7e01d533c63b0ec3fb37a1c4f7d3776de [file] [log] [blame]
// Copyright 2012 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/tabs/recent_tabs_sub_menu_model.h"
#include <stddef.h>
#include <algorithm>
#include <memory>
#include <set>
#include "base/containers/fixed_flat_set.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/histogram_macros.h"
#include "base/metrics/user_metrics.h"
#include "base/notreached.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/app/vector_icons/vector_icons.h"
#include "chrome/browser/favicon/favicon_service_factory.h"
#include "chrome/browser/favicon/favicon_utils.h"
#include "chrome/browser/favicon/history_ui_favicon_request_handler_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/search/search.h"
#include "chrome/browser/sessions/session_restore.h"
#include "chrome/browser/sessions/tab_restore_service_factory.h"
#include "chrome/browser/signin/signin_util.h"
#include "chrome/browser/sync/session_sync_service_factory.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_live_tab_context.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
#include "chrome/browser/ui/tabs/tab_group_theme.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/toolbar/app_menu_model.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/browser/ui/views/side_panel/history_clusters/history_clusters_side_panel_coordinator.h"
#include "chrome/grit/generated_resources.h"
#include "components/favicon/core/history_ui_favicon_request_handler.h"
#include "components/favicon_base/favicon_types.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "components/sessions/core/tab_restore_service.h"
#include "components/strings/grit/components_strings.h"
#include "components/sync/base/features.h"
#include "components/sync_sessions/open_tabs_ui_delegate.h"
#include "components/sync_sessions/session_sync_service.h"
#include "components/sync_sessions/synced_session.h"
#include "components/tab_groups/tab_group_id.h"
#include "components/vector_icons/vector_icons.h"
#include "ui/base/accelerators/accelerator.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/models/image_model.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/base/ui_base_features.h"
#include "ui/base/window_open_disposition_utils.h"
#include "ui/color/color_id.h"
#include "ui/color/color_provider.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/favicon_size.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/resources/grit/ui_resources.h"
namespace {
// Command ID for recently closed items header or disabled item to which the
// accelerator string will be appended.
static constexpr int kDisabledRecentlyClosedHeaderCommandId =
AppMenuModel::kMinRecentTabsCommandId;
static constexpr int kFirstMenuEntryCommandId =
kDisabledRecentlyClosedHeaderCommandId +
AppMenuModel::kNumUnboundedMenuTypes;
// The index of the first tab in the group menu item. Before the tab item is the
// "Restore group" item and a separator.
constexpr int kInitialGroupItem = 2;
// The maximum number of local recently closed entries (tab or window) to be
// shown in the menu.
const int kMaxLocalEntries = 8;
// Comparator function for use with std::sort that will sort sessions by
// descending modified_time (i.e., most recent first).
bool SortSessionsByRecency(const sync_sessions::SyncedSession* s1,
const sync_sessions::SyncedSession* s2) {
return s1->GetModifiedTime() > s2->GetModifiedTime();
}
ui::ImageModel CreateFavicon(const gfx::VectorIcon& icon) {
return ui::ImageModel::FromVectorIcon(icon, ui::kColorMenuIcon,
gfx::kFaviconSize);
}
} // namespace
// An element in |RecentTabsSubMenuModel::local_tab_items_| or
// |RecentTabsSubMenuModel::remote_tab_items_| that stores
// the navigation information of a local or other devices' tab required to
// restore the tab.
struct RecentTabsSubMenuModel::TabItem {
TabItem() : tab_id(SessionID::InvalidValue()) {}
TabItem(const std::string& session_tag,
SessionID tab_id,
const std::u16string& title,
const GURL& url)
: session_tag(session_tag), tab_id(tab_id), title(title), url(url) {}
// For use by std::set for sorting.
bool operator<(const TabItem& other) const { return url < other.url; }
// Empty for local tabs, non-empty for other devices' tabs.
std::string session_tag;
SessionID tab_id; // Might be invalid.
std::u16string title;
GURL url;
};
// An element in |RecentTabsSubMenuModel::sub_menu_items_| that records a sub
// menu item's model and its own command id.
// TODO(emshack): This solution, where sub menus are represented by a
// SimpleMenuModel and managed by the parent RecentTabsSubMenuModel, is not
// ideal. However, AppMenuModel requires unique ids across its descendant sub
// menus, and queries a single model for these ids. This doesn't work well with
// our preferred approach, which would decouple sub menus from their parent and
// allow them to manage their own items and command ids.
struct RecentTabsSubMenuModel::SubMenuItem {
SubMenuItem(int command_id,
std::unique_ptr<ui::SimpleMenuModel> sub_menu_model)
: parent_id(command_id), menu_model(std::move(sub_menu_model)) {
const size_t child_id_count = menu_model->GetItemCount();
for (size_t i = 0; i < child_id_count; ++i) {
child_ids.insert(menu_model->GetCommandIdAt(i));
}
}
const int parent_id;
std::unordered_set<int> child_ids;
std::unique_ptr<ui::SimpleMenuModel> menu_model;
};
RecentTabsSubMenuModel::RecentTabsSubMenuModel(
ui::AcceleratorProvider* accelerator_provider,
Browser* browser)
: ui::SimpleMenuModel(this),
browser_(browser),
session_sync_service_(
SessionSyncServiceFactory::GetInstance()->GetForProfile(
browser->profile())),
next_menu_id_(kFirstMenuEntryCommandId) {
// Invoke asynchronous call to load tabs from local last session, which does
// nothing if the tabs have already been loaded or they shouldn't be loaded.
// TabRestoreServiceChanged() will be called after the tabs are loaded.
sessions::TabRestoreService* service =
TabRestoreServiceFactory::GetForProfile(browser_->profile());
if (service) {
service->LoadTabsFromLastSession();
tab_restore_service_observation_.Observe(service);
}
if (session_sync_service_) {
// Using a weak pointer below for simplicity although, strictly speaking,
// it's not needed because the subscription itself should take care.
foreign_session_updated_subscription_ =
session_sync_service_->SubscribeToForeignSessionsChanged(
base::BindRepeating(
&RecentTabsSubMenuModel::OnForeignSessionUpdated,
weak_ptr_factory_.GetWeakPtr()));
}
Build();
if (accelerator_provider) {
accelerator_provider->GetAcceleratorForCommandId(
IDC_RESTORE_TAB, &reopen_closed_tab_accelerator_);
accelerator_provider->GetAcceleratorForCommandId(
IDC_SHOW_HISTORY, &show_history_accelerator_);
}
}
RecentTabsSubMenuModel::~RecentTabsSubMenuModel() = default;
bool RecentTabsSubMenuModel::IsCommandIdChecked(int command_id) const {
return false;
}
bool RecentTabsSubMenuModel::IsCommandIdEnabled(int command_id) const {
return command_id != kDisabledRecentlyClosedHeaderCommandId &&
command_id != IDC_RECENT_TABS_NO_DEVICE_TABS;
}
bool RecentTabsSubMenuModel::GetAcceleratorForCommandId(
int command_id,
ui::Accelerator* accelerator) const {
if (command_id == IDC_SHOW_HISTORY) {
*accelerator = show_history_accelerator_;
return true;
}
if (command_id == kDisabledRecentlyClosedHeaderCommandId) {
*accelerator = reopen_closed_tab_accelerator_;
return true;
}
// If there are no recently closed items, we show the accelerator beside
// the header, otherwise, we show it beside the first item underneath it.
// If the first item underneath it is a submenu, we instead show it beside
// the first item in that submenu.
if (recent_tabs_title_index_.has_value() &&
reopen_closed_tab_accelerator_.key_code() != ui::VKEY_UNKNOWN) {
const std::optional<size_t> index_in_menu = GetIndexOfCommandId(command_id);
// Ensure the first tab in `local_tab_items_` is not nested in a group or
// window submenu by checking its index.
const bool tab_is_first_item =
command_id == local_tab_items_.begin()->first &&
(index_in_menu == recent_tabs_title_index_.value() + 1);
if (tab_is_first_item) {
*accelerator = reopen_closed_tab_accelerator_;
return true;
}
const int parent_id = GetParentCommandId(command_id);
const std::optional<size_t> parent_index =
parent_id == -1 ? std::nullopt : GetIndexOfCommandId(parent_id);
// The first item is "Restore (Group / Window)".
const bool group_or_window_is_first_item =
(command_id == local_group_items_.begin()->first ||
command_id == local_window_items_.begin()->first) &&
parent_index == recent_tabs_title_index_.value() + 1;
if (group_or_window_is_first_item) {
*accelerator = reopen_closed_tab_accelerator_;
return true;
}
}
return false;
}
bool RecentTabsSubMenuModel::ExecuteCustomCommand(int command_id,
int event_flags) {
// Supported custom commands.
static constexpr auto custom_commands = base::MakeFixedFlatSet<int>(
{IDC_SHOW_HISTORY, IDC_SHOW_HISTORY_CLUSTERS_SIDE_PANEL,
IDC_RECENT_TABS_LOGIN_FOR_DEVICE_TABS});
if (!custom_commands.contains(command_id)) {
return false;
}
if (command_id == IDC_SHOW_HISTORY_CLUSTERS_SIDE_PANEL &&
!HistoryClustersSidePanelCoordinator::IsSupported(browser_->profile())) {
return false;
}
if (log_menu_metrics_callback_) {
log_menu_metrics_callback_.Run(command_id);
}
if (command_id == IDC_SHOW_HISTORY) {
LogWrenchMenuAction(MENU_ACTION_SHOW_HISTORY);
}
chrome::ExecuteCommandWithDisposition(
browser_, command_id, ui::DispositionFromEventFlags(event_flags));
return true;
}
void RecentTabsSubMenuModel::ExecuteCommand(int command_id, int event_flags) {
if (ExecuteCustomCommand(command_id, event_flags)) {
return;
}
DCHECK_NE(IDC_RECENT_TABS_NO_DEVICE_TABS, command_id);
sessions::TabRestoreService* service =
TabRestoreServiceFactory::GetForProfile(browser_->profile());
CHECK(service);
sessions::LiveTabContext* context =
browser_->GetFeatures().live_tab_context();
CHECK(context);
WindowOpenDisposition disposition = ui::DispositionFromEventFlags(
event_flags, WindowOpenDisposition::NEW_FOREGROUND_TAB);
if (IsCommandType(CommandType::Tab, command_id)) {
const TabItems& tab_items = *GetTabVectorForCommandId(command_id);
const TabItem& item = tab_items.at(command_id);
DCHECK(item.tab_id.is_valid() && item.url.is_valid());
if (item.session_tag.empty()) { // Restore tab of local session.
base::RecordAction(
base::UserMetricsAction("WrenchMenu_OpenRecentTabFromLocal"));
service->RestoreEntryById(context, item.tab_id, disposition);
} else { // Restore tab of session from other devices.
sync_sessions::OpenTabsUIDelegate* open_tabs = GetOpenTabsUIDelegate();
if (!open_tabs) {
return;
}
const sessions::SessionTab* session_tab;
if (!open_tabs->GetForeignTab(item.session_tag, item.tab_id,
&session_tab)) {
return;
}
if (session_tab->navigations.empty()) {
return;
}
base::RecordAction(
base::UserMetricsAction("WrenchMenu_OpenRecentTabFromDevice"));
SessionRestore::RestoreForeignSessionTab(
browser_->tab_strip_model()->GetActiveWebContents(), *session_tab,
disposition);
}
} else if (IsCommandType(CommandType::Window, command_id)) {
base::RecordAction(base::UserMetricsAction("WrenchMenu_OpenRecentWindow"));
service->RestoreEntryById(context, local_window_items_.at(command_id),
disposition);
} else if (IsCommandType(CommandType::Group, command_id)) {
base::RecordAction(base::UserMetricsAction("WrenchMenu_OpenRecentGroup"));
service->RestoreEntryById(context, local_group_items_.at(command_id),
disposition);
} else {
CHECK(IsCommandType(CommandType::Submenu, command_id) ||
IsCommandType(CommandType::OtherDevice, command_id));
return;
}
if (log_menu_metrics_callback_) {
log_menu_metrics_callback_.Run(IDC_OPEN_RECENT_TAB);
}
}
// static
int RecentTabsSubMenuModel::GetDisabledRecentlyClosedHeaderCommandId() {
return kDisabledRecentlyClosedHeaderCommandId;
}
int RecentTabsSubMenuModel::GetFirstRecentTabsCommandId() {
return local_window_items_.begin()->first;
}
void RecentTabsSubMenuModel::RegisterLogMenuMetricsCallback(
LogMenuMetricsCallback callback) {
log_menu_metrics_callback_ = std::move(callback);
}
void RecentTabsSubMenuModel::Build() {
// The menu contains:
// - History to open the full history tab.
// - History to open in side panel.
// - Separator
// - Recent tabs header
// - A list of local recently closed tabs, groups, and/or windows.
// - Separator
// - Your devices header
// - A list of remote devices.
InsertItemWithStringIdAt(0, IDC_SHOW_HISTORY, IDS_HISTORY_SHOW_HISTORY);
SetCommandIcon(this, IDC_SHOW_HISTORY,
vector_icons::kHistoryChromeRefreshIcon);
if (browser_->GetFeatures().side_panel_coordinator() &&
HistoryClustersSidePanelCoordinator::IsSupported(browser_->profile())) {
InsertItemWithStringIdAt(1, IDC_SHOW_HISTORY_CLUSTERS_SIDE_PANEL,
IDS_HISTORY_CLUSTERS_SHOW_SIDE_PANEL);
SetCommandIcon(this, IDC_SHOW_HISTORY_CLUSTERS_SIDE_PANEL,
vector_icons::kHistoryChromeRefreshIcon);
}
AddSeparator(ui::NORMAL_SEPARATOR);
history_separator_index_ = GetItemCount() - 1;
BuildLocalEntries();
BuildTabsFromOtherDevices();
}
void RecentTabsSubMenuModel::BuildLocalEntries() {
last_local_model_index_ = history_separator_index_;
// All local items use InsertItem*At() to append or insert a menu item.
// We're appending if building the entries for the first time i.e. invoked
// from Constructor(), inserting when local entries change subsequently i.e.
// invoked from TabRestoreServiceChanged().
sessions::TabRestoreService* service =
TabRestoreServiceFactory::GetForProfile(browser_->profile());
if (!service || service->entries().empty()) {
// This is to show a disabled restore tab entry with the accelerator to
// teach users about this command.
InsertItemWithStringIdAt(++last_local_model_index_,
kDisabledRecentlyClosedHeaderCommandId,
IDS_RECENT_TABS);
} else {
recent_tabs_title_index_ = ++last_local_model_index_;
InsertTitleWithStringIdAt(recent_tabs_title_index_.value(),
IDS_RECENT_TABS);
int added_count = 0;
for (const auto& entry : service->entries()) {
if (added_count == kMaxLocalEntries) {
break;
}
switch (entry->type) {
case sessions::tab_restore::Type::TAB: {
auto& tab = static_cast<const sessions::tab_restore::Tab&>(*entry);
BuildLocalTabItem(tab, ++last_local_model_index_);
break;
}
case sessions::tab_restore::Type::WINDOW: {
auto& window = static_cast<sessions::tab_restore::Window&>(*entry);
// TODO(https://crbug.com/41227458): Consider if we should re-add the
// ability for single tab windows to be represented as single tabs.
BuildLocalWindowItem(window, ++last_local_model_index_);
break;
}
case sessions::tab_restore::Type::GROUP: {
auto& group =
static_cast<const sessions::tab_restore::Group&>(*entry);
BuildLocalGroupItem(group, ++last_local_model_index_);
break;
}
}
++added_count;
}
}
CHECK_GE(last_local_model_index_, 0u);
}
void RecentTabsSubMenuModel::BuildTabsFromOtherDevices() {
// This option should not be built if history sync is disabled by policy.
if (base::FeatureList::IsEnabled(
syncer::kReplaceSyncPromosWithSignInPromos) &&
!signin_util::IsHistorySyncOptinAllowedByPolicy(*browser_->profile())) {
return;
}
// All other devices' items (device headers or tabs) use AddItem*() to append
// a menu item, because they take always place in the end of menu.
AddSeparator(ui::NORMAL_SEPARATOR);
AddTitleWithStringId(IDS_YOUR_DEVICES);
sync_sessions::OpenTabsUIDelegate* open_tabs = GetOpenTabsUIDelegate();
std::vector<raw_ptr<const sync_sessions::SyncedSession, VectorExperimental>>
sessions;
if (!open_tabs || !open_tabs->GetAllForeignSessions(&sessions)) {
if (open_tabs) {
AddItemWithStringId(IDC_RECENT_TABS_NO_DEVICE_TABS,
IDS_RECENT_TABS_NO_DEVICE_TABS);
} else {
AddItemWithStringIdAndIcon(IDC_RECENT_TABS_LOGIN_FOR_DEVICE_TABS,
IDS_RECENT_TABS_LOGIN_FOR_DEVICE_TABS,
ui::ImageModel::FromVectorIcon(
kSyncRefreshIcon, ui::kColorMenuIcon,
ui::SimpleMenuModel::kDefaultIconSize));
}
return;
}
// Sort sessions from most recent to least recent.
std::sort(sessions.begin(), sessions.end(), SortSessionsByRecency);
const size_t kMaxSessionsToShow = 8;
size_t num_sessions_added = 0;
for (size_t i = 0;
i < sessions.size() && num_sessions_added < kMaxSessionsToShow; ++i) {
const sync_sessions::SyncedSession* session = sessions[i];
const std::string& session_tag = session->GetSessionTag();
// Collect tabs from all windows of the session, ordered by recency.
std::vector<const sessions::SessionTab*> tabs_in_session;
if (!open_tabs->GetForeignSessionTabs(session_tag, &tabs_in_session) ||
tabs_in_session.empty()) {
continue;
}
// Add the header for the device session.
DCHECK(!session->GetSessionName().empty());
std::unique_ptr<ui::SimpleMenuModel> device_menu_model =
CreateOtherDeviceSubMenu(session, tabs_in_session);
const int command_id = GetAndIncrementNextMenuID();
AddSubMenu(command_id, base::UTF8ToUTF16(session->GetSessionName()),
device_menu_model.get());
AddDeviceFavicon(this, GetItemCount() - 1, session->GetDeviceFormFactor());
remote_sub_menu_items_.emplace(
command_id, SubMenuItem(command_id, std::move(device_menu_model)));
++num_sessions_added;
} // for all sessions
// We are not supposed to get here unless at least some items were added.
DCHECK_GT(GetItemCount(), 0u);
}
void RecentTabsSubMenuModel::BuildLocalTabItem(
const sessions::tab_restore::Tab& tab,
size_t curr_model_index) {
const int command_id = GetAndIncrementNextMenuID();
const sessions::SerializedNavigationEntry& current_navigation =
tab.navigations.at(tab.normalized_navigation_index());
TabItem item(std::string(), tab.id, current_navigation.title(),
current_navigation.virtual_url());
// See comments in BuildLocalEntries() about usage of InsertItem*At().
// There may be no tab title, in which case, use the url as tab title.
InsertItemAt(
curr_model_index, command_id,
item.title.empty() ? base::UTF8ToUTF16(item.url.spec()) : item.title);
local_tab_items_.emplace(command_id, item);
AddTabFavicon(command_id, this, item.url);
// We shouldn't get here if there is no recently closed title.
DCHECK(recent_tabs_title_index_.has_value());
// visual_data should only be populated if the tab was part of a tab group
// when closed. We shouldn't set the minor icon for the item most recently
// closed, as this creates visual clutter alongside the shortcut text.
if (tab.group_visual_data.has_value() &&
curr_model_index > recent_tabs_title_index_.value() + 1) {
const ui::ColorProvider* color_provider =
browser_->window()->GetColorProvider();
const ui::ColorId color_id =
GetTabGroupContextMenuColorId(tab.group_visual_data.value().color());
constexpr int kIconSize = 12;
SetMinorIcon(
curr_model_index,
ui::ImageModel::FromVectorIcon(
kTabGroupIcon, color_provider->GetColor(color_id), kIconSize));
}
}
void RecentTabsSubMenuModel::BuildLocalWindowItem(
const sessions::tab_restore::Window& window,
size_t curr_model_index) {
const int command_id = GetAndIncrementNextMenuID();
std::unique_ptr<ui::SimpleMenuModel> window_model =
CreateWindowSubMenuModel(window);
InsertSubMenuAt(
curr_model_index, command_id,
l10n_util::GetPluralStringFUTF16(IDS_RECENTLY_CLOSED_WINDOW,
static_cast<int>(window.tabs.size())),
window_model.get());
local_sub_menu_items_.emplace(
command_id, SubMenuItem(command_id, std::move(window_model)));
SetIcon(curr_model_index, CreateFavicon(kTabIcon));
}
void RecentTabsSubMenuModel::BuildLocalGroupItem(
const sessions::tab_restore::Group& group,
size_t curr_model_index) {
// Set the item label to the name of the group and the number of
// tabs.
std::u16string item_label =
GetGroupItemLabel(group.visual_data.title(), group.tabs.size());
const int command_id = GetAndIncrementNextMenuID();
std::unique_ptr<ui::SimpleMenuModel> group_model =
CreateGroupSubMenuModel(group);
InsertSubMenuAt(curr_model_index, command_id, item_label, group_model.get());
local_sub_menu_items_.emplace(
command_id, SubMenuItem(command_id, std::move(group_model)));
// Set the item icon to the group color.
const ui::ColorProvider* color_provider =
browser_->window()->GetColorProvider();
const ui::ColorId color_id =
GetTabGroupContextMenuColorId(group.visual_data.color());
ui::ImageModel group_icon = ui::ImageModel::FromVectorIcon(
kTabGroupIcon, color_provider->GetColor(color_id), gfx::kFaviconSize);
SetIcon(curr_model_index, group_icon);
}
void RecentTabsSubMenuModel::BuildOtherDevicesTabItem(
SimpleMenuModel* containing_model,
const std::string& session_tag,
const sessions::SessionTab& tab) {
const int command_id = GetAndIncrementNextMenuID();
const sessions::SerializedNavigationEntry& current_navigation =
tab.navigations.at(tab.normalized_navigation_index());
TabItem item(session_tag, tab.tab_id, current_navigation.title(),
current_navigation.virtual_url());
// See comments in BuildTabsFromOtherDevices() about usage of AddItem*().
// There may be no tab title, in which case, use the url as tab title.
containing_model->AddItem(command_id, current_navigation.title().empty()
? base::UTF8ToUTF16(item.url.spec())
: current_navigation.title());
remote_tab_items_.emplace(command_id, item);
AddTabFavicon(command_id, containing_model, item.url);
}
std::unique_ptr<ui::SimpleMenuModel>
RecentTabsSubMenuModel::CreateOtherDeviceSubMenu(
const sync_sessions::SyncedSession* session,
const std::vector<const sessions::SessionTab*>& tabs_in_session) {
auto other_device_submenu = std::make_unique<ui::SimpleMenuModel>(this);
const std::string& session_tag = session->GetSessionTag();
for (auto* tab : tabs_in_session) {
BuildOtherDevicesTabItem(other_device_submenu.get(), session_tag, *tab);
}
return other_device_submenu;
}
std::unique_ptr<ui::SimpleMenuModel>
RecentTabsSubMenuModel::CreateWindowSubMenuModel(
const sessions::tab_restore::Window& window) {
std::unique_ptr<ui::SimpleMenuModel> window_model =
std::make_unique<ui::SimpleMenuModel>(this);
const int restore_all_command_id = GetAndIncrementNextMenuID();
window_model->AddItemWithStringIdAndIcon(
restore_all_command_id, IDS_RESTORE_WINDOW,
ui::ImageModel::FromVectorIcon(vector_icons::kLaunchIcon));
local_window_items_.emplace(restore_all_command_id, window.id);
window_model->AddSeparator(ui::NORMAL_SEPARATOR);
base::flat_set<tab_groups::TabGroupId> seen_groups;
std::unique_ptr<ui::SimpleMenuModel> current_group_model;
sessions::tab_restore::Group* current_group;
for (const auto& tab : window.tabs) {
if (tab->group.has_value() && !seen_groups.contains(tab->group.value())) {
if (current_group_model) {
// Add the current group before we start working on the next one.
AddGroupItemToModel(window_model.get(), std::move(current_group_model),
*current_group);
}
CHECK(window.tab_groups.contains(tab->group.value()));
seen_groups.emplace(tab->group.value());
current_group = window.tab_groups.at(tab->group.value()).get();
current_group_model = CreateGroupSubMenuModel(*current_group);
}
const int tab_command_id = GetAndIncrementNextMenuID();
if (!tab->group.has_value()) {
// Add the tab item to the window.
AddTabItemToModel(tab.get(), window_model.get(), tab_command_id);
} else {
// Add the tab item to the current group.
AddTabItemToModel(tab.get(), current_group_model.get(), tab_command_id);
}
}
if (current_group_model) {
AddGroupItemToModel(window_model.get(), std::move(current_group_model),
*current_group);
}
return window_model;
}
std::unique_ptr<ui::SimpleMenuModel>
RecentTabsSubMenuModel::CreateGroupSubMenuModel(
const sessions::tab_restore::Group& group) {
std::unique_ptr<ui::SimpleMenuModel> group_model =
std::make_unique<ui::SimpleMenuModel>(this);
int command_id = GetAndIncrementNextMenuID();
group_model->AddItemWithStringIdAndIcon(
command_id, IDS_RESTORE_GROUP,
ui::ImageModel::FromVectorIcon(vector_icons::kLaunchIcon));
local_group_items_.emplace(command_id, group.id);
group_model->AddSeparator(ui::NORMAL_SEPARATOR);
CHECK_EQ(static_cast<int>(group_model->GetItemCount()), kInitialGroupItem);
for (auto& tab : group.tabs) {
command_id = GetAndIncrementNextMenuID();
AddTabItemToModel(tab.get(), group_model.get(), command_id);
}
return group_model;
}
void RecentTabsSubMenuModel::AddGroupItemToModel(
SimpleMenuModel* parent_model,
std::unique_ptr<SimpleMenuModel> group_model,
const sessions::tab_restore::Group& group) {
// We only want to count the tabs in the group model. So we subtract the
// "Restore group" and separator items from the count.
const int item_count = group_model->GetItemCount() - kInitialGroupItem;
const int sub_menu_command_id = GetAndIncrementNextMenuID();
const std::u16string sub_menu_label =
GetGroupItemLabel(group.visual_data.title(), item_count);
// Set the item icon to the group color.
const ui::ColorProvider* color_provider =
browser_->window()->GetColorProvider();
const ui::ColorId color_id =
GetTabGroupContextMenuColorId(group.visual_data.color());
ui::ImageModel group_icon = ui::ImageModel::FromVectorIcon(
kTabGroupIcon, color_provider->GetColor(color_id), gfx::kFaviconSize);
parent_model->AddSubMenuWithIcon(sub_menu_command_id, sub_menu_label,
group_model.get(), group_icon);
local_sub_menu_items_.emplace(
sub_menu_command_id,
SubMenuItem(sub_menu_command_id, std::move(group_model)));
}
void RecentTabsSubMenuModel::AddTabItemToModel(
const sessions::tab_restore::Tab* tab,
ui::SimpleMenuModel* model,
int command_id) {
const sessions::SerializedNavigationEntry& current_navigation =
tab->navigations.at(tab->normalized_navigation_index());
TabItem item(std::string(), tab->id, current_navigation.title(),
current_navigation.virtual_url());
local_tab_items_.emplace(command_id, item);
// There may be no tab title, in which case, use the url as tab title.
model->AddItem(command_id, current_navigation.title().empty()
? base::UTF8ToUTF16(
current_navigation.virtual_url().spec())
: current_navigation.title());
AddTabFavicon(command_id, model, current_navigation.virtual_url());
}
std::u16string RecentTabsSubMenuModel::GetGroupItemLabel(std::u16string title,
size_t num_tabs) {
std::u16string item_label;
if (title.empty()) {
item_label = l10n_util::GetPluralStringFUTF16(
IDS_RECENTLY_CLOSED_GROUP_UNNAMED, static_cast<int>(num_tabs));
} else {
item_label = l10n_util::GetPluralStringFUTF16(IDS_RECENTLY_CLOSED_GROUP,
static_cast<int>(num_tabs));
item_label = base::ReplaceStringPlaceholders(item_label, title, nullptr);
}
return item_label;
}
int RecentTabsSubMenuModel::GetParentCommandId(int command_id) const {
for (auto& sub_menu_item : local_sub_menu_items_) {
if (sub_menu_item.second.child_ids.find(command_id) !=
sub_menu_item.second.child_ids.end()) {
return sub_menu_item.second.parent_id;
}
}
return -1;
}
void RecentTabsSubMenuModel::AddDeviceFavicon(
SimpleMenuModel* containing_model,
size_t index_in_menu,
syncer::DeviceInfo::FormFactor device_form_factor) {
const gfx::VectorIcon* favicon = nullptr;
switch (device_form_factor) {
case syncer::DeviceInfo::FormFactor::kPhone:
favicon = &kSmartphoneIcon;
break;
case syncer::DeviceInfo::FormFactor::kTablet:
favicon = &kTabletIcon;
break;
// Return the laptop icon as default.
case syncer::DeviceInfo::FormFactor::kUnknown:
[[fallthrough]];
case syncer::DeviceInfo::FormFactor::kAutomotive:
[[fallthrough]];
case syncer::DeviceInfo::FormFactor::kWearable:
[[fallthrough]];
case syncer::DeviceInfo::FormFactor::kTv:
[[fallthrough]];
case syncer::DeviceInfo::FormFactor::kDesktop:
favicon = &kLaptopIcon;
break;
}
containing_model->SetIcon(index_in_menu, CreateFavicon(*favicon));
}
void RecentTabsSubMenuModel::AddTabFavicon(int command_id,
ui::SimpleMenuModel* menu_model,
const GURL& url) {
const size_t index_in_menu =
menu_model->GetIndexOfCommandId(command_id).value();
// Set default icon first.
menu_model->SetIcon(index_in_menu, favicon::GetDefaultFaviconModel());
const bool is_local_tab = IsCommandType(CommandType::LocalTab, command_id);
if (is_local_tab) {
// Request only from local storage to avoid leaking user data.
favicon::FaviconService* favicon_service =
FaviconServiceFactory::GetForProfile(
browser_->profile(), ServiceAccessType::EXPLICIT_ACCESS);
// Can be null for tests.
if (!favicon_service) {
return;
}
favicon_service->GetFaviconImageForPageURL(
url,
base::BindOnce(&RecentTabsSubMenuModel::OnFaviconDataAvailable,
weak_ptr_factory_.GetWeakPtr(), command_id, menu_model),
&local_tab_cancelable_task_tracker_);
} else {
favicon::HistoryUiFaviconRequestHandler*
history_ui_favicon_request_handler =
HistoryUiFaviconRequestHandlerFactory::GetForBrowserContext(
browser_->profile());
// Can be null for tests.
if (!history_ui_favicon_request_handler) {
return;
}
history_ui_favicon_request_handler->GetFaviconImageForPageURL(
url,
base::BindOnce(&RecentTabsSubMenuModel::OnFaviconDataAvailable,
weak_ptr_factory_for_other_devices_tab_.GetWeakPtr(),
command_id, menu_model));
}
}
void RecentTabsSubMenuModel::OnFaviconDataAvailable(
int command_id,
ui::SimpleMenuModel* menu_model,
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));
ui::MenuModelDelegate* delegate = menu_model_delegate();
if (delegate) {
delegate->OnIconChanged(command_id);
}
return;
}
RecentTabsSubMenuModel::TabItems*
RecentTabsSubMenuModel::GetTabVectorForCommandId(int command_id) {
DCHECK(IsCommandType(CommandType::Tab, command_id));
return IsCommandType(CommandType::RemoteTab, command_id) ? &remote_tab_items_
: &local_tab_items_;
}
void RecentTabsSubMenuModel::ClearLocalEntries() {
// Remove local items (recently closed tabs and windows) from menumodel.
while (last_local_model_index_ > history_separator_index_) {
RemoveItemAt(last_local_model_index_--);
}
recent_tabs_title_index_.reset();
// Cancel asynchronous FaviconService::GetFaviconImageForPageURL() tasks of
// all local tabs.
local_tab_cancelable_task_tracker_.TryCancelAll();
// Remove all local items.
local_tab_items_.clear();
local_group_items_.clear();
local_window_items_.clear();
local_sub_menu_items_.clear();
}
void RecentTabsSubMenuModel::ClearTabsFromOtherDevices() {
for (size_t index = GetItemCount(); index > last_local_model_index_ + 1;
--index) {
RemoveItemAt(index - 1);
}
weak_ptr_factory_for_other_devices_tab_.InvalidateWeakPtrs();
remote_tab_items_.clear();
remote_sub_menu_items_.clear();
}
sync_sessions::OpenTabsUIDelegate*
RecentTabsSubMenuModel::GetOpenTabsUIDelegate() {
DCHECK(session_sync_service_);
return session_sync_service_->GetOpenTabsUIDelegate();
}
void RecentTabsSubMenuModel::TabRestoreServiceChanged(
sessions::TabRestoreService* service) {
ClearLocalEntries();
BuildLocalEntries();
ui::MenuModelDelegate* delegate = menu_model_delegate();
if (delegate) {
delegate->OnMenuStructureChanged();
}
}
void RecentTabsSubMenuModel::TabRestoreServiceDestroyed(
sessions::TabRestoreService* service) {
TabRestoreServiceChanged(service);
}
void RecentTabsSubMenuModel::OnForeignSessionUpdated() {
ClearTabsFromOtherDevices();
BuildTabsFromOtherDevices();
ui::MenuModelDelegate* delegate = menu_model_delegate();
if (delegate) {
delegate->OnMenuStructureChanged();
}
}
int RecentTabsSubMenuModel::GetAndIncrementNextMenuID() {
const int current_id = next_menu_id_;
next_menu_id_ += AppMenuModel::kNumUnboundedMenuTypes;
return current_id;
}
bool RecentTabsSubMenuModel::IsCommandType(CommandType command_type,
int command_id) const {
switch (command_type) {
case Tab:
return local_tab_items_.contains(command_id) ||
remote_tab_items_.contains(command_id);
case LocalTab:
return local_tab_items_.contains(command_id);
case RemoteTab:
return remote_tab_items_.contains(command_id);
case Group:
return local_group_items_.contains(command_id);
case Window:
return local_window_items_.contains(command_id);
case Submenu:
return local_sub_menu_items_.contains(command_id);
case OtherDevice:
return remote_sub_menu_items_.contains(command_id);
}
NOTREACHED();
}