bookmark_menu_delegate.cc revision 0f1bc08d4cfcc34181b0b5cbf065c40f687bf740
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.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" 32 33using content::PageNavigator; 34using content::UserMetricsAction; 35using views::MenuItemView; 36 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. 39static const int kMaxMenuWidth = 400; 40 41BookmarkMenuDelegate::BookmarkMenuDelegate(Browser* browser, 42 PageNavigator* navigator, 43 views::Widget* parent, 44 int first_menu_id, 45 int max_menu_id) 46 : browser_(browser), 47 profile_(browser->profile()), 48 page_navigator_(navigator), 49 parent_(parent), 50 menu_(NULL), 51 for_drop_(false), 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), 56 real_delegate_(NULL), 57 is_mutating_model_(false), 58 location_(BOOKMARK_LAUNCH_LOCATION_NONE) {} 59 60BookmarkMenuDelegate::~BookmarkMenuDelegate() { 61 GetBookmarkModel()->RemoveObserver(this); 62} 63 64void BookmarkMenuDelegate::Init(views::MenuDelegate* real_delegate, 65 MenuItemView* parent, 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; 72 location_ = location; 73 if (parent) { 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(); 80 } 81 BuildMenu(node, start_child_index, parent, &next_menu_id_); 82 if (show_options == SHOW_PERMANENT_FOLDERS) 83 BuildMenusForPermanentNodes(parent, &next_menu_id_); 84 } else { 85 menu_ = CreateMenu(node, start_child_index, show_options); 86 } 87} 88 89void BookmarkMenuDelegate::SetPageNavigator(PageNavigator* navigator) { 90 page_navigator_ = navigator; 91 if (context_menu_.get()) 92 context_menu_->SetPageNavigator(navigator); 93} 94 95BookmarkModel* BookmarkMenuDelegate::GetBookmarkModel() { 96 return BookmarkModelFactory::GetForProfile(profile_); 97} 98 99void BookmarkMenuDelegate::SetActiveMenu(const BookmarkNode* node, 100 int start_index) { 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]; 105} 106 107string16 BookmarkMenuDelegate::GetTooltipText( 108 int id, 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 string16(); 115 } 116 117 const BookmarkNode* node = i->second; 118 if (node->is_url()) { 119 return BookmarkBarView::CreateToolTipForURLAndTitle( 120 parent_, screen_loc, node->url(), node->GetTitle(), profile_); 121 } 122 return string16(); 123} 124 125bool 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); 130} 131 132void BookmarkMenuDelegate::ExecuteCommand(int id, int mouse_event_flags) { 133 DCHECK(menu_id_to_node_map_.find(id) != menu_id_to_node_map_.end()); 134 135 const BookmarkNode* node = menu_id_to_node_map_[id]; 136 std::vector<const BookmarkNode*> selection; 137 selection.push_back(node); 138 139 chrome::OpenAll(parent_->GetNativeWindow(), page_navigator_, selection, 140 ui::DispositionFromEventFlags(mouse_event_flags), 141 profile_); 142 RecordBookmarkLaunch(node, location_); 143} 144 145bool BookmarkMenuDelegate::ShouldExecuteCommandWithoutClosingMenu( 146 int id, 147 const ui::Event& event) { 148 return (event.flags() & ui::EF_LEFT_MOUSE_BUTTON) && 149 ui::DispositionFromEventFlags(event.flags()) == NEW_BACKGROUND_TAB; 150} 151 152bool BookmarkMenuDelegate::GetDropFormats( 153 MenuItemView* menu, 154 int* formats, 155 std::set<ui::OSExchangeData::CustomFormat>* custom_formats) { 156 *formats = ui::OSExchangeData::URL; 157 custom_formats->insert(BookmarkNodeData::GetBookmarkCustomFormat()); 158 return true; 159} 160 161bool BookmarkMenuDelegate::AreDropTypesRequired(MenuItemView* menu) { 162 return true; 163} 164 165bool 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. 169 170 if (!drop_data_.Read(data) || drop_data_.elements.size() != 1 || 171 !profile_->GetPrefs()->GetBoolean(prefs::kEditBookmarksEnabled)) 172 return false; 173 174 if (drop_data_.has_single_url()) 175 return true; 176 177 const BookmarkNode* drag_node = drop_data_.GetFirstNode(profile_); 178 if (!drag_node) { 179 // Dragging a folder from another profile, always accept. 180 return true; 181 } 182 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. 189 return true; 190 } 191 const BookmarkNode* drop_node = menu_id_to_node_map_[menu->GetCommand()]; 192 DCHECK(drop_node); 193 while (drop_node && drop_node != drag_node) 194 drop_node = drop_node->parent(); 195 return (drop_node == NULL); 196} 197 198int BookmarkMenuDelegate::GetDropOperation( 199 MenuItemView* item, 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()); 204 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(); 209 switch (*position) { 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; 214 } 215 index_to_drop_at++; 216 break; 217 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; 222 } 223 break; 224 225 case views::MenuDelegate::DROP_ON: 226 drop_parent = node; 227 index_to_drop_at = node->child_count(); 228 break; 229 230 default: 231 break; 232 } 233 DCHECK(drop_parent); 234 return chrome::GetBookmarkDropOperation(profile_, event, drop_data_, 235 drop_parent, index_to_drop_at); 236} 237 238int BookmarkMenuDelegate::OnPerformDrop( 239 MenuItemView* menu, 240 views::MenuDelegate::DropPosition position, 241 const ui::DropTargetEvent& event) { 242 const BookmarkNode* drop_node = menu_id_to_node_map_[menu->GetCommand()]; 243 DCHECK(drop_node); 244 BookmarkModel* model = GetBookmarkModel(); 245 DCHECK(model); 246 const BookmarkNode* drop_parent = drop_node->parent(); 247 DCHECK(drop_parent); 248 int index_to_drop_at = drop_parent->GetIndexOf(drop_node); 249 switch (position) { 250 case views::MenuDelegate::DROP_AFTER: 251 index_to_drop_at++; 252 break; 253 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(); 258 break; 259 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(); 266 } 267 break; 268 269 default: 270 break; 271 } 272 273 return chrome::DropBookmarks(profile_, drop_data_, 274 drop_parent, index_to_drop_at); 275} 276 277bool BookmarkMenuDelegate::ShowContextMenu(MenuItemView* source, 278 int id, 279 const gfx::Point& p, 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); 287 context_menu_.reset( 288 new BookmarkContextMenu( 289 parent_, 290 browser_, 291 profile_, 292 page_navigator_, 293 nodes[0]->parent(), 294 nodes, 295 close_on_delete)); 296 context_menu_->set_observer(this); 297 context_menu_->RunMenuAt(p, source_type); 298 context_menu_.reset(NULL); 299 return true; 300} 301 302bool 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(); 306} 307 308void BookmarkMenuDelegate::WriteDragData(MenuItemView* sender, 309 ui::OSExchangeData* data) { 310 DCHECK(sender && data); 311 312 content::RecordAction(UserMetricsAction("BookmarkBar_DragFromFolder")); 313 314 BookmarkNodeData drag_data(menu_id_to_node_map_[sender->GetCommand()]); 315 drag_data.Write(profile_, data); 316} 317 318int BookmarkMenuDelegate::GetDragOperations(MenuItemView* sender) { 319 return chrome::GetBookmarkDragOperation( 320 profile_, menu_id_to_node_map_[sender->GetCommand()]); 321} 322 323int BookmarkMenuDelegate::GetMaxWidthForMenu(MenuItemView* menu) { 324 return kMaxMenuWidth; 325} 326 327void BookmarkMenuDelegate::BookmarkModelChanged() { 328} 329 330void 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. 336 337 menu_pair->second->SetIcon(model->GetFavicon(node).AsImageSkia()); 338} 339 340void BookmarkMenuDelegate::WillRemoveBookmarks( 341 const std::vector<const BookmarkNode*>& bookmarks) { 342 DCHECK(!is_mutating_model_); 343 is_mutating_model_ = true; // Set to false in DidRemoveBookmarks(). 344 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); 348 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. 359 if (parent) { 360 changed_parent_menus.insert(parent); 361 parent->RemoveMenuItemAt(menu->parent()->GetIndexOf(menu)); 362 } 363 node_to_menu_map_.erase(node_to_menu); 364 menu_id_to_node_map_.erase(menu->GetCommand()); 365 } 366 } 367 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 371 // is the DCHECK. 372 DCHECK(changed_parent_menus.size() <= 1); 373 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; 382 break; 383 } 384 } 385 if (ancestor_removed) { 386 menu_id_to_node_map_.erase(i->second->GetCommand()); 387 node_to_menu_map_.erase(i++); 388 } else { 389 ++i; 390 } 391 } 392 393 for (std::set<MenuItemView*>::const_iterator i(changed_parent_menus.begin()); 394 i != changed_parent_menus.end(); ++i) 395 (*i)->ChildrenChanged(); 396} 397 398void BookmarkMenuDelegate::DidRemoveBookmarks() { 399 // Balances remove in WillRemoveBookmarksImpl. 400 GetBookmarkModel()->AddObserver(this); 401 DCHECK(is_mutating_model_); 402 is_mutating_model_ = false; 403} 404 405MenuItemView* 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_); 415 return menu; 416} 417 418void BookmarkMenuDelegate::BuildMenusForPermanentNodes( 419 views::MenuItemView* menu, 420 int* next_menu_id) { 421 BookmarkModel* model = GetBookmarkModel(); 422 bool added_separator = false; 423 BuildMenuForPermanentNode(model->other_node(), menu, next_menu_id, 424 &added_separator); 425 BuildMenuForPermanentNode(model->mobile_node(), menu, next_menu_id, 426 &added_separator); 427} 428 429void BookmarkMenuDelegate::BuildMenuForPermanentNode( 430 const BookmarkNode* node, 431 MenuItemView* menu, 432 int* next_menu_id, 433 bool* added_separator) { 434 if (!node->IsVisible() || node->GetTotalNodeCount() == 1) 435 return; // No children, don't create a menu. 436 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)) 440 return; 441 (*next_menu_id)++; 442 443 if (!*added_separator) { 444 *added_separator = true; 445 menu->AppendSeparator(); 446 } 447 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; 454} 455 456void BookmarkMenuDelegate::BuildMenu(const BookmarkNode* parent, 457 int start_child_index, 458 MenuItemView* menu, 459 int* next_menu_id) { 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)) 468 break; 469 470 (*next_menu_id)++; 471 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); 485 } else { 486 NOTREACHED(); 487 } 488 } 489} 490 491bool BookmarkMenuDelegate::IsOutsideMenuIdRange(int menu_id) const { 492 return menu_id < min_menu_id_ || menu_id > max_menu_id_; 493} 494