| // Copyright (c) 2010 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/gtk/bookmark_menu_controller_gtk.h" |
| |
| #include <gtk/gtk.h> |
| |
| #include "app/gtk_dnd_util.h" |
| #include "app/l10n_util.h" |
| #include "base/string_util.h" |
| #include "base/utf_string_conversions.h" |
| #include "chrome/browser/bookmarks/bookmark_model.h" |
| #include "chrome/browser/bookmarks/bookmark_utils.h" |
| #include "chrome/browser/gtk/bookmark_utils_gtk.h" |
| #include "chrome/browser/gtk/gtk_chrome_button.h" |
| #include "chrome/browser/gtk/gtk_theme_provider.h" |
| #include "chrome/browser/gtk/gtk_util.h" |
| #include "chrome/browser/gtk/menu_gtk.h" |
| #include "chrome/browser/profile.h" |
| #include "chrome/browser/tab_contents/page_navigator.h" |
| #include "gfx/gtk_util.h" |
| #include "grit/app_resources.h" |
| #include "grit/generated_resources.h" |
| #include "grit/theme_resources.h" |
| #include "webkit/glue/window_open_disposition.h" |
| |
| namespace { |
| |
| // TODO(estade): It might be a good idea to vary this by locale. |
| const int kMaxChars = 50; |
| |
| void SetImageMenuItem(GtkWidget* menu_item, |
| const BookmarkNode* node, |
| BookmarkModel* model) { |
| GdkPixbuf* pixbuf = bookmark_utils::GetPixbufForNode(node, model, true); |
| gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(menu_item), |
| gtk_image_new_from_pixbuf(pixbuf)); |
| g_object_unref(pixbuf); |
| } |
| |
| const BookmarkNode* GetNodeFromMenuItem(GtkWidget* menu_item) { |
| return static_cast<const BookmarkNode*>( |
| g_object_get_data(G_OBJECT(menu_item), "bookmark-node")); |
| } |
| |
| const BookmarkNode* GetParentNodeFromEmptyMenu(GtkWidget* menu) { |
| return static_cast<const BookmarkNode*>( |
| g_object_get_data(G_OBJECT(menu), "parent-node")); |
| } |
| |
| void* AsVoid(const BookmarkNode* node) { |
| return const_cast<BookmarkNode*>(node); |
| } |
| |
| // The context menu has been dismissed, restore the X and application grabs |
| // to whichever menu last had them. (Assuming that menu is still showing.) |
| void OnContextMenuHide(GtkWidget* context_menu, GtkWidget* grab_menu) { |
| gtk_util::GrabAllInput(grab_menu); |
| |
| // Match the ref we took when connecting this signal. |
| g_object_unref(grab_menu); |
| } |
| |
| } // namespace |
| |
| BookmarkMenuController::BookmarkMenuController(Browser* browser, |
| Profile* profile, |
| PageNavigator* navigator, |
| GtkWindow* window, |
| const BookmarkNode* node, |
| int start_child_index) |
| : browser_(browser), |
| profile_(profile), |
| page_navigator_(navigator), |
| parent_window_(window), |
| model_(profile->GetBookmarkModel()), |
| node_(node), |
| drag_icon_(NULL), |
| ignore_button_release_(false), |
| triggering_widget_(NULL) { |
| menu_ = gtk_menu_new(); |
| g_object_ref_sink(menu_); |
| BuildMenu(node, start_child_index, menu_); |
| signals_.Connect(menu_, "hide", |
| G_CALLBACK(OnMenuHiddenThunk), this); |
| gtk_widget_show_all(menu_); |
| } |
| |
| BookmarkMenuController::~BookmarkMenuController() { |
| profile_->GetBookmarkModel()->RemoveObserver(this); |
| gtk_widget_destroy(menu_); |
| g_object_unref(menu_); |
| } |
| |
| void BookmarkMenuController::Popup(GtkWidget* widget, gint button_type, |
| guint32 timestamp) { |
| profile_->GetBookmarkModel()->AddObserver(this); |
| |
| triggering_widget_ = widget; |
| signals_.Connect(triggering_widget_, "destroy", |
| G_CALLBACK(gtk_widget_destroyed), &triggering_widget_); |
| gtk_chrome_button_set_paint_state(GTK_CHROME_BUTTON(widget), |
| GTK_STATE_ACTIVE); |
| gtk_menu_popup(GTK_MENU(menu_), NULL, NULL, |
| &MenuGtk::WidgetMenuPositionFunc, |
| widget, button_type, timestamp); |
| } |
| |
| void BookmarkMenuController::BookmarkModelChanged() { |
| gtk_menu_popdown(GTK_MENU(menu_)); |
| } |
| |
| void BookmarkMenuController::BookmarkNodeFavIconLoaded( |
| BookmarkModel* model, const BookmarkNode* node) { |
| std::map<const BookmarkNode*, GtkWidget*>::iterator it = |
| node_to_menu_widget_map_.find(node); |
| if (it != node_to_menu_widget_map_.end()) |
| SetImageMenuItem(it->second, node, model); |
| } |
| |
| void BookmarkMenuController::WillExecuteCommand() { |
| gtk_menu_popdown(GTK_MENU(menu_)); |
| } |
| |
| void BookmarkMenuController::CloseMenu() { |
| context_menu_->Cancel(); |
| } |
| |
| void BookmarkMenuController::NavigateToMenuItem( |
| GtkWidget* menu_item, |
| WindowOpenDisposition disposition) { |
| const BookmarkNode* node = GetNodeFromMenuItem(menu_item); |
| DCHECK(node); |
| DCHECK(page_navigator_); |
| page_navigator_->OpenURL( |
| node->GetURL(), GURL(), disposition, PageTransition::AUTO_BOOKMARK); |
| } |
| |
| void BookmarkMenuController::BuildMenu(const BookmarkNode* parent, |
| int start_child_index, |
| GtkWidget* menu) { |
| DCHECK(!parent->GetChildCount() || |
| start_child_index < parent->GetChildCount()); |
| |
| signals_.Connect(menu, "button-press-event", |
| G_CALLBACK(OnMenuButtonPressedOrReleasedThunk), this); |
| signals_.Connect(menu, "button-release-event", |
| G_CALLBACK(OnMenuButtonPressedOrReleasedThunk), this); |
| |
| for (int i = start_child_index; i < parent->GetChildCount(); ++i) { |
| const BookmarkNode* node = parent->GetChild(i); |
| |
| // This breaks on word boundaries. Ideally we would break on character |
| // boundaries. |
| std::string elided_name = WideToUTF8( |
| l10n_util::TruncateString(UTF16ToWideHack(node->GetTitle()), |
| kMaxChars)); |
| GtkWidget* menu_item = |
| gtk_image_menu_item_new_with_label(elided_name.c_str()); |
| g_object_set_data(G_OBJECT(menu_item), "bookmark-node", AsVoid(node)); |
| SetImageMenuItem(menu_item, node, profile_->GetBookmarkModel()); |
| gtk_util::SetAlwaysShowImage(menu_item); |
| |
| signals_.Connect(menu_item, "button-release-event", |
| G_CALLBACK(OnButtonReleasedThunk), this); |
| if (node->is_url()) { |
| signals_.Connect(menu_item, "activate", |
| G_CALLBACK(OnMenuItemActivatedThunk), this); |
| } else if (node->is_folder()) { |
| GtkWidget* submenu = gtk_menu_new(); |
| BuildMenu(node, 0, submenu); |
| gtk_menu_item_set_submenu(GTK_MENU_ITEM(menu_item), submenu); |
| } else { |
| NOTREACHED(); |
| } |
| |
| gtk_drag_source_set(menu_item, GDK_BUTTON1_MASK, NULL, 0, |
| static_cast<GdkDragAction>(GDK_ACTION_COPY | GDK_ACTION_LINK)); |
| int target_mask = gtk_dnd_util::CHROME_BOOKMARK_ITEM; |
| if (node->is_url()) |
| target_mask |= gtk_dnd_util::TEXT_URI_LIST | gtk_dnd_util::NETSCAPE_URL; |
| gtk_dnd_util::SetSourceTargetListFromCodeMask(menu_item, target_mask); |
| signals_.Connect(menu_item, "drag-begin", |
| G_CALLBACK(OnMenuItemDragBeginThunk), this); |
| signals_.Connect(menu_item, "drag-end", |
| G_CALLBACK(OnMenuItemDragEndThunk), this); |
| signals_.Connect(menu_item, "drag-data-get", |
| G_CALLBACK(OnMenuItemDragGetThunk), this); |
| |
| // It is important to connect to this signal after setting up the drag |
| // source because we only want to stifle the menu's default handler and |
| // not the handler that the drag source uses. |
| if (node->is_folder()) { |
| signals_.Connect(menu_item, "button-press-event", |
| G_CALLBACK(OnFolderButtonPressedThunk), this); |
| } |
| |
| gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item); |
| node_to_menu_widget_map_[node] = menu_item; |
| } |
| |
| if (parent->GetChildCount() == 0) { |
| GtkWidget* empty_menu = gtk_menu_item_new_with_label( |
| l10n_util::GetStringUTF8(IDS_MENU_EMPTY_SUBMENU).c_str()); |
| gtk_widget_set_sensitive(empty_menu, FALSE); |
| g_object_set_data(G_OBJECT(menu), "parent-node", AsVoid(parent)); |
| gtk_menu_shell_append(GTK_MENU_SHELL(menu), empty_menu); |
| } |
| } |
| |
| gboolean BookmarkMenuController::OnMenuButtonPressedOrReleased( |
| GtkWidget* sender, |
| GdkEventButton* event) { |
| // Handle middle mouse downs and right mouse ups. |
| if (!((event->button == 2 && event->type == GDK_BUTTON_RELEASE) || |
| (event->button == 3 && event->type == GDK_BUTTON_PRESS))) { |
| return FALSE; |
| } |
| |
| ignore_button_release_ = false; |
| GtkMenuShell* menu_shell = GTK_MENU_SHELL(sender); |
| // If the cursor is outside our bounds, pass this event up to the parent. |
| if (!gtk_util::WidgetContainsCursor(sender)) { |
| if (menu_shell->parent_menu_shell) { |
| return OnMenuButtonPressedOrReleased(menu_shell->parent_menu_shell, |
| event); |
| } else { |
| // We are the top level menu; we can propagate no further. |
| return FALSE; |
| } |
| } |
| |
| // This will return NULL if we are not an empty menu. |
| const BookmarkNode* parent = GetParentNodeFromEmptyMenu(sender); |
| bool is_empty_menu = !!parent; |
| // If there is no active menu item and we are not an empty menu, then do |
| // nothing. This can happen if the user has canceled a context menu while |
| // the cursor is hovering over a bookmark menu. Doing nothing is not optimal |
| // (the hovered item should be active), but it's a hopefully rare corner |
| // case. |
| GtkWidget* menu_item = menu_shell->active_menu_item; |
| if (!is_empty_menu && !menu_item) |
| return TRUE; |
| const BookmarkNode* node = |
| menu_item ? GetNodeFromMenuItem(menu_item) : NULL; |
| |
| if (event->button == 2 && node && node->is_folder()) { |
| bookmark_utils::OpenAll(parent_window_, |
| profile_, page_navigator_, |
| node, NEW_BACKGROUND_TAB); |
| gtk_menu_popdown(GTK_MENU(menu_)); |
| return TRUE; |
| } else if (event->button == 3) { |
| DCHECK_NE(is_empty_menu, !!node); |
| if (!is_empty_menu) |
| parent = node->GetParent(); |
| |
| // Show the right click menu and stop processing this button event. |
| std::vector<const BookmarkNode*> nodes; |
| if (node) |
| nodes.push_back(node); |
| context_menu_controller_.reset( |
| new BookmarkContextMenuController( |
| parent_window_, this, profile_, |
| page_navigator_, parent, nodes)); |
| context_menu_.reset( |
| new MenuGtk(NULL, context_menu_controller_->menu_model())); |
| |
| // Our bookmark folder menu loses the grab to the context menu. When the |
| // context menu is hidden, re-assert our grab. |
| GtkWidget* grabbing_menu = gtk_grab_get_current(); |
| g_object_ref(grabbing_menu); |
| signals_.Connect(context_menu_->widget(), "hide", |
| G_CALLBACK(OnContextMenuHide), grabbing_menu); |
| |
| context_menu_->PopupAsContext(event->time); |
| return TRUE; |
| } |
| |
| return FALSE; |
| } |
| |
| gboolean BookmarkMenuController::OnButtonReleased( |
| GtkWidget* sender, |
| GdkEventButton* event) { |
| if (ignore_button_release_) { |
| // Don't handle this message; it was a drag. |
| ignore_button_release_ = false; |
| return FALSE; |
| } |
| |
| // Releasing either button 1 or 2 should trigger the bookmark. |
| if (!gtk_menu_item_get_submenu(GTK_MENU_ITEM(sender))) { |
| // The menu item is a link node. |
| if (event->button == 1 || event->button == 2) { |
| WindowOpenDisposition disposition = |
| event_utils::DispositionFromEventFlags(event->state); |
| NavigateToMenuItem(sender, disposition); |
| |
| // We need to manually dismiss the popup menu because we're overriding |
| // button-release-event. |
| gtk_menu_popdown(GTK_MENU(menu_)); |
| return TRUE; |
| } |
| } else { |
| // The menu item is a folder node. |
| if (event->button == 1) { |
| // Having overriden the normal handling, we need to manually activate |
| // the item. |
| gtk_menu_shell_select_item(GTK_MENU_SHELL(sender->parent), sender); |
| g_signal_emit_by_name(sender->parent, "activate-current"); |
| return TRUE; |
| } |
| } |
| |
| return FALSE; |
| } |
| |
| gboolean BookmarkMenuController::OnFolderButtonPressed( |
| GtkWidget* sender, GdkEventButton* event) { |
| // The button press may start a drag; don't let the default handler run. |
| if (event->button == 1) |
| return TRUE; |
| return FALSE; |
| } |
| |
| void BookmarkMenuController::OnMenuHidden(GtkWidget* menu) { |
| if (triggering_widget_) |
| gtk_chrome_button_unset_paint_state(GTK_CHROME_BUTTON(triggering_widget_)); |
| } |
| |
| void BookmarkMenuController::OnMenuItemActivated(GtkWidget* menu_item) { |
| NavigateToMenuItem(menu_item, CURRENT_TAB); |
| } |
| |
| void BookmarkMenuController::OnMenuItemDragBegin(GtkWidget* menu_item, |
| GdkDragContext* drag_context) { |
| // The parent menu item might be removed during the drag. Ref it so |button| |
| // won't get destroyed. |
| g_object_ref(menu_item->parent); |
| |
| // Signal to any future OnButtonReleased calls that we're dragging instead of |
| // pressing. |
| ignore_button_release_ = true; |
| |
| const BookmarkNode* node = bookmark_utils::BookmarkNodeForWidget(menu_item); |
| drag_icon_ = bookmark_utils::GetDragRepresentationForNode( |
| node, model_, GtkThemeProvider::GetFrom(profile_)); |
| gint x, y; |
| gtk_widget_get_pointer(menu_item, &x, &y); |
| gtk_drag_set_icon_widget(drag_context, drag_icon_, x, y); |
| |
| // Hide our node. |
| gtk_widget_hide(menu_item); |
| } |
| |
| void BookmarkMenuController::OnMenuItemDragEnd(GtkWidget* menu_item, |
| GdkDragContext* drag_context) { |
| gtk_widget_show(menu_item); |
| g_object_unref(menu_item->parent); |
| |
| gtk_widget_destroy(drag_icon_); |
| drag_icon_ = NULL; |
| } |
| |
| void BookmarkMenuController::OnMenuItemDragGet( |
| GtkWidget* widget, GdkDragContext* context, |
| GtkSelectionData* selection_data, |
| guint target_type, guint time) { |
| const BookmarkNode* node = bookmark_utils::BookmarkNodeForWidget(widget); |
| bookmark_utils::WriteBookmarkToSelection(node, selection_data, target_type, |
| profile_); |
| } |