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