[go: nahoru, domu]

blob: 71f3a6cd8a4731445c5b4e0bb7d66b519eafcd89 [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/cocoa/history_menu_bridge.h"
#include <stddef.h>
#include <string>
#include "base/apple/foundation_util.h"
#include "base/functional/bind.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#import "base/task/single_thread_task_runner.h"
#include "base/task/single_thread_task_runner.h"
#include "chrome/app/chrome_command_ids.h" // IDC_HISTORY_MENU
#include "chrome/app/vector_icons/vector_icons.h"
#include "chrome/browser/app_controller_mac.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/favicon/favicon_service_factory.h"
#include "chrome/browser/history/history_service_factory.h"
#include "chrome/browser/profiles/keep_alive/profile_keep_alive_types.h"
#include "chrome/browser/profiles/keep_alive/scoped_profile_keep_alive.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/sessions/tab_restore_service_factory.h"
#include "chrome/browser/themes/theme_service.h"
#import "chrome/browser/ui/cocoa/history_menu_cocoa_controller.h"
#include "chrome/browser/ui/tabs/tab_group_theme.h"
#include "chrome/common/chrome_features.h"
#include "chrome/grit/generated_resources.h"
#include "components/grit/components_scaled_resources.h"
#include "components/tab_groups/tab_group_visual_data.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/models/image_model.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/color/color_provider.h"
#include "ui/gfx/favicon_size.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/image/image_skia_util_mac.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/resources/grit/ui_resources.h"
namespace {
// Number of days to consider when getting the number of visited items.
const int kVisitedScope = 90;
// The number of visited results to get.
const int kVisitedCount = 15;
// The number of recently closed items to get.
const unsigned int kRecentlyClosedCount = 10;
} // namespace
HistoryMenuBridge::HistoryItem::HistoryItem()
: session_id(SessionID::InvalidValue()) {}
HistoryMenuBridge::HistoryItem::HistoryItem(const HistoryItem& copy)
: title(copy.title), url(copy.url), session_id(copy.session_id) {}
HistoryMenuBridge::HistoryItem::~HistoryItem() = default;
HistoryMenuBridge::HistoryMenuBridge(Profile* profile)
: controller_([[HistoryMenuCocoaController alloc] initWithBridge:this]),
profile_(profile) {
DCHECK(profile_);
profile_dir_ = profile_->GetPath();
if (auto* profile_manager = g_browser_process->profile_manager())
profile_manager_observation_.Observe(profile_manager);
// Set the static icons in the menu.
ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
NSMenuItem* full_history_item = [HistoryMenu() itemWithTag:IDC_SHOW_HISTORY];
[full_history_item
setImage:rb.GetNativeImageNamed(IDR_HISTORY_FAVICON).ToNSImage()];
// Set the visibility of menu items according to profile type.
// "Recently Visited", "Recently Closed" and "Show Full History" sections
// should be hidden for incognito mode, while incognito disclaimer should be
// visible.
SetVisibilityOfMenuItems();
// If the profile is incognito, no need to set history and tab restore
// services.
if (profile_->IsOffTheRecord())
return;
// Check to see if the history service is ready. Because it loads async, it
// may not be ready when the Bridge is created. If this happens, register for
// a notification that tells us the HistoryService is ready.
history::HistoryService* hs = HistoryServiceFactory::GetForProfile(
profile_, ServiceAccessType::EXPLICIT_ACCESS);
if (hs) {
history_service_observation_.Observe(hs);
history_service_keep_alive_ = std::make_unique<ScopedProfileKeepAlive>(
profile_, ProfileKeepAliveOrigin::kHistoryMenuBridge);
if (hs->BackendLoaded()) {
history_service_ = hs;
Init();
}
}
tab_restore_service_ = TabRestoreServiceFactory::GetForProfile(profile_);
if (tab_restore_service_) {
tab_restore_service_observation_.Observe(tab_restore_service_.get());
// If the tab entries are already loaded, invoke the observer method to
// build the "Recently Closed" section. Otherwise it will be when the
// backend loads.
if (!tab_restore_service_->IsLoaded()) {
tab_restore_service_keep_alive_ =
std::make_unique<ScopedProfileKeepAlive>(
profile_, ProfileKeepAliveOrigin::kHistoryMenuBridge);
tab_restore_service_->LoadTabsFromLastSession();
} else {
TabRestoreServiceChanged(tab_restore_service_);
}
}
default_favicon_ = rb.GetNativeImageNamed(IDR_DEFAULT_FAVICON).ToNSImage();
[HistoryMenu() setDelegate:controller_];
}
// Note that all requests sent to either the history service or the favicon
// service will be automatically cancelled by their respective Consumers, so
// task cancellation is not done manually here in the dtor.
HistoryMenuBridge::~HistoryMenuBridge() = default;
void HistoryMenuBridge::TabRestoreServiceChanged(
sessions::TabRestoreService* service) {
const sessions::TabRestoreService::Entries& entries = service->entries();
// Clear the history menu before rebuilding.
NSMenu* menu = HistoryMenu();
ClearMenuSection(menu, kRecentlyClosed);
// Index for the next menu item.
NSInteger index = [menu indexOfItemWithTag:kRecentlyClosedTitle] + 1;
NSUInteger added_count = 0;
for (const auto& entry : entries) {
if (added_count >= kRecentlyClosedCount)
break;
if (entry->type == sessions::TabRestoreService::WINDOW) {
bool added = AddWindowEntryToMenu(
static_cast<sessions::TabRestoreService::Window*>(entry.get()), menu,
kRecentlyClosed, index);
if (added) {
++index;
++added_count;
}
} else if (entry->type == sessions::TabRestoreService::TAB) {
const auto& tab = static_cast<sessions::TabRestoreService::Tab&>(*entry);
std::unique_ptr<HistoryItem> item = HistoryItemForTab(tab);
if (item) {
AddItemToMenu(std::move(item), menu, kRecentlyClosed, index++);
++added_count;
}
} else if (entry->type == sessions::TabRestoreService::GROUP) {
bool added = AddGroupEntryToMenu(
static_cast<sessions::TabRestoreService::Group*>(entry.get()), menu,
kRecentlyClosed, index);
if (added) {
++index;
++added_count;
}
}
}
tab_restore_service_keep_alive_.reset();
}
void HistoryMenuBridge::TabRestoreServiceDestroyed(
sessions::TabRestoreService* service) {
DCHECK_EQ(service, tab_restore_service_);
tab_restore_service_observation_.Reset();
tab_restore_service_ = nullptr;
}
void HistoryMenuBridge::TabRestoreServiceLoaded(
sessions::TabRestoreService* service) {
// `TabRestoreServiceChanged()` is not called if the tab restore service is
// empty when it is loaded. The menu still needs to be updated and
// `tab_restore_service_keep_alive_` must be released.
TabRestoreServiceChanged(service);
}
void HistoryMenuBridge::ResetMenu() {
NSMenu* menu = HistoryMenu();
ClearMenuSection(menu, kVisited);
ClearMenuSection(menu, kRecentlyClosed);
DCHECK(menu_item_map_.empty());
}
void HistoryMenuBridge::BuildMenu() {
// If the history service is ready, use it. Otherwise, a Notification will
// force an update when it's loaded.
if (history_service_)
CreateMenu();
}
HistoryMenuBridge::HistoryItem* HistoryMenuBridge::HistoryItemForMenuItem(
NSMenuItem* item) {
auto it = menu_item_map_.find(item);
if (it != menu_item_map_.end()) {
return it->second.get();
}
return nullptr;
}
void HistoryMenuBridge::SetIsMenuOpen(bool flag) {
is_menu_open_ = flag;
if (!is_menu_open_ && need_recreate_) {
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(&HistoryMenuBridge::CreateMenu,
weak_factory_.GetWeakPtr()));
}
}
history::HistoryService* HistoryMenuBridge::service() {
return history_service_;
}
Profile* HistoryMenuBridge::profile() {
return profile_;
}
const base::FilePath& HistoryMenuBridge::profile_dir() const {
return profile_dir_;
}
NSMenu* HistoryMenuBridge::HistoryMenu() {
NSMenu* history_menu = [[[NSApp mainMenu] itemWithTag:IDC_HISTORY_MENU]
submenu];
return history_menu;
}
void HistoryMenuBridge::ClearMenuSection(NSMenu* menu, NSInteger tag) {
for (NSMenuItem* menu_item in [menu itemArray]) {
if ([menu_item tag] == tag && [menu_item target] == controller_) {
// This is an item that should be removed, so find the corresponding model
// item.
HistoryItem* item = HistoryItemForMenuItem(menu_item);
// Cancel favicon requests that could hold onto stale pointers. Also
// remove the item from the mapping.
if (item) {
CancelFaviconRequest(item);
menu_item_map_.erase(menu_item);
}
// If this menu item has a submenu, recurse.
if ([menu_item hasSubmenu]) {
ClearMenuSection([menu_item submenu], tag + 1);
}
// Now actually remove the item from the menu.
[menu removeItem:menu_item];
}
}
}
NSMenuItem* HistoryMenuBridge::AddItemToMenu(std::unique_ptr<HistoryItem> item,
NSMenu* menu,
NSInteger tag,
NSInteger index) {
// Elide the title of the history item, or use the URL if there is none.
const std::string& url = item->url.possibly_invalid_spec();
const std::u16string& full_title = item->title;
const std::u16string& title =
full_title.empty() ? base::UTF8ToUTF16(url) : full_title;
item->menu_item =
[[NSMenuItem alloc] initWithTitle:base::SysUTF16ToNSString(title)
action:nil
keyEquivalent:@""];
[item->menu_item setTarget:controller_];
[item->menu_item setAction:@selector(openHistoryMenuItem:)];
[item->menu_item setTag:tag];
if (item->icon) {
[item->menu_item setImage:item->icon];
} else if (item->tabs.empty()) {
[item->menu_item setImage:default_favicon_];
}
// Add a tooltip if the history item is for a single tab.
if (item->tabs.empty()) {
NSString* tooltip = [NSString stringWithFormat:@"%@\n%@",
base::SysUTF16ToNSString(full_title), base::SysUTF8ToNSString(url)];
[item->menu_item setToolTip:tooltip];
}
[menu insertItem:item->menu_item atIndex:index];
NSMenuItem* menu_item = item->menu_item;
auto it = menu_item_map_.emplace(menu_item, std::move(item));
CHECK(it.second);
return menu_item;
}
bool HistoryMenuBridge::AddWindowEntryToMenu(
sessions::TabRestoreService::Window* window,
NSMenu* menu,
NSInteger tag,
NSInteger index) {
const std::vector<std::unique_ptr<sessions::TabRestoreService::Tab>>& tabs =
window->tabs;
if (tabs.empty())
return false;
// Create the item for the parent/window. Do not set the title yet because
// the actual number of items that are in the menu will not be known until
// things like the NTP are filtered out, which is done when the tab items
// are actually created.
auto item = std::make_unique<HistoryItem>();
item->session_id = window->id;
// Create the submenu.
NSMenu* submenu = [[NSMenu alloc] init];
int added_count = AddTabsToSubmenu(submenu, item.get(), tabs);
// Sometimes it is possible for there to not be any subitems for a given
// window; if that is the case, do not add the entry to the main menu.
if (added_count == 0)
return false;
// Now that the number of tabs that has been added is known, set the title
// of the parent menu item.
item->title = l10n_util::GetPluralStringFUTF16(IDS_RECENTLY_CLOSED_WINDOW,
item->tabs.size());
// Create the menu item parent.
NSMenuItem* parent_item = AddItemToMenu(std::move(item), menu, tag, index);
parent_item.submenu = submenu;
return true;
}
bool HistoryMenuBridge::AddGroupEntryToMenu(
sessions::TabRestoreService::Group* group,
NSMenu* menu,
NSInteger tag,
NSInteger index) {
const std::vector<std::unique_ptr<sessions::TabRestoreService::Tab>>& tabs =
group->tabs;
if (tabs.empty())
return false;
// Create the item for the parent/group.
auto item = std::make_unique<HistoryItem>();
item->session_id = group->id;
// Set the title of the group.
if (group->visual_data.title().empty()) {
item->title = l10n_util::GetPluralStringFUTF16(
IDS_RECENTLY_CLOSED_GROUP_UNNAMED, tabs.size());
} else {
item->title = l10n_util::GetPluralStringFUTF16(IDS_RECENTLY_CLOSED_GROUP,
tabs.size());
item->title = base::ReplaceStringPlaceholders(
item->title, {group->visual_data.title()}, nullptr);
}
// Set the icon of the group to the group color circle.
const auto& color_provider =
[AppController.sharedController lastActiveColorProvider];
const ui::ColorId color_id =
GetTabGroupContextMenuColorId(group->visual_data.color());
gfx::ImageSkia group_icon = gfx::CreateVectorIcon(
kTabGroupIcon, gfx::kFaviconSize, color_provider.GetColor(color_id));
// Create the submenu.
NSMenu* submenu = [[NSMenu alloc] init];
AddTabsToSubmenu(submenu, item.get(), tabs);
NSImage* image = NSImageFromImageSkia(group_icon);
item->icon = image;
[item->menu_item setImage:item->icon];
// Create the menu item parent.
NSMenuItem* parent_item = AddItemToMenu(std::move(item), menu, tag, index);
[parent_item setSubmenu:submenu];
return true;
}
int HistoryMenuBridge::AddTabsToSubmenu(
NSMenu* submenu,
HistoryItem* item,
const std::vector<std::unique_ptr<sessions::TabRestoreService::Tab>>&
tabs) {
// Create standard items within the submenu.
// Duplicate the HistoryItem otherwise the different NSMenuItems will
// point to the same HistoryItem, which would then be double-freed when
// removing the items from the map or in the dtor.
auto restore_item = std::make_unique<HistoryItem>(*item);
NSString* restore_title =
l10n_util::GetNSString(IDS_HISTORY_CLOSED_RESTORE_WINDOW_MAC);
restore_item->menu_item =
[[NSMenuItem alloc] initWithTitle:restore_title
action:@selector(openHistoryMenuItem:)
keyEquivalent:@""];
NSMenuItem* restore_menu_item = restore_item->menu_item;
[restore_menu_item setTag:kRecentlyClosed + 1]; // +1 for submenu item.
[restore_menu_item setTarget:controller_];
auto it = menu_item_map_.emplace(restore_menu_item, std::move(restore_item));
CHECK(it.second);
[submenu addItem:restore_menu_item];
[submenu addItem:[NSMenuItem separatorItem]];
// Loop over the tabs and add them to the submenu. This filters out
// uninteresting tabs like the NTP.
NSInteger subindex = [[submenu itemArray] count];
int added_count = 0;
for (const auto& tab : tabs) {
std::unique_ptr<HistoryItem> tab_item = HistoryItemForTab(*tab);
if (tab_item) {
item->tabs.push_back(tab_item.get());
AddItemToMenu(std::move(tab_item), submenu, kRecentlyClosed + 1,
subindex++);
++added_count;
}
}
return added_count;
}
void HistoryMenuBridge::Init() {
DCHECK(history_service_);
need_recreate_ = true;
CreateMenu();
}
void HistoryMenuBridge::CreateMenu() {
// If we're currently running CreateMenu(), wait until it finishes.
// If the menu is currently open, wait until it closes.
// If the history service got torn down while our async task was queued, don't
// do anything - the browser is exiting anyway.
// If the current profile is incognito, do not fill the menu.
if (create_in_progress_ || is_menu_open_ || !history_service_ ||
profile_->IsOffTheRecord()) {
return;
}
create_in_progress_ = true;
need_recreate_ = false;
history::QueryOptions options;
options.max_count = kVisitedCount;
options.SetRecentDayRange(kVisitedScope);
history_service_->QueryHistory(
std::u16string(), options,
base::BindOnce(&HistoryMenuBridge::OnVisitedHistoryResults,
base::Unretained(this)),
&cancelable_task_tracker_);
}
void HistoryMenuBridge::OnHistoryChanged() {
// History has changed, rebuild menu.
need_recreate_ = true;
CreateMenu();
}
void HistoryMenuBridge::OnVisitedHistoryResults(history::QueryResults results) {
// It's possible for history_service_ to have been destroyed while our request
// was waiting to be returned to us, because both the initial request *and the
// delivery of the reply* from the service are async - i.e., this can happen:
// 1. We call HistoryService::QueryHistory()
// 2. That message loop runs, the query happens, the reply to us is posted
// 3. HistoryService is destroyed
// 4. The posted reply to us arrives
// To guard against that, check for history_service_ here.
if (!history_service_)
return;
NSMenu* menu = HistoryMenu();
ClearMenuSection(menu, kVisited);
NSInteger top_item = [menu indexOfItemWithTag:kVisitedTitle] + 1;
size_t count = results.size();
for (size_t i = 0; i < count; ++i) {
const history::URLResult& result = results[i];
auto item = std::make_unique<HistoryItem>();
item->title = result.title();
item->url = result.url();
// Need to explicitly get the favicon for each row.
GetFaviconForHistoryItem(item.get());
// This will add |item| to the |menu_item_map_|, which takes ownership.
AddItemToMenu(std::move(item), HistoryMenu(), kVisited, top_item + i);
}
// We are already invalid by the time we finished, darn.
if (need_recreate_)
CreateMenu();
else
history_service_keep_alive_.reset();
create_in_progress_ = false;
}
std::unique_ptr<HistoryMenuBridge::HistoryItem>
HistoryMenuBridge::HistoryItemForTab(
const sessions::TabRestoreService::Tab& entry) {
DCHECK(!entry.navigations.empty());
const sessions::SerializedNavigationEntry& current_navigation =
entry.navigations.at(entry.current_navigation_index);
auto item = std::make_unique<HistoryItem>();
item->title = current_navigation.title();
item->url = current_navigation.virtual_url();
item->session_id = entry.id;
// Tab navigations don't come with icons, so we always have to request them.
GetFaviconForHistoryItem(item.get());
return item;
}
void HistoryMenuBridge::GetFaviconForHistoryItem(HistoryItem* item) {
favicon::FaviconService* service = FaviconServiceFactory::GetForProfile(
profile_, ServiceAccessType::EXPLICIT_ACCESS);
base::CancelableTaskTracker::TaskId task_id =
service->GetFaviconImageForPageURL(
item->url,
base::BindOnce(&HistoryMenuBridge::GotFaviconData,
base::Unretained(this), item),
&cancelable_task_tracker_);
item->icon_task_id = task_id;
item->icon_requested = true;
}
void HistoryMenuBridge::GotFaviconData(
HistoryItem* item,
const favicon_base::FaviconImageResult& image_result) {
// Since we're going to do Cocoa-y things, make sure this is the main thread.
DCHECK([NSThread isMainThread]);
DCHECK(item);
item->icon_requested = false;
item->icon_task_id = base::CancelableTaskTracker::kBadTaskId;
NSImage* image = image_result.image.AsNSImage();
if (image) {
item->icon = image;
[item->menu_item setImage:item->icon];
}
}
void HistoryMenuBridge::CancelFaviconRequest(HistoryItem* item) {
DCHECK(item);
if (item->icon_requested) {
cancelable_task_tracker_.TryCancel(item->icon_task_id);
item->icon_requested = false;
item->icon_task_id = base::CancelableTaskTracker::kBadTaskId;
}
}
void HistoryMenuBridge::OnURLVisited(history::HistoryService* history_service,
const history::URLRow& url_row,
const history::VisitRow& new_visit) {
OnHistoryChanged();
}
void HistoryMenuBridge::OnURLsModified(history::HistoryService* history_service,
const history::URLRows& changed_urls) {
OnHistoryChanged();
}
void HistoryMenuBridge::OnURLsDeleted(
history::HistoryService* history_service,
const history::DeletionInfo& deletion_info) {
OnHistoryChanged();
}
void HistoryMenuBridge::OnHistoryServiceLoaded(
history::HistoryService* history_service) {
history_service_ = history_service;
Init();
}
void HistoryMenuBridge::HistoryServiceBeingDeleted(
history::HistoryService* history_service) {
history_service_observation_.Reset();
history_service_ = nullptr;
}
void HistoryMenuBridge::SetVisibilityOfMenuItems() {
NSMenu* menu = HistoryMenu();
for (int i = 0; i < [menu numberOfItems]; i++) {
NSMenuItem* item = [menu itemAtIndex:i];
[item setHidden:!ShouldMenuItemBeVisible(item)];
}
}
bool HistoryMenuBridge::ShouldMenuItemBeVisible(NSMenuItem* item) {
int tag = [item tag];
switch (tag) {
// The common menu items for both profiles
case IDC_HOME:
case IDC_BACK:
case IDC_FORWARD:
return true;
// The original profile specific menu items
case kRecentlyClosedSeparator:
case kRecentlyClosedTitle:
case kVisitedSeparator:
case kVisitedTitle:
case kShowFullSeparator:
case IDC_SHOW_HISTORY:
return !profile_->IsOffTheRecord();
}
// When a new menu item is introduced, it should be added to one of the cases
// above.
NOTREACHED();
return false;
}
void HistoryMenuBridge::OnProfileMarkedForPermanentDeletion(Profile* profile) {
if (profile != profile_)
return;
ResetMenu();
}
void HistoryMenuBridge::OnProfileManagerDestroying() {
profile_manager_observation_.Reset();
}
void HistoryMenuBridge::OnProfileWillBeDestroyed() {
profile_ = nullptr;
history_service_observation_.Reset();
history_service_ = nullptr;
tab_restore_service_observation_.Reset();
tab_restore_service_ = nullptr;
}