Home | History | Annotate | Download | only in extensions
      1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 #include "chrome/browser/extensions/context_menu_matcher.h"
      6 
      7 #include "base/strings/utf_string_conversions.h"
      8 #include "chrome/app/chrome_command_ids.h"
      9 #include "chrome/browser/extensions/extension_util.h"
     10 #include "chrome/common/extensions/api/context_menus.h"
     11 #include "content/public/browser/browser_context.h"
     12 #include "content/public/common/context_menu_params.h"
     13 #include "extensions/browser/extension_registry.h"
     14 #include "ui/gfx/favicon_size.h"
     15 #include "ui/gfx/image/image.h"
     16 
     17 namespace extensions {
     18 
     19 namespace {
     20 
     21 // The range of command IDs reserved for extension's custom menus.
     22 // TODO(oshima): These values will be injected by embedders.
     23 int extensions_context_custom_first = IDC_EXTENSIONS_CONTEXT_CUSTOM_FIRST;
     24 int extensions_context_custom_last = IDC_EXTENSIONS_CONTEXT_CUSTOM_LAST;
     25 
     26 }  // namespace
     27 
     28 // static
     29 const size_t ContextMenuMatcher::kMaxExtensionItemTitleLength = 75;
     30 
     31 // static
     32 int ContextMenuMatcher::ConvertToExtensionsCustomCommandId(int id) {
     33   return extensions_context_custom_first + id;
     34 }
     35 
     36 // static
     37 bool ContextMenuMatcher::IsExtensionsCustomCommandId(int id) {
     38   return id >= extensions_context_custom_first &&
     39          id <= extensions_context_custom_last;
     40 }
     41 
     42 ContextMenuMatcher::ContextMenuMatcher(
     43     content::BrowserContext* browser_context,
     44     ui::SimpleMenuModel::Delegate* delegate,
     45     ui::SimpleMenuModel* menu_model,
     46     const base::Callback<bool(const MenuItem*)>& filter)
     47     : browser_context_(browser_context),
     48       menu_model_(menu_model),
     49       delegate_(delegate),
     50       filter_(filter) {
     51 }
     52 
     53 void ContextMenuMatcher::AppendExtensionItems(
     54     const MenuItem::ExtensionKey& extension_key,
     55     const base::string16& selection_text,
     56     int* index,
     57     bool is_action_menu) {
     58   DCHECK_GE(*index, 0);
     59   int max_index =
     60       extensions_context_custom_last - extensions_context_custom_first;
     61   if (*index >= max_index)
     62     return;
     63 
     64   const Extension* extension = NULL;
     65   MenuItem::List items;
     66   bool can_cross_incognito;
     67   if (!GetRelevantExtensionTopLevelItems(
     68           extension_key, &extension, &can_cross_incognito, &items))
     69     return;
     70 
     71   if (items.empty())
     72     return;
     73 
     74   // If this is the first extension-provided menu item, and there are other
     75   // items in the menu, and the last item is not a separator add a separator.
     76   if (*index == 0 && menu_model_->GetItemCount())
     77     menu_model_->AddSeparator(ui::NORMAL_SEPARATOR);
     78 
     79   // Extensions (other than platform apps) are only allowed one top-level slot
     80   // (and it can't be a radio or checkbox item because we are going to put the
     81   // extension icon next to it), unless the context menu is an an action menu.
     82   // Action menus do not include the extension action, and they only include
     83   // items from one extension, so they are not placed within a submenu.
     84   // Otherwise, we automatically push them into a submenu if there is more than
     85   // one top-level item.
     86   if (extension->is_platform_app() || is_action_menu) {
     87     RecursivelyAppendExtensionItems(items,
     88                                     can_cross_incognito,
     89                                     selection_text,
     90                                     menu_model_,
     91                                     index,
     92                                     is_action_menu);
     93   } else {
     94     int menu_id = ConvertToExtensionsCustomCommandId(*index);
     95     (*index)++;
     96     base::string16 title;
     97     MenuItem::List submenu_items;
     98 
     99     if (items.size() > 1 || items[0]->type() != MenuItem::NORMAL) {
    100       title = base::UTF8ToUTF16(extension->name());
    101       submenu_items = items;
    102     } else {
    103       MenuItem* item = items[0];
    104       extension_item_map_[menu_id] = item->id();
    105       title = item->TitleWithReplacement(selection_text,
    106                                        kMaxExtensionItemTitleLength);
    107       submenu_items = GetRelevantExtensionItems(item->children(),
    108                                                 can_cross_incognito);
    109     }
    110 
    111     // Now add our item(s) to the menu_model_.
    112     if (submenu_items.empty()) {
    113       menu_model_->AddItem(menu_id, title);
    114     } else {
    115       ui::SimpleMenuModel* submenu = new ui::SimpleMenuModel(delegate_);
    116       extension_menu_models_.push_back(submenu);
    117       menu_model_->AddSubMenu(menu_id, title, submenu);
    118       RecursivelyAppendExtensionItems(submenu_items,
    119                                       can_cross_incognito,
    120                                       selection_text,
    121                                       submenu,
    122                                       index,
    123                                       false);  // is_action_menu_top_level
    124     }
    125     if (!is_action_menu)
    126       SetExtensionIcon(extension_key.extension_id);
    127   }
    128 }
    129 
    130 void ContextMenuMatcher::Clear() {
    131   extension_item_map_.clear();
    132   extension_menu_models_.clear();
    133 }
    134 
    135 base::string16 ContextMenuMatcher::GetTopLevelContextMenuTitle(
    136     const MenuItem::ExtensionKey& extension_key,
    137     const base::string16& selection_text) {
    138   const Extension* extension = NULL;
    139   MenuItem::List items;
    140   bool can_cross_incognito;
    141   GetRelevantExtensionTopLevelItems(
    142       extension_key, &extension, &can_cross_incognito, &items);
    143 
    144   base::string16 title;
    145 
    146   if (items.empty() ||
    147       items.size() > 1 ||
    148       items[0]->type() != MenuItem::NORMAL) {
    149     title = base::UTF8ToUTF16(extension->name());
    150   } else {
    151     MenuItem* item = items[0];
    152     title = item->TitleWithReplacement(
    153         selection_text, kMaxExtensionItemTitleLength);
    154   }
    155   return title;
    156 }
    157 
    158 bool ContextMenuMatcher::IsCommandIdChecked(int command_id) const {
    159   MenuItem* item = GetExtensionMenuItem(command_id);
    160   if (!item)
    161     return false;
    162   return item->checked();
    163 }
    164 
    165 bool ContextMenuMatcher::IsCommandIdEnabled(int command_id) const {
    166   MenuItem* item = GetExtensionMenuItem(command_id);
    167   if (!item)
    168     return true;
    169   return item->enabled();
    170 }
    171 
    172 void ContextMenuMatcher::ExecuteCommand(int command_id,
    173     content::WebContents* web_contents,
    174     const content::ContextMenuParams& params) {
    175   MenuItem* item = GetExtensionMenuItem(command_id);
    176   if (!item)
    177     return;
    178 
    179   MenuManager* manager = MenuManager::Get(browser_context_);
    180   manager->ExecuteCommand(browser_context_, web_contents, params, item->id());
    181 }
    182 
    183 bool ContextMenuMatcher::GetRelevantExtensionTopLevelItems(
    184     const MenuItem::ExtensionKey& extension_key,
    185     const Extension** extension,
    186     bool* can_cross_incognito,
    187     MenuItem::List* items) {
    188   *extension = ExtensionRegistry::Get(
    189       browser_context_)->enabled_extensions().GetByID(
    190           extension_key.extension_id);
    191   if (!*extension)
    192     return false;
    193 
    194   // Find matching items.
    195   MenuManager* manager = MenuManager::Get(browser_context_);
    196   const MenuItem::List* all_items = manager->MenuItems(extension_key);
    197   if (!all_items || all_items->empty())
    198     return false;
    199 
    200   *can_cross_incognito = util::CanCrossIncognito(*extension, browser_context_);
    201   *items = GetRelevantExtensionItems(*all_items, *can_cross_incognito);
    202 
    203   return true;
    204 }
    205 
    206 MenuItem::List ContextMenuMatcher::GetRelevantExtensionItems(
    207     const MenuItem::List& items,
    208     bool can_cross_incognito) {
    209   MenuItem::List result;
    210   for (MenuItem::List::const_iterator i = items.begin();
    211        i != items.end(); ++i) {
    212     const MenuItem* item = *i;
    213 
    214     if (!filter_.Run(item))
    215       continue;
    216 
    217     if (item->id().incognito == browser_context_->IsOffTheRecord() ||
    218         can_cross_incognito)
    219       result.push_back(*i);
    220   }
    221   return result;
    222 }
    223 
    224 void ContextMenuMatcher::RecursivelyAppendExtensionItems(
    225     const MenuItem::List& items,
    226     bool can_cross_incognito,
    227     const base::string16& selection_text,
    228     ui::SimpleMenuModel* menu_model,
    229     int* index,
    230     bool is_action_menu_top_level) {
    231   MenuItem::Type last_type = MenuItem::NORMAL;
    232   int radio_group_id = 1;
    233   int num_items = 0;
    234 
    235   for (MenuItem::List::const_iterator i = items.begin();
    236        i != items.end(); ++i) {
    237     MenuItem* item = *i;
    238 
    239     // If last item was of type radio but the current one isn't, auto-insert
    240     // a separator.  The converse case is handled below.
    241     if (last_type == MenuItem::RADIO &&
    242         item->type() != MenuItem::RADIO) {
    243       menu_model->AddSeparator(ui::NORMAL_SEPARATOR);
    244       last_type = MenuItem::SEPARATOR;
    245     }
    246 
    247     int menu_id = ConvertToExtensionsCustomCommandId(*index);
    248     ++(*index);
    249     ++num_items;
    250     // Action context menus have a limit for top level extension items to
    251     // prevent control items from being pushed off the screen, since extension
    252     // items will not be placed in a submenu.
    253     const int top_level_limit = api::context_menus::ACTION_MENU_TOP_LEVEL_LIMIT;
    254     if (menu_id >= extensions_context_custom_last ||
    255         (is_action_menu_top_level && num_items >= top_level_limit))
    256       return;
    257 
    258     extension_item_map_[menu_id] = item->id();
    259     base::string16 title = item->TitleWithReplacement(selection_text,
    260                                                 kMaxExtensionItemTitleLength);
    261     if (item->type() == MenuItem::NORMAL) {
    262       MenuItem::List children =
    263           GetRelevantExtensionItems(item->children(), can_cross_incognito);
    264       if (children.empty()) {
    265         menu_model->AddItem(menu_id, title);
    266       } else {
    267         ui::SimpleMenuModel* submenu = new ui::SimpleMenuModel(delegate_);
    268         extension_menu_models_.push_back(submenu);
    269         menu_model->AddSubMenu(menu_id, title, submenu);
    270         RecursivelyAppendExtensionItems(children,
    271                                         can_cross_incognito,
    272                                         selection_text,
    273                                         submenu,
    274                                         index,
    275                                         false);  // is_action_menu_top_level
    276       }
    277     } else if (item->type() == MenuItem::CHECKBOX) {
    278       menu_model->AddCheckItem(menu_id, title);
    279     } else if (item->type() == MenuItem::RADIO) {
    280       if (i != items.begin() &&
    281           last_type != MenuItem::RADIO) {
    282         radio_group_id++;
    283 
    284         // Auto-append a separator if needed.
    285         menu_model->AddSeparator(ui::NORMAL_SEPARATOR);
    286       }
    287 
    288       menu_model->AddRadioItem(menu_id, title, radio_group_id);
    289     } else if (item->type() == MenuItem::SEPARATOR) {
    290       menu_model->AddSeparator(ui::NORMAL_SEPARATOR);
    291     }
    292     last_type = item->type();
    293   }
    294 }
    295 
    296 MenuItem* ContextMenuMatcher::GetExtensionMenuItem(int id) const {
    297   MenuManager* manager = MenuManager::Get(browser_context_);
    298   std::map<int, MenuItem::Id>::const_iterator i =
    299       extension_item_map_.find(id);
    300   if (i != extension_item_map_.end()) {
    301     MenuItem* item = manager->GetItemById(i->second);
    302     if (item)
    303       return item;
    304   }
    305   return NULL;
    306 }
    307 
    308 void ContextMenuMatcher::SetExtensionIcon(const std::string& extension_id) {
    309   MenuManager* menu_manager = MenuManager::Get(browser_context_);
    310 
    311   int index = menu_model_->GetItemCount() - 1;
    312   DCHECK_GE(index, 0);
    313 
    314   const SkBitmap& icon = menu_manager->GetIconForExtension(extension_id);
    315   DCHECK(icon.width() == gfx::kFaviconSize);
    316   DCHECK(icon.height() == gfx::kFaviconSize);
    317 
    318   menu_model_->SetIcon(index, gfx::Image::CreateFrom1xBitmap(icon));
    319 }
    320 
    321 }  // namespace extensions
    322