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