bookmark_menu_delegate.cc revision 5f1c94371a64b3196d4be9466099bb892df9b88e
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. 4 5#include "chrome/browser/ui/views/bookmarks/bookmark_menu_delegate.h" 6 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/generated_resources.h" 24#include "grit/theme_resources.h" 25#include "grit/ui_resources.h" 26#include "ui/base/dragdrop/os_exchange_data.h" 27#include "ui/base/l10n/l10n_util.h" 28#include "ui/base/resource/resource_bundle.h" 29#include "ui/base/window_open_disposition.h" 30#include "ui/views/controls/button/menu_button.h" 31#include "ui/views/controls/menu/menu_item_view.h" 32#include "ui/views/controls/menu/submenu_view.h" 33#include "ui/views/widget/widget.h" 34 35using base::UserMetricsAction; 36using bookmarks::BookmarkNodeData; 37using content::PageNavigator; 38using views::MenuItemView; 39 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. 42static const int kMaxMenuWidth = 400; 43 44BookmarkMenuDelegate::BookmarkMenuDelegate(Browser* browser, 45 PageNavigator* navigator, 46 views::Widget* parent, 47 int first_menu_id, 48 int max_menu_id) 49 : browser_(browser), 50 profile_(browser->profile()), 51 page_navigator_(navigator), 52 parent_(parent), 53 menu_(NULL), 54 parent_menu_item_(NULL), 55 next_menu_id_(first_menu_id), 56 min_menu_id_(first_menu_id), 57 max_menu_id_(max_menu_id), 58 real_delegate_(NULL), 59 is_mutating_model_(false), 60 location_(BOOKMARK_LAUNCH_LOCATION_NONE) {} 61 62BookmarkMenuDelegate::~BookmarkMenuDelegate() { 63 GetBookmarkModel()->RemoveObserver(this); 64} 65 66void BookmarkMenuDelegate::Init(views::MenuDelegate* real_delegate, 67 MenuItemView* parent, 68 const BookmarkNode* node, 69 int start_child_index, 70 ShowOptions show_options, 71 BookmarkLaunchLocation location) { 72 GetBookmarkModel()->AddObserver(this); 73 real_delegate_ = real_delegate; 74 location_ = location; 75 if (parent) { 76 parent_menu_item_ = parent; 77 78 // Add a separator if there are existing items in the menu, and if the 79 // current node has children. If |node| is the bookmark bar then the 80 // managed node is shown as its first child, if it's not empty. 81 BookmarkModel* model = GetBookmarkModel(); 82 ChromeBookmarkClient* client = GetChromeBookmarkClient(); 83 bool show_managed = show_options == SHOW_PERMANENT_FOLDERS && 84 node == model->bookmark_bar_node() && 85 !client->managed_node()->empty(); 86 bool has_children = 87 (start_child_index < node->child_count()) || show_managed; 88 int initial_count = parent->GetSubmenu() ? 89 parent->GetSubmenu()->GetMenuItemCount() : 0; 90 if (has_children && initial_count > 0) 91 parent->AppendSeparator(); 92 if (show_managed) 93 BuildMenuForManagedNode(parent, &next_menu_id_); 94 BuildMenu(node, start_child_index, parent, &next_menu_id_); 95 if (show_options == SHOW_PERMANENT_FOLDERS) 96 BuildMenusForPermanentNodes(parent, &next_menu_id_); 97 } else { 98 menu_ = CreateMenu(node, start_child_index, show_options); 99 } 100} 101 102void BookmarkMenuDelegate::SetPageNavigator(PageNavigator* navigator) { 103 page_navigator_ = navigator; 104 if (context_menu_.get()) 105 context_menu_->SetPageNavigator(navigator); 106} 107 108BookmarkModel* BookmarkMenuDelegate::GetBookmarkModel() { 109 return BookmarkModelFactory::GetForProfile(profile_); 110} 111 112ChromeBookmarkClient* BookmarkMenuDelegate::GetChromeBookmarkClient() { 113 return ChromeBookmarkClientFactory::GetForProfile(profile_); 114} 115 116void BookmarkMenuDelegate::SetActiveMenu(const BookmarkNode* node, 117 int start_index) { 118 DCHECK(!parent_menu_item_); 119 if (!node_to_menu_map_[node]) 120 CreateMenu(node, start_index, HIDE_PERMANENT_FOLDERS); 121 menu_ = node_to_menu_map_[node]; 122} 123 124base::string16 BookmarkMenuDelegate::GetTooltipText( 125 int id, 126 const gfx::Point& screen_loc) const { 127 MenuIDToNodeMap::const_iterator i = menu_id_to_node_map_.find(id); 128 // When removing bookmarks it may be possible to end up here without a node. 129 if (i == menu_id_to_node_map_.end()) { 130 DCHECK(is_mutating_model_); 131 return base::string16(); 132 } 133 134 const BookmarkNode* node = i->second; 135 if (node->is_url()) { 136 return BookmarkBarView::CreateToolTipForURLAndTitle( 137 parent_, screen_loc, node->url(), node->GetTitle(), profile_); 138 } 139 return base::string16(); 140} 141 142bool BookmarkMenuDelegate::IsTriggerableEvent(views::MenuItemView* menu, 143 const ui::Event& e) { 144 return e.type() == ui::ET_GESTURE_TAP || 145 e.type() == ui::ET_GESTURE_TAP_DOWN || 146 event_utils::IsPossibleDispositionEvent(e); 147} 148 149void BookmarkMenuDelegate::ExecuteCommand(int id, int mouse_event_flags) { 150 DCHECK(menu_id_to_node_map_.find(id) != menu_id_to_node_map_.end()); 151 152 const BookmarkNode* node = menu_id_to_node_map_[id]; 153 std::vector<const BookmarkNode*> selection; 154 selection.push_back(node); 155 156 chrome::OpenAll(parent_->GetNativeWindow(), page_navigator_, selection, 157 ui::DispositionFromEventFlags(mouse_event_flags), 158 profile_); 159 RecordBookmarkLaunch(node, location_); 160} 161 162bool BookmarkMenuDelegate::ShouldExecuteCommandWithoutClosingMenu( 163 int id, 164 const ui::Event& event) { 165 return (event.flags() & ui::EF_LEFT_MOUSE_BUTTON) && 166 ui::DispositionFromEventFlags(event.flags()) == NEW_BACKGROUND_TAB; 167} 168 169bool BookmarkMenuDelegate::GetDropFormats( 170 MenuItemView* menu, 171 int* formats, 172 std::set<ui::OSExchangeData::CustomFormat>* custom_formats) { 173 *formats = ui::OSExchangeData::URL; 174 custom_formats->insert(BookmarkNodeData::GetBookmarkCustomFormat()); 175 return true; 176} 177 178bool BookmarkMenuDelegate::AreDropTypesRequired(MenuItemView* menu) { 179 return true; 180} 181 182bool BookmarkMenuDelegate::CanDrop(MenuItemView* menu, 183 const ui::OSExchangeData& data) { 184 // Only accept drops of 1 node, which is the case for all data dragged from 185 // bookmark bar and menus. 186 187 if (!drop_data_.Read(data) || drop_data_.elements.size() != 1 || 188 !profile_->GetPrefs()->GetBoolean(prefs::kEditBookmarksEnabled)) 189 return false; 190 191 if (drop_data_.has_single_url()) 192 return true; 193 194 const BookmarkNode* drag_node = 195 drop_data_.GetFirstNode(GetBookmarkModel(), profile_->GetPath()); 196 if (!drag_node) { 197 // Dragging a folder from another profile, always accept. 198 return true; 199 } 200 201 // Drag originated from same profile and is not a URL. Only accept it if 202 // the dragged node is not a parent of the node menu represents. 203 if (menu_id_to_node_map_.find(menu->GetCommand()) == 204 menu_id_to_node_map_.end()) { 205 // If we don't know the menu assume its because we're embedded. We'll 206 // figure out the real operation when GetDropOperation is invoked. 207 return true; 208 } 209 const BookmarkNode* drop_node = menu_id_to_node_map_[menu->GetCommand()]; 210 DCHECK(drop_node); 211 while (drop_node && drop_node != drag_node) 212 drop_node = drop_node->parent(); 213 return (drop_node == NULL); 214} 215 216int BookmarkMenuDelegate::GetDropOperation( 217 MenuItemView* item, 218 const ui::DropTargetEvent& event, 219 views::MenuDelegate::DropPosition* position) { 220 // Should only get here if we have drop data. 221 DCHECK(drop_data_.is_valid()); 222 223 const BookmarkNode* node = menu_id_to_node_map_[item->GetCommand()]; 224 const BookmarkNode* drop_parent = node->parent(); 225 int index_to_drop_at = drop_parent->GetIndexOf(node); 226 BookmarkModel* model = GetBookmarkModel(); 227 switch (*position) { 228 case views::MenuDelegate::DROP_AFTER: 229 if (node == model->other_node() || node == model->mobile_node()) { 230 // Dropping after these nodes makes no sense. 231 *position = views::MenuDelegate::DROP_NONE; 232 } 233 index_to_drop_at++; 234 break; 235 236 case views::MenuDelegate::DROP_BEFORE: 237 if (node == model->mobile_node()) { 238 // Dropping before this node makes no sense. 239 *position = views::MenuDelegate::DROP_NONE; 240 } 241 break; 242 243 case views::MenuDelegate::DROP_ON: 244 drop_parent = node; 245 index_to_drop_at = node->child_count(); 246 break; 247 248 default: 249 break; 250 } 251 DCHECK(drop_parent); 252 return chrome::GetBookmarkDropOperation( 253 profile_, event, drop_data_, drop_parent, index_to_drop_at); 254} 255 256int BookmarkMenuDelegate::OnPerformDrop( 257 MenuItemView* menu, 258 views::MenuDelegate::DropPosition position, 259 const ui::DropTargetEvent& event) { 260 const BookmarkNode* drop_node = menu_id_to_node_map_[menu->GetCommand()]; 261 DCHECK(drop_node); 262 BookmarkModel* model = GetBookmarkModel(); 263 DCHECK(model); 264 const BookmarkNode* drop_parent = drop_node->parent(); 265 DCHECK(drop_parent); 266 int index_to_drop_at = drop_parent->GetIndexOf(drop_node); 267 switch (position) { 268 case views::MenuDelegate::DROP_AFTER: 269 index_to_drop_at++; 270 break; 271 272 case views::MenuDelegate::DROP_ON: 273 DCHECK(drop_node->is_folder()); 274 drop_parent = drop_node; 275 index_to_drop_at = drop_node->child_count(); 276 break; 277 278 case views::MenuDelegate::DROP_BEFORE: 279 if (drop_node == model->other_node() || 280 drop_node == model->mobile_node()) { 281 // This can happen with SHOW_PERMANENT_FOLDERS. 282 drop_parent = model->bookmark_bar_node(); 283 index_to_drop_at = drop_parent->child_count(); 284 } 285 break; 286 287 default: 288 break; 289 } 290 291 bool copy = event.source_operations() == ui::DragDropTypes::DRAG_COPY; 292 return chrome::DropBookmarks(profile_, drop_data_, 293 drop_parent, index_to_drop_at, copy); 294} 295 296bool BookmarkMenuDelegate::ShowContextMenu(MenuItemView* source, 297 int id, 298 const gfx::Point& p, 299 ui::MenuSourceType source_type) { 300 DCHECK(menu_id_to_node_map_.find(id) != menu_id_to_node_map_.end()); 301 std::vector<const BookmarkNode*> nodes; 302 nodes.push_back(menu_id_to_node_map_[id]); 303 bool close_on_delete = !parent_menu_item_ && 304 (nodes[0]->parent() == GetBookmarkModel()->other_node() && 305 nodes[0]->parent()->child_count() == 1); 306 context_menu_.reset( 307 new BookmarkContextMenu( 308 parent_, 309 browser_, 310 profile_, 311 page_navigator_, 312 nodes[0]->parent(), 313 nodes, 314 close_on_delete)); 315 context_menu_->set_observer(this); 316 context_menu_->RunMenuAt(p, source_type); 317 context_menu_.reset(NULL); 318 return true; 319} 320 321bool BookmarkMenuDelegate::CanDrag(MenuItemView* menu) { 322 const BookmarkNode* node = menu_id_to_node_map_[menu->GetCommand()]; 323 // Don't let users drag the other folder. 324 return node->parent() != GetBookmarkModel()->root_node(); 325} 326 327void BookmarkMenuDelegate::WriteDragData(MenuItemView* sender, 328 ui::OSExchangeData* data) { 329 DCHECK(sender && data); 330 331 content::RecordAction(UserMetricsAction("BookmarkBar_DragFromFolder")); 332 333 BookmarkNodeData drag_data(menu_id_to_node_map_[sender->GetCommand()]); 334 drag_data.Write(profile_->GetPath(), data); 335} 336 337int BookmarkMenuDelegate::GetDragOperations(MenuItemView* sender) { 338 return chrome::GetBookmarkDragOperation( 339 profile_, menu_id_to_node_map_[sender->GetCommand()]); 340} 341 342int BookmarkMenuDelegate::GetMaxWidthForMenu(MenuItemView* menu) { 343 return kMaxMenuWidth; 344} 345 346void BookmarkMenuDelegate::BookmarkModelChanged() { 347} 348 349void BookmarkMenuDelegate::BookmarkNodeFaviconChanged( 350 BookmarkModel* model, 351 const BookmarkNode* node) { 352 NodeToMenuMap::iterator menu_pair = node_to_menu_map_.find(node); 353 if (menu_pair == node_to_menu_map_.end()) 354 return; // We're not showing a menu item for the node. 355 356 menu_pair->second->SetIcon(model->GetFavicon(node).AsImageSkia()); 357} 358 359void BookmarkMenuDelegate::WillRemoveBookmarks( 360 const std::vector<const BookmarkNode*>& bookmarks) { 361 DCHECK(!is_mutating_model_); 362 is_mutating_model_ = true; // Set to false in DidRemoveBookmarks(). 363 364 // Remove the observer so that when the remove happens we don't prematurely 365 // cancel the menu. The observer is added back in DidRemoveBookmarks(). 366 GetBookmarkModel()->RemoveObserver(this); 367 368 // Remove the menu items. 369 std::set<MenuItemView*> changed_parent_menus; 370 for (std::vector<const BookmarkNode*>::const_iterator i(bookmarks.begin()); 371 i != bookmarks.end(); ++i) { 372 NodeToMenuMap::iterator node_to_menu = node_to_menu_map_.find(*i); 373 if (node_to_menu != node_to_menu_map_.end()) { 374 MenuItemView* menu = node_to_menu->second; 375 MenuItemView* parent = menu->GetParentMenuItem(); 376 // |parent| is NULL when removing a root. This happens when right clicking 377 // to delete an empty folder. 378 if (parent) { 379 changed_parent_menus.insert(parent); 380 parent->RemoveMenuItemAt(menu->parent()->GetIndexOf(menu)); 381 } 382 node_to_menu_map_.erase(node_to_menu); 383 menu_id_to_node_map_.erase(menu->GetCommand()); 384 } 385 } 386 387 // All the bookmarks in |bookmarks| should have the same parent. It's possible 388 // to support different parents, but this would need to prune any nodes whose 389 // parent has been removed. As all nodes currently have the same parent, there 390 // is the DCHECK. 391 DCHECK(changed_parent_menus.size() <= 1); 392 393 // Remove any descendants of the removed nodes in |node_to_menu_map_|. 394 for (NodeToMenuMap::iterator i(node_to_menu_map_.begin()); 395 i != node_to_menu_map_.end(); ) { 396 bool ancestor_removed = false; 397 for (std::vector<const BookmarkNode*>::const_iterator j(bookmarks.begin()); 398 j != bookmarks.end(); ++j) { 399 if (i->first->HasAncestor(*j)) { 400 ancestor_removed = true; 401 break; 402 } 403 } 404 if (ancestor_removed) { 405 menu_id_to_node_map_.erase(i->second->GetCommand()); 406 node_to_menu_map_.erase(i++); 407 } else { 408 ++i; 409 } 410 } 411 412 for (std::set<MenuItemView*>::const_iterator i(changed_parent_menus.begin()); 413 i != changed_parent_menus.end(); ++i) 414 (*i)->ChildrenChanged(); 415} 416 417void BookmarkMenuDelegate::DidRemoveBookmarks() { 418 // Balances remove in WillRemoveBookmarksImpl. 419 GetBookmarkModel()->AddObserver(this); 420 DCHECK(is_mutating_model_); 421 is_mutating_model_ = false; 422} 423 424MenuItemView* BookmarkMenuDelegate::CreateMenu(const BookmarkNode* parent, 425 int start_child_index, 426 ShowOptions show_options) { 427 MenuItemView* menu = new MenuItemView(real_delegate_); 428 menu->SetCommand(next_menu_id_++); 429 menu_id_to_node_map_[menu->GetCommand()] = parent; 430 menu->set_has_icons(true); 431 bool show_permanent = show_options == SHOW_PERMANENT_FOLDERS; 432 if (show_permanent && parent == GetBookmarkModel()->bookmark_bar_node()) 433 BuildMenuForManagedNode(menu, &next_menu_id_); 434 BuildMenu(parent, start_child_index, menu, &next_menu_id_); 435 if (show_permanent) 436 BuildMenusForPermanentNodes(menu, &next_menu_id_); 437 return menu; 438} 439 440void BookmarkMenuDelegate::BuildMenusForPermanentNodes( 441 views::MenuItemView* menu, 442 int* next_menu_id) { 443 BookmarkModel* model = GetBookmarkModel(); 444 bool added_separator = false; 445 BuildMenuForPermanentNode(model->other_node(), IDR_BOOKMARK_BAR_FOLDER, menu, 446 next_menu_id, &added_separator); 447 BuildMenuForPermanentNode(model->mobile_node(), IDR_BOOKMARK_BAR_FOLDER, menu, 448 next_menu_id, &added_separator); 449} 450 451void BookmarkMenuDelegate::BuildMenuForPermanentNode( 452 const BookmarkNode* node, 453 int icon_resource_id, 454 MenuItemView* menu, 455 int* next_menu_id, 456 bool* added_separator) { 457 if (!node->IsVisible() || node->GetTotalNodeCount() == 1) 458 return; // No children, don't create a menu. 459 460 int id = *next_menu_id; 461 // Don't create the submenu if its menu ID will be outside the range allowed. 462 if (IsOutsideMenuIdRange(id)) 463 return; 464 (*next_menu_id)++; 465 466 if (!*added_separator) { 467 *added_separator = true; 468 menu->AppendSeparator(); 469 } 470 471 ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance(); 472 gfx::ImageSkia* folder_icon = rb->GetImageSkiaNamed(icon_resource_id); 473 MenuItemView* submenu = menu->AppendSubMenuWithIcon( 474 id, node->GetTitle(), *folder_icon); 475 BuildMenu(node, 0, submenu, next_menu_id); 476 menu_id_to_node_map_[id] = node; 477} 478 479void BookmarkMenuDelegate::BuildMenuForManagedNode( 480 MenuItemView* menu, 481 int* next_menu_id) { 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, 486 next_menu_id, &added_separator); 487} 488 489void BookmarkMenuDelegate::BuildMenu(const BookmarkNode* parent, 490 int start_child_index, 491 MenuItemView* menu, 492 int* next_menu_id) { 493 node_to_menu_map_[parent] = menu; 494 DCHECK(parent->empty() || start_child_index < parent->child_count()); 495 ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance(); 496 for (int i = start_child_index; i < parent->child_count(); ++i) { 497 const BookmarkNode* node = parent->GetChild(i); 498 const int id = *next_menu_id; 499 // Don't create the item if its menu ID will be outside the range allowed. 500 if (IsOutsideMenuIdRange(id)) 501 break; 502 503 (*next_menu_id)++; 504 505 menu_id_to_node_map_[id] = node; 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(); 510 node_to_menu_map_[node] = 511 menu->AppendMenuItemWithIcon(id, node->GetTitle(), *icon); 512 } else if (node->is_folder()) { 513 gfx::ImageSkia* folder_icon = 514 rb->GetImageSkiaNamed(IDR_BOOKMARK_BAR_FOLDER); 515 MenuItemView* submenu = menu->AppendSubMenuWithIcon( 516 id, node->GetTitle(), *folder_icon); 517 BuildMenu(node, 0, submenu, next_menu_id); 518 } else { 519 NOTREACHED(); 520 } 521 } 522} 523 524bool BookmarkMenuDelegate::IsOutsideMenuIdRange(int menu_id) const { 525 return menu_id < min_menu_id_ || menu_id > max_menu_id_; 526} 527