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