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