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.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"
33 using base::UserMetricsAction
;
34 using content::PageNavigator
;
35 using views::MenuItemView
;
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.
39 static const int kMaxMenuWidth
= 400;
41 BookmarkMenuDelegate::BookmarkMenuDelegate(Browser
* browser
,
42 PageNavigator
* navigator
,
43 views::Widget
* parent
,
47 profile_(browser
->profile()),
48 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
;
75 int initial_count
= parent
->GetSubmenu() ?
76 parent
->GetSubmenu()->GetMenuItemCount() : 0;
77 if ((start_child_index
< node
->child_count()) &&
78 (initial_count
> 0)) {
79 parent
->AppendSeparator();
81 BuildMenu(node
, start_child_index
, parent
, &next_menu_id_
);
82 if (show_options
== SHOW_PERMANENT_FOLDERS
)
83 BuildMenusForPermanentNodes(parent
, &next_menu_id_
);
85 menu_
= CreateMenu(node
, start_child_index
, show_options
);
89 void BookmarkMenuDelegate::SetPageNavigator(PageNavigator
* navigator
) {
90 page_navigator_
= navigator
;
91 if (context_menu_
.get())
92 context_menu_
->SetPageNavigator(navigator
);
95 BookmarkModel
* BookmarkMenuDelegate::GetBookmarkModel() {
96 return BookmarkModelFactory::GetForProfile(profile_
);
99 void BookmarkMenuDelegate::SetActiveMenu(const BookmarkNode
* node
,
101 DCHECK(!parent_menu_item_
);
102 if (!node_to_menu_map_
[node
])
103 CreateMenu(node
, start_index
, HIDE_PERMANENT_FOLDERS
);
104 menu_
= node_to_menu_map_
[node
];
107 base::string16
BookmarkMenuDelegate::GetTooltipText(
109 const gfx::Point
& screen_loc
) const {
110 MenuIDToNodeMap::const_iterator i
= menu_id_to_node_map_
.find(id
);
111 // When removing bookmarks it may be possible to end up here without a node.
112 if (i
== menu_id_to_node_map_
.end()) {
113 DCHECK(is_mutating_model_
);
114 return base::string16();
117 const BookmarkNode
* node
= i
->second
;
118 if (node
->is_url()) {
119 return BookmarkBarView::CreateToolTipForURLAndTitle(
120 parent_
, screen_loc
, node
->url(), node
->GetTitle(), profile_
);
122 return base::string16();
125 bool BookmarkMenuDelegate::IsTriggerableEvent(views::MenuItemView
* menu
,
126 const ui::Event
& e
) {
127 return e
.type() == ui::ET_GESTURE_TAP
||
128 e
.type() == ui::ET_GESTURE_TAP_DOWN
||
129 event_utils::IsPossibleDispositionEvent(e
);
132 void BookmarkMenuDelegate::ExecuteCommand(int id
, int mouse_event_flags
) {
133 DCHECK(menu_id_to_node_map_
.find(id
) != menu_id_to_node_map_
.end());
135 const BookmarkNode
* node
= menu_id_to_node_map_
[id
];
136 std::vector
<const BookmarkNode
*> selection
;
137 selection
.push_back(node
);
139 chrome::OpenAll(parent_
->GetNativeWindow(), page_navigator_
, selection
,
140 ui::DispositionFromEventFlags(mouse_event_flags
),
142 RecordBookmarkLaunch(node
, location_
);
145 bool BookmarkMenuDelegate::ShouldExecuteCommandWithoutClosingMenu(
147 const ui::Event
& event
) {
148 return (event
.flags() & ui::EF_LEFT_MOUSE_BUTTON
) &&
149 ui::DispositionFromEventFlags(event
.flags()) == NEW_BACKGROUND_TAB
;
152 bool BookmarkMenuDelegate::GetDropFormats(
155 std::set
<ui::OSExchangeData::CustomFormat
>* custom_formats
) {
156 *formats
= ui::OSExchangeData::URL
;
157 custom_formats
->insert(BookmarkNodeData::GetBookmarkCustomFormat());
161 bool BookmarkMenuDelegate::AreDropTypesRequired(MenuItemView
* menu
) {
165 bool BookmarkMenuDelegate::CanDrop(MenuItemView
* menu
,
166 const ui::OSExchangeData
& data
) {
167 // Only accept drops of 1 node, which is the case for all data dragged from
168 // bookmark bar and menus.
170 if (!drop_data_
.Read(data
) || drop_data_
.elements
.size() != 1 ||
171 !profile_
->GetPrefs()->GetBoolean(prefs::kEditBookmarksEnabled
))
174 if (drop_data_
.has_single_url())
177 const BookmarkNode
* drag_node
= drop_data_
.GetFirstNode(profile_
);
179 // Dragging a folder from another profile, always accept.
183 // Drag originated from same profile and is not a URL. Only accept it if
184 // the dragged node is not a parent of the node menu represents.
185 if (menu_id_to_node_map_
.find(menu
->GetCommand()) ==
186 menu_id_to_node_map_
.end()) {
187 // If we don't know the menu assume its because we're embedded. We'll
188 // figure out the real operation when GetDropOperation is invoked.
191 const BookmarkNode
* drop_node
= menu_id_to_node_map_
[menu
->GetCommand()];
193 while (drop_node
&& drop_node
!= drag_node
)
194 drop_node
= drop_node
->parent();
195 return (drop_node
== NULL
);
198 int BookmarkMenuDelegate::GetDropOperation(
200 const ui::DropTargetEvent
& event
,
201 views::MenuDelegate::DropPosition
* position
) {
202 // Should only get here if we have drop data.
203 DCHECK(drop_data_
.is_valid());
205 const BookmarkNode
* node
= menu_id_to_node_map_
[item
->GetCommand()];
206 const BookmarkNode
* drop_parent
= node
->parent();
207 int index_to_drop_at
= drop_parent
->GetIndexOf(node
);
208 BookmarkModel
* model
= GetBookmarkModel();
210 case views::MenuDelegate::DROP_AFTER
:
211 if (node
== model
->other_node() || node
== model
->mobile_node()) {
212 // Dropping after these nodes makes no sense.
213 *position
= views::MenuDelegate::DROP_NONE
;
218 case views::MenuDelegate::DROP_BEFORE
:
219 if (node
== model
->mobile_node()) {
220 // Dropping before this node makes no sense.
221 *position
= views::MenuDelegate::DROP_NONE
;
225 case views::MenuDelegate::DROP_ON
:
227 index_to_drop_at
= node
->child_count();
234 return chrome::GetBookmarkDropOperation(profile_
, event
, drop_data_
,
235 drop_parent
, index_to_drop_at
);
238 int BookmarkMenuDelegate::OnPerformDrop(
240 views::MenuDelegate::DropPosition position
,
241 const ui::DropTargetEvent
& event
) {
242 const BookmarkNode
* drop_node
= menu_id_to_node_map_
[menu
->GetCommand()];
244 BookmarkModel
* model
= GetBookmarkModel();
246 const BookmarkNode
* drop_parent
= drop_node
->parent();
248 int index_to_drop_at
= drop_parent
->GetIndexOf(drop_node
);
250 case views::MenuDelegate::DROP_AFTER
:
254 case views::MenuDelegate::DROP_ON
:
255 DCHECK(drop_node
->is_folder());
256 drop_parent
= drop_node
;
257 index_to_drop_at
= drop_node
->child_count();
260 case views::MenuDelegate::DROP_BEFORE
:
261 if (drop_node
== model
->other_node() ||
262 drop_node
== model
->mobile_node()) {
263 // This can happen with SHOW_PERMANENT_FOLDERS.
264 drop_parent
= model
->bookmark_bar_node();
265 index_to_drop_at
= drop_parent
->child_count();
273 return chrome::DropBookmarks(profile_
, drop_data_
,
274 drop_parent
, index_to_drop_at
);
277 bool BookmarkMenuDelegate::ShowContextMenu(MenuItemView
* source
,
280 ui::MenuSourceType source_type
) {
281 DCHECK(menu_id_to_node_map_
.find(id
) != menu_id_to_node_map_
.end());
282 std::vector
<const BookmarkNode
*> nodes
;
283 nodes
.push_back(menu_id_to_node_map_
[id
]);
284 bool close_on_delete
= !parent_menu_item_
&&
285 (nodes
[0]->parent() == GetBookmarkModel()->other_node() &&
286 nodes
[0]->parent()->child_count() == 1);
288 new BookmarkContextMenu(
296 context_menu_
->set_observer(this);
297 context_menu_
->RunMenuAt(p
, source_type
);
298 context_menu_
.reset(NULL
);
302 bool BookmarkMenuDelegate::CanDrag(MenuItemView
* menu
) {
303 const BookmarkNode
* node
= menu_id_to_node_map_
[menu
->GetCommand()];
304 // Don't let users drag the other folder.
305 return node
->parent() != GetBookmarkModel()->root_node();
308 void BookmarkMenuDelegate::WriteDragData(MenuItemView
* sender
,
309 ui::OSExchangeData
* data
) {
310 DCHECK(sender
&& data
);
312 content::RecordAction(UserMetricsAction("BookmarkBar_DragFromFolder"));
314 BookmarkNodeData
drag_data(menu_id_to_node_map_
[sender
->GetCommand()]);
315 drag_data
.Write(profile_
, data
);
318 int BookmarkMenuDelegate::GetDragOperations(MenuItemView
* sender
) {
319 return chrome::GetBookmarkDragOperation(
320 profile_
, menu_id_to_node_map_
[sender
->GetCommand()]);
323 int BookmarkMenuDelegate::GetMaxWidthForMenu(MenuItemView
* menu
) {
324 return kMaxMenuWidth
;
327 void BookmarkMenuDelegate::BookmarkModelChanged() {
330 void BookmarkMenuDelegate::BookmarkNodeFaviconChanged(
331 BookmarkModel
* model
,
332 const BookmarkNode
* node
) {
333 NodeToMenuMap::iterator menu_pair
= node_to_menu_map_
.find(node
);
334 if (menu_pair
== node_to_menu_map_
.end())
335 return; // We're not showing a menu item for the node.
337 menu_pair
->second
->SetIcon(model
->GetFavicon(node
).AsImageSkia());
340 void BookmarkMenuDelegate::WillRemoveBookmarks(
341 const std::vector
<const BookmarkNode
*>& bookmarks
) {
342 DCHECK(!is_mutating_model_
);
343 is_mutating_model_
= true; // Set to false in DidRemoveBookmarks().
345 // Remove the observer so that when the remove happens we don't prematurely
346 // cancel the menu. The observer is added back in DidRemoveBookmarks().
347 GetBookmarkModel()->RemoveObserver(this);
349 // Remove the menu items.
350 std::set
<MenuItemView
*> changed_parent_menus
;
351 for (std::vector
<const BookmarkNode
*>::const_iterator
i(bookmarks
.begin());
352 i
!= bookmarks
.end(); ++i
) {
353 NodeToMenuMap::iterator node_to_menu
= node_to_menu_map_
.find(*i
);
354 if (node_to_menu
!= node_to_menu_map_
.end()) {
355 MenuItemView
* menu
= node_to_menu
->second
;
356 MenuItemView
* parent
= menu
->GetParentMenuItem();
357 // |parent| is NULL when removing a root. This happens when right clicking
358 // to delete an empty folder.
360 changed_parent_menus
.insert(parent
);
361 parent
->RemoveMenuItemAt(menu
->parent()->GetIndexOf(menu
));
363 node_to_menu_map_
.erase(node_to_menu
);
364 menu_id_to_node_map_
.erase(menu
->GetCommand());
368 // All the bookmarks in |bookmarks| should have the same parent. It's possible
369 // to support different parents, but this would need to prune any nodes whose
370 // parent has been removed. As all nodes currently have the same parent, there
372 DCHECK(changed_parent_menus
.size() <= 1);
374 // Remove any descendants of the removed nodes in |node_to_menu_map_|.
375 for (NodeToMenuMap::iterator
i(node_to_menu_map_
.begin());
376 i
!= node_to_menu_map_
.end(); ) {
377 bool ancestor_removed
= false;
378 for (std::vector
<const BookmarkNode
*>::const_iterator
j(bookmarks
.begin());
379 j
!= bookmarks
.end(); ++j
) {
380 if (i
->first
->HasAncestor(*j
)) {
381 ancestor_removed
= true;
385 if (ancestor_removed
) {
386 menu_id_to_node_map_
.erase(i
->second
->GetCommand());
387 node_to_menu_map_
.erase(i
++);
393 for (std::set
<MenuItemView
*>::const_iterator
i(changed_parent_menus
.begin());
394 i
!= changed_parent_menus
.end(); ++i
)
395 (*i
)->ChildrenChanged();
398 void BookmarkMenuDelegate::DidRemoveBookmarks() {
399 // Balances remove in WillRemoveBookmarksImpl.
400 GetBookmarkModel()->AddObserver(this);
401 DCHECK(is_mutating_model_
);
402 is_mutating_model_
= false;
405 MenuItemView
* BookmarkMenuDelegate::CreateMenu(const BookmarkNode
* parent
,
406 int start_child_index
,
407 ShowOptions show_options
) {
408 MenuItemView
* menu
= new MenuItemView(real_delegate_
);
409 menu
->SetCommand(next_menu_id_
++);
410 menu_id_to_node_map_
[menu
->GetCommand()] = parent
;
411 menu
->set_has_icons(true);
412 BuildMenu(parent
, start_child_index
, menu
, &next_menu_id_
);
413 if (show_options
== SHOW_PERMANENT_FOLDERS
)
414 BuildMenusForPermanentNodes(menu
, &next_menu_id_
);
418 void BookmarkMenuDelegate::BuildMenusForPermanentNodes(
419 views::MenuItemView
* menu
,
421 BookmarkModel
* model
= GetBookmarkModel();
422 bool added_separator
= false;
423 BuildMenuForPermanentNode(model
->other_node(), menu
, next_menu_id
,
425 BuildMenuForPermanentNode(model
->mobile_node(), menu
, next_menu_id
,
429 void BookmarkMenuDelegate::BuildMenuForPermanentNode(
430 const BookmarkNode
* node
,
433 bool* added_separator
) {
434 if (!node
->IsVisible() || node
->GetTotalNodeCount() == 1)
435 return; // No children, don't create a menu.
437 int id
= *next_menu_id
;
438 // Don't create the submenu if its menu ID will be outside the range allowed.
439 if (IsOutsideMenuIdRange(id
))
443 if (!*added_separator
) {
444 *added_separator
= true;
445 menu
->AppendSeparator();
448 ui::ResourceBundle
* rb
= &ui::ResourceBundle::GetSharedInstance();
449 gfx::ImageSkia
* folder_icon
= rb
->GetImageSkiaNamed(IDR_BOOKMARK_BAR_FOLDER
);
450 MenuItemView
* submenu
= menu
->AppendSubMenuWithIcon(
451 id
, node
->GetTitle(), *folder_icon
);
452 BuildMenu(node
, 0, submenu
, next_menu_id
);
453 menu_id_to_node_map_
[id
] = node
;
456 void BookmarkMenuDelegate::BuildMenu(const BookmarkNode
* parent
,
457 int start_child_index
,
460 node_to_menu_map_
[parent
] = menu
;
461 DCHECK(parent
->empty() || start_child_index
< parent
->child_count());
462 ui::ResourceBundle
* rb
= &ui::ResourceBundle::GetSharedInstance();
463 for (int i
= start_child_index
; i
< parent
->child_count(); ++i
) {
464 const BookmarkNode
* node
= parent
->GetChild(i
);
465 const int id
= *next_menu_id
;
466 // Don't create the item if its menu ID will be outside the range allowed.
467 if (IsOutsideMenuIdRange(id
))
472 menu_id_to_node_map_
[id
] = node
;
473 if (node
->is_url()) {
474 const gfx::Image
& image
= GetBookmarkModel()->GetFavicon(node
);
475 const gfx::ImageSkia
* icon
= image
.IsEmpty() ?
476 rb
->GetImageSkiaNamed(IDR_DEFAULT_FAVICON
) : image
.ToImageSkia();
477 node_to_menu_map_
[node
] =
478 menu
->AppendMenuItemWithIcon(id
, node
->GetTitle(), *icon
);
479 } else if (node
->is_folder()) {
480 gfx::ImageSkia
* folder_icon
=
481 rb
->GetImageSkiaNamed(IDR_BOOKMARK_BAR_FOLDER
);
482 MenuItemView
* submenu
= menu
->AppendSubMenuWithIcon(
483 id
, node
->GetTitle(), *folder_icon
);
484 BuildMenu(node
, 0, submenu
, next_menu_id
);
491 bool BookmarkMenuDelegate::IsOutsideMenuIdRange(int menu_id
) const {
492 return menu_id
< min_menu_id_
|| menu_id
> max_menu_id_
;