1// Copyright (c) 2011 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_controller_views.h"
6
7#include "base/stl_util-inl.h"
8#include "base/utf_string_conversions.h"
9#include "chrome/browser/bookmarks/bookmark_model.h"
10#include "chrome/browser/bookmarks/bookmark_node_data.h"
11#include "chrome/browser/bookmarks/bookmark_utils.h"
12#include "chrome/browser/metrics/user_metrics.h"
13#include "chrome/browser/prefs/pref_service.h"
14#include "chrome/browser/profiles/profile.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/event_utils.h"
18#include "chrome/common/pref_names.h"
19#include "content/browser/tab_contents/page_navigator.h"
20#include "content/common/page_transition_types.h"
21#include "grit/app_resources.h"
22#include "grit/generated_resources.h"
23#include "grit/theme_resources.h"
24#include "ui/base/dragdrop/os_exchange_data.h"
25#include "ui/base/resource/resource_bundle.h"
26#include "views/controls/button/menu_button.h"
27
28using views::MenuItemView;
29
30// Max width of a menu. There does not appear to be an OS value for this, yet
31// both IE and FF restrict the max width of a menu.
32static const int kMaxMenuWidth = 400;
33
34BookmarkMenuController::BookmarkMenuController(Browser* browser,
35                                               Profile* profile,
36                                               PageNavigator* navigator,
37                                               gfx::NativeWindow parent,
38                                               const BookmarkNode* node,
39                                               int start_child_index)
40    : browser_(browser),
41      profile_(profile),
42      page_navigator_(navigator),
43      parent_(parent),
44      node_(node),
45      menu_(NULL),
46      observer_(NULL),
47      for_drop_(false),
48      bookmark_bar_(NULL),
49      next_menu_id_(1) {
50  menu_ = CreateMenu(node, start_child_index);
51}
52
53void BookmarkMenuController::RunMenuAt(BookmarkBarView* bookmark_bar,
54                                       bool for_drop) {
55  bookmark_bar_ = bookmark_bar;
56  views::MenuButton* menu_button = bookmark_bar_->GetMenuButtonForNode(node_);
57  DCHECK(menu_button);
58  MenuItemView::AnchorPosition anchor;
59  int start_index;
60  bookmark_bar_->GetAnchorPositionAndStartIndexForButton(
61      menu_button, &anchor, &start_index);
62  RunMenuAt(menu_button, anchor, for_drop);
63}
64
65void BookmarkMenuController::RunMenuAt(
66    views::MenuButton* button,
67    MenuItemView::AnchorPosition position,
68    bool for_drop) {
69  gfx::Point screen_loc;
70  views::View::ConvertPointToScreen(button, &screen_loc);
71  // Subtract 1 from the height to make the popup flush with the button border.
72  gfx::Rect bounds(screen_loc.x(), screen_loc.y(), button->width(),
73                   button->height() - 1);
74  for_drop_ = for_drop;
75  profile_->GetBookmarkModel()->AddObserver(this);
76  // The constructor creates the initial menu and that is all we should have
77  // at this point.
78  DCHECK(node_to_menu_map_.size() == 1);
79  if (for_drop) {
80    menu_->RunMenuForDropAt(parent_, bounds, position);
81  } else {
82    menu_->RunMenuAt(parent_, button, bounds, position, false);
83    delete this;
84  }
85}
86
87void BookmarkMenuController::Cancel() {
88  menu_->Cancel();
89}
90
91std::wstring BookmarkMenuController::GetTooltipText(
92    int id, const gfx::Point& screen_loc) {
93  DCHECK(menu_id_to_node_map_.find(id) != menu_id_to_node_map_.end());
94
95  const BookmarkNode* node = menu_id_to_node_map_[id];
96  if (node->type() == BookmarkNode::URL)
97    return BookmarkBarView::CreateToolTipForURLAndTitle(
98        screen_loc, node->GetURL(), UTF16ToWide(node->GetTitle()), profile_);
99  return std::wstring();
100}
101
102bool BookmarkMenuController::IsTriggerableEvent(const views::MouseEvent& e) {
103  return event_utils::IsPossibleDispositionEvent(e);
104}
105
106void BookmarkMenuController::ExecuteCommand(int id, int mouse_event_flags) {
107  DCHECK(page_navigator_);
108  DCHECK(menu_id_to_node_map_.find(id) != menu_id_to_node_map_.end());
109
110  const BookmarkNode* node = menu_id_to_node_map_[id];
111  std::vector<const BookmarkNode*> selection;
112  selection.push_back(node);
113
114  WindowOpenDisposition initial_disposition =
115      event_utils::DispositionFromEventFlags(mouse_event_flags);
116
117  bookmark_utils::OpenAll(parent_, profile_, page_navigator_, selection,
118                          initial_disposition);
119}
120
121bool BookmarkMenuController::GetDropFormats(
122      MenuItemView* menu,
123      int* formats,
124      std::set<ui::OSExchangeData::CustomFormat>* custom_formats) {
125  *formats = ui::OSExchangeData::URL;
126  custom_formats->insert(BookmarkNodeData::GetBookmarkCustomFormat());
127  return true;
128}
129
130bool BookmarkMenuController::AreDropTypesRequired(MenuItemView* menu) {
131  return true;
132}
133
134bool BookmarkMenuController::CanDrop(MenuItemView* menu,
135                                     const ui::OSExchangeData& data) {
136  // Only accept drops of 1 node, which is the case for all data dragged from
137  // bookmark bar and menus.
138
139  if (!drop_data_.Read(data) || drop_data_.elements.size() != 1 ||
140      !profile_->GetPrefs()->GetBoolean(prefs::kEditBookmarksEnabled))
141    return false;
142
143  if (drop_data_.has_single_url())
144    return true;
145
146  const BookmarkNode* drag_node = drop_data_.GetFirstNode(profile_);
147  if (!drag_node) {
148    // Dragging a folder from another profile, always accept.
149    return true;
150  }
151
152  // Drag originated from same profile and is not a URL. Only accept it if
153  // the dragged node is not a parent of the node menu represents.
154  const BookmarkNode* drop_node = menu_id_to_node_map_[menu->GetCommand()];
155  DCHECK(drop_node);
156  while (drop_node && drop_node != drag_node)
157    drop_node = drop_node->parent();
158  return (drop_node == NULL);
159}
160
161int BookmarkMenuController::GetDropOperation(
162    MenuItemView* item,
163    const views::DropTargetEvent& event,
164    DropPosition* position) {
165  // Should only get here if we have drop data.
166  DCHECK(drop_data_.is_valid());
167
168  const BookmarkNode* node = menu_id_to_node_map_[item->GetCommand()];
169  const BookmarkNode* drop_parent = node->parent();
170  int index_to_drop_at = drop_parent->GetIndexOf(node);
171  if (*position == DROP_AFTER) {
172    index_to_drop_at++;
173  } else if (*position == DROP_ON) {
174    drop_parent = node;
175    index_to_drop_at = node->child_count();
176  }
177  DCHECK(drop_parent);
178  return bookmark_utils::BookmarkDropOperation(
179      profile_, event, drop_data_, drop_parent, index_to_drop_at);
180}
181
182int BookmarkMenuController::OnPerformDrop(MenuItemView* menu,
183                                          DropPosition position,
184                                          const views::DropTargetEvent& event) {
185  const BookmarkNode* drop_node = menu_id_to_node_map_[menu->GetCommand()];
186  DCHECK(drop_node);
187  BookmarkModel* model = profile_->GetBookmarkModel();
188  DCHECK(model);
189  const BookmarkNode* drop_parent = drop_node->parent();
190  DCHECK(drop_parent);
191  int index_to_drop_at = drop_parent->GetIndexOf(drop_node);
192  if (position == DROP_AFTER) {
193    index_to_drop_at++;
194  } else if (position == DROP_ON) {
195    DCHECK(drop_node->is_folder());
196    drop_parent = drop_node;
197    index_to_drop_at = drop_node->child_count();
198  }
199
200  int result = bookmark_utils::PerformBookmarkDrop(
201      profile_, drop_data_, drop_parent, index_to_drop_at);
202  if (for_drop_)
203    delete this;
204  return result;
205}
206
207bool BookmarkMenuController::ShowContextMenu(MenuItemView* source,
208                                             int id,
209                                             const gfx::Point& p,
210                                             bool is_mouse_gesture) {
211  DCHECK(menu_id_to_node_map_.find(id) != menu_id_to_node_map_.end());
212  std::vector<const BookmarkNode*> nodes;
213  nodes.push_back(menu_id_to_node_map_[id]);
214  context_menu_.reset(
215      new BookmarkContextMenu(
216          parent_,
217          profile_,
218          page_navigator_,
219          nodes[0]->parent(),
220          nodes));
221  context_menu_->set_observer(this);
222  context_menu_->RunMenuAt(p);
223  context_menu_.reset(NULL);
224  return true;
225}
226
227void BookmarkMenuController::DropMenuClosed(MenuItemView* menu) {
228  delete this;
229}
230
231bool BookmarkMenuController::CanDrag(MenuItemView* menu) {
232  return true;
233}
234
235void BookmarkMenuController::WriteDragData(MenuItemView* sender,
236                                           ui::OSExchangeData* data) {
237  DCHECK(sender && data);
238
239  UserMetrics::RecordAction(UserMetricsAction("BookmarkBar_DragFromFolder"),
240                            profile_);
241
242  BookmarkNodeData drag_data(menu_id_to_node_map_[sender->GetCommand()]);
243  drag_data.Write(profile_, data);
244}
245
246int BookmarkMenuController::GetDragOperations(MenuItemView* sender) {
247  return bookmark_utils::BookmarkDragOperation(profile_,
248      menu_id_to_node_map_[sender->GetCommand()]);
249}
250
251views::MenuItemView* BookmarkMenuController::GetSiblingMenu(
252    views::MenuItemView* menu,
253    const gfx::Point& screen_point,
254    views::MenuItemView::AnchorPosition* anchor,
255    bool* has_mnemonics,
256    views::MenuButton** button) {
257  if (!bookmark_bar_ || for_drop_)
258    return NULL;
259  gfx::Point bookmark_bar_loc(screen_point);
260  views::View::ConvertPointToView(NULL, bookmark_bar_, &bookmark_bar_loc);
261  int start_index;
262  const BookmarkNode* node =
263      bookmark_bar_->GetNodeForButtonAt(bookmark_bar_loc, &start_index);
264  if (!node || !node->is_folder())
265    return NULL;
266
267  MenuItemView* alt_menu = node_to_menu_map_[node];
268  if (!alt_menu)
269    alt_menu = CreateMenu(node, start_index);
270
271  menu_ = alt_menu;
272
273  *button = bookmark_bar_->GetMenuButtonForNode(node);
274  bookmark_bar_->GetAnchorPositionAndStartIndexForButton(
275      *button, anchor, &start_index);
276  *has_mnemonics = false;
277  return alt_menu;
278}
279
280int BookmarkMenuController::GetMaxWidthForMenu() {
281  return kMaxMenuWidth;
282}
283
284void BookmarkMenuController::BookmarkModelChanged() {
285  menu_->Cancel();
286}
287
288void BookmarkMenuController::BookmarkNodeFaviconLoaded(
289    BookmarkModel* model, const BookmarkNode* node) {
290  NodeToMenuIDMap::iterator menu_pair = node_to_menu_id_map_.find(node);
291  if (menu_pair == node_to_menu_id_map_.end())
292    return;  // We're not showing a menu item for the node.
293
294  // Iterate through the menus looking for the menu containing node.
295  for (NodeToMenuMap::iterator i = node_to_menu_map_.begin();
296       i != node_to_menu_map_.end(); ++i) {
297    MenuItemView* menu_item = i->second->GetMenuItemByID(menu_pair->second);
298    if (menu_item) {
299      menu_item->SetIcon(model->GetFavicon(node));
300      return;
301    }
302  }
303}
304
305void BookmarkMenuController::WillRemoveBookmarks(
306    const std::vector<const BookmarkNode*>& bookmarks) {
307  std::set<MenuItemView*> removed_menus;
308
309  WillRemoveBookmarksImpl(bookmarks, &removed_menus);
310
311  STLDeleteElements(&removed_menus);
312}
313
314void BookmarkMenuController::DidRemoveBookmarks() {
315  profile_->GetBookmarkModel()->AddObserver(this);
316}
317
318MenuItemView* BookmarkMenuController::CreateMenu(const BookmarkNode* parent,
319                                                 int start_child_index) {
320  MenuItemView* menu = new MenuItemView(this);
321  menu->SetCommand(next_menu_id_++);
322  menu_id_to_node_map_[menu->GetCommand()] = parent;
323  menu->set_has_icons(true);
324  BuildMenu(parent, start_child_index, menu, &next_menu_id_);
325  node_to_menu_map_[parent] = menu;
326  return menu;
327}
328
329void BookmarkMenuController::BuildMenu(const BookmarkNode* parent,
330                                       int start_child_index,
331                                       MenuItemView* menu,
332                                       int* next_menu_id) {
333  DCHECK(!parent->child_count() ||
334         start_child_index < parent->child_count());
335  for (int i = start_child_index; i < parent->child_count(); ++i) {
336    const BookmarkNode* node = parent->GetChild(i);
337    int id = *next_menu_id;
338
339    (*next_menu_id)++;
340    if (node->is_url()) {
341      SkBitmap icon = profile_->GetBookmarkModel()->GetFavicon(node);
342      if (icon.width() == 0) {
343        icon = *ResourceBundle::GetSharedInstance().
344            GetBitmapNamed(IDR_DEFAULT_FAVICON);
345      }
346      menu->AppendMenuItemWithIcon(id, UTF16ToWide(node->GetTitle()), icon);
347      node_to_menu_id_map_[node] = id;
348    } else if (node->is_folder()) {
349      SkBitmap* folder_icon = ResourceBundle::GetSharedInstance().
350          GetBitmapNamed(IDR_BOOKMARK_BAR_FOLDER);
351      MenuItemView* submenu = menu->AppendSubMenuWithIcon(id,
352          UTF16ToWide(node->GetTitle()), *folder_icon);
353      node_to_menu_id_map_[node] = id;
354      BuildMenu(node, 0, submenu, next_menu_id);
355    } else {
356      NOTREACHED();
357    }
358    menu_id_to_node_map_[id] = node;
359  }
360}
361
362BookmarkMenuController::~BookmarkMenuController() {
363  profile_->GetBookmarkModel()->RemoveObserver(this);
364  if (observer_)
365    observer_->BookmarkMenuDeleted(this);
366  STLDeleteValues(&node_to_menu_map_);
367}
368
369MenuItemView* BookmarkMenuController::GetMenuByID(int id) {
370  for (NodeToMenuMap::const_iterator i = node_to_menu_map_.begin();
371       i != node_to_menu_map_.end(); ++i) {
372    MenuItemView* menu = i->second->GetMenuItemByID(id);
373    if (menu)
374      return menu;
375  }
376  return NULL;
377}
378
379void BookmarkMenuController::WillRemoveBookmarksImpl(
380      const std::vector<const BookmarkNode*>& bookmarks,
381      std::set<views::MenuItemView*>* removed_menus) {
382  // Remove the observer so that when the remove happens we don't prematurely
383  // cancel the menu.
384  profile_->GetBookmarkModel()->RemoveObserver(this);
385
386  // Remove the menu items.
387  std::set<MenuItemView*> changed_parent_menus;
388  for (std::vector<const BookmarkNode*>::const_iterator i = bookmarks.begin();
389       i != bookmarks.end(); ++i) {
390    NodeToMenuIDMap::iterator node_to_menu = node_to_menu_id_map_.find(*i);
391    if (node_to_menu != node_to_menu_id_map_.end()) {
392      MenuItemView* menu = GetMenuByID(node_to_menu->second);
393      DCHECK(menu);  // If there an entry in node_to_menu_id_map_, there should
394                     // be a menu.
395      removed_menus->insert(menu);
396      changed_parent_menus.insert(menu->GetParentMenuItem());
397      menu->parent()->RemoveChildView(menu);
398      node_to_menu_id_map_.erase(node_to_menu);
399    }
400  }
401
402  // All the bookmarks in |bookmarks| should have the same parent. It's possible
403  // to support different parents, but this would need to prune any nodes whose
404  // parent has been removed. As all nodes currently have the same parent, there
405  // is the DCHECK.
406  DCHECK(changed_parent_menus.size() <= 1);
407
408  for (std::set<MenuItemView*>::const_iterator i = changed_parent_menus.begin();
409       i != changed_parent_menus.end(); ++i) {
410    (*i)->ChildrenChanged();
411  }
412
413  // Remove any descendants of the removed nodes in node_to_menu_id_map_.
414  for (NodeToMenuIDMap::iterator i = node_to_menu_id_map_.begin();
415       i != node_to_menu_id_map_.end(); ) {
416    bool ancestor_removed = false;
417    for (std::vector<const BookmarkNode*>::const_iterator j = bookmarks.begin();
418         j != bookmarks.end(); ++j) {
419      if (i->first->HasAncestor(*j)) {
420        ancestor_removed = true;
421        break;
422      }
423    }
424    if (ancestor_removed) {
425      node_to_menu_id_map_.erase(i++);
426    } else {
427      ++i;
428    }
429  }
430}
431