chromium/chrome/browser/ui/cocoa/bookmarks/bookmark_menu_bridge.mm

// 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.

#import <AppKit/AppKit.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_model_factory.h"
#include "chrome/browser/bookmarks/managed_bookmark_service_factory.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"
#include "chrome/browser/ui/cocoa/bookmarks/bookmark_menu_bridge.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/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) {
  DCHECK(menu);
  // Either the delegate has been cleared, or items were never added.
  DCHECK(![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) {
  DCHECK(profile_);
  profile_dir_ = profile->GetPath();
  DCHECK(menu_root_);
  DCHECK(![menu_root_ delegate]);
  [menu_root_ setDelegate:controller_];

  ObserveBookmarkModel();
}

BookmarkMenuBridge::~BookmarkMenuBridge() {
  ClearBookmarkMenu();
  [menu_root_ setDelegate:nil];
}

void BookmarkMenuBridge::BookmarkModelLoaded(bool ids_reassigned) {
  InvalidateMenu();
}

void BookmarkMenuBridge::UpdateMenu(NSMenu* menu,
                                    const BookmarkNode* node,
                                    bool recurse) {
  DCHECK(menu);
  DCHECK(controller_);
  DCHECK_EQ([menu delegate], controller_);

  if (menu == menu_root_) {
    if (!IsMenuValid())
      BuildRootMenu(recurse);
    return;
  }

  DCHECK(node);
  AddNodeToMenu(node, menu, recurse);
  // Clear the delegate to prevent further refreshes.
  [menu setDelegate:nil];
}

void BookmarkMenuBridge::BuildRootMenu(bool recurse) {
  BookmarkModel* model = GetBookmarkModel();
  if (!model || !model->loaded())
    return;

  if (!folder_image_) {
    ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
    folder_image_ = rb.GetNativeImageNamed(IDR_FOLDER_CLOSED).ToNSImage();
    [folder_image_ setTemplate:YES];
  }

  ClearBookmarkMenu();

  // Add at most one separator for the bookmark bar and the managed bookmarks
  // folder.
  bookmarks::ManagedBookmarkService* managed =
      ManagedBookmarkServiceFactory::GetForProfile(profile_);
  const BookmarkNode* barNode = model->bookmark_bar_node();
  const BookmarkNode* managedNode = managed->managed_node();
  if (!barNode->children().empty() || !managedNode->children().empty())
    [menu_root_ addItem:[NSMenuItem separatorItem]];
  if (!managedNode->children().empty()) {
    // 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();
    AddNodeAsSubmenu(menu_root_, managedNode, image, recurse);
  }
  if (!barNode->children().empty())
    AddNodeToMenu(barNode, menu_root_, recurse);

  // If the "Other Bookmarks" folder has any content, make a submenu for it and
  // fill it in.
  if (!model->other_node()->children().empty()) {
    [menu_root_ addItem:[NSMenuItem separatorItem]];
    AddNodeAsSubmenu(menu_root_, model->other_node(), folder_image_, recurse);
  }

  // If the "Mobile Bookmarks" folder has any content, make a submenu for it and
  // fill it in.
  if (!model->mobile_node()->children().empty()) {
    // Add a separator if we did not already add one due to a non-empty
    // "Other Bookmarks" folder.
    if (model->other_node()->children().empty())
      [menu_root_ addItem:[NSMenuItem separatorItem]];

    AddNodeAsSubmenu(menu_root_, model->mobile_node(), folder_image_, recurse);
  }

  menuIsValid_ = true;
}

void BookmarkMenuBridge::BookmarkModelBeingDeleted() {}

void BookmarkMenuBridge::BookmarkNodeMoved(const BookmarkNode* old_parent,
                                           size_t old_index,
                                           const BookmarkNode* new_parent,
                                           size_t new_index) {
  InvalidateMenu();
}

void BookmarkMenuBridge::BookmarkNodeAdded(const BookmarkNode* parent,
                                           size_t index,
                                           bool added_by_user) {
  InvalidateMenu();
}

void BookmarkMenuBridge::BookmarkNodeRemoved(const BookmarkNode* parent,
                                             size_t old_index,
                                             const BookmarkNode* node,
                                             const std::set<GURL>& removed_urls,
                                             const base::Location& location) {
  InvalidateMenu();
}

void BookmarkMenuBridge::BookmarkAllUserNodesRemoved(
    const std::set<GURL>& removed_urls,
    const base::Location& location) {
  InvalidateMenu();
}

void BookmarkMenuBridge::BookmarkNodeChanged(const BookmarkNode* node) {
  NSMenuItem* item = MenuItemForNode(node);
  if (item)
    ConfigureMenuItem(node, item, true);
}

void BookmarkMenuBridge::BookmarkNodeFaviconChanged(const BookmarkNode* node) {
  NSMenuItem* item = MenuItemForNode(node);
  if (item)
    ConfigureMenuItem(node, item, false);
}

void BookmarkMenuBridge::BookmarkNodeChildrenReordered(
    const BookmarkNode* node) {
  InvalidateMenu();
}

// Watch for changes.
void BookmarkMenuBridge::ObserveBookmarkModel() {
  BookmarkModel* model = GetBookmarkModel();

  // In Guest mode, there is no bookmark model.
  if (!model)
    return;

  bookmark_model_observation_.Observe(model);
  if (model->loaded()) {
    BookmarkModelLoaded(false);
  }
}

BookmarkModel* BookmarkMenuBridge::GetBookmarkModel() {
  DCHECK(profile_);
  return BookmarkModelFactory::GetForBrowserContext(profile_);
}

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::AddNodeAsSubmenu(NSMenu* menu,
                                          const BookmarkNode* node,
                                          NSImage* image,
                                          bool recurse) {
  NSString* title = MenuTitleForNode(node);
  NSMenuItem* items = [[NSMenuItem alloc] initWithTitle:title
                                                 action:nil
                                          keyEquivalent:@""];
  [items setImage:image];
  NSMenu* submenu = [[NSMenu alloc] initWithTitle:title];
  [menu setSubmenu:submenu forItem:items];

  // 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_];
  [items setTag:node->id()];
  tag_to_guid_[node->id()] = node->uuid();

  [menu addItem:items];

  if (recurse)
    AddNodeToMenu(node, submenu, recurse);
}

// TODO(jrg): limit the number of bookmarks in the menubar?
void BookmarkMenuBridge::AddNodeToMenu(const BookmarkNode* node,
                                       NSMenu* menu,
                                       bool recurse) {
  if (node->children().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 auto& child : node->children()) {
    if (child->is_folder()) {
      AddNodeAsSubmenu(menu, child.get(), folder_image_, recurse);
    } else {
      NSMenuItem* item =
          [[NSMenuItem alloc] initWithTitle:MenuTitleForNode(child.get())
                                     action:nil
                              keyEquivalent:@""];
      bookmark_nodes_[child.get()] = item;
      tag_to_guid_[child->id()] = child->uuid();
      ConfigureMenuItem(child.get(), item, false);
      [menu addItem:item];
    }
  }
}

void BookmarkMenuBridge::ConfigureMenuItem(const BookmarkNode* node,
                                           NSMenuItem* item,
                                           bool set_title) {
  if (set_title)
    [item setTitle:MenuTitleForNode(node)];
  [item setTarget:controller_];
  [item setAction:@selector(openBookmarkMenuItem:)];
  [item setTag:node->id()];
  tag_to_guid_[node->id()] = node->uuid();
  if (node->is_url())
    [item setToolTip:[BookmarkMenuCocoaController tooltipForNode:node]];
  // Check to see if we have a favicon.
  NSImage* favicon = nil;
  BookmarkModel* model = GetBookmarkModel();
  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() {
  BuildRootMenu(/*recurse=*/true);
  profile_ = nullptr;
  bookmark_model_observation_.Reset();
  // |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;
}