blob: ba3ecc905f49ff924b0534a7f04d80f1f49b1b49 [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/bookmarks/bookmark_menu_bridge.h"
#include "base/strings/sys_string_conversions.h"
#include "chrome/app/chrome_command_ids.h"
#import "chrome/browser/app_controller_mac.h"
#include "chrome/browser/bookmarks/bookmark_merged_surface_service.h"
#include "chrome/browser/bookmarks/bookmark_merged_surface_service_factory.h"
#include "chrome/browser/bookmarks/bookmark_parent_folder.h"
#include "chrome/browser/bookmarks/bookmark_parent_folder_children.h"
#include "chrome/browser/favicon/favicon_utils.h"
#include "chrome/browser/prefs/incognito_mode_prefs.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/bookmarks/bookmark_utils_desktop.h"
#include "chrome/browser/ui/browser_list.h"
#import "chrome/browser/ui/cocoa/bookmarks/bookmark_menu_cocoa_controller.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/grit/theme_resources.h"
#include "components/bookmarks/browser/bookmark_model.h"
#include "components/bookmarks/browser/bookmark_node.h"
#include "components/bookmarks/managed/managed_bookmark_service.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/image/image.h"
#include "ui/resources/grit/ui_resources.h"
using bookmarks::BookmarkModel;
using bookmarks::BookmarkNode;
namespace {
// Recursively clear any delegates from |menu| and its unbuilt submenus.
void ClearDelegatesFromSubmenu(NSMenu* menu) {
CHECK(menu);
// Either the delegate has been cleared, or items were never added.
CHECK(![menu delegate] || [menu numberOfItems] == 0);
[menu setDelegate:nil];
NSArray* items = [menu itemArray];
for (NSMenuItem* item in items) {
if ([item hasSubmenu]) {
ClearDelegatesFromSubmenu([item submenu]);
}
}
}
NSString* MenuTitleForNode(const BookmarkNode* node) {
return base::SysUTF16ToNSString(node->GetTitle());
}
} // namespace
BookmarkMenuBridge::BookmarkMenuBridge(Profile* profile, NSMenu* menu_root)
: profile_(profile),
controller_([[BookmarkMenuCocoaController alloc] initWithBridge:this]),
menu_root_(menu_root) {
CHECK(profile_);
profile_dir_ = profile->GetPath();
CHECK(menu_root_);
CHECK(![menu_root_ delegate]);
[menu_root_ setDelegate:controller_];
bookmark_service_ =
BookmarkMergedSurfaceServiceFactory::GetForProfile(profile_);
// The bookmark service is only availble for Regular and Guest profiles.
if (!bookmark_service_) {
return;
}
bookmark_service_observation_.Observe(bookmark_service_);
if (bookmark_service_->loaded()) {
BookmarkMergedSurfaceServiceLoaded();
}
}
BookmarkMenuBridge::~BookmarkMenuBridge() {
ClearBookmarkMenu();
[menu_root_ setDelegate:nil];
}
void BookmarkMenuBridge::BookmarkMergedSurfaceServiceLoaded() {
InvalidateMenu();
}
bool BookmarkMenuBridge::IsMenuRoot(NSMenu* menu) {
CHECK(menu);
return menu == menu_root_;
}
void BookmarkMenuBridge::UpdateRootMenuIfInvalid() {
CHECK(menu_root_);
if (!IsMenuValid()) {
BuildRootMenu(/*recurse=*/false);
}
}
void BookmarkMenuBridge::UpdateNonRootMenu(NSMenu* menu,
const BookmarkParentFolder& folder) {
CHECK(menu);
CHECK(!IsMenuRoot(menu));
CHECK(controller_);
CHECK_EQ([menu delegate], controller_);
AddChildrenToMenu(folder, menu, /*recurse=*/false);
// Clear the delegate to prevent further refreshes.
[menu setDelegate:nil];
}
bool BookmarkMenuBridge::HasContent(const BookmarkParentFolder& folder) {
// TODO(crbug.com/390398329): Verify if this should be replaced with
// checking visibility of underlying nodes.
return bookmark_service_->GetChildrenCount(folder) > 0;
}
void BookmarkMenuBridge::BuildRootMenu(bool recurse) {
if (!bookmark_service_ || !bookmark_service_->loaded()) {
return;
}
if (!folder_image_) {
ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
folder_image_ = rb.GetNativeImageNamed(IDR_FOLDER_CLOSED).ToNSImage();
[folder_image_ setTemplate:YES];
}
ClearBookmarkMenu();
BookmarkParentFolder bookmark_bar_folder =
BookmarkParentFolder::BookmarkBarFolder();
BookmarkParentFolder managed_folder = BookmarkParentFolder::ManagedFolder();
// Add at most one separator for the bookmark bar and the managed bookmarks
// folder.
if (HasContent(bookmark_bar_folder) || HasContent(managed_folder)) {
[menu_root_ addItem:[NSMenuItem separatorItem]];
}
if (HasContent(managed_folder)) {
// Most users never see this node, so the image is only loaded if needed.
ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
NSImage* image =
rb.GetNativeImageNamed(IDR_BOOKMARK_BAR_FOLDER_MANAGED).ToNSImage();
AddSubmenu(menu_root_, managed_folder, image, recurse);
}
// Add entries from the bookmark bar to the root menu.
for (const BookmarkNode* node :
bookmark_service_->GetChildren(bookmark_bar_folder)) {
AddNodeToMenu(node, menu_root_, recurse);
}
BookmarkParentFolder other_folder = BookmarkParentFolder::OtherFolder();
BookmarkParentFolder mobile_folder = BookmarkParentFolder::MobileFolder();
// Add at most one separator for the "Other Bookmarks" and "Mobile Bookmarks"
// folders.
if (HasContent(other_folder) || HasContent(mobile_folder)) {
[menu_root_ addItem:[NSMenuItem separatorItem]];
}
if (HasContent(other_folder)) {
AddSubmenu(menu_root_, other_folder, folder_image_, recurse);
}
if (HasContent(mobile_folder)) {
AddSubmenu(menu_root_, mobile_folder, folder_image_, recurse);
}
is_menu_valid_ = true;
}
void BookmarkMenuBridge::BookmarkMergedSurfaceServiceBeingDeleted() {}
void BookmarkMenuBridge::BookmarkNodeAdded(const BookmarkParentFolder& parent,
size_t index) {
InvalidateMenu();
}
void BookmarkMenuBridge::BookmarkNodesRemoved(
const BookmarkParentFolder& parent,
const base::flat_set<const bookmarks::BookmarkNode*>& nodes) {
InvalidateMenu();
}
void BookmarkMenuBridge::BookmarkNodeMoved(
const BookmarkParentFolder& old_parent,
size_t old_index,
const BookmarkParentFolder& new_parent,
size_t new_index) {
InvalidateMenu();
}
void BookmarkMenuBridge::BookmarkNodeChanged(const BookmarkNode* node) {
NSMenuItem* item = MenuItemForNode(node);
if (item) {
ConfigureMenuItem(node, item);
}
}
void BookmarkMenuBridge::BookmarkNodeFaviconChanged(const BookmarkNode* node) {
NSMenuItem* item = MenuItemForNode(node);
if (item) {
ConfigureMenuItem(node, item);
}
}
void BookmarkMenuBridge::BookmarkParentFolderChildrenReordered(
const BookmarkParentFolder& folder) {
InvalidateMenu();
}
void BookmarkMenuBridge::BookmarkAllUserNodesRemoved() {
InvalidateMenu();
}
BookmarkModel* BookmarkMenuBridge::GetBookmarkModelForTesting() {
CHECK(bookmark_service_);
return bookmark_service_->bookmark_model();
}
Profile* BookmarkMenuBridge::GetProfile() {
return profile_;
}
const base::FilePath& BookmarkMenuBridge::GetProfileDir() const {
return profile_dir_;
}
NSMenu* BookmarkMenuBridge::BookmarkMenu() {
return menu_root_;
}
void BookmarkMenuBridge::ClearBookmarkMenu() {
InvalidateMenu();
bookmark_nodes_.clear();
tag_to_guid_.clear();
// Recursively delete all menus that look like a bookmark. Also delete all
// separator items since we explicitly add them back in. This deletes
// everything except the first item ("Add Bookmark...").
NSArray* items = [menu_root_ itemArray];
for (NSMenuItem* item in items) {
// If there's a submenu, it may have a reference to |controller_|. Ensure
// that gets nerfed recursively.
if ([item hasSubmenu]) {
ClearDelegatesFromSubmenu([item submenu]);
}
// Convention: items in the bookmark list which are bookmarks have
// an action of openBookmarkMenuItem:. Also, assume all items
// with submenus are submenus of bookmarks.
if (([item action] == @selector(openBookmarkMenuItem:)) ||
[item hasSubmenu] || [item isSeparatorItem]) {
// This will eventually [obj release] all its kids, if it has any.
[menu_root_ removeItem:item];
} else {
// Leave it alone.
}
}
}
void BookmarkMenuBridge::AddSubmenu(NSMenu* menu,
const BookmarkParentFolder& folder,
NSImage* image,
bool recurse) {
// For permanent folders containing multiple nodes, use the first node's title
// as the menu title.
auto nodes = bookmark_service_->GetUnderlyingNodes(folder);
CHECK(!nodes.empty());
const BookmarkNode* node = nodes[0];
NSString* title = MenuTitleForNode(node);
NSMenuItem* item = [[NSMenuItem alloc] initWithTitle:title
action:nil
keyEquivalent:@""];
[item setImage:image];
ConfigureMenuItem(node, item);
bookmark_nodes_[node] = item;
NSMenu* submenu = [[NSMenu alloc] initWithTitle:title];
[menu setSubmenu:submenu forItem:item];
// Set a delegate and a tag on the item so that the submenu can be populated
// when (and if) Cocoa asks for it.
if (!recurse) {
[submenu setDelegate:controller_];
}
[menu addItem:item];
if (recurse) {
AddChildrenToMenu(folder, submenu, recurse);
}
}
void BookmarkMenuBridge::AddChildrenToMenu(const BookmarkParentFolder& folder,
NSMenu* menu,
bool recurse) {
BookmarkParentFolderChildren children =
bookmark_service_->GetChildren(folder);
if (!children.size()) {
// Permanent folders with no children are not visible.
CHECK(folder.HoldsNonPermanentFolder());
// For empty non-permanent folder, show an unclickable entry with the text
// "(empty)".
NSString* empty_string = l10n_util::GetNSString(IDS_MENU_EMPTY_SUBMENU);
NSMenuItem* item = [[NSMenuItem alloc] initWithTitle:empty_string
action:nil
keyEquivalent:@""];
[menu addItem:item];
return;
}
for (const BookmarkNode* child : children) {
AddNodeToMenu(child, menu, recurse);
}
}
void BookmarkMenuBridge::AddNodeToMenu(const BookmarkNode* node,
NSMenu* menu,
bool recurse) {
if (node->is_folder()) {
AddSubmenu(menu, BookmarkParentFolder::FromFolderNode(node), folder_image_,
recurse);
} else {
CHECK(node->is_url());
NSMenuItem* item = [[NSMenuItem alloc] initWithTitle:MenuTitleForNode(node)
action:nil
keyEquivalent:@""];
bookmark_nodes_[node] = item;
ConfigureMenuItem(node, item);
[menu addItem:item];
}
}
void BookmarkMenuBridge::ConfigureMenuItem(const BookmarkNode* node,
NSMenuItem* item) {
[item setTitle:MenuTitleForNode(node)];
[item setTag:node->id()];
tag_to_guid_[node->id()] = node->uuid();
// The following settings only apply to URL items.
if (node->is_folder()) {
return;
}
CHECK(node->is_url());
[item setTarget:controller_];
[item setAction:@selector(openBookmarkMenuItem:)];
[item setToolTip:[BookmarkMenuCocoaController tooltipForNode:node]];
// Check to see if we have a favicon.
NSImage* favicon = nil;
BookmarkModel* model = bookmark_service_->bookmark_model();
if (model) {
const gfx::Image& image = model->GetFavicon(node);
if (!image.IsEmpty()) {
favicon = image.ToNSImage();
}
}
// If we do not have a loaded favicon, use the default site image instead.
if (!favicon) {
favicon = favicon::GetDefaultFavicon().ToNSImage();
[favicon setTemplate:YES];
}
[item setImage:favicon];
}
NSMenuItem* BookmarkMenuBridge::MenuItemForNode(const BookmarkNode* node) {
if (!node) {
return nil;
}
auto it = bookmark_nodes_.find(node);
if (it == bookmark_nodes_.end()) {
return nil;
}
return it->second;
}
NSMenuItem* BookmarkMenuBridge::MenuItemForNodeForTest(
const bookmarks::BookmarkNode* node) {
return MenuItemForNode(node);
}
void BookmarkMenuBridge::OnProfileWillBeDestroyed() {
// Recursively populate the menu before the bookmark service is destroyed.
BuildRootMenu(/*recurse=*/true);
bookmark_service_observation_.Reset();
bookmark_service_ = nullptr;
profile_ = nullptr;
// |bookmark_nodes_| stores the nodes by pointer, so it would be unsafe to
// keep them.
bookmark_nodes_.clear();
}
base::Uuid BookmarkMenuBridge::TagToGUID(int64_t tag) const {
const auto& it = tag_to_guid_.find(tag);
return it == tag_to_guid_.end() ? base::Uuid() : it->second;
}