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
17namespace extensions {
18
19namespace {
20
21// The range of command IDs reserved for extension's custom menus.
22// TODO(oshima): These values will be injected by embedders.
23int extensions_context_custom_first = IDC_EXTENSIONS_CONTEXT_CUSTOM_FIRST;
24int extensions_context_custom_last = IDC_EXTENSIONS_CONTEXT_CUSTOM_LAST;
25
26}  // namespace
27
28// static
29const size_t ContextMenuMatcher::kMaxExtensionItemTitleLength = 75;
30
31// static
32int ContextMenuMatcher::ConvertToExtensionsCustomCommandId(int id) {
33  return extensions_context_custom_first + id;
34}
35
36// static
37bool ContextMenuMatcher::IsExtensionsCustomCommandId(int id) {
38  return id >= extensions_context_custom_first &&
39         id <= extensions_context_custom_last;
40}
41
42ContextMenuMatcher::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
53void 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
130void ContextMenuMatcher::Clear() {
131  extension_item_map_.clear();
132  extension_menu_models_.clear();
133}
134
135base::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
158bool ContextMenuMatcher::IsCommandIdChecked(int command_id) const {
159  MenuItem* item = GetExtensionMenuItem(command_id);
160  if (!item)
161    return false;
162  return item->checked();
163}
164
165bool ContextMenuMatcher::IsCommandIdEnabled(int command_id) const {
166  MenuItem* item = GetExtensionMenuItem(command_id);
167  if (!item)
168    return true;
169  return item->enabled();
170}
171
172void 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
183bool 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
206MenuItem::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
224void 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
296MenuItem* 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
308void 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