context_menu_matcher.cc revision a1401311d1ab56c4ed0a474bd38c108f75cb0cd9
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 "base/strings/utf_string_conversions.h"
6#include "chrome/app/chrome_command_ids.h"
7#include "chrome/browser/extensions/context_menu_matcher.h"
8#include "chrome/browser/extensions/extension_service.h"
9#include "chrome/browser/extensions/extension_util.h"
10#include "chrome/browser/profiles/profile.h"
11#include "content/public/common/context_menu_params.h"
12#include "extensions/browser/extension_system.h"
13#include "ui/gfx/favicon_size.h"
14#include "ui/gfx/image/image.h"
15
16namespace extensions {
17
18// static
19const size_t ContextMenuMatcher::kMaxExtensionItemTitleLength = 75;
20
21ContextMenuMatcher::ContextMenuMatcher(
22    Profile* profile,
23    ui::SimpleMenuModel::Delegate* delegate,
24    ui::SimpleMenuModel* menu_model,
25    const base::Callback<bool(const MenuItem*)>& filter)
26    : profile_(profile), menu_model_(menu_model), delegate_(delegate),
27      filter_(filter) {
28}
29
30void ContextMenuMatcher::AppendExtensionItems(
31    const MenuItem::ExtensionKey& extension_key,
32    const base::string16& selection_text,
33    int* index) {
34  DCHECK_GE(*index, 0);
35  int max_index =
36      IDC_EXTENSIONS_CONTEXT_CUSTOM_LAST - IDC_EXTENSIONS_CONTEXT_CUSTOM_FIRST;
37  if (*index >= max_index)
38    return;
39
40  const Extension* extension = NULL;
41  MenuItem::List items;
42  bool can_cross_incognito;
43  if (!GetRelevantExtensionTopLevelItems(
44          extension_key, &extension, &can_cross_incognito, items))
45    return;
46
47  if (items.empty())
48    return;
49
50  // If this is the first extension-provided menu item, and there are other
51  // items in the menu, and the last item is not a separator add a separator.
52  if (*index == 0 && menu_model_->GetItemCount())
53    menu_model_->AddSeparator(ui::NORMAL_SEPARATOR);
54
55  // Extensions (other than platform apps) are only allowed one top-level slot
56  // (and it can't be a radio or checkbox item because we are going to put the
57  // extension icon next to it).
58  // If they have more than that, we automatically push them into a submenu.
59  if (extension->is_platform_app()) {
60    RecursivelyAppendExtensionItems(items, can_cross_incognito, selection_text,
61                                    menu_model_, index);
62  } else {
63    int menu_id = IDC_EXTENSIONS_CONTEXT_CUSTOM_FIRST + (*index)++;
64    base::string16 title;
65    MenuItem::List submenu_items;
66
67    if (items.size() > 1 || items[0]->type() != MenuItem::NORMAL) {
68      title = base::UTF8ToUTF16(extension->name());
69      submenu_items = items;
70    } else {
71      MenuItem* item = items[0];
72      extension_item_map_[menu_id] = item->id();
73      title = item->TitleWithReplacement(selection_text,
74                                       kMaxExtensionItemTitleLength);
75      submenu_items = GetRelevantExtensionItems(item->children(),
76                                                can_cross_incognito);
77    }
78
79    // Now add our item(s) to the menu_model_.
80    if (submenu_items.empty()) {
81      menu_model_->AddItem(menu_id, title);
82    } else {
83      ui::SimpleMenuModel* submenu = new ui::SimpleMenuModel(delegate_);
84      extension_menu_models_.push_back(submenu);
85      menu_model_->AddSubMenu(menu_id, title, submenu);
86      RecursivelyAppendExtensionItems(submenu_items, can_cross_incognito,
87                                      selection_text, submenu, index);
88    }
89    SetExtensionIcon(extension_key.extension_id);
90  }
91}
92
93void ContextMenuMatcher::Clear() {
94  extension_item_map_.clear();
95  extension_menu_models_.clear();
96}
97
98base::string16 ContextMenuMatcher::GetTopLevelContextMenuTitle(
99    const MenuItem::ExtensionKey& extension_key,
100    const base::string16& selection_text) {
101  const Extension* extension = NULL;
102  MenuItem::List items;
103  bool can_cross_incognito;
104  GetRelevantExtensionTopLevelItems(
105      extension_key, &extension, &can_cross_incognito, items);
106
107  base::string16 title;
108
109  if (items.empty() ||
110      items.size() > 1 ||
111      items[0]->type() != MenuItem::NORMAL) {
112    title = base::UTF8ToUTF16(extension->name());
113  } else {
114    MenuItem* item = items[0];
115    title = item->TitleWithReplacement(
116        selection_text, kMaxExtensionItemTitleLength);
117  }
118  return title;
119}
120
121bool ContextMenuMatcher::IsCommandIdChecked(int command_id) const {
122  MenuItem* item = GetExtensionMenuItem(command_id);
123  if (!item)
124    return false;
125  return item->checked();
126}
127
128bool ContextMenuMatcher::IsCommandIdEnabled(int command_id) const {
129  MenuItem* item = GetExtensionMenuItem(command_id);
130  if (!item)
131    return true;
132  return item->enabled();
133}
134
135void ContextMenuMatcher::ExecuteCommand(int command_id,
136    content::WebContents* web_contents,
137    const content::ContextMenuParams& params) {
138  MenuItem* item = GetExtensionMenuItem(command_id);
139  if (!item)
140    return;
141
142  MenuManager* manager = MenuManager::Get(profile_);
143  manager->ExecuteCommand(profile_, web_contents, params, item->id());
144}
145
146bool ContextMenuMatcher::GetRelevantExtensionTopLevelItems(
147    const MenuItem::ExtensionKey& extension_key,
148    const Extension** extension,
149    bool* can_cross_incognito,
150    MenuItem::List& items) {
151  ExtensionService* service =
152      extensions::ExtensionSystem::Get(profile_)->extension_service();
153  *extension = service->GetExtensionById(extension_key.extension_id, false);
154
155  if (!*extension)
156    return false;
157
158  // Find matching items.
159  MenuManager* manager = MenuManager::Get(profile_);
160  const MenuItem::List* all_items = manager->MenuItems(extension_key);
161  if (!all_items || all_items->empty())
162    return false;
163
164  *can_cross_incognito = util::CanCrossIncognito(*extension, profile_);
165  items = GetRelevantExtensionItems(*all_items,
166                                    *can_cross_incognito);
167
168  return true;
169}
170
171MenuItem::List ContextMenuMatcher::GetRelevantExtensionItems(
172    const MenuItem::List& items,
173    bool can_cross_incognito) {
174  MenuItem::List result;
175  for (MenuItem::List::const_iterator i = items.begin();
176       i != items.end(); ++i) {
177    const MenuItem* item = *i;
178
179    if (!filter_.Run(item))
180      continue;
181
182    if (item->id().incognito == profile_->IsOffTheRecord() ||
183        can_cross_incognito)
184      result.push_back(*i);
185  }
186  return result;
187}
188
189void ContextMenuMatcher::RecursivelyAppendExtensionItems(
190    const MenuItem::List& items,
191    bool can_cross_incognito,
192    const base::string16& selection_text,
193    ui::SimpleMenuModel* menu_model,
194    int* index)
195{
196  MenuItem::Type last_type = MenuItem::NORMAL;
197  int radio_group_id = 1;
198
199  for (MenuItem::List::const_iterator i = items.begin();
200       i != items.end(); ++i) {
201    MenuItem* item = *i;
202
203    // If last item was of type radio but the current one isn't, auto-insert
204    // a separator.  The converse case is handled below.
205    if (last_type == MenuItem::RADIO &&
206        item->type() != MenuItem::RADIO) {
207      menu_model->AddSeparator(ui::NORMAL_SEPARATOR);
208      last_type = MenuItem::SEPARATOR;
209    }
210
211    int menu_id = IDC_EXTENSIONS_CONTEXT_CUSTOM_FIRST + (*index)++;
212    if (menu_id >= IDC_EXTENSIONS_CONTEXT_CUSTOM_LAST)
213      return;
214    extension_item_map_[menu_id] = item->id();
215    base::string16 title = item->TitleWithReplacement(selection_text,
216                                                kMaxExtensionItemTitleLength);
217    if (item->type() == MenuItem::NORMAL) {
218      MenuItem::List children =
219          GetRelevantExtensionItems(item->children(), can_cross_incognito);
220      if (children.empty()) {
221        menu_model->AddItem(menu_id, title);
222      } else {
223        ui::SimpleMenuModel* submenu = new ui::SimpleMenuModel(delegate_);
224        extension_menu_models_.push_back(submenu);
225        menu_model->AddSubMenu(menu_id, title, submenu);
226        RecursivelyAppendExtensionItems(children, can_cross_incognito,
227                                        selection_text, submenu, index);
228      }
229    } else if (item->type() == MenuItem::CHECKBOX) {
230      menu_model->AddCheckItem(menu_id, title);
231    } else if (item->type() == MenuItem::RADIO) {
232      if (i != items.begin() &&
233          last_type != MenuItem::RADIO) {
234        radio_group_id++;
235
236        // Auto-append a separator if needed.
237        menu_model->AddSeparator(ui::NORMAL_SEPARATOR);
238      }
239
240      menu_model->AddRadioItem(menu_id, title, radio_group_id);
241    } else if (item->type() == MenuItem::SEPARATOR) {
242      menu_model->AddSeparator(ui::NORMAL_SEPARATOR);
243    }
244    last_type = item->type();
245  }
246}
247
248MenuItem* ContextMenuMatcher::GetExtensionMenuItem(int id) const {
249  MenuManager* manager = MenuManager::Get(profile_);
250  std::map<int, MenuItem::Id>::const_iterator i =
251      extension_item_map_.find(id);
252  if (i != extension_item_map_.end()) {
253    MenuItem* item = manager->GetItemById(i->second);
254    if (item)
255      return item;
256  }
257  return NULL;
258}
259
260void ContextMenuMatcher::SetExtensionIcon(const std::string& extension_id) {
261  MenuManager* menu_manager = MenuManager::Get(profile_);
262
263  int index = menu_model_->GetItemCount() - 1;
264  DCHECK_GE(index, 0);
265
266  const SkBitmap& icon = menu_manager->GetIconForExtension(extension_id);
267  DCHECK(icon.width() == gfx::kFaviconSize);
268  DCHECK(icon.height() == gfx::kFaviconSize);
269
270  menu_model_->SetIcon(index, gfx::Image::CreateFrom1xBitmap(icon));
271}
272
273}  // namespace extensions
274