bookmark_menu_delegate.cc revision 868fa2fe829687343ffae624259930155e16dbd8
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/ui/views/bookmarks/bookmark_menu_delegate.h"
6
7#include "base/prefs/pref_service.h"
8#include "base/strings/utf_string_conversions.h"
9#include "chrome/browser/bookmarks/bookmark_model.h"
10#include "chrome/browser/bookmarks/bookmark_model_factory.h"
11#include "chrome/browser/bookmarks/bookmark_node_data.h"
12#include "chrome/browser/bookmarks/bookmark_utils.h"
13#include "chrome/browser/profiles/profile.h"
14#include "chrome/browser/ui/bookmarks/bookmark_drag_drop.h"
15#include "chrome/browser/ui/bookmarks/bookmark_utils.h"
16#include "chrome/browser/ui/browser.h"
17#include "chrome/browser/ui/views/bookmarks/bookmark_bar_view.h"
18#include "chrome/browser/ui/views/bookmarks/bookmark_drag_drop_views.h"
19#include "chrome/browser/ui/views/event_utils.h"
20#include "chrome/common/pref_names.h"
21#include "content/public/browser/page_navigator.h"
22#include "content/public/browser/user_metrics.h"
23#include "grit/generated_resources.h"
24#include "grit/theme_resources.h"
25#include "grit/ui_resources.h"
26#include "ui/base/dragdrop/os_exchange_data.h"
27#include "ui/base/l10n/l10n_util.h"
28#include "ui/base/resource/resource_bundle.h"
29#include "ui/base/window_open_disposition.h"
30#include "ui/views/controls/button/menu_button.h"
31#include "ui/views/controls/menu/menu_item_view.h"
32#include "ui/views/controls/menu/submenu_view.h"
33#include "ui/views/widget/widget.h"
34
35using content::PageNavigator;
36using content::UserMetricsAction;
37using views::MenuItemView;
38
39// Max width of a menu. There does not appear to be an OS value for this, yet
40// both IE and FF restrict the max width of a menu.
41static const int kMaxMenuWidth = 400;
42
43BookmarkMenuDelegate::BookmarkMenuDelegate(Browser* browser,
44                                           PageNavigator* navigator,
45                                           views::Widget* parent,
46                                           int first_menu_id)
47    : browser_(browser),
48      profile_(browser->profile()),
49      page_navigator_(navigator),
50      parent_(parent),
51      menu_(NULL),
52      for_drop_(false),
53      parent_menu_item_(NULL),
54      next_menu_id_(first_menu_id),
55      real_delegate_(NULL),
56      is_mutating_model_(false),
57      location_(bookmark_utils::LAUNCH_NONE){
58}
59
60BookmarkMenuDelegate::~BookmarkMenuDelegate() {
61  BookmarkModelFactory::GetForProfile(profile_)->RemoveObserver(this);
62}
63
64void BookmarkMenuDelegate::Init(
65    views::MenuDelegate* real_delegate,
66    MenuItemView* parent,
67    const BookmarkNode* node,
68    int start_child_index,
69    ShowOptions show_options,
70    bookmark_utils::BookmarkLaunchLocation location) {
71  BookmarkModelFactory::GetForProfile(profile_)->AddObserver(this);
72  real_delegate_ = real_delegate;
73  if (parent) {
74    parent_menu_item_ = parent;
75    int initial_count = parent->GetSubmenu() ?
76        parent->GetSubmenu()->GetMenuItemCount() : 0;
77    if ((start_child_index < node->child_count()) &&
78        (initial_count > 0)) {
79      parent->AppendSeparator();
80    }
81    BuildMenu(node, start_child_index, parent, &next_menu_id_);
82    if (show_options == SHOW_PERMANENT_FOLDERS)
83      BuildMenusForPermanentNodes(parent, &next_menu_id_);
84  } else {
85    menu_ = CreateMenu(node, start_child_index, show_options);
86  }
87
88  location_ = location;
89}
90
91void BookmarkMenuDelegate::SetPageNavigator(PageNavigator* navigator) {
92  page_navigator_ = navigator;
93  if (context_menu_.get())
94    context_menu_->SetPageNavigator(navigator);
95}
96
97void BookmarkMenuDelegate::SetActiveMenu(const BookmarkNode* node,
98                                         int start_index) {
99  DCHECK(!parent_menu_item_);
100  if (!node_to_menu_map_[node])
101    CreateMenu(node, start_index, HIDE_PERMANENT_FOLDERS);
102  menu_ = node_to_menu_map_[node];
103}
104
105string16 BookmarkMenuDelegate::GetTooltipText(
106    int id,
107    const gfx::Point& screen_loc) const {
108  MenuIDToNodeMap::const_iterator i = menu_id_to_node_map_.find(id);
109  DCHECK(i != menu_id_to_node_map_.end());
110  const BookmarkNode* node = i->second;
111  if (node->is_url()) {
112    return BookmarkBarView::CreateToolTipForURLAndTitle(
113        screen_loc, node->url(), node->GetTitle(), profile_,
114        parent()->GetNativeView());
115  }
116  return string16();
117}
118
119bool BookmarkMenuDelegate::IsTriggerableEvent(views::MenuItemView* menu,
120                                              const ui::Event& e) {
121  return e.type() == ui::ET_GESTURE_TAP ||
122         e.type() == ui::ET_GESTURE_TAP_DOWN ||
123         event_utils::IsPossibleDispositionEvent(e);
124}
125
126void BookmarkMenuDelegate::ExecuteCommand(int id, int mouse_event_flags) {
127  DCHECK(menu_id_to_node_map_.find(id) != menu_id_to_node_map_.end());
128
129  const BookmarkNode* node = menu_id_to_node_map_[id];
130  std::vector<const BookmarkNode*> selection;
131  selection.push_back(node);
132
133  chrome::OpenAll(parent_->GetNativeWindow(), page_navigator_, selection,
134                  ui::DispositionFromEventFlags(mouse_event_flags),
135                  profile_);
136  bookmark_utils::RecordBookmarkLaunch(location_);
137}
138
139bool BookmarkMenuDelegate::ShouldExecuteCommandWithoutClosingMenu(
140    int id, const ui::Event& event) {
141  return (event.flags() & ui::EF_LEFT_MOUSE_BUTTON) &&
142         ui::DispositionFromEventFlags(event.flags()) == NEW_BACKGROUND_TAB;
143}
144
145bool BookmarkMenuDelegate::GetDropFormats(
146    MenuItemView* menu,
147    int* formats,
148    std::set<ui::OSExchangeData::CustomFormat>* custom_formats) {
149  *formats = ui::OSExchangeData::URL;
150  custom_formats->insert(BookmarkNodeData::GetBookmarkCustomFormat());
151  return true;
152}
153
154bool BookmarkMenuDelegate::AreDropTypesRequired(MenuItemView* menu) {
155  return true;
156}
157
158bool BookmarkMenuDelegate::CanDrop(MenuItemView* menu,
159                                   const ui::OSExchangeData& data) {
160  // Only accept drops of 1 node, which is the case for all data dragged from
161  // bookmark bar and menus.
162
163  if (!drop_data_.Read(data) || drop_data_.elements.size() != 1 ||
164      !profile_->GetPrefs()->GetBoolean(prefs::kEditBookmarksEnabled))
165    return false;
166
167  if (drop_data_.has_single_url())
168    return true;
169
170  const BookmarkNode* drag_node = drop_data_.GetFirstNode(profile_);
171  if (!drag_node) {
172    // Dragging a folder from another profile, always accept.
173    return true;
174  }
175
176  // Drag originated from same profile and is not a URL. Only accept it if
177  // the dragged node is not a parent of the node menu represents.
178  if (menu_id_to_node_map_.find(menu->GetCommand()) ==
179      menu_id_to_node_map_.end()) {
180    // If we don't know the menu assume its because we're embedded. We'll
181    // figure out the real operation when GetDropOperation is invoked.
182    return true;
183  }
184  const BookmarkNode* drop_node = menu_id_to_node_map_[menu->GetCommand()];
185  DCHECK(drop_node);
186  while (drop_node && drop_node != drag_node)
187    drop_node = drop_node->parent();
188  return (drop_node == NULL);
189}
190
191int BookmarkMenuDelegate::GetDropOperation(
192    MenuItemView* item,
193    const ui::DropTargetEvent& event,
194    views::MenuDelegate::DropPosition* position) {
195  // Should only get here if we have drop data.
196  DCHECK(drop_data_.is_valid());
197
198  const BookmarkNode* node = menu_id_to_node_map_[item->GetCommand()];
199  const BookmarkNode* drop_parent = node->parent();
200  int index_to_drop_at = drop_parent->GetIndexOf(node);
201  switch (*position) {
202    case views::MenuDelegate::DROP_AFTER:
203      if (node == BookmarkModelFactory::GetForProfile(
204              profile_)->other_node() ||
205          node == BookmarkModelFactory::GetForProfile(
206              profile_)->mobile_node()) {
207        // Dropping after these nodes makes no sense.
208        *position = views::MenuDelegate::DROP_NONE;
209      }
210      index_to_drop_at++;
211      break;
212
213    case views::MenuDelegate::DROP_BEFORE:
214      if (node == BookmarkModelFactory::GetForProfile(
215              profile_)->mobile_node()) {
216        // Dropping before this node makes no sense.
217        *position = views::MenuDelegate::DROP_NONE;
218      }
219      break;
220
221    case views::MenuDelegate::DROP_ON:
222      drop_parent = node;
223      index_to_drop_at = node->child_count();
224      break;
225
226    default:
227      break;
228  }
229  DCHECK(drop_parent);
230  return chrome::GetBookmarkDropOperation(profile_, event, drop_data_,
231                                          drop_parent, index_to_drop_at);
232}
233
234int BookmarkMenuDelegate::OnPerformDrop(
235    MenuItemView* menu,
236    views::MenuDelegate::DropPosition position,
237    const ui::DropTargetEvent& event) {
238  const BookmarkNode* drop_node = menu_id_to_node_map_[menu->GetCommand()];
239  DCHECK(drop_node);
240  BookmarkModel* model = BookmarkModelFactory::GetForProfile(profile_);
241  DCHECK(model);
242  const BookmarkNode* drop_parent = drop_node->parent();
243  DCHECK(drop_parent);
244  int index_to_drop_at = drop_parent->GetIndexOf(drop_node);
245  switch (position) {
246    case views::MenuDelegate::DROP_AFTER:
247      index_to_drop_at++;
248      break;
249
250    case views::MenuDelegate::DROP_ON:
251      DCHECK(drop_node->is_folder());
252      drop_parent = drop_node;
253      index_to_drop_at = drop_node->child_count();
254      break;
255
256    case views::MenuDelegate::DROP_BEFORE:
257      if (drop_node == model->other_node() ||
258          drop_node == model->mobile_node()) {
259        // This can happen with SHOW_PERMANENT_FOLDERS.
260        drop_parent = model->bookmark_bar_node();
261        index_to_drop_at = drop_parent->child_count();
262      }
263      break;
264
265    default:
266      break;
267  }
268
269  return chrome::DropBookmarks(profile_, drop_data_,
270                               drop_parent, index_to_drop_at);
271}
272
273bool BookmarkMenuDelegate::ShowContextMenu(MenuItemView* source,
274                                           int id,
275                                           const gfx::Point& p,
276                                           bool is_mouse_gesture) {
277  DCHECK(menu_id_to_node_map_.find(id) != menu_id_to_node_map_.end());
278  std::vector<const BookmarkNode*> nodes;
279  nodes.push_back(menu_id_to_node_map_[id]);
280  bool close_on_delete = !parent_menu_item_ &&
281      (nodes[0]->parent() == BookmarkModelFactory::GetForProfile(
282          profile())->other_node() &&
283       nodes[0]->parent()->child_count() == 1);
284  context_menu_.reset(
285      new BookmarkContextMenu(
286          parent_,
287          browser_,
288          profile_,
289          page_navigator_,
290          nodes[0]->parent(),
291          nodes,
292          close_on_delete));
293  context_menu_->set_observer(this);
294  context_menu_->RunMenuAt(p);
295  context_menu_.reset(NULL);
296  return true;
297}
298
299bool BookmarkMenuDelegate::CanDrag(MenuItemView* menu) {
300  const BookmarkNode* node = menu_id_to_node_map_[menu->GetCommand()];
301  // Don't let users drag the other folder.
302  return node->parent() != BookmarkModelFactory::GetForProfile(
303      profile_)->root_node();
304}
305
306void BookmarkMenuDelegate::WriteDragData(MenuItemView* sender,
307                                         ui::OSExchangeData* data) {
308  DCHECK(sender && data);
309
310  content::RecordAction(UserMetricsAction("BookmarkBar_DragFromFolder"));
311
312  BookmarkNodeData drag_data(menu_id_to_node_map_[sender->GetCommand()]);
313  drag_data.Write(profile_, data);
314}
315
316int BookmarkMenuDelegate::GetDragOperations(MenuItemView* sender) {
317  return chrome::GetBookmarkDragOperation(
318      profile_, menu_id_to_node_map_[sender->GetCommand()]);
319}
320
321int BookmarkMenuDelegate::GetMaxWidthForMenu(MenuItemView* menu) {
322  return kMaxMenuWidth;
323}
324
325void BookmarkMenuDelegate::BookmarkModelChanged() {
326}
327
328void BookmarkMenuDelegate::BookmarkNodeFaviconChanged(
329    BookmarkModel* model,
330    const BookmarkNode* node) {
331  NodeToMenuMap::iterator menu_pair = node_to_menu_map_.find(node);
332  if (menu_pair == node_to_menu_map_.end())
333    return;  // We're not showing a menu item for the node.
334
335  menu_pair->second->SetIcon(model->GetFavicon(node).AsImageSkia());
336}
337
338void BookmarkMenuDelegate::WillRemoveBookmarks(
339    const std::vector<const BookmarkNode*>& bookmarks) {
340  DCHECK(!is_mutating_model_);
341  is_mutating_model_ = true;  // Set to false in DidRemoveBookmarks().
342
343  // Remove the observer so that when the remove happens we don't prematurely
344  // cancel the menu. The observer is added back in DidRemoveBookmarks().
345  BookmarkModelFactory::GetForProfile(profile_)->RemoveObserver(this);
346
347  // Remove the menu items.
348  std::set<MenuItemView*> changed_parent_menus;
349  for (std::vector<const BookmarkNode*>::const_iterator i(bookmarks.begin());
350       i != bookmarks.end(); ++i) {
351    NodeToMenuMap::iterator node_to_menu = node_to_menu_map_.find(*i);
352    if (node_to_menu != node_to_menu_map_.end()) {
353      MenuItemView* menu = node_to_menu->second;
354      MenuItemView* parent = menu->GetParentMenuItem();
355      // |parent| is NULL when removing a root. This happens when right clicking
356      // to delete an empty folder.
357      if (parent) {
358        changed_parent_menus.insert(parent);
359        parent->RemoveMenuItemAt(menu->parent()->GetIndexOf(menu));
360      }
361      node_to_menu_map_.erase(node_to_menu);
362      menu_id_to_node_map_.erase(menu->GetCommand());
363    }
364  }
365
366  // All the bookmarks in |bookmarks| should have the same parent. It's possible
367  // to support different parents, but this would need to prune any nodes whose
368  // parent has been removed. As all nodes currently have the same parent, there
369  // is the DCHECK.
370  DCHECK(changed_parent_menus.size() <= 1);
371
372  for (std::set<MenuItemView*>::const_iterator i(changed_parent_menus.begin());
373       i != changed_parent_menus.end(); ++i)
374    (*i)->ChildrenChanged();
375
376  // Remove any descendants of the removed nodes in |node_to_menu_map_|.
377  for (NodeToMenuMap::iterator i(node_to_menu_map_.begin());
378       i != node_to_menu_map_.end(); ) {
379    bool ancestor_removed = false;
380    for (std::vector<const BookmarkNode*>::const_iterator j(bookmarks.begin());
381         j != bookmarks.end(); ++j) {
382      if (i->first->HasAncestor(*j)) {
383        ancestor_removed = true;
384        break;
385      }
386    }
387    if (ancestor_removed) {
388      menu_id_to_node_map_.erase(i->second->GetCommand());
389      node_to_menu_map_.erase(i++);
390    } else {
391      ++i;
392    }
393  }
394}
395
396void BookmarkMenuDelegate::DidRemoveBookmarks() {
397  // Balances remove in WillRemoveBookmarksImpl.
398  BookmarkModelFactory::GetForProfile(profile_)->AddObserver(this);
399  DCHECK(is_mutating_model_);
400  is_mutating_model_ = false;
401}
402
403MenuItemView* BookmarkMenuDelegate::CreateMenu(const BookmarkNode* parent,
404                                               int start_child_index,
405                                               ShowOptions show_options) {
406  MenuItemView* menu = new MenuItemView(real_delegate_);
407  menu->SetCommand(next_menu_id_++);
408  menu_id_to_node_map_[menu->GetCommand()] = parent;
409  menu->set_has_icons(true);
410  BuildMenu(parent, start_child_index, menu, &next_menu_id_);
411  if (show_options == SHOW_PERMANENT_FOLDERS)
412    BuildMenusForPermanentNodes(menu, &next_menu_id_);
413  return menu;
414}
415
416void BookmarkMenuDelegate::BuildMenusForPermanentNodes(
417    views::MenuItemView* menu,
418    int* next_menu_id) {
419  BookmarkModel* model = BookmarkModelFactory::GetForProfile(profile_);
420  bool added_separator = false;
421  BuildMenuForPermanentNode(model->other_node(), menu, next_menu_id,
422                            &added_separator);
423  BuildMenuForPermanentNode(model->mobile_node(), menu, next_menu_id,
424                            &added_separator);
425}
426
427void BookmarkMenuDelegate::BuildMenuForPermanentNode(
428    const BookmarkNode* node,
429    MenuItemView* menu,
430    int* next_menu_id,
431    bool* added_separator) {
432  if (!node->IsVisible() || node->GetTotalNodeCount() == 1)
433    return;  // No children, don't create a menu.
434
435  if (!*added_separator) {
436    *added_separator = true;
437    menu->AppendSeparator();
438  }
439  int id = *next_menu_id;
440  (*next_menu_id)++;
441  ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
442  gfx::ImageSkia* folder_icon = rb.GetImageSkiaNamed(IDR_BOOKMARK_BAR_FOLDER);
443  MenuItemView* submenu = menu->AppendSubMenuWithIcon(
444      id, node->GetTitle(), *folder_icon);
445  BuildMenu(node, 0, submenu, next_menu_id);
446  menu_id_to_node_map_[id] = node;
447}
448
449void BookmarkMenuDelegate::BuildMenu(const BookmarkNode* parent,
450                                     int start_child_index,
451                                     MenuItemView* menu,
452                                     int* next_menu_id) {
453  node_to_menu_map_[parent] = menu;
454  DCHECK(parent->empty() || start_child_index < parent->child_count());
455  ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
456  for (int i = start_child_index; i < parent->child_count(); ++i) {
457    const BookmarkNode* node = parent->GetChild(i);
458    const int id = *next_menu_id;
459    (*next_menu_id)++;
460
461    menu_id_to_node_map_[id] = node;
462    if (node->is_url()) {
463      const gfx::Image& image = BookmarkModelFactory::GetForProfile(
464          profile_)->GetFavicon(node);
465      const gfx::ImageSkia* icon = image.IsEmpty() ?
466          rb.GetImageSkiaNamed(IDR_DEFAULT_FAVICON) : image.ToImageSkia();
467      node_to_menu_map_[node] =
468          menu->AppendMenuItemWithIcon(id, node->GetTitle(), *icon);
469    } else if (node->is_folder()) {
470      gfx::ImageSkia* folder_icon =
471          rb.GetImageSkiaNamed(IDR_BOOKMARK_BAR_FOLDER);
472      MenuItemView* submenu = menu->AppendSubMenuWithIcon(
473          id, node->GetTitle(), *folder_icon);
474      BuildMenu(node, 0, submenu, next_menu_id);
475    } else {
476      NOTREACHED();
477    }
478  }
479}
480