Home | History | Annotate | Download | only in bookmarks
      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