blob: 8b4ce471addbfb5de59e41ee2bb024676e47ad82 [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/extensions/context_menu_matcher.h"
#include <string>
#include "base/memory/ptr_util.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/extensions/extension_util.h"
#include "chrome/common/extensions/api/context_menus.h"
#include "content/public/browser/browser_context.h"
#include "content/public/common/context_menu_params.h"
#include "extensions/browser/extension_registry.h"
#include "ui/gfx/favicon_size.h"
#include "ui/gfx/image/image.h"
namespace extensions {
namespace {
// The range of command IDs reserved for extension's custom menus.
// TODO(oshima): These values will be injected by embedders.
int extensions_context_custom_first = IDC_EXTENSIONS_CONTEXT_CUSTOM_FIRST;
int extensions_context_custom_last = IDC_EXTENSIONS_CONTEXT_CUSTOM_LAST;
} // namespace
// static
const size_t ContextMenuMatcher::kMaxExtensionItemTitleLength = 75;
// static
int ContextMenuMatcher::ConvertToExtensionsCustomCommandId(int id) {
return extensions_context_custom_first + id;
}
// static
bool ContextMenuMatcher::IsExtensionsCustomCommandId(int id) {
return id >= extensions_context_custom_first &&
id <= extensions_context_custom_last;
}
ContextMenuMatcher::ContextMenuMatcher(
content::BrowserContext* browser_context,
ui::SimpleMenuModel::Delegate* delegate,
ui::SimpleMenuModel* menu_model,
const base::Callback<bool(const MenuItem*)>& filter)
: browser_context_(browser_context),
menu_model_(menu_model),
delegate_(delegate),
filter_(filter),
is_smart_text_selection_enabled_(false) {}
void ContextMenuMatcher::AppendExtensionItems(
const MenuItem::ExtensionKey& extension_key,
const base::string16& selection_text,
int* index,
bool is_action_menu) {
DCHECK_GE(*index, 0);
int max_index =
extensions_context_custom_last - extensions_context_custom_first;
if (*index >= max_index)
return;
const Extension* extension = NULL;
MenuItem::List items;
bool can_cross_incognito;
if (!GetRelevantExtensionTopLevelItems(
extension_key, &extension, &can_cross_incognito, &items))
return;
if (items.empty())
return;
bool prepend_separator = false;
#if !defined(OS_CHROMEOS)
// If this is the first extension-provided menu item, and there are other
// items in the menu, and the last item is not a separator add a separator.
// Also, don't add separators when Smart Text Selection is enabled. Smart
// actions are grouped with extensions and the separator logic is
// handled by them.
prepend_separator = *index == 0 && menu_model_->GetItemCount() &&
!is_smart_text_selection_enabled_;
#endif
// Extensions (other than platform apps) are only allowed one top-level slot
// (and it can't be a radio or checkbox item because we are going to put the
// extension icon next to it), unless the context menu is an an action menu.
// Action menus do not include the extension action, and they only include
// items from one extension, so they are not placed within a submenu.
// Otherwise, we automatically push them into a submenu if there is more than
// one top-level item.
if (extension->is_platform_app() || is_action_menu) {
if (prepend_separator)
menu_model_->AddSeparator(ui::NORMAL_SEPARATOR);
RecursivelyAppendExtensionItems(items,
can_cross_incognito,
selection_text,
menu_model_,
index,
is_action_menu);
} else {
int menu_id = ConvertToExtensionsCustomCommandId(*index);
(*index)++;
base::string16 title;
MenuItem::List submenu_items;
if (items.size() > 1 || items[0]->type() != MenuItem::NORMAL) {
if (prepend_separator)
menu_model_->AddSeparator(ui::NORMAL_SEPARATOR);
title = base::UTF8ToUTF16(extension->name());
submenu_items = items;
} else {
// The top-level menu item, |item[0]|, is sandwiched between two menu
// separators. If the top-level menu item is visible, its preceding
// separator should be included in the UI model, so that both separators
// are shown. Otherwise if the top-level menu item is hidden, the
// preceding separator should be excluded, so that only one of the two
// separators remain.
if (prepend_separator && items[0]->visible())
menu_model_->AddSeparator(ui::NORMAL_SEPARATOR);
MenuItem* item = items[0];
extension_item_map_[menu_id] = item->id();
title = item->TitleWithReplacement(selection_text,
kMaxExtensionItemTitleLength);
submenu_items = GetRelevantExtensionItems(item->children(),
can_cross_incognito);
}
// Now add our item(s) to the menu_model_.
if (submenu_items.empty()) {
menu_model_->AddItem(menu_id, title);
} else {
ui::SimpleMenuModel* submenu = new ui::SimpleMenuModel(delegate_);
extension_menu_models_.push_back(base::WrapUnique(submenu));
menu_model_->AddSubMenu(menu_id, title, submenu);
RecursivelyAppendExtensionItems(submenu_items, can_cross_incognito,
selection_text, submenu, index,
false); // is_action_menu_top_level
}
if (!is_action_menu)
SetExtensionIcon(extension_key.extension_id);
}
}
void ContextMenuMatcher::Clear() {
extension_item_map_.clear();
extension_menu_models_.clear();
}
base::string16 ContextMenuMatcher::GetTopLevelContextMenuTitle(
const MenuItem::ExtensionKey& extension_key,
const base::string16& selection_text) {
const Extension* extension = NULL;
MenuItem::List items;
bool can_cross_incognito;
GetRelevantExtensionTopLevelItems(
extension_key, &extension, &can_cross_incognito, &items);
base::string16 title;
if (items.empty() ||
items.size() > 1 ||
items[0]->type() != MenuItem::NORMAL) {
title = base::UTF8ToUTF16(extension->name());
} else {
MenuItem* item = items[0];
title = item->TitleWithReplacement(
selection_text, kMaxExtensionItemTitleLength);
}
return title;
}
bool ContextMenuMatcher::IsCommandIdChecked(int command_id) const {
MenuItem* item = GetExtensionMenuItem(command_id);
if (!item)
return false;
return item->checked();
}
bool ContextMenuMatcher::IsCommandIdVisible(int command_id) const {
MenuItem* item = GetExtensionMenuItem(command_id);
// The context menu code creates a top-level menu item, labeled with the
// extension's name, that is a container of an extension's menu items. This
// top-level menu item is not added to the context menu, so checking its
// visibility is a special case handled below. This top-level menu item should
// always be displayed.
if (!item && ContextMenuMatcher::IsExtensionsCustomCommandId(command_id)) {
return true;
} else if (item) {
return item->visible();
} else {
return false;
}
}
bool ContextMenuMatcher::IsCommandIdEnabled(int command_id) const {
MenuItem* item = GetExtensionMenuItem(command_id);
if (!item)
return true;
return item->enabled();
}
void ContextMenuMatcher::ExecuteCommand(
int command_id,
content::WebContents* web_contents,
content::RenderFrameHost* render_frame_host,
const content::ContextMenuParams& params) {
MenuItem* item = GetExtensionMenuItem(command_id);
if (!item)
return;
MenuManager* manager = MenuManager::Get(browser_context_);
manager->ExecuteCommand(browser_context_, web_contents, render_frame_host,
params, item->id());
}
bool ContextMenuMatcher::GetRelevantExtensionTopLevelItems(
const MenuItem::ExtensionKey& extension_key,
const Extension** extension,
bool* can_cross_incognito,
MenuItem::List* items) {
*extension = ExtensionRegistry::Get(
browser_context_)->enabled_extensions().GetByID(
extension_key.extension_id);
if (!*extension)
return false;
// Find matching items.
MenuManager* manager = MenuManager::Get(browser_context_);
const MenuItem::OwnedList* all_items = manager->MenuItems(extension_key);
if (!all_items || all_items->empty())
return false;
*can_cross_incognito = util::CanCrossIncognito(*extension, browser_context_);
*items = GetRelevantExtensionItems(*all_items, *can_cross_incognito);
return true;
}
MenuItem::List ContextMenuMatcher::GetRelevantExtensionItems(
const MenuItem::OwnedList& items,
bool can_cross_incognito) {
MenuItem::List result;
for (auto i = items.begin(); i != items.end(); ++i) {
MenuItem* item = i->get();
if (!filter_.Run(item))
continue;
if (item->id().incognito == browser_context_->IsOffTheRecord() ||
can_cross_incognito)
result.push_back(item);
}
return result;
}
void ContextMenuMatcher::RecursivelyAppendExtensionItems(
const MenuItem::List& items,
bool can_cross_incognito,
const base::string16& selection_text,
ui::SimpleMenuModel* menu_model,
int* index,
bool is_action_menu_top_level) {
MenuItem::Type last_type = MenuItem::NORMAL;
int radio_group_id = 1;
int num_visible_items = 0;
bool enable_separators = false;
#if !defined(OS_CHROMEOS)
enable_separators = true;
#endif
for (auto i = items.begin(); i != items.end(); ++i) {
MenuItem* item = *i;
// If last item was of type radio but the current one isn't, auto-insert
// a separator. The converse case is handled below.
if (last_type == MenuItem::RADIO && item->type() != MenuItem::RADIO &&
enable_separators) {
menu_model->AddSeparator(ui::NORMAL_SEPARATOR);
last_type = MenuItem::SEPARATOR;
}
int menu_id = ConvertToExtensionsCustomCommandId(*index);
// Action context menus have a limit for top level extension items to
// prevent control items from being pushed off the screen, since extension
// items will not be placed in a submenu.
const int top_level_limit = api::context_menus::ACTION_MENU_TOP_LEVEL_LIMIT;
if (menu_id >= extensions_context_custom_last ||
(is_action_menu_top_level && num_visible_items >= top_level_limit))
return;
++(*index);
if (item->visible())
++num_visible_items;
extension_item_map_[menu_id] = item->id();
base::string16 title = item->TitleWithReplacement(selection_text,
kMaxExtensionItemTitleLength);
if (item->type() == MenuItem::NORMAL) {
MenuItem::List children =
GetRelevantExtensionItems(item->children(), can_cross_incognito);
if (children.empty()) {
menu_model->AddItem(menu_id, title);
} else {
ui::SimpleMenuModel* submenu = new ui::SimpleMenuModel(delegate_);
extension_menu_models_.push_back(base::WrapUnique(submenu));
menu_model->AddSubMenu(menu_id, title, submenu);
RecursivelyAppendExtensionItems(children, can_cross_incognito,
selection_text, submenu, index,
false); // is_action_menu_top_level
}
} else if (item->type() == MenuItem::CHECKBOX) {
menu_model->AddCheckItem(menu_id, title);
} else if (item->type() == MenuItem::RADIO) {
if (i != items.begin() &&
last_type != MenuItem::RADIO) {
radio_group_id++;
// Auto-append a separator if needed.
if (enable_separators)
menu_model->AddSeparator(ui::NORMAL_SEPARATOR);
}
menu_model->AddRadioItem(menu_id, title, radio_group_id);
} else if (item->type() == MenuItem::SEPARATOR && enable_separators) {
menu_model->AddSeparator(ui::NORMAL_SEPARATOR);
}
last_type = item->type();
}
}
MenuItem* ContextMenuMatcher::GetExtensionMenuItem(int id) const {
MenuManager* manager = MenuManager::Get(browser_context_);
auto i = extension_item_map_.find(id);
if (i != extension_item_map_.end()) {
MenuItem* item = manager->GetItemById(i->second);
if (item)
return item;
}
return NULL;
}
void ContextMenuMatcher::SetExtensionIcon(const std::string& extension_id) {
MenuManager* menu_manager = MenuManager::Get(browser_context_);
int index = menu_model_->GetItemCount() - 1;
DCHECK_GE(index, 0);
gfx::Image icon = menu_manager->GetIconForExtension(extension_id);
DCHECK_EQ(gfx::kFaviconSize, icon.Width());
DCHECK_EQ(gfx::kFaviconSize, icon.Height());
menu_model_->SetIcon(index, icon);
}
} // namespace extensions