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/gtk/bookmarks/bookmark_menu_controller_gtk.h" 6 7#include <gtk/gtk.h> 8 9#include "base/strings/string_util.h" 10#include "base/strings/utf_string_conversions.h" 11#include "chrome/browser/bookmarks/bookmark_model.h" 12#include "chrome/browser/bookmarks/bookmark_model_factory.h" 13#include "chrome/browser/bookmarks/bookmark_stats.h" 14#include "chrome/browser/bookmarks/bookmark_utils.h" 15#include "chrome/browser/profiles/profile.h" 16#include "chrome/browser/ui/bookmarks/bookmark_utils.h" 17#include "chrome/browser/ui/browser.h" 18#include "chrome/browser/ui/gtk/bookmarks/bookmark_utils_gtk.h" 19#include "chrome/browser/ui/gtk/event_utils.h" 20#include "chrome/browser/ui/gtk/gtk_chrome_button.h" 21#include "chrome/browser/ui/gtk/gtk_theme_service.h" 22#include "chrome/browser/ui/gtk/gtk_util.h" 23#include "chrome/browser/ui/gtk/menu_gtk.h" 24#include "content/public/browser/page_navigator.h" 25#include "grit/generated_resources.h" 26#include "grit/theme_resources.h" 27#include "grit/ui_resources.h" 28#include "ui/base/dragdrop/gtk_dnd_util.h" 29#include "ui/base/l10n/l10n_util.h" 30#include "ui/base/window_open_disposition.h" 31#include "ui/gfx/gtk_util.h" 32 33using content::OpenURLParams; 34using content::PageNavigator; 35 36namespace { 37 38void SetImageMenuItem(GtkWidget* menu_item, 39 const BookmarkNode* node, 40 BookmarkModel* model) { 41 GdkPixbuf* pixbuf = GetPixbufForNode(node, model, true); 42 gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(menu_item), 43 gtk_image_new_from_pixbuf(pixbuf)); 44 g_object_unref(pixbuf); 45} 46 47const BookmarkNode* GetNodeFromMenuItem(GtkWidget* menu_item) { 48 return static_cast<const BookmarkNode*>( 49 g_object_get_data(G_OBJECT(menu_item), "bookmark-node")); 50} 51 52const BookmarkNode* GetParentNodeFromEmptyMenu(GtkWidget* menu) { 53 return static_cast<const BookmarkNode*>( 54 g_object_get_data(G_OBJECT(menu), "parent-node")); 55} 56 57void* AsVoid(const BookmarkNode* node) { 58 return const_cast<BookmarkNode*>(node); 59} 60 61// The context menu has been dismissed, restore the X and application grabs 62// to whichever menu last had them. (Assuming that menu is still showing.) 63void OnContextMenuHide(GtkWidget* context_menu, GtkWidget* grab_menu) { 64 gtk_util::GrabAllInput(grab_menu); 65 66 // Match the ref we took when connecting this signal. 67 g_object_unref(grab_menu); 68} 69 70} // namespace 71 72BookmarkMenuController::BookmarkMenuController(Browser* browser, 73 PageNavigator* navigator, 74 GtkWindow* window, 75 const BookmarkNode* node, 76 int start_child_index) 77 : browser_(browser), 78 page_navigator_(navigator), 79 parent_window_(window), 80 model_(BookmarkModelFactory::GetForProfile(browser->profile())), 81 node_(node), 82 drag_icon_(NULL), 83 ignore_button_release_(false), 84 triggering_widget_(NULL) { 85 menu_ = gtk_menu_new(); 86 g_object_ref_sink(menu_); 87 BuildMenu(node, start_child_index, menu_); 88 signals_.Connect(menu_, "hide", G_CALLBACK(OnMenuHiddenThunk), this); 89 gtk_widget_show_all(menu_); 90} 91 92BookmarkMenuController::~BookmarkMenuController() { 93 model_->RemoveObserver(this); 94 // Make sure the hide handler runs. 95 gtk_widget_hide(menu_); 96 gtk_widget_destroy(menu_); 97 g_object_unref(menu_); 98} 99 100void BookmarkMenuController::Popup(GtkWidget* widget, gint button_type, 101 guint32 timestamp) { 102 model_->AddObserver(this); 103 104 triggering_widget_ = widget; 105 signals_.Connect(triggering_widget_, "destroy", 106 G_CALLBACK(gtk_widget_destroyed), &triggering_widget_); 107 gtk_chrome_button_set_paint_state(GTK_CHROME_BUTTON(widget), 108 GTK_STATE_ACTIVE); 109 gtk_menu_popup(GTK_MENU(menu_), NULL, NULL, 110 &MenuGtk::WidgetMenuPositionFunc, 111 widget, button_type, timestamp); 112} 113 114void BookmarkMenuController::BookmarkModelChanged() { 115 gtk_menu_popdown(GTK_MENU(menu_)); 116} 117 118void BookmarkMenuController::BookmarkNodeFaviconChanged( 119 BookmarkModel* model, const BookmarkNode* node) { 120 std::map<const BookmarkNode*, GtkWidget*>::iterator it = 121 node_to_menu_widget_map_.find(node); 122 if (it != node_to_menu_widget_map_.end()) 123 SetImageMenuItem(it->second, node, model); 124} 125 126void BookmarkMenuController::WillExecuteCommand( 127 int command_id, 128 const std::vector<const BookmarkNode*>& bookmarks) { 129 gtk_menu_popdown(GTK_MENU(menu_)); 130} 131 132void BookmarkMenuController::CloseMenu() { 133 context_menu_->Cancel(); 134} 135 136void BookmarkMenuController::NavigateToMenuItem( 137 GtkWidget* menu_item, 138 WindowOpenDisposition disposition) { 139 const BookmarkNode* node = GetNodeFromMenuItem(menu_item); 140 DCHECK(node); 141 DCHECK(page_navigator_); 142 RecordBookmarkLaunch(node, BOOKMARK_LAUNCH_LOCATION_BAR_SUBFOLDER); 143 page_navigator_->OpenURL(OpenURLParams( 144 node->url(), content::Referrer(), disposition, 145 content::PAGE_TRANSITION_AUTO_BOOKMARK, false)); 146} 147 148void BookmarkMenuController::BuildMenu(const BookmarkNode* parent, 149 int start_child_index, 150 GtkWidget* menu) { 151 DCHECK(parent->empty() || start_child_index < parent->child_count()); 152 153 signals_.Connect(menu, "button-press-event", 154 G_CALLBACK(OnMenuButtonPressedOrReleasedThunk), this); 155 signals_.Connect(menu, "button-release-event", 156 G_CALLBACK(OnMenuButtonPressedOrReleasedThunk), this); 157 158 for (int i = start_child_index; i < parent->child_count(); ++i) { 159 const BookmarkNode* node = parent->GetChild(i); 160 161 GtkWidget* menu_item = 162 gtk_image_menu_item_new_with_label(BuildMenuLabelFor(node).c_str()); 163 g_object_set_data(G_OBJECT(menu_item), "bookmark-node", AsVoid(node)); 164 SetImageMenuItem(menu_item, node, model_); 165 gtk_util::SetAlwaysShowImage(menu_item); 166 167 signals_.Connect(menu_item, "button-release-event", 168 G_CALLBACK(OnButtonReleasedThunk), this); 169 if (node->is_url()) { 170 signals_.Connect(menu_item, "activate", 171 G_CALLBACK(OnMenuItemActivatedThunk), this); 172 } else if (node->is_folder()) { 173 GtkWidget* submenu = gtk_menu_new(); 174 BuildMenu(node, 0, submenu); 175 gtk_menu_item_set_submenu(GTK_MENU_ITEM(menu_item), submenu); 176 } else { 177 NOTREACHED(); 178 } 179 180 gtk_drag_source_set(menu_item, GDK_BUTTON1_MASK, NULL, 0, 181 static_cast<GdkDragAction>(GDK_ACTION_COPY | GDK_ACTION_LINK)); 182 int target_mask = ui::CHROME_BOOKMARK_ITEM; 183 if (node->is_url()) 184 target_mask |= ui::TEXT_URI_LIST | ui::NETSCAPE_URL; 185 ui::SetSourceTargetListFromCodeMask(menu_item, target_mask); 186 signals_.Connect(menu_item, "drag-begin", 187 G_CALLBACK(OnMenuItemDragBeginThunk), this); 188 signals_.Connect(menu_item, "drag-end", 189 G_CALLBACK(OnMenuItemDragEndThunk), this); 190 signals_.Connect(menu_item, "drag-data-get", 191 G_CALLBACK(OnMenuItemDragGetThunk), this); 192 193 // It is important to connect to this signal after setting up the drag 194 // source because we only want to stifle the menu's default handler and 195 // not the handler that the drag source uses. 196 if (node->is_folder()) { 197 signals_.Connect(menu_item, "button-press-event", 198 G_CALLBACK(OnFolderButtonPressedThunk), this); 199 } 200 201 gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item); 202 node_to_menu_widget_map_[node] = menu_item; 203 } 204 205 if (parent->empty()) { 206 GtkWidget* empty_menu = gtk_menu_item_new_with_label( 207 l10n_util::GetStringUTF8(IDS_MENU_EMPTY_SUBMENU).c_str()); 208 gtk_widget_set_sensitive(empty_menu, FALSE); 209 g_object_set_data(G_OBJECT(menu), "parent-node", AsVoid(parent)); 210 gtk_menu_shell_append(GTK_MENU_SHELL(menu), empty_menu); 211 } 212} 213 214gboolean BookmarkMenuController::OnMenuButtonPressedOrReleased( 215 GtkWidget* sender, 216 GdkEventButton* event) { 217 // Handle middle mouse downs and right mouse ups. 218 if (!((event->button == 2 && event->type == GDK_BUTTON_RELEASE) || 219 (event->button == 3 && event->type == GDK_BUTTON_PRESS))) { 220 return FALSE; 221 } 222 223 ignore_button_release_ = false; 224 GtkMenuShell* menu_shell = GTK_MENU_SHELL(sender); 225 // If the cursor is outside our bounds, pass this event up to the parent. 226 if (!gtk_util::WidgetContainsCursor(sender)) { 227 if (menu_shell->parent_menu_shell) { 228 return OnMenuButtonPressedOrReleased(menu_shell->parent_menu_shell, 229 event); 230 } else { 231 // We are the top level menu; we can propagate no further. 232 return FALSE; 233 } 234 } 235 236 // This will return NULL if we are not an empty menu. 237 const BookmarkNode* parent = GetParentNodeFromEmptyMenu(sender); 238 bool is_empty_menu = !!parent; 239 // If there is no active menu item and we are not an empty menu, then do 240 // nothing. This can happen if the user has canceled a context menu while 241 // the cursor is hovering over a bookmark menu. Doing nothing is not optimal 242 // (the hovered item should be active), but it's a hopefully rare corner 243 // case. 244 GtkWidget* menu_item = menu_shell->active_menu_item; 245 if (!is_empty_menu && !menu_item) 246 return TRUE; 247 const BookmarkNode* node = 248 menu_item ? GetNodeFromMenuItem(menu_item) : NULL; 249 250 if (event->button == 2 && node && node->is_folder()) { 251 chrome::OpenAll(parent_window_, page_navigator_, node, NEW_BACKGROUND_TAB, 252 browser_->profile()); 253 gtk_menu_popdown(GTK_MENU(menu_)); 254 return TRUE; 255 } else if (event->button == 3) { 256 DCHECK_NE(is_empty_menu, !!node); 257 if (!is_empty_menu) 258 parent = node->parent(); 259 260 // Show the right click menu and stop processing this button event. 261 std::vector<const BookmarkNode*> nodes; 262 if (node) 263 nodes.push_back(node); 264 context_menu_controller_.reset( 265 new BookmarkContextMenuController( 266 parent_window_, this, browser_, browser_->profile(), 267 page_navigator_, parent, nodes)); 268 context_menu_.reset( 269 new MenuGtk(NULL, context_menu_controller_->menu_model())); 270 271 // Our bookmark folder menu loses the grab to the context menu. When the 272 // context menu is hidden, re-assert our grab. 273 GtkWidget* grabbing_menu = gtk_grab_get_current(); 274 g_object_ref(grabbing_menu); 275 signals_.Connect(context_menu_->widget(), "hide", 276 G_CALLBACK(OnContextMenuHide), grabbing_menu); 277 278 context_menu_->PopupAsContext(gfx::Point(event->x_root, event->y_root), 279 event->time); 280 return TRUE; 281 } 282 283 return FALSE; 284} 285 286gboolean BookmarkMenuController::OnButtonReleased( 287 GtkWidget* sender, 288 GdkEventButton* event) { 289 if (ignore_button_release_) { 290 // Don't handle this message; it was a drag. 291 ignore_button_release_ = false; 292 return FALSE; 293 } 294 295 // Releasing either button 1 or 2 should trigger the bookmark. 296 if (!gtk_menu_item_get_submenu(GTK_MENU_ITEM(sender))) { 297 // The menu item is a link node. 298 if (event->button == 1 || event->button == 2) { 299 WindowOpenDisposition disposition = 300 event_utils::DispositionFromGdkState(event->state); 301 302 NavigateToMenuItem(sender, disposition); 303 304 // We need to manually dismiss the popup menu because we're overriding 305 // button-release-event. 306 gtk_menu_popdown(GTK_MENU(menu_)); 307 return TRUE; 308 } 309 } else { 310 // The menu item is a folder node. 311 if (event->button == 1) { 312 // Having overriden the normal handling, we need to manually activate 313 // the item. 314 gtk_menu_shell_select_item(GTK_MENU_SHELL(sender->parent), sender); 315 g_signal_emit_by_name(sender->parent, "activate-current"); 316 return TRUE; 317 } 318 } 319 320 return FALSE; 321} 322 323gboolean BookmarkMenuController::OnFolderButtonPressed( 324 GtkWidget* sender, GdkEventButton* event) { 325 // The button press may start a drag; don't let the default handler run. 326 if (event->button == 1) 327 return TRUE; 328 return FALSE; 329} 330 331void BookmarkMenuController::OnMenuHidden(GtkWidget* menu) { 332 if (triggering_widget_) 333 gtk_chrome_button_unset_paint_state(GTK_CHROME_BUTTON(triggering_widget_)); 334} 335 336void BookmarkMenuController::OnMenuItemActivated(GtkWidget* menu_item) { 337 NavigateToMenuItem(menu_item, CURRENT_TAB); 338} 339 340void BookmarkMenuController::OnMenuItemDragBegin(GtkWidget* menu_item, 341 GdkDragContext* drag_context) { 342 // The parent menu item might be removed during the drag. Ref it so |button| 343 // won't get destroyed. 344 g_object_ref(menu_item->parent); 345 346 // Signal to any future OnButtonReleased calls that we're dragging instead of 347 // pressing. 348 ignore_button_release_ = true; 349 350 const BookmarkNode* node = BookmarkNodeForWidget(menu_item); 351 drag_icon_ = GetDragRepresentationForNode( 352 node, model_, GtkThemeService::GetFrom(browser_->profile())); 353 gint x, y; 354 gtk_widget_get_pointer(menu_item, &x, &y); 355 gtk_drag_set_icon_widget(drag_context, drag_icon_, x, y); 356 357 // Hide our node. 358 gtk_widget_hide(menu_item); 359} 360 361void BookmarkMenuController::OnMenuItemDragEnd(GtkWidget* menu_item, 362 GdkDragContext* drag_context) { 363 gtk_widget_show(menu_item); 364 g_object_unref(menu_item->parent); 365 366 gtk_widget_destroy(drag_icon_); 367 drag_icon_ = NULL; 368} 369 370void BookmarkMenuController::OnMenuItemDragGet(GtkWidget* widget, 371 GdkDragContext* context, 372 GtkSelectionData* selection_data, 373 guint target_type, 374 guint time) { 375 const BookmarkNode* node = BookmarkNodeForWidget(widget); 376 WriteBookmarkToSelection( 377 node, selection_data, target_type, browser_->profile()); 378} 379