| // 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/toolbar/recent_tabs_sub_menu_model.h" |
| |
| #include <stddef.h> |
| |
| #include <algorithm> |
| #include <memory> |
| |
| #include "base/functional/bind.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/metrics/user_metrics.h" |
| #include "base/strings/string_number_conversions.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/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/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/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_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 { |
| |
| // 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 |
| |
| enum RecentTabAction { |
| LOCAL_SESSION_TAB = 0, |
| OTHER_DEVICE_TAB, |
| RESTORE_WINDOW, |
| SHOW_MORE, |
| LIMIT_RECENT_TAB_ACTION, |
| RESTORE_GROUP |
| }; |
| |
| // An element in |RecentTabsSubMenuModel::local_tab_navigation_items_| or |
| // |RecentTabsSubMenuModel::other_devices_tab_navigation_items_| that stores |
| // the navigation information of a local or other devices' tab required to |
| // restore the tab. |
| struct RecentTabsSubMenuModel::TabNavigationItem { |
| TabNavigationItem() : tab_id(SessionID::InvalidValue()) {} |
| |
| TabNavigationItem(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 TabNavigationItem& 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 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. |
| const absl::optional<size_t> index_in_menu = GetIndexOfCommandId(command_id); |
| const int parent_id = GetParentCommandId(command_id); |
| const absl::optional<size_t> parent_index = |
| parent_id == -1 ? absl::nullopt : GetIndexOfCommandId(parent_id); |
| if ((command_id == kDisabledRecentlyClosedHeaderCommandId || |
| (recently_closed_title_index_.has_value() && |
| ((!IsSubMenuModelCommandId(command_id) && |
| index_in_menu == recently_closed_title_index_.value() + 1) || |
| ((command_id == local_window_items_.begin()->first || |
| command_id == local_group_items_.begin()->first) && |
| parent_index == recently_closed_title_index_.value() + 1)))) && |
| reopen_closed_tab_accelerator_.key_code() != ui::VKEY_UNKNOWN) { |
| *accelerator = reopen_closed_tab_accelerator_; |
| return true; |
| } |
| |
| if (command_id == IDC_SHOW_HISTORY) { |
| *accelerator = show_history_accelerator_; |
| return true; |
| } |
| |
| return false; |
| } |
| |
| void RecentTabsSubMenuModel::ExecuteCommand(int command_id, int event_flags) { |
| if (command_id == IDC_SHOW_HISTORY) { |
| UMA_HISTOGRAM_ENUMERATION("WrenchMenu.RecentTabsSubMenu", SHOW_MORE, |
| LIMIT_RECENT_TAB_ACTION); |
| if (log_menu_metrics_callback_) { |
| log_menu_metrics_callback_.Run(command_id); |
| } |
| LogWrenchMenuAction(MENU_ACTION_SHOW_HISTORY); |
| // We show all "other devices" on the history page. |
| chrome::ExecuteCommandWithDisposition( |
| browser_, IDC_SHOW_HISTORY, ui::DispositionFromEventFlags(event_flags)); |
| return; |
| } |
| if (command_id == IDC_RECENT_TABS_LOGIN_FOR_DEVICE_TABS) { |
| if (log_menu_metrics_callback_) { |
| log_menu_metrics_callback_.Run(command_id); |
| } |
| chrome::ExecuteCommandWithDisposition( |
| browser_, IDC_RECENT_TABS_LOGIN_FOR_DEVICE_TABS, |
| ui::DispositionFromEventFlags(event_flags)); |
| return; |
| } |
| |
| DCHECK_NE(IDC_RECENT_TABS_NO_DEVICE_TABS, command_id); |
| |
| WindowOpenDisposition disposition = ui::DispositionFromEventFlags( |
| event_flags, WindowOpenDisposition::NEW_FOREGROUND_TAB); |
| |
| sessions::TabRestoreService* service = |
| TabRestoreServiceFactory::GetForProfile(browser_->profile()); |
| sessions::LiveTabContext* context = |
| BrowserLiveTabContext::FindContextForWebContents( |
| browser_->tab_strip_model()->GetActiveWebContents()); |
| if (IsTabModelCommandId(command_id)) { |
| TabNavigationItems* tab_items = GetTabVectorForCommandId(command_id); |
| const TabNavigationItem& item = (*tab_items)[command_id]; |
| DCHECK(item.tab_id.is_valid() && item.url.is_valid()); |
| |
| if (item.session_tag.empty()) { // Restore tab of local session. |
| if (service && context) { |
| base::RecordAction( |
| base::UserMetricsAction("WrenchMenu_OpenRecentTabFromLocal")); |
| UMA_HISTOGRAM_ENUMERATION("WrenchMenu.RecentTabsSubMenu", |
| LOCAL_SESSION_TAB, LIMIT_RECENT_TAB_ACTION); |
| 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* tab; |
| if (!open_tabs->GetForeignTab(item.session_tag, item.tab_id, &tab)) |
| return; |
| if (tab->navigations.empty()) { |
| return; |
| } |
| base::RecordAction( |
| base::UserMetricsAction("WrenchMenu_OpenRecentTabFromDevice")); |
| UMA_HISTOGRAM_ENUMERATION("WrenchMenu.RecentTabsSubMenu", |
| OTHER_DEVICE_TAB, LIMIT_RECENT_TAB_ACTION); |
| SessionRestore::RestoreForeignSessionTab( |
| browser_->tab_strip_model()->GetActiveWebContents(), *tab, |
| disposition); |
| } |
| } else if (IsWindowModelCommandId(command_id)) { |
| if (service && context) { |
| base::RecordAction( |
| base::UserMetricsAction("WrenchMenu_OpenRecentWindow")); |
| UMA_HISTOGRAM_ENUMERATION("WrenchMenu.RecentTabsSubMenu", RESTORE_WINDOW, |
| LIMIT_RECENT_TAB_ACTION); |
| service->RestoreEntryById(context, local_window_items_.at(command_id), |
| disposition); |
| } |
| } else if (IsGroupModelCommandId(command_id)) { |
| base::RecordAction(base::UserMetricsAction("WrenchMenu_OpenRecentGroup")); |
| UMA_HISTOGRAM_ENUMERATION("WrenchMenu.RecentTabsSubMenu", RESTORE_GROUP, |
| LIMIT_RECENT_TAB_ACTION); |
| service->RestoreEntryById(context, local_group_items_.at(command_id), |
| disposition); |
| } else { |
| CHECK(IsSubMenuModelCommandId(command_id)); |
| return; |
| } |
| |
| if (log_menu_metrics_callback_) { |
| log_menu_metrics_callback_.Run(IDC_OPEN_RECENT_TAB); |
| } |
| } |
| |
| 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. |
| // - Separator |
| // - Recently closed header, then list of local recently closed tabs/windows, |
| // then separator |
| // - device 1 section header, then list of tabs from device, then separator |
| // - device 2 section header, then list of tabs from device, then separator |
| // - device 3 section header, then list of tabs from device, then separator |
| // |local_tab_navigation_items_| and |other_devices_tab_navigation_items_| |
| // only contain navigatable (and hence executable) tab items for local |
| // recently closed tabs and tabs from other devices respectively. |
| // |local_window_items_| contains the local recently closed windows. |
| InsertItemWithStringIdAt(0, IDC_SHOW_HISTORY, IDS_HISTORY_SHOW_HISTORY); |
| if (features::IsChromeRefresh2023()) { |
| SetCommandIcon(this, IDC_SHOW_HISTORY, |
| vector_icons::kHistoryChromeRefreshIcon); |
| } |
| InsertSeparatorAt(1, ui::NORMAL_SEPARATOR); |
| BuildLocalEntries(); |
| BuildTabsFromOtherDevices(); |
| } |
| |
| void RecentTabsSubMenuModel::BuildLocalEntries() { |
| last_local_model_index_ = kHistorySeparatorIndex; |
| |
| // 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, |
| features::IsChromeRefresh2023() ? IDS_RECENT_TABS |
| : IDS_RECENTLY_CLOSED); |
| } else { |
| recently_closed_title_index_ = ++last_local_model_index_; |
| InsertTitleWithStringIdAt(recently_closed_title_index_.value(), |
| features::IsChromeRefresh2023() |
| ? IDS_RECENT_TABS |
| : IDS_RECENTLY_CLOSED); |
| if (!features::IsChromeRefresh2023()) { |
| SetIcon(last_local_model_index_, CreateFavicon(kTabIcon)); |
| } |
| |
| int added_count = 0; |
| for (const auto& entry : service->entries()) { |
| if (added_count == kMaxLocalEntries) { |
| break; |
| } |
| switch (entry->type) { |
| case sessions::TabRestoreService::TAB: { |
| auto& tab = |
| static_cast<const sessions::TabRestoreService::Tab&>(*entry); |
| const sessions::SerializedNavigationEntry& current_navigation = |
| tab.navigations.at(tab.current_navigation_index); |
| BuildLocalTabItem( |
| entry->id, tab.group_visual_data, current_navigation.title(), |
| current_navigation.virtual_url(), ++last_local_model_index_); |
| break; |
| } |
| case sessions::TabRestoreService::WINDOW: { |
| auto& window = |
| static_cast<sessions::TabRestoreService::Window&>(*entry); |
| BuildLocalWindowItem(entry->id, CreateWindowSubMenuModel(window), |
| window.tabs.size(), ++last_local_model_index_); |
| break; |
| } |
| case sessions::TabRestoreService::GROUP: { |
| auto& group = |
| static_cast<const sessions::TabRestoreService::Group&>(*entry); |
| BuildLocalGroupItem(group.id, group.visual_data, |
| CreateGroupSubMenuModel(group), group.tabs.size(), |
| ++last_local_model_index_); |
| break; |
| } |
| } |
| ++added_count; |
| } |
| } |
| DCHECK_GE(last_local_model_index_, 0u); |
| } |
| |
| void RecentTabsSubMenuModel::BuildTabsFromOtherDevices() { |
| // 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. |
| |
| if (features::IsChromeRefresh2023()) { |
| AddSeparator(ui::NORMAL_SEPARATOR); |
| AddTitleWithStringId(IDS_YOUR_DEVICES); |
| } |
| |
| sync_sessions::OpenTabsUIDelegate* open_tabs = GetOpenTabsUIDelegate(); |
| std::vector<const sync_sessions::SyncedSession*> sessions; |
| if (!open_tabs || !open_tabs->GetAllForeignSessions(&sessions)) { |
| if (!features::IsChromeRefresh2023()) { |
| AddSeparator(ui::NORMAL_SEPARATOR); |
| } |
| if (open_tabs || !features::IsChromeRefresh2023()) { |
| 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 = features::IsChromeRefresh2023() ? 8 : 3; |
| 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()); |
| if (features::IsChromeRefresh2023()) { |
| 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()); |
| device_sub_menu_items_.emplace( |
| command_id, SubMenuItem(command_id, std::move(device_menu_model))); |
| } else { |
| AddSeparator(ui::NORMAL_SEPARATOR); |
| AddTitle(base::UTF8ToUTF16(session->GetSessionName())); |
| AddDeviceFavicon(this, GetItemCount() - 1, |
| session->GetDeviceFormFactor()); |
| |
| // Build tab menu items from sorted session tabs. |
| const size_t kMaxTabsPerSessionToShow = 4; |
| for (size_t k = 0; |
| k < std::min(tabs_in_session.size(), kMaxTabsPerSessionToShow); |
| ++k) { |
| BuildOtherDevicesTabItem(this, session_tag, *tabs_in_session[k]); |
| } // for all tabs in one session |
| } |
| ++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( |
| SessionID session_id, |
| absl::optional<tab_groups::TabGroupVisualData> visual_data, |
| const std::u16string& title, |
| const GURL& url, |
| size_t curr_model_index) { |
| TabNavigationItem item(std::string(), session_id, title, url); |
| const int command_id = GetAndIncrementNextMenuID(); |
| // 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, |
| title.empty() ? base::UTF8ToUTF16(item.url.spec()) : title); |
| local_tab_navigation_items_.emplace(command_id, item); |
| AddTabFavicon(command_id, this, item.url); |
| // We shouldn't get here if there is no recently closed title. |
| DCHECK(recently_closed_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 (visual_data.has_value() && |
| curr_model_index > recently_closed_title_index_.value() + 1) { |
| const ui::ColorProvider* color_provider = |
| browser_->window()->GetColorProvider(); |
| const ui::ColorId color_id = |
| GetTabGroupContextMenuColorId(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( |
| SessionID window_id, |
| std::unique_ptr<ui::SimpleMenuModel> window_model, |
| size_t num_tabs, |
| size_t curr_model_index) { |
| const int command_id = GetAndIncrementNextMenuID(); |
| InsertSubMenuAt(curr_model_index, command_id, |
| l10n_util::GetPluralStringFUTF16(IDS_RECENTLY_CLOSED_WINDOW, |
| static_cast<int>(num_tabs)), |
| 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( |
| SessionID session_id, |
| tab_groups::TabGroupVisualData visual_data, |
| std::unique_ptr<ui::SimpleMenuModel> group_model, |
| size_t num_tabs, |
| size_t curr_model_index) { |
| const int command_id = GetAndIncrementNextMenuID(); |
| // Set the item label to the name of the group and the number of tabs. |
| std::u16string item_label = GetGroupItemLabel(visual_data.title(), num_tabs); |
| 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(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 sessions::SerializedNavigationEntry& current_navigation = |
| tab.navigations.at(tab.normalized_navigation_index()); |
| TabNavigationItem item(session_tag, tab.tab_id, current_navigation.title(), |
| current_navigation.virtual_url()); |
| const int command_id = GetAndIncrementNextMenuID(); |
| // 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()); |
| other_devices_tab_navigation_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::TabRestoreService::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); |
| if (features::IsChromeRefresh2023()) { |
| window_model->AddSeparator(ui::NORMAL_SEPARATOR); |
| } |
| |
| absl::optional<tab_groups::TabGroupId> last_group; |
| tab_groups::TabGroupVisualData current_group_visual_data; |
| std::unique_ptr<ui::SimpleMenuModel> current_group_model; |
| for (const std::unique_ptr<sessions::TabRestoreService::Tab>& tab : |
| window.tabs) { |
| const sessions::SerializedNavigationEntry& current_navigation = |
| tab->navigations.at(tab->current_navigation_index); |
| const int tab_command_id = GetAndIncrementNextMenuID(); |
| if (tab->group != last_group && current_group_model) { |
| AddGroupItemToModel(window_model.get(), std::move(current_group_model), |
| current_group_visual_data); |
| } |
| if (tab->group.has_value()) { |
| if (!current_group_model) { |
| last_group = tab->group.value(); |
| current_group_visual_data = tab->group_visual_data.value(); |
| current_group_model = std::make_unique<ui::SimpleMenuModel>(this); |
| // TODO(emshack): Add restore all menu item as first item in model. |
| } |
| // The following comment silences a bugprone-use-after-move warning, |
| // which does not apply to this use case as current_group_model will |
| // always be set to a new value before adding an item to it. |
| // NOLINTNEXTLINE |
| current_group_model->AddItem( |
| tab_command_id, |
| current_navigation.title().empty() |
| ? base::UTF8ToUTF16(current_navigation.virtual_url().spec()) |
| : current_navigation.title()); |
| local_group_items_.emplace(tab_command_id, tab->id); |
| AddTabFavicon(tab_command_id, current_group_model.get(), |
| current_navigation.virtual_url()); |
| } else { |
| // There may be no tab title, in which case, use the url as tab title. |
| window_model->AddItem( |
| tab_command_id, |
| current_navigation.title().empty() |
| ? base::UTF8ToUTF16(current_navigation.virtual_url().spec()) |
| : current_navigation.title()); |
| local_window_items_.emplace(tab_command_id, tab->id); |
| AddTabFavicon(tab_command_id, window_model.get(), |
| current_navigation.virtual_url()); |
| } |
| } |
| if (current_group_model) { |
| AddGroupItemToModel(window_model.get(), std::move(current_group_model), |
| current_group_visual_data); |
| } |
| return window_model; |
| } |
| |
| std::unique_ptr<ui::SimpleMenuModel> |
| RecentTabsSubMenuModel::CreateGroupSubMenuModel( |
| const sessions::TabRestoreService::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); |
| for (auto& tab : group.tabs) { |
| const sessions::SerializedNavigationEntry& current_navigation = |
| tab->navigations.at(tab->current_navigation_index); |
| command_id = GetAndIncrementNextMenuID(); |
| // There may be no tab title, in which case, use the url as tab title. |
| group_model->AddItem( |
| command_id, |
| current_navigation.title().empty() |
| ? base::UTF8ToUTF16(current_navigation.virtual_url().spec()) |
| : current_navigation.title()); |
| local_group_items_.emplace(command_id, tab->id); |
| AddTabFavicon(command_id, group_model.get(), |
| current_navigation.virtual_url()); |
| } |
| return group_model; |
| } |
| |
| void RecentTabsSubMenuModel::AddGroupItemToModel( |
| SimpleMenuModel* parent_model, |
| std::unique_ptr<SimpleMenuModel> group_model, |
| tab_groups::TabGroupVisualData group_visual_data) { |
| const int sub_menu_command_id = GetAndIncrementNextMenuID(); |
| const std::u16string sub_menu_label = |
| GetGroupItemLabel(group_visual_data.title(), group_model->GetItemCount()); |
| // 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))); |
| } |
| |
| 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; |
| case syncer::DeviceInfo::FormFactor::kUnknown: |
| [[fallthrough]]; // Return the laptop icon as default. |
| 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 = IsLocalTabModelCommandId(command_id) || |
| IsWindowModelCommandId(command_id) || |
| IsGroupModelCommandId(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), |
| |
| favicon::HistoryUiFaviconRequestOrigin::kRecentTabs); |
| } |
| } |
| |
| 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 absl::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::TabNavigationItems* |
| RecentTabsSubMenuModel::GetTabVectorForCommandId(int command_id) { |
| DCHECK(IsTabModelCommandId(command_id)); |
| return IsOtherDeviceTabModelCommandId(command_id) |
| ? &other_devices_tab_navigation_items_ |
| : &local_tab_navigation_items_; |
| } |
| |
| void RecentTabsSubMenuModel::ClearLocalEntries() { |
| // Remove local items (recently closed tabs and windows) from menumodel. |
| while (last_local_model_index_ > kHistorySeparatorIndex) { |
| RemoveItemAt(last_local_model_index_--); |
| } |
| recently_closed_title_index_.reset(); |
| |
| // Cancel asynchronous FaviconService::GetFaviconImageForPageURL() tasks of |
| // all local tabs. |
| local_tab_cancelable_task_tracker_.TryCancelAll(); |
| |
| // Remove all local tab navigation items. |
| local_tab_navigation_items_.clear(); |
| |
| // Remove all local window items. |
| local_window_items_.clear(); |
| |
| // Remove all local sub menu items. |
| 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(); |
| |
| other_devices_tab_navigation_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::IsTabModelCommandId(int command_id) const { |
| return IsLocalTabModelCommandId(command_id) || |
| IsOtherDeviceTabModelCommandId(command_id); |
| } |
| |
| bool RecentTabsSubMenuModel::IsLocalTabModelCommandId(int command_id) const { |
| return local_tab_navigation_items_.count(command_id) > 0; |
| } |
| |
| bool RecentTabsSubMenuModel::IsOtherDeviceTabModelCommandId( |
| int command_id) const { |
| return other_devices_tab_navigation_items_.count(command_id) > 0; |
| } |
| |
| bool RecentTabsSubMenuModel::IsWindowModelCommandId(int command_id) const { |
| return local_window_items_.count(command_id) > 0; |
| } |
| |
| bool RecentTabsSubMenuModel::IsGroupModelCommandId(int command_id) const { |
| return local_group_items_.count(command_id) > 0; |
| } |
| |
| bool RecentTabsSubMenuModel::IsSubMenuModelCommandId(int command_id) const { |
| return local_sub_menu_items_.count(command_id) > 0; |
| } |