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/managed_bookmark_service_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/event_utils.h"
18 #include "chrome/common/pref_names.h"
19 #include "components/bookmarks/browser/bookmark_model.h"
20 #include "components/bookmarks/managed/managed_bookmark_service.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 bookmarks::ManagedBookmarkService
* managed
= GetManagedBookmarkService();
80 bool show_forced_folders
= show_options
== SHOW_PERMANENT_FOLDERS
&&
81 node
== model
->bookmark_bar_node();
83 show_forced_folders
&& !managed
->managed_node()->empty();
84 bool show_supervised
=
85 show_forced_folders
&& !managed
->supervised_node()->empty();
86 bool has_children
= (start_child_index
< node
->child_count()) ||
87 show_managed
|| show_supervised
;
88 int initial_count
= parent
->GetSubmenu() ?
89 parent
->GetSubmenu()->GetMenuItemCount() : 0;
90 if (has_children
&& initial_count
> 0)
91 parent
->AppendSeparator();
93 BuildMenuForManagedNode(parent
);
95 BuildMenuForSupervisedNode(parent
);
96 BuildMenu(node
, start_child_index
, parent
);
97 if (show_options
== SHOW_PERMANENT_FOLDERS
)
98 BuildMenusForPermanentNodes(parent
);
100 menu_
= CreateMenu(node
, start_child_index
, show_options
);
104 void BookmarkMenuDelegate::SetPageNavigator(PageNavigator
* navigator
) {
105 page_navigator_
= navigator
;
106 if (context_menu_
.get())
107 context_menu_
->SetPageNavigator(navigator
);
110 BookmarkModel
* BookmarkMenuDelegate::GetBookmarkModel() {
111 return BookmarkModelFactory::GetForProfile(profile_
);
114 bookmarks::ManagedBookmarkService
*
115 BookmarkMenuDelegate::GetManagedBookmarkService() {
116 return ManagedBookmarkServiceFactory::GetForProfile(profile_
);
119 void BookmarkMenuDelegate::SetActiveMenu(const BookmarkNode
* node
,
121 DCHECK(!parent_menu_item_
);
122 if (!node_to_menu_map_
[node
])
123 CreateMenu(node
, start_index
, HIDE_PERMANENT_FOLDERS
);
124 menu_
= node_to_menu_map_
[node
];
127 base::string16
BookmarkMenuDelegate::GetTooltipText(
129 const gfx::Point
& screen_loc
) const {
130 MenuIDToNodeMap::const_iterator i
= menu_id_to_node_map_
.find(id
);
131 // When removing bookmarks it may be possible to end up here without a node.
132 if (i
== menu_id_to_node_map_
.end()) {
133 DCHECK(is_mutating_model_
);
134 return base::string16();
137 const BookmarkNode
* node
= i
->second
;
138 if (node
->is_url()) {
139 return BookmarkBarView::CreateToolTipForURLAndTitle(
140 parent_
, screen_loc
, node
->url(), node
->GetTitle(), profile_
);
142 return base::string16();
145 bool BookmarkMenuDelegate::IsTriggerableEvent(views::MenuItemView
* menu
,
146 const ui::Event
& e
) {
147 return e
.type() == ui::ET_GESTURE_TAP
||
148 e
.type() == ui::ET_GESTURE_TAP_DOWN
||
149 event_utils::IsPossibleDispositionEvent(e
);
152 void BookmarkMenuDelegate::ExecuteCommand(int id
, int mouse_event_flags
) {
153 DCHECK(menu_id_to_node_map_
.find(id
) != menu_id_to_node_map_
.end());
155 const BookmarkNode
* node
= menu_id_to_node_map_
[id
];
156 std::vector
<const BookmarkNode
*> selection
;
157 selection
.push_back(node
);
159 chrome::OpenAll(parent_
->GetNativeWindow(), page_navigator_
, selection
,
160 ui::DispositionFromEventFlags(mouse_event_flags
),
162 RecordBookmarkLaunch(node
, location_
);
165 bool BookmarkMenuDelegate::ShouldExecuteCommandWithoutClosingMenu(
167 const ui::Event
& event
) {
168 return (event
.flags() & ui::EF_LEFT_MOUSE_BUTTON
) &&
169 ui::DispositionFromEventFlags(event
.flags()) == NEW_BACKGROUND_TAB
;
172 bool BookmarkMenuDelegate::GetDropFormats(
175 std::set
<ui::OSExchangeData::CustomFormat
>* custom_formats
) {
176 *formats
= ui::OSExchangeData::URL
;
177 custom_formats
->insert(BookmarkNodeData::GetBookmarkFormatType());
181 bool BookmarkMenuDelegate::AreDropTypesRequired(MenuItemView
* menu
) {
185 bool BookmarkMenuDelegate::CanDrop(MenuItemView
* menu
,
186 const ui::OSExchangeData
& data
) {
187 // Only accept drops of 1 node, which is the case for all data dragged from
188 // bookmark bar and menus.
190 if (!drop_data_
.Read(data
) || drop_data_
.size() != 1 ||
191 !profile_
->GetPrefs()->GetBoolean(
192 bookmarks::prefs::kEditBookmarksEnabled
))
195 if (drop_data_
.has_single_url())
198 const BookmarkNode
* drag_node
=
199 drop_data_
.GetFirstNode(GetBookmarkModel(), profile_
->GetPath());
201 // Dragging a folder from another profile, always accept.
205 // Drag originated from same profile and is not a URL. Only accept it if
206 // the dragged node is not a parent of the node menu represents.
207 if (menu_id_to_node_map_
.find(menu
->GetCommand()) ==
208 menu_id_to_node_map_
.end()) {
209 // If we don't know the menu assume its because we're embedded. We'll
210 // figure out the real operation when GetDropOperation is invoked.
213 const BookmarkNode
* drop_node
= menu_id_to_node_map_
[menu
->GetCommand()];
215 while (drop_node
&& drop_node
!= drag_node
)
216 drop_node
= drop_node
->parent();
217 return (drop_node
== NULL
);
220 int BookmarkMenuDelegate::GetDropOperation(
222 const ui::DropTargetEvent
& event
,
223 views::MenuDelegate::DropPosition
* position
) {
224 // Should only get here if we have drop data.
225 DCHECK(drop_data_
.is_valid());
227 const BookmarkNode
* node
= menu_id_to_node_map_
[item
->GetCommand()];
228 const BookmarkNode
* drop_parent
= node
->parent();
229 int index_to_drop_at
= drop_parent
->GetIndexOf(node
);
230 BookmarkModel
* model
= GetBookmarkModel();
232 case views::MenuDelegate::DROP_AFTER
:
233 if (node
== model
->other_node() || node
== model
->mobile_node()) {
234 // Dropping after these nodes makes no sense.
235 *position
= views::MenuDelegate::DROP_NONE
;
240 case views::MenuDelegate::DROP_BEFORE
:
241 if (node
== model
->mobile_node()) {
242 // Dropping before this node makes no sense.
243 *position
= views::MenuDelegate::DROP_NONE
;
247 case views::MenuDelegate::DROP_ON
:
249 index_to_drop_at
= node
->child_count();
256 return chrome::GetBookmarkDropOperation(
257 profile_
, event
, drop_data_
, drop_parent
, index_to_drop_at
);
260 int BookmarkMenuDelegate::OnPerformDrop(
262 views::MenuDelegate::DropPosition position
,
263 const ui::DropTargetEvent
& event
) {
264 const BookmarkNode
* drop_node
= menu_id_to_node_map_
[menu
->GetCommand()];
266 BookmarkModel
* model
= GetBookmarkModel();
268 const BookmarkNode
* drop_parent
= drop_node
->parent();
270 int index_to_drop_at
= drop_parent
->GetIndexOf(drop_node
);
272 case views::MenuDelegate::DROP_AFTER
:
276 case views::MenuDelegate::DROP_ON
:
277 DCHECK(drop_node
->is_folder());
278 drop_parent
= drop_node
;
279 index_to_drop_at
= drop_node
->child_count();
282 case views::MenuDelegate::DROP_BEFORE
:
283 if (drop_node
== model
->other_node() ||
284 drop_node
== model
->mobile_node()) {
285 // This can happen with SHOW_PERMANENT_FOLDERS.
286 drop_parent
= model
->bookmark_bar_node();
287 index_to_drop_at
= drop_parent
->child_count();
295 bool copy
= event
.source_operations() == ui::DragDropTypes::DRAG_COPY
;
296 return chrome::DropBookmarks(profile_
, drop_data_
,
297 drop_parent
, index_to_drop_at
, copy
);
300 bool BookmarkMenuDelegate::ShowContextMenu(MenuItemView
* source
,
303 ui::MenuSourceType source_type
) {
304 DCHECK(menu_id_to_node_map_
.find(id
) != menu_id_to_node_map_
.end());
305 std::vector
<const BookmarkNode
*> nodes
;
306 nodes
.push_back(menu_id_to_node_map_
[id
]);
307 bool close_on_delete
= !parent_menu_item_
&&
308 (nodes
[0]->parent() == GetBookmarkModel()->other_node() &&
309 nodes
[0]->parent()->child_count() == 1);
311 new BookmarkContextMenu(
319 context_menu_
->set_observer(this);
320 context_menu_
->RunMenuAt(p
, source_type
);
321 context_menu_
.reset(NULL
);
325 bool BookmarkMenuDelegate::CanDrag(MenuItemView
* menu
) {
326 const BookmarkNode
* node
= menu_id_to_node_map_
[menu
->GetCommand()];
327 // Don't let users drag the other folder.
328 return node
->parent() != GetBookmarkModel()->root_node();
331 void BookmarkMenuDelegate::WriteDragData(MenuItemView
* sender
,
332 ui::OSExchangeData
* data
) {
333 DCHECK(sender
&& data
);
335 content::RecordAction(UserMetricsAction("BookmarkBar_DragFromFolder"));
337 BookmarkNodeData
drag_data(menu_id_to_node_map_
[sender
->GetCommand()]);
338 drag_data
.Write(profile_
->GetPath(), data
);
341 int BookmarkMenuDelegate::GetDragOperations(MenuItemView
* sender
) {
342 return chrome::GetBookmarkDragOperation(
343 profile_
, menu_id_to_node_map_
[sender
->GetCommand()]);
346 int BookmarkMenuDelegate::GetMaxWidthForMenu(MenuItemView
* menu
) {
347 return kMaxMenuWidth
;
350 void BookmarkMenuDelegate::WillShowMenu(MenuItemView
* menu
) {
351 auto iter
= menu_id_to_node_map_
.find(menu
->GetCommand());
352 if ((iter
!= menu_id_to_node_map_
.end()) && iter
->second
->child_count() &&
353 !menu
->GetSubmenu()->GetMenuItemCount())
354 BuildMenu(iter
->second
, 0, menu
);
357 void BookmarkMenuDelegate::BookmarkModelChanged() {
360 void BookmarkMenuDelegate::BookmarkNodeFaviconChanged(
361 BookmarkModel
* model
,
362 const BookmarkNode
* node
) {
363 NodeToMenuMap::iterator menu_pair
= node_to_menu_map_
.find(node
);
364 if (menu_pair
== node_to_menu_map_
.end())
365 return; // We're not showing a menu item for the node.
367 menu_pair
->second
->SetIcon(model
->GetFavicon(node
).AsImageSkia());
370 void BookmarkMenuDelegate::WillRemoveBookmarks(
371 const std::vector
<const BookmarkNode
*>& bookmarks
) {
372 DCHECK(!is_mutating_model_
);
373 is_mutating_model_
= true; // Set to false in DidRemoveBookmarks().
375 // Remove the observer so that when the remove happens we don't prematurely
376 // cancel the menu. The observer is added back in DidRemoveBookmarks().
377 GetBookmarkModel()->RemoveObserver(this);
379 // Remove the menu items.
380 std::set
<MenuItemView
*> changed_parent_menus
;
381 for (std::vector
<const BookmarkNode
*>::const_iterator
i(bookmarks
.begin());
382 i
!= bookmarks
.end(); ++i
) {
383 NodeToMenuMap::iterator node_to_menu
= node_to_menu_map_
.find(*i
);
384 if (node_to_menu
!= node_to_menu_map_
.end()) {
385 MenuItemView
* menu
= node_to_menu
->second
;
386 MenuItemView
* parent
= menu
->GetParentMenuItem();
387 // |parent| is NULL when removing a root. This happens when right clicking
388 // to delete an empty folder.
390 changed_parent_menus
.insert(parent
);
391 parent
->RemoveMenuItemAt(menu
->parent()->GetIndexOf(menu
));
393 node_to_menu_map_
.erase(node_to_menu
);
394 menu_id_to_node_map_
.erase(menu
->GetCommand());
398 // All the bookmarks in |bookmarks| should have the same parent. It's possible
399 // to support different parents, but this would need to prune any nodes whose
400 // parent has been removed. As all nodes currently have the same parent, there
402 DCHECK(changed_parent_menus
.size() <= 1);
404 // Remove any descendants of the removed nodes in |node_to_menu_map_|.
405 for (NodeToMenuMap::iterator
i(node_to_menu_map_
.begin());
406 i
!= node_to_menu_map_
.end(); ) {
407 bool ancestor_removed
= false;
408 for (std::vector
<const BookmarkNode
*>::const_iterator
j(bookmarks
.begin());
409 j
!= bookmarks
.end(); ++j
) {
410 if (i
->first
->HasAncestor(*j
)) {
411 ancestor_removed
= true;
415 if (ancestor_removed
) {
416 menu_id_to_node_map_
.erase(i
->second
->GetCommand());
417 node_to_menu_map_
.erase(i
++);
423 for (std::set
<MenuItemView
*>::const_iterator
i(changed_parent_menus
.begin());
424 i
!= changed_parent_menus
.end(); ++i
)
425 (*i
)->ChildrenChanged();
428 void BookmarkMenuDelegate::DidRemoveBookmarks() {
429 // Balances remove in WillRemoveBookmarksImpl.
430 GetBookmarkModel()->AddObserver(this);
431 DCHECK(is_mutating_model_
);
432 is_mutating_model_
= false;
435 MenuItemView
* BookmarkMenuDelegate::CreateMenu(const BookmarkNode
* parent
,
436 int start_child_index
,
437 ShowOptions show_options
) {
438 MenuItemView
* menu
= new MenuItemView(real_delegate_
);
439 menu
->SetCommand(next_menu_id_
++);
440 AddMenuToMaps(menu
, parent
);
441 menu
->set_has_icons(true);
442 bool show_permanent
= show_options
== SHOW_PERMANENT_FOLDERS
;
443 if (show_permanent
&& parent
== GetBookmarkModel()->bookmark_bar_node()) {
444 BuildMenuForManagedNode(menu
);
445 BuildMenuForSupervisedNode(menu
);
447 BuildMenu(parent
, start_child_index
, menu
);
449 BuildMenusForPermanentNodes(menu
);
453 void BookmarkMenuDelegate::BuildMenusForPermanentNodes(
454 views::MenuItemView
* menu
) {
455 BookmarkModel
* model
= GetBookmarkModel();
456 bool added_separator
= false;
457 BuildMenuForPermanentNode(model
->other_node(),
458 chrome::GetBookmarkFolderIcon(), menu
,
460 BuildMenuForPermanentNode(model
->mobile_node(),
461 chrome::GetBookmarkFolderIcon(), menu
,
465 void BookmarkMenuDelegate::BuildMenuForPermanentNode(const BookmarkNode
* node
,
466 const gfx::ImageSkia
& icon
,
468 bool* added_separator
) {
469 if (!node
->IsVisible() || node
->GetTotalNodeCount() == 1)
470 return; // No children, don't create a menu.
472 if (!*added_separator
) {
473 *added_separator
= true;
474 menu
->AppendSeparator();
478 menu
->AppendSubMenuWithIcon(next_menu_id_
++, node
->GetTitle(), icon
),
482 void BookmarkMenuDelegate::BuildMenuForManagedNode(MenuItemView
* menu
) {
483 // Don't add a separator for this menu.
484 bool added_separator
= true;
485 const BookmarkNode
* node
= GetManagedBookmarkService()->managed_node();
486 BuildMenuForPermanentNode(node
, chrome::GetBookmarkManagedFolderIcon(), menu
,
490 void BookmarkMenuDelegate::BuildMenuForSupervisedNode(MenuItemView
* menu
) {
491 // Don't add a separator for this menu.
492 bool added_separator
= true;
493 const BookmarkNode
* node
= GetManagedBookmarkService()->supervised_node();
494 BuildMenuForPermanentNode(node
, chrome::GetBookmarkSupervisedFolderIcon(),
495 menu
, &added_separator
);
498 void BookmarkMenuDelegate::BuildMenu(const BookmarkNode
* parent
,
499 int start_child_index
,
500 MenuItemView
* menu
) {
501 DCHECK(parent
->empty() || start_child_index
< parent
->child_count());
502 ui::ResourceBundle
* rb
= &ui::ResourceBundle::GetSharedInstance();
503 for (int i
= start_child_index
; i
< parent
->child_count(); ++i
) {
504 const BookmarkNode
* node
= parent
->GetChild(i
);
505 const int id
= next_menu_id_
++;
506 MenuItemView
* child_menu_item
;
507 if (node
->is_url()) {
508 const gfx::Image
& image
= GetBookmarkModel()->GetFavicon(node
);
509 const gfx::ImageSkia
* icon
= image
.IsEmpty() ?
510 rb
->GetImageSkiaNamed(IDR_DEFAULT_FAVICON
) : image
.ToImageSkia();
512 menu
->AppendMenuItemWithIcon(id
, node
->GetTitle(), *icon
);
514 DCHECK(node
->is_folder());
515 child_menu_item
= menu
->AppendSubMenuWithIcon(
516 id
, node
->GetTitle(), chrome::GetBookmarkFolderIcon());
518 AddMenuToMaps(child_menu_item
, node
);
522 void BookmarkMenuDelegate::AddMenuToMaps(MenuItemView
* menu
,
523 const BookmarkNode
* node
) {
524 menu_id_to_node_map_
[menu
->GetCommand()] = node
;
525 node_to_menu_map_
[node
] = menu
;