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