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