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