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/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/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::BookmarkNodeData
;
35 using content::PageNavigator
;
36 using views::MenuItemView
;
38 // Max width of a menu. There does not appear to be an OS value for this, yet
39 // both IE and FF restrict the max width of a menu.
40 static const int kMaxMenuWidth
= 400;
42 BookmarkMenuDelegate::BookmarkMenuDelegate(Browser
* browser
,
43 PageNavigator
* navigator
,
44 views::Widget
* parent
,
48 profile_(browser
->profile()),
49 page_navigator_(navigator
),
52 parent_menu_item_(NULL
),
53 next_menu_id_(first_menu_id
),
54 min_menu_id_(first_menu_id
),
55 max_menu_id_(max_menu_id
),
57 is_mutating_model_(false),
58 location_(BOOKMARK_LAUNCH_LOCATION_NONE
) {}
60 BookmarkMenuDelegate::~BookmarkMenuDelegate() {
61 GetBookmarkModel()->RemoveObserver(this);
64 void BookmarkMenuDelegate::Init(views::MenuDelegate
* real_delegate
,
66 const BookmarkNode
* node
,
67 int start_child_index
,
68 ShowOptions show_options
,
69 BookmarkLaunchLocation location
) {
70 GetBookmarkModel()->AddObserver(this);
71 real_delegate_
= real_delegate
;
74 parent_menu_item_
= parent
;
76 // Add a separator if there are existing items in the menu, and if the
77 // current node has children. If |node| is the bookmark bar then the
78 // managed node is shown as its first child, if it's not empty.
79 BookmarkModel
* model
= GetBookmarkModel();
80 ChromeBookmarkClient
* client
= GetChromeBookmarkClient();
81 bool show_managed
= show_options
== SHOW_PERMANENT_FOLDERS
&&
82 node
== model
->bookmark_bar_node() &&
83 !client
->managed_node()->empty();
85 (start_child_index
< node
->child_count()) || show_managed
;
86 int initial_count
= parent
->GetSubmenu() ?
87 parent
->GetSubmenu()->GetMenuItemCount() : 0;
88 if (has_children
&& initial_count
> 0)
89 parent
->AppendSeparator();
91 BuildMenuForManagedNode(parent
, &next_menu_id_
);
92 BuildMenu(node
, start_child_index
, parent
, &next_menu_id_
);
93 if (show_options
== SHOW_PERMANENT_FOLDERS
)
94 BuildMenusForPermanentNodes(parent
, &next_menu_id_
);
96 menu_
= CreateMenu(node
, start_child_index
, show_options
);
100 void BookmarkMenuDelegate::SetPageNavigator(PageNavigator
* navigator
) {
101 page_navigator_
= navigator
;
102 if (context_menu_
.get())
103 context_menu_
->SetPageNavigator(navigator
);
106 BookmarkModel
* BookmarkMenuDelegate::GetBookmarkModel() {
107 return BookmarkModelFactory::GetForProfile(profile_
);
110 ChromeBookmarkClient
* BookmarkMenuDelegate::GetChromeBookmarkClient() {
111 return ChromeBookmarkClientFactory::GetForProfile(profile_
);
114 void BookmarkMenuDelegate::SetActiveMenu(const BookmarkNode
* node
,
116 DCHECK(!parent_menu_item_
);
117 if (!node_to_menu_map_
[node
])
118 CreateMenu(node
, start_index
, HIDE_PERMANENT_FOLDERS
);
119 menu_
= node_to_menu_map_
[node
];
122 base::string16
BookmarkMenuDelegate::GetTooltipText(
124 const gfx::Point
& screen_loc
) const {
125 MenuIDToNodeMap::const_iterator i
= menu_id_to_node_map_
.find(id
);
126 // When removing bookmarks it may be possible to end up here without a node.
127 if (i
== menu_id_to_node_map_
.end()) {
128 DCHECK(is_mutating_model_
);
129 return base::string16();
132 const BookmarkNode
* node
= i
->second
;
133 if (node
->is_url()) {
134 return BookmarkBarView::CreateToolTipForURLAndTitle(
135 parent_
, screen_loc
, node
->url(), node
->GetTitle(), profile_
);
137 return base::string16();
140 bool BookmarkMenuDelegate::IsTriggerableEvent(views::MenuItemView
* menu
,
141 const ui::Event
& e
) {
142 return e
.type() == ui::ET_GESTURE_TAP
||
143 e
.type() == ui::ET_GESTURE_TAP_DOWN
||
144 event_utils::IsPossibleDispositionEvent(e
);
147 void BookmarkMenuDelegate::ExecuteCommand(int id
, int mouse_event_flags
) {
148 DCHECK(menu_id_to_node_map_
.find(id
) != menu_id_to_node_map_
.end());
150 const BookmarkNode
* node
= menu_id_to_node_map_
[id
];
151 std::vector
<const BookmarkNode
*> selection
;
152 selection
.push_back(node
);
154 chrome::OpenAll(parent_
->GetNativeWindow(), page_navigator_
, selection
,
155 ui::DispositionFromEventFlags(mouse_event_flags
),
157 RecordBookmarkLaunch(node
, location_
);
160 bool BookmarkMenuDelegate::ShouldExecuteCommandWithoutClosingMenu(
162 const ui::Event
& event
) {
163 return (event
.flags() & ui::EF_LEFT_MOUSE_BUTTON
) &&
164 ui::DispositionFromEventFlags(event
.flags()) == NEW_BACKGROUND_TAB
;
167 bool BookmarkMenuDelegate::GetDropFormats(
170 std::set
<ui::OSExchangeData::CustomFormat
>* custom_formats
) {
171 *formats
= ui::OSExchangeData::URL
;
172 custom_formats
->insert(BookmarkNodeData::GetBookmarkCustomFormat());
176 bool BookmarkMenuDelegate::AreDropTypesRequired(MenuItemView
* menu
) {
180 bool BookmarkMenuDelegate::CanDrop(MenuItemView
* menu
,
181 const ui::OSExchangeData
& data
) {
182 // Only accept drops of 1 node, which is the case for all data dragged from
183 // bookmark bar and menus.
185 if (!drop_data_
.Read(data
) || drop_data_
.elements
.size() != 1 ||
186 !profile_
->GetPrefs()->GetBoolean(
187 bookmarks::prefs::kEditBookmarksEnabled
))
190 if (drop_data_
.has_single_url())
193 const BookmarkNode
* drag_node
=
194 drop_data_
.GetFirstNode(GetBookmarkModel(), profile_
->GetPath());
196 // Dragging a folder from another profile, always accept.
200 // Drag originated from same profile and is not a URL. Only accept it if
201 // the dragged node is not a parent of the node menu represents.
202 if (menu_id_to_node_map_
.find(menu
->GetCommand()) ==
203 menu_id_to_node_map_
.end()) {
204 // If we don't know the menu assume its because we're embedded. We'll
205 // figure out the real operation when GetDropOperation is invoked.
208 const BookmarkNode
* drop_node
= menu_id_to_node_map_
[menu
->GetCommand()];
210 while (drop_node
&& drop_node
!= drag_node
)
211 drop_node
= drop_node
->parent();
212 return (drop_node
== NULL
);
215 int BookmarkMenuDelegate::GetDropOperation(
217 const ui::DropTargetEvent
& event
,
218 views::MenuDelegate::DropPosition
* position
) {
219 // Should only get here if we have drop data.
220 DCHECK(drop_data_
.is_valid());
222 const BookmarkNode
* node
= menu_id_to_node_map_
[item
->GetCommand()];
223 const BookmarkNode
* drop_parent
= node
->parent();
224 int index_to_drop_at
= drop_parent
->GetIndexOf(node
);
225 BookmarkModel
* model
= GetBookmarkModel();
227 case views::MenuDelegate::DROP_AFTER
:
228 if (node
== model
->other_node() || node
== model
->mobile_node()) {
229 // Dropping after these nodes makes no sense.
230 *position
= views::MenuDelegate::DROP_NONE
;
235 case views::MenuDelegate::DROP_BEFORE
:
236 if (node
== model
->mobile_node()) {
237 // Dropping before this node makes no sense.
238 *position
= views::MenuDelegate::DROP_NONE
;
242 case views::MenuDelegate::DROP_ON
:
244 index_to_drop_at
= node
->child_count();
251 return chrome::GetBookmarkDropOperation(
252 profile_
, event
, drop_data_
, drop_parent
, index_to_drop_at
);
255 int BookmarkMenuDelegate::OnPerformDrop(
257 views::MenuDelegate::DropPosition position
,
258 const ui::DropTargetEvent
& event
) {
259 const BookmarkNode
* drop_node
= menu_id_to_node_map_
[menu
->GetCommand()];
261 BookmarkModel
* model
= GetBookmarkModel();
263 const BookmarkNode
* drop_parent
= drop_node
->parent();
265 int index_to_drop_at
= drop_parent
->GetIndexOf(drop_node
);
267 case views::MenuDelegate::DROP_AFTER
:
271 case views::MenuDelegate::DROP_ON
:
272 DCHECK(drop_node
->is_folder());
273 drop_parent
= drop_node
;
274 index_to_drop_at
= drop_node
->child_count();
277 case views::MenuDelegate::DROP_BEFORE
:
278 if (drop_node
== model
->other_node() ||
279 drop_node
== model
->mobile_node()) {
280 // This can happen with SHOW_PERMANENT_FOLDERS.
281 drop_parent
= model
->bookmark_bar_node();
282 index_to_drop_at
= drop_parent
->child_count();
290 bool copy
= event
.source_operations() == ui::DragDropTypes::DRAG_COPY
;
291 return chrome::DropBookmarks(profile_
, drop_data_
,
292 drop_parent
, index_to_drop_at
, copy
);
295 bool BookmarkMenuDelegate::ShowContextMenu(MenuItemView
* source
,
298 ui::MenuSourceType source_type
) {
299 DCHECK(menu_id_to_node_map_
.find(id
) != menu_id_to_node_map_
.end());
300 std::vector
<const BookmarkNode
*> nodes
;
301 nodes
.push_back(menu_id_to_node_map_
[id
]);
302 bool close_on_delete
= !parent_menu_item_
&&
303 (nodes
[0]->parent() == GetBookmarkModel()->other_node() &&
304 nodes
[0]->parent()->child_count() == 1);
306 new BookmarkContextMenu(
314 context_menu_
->set_observer(this);
315 context_menu_
->RunMenuAt(p
, source_type
);
316 context_menu_
.reset(NULL
);
320 bool BookmarkMenuDelegate::CanDrag(MenuItemView
* menu
) {
321 const BookmarkNode
* node
= menu_id_to_node_map_
[menu
->GetCommand()];
322 // Don't let users drag the other folder.
323 return node
->parent() != GetBookmarkModel()->root_node();
326 void BookmarkMenuDelegate::WriteDragData(MenuItemView
* sender
,
327 ui::OSExchangeData
* data
) {
328 DCHECK(sender
&& data
);
330 content::RecordAction(UserMetricsAction("BookmarkBar_DragFromFolder"));
332 BookmarkNodeData
drag_data(menu_id_to_node_map_
[sender
->GetCommand()]);
333 drag_data
.Write(profile_
->GetPath(), data
);
336 int BookmarkMenuDelegate::GetDragOperations(MenuItemView
* sender
) {
337 return chrome::GetBookmarkDragOperation(
338 profile_
, menu_id_to_node_map_
[sender
->GetCommand()]);
341 int BookmarkMenuDelegate::GetMaxWidthForMenu(MenuItemView
* menu
) {
342 return kMaxMenuWidth
;
345 void BookmarkMenuDelegate::BookmarkModelChanged() {
348 void BookmarkMenuDelegate::BookmarkNodeFaviconChanged(
349 BookmarkModel
* model
,
350 const BookmarkNode
* node
) {
351 NodeToMenuMap::iterator menu_pair
= node_to_menu_map_
.find(node
);
352 if (menu_pair
== node_to_menu_map_
.end())
353 return; // We're not showing a menu item for the node.
355 menu_pair
->second
->SetIcon(model
->GetFavicon(node
).AsImageSkia());
358 void BookmarkMenuDelegate::WillRemoveBookmarks(
359 const std::vector
<const BookmarkNode
*>& bookmarks
) {
360 DCHECK(!is_mutating_model_
);
361 is_mutating_model_
= true; // Set to false in DidRemoveBookmarks().
363 // Remove the observer so that when the remove happens we don't prematurely
364 // cancel the menu. The observer is added back in DidRemoveBookmarks().
365 GetBookmarkModel()->RemoveObserver(this);
367 // Remove the menu items.
368 std::set
<MenuItemView
*> changed_parent_menus
;
369 for (std::vector
<const BookmarkNode
*>::const_iterator
i(bookmarks
.begin());
370 i
!= bookmarks
.end(); ++i
) {
371 NodeToMenuMap::iterator node_to_menu
= node_to_menu_map_
.find(*i
);
372 if (node_to_menu
!= node_to_menu_map_
.end()) {
373 MenuItemView
* menu
= node_to_menu
->second
;
374 MenuItemView
* parent
= menu
->GetParentMenuItem();
375 // |parent| is NULL when removing a root. This happens when right clicking
376 // to delete an empty folder.
378 changed_parent_menus
.insert(parent
);
379 parent
->RemoveMenuItemAt(menu
->parent()->GetIndexOf(menu
));
381 node_to_menu_map_
.erase(node_to_menu
);
382 menu_id_to_node_map_
.erase(menu
->GetCommand());
386 // All the bookmarks in |bookmarks| should have the same parent. It's possible
387 // to support different parents, but this would need to prune any nodes whose
388 // parent has been removed. As all nodes currently have the same parent, there
390 DCHECK(changed_parent_menus
.size() <= 1);
392 // Remove any descendants of the removed nodes in |node_to_menu_map_|.
393 for (NodeToMenuMap::iterator
i(node_to_menu_map_
.begin());
394 i
!= node_to_menu_map_
.end(); ) {
395 bool ancestor_removed
= false;
396 for (std::vector
<const BookmarkNode
*>::const_iterator
j(bookmarks
.begin());
397 j
!= bookmarks
.end(); ++j
) {
398 if (i
->first
->HasAncestor(*j
)) {
399 ancestor_removed
= true;
403 if (ancestor_removed
) {
404 menu_id_to_node_map_
.erase(i
->second
->GetCommand());
405 node_to_menu_map_
.erase(i
++);
411 for (std::set
<MenuItemView
*>::const_iterator
i(changed_parent_menus
.begin());
412 i
!= changed_parent_menus
.end(); ++i
)
413 (*i
)->ChildrenChanged();
416 void BookmarkMenuDelegate::DidRemoveBookmarks() {
417 // Balances remove in WillRemoveBookmarksImpl.
418 GetBookmarkModel()->AddObserver(this);
419 DCHECK(is_mutating_model_
);
420 is_mutating_model_
= false;
423 MenuItemView
* BookmarkMenuDelegate::CreateMenu(const BookmarkNode
* parent
,
424 int start_child_index
,
425 ShowOptions show_options
) {
426 MenuItemView
* menu
= new MenuItemView(real_delegate_
);
427 menu
->SetCommand(next_menu_id_
++);
428 menu_id_to_node_map_
[menu
->GetCommand()] = parent
;
429 menu
->set_has_icons(true);
430 bool show_permanent
= show_options
== SHOW_PERMANENT_FOLDERS
;
431 if (show_permanent
&& parent
== GetBookmarkModel()->bookmark_bar_node())
432 BuildMenuForManagedNode(menu
, &next_menu_id_
);
433 BuildMenu(parent
, start_child_index
, menu
, &next_menu_id_
);
435 BuildMenusForPermanentNodes(menu
, &next_menu_id_
);
439 void BookmarkMenuDelegate::BuildMenusForPermanentNodes(
440 views::MenuItemView
* menu
,
442 BookmarkModel
* model
= GetBookmarkModel();
443 bool added_separator
= false;
444 BuildMenuForPermanentNode(model
->other_node(), IDR_BOOKMARK_BAR_FOLDER
, menu
,
445 next_menu_id
, &added_separator
);
446 BuildMenuForPermanentNode(model
->mobile_node(), IDR_BOOKMARK_BAR_FOLDER
, menu
,
447 next_menu_id
, &added_separator
);
450 void BookmarkMenuDelegate::BuildMenuForPermanentNode(
451 const BookmarkNode
* node
,
452 int icon_resource_id
,
455 bool* added_separator
) {
456 if (!node
->IsVisible() || node
->GetTotalNodeCount() == 1)
457 return; // No children, don't create a menu.
459 int id
= *next_menu_id
;
460 // Don't create the submenu if its menu ID will be outside the range allowed.
461 if (IsOutsideMenuIdRange(id
))
465 if (!*added_separator
) {
466 *added_separator
= true;
467 menu
->AppendSeparator();
470 ui::ResourceBundle
* rb
= &ui::ResourceBundle::GetSharedInstance();
471 gfx::ImageSkia
* folder_icon
= rb
->GetImageSkiaNamed(icon_resource_id
);
472 MenuItemView
* submenu
= menu
->AppendSubMenuWithIcon(
473 id
, node
->GetTitle(), *folder_icon
);
474 BuildMenu(node
, 0, submenu
, next_menu_id
);
475 menu_id_to_node_map_
[id
] = node
;
478 void BookmarkMenuDelegate::BuildMenuForManagedNode(
481 // Don't add a separator for this menu.
482 bool added_separator
= true;
483 const BookmarkNode
* node
= GetChromeBookmarkClient()->managed_node();
484 BuildMenuForPermanentNode(node
, IDR_BOOKMARK_BAR_FOLDER_MANAGED
, menu
,
485 next_menu_id
, &added_separator
);
488 void BookmarkMenuDelegate::BuildMenu(const BookmarkNode
* parent
,
489 int start_child_index
,
492 node_to_menu_map_
[parent
] = menu
;
493 DCHECK(parent
->empty() || start_child_index
< parent
->child_count());
494 ui::ResourceBundle
* rb
= &ui::ResourceBundle::GetSharedInstance();
495 for (int i
= start_child_index
; i
< parent
->child_count(); ++i
) {
496 const BookmarkNode
* node
= parent
->GetChild(i
);
497 const int id
= *next_menu_id
;
498 // Don't create the item if its menu ID will be outside the range allowed.
499 if (IsOutsideMenuIdRange(id
))
504 menu_id_to_node_map_
[id
] = node
;
505 if (node
->is_url()) {
506 const gfx::Image
& image
= GetBookmarkModel()->GetFavicon(node
);
507 const gfx::ImageSkia
* icon
= image
.IsEmpty() ?
508 rb
->GetImageSkiaNamed(IDR_DEFAULT_FAVICON
) : image
.ToImageSkia();
509 node_to_menu_map_
[node
] =
510 menu
->AppendMenuItemWithIcon(id
, node
->GetTitle(), *icon
);
511 } else if (node
->is_folder()) {
512 gfx::ImageSkia
* folder_icon
=
513 rb
->GetImageSkiaNamed(IDR_BOOKMARK_BAR_FOLDER
);
514 MenuItemView
* submenu
= menu
->AppendSubMenuWithIcon(
515 id
, node
->GetTitle(), *folder_icon
);
516 BuildMenu(node
, 0, submenu
, next_menu_id
);
523 bool BookmarkMenuDelegate::IsOutsideMenuIdRange(int menu_id
) const {
524 return menu_id
< min_menu_id_
|| menu_id
> max_menu_id_
;