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
SetImageMenuItem(GtkWidget * menu_item,const BookmarkNode * node,BookmarkModel * model)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
GetNodeFromMenuItem(GtkWidget * menu_item)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
GetParentNodeFromEmptyMenu(GtkWidget * menu)47 const BookmarkNode* GetParentNodeFromEmptyMenu(GtkWidget* menu) {
48 return static_cast<const BookmarkNode*>(
49 g_object_get_data(G_OBJECT(menu), "parent-node"));
50 }
51
AsVoid(const BookmarkNode * node)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.)
OnContextMenuHide(GtkWidget * context_menu,GtkWidget * grab_menu)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
BookmarkMenuController(Browser * browser,Profile * profile,PageNavigator * navigator,GtkWindow * window,const BookmarkNode * node,int start_child_index)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
~BookmarkMenuController()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
Popup(GtkWidget * widget,gint button_type,guint32 timestamp)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
BookmarkModelChanged()112 void BookmarkMenuController::BookmarkModelChanged() {
113 gtk_menu_popdown(GTK_MENU(menu_));
114 }
115
BookmarkNodeFaviconLoaded(BookmarkModel * model,const BookmarkNode * node)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
WillExecuteCommand()124 void BookmarkMenuController::WillExecuteCommand() {
125 gtk_menu_popdown(GTK_MENU(menu_));
126 }
127
CloseMenu()128 void BookmarkMenuController::CloseMenu() {
129 context_menu_->Cancel();
130 }
131
NavigateToMenuItem(GtkWidget * menu_item,WindowOpenDisposition disposition)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
BuildMenu(const BookmarkNode * parent,int start_child_index,GtkWidget * menu)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
OnMenuButtonPressedOrReleased(GtkWidget * sender,GdkEventButton * event)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
OnButtonReleased(GtkWidget * sender,GdkEventButton * event)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
OnFolderButtonPressed(GtkWidget * sender,GdkEventButton * event)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
OnMenuHidden(GtkWidget * menu)330 void BookmarkMenuController::OnMenuHidden(GtkWidget* menu) {
331 if (triggering_widget_)
332 gtk_chrome_button_unset_paint_state(GTK_CHROME_BUTTON(triggering_widget_));
333 }
334
OnMenuItemActivated(GtkWidget * menu_item)335 void BookmarkMenuController::OnMenuItemActivated(GtkWidget* menu_item) {
336 NavigateToMenuItem(menu_item, CURRENT_TAB);
337 }
338
OnMenuItemDragBegin(GtkWidget * menu_item,GdkDragContext * drag_context)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
OnMenuItemDragEnd(GtkWidget * menu_item,GdkDragContext * drag_context)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
OnMenuItemDragGet(GtkWidget * widget,GdkDragContext * context,GtkSelectionData * selection_data,guint target_type,guint time)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