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
28namespace {
29
30// TODO(estade): It might be a good idea to vary this by locale.
31const int kMaxChars = 50;
32
33void 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
42const BookmarkNode* GetNodeFromMenuItem(GtkWidget* menu_item) {
43  return static_cast<const BookmarkNode*>(
44      g_object_get_data(G_OBJECT(menu_item), "bookmark-node"));
45}
46
47const BookmarkNode* GetParentNodeFromEmptyMenu(GtkWidget* menu) {
48  return static_cast<const BookmarkNode*>(
49      g_object_get_data(G_OBJECT(menu), "parent-node"));
50}
51
52void* 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.)
58void 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
67BookmarkMenuController::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
90BookmarkMenuController::~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
98void 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
112void BookmarkMenuController::BookmarkModelChanged() {
113  gtk_menu_popdown(GTK_MENU(menu_));
114}
115
116void 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
124void BookmarkMenuController::WillExecuteCommand() {
125  gtk_menu_popdown(GTK_MENU(menu_));
126}
127
128void BookmarkMenuController::CloseMenu() {
129  context_menu_->Cancel();
130}
131
132void 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
142void 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
213gboolean 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
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::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
322gboolean 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
330void BookmarkMenuController::OnMenuHidden(GtkWidget* menu) {
331  if (triggering_widget_)
332    gtk_chrome_button_unset_paint_state(GTK_CHROME_BUTTON(triggering_widget_));
333}
334
335void BookmarkMenuController::OnMenuItemActivated(GtkWidget* menu_item) {
336  NavigateToMenuItem(menu_item, CURRENT_TAB);
337}
338
339void 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
360void 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
369void 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