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