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.
5 #include "chrome/browser/ui/views/bookmarks/bookmark_menu_delegate.h"
7 #include "base/prefs/pref_service.h"
8 #include "base/strings/utf_string_conversions.h"
9 #include "chrome/app/chrome_command_ids.h"
10 #include "chrome/browser/bookmarks/bookmark_model_factory.h"
11 #include "chrome/browser/bookmarks/chrome_bookmark_client.h"
12 #include "chrome/browser/bookmarks/chrome_bookmark_client_factory.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/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/theme_resources.h"
24 #include "ui/base/dragdrop/os_exchange_data.h"
25 #include "ui/base/resource/resource_bundle.h"
26 #include "ui/base/window_open_disposition.h"
27 #include "ui/resources/grit/ui_resources.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"
33 using base::UserMetricsAction
;
34 using bookmarks::BookmarkModel
;
35 using bookmarks::BookmarkNode
;
36 using bookmarks::BookmarkNodeData
;
37 using content::PageNavigator
;
38 using views::MenuItemView
;
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.
42 static const int kMaxMenuWidth
= 400;
44 BookmarkMenuDelegate::BookmarkMenuDelegate(Browser
* browser
,
45 PageNavigator
* navigator
,
46 views::Widget
* parent
)
48 profile_(browser
->profile()),
49 page_navigator_(navigator
),
52 parent_menu_item_(NULL
),
53 next_menu_id_(IDC_FIRST_BOOKMARK_MENU
),
55 is_mutating_model_(false),
56 location_(BOOKMARK_LAUNCH_LOCATION_NONE
) {
59 BookmarkMenuDelegate::~BookmarkMenuDelegate() {
60 GetBookmarkModel()->RemoveObserver(this);
63 void BookmarkMenuDelegate::Init(views::MenuDelegate
* real_delegate
,
65 const BookmarkNode
* node
,
66 int start_child_index
,
67 ShowOptions show_options
,
68 BookmarkLaunchLocation location
) {
69 GetBookmarkModel()->AddObserver(this);
70 real_delegate_
= real_delegate
;
73 parent_menu_item_
= parent
;
75 // Add a separator if there are existing items in the menu, and if the
76 // current node has children. If |node| is the bookmark bar then the
77 // managed node is shown as its first child, if it's not empty.
78 BookmarkModel
* model
= GetBookmarkModel();
79 ChromeBookmarkClient
* client
= GetChromeBookmarkClient();
80 bool show_forced_folders
= show_options
== SHOW_PERMANENT_FOLDERS
&&
81 node
== model
->bookmark_bar_node();
82 bool show_managed
= show_forced_folders
&& !client
->managed_node()->empty();
83 bool show_supervised
=
84 show_forced_folders
&& !client
->supervised_node()->empty();
85 bool has_children
= (start_child_index
< node
->child_count()) ||
86 show_managed
|| show_supervised
;
87 int initial_count
= parent
->GetSubmenu() ?
88 parent
->GetSubmenu()->GetMenuItemCount() : 0;
89 if (has_children
&& initial_count
> 0)
90 parent
->AppendSeparator();
92 BuildMenuForManagedNode(parent
);
94 BuildMenuForSupervisedNode(parent
);
95 BuildMenu(node
, start_child_index
, parent
);
96 if (show_options
== SHOW_PERMANENT_FOLDERS
)
97 BuildMenusForPermanentNodes(parent
);
99 menu_
= CreateMenu(node
, start_child_index
, show_options
);
103 void BookmarkMenuDelegate::SetPageNavigator(PageNavigator
* navigator
) {
104 page_navigator_
= navigator
;
105 if (context_menu_
.get())
106 context_menu_
->SetPageNavigator(navigator
);
109 BookmarkModel
* BookmarkMenuDelegate::GetBookmarkModel() {
110 return BookmarkModelFactory::GetForProfile(profile_
);
113 ChromeBookmarkClient
* BookmarkMenuDelegate::GetChromeBookmarkClient() {
114 return ChromeBookmarkClientFactory::GetForProfile(profile_
);
117 void BookmarkMenuDelegate::SetActiveMenu(const BookmarkNode
* node
,
119 DCHECK(!parent_menu_item_
);
120 if (!node_to_menu_map_
[node
])
121 CreateMenu(node
, start_index
, HIDE_PERMANENT_FOLDERS
);
122 menu_
= node_to_menu_map_
[node
];
125 base::string16
BookmarkMenuDelegate::GetTooltipText(
127 const gfx::Point
& screen_loc
) const {
128 MenuIDToNodeMap::const_iterator i
= menu_id_to_node_map_
.find(id
);
129 // When removing bookmarks it may be possible to end up here without a node.
130 if (i
== menu_id_to_node_map_
.end()) {
131 DCHECK(is_mutating_model_
);
132 return base::string16();
135 const BookmarkNode
* node
= i
->second
;
136 if (node
->is_url()) {
137 return BookmarkBarView::CreateToolTipForURLAndTitle(
138 parent_
, screen_loc
, node
->url(), node
->GetTitle(), profile_
);
140 return base::string16();
143 bool BookmarkMenuDelegate::IsTriggerableEvent(views::MenuItemView
* menu
,
144 const ui::Event
& e
) {
145 return e
.type() == ui::ET_GESTURE_TAP
||
146 e
.type() == ui::ET_GESTURE_TAP_DOWN
||
147 event_utils::IsPossibleDispositionEvent(e
);
150 void BookmarkMenuDelegate::ExecuteCommand(int id
, int mouse_event_flags
) {
151 DCHECK(menu_id_to_node_map_
.find(id
) != menu_id_to_node_map_
.end());
153 const BookmarkNode
* node
= menu_id_to_node_map_
[id
];
154 std::vector
<const BookmarkNode
*> selection
;
155 selection
.push_back(node
);
157 chrome::OpenAll(parent_
->GetNativeWindow(), page_navigator_
, selection
,
158 ui::DispositionFromEventFlags(mouse_event_flags
),
160 RecordBookmarkLaunch(node
, location_
);
163 bool BookmarkMenuDelegate::ShouldExecuteCommandWithoutClosingMenu(
165 const ui::Event
& event
) {
166 return (event
.flags() & ui::EF_LEFT_MOUSE_BUTTON
) &&
167 ui::DispositionFromEventFlags(event
.flags()) == NEW_BACKGROUND_TAB
;
170 bool BookmarkMenuDelegate::GetDropFormats(
173 std::set
<ui::OSExchangeData::CustomFormat
>* custom_formats
) {
174 *formats
= ui::OSExchangeData::URL
;
175 custom_formats
->insert(BookmarkNodeData::GetBookmarkCustomFormat());
179 bool BookmarkMenuDelegate::AreDropTypesRequired(MenuItemView
* menu
) {
183 bool BookmarkMenuDelegate::CanDrop(MenuItemView
* menu
,
184 const ui::OSExchangeData
& data
) {
185 // Only accept drops of 1 node, which is the case for all data dragged from
186 // bookmark bar and menus.
188 if (!drop_data_
.Read(data
) || drop_data_
.size() != 1 ||
189 !profile_
->GetPrefs()->GetBoolean(
190 bookmarks::prefs::kEditBookmarksEnabled
))
193 if (drop_data_
.has_single_url())
196 const BookmarkNode
* drag_node
=
197 drop_data_
.GetFirstNode(GetBookmarkModel(), profile_
->GetPath());
199 // Dragging a folder from another profile, always accept.
203 // Drag originated from same profile and is not a URL. Only accept it if
204 // the dragged node is not a parent of the node menu represents.
205 if (menu_id_to_node_map_
.find(menu
->GetCommand()) ==
206 menu_id_to_node_map_
.end()) {
207 // If we don't know the menu assume its because we're embedded. We'll
208 // figure out the real operation when GetDropOperation is invoked.
211 const BookmarkNode
* drop_node
= menu_id_to_node_map_
[menu
->GetCommand()];
213 while (drop_node
&& drop_node
!= drag_node
)
214 drop_node
= drop_node
->parent();
215 return (drop_node
== NULL
);
218 int BookmarkMenuDelegate::GetDropOperation(
220 const ui::DropTargetEvent
& event
,
221 views::MenuDelegate::DropPosition
* position
) {
222 // Should only get here if we have drop data.
223 DCHECK(drop_data_
.is_valid());
225 const BookmarkNode
* node
= menu_id_to_node_map_
[item
->GetCommand()];
226 const BookmarkNode
* drop_parent
= node
->parent();
227 int index_to_drop_at
= drop_parent
->GetIndexOf(node
);
228 BookmarkModel
* model
= GetBookmarkModel();
230 case views::MenuDelegate::DROP_AFTER
:
231 if (node
== model
->other_node() || node
== model
->mobile_node()) {
232 // Dropping after these nodes makes no sense.
233 *position
= views::MenuDelegate::DROP_NONE
;
238 case views::MenuDelegate::DROP_BEFORE
:
239 if (node
== model
->mobile_node()) {
240 // Dropping before this node makes no sense.
241 *position
= views::MenuDelegate::DROP_NONE
;
245 case views::MenuDelegate::DROP_ON
:
247 index_to_drop_at
= node
->child_count();
254 return chrome::GetBookmarkDropOperation(
255 profile_
, event
, drop_data_
, drop_parent
, index_to_drop_at
);
258 int BookmarkMenuDelegate::OnPerformDrop(
260 views::MenuDelegate::DropPosition position
,
261 const ui::DropTargetEvent
& event
) {
262 const BookmarkNode
* drop_node
= menu_id_to_node_map_
[menu
->GetCommand()];
264 BookmarkModel
* model
= GetBookmarkModel();
266 const BookmarkNode
* drop_parent
= drop_node
->parent();
268 int index_to_drop_at
= drop_parent
->GetIndexOf(drop_node
);
270 case views::MenuDelegate::DROP_AFTER
:
274 case views::MenuDelegate::DROP_ON
:
275 DCHECK(drop_node
->is_folder());
276 drop_parent
= drop_node
;
277 index_to_drop_at
= drop_node
->child_count();
280 case views::MenuDelegate::DROP_BEFORE
:
281 if (drop_node
== model
->other_node() ||
282 drop_node
== model
->mobile_node()) {
283 // This can happen with SHOW_PERMANENT_FOLDERS.
284 drop_parent
= model
->bookmark_bar_node();
285 index_to_drop_at
= drop_parent
->child_count();
293 bool copy
= event
.source_operations() == ui::DragDropTypes::DRAG_COPY
;
294 return chrome::DropBookmarks(profile_
, drop_data_
,
295 drop_parent
, index_to_drop_at
, copy
);
298 bool BookmarkMenuDelegate::ShowContextMenu(MenuItemView
* source
,
301 ui::MenuSourceType source_type
) {
302 DCHECK(menu_id_to_node_map_
.find(id
) != menu_id_to_node_map_
.end());
303 std::vector
<const BookmarkNode
*> nodes
;
304 nodes
.push_back(menu_id_to_node_map_
[id
]);
305 bool close_on_delete
= !parent_menu_item_
&&
306 (nodes
[0]->parent() == GetBookmarkModel()->other_node() &&
307 nodes
[0]->parent()->child_count() == 1);
309 new BookmarkContextMenu(
317 context_menu_
->set_observer(this);
318 context_menu_
->RunMenuAt(p
, source_type
);
319 context_menu_
.reset(NULL
);
323 bool BookmarkMenuDelegate::CanDrag(MenuItemView
* menu
) {
324 const BookmarkNode
* node
= menu_id_to_node_map_
[menu
->GetCommand()];
325 // Don't let users drag the other folder.
326 return node
->parent() != GetBookmarkModel()->root_node();
329 void BookmarkMenuDelegate::WriteDragData(MenuItemView
* sender
,
330 ui::OSExchangeData
* data
) {
331 DCHECK(sender
&& data
);
333 content::RecordAction(UserMetricsAction("BookmarkBar_DragFromFolder"));
335 BookmarkNodeData
drag_data(menu_id_to_node_map_
[sender
->GetCommand()]);
336 drag_data
.Write(profile_
->GetPath(), data
);
339 int BookmarkMenuDelegate::GetDragOperations(MenuItemView
* sender
) {
340 return chrome::GetBookmarkDragOperation(
341 profile_
, menu_id_to_node_map_
[sender
->GetCommand()]);
344 int BookmarkMenuDelegate::GetMaxWidthForMenu(MenuItemView
* menu
) {
345 return kMaxMenuWidth
;
348 void BookmarkMenuDelegate::WillShowMenu(MenuItemView
* menu
) {
349 auto iter
= menu_id_to_node_map_
.find(menu
->GetCommand());
350 if ((iter
!= menu_id_to_node_map_
.end()) && iter
->second
->child_count() &&
351 !menu
->GetSubmenu()->GetMenuItemCount())
352 BuildMenu(iter
->second
, 0, menu
);
355 void BookmarkMenuDelegate::BookmarkModelChanged() {
358 void BookmarkMenuDelegate::BookmarkNodeFaviconChanged(
359 BookmarkModel
* model
,
360 const BookmarkNode
* node
) {
361 NodeToMenuMap::iterator menu_pair
= node_to_menu_map_
.find(node
);
362 if (menu_pair
== node_to_menu_map_
.end())
363 return; // We're not showing a menu item for the node.
365 menu_pair
->second
->SetIcon(model
->GetFavicon(node
).AsImageSkia());
368 void BookmarkMenuDelegate::WillRemoveBookmarks(
369 const std::vector
<const BookmarkNode
*>& bookmarks
) {
370 DCHECK(!is_mutating_model_
);
371 is_mutating_model_
= true; // Set to false in DidRemoveBookmarks().
373 // Remove the observer so that when the remove happens we don't prematurely
374 // cancel the menu. The observer is added back in DidRemoveBookmarks().
375 GetBookmarkModel()->RemoveObserver(this);
377 // Remove the menu items.
378 std::set
<MenuItemView
*> changed_parent_menus
;
379 for (std::vector
<const BookmarkNode
*>::const_iterator
i(bookmarks
.begin());
380 i
!= bookmarks
.end(); ++i
) {
381 NodeToMenuMap::iterator node_to_menu
= node_to_menu_map_
.find(*i
);
382 if (node_to_menu
!= node_to_menu_map_
.end()) {
383 MenuItemView
* menu
= node_to_menu
->second
;
384 MenuItemView
* parent
= menu
->GetParentMenuItem();
385 // |parent| is NULL when removing a root. This happens when right clicking
386 // to delete an empty folder.
388 changed_parent_menus
.insert(parent
);
389 parent
->RemoveMenuItemAt(menu
->parent()->GetIndexOf(menu
));
391 node_to_menu_map_
.erase(node_to_menu
);
392 menu_id_to_node_map_
.erase(menu
->GetCommand());
396 // All the bookmarks in |bookmarks| should have the same parent. It's possible
397 // to support different parents, but this would need to prune any nodes whose
398 // parent has been removed. As all nodes currently have the same parent, there
400 DCHECK(changed_parent_menus
.size() <= 1);
402 // Remove any descendants of the removed nodes in |node_to_menu_map_|.
403 for (NodeToMenuMap::iterator
i(node_to_menu_map_
.begin());
404 i
!= node_to_menu_map_
.end(); ) {
405 bool ancestor_removed
= false;
406 for (std::vector
<const BookmarkNode
*>::const_iterator
j(bookmarks
.begin());
407 j
!= bookmarks
.end(); ++j
) {
408 if (i
->first
->HasAncestor(*j
)) {
409 ancestor_removed
= true;
413 if (ancestor_removed
) {
414 menu_id_to_node_map_
.erase(i
->second
->GetCommand());
415 node_to_menu_map_
.erase(i
++);
421 for (std::set
<MenuItemView
*>::const_iterator
i(changed_parent_menus
.begin());
422 i
!= changed_parent_menus
.end(); ++i
)
423 (*i
)->ChildrenChanged();
426 void BookmarkMenuDelegate::DidRemoveBookmarks() {
427 // Balances remove in WillRemoveBookmarksImpl.
428 GetBookmarkModel()->AddObserver(this);
429 DCHECK(is_mutating_model_
);
430 is_mutating_model_
= false;
433 MenuItemView
* BookmarkMenuDelegate::CreateMenu(const BookmarkNode
* parent
,
434 int start_child_index
,
435 ShowOptions show_options
) {
436 MenuItemView
* menu
= new MenuItemView(real_delegate_
);
437 menu
->SetCommand(next_menu_id_
++);
438 AddMenuToMaps(menu
, parent
);
439 menu
->set_has_icons(true);
440 bool show_permanent
= show_options
== SHOW_PERMANENT_FOLDERS
;
441 if (show_permanent
&& parent
== GetBookmarkModel()->bookmark_bar_node()) {
442 BuildMenuForManagedNode(menu
);
443 BuildMenuForSupervisedNode(menu
);
445 BuildMenu(parent
, start_child_index
, menu
);
447 BuildMenusForPermanentNodes(menu
);
451 void BookmarkMenuDelegate::BuildMenusForPermanentNodes(
452 views::MenuItemView
* menu
) {
453 BookmarkModel
* model
= GetBookmarkModel();
454 bool added_separator
= false;
455 BuildMenuForPermanentNode(model
->other_node(), IDR_BOOKMARK_BAR_FOLDER
, menu
,
457 BuildMenuForPermanentNode(model
->mobile_node(), IDR_BOOKMARK_BAR_FOLDER
, menu
,
461 void BookmarkMenuDelegate::BuildMenuForPermanentNode(
462 const BookmarkNode
* node
,
463 int icon_resource_id
,
465 bool* added_separator
) {
466 if (!node
->IsVisible() || node
->GetTotalNodeCount() == 1)
467 return; // No children, don't create a menu.
469 if (!*added_separator
) {
470 *added_separator
= true;
471 menu
->AppendSeparator();
474 ui::ResourceBundle
* rb
= &ui::ResourceBundle::GetSharedInstance();
475 gfx::ImageSkia
* folder_icon
= rb
->GetImageSkiaNamed(icon_resource_id
);
476 AddMenuToMaps(menu
->AppendSubMenuWithIcon(next_menu_id_
++, node
->GetTitle(),
481 void BookmarkMenuDelegate::BuildMenuForManagedNode(MenuItemView
* menu
) {
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
,
489 void BookmarkMenuDelegate::BuildMenuForSupervisedNode(MenuItemView
* menu
) {
490 // Don't add a separator for this menu.
491 bool added_separator
= true;
492 const BookmarkNode
* node
= GetChromeBookmarkClient()->supervised_node();
493 BuildMenuForPermanentNode(node
, IDR_BOOKMARK_BAR_FOLDER_SUPERVISED
, menu
,
497 void BookmarkMenuDelegate::BuildMenu(const BookmarkNode
* parent
,
498 int start_child_index
,
499 MenuItemView
* menu
) {
500 DCHECK(parent
->empty() || start_child_index
< parent
->child_count());
501 ui::ResourceBundle
* rb
= &ui::ResourceBundle::GetSharedInstance();
502 for (int i
= start_child_index
; i
< parent
->child_count(); ++i
) {
503 const BookmarkNode
* node
= parent
->GetChild(i
);
504 const int id
= next_menu_id_
++;
505 MenuItemView
* child_menu_item
;
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();
511 menu
->AppendMenuItemWithIcon(id
, node
->GetTitle(), *icon
);
513 DCHECK(node
->is_folder());
514 gfx::ImageSkia
* folder_icon
=
515 rb
->GetImageSkiaNamed(IDR_BOOKMARK_BAR_FOLDER
);
517 menu
->AppendSubMenuWithIcon(id
, node
->GetTitle(), *folder_icon
);
519 AddMenuToMaps(child_menu_item
, node
);
523 void BookmarkMenuDelegate::AddMenuToMaps(MenuItemView
* menu
,
524 const BookmarkNode
* node
) {
525 menu_id_to_node_map_
[menu
->GetCommand()] = node
;
526 node_to_menu_map_
[node
] = menu
;