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/menu_gtk.h"
6
7 #include <map>
8
9 #include "base/bind.h"
10 #include "base/i18n/rtl.h"
11 #include "base/logging.h"
12 #include "base/message_loop/message_loop.h"
13 #include "base/stl_util.h"
14 #include "base/strings/utf_string_conversions.h"
15 #include "chrome/app/chrome_command_ids.h"
16 #include "chrome/browser/ui/gtk/event_utils.h"
17 #include "chrome/browser/ui/gtk/gtk_custom_menu.h"
18 #include "chrome/browser/ui/gtk/gtk_custom_menu_item.h"
19 #include "chrome/browser/ui/gtk/gtk_util.h"
20 #include "third_party/skia/include/core/SkBitmap.h"
21 #include "ui/base/accelerators/menu_label_accelerator_util_linux.h"
22 #include "ui/base/accelerators/platform_accelerator_gtk.h"
23 #include "ui/base/models/button_menu_item_model.h"
24 #include "ui/base/models/menu_model.h"
25 #include "ui/base/window_open_disposition.h"
26 #include "ui/gfx/gtk_util.h"
27 #include "ui/gfx/image/image.h"
28
29 bool MenuGtk::block_activation_ = false;
30
31 namespace {
32
33 // Sets the ID of a menu item.
SetMenuItemID(GtkWidget * menu_item,int menu_id)34 void SetMenuItemID(GtkWidget* menu_item, int menu_id) {
35 DCHECK_GE(menu_id, 0);
36
37 // Add 1 to the menu_id to avoid setting zero (null) to "menu-id".
38 g_object_set_data(G_OBJECT(menu_item), "menu-id",
39 GINT_TO_POINTER(menu_id + 1));
40 }
41
42 // Gets the ID of a menu item.
43 // Returns true if the menu item has an ID.
GetMenuItemID(GtkWidget * menu_item,int * menu_id)44 bool GetMenuItemID(GtkWidget* menu_item, int* menu_id) {
45 gpointer id_ptr = g_object_get_data(G_OBJECT(menu_item), "menu-id");
46 if (id_ptr != NULL) {
47 *menu_id = GPOINTER_TO_INT(id_ptr) - 1;
48 return true;
49 }
50
51 return false;
52 }
53
ModelForMenuItem(GtkMenuItem * menu_item)54 ui::MenuModel* ModelForMenuItem(GtkMenuItem* menu_item) {
55 return reinterpret_cast<ui::MenuModel*>(
56 g_object_get_data(G_OBJECT(menu_item), "model"));
57 }
58
SetUpButtonShowHandler(GtkWidget * button,ui::ButtonMenuItemModel * model,int index)59 void SetUpButtonShowHandler(GtkWidget* button,
60 ui::ButtonMenuItemModel* model,
61 int index) {
62 g_object_set_data(G_OBJECT(button), "button-model",
63 model);
64 g_object_set_data(G_OBJECT(button), "button-model-id",
65 GINT_TO_POINTER(index));
66 }
67
OnSubmenuShowButtonImage(GtkWidget * widget,GtkButton * button)68 void OnSubmenuShowButtonImage(GtkWidget* widget, GtkButton* button) {
69 MenuGtk::Delegate* delegate = reinterpret_cast<MenuGtk::Delegate*>(
70 g_object_get_data(G_OBJECT(button), "menu-gtk-delegate"));
71 int icon_idr = GPOINTER_TO_INT(g_object_get_data(
72 G_OBJECT(button), "button-image-idr"));
73
74 GtkIconSet* icon_set = delegate->GetIconSetForId(icon_idr);
75 if (icon_set) {
76 gtk_button_set_image(
77 button, gtk_image_new_from_icon_set(icon_set,
78 GTK_ICON_SIZE_MENU));
79 }
80 }
81
SetupImageIcon(GtkWidget * button,GtkWidget * menu,int icon_idr,MenuGtk::Delegate * menu_gtk_delegate)82 void SetupImageIcon(GtkWidget* button,
83 GtkWidget* menu,
84 int icon_idr,
85 MenuGtk::Delegate* menu_gtk_delegate) {
86 g_object_set_data(G_OBJECT(button), "button-image-idr",
87 GINT_TO_POINTER(icon_idr));
88 g_object_set_data(G_OBJECT(button), "menu-gtk-delegate",
89 menu_gtk_delegate);
90
91 g_signal_connect(menu, "show", G_CALLBACK(OnSubmenuShowButtonImage), button);
92 }
93
94 // Popup menus may get squished if they open up too close to the bottom of the
95 // screen. This function takes the size of the screen, the size of the menu,
96 // an optional widget, the Y position of the mouse click, and adjusts the popup
97 // menu's Y position to make it fit if it's possible to do so.
98 // Returns the new Y position of the popup menu.
CalculateMenuYPosition(const GdkRectangle * screen_rect,const GtkRequisition * menu_req,GtkWidget * widget,const int y)99 int CalculateMenuYPosition(const GdkRectangle* screen_rect,
100 const GtkRequisition* menu_req,
101 GtkWidget* widget, const int y) {
102 CHECK(screen_rect);
103 CHECK(menu_req);
104 // If the menu would run off the bottom of the screen, and there is enough
105 // screen space upwards to accommodate the menu, then pop upwards. If there
106 // is a widget, then also move the anchor point to the top of the widget
107 // rather than the bottom.
108 const int screen_top = screen_rect->y;
109 const int screen_bottom = screen_rect->y + screen_rect->height;
110 const int menu_bottom = y + menu_req->height;
111 int alternate_y = y - menu_req->height;
112 if (widget) {
113 GtkAllocation allocation;
114 gtk_widget_get_allocation(widget, &allocation);
115 alternate_y -= allocation.height;
116 }
117 if (menu_bottom >= screen_bottom && alternate_y >= screen_top)
118 return alternate_y;
119 return y;
120 }
121
122 } // namespace
123
AlwaysShowIconForCmd(int command_id) const124 bool MenuGtk::Delegate::AlwaysShowIconForCmd(int command_id) const {
125 return false;
126 }
127
GetIconSetForId(int idr)128 GtkIconSet* MenuGtk::Delegate::GetIconSetForId(int idr) { return NULL; }
129
GetDefaultImageForCommandId(int command_id)130 GtkWidget* MenuGtk::Delegate::GetDefaultImageForCommandId(int command_id) {
131 const char* stock;
132 switch (command_id) {
133 case IDC_NEW_TAB:
134 case IDC_CONTENT_CONTEXT_OPENIMAGENEWTAB:
135 case IDC_CONTENT_CONTEXT_SEARCHWEBFORIMAGE:
136 case IDC_CONTENT_CONTEXT_OPENLINKNEWTAB:
137 case IDC_CONTENT_CONTEXT_OPENAVNEWTAB:
138 stock = GTK_STOCK_NEW;
139 break;
140
141 case IDC_CLOSE_TAB:
142 stock = GTK_STOCK_CLOSE;
143 break;
144
145 case IDC_CONTENT_CONTEXT_SAVEIMAGEAS:
146 case IDC_CONTENT_CONTEXT_SAVEAVAS:
147 case IDC_CONTENT_CONTEXT_SAVELINKAS:
148 stock = GTK_STOCK_SAVE_AS;
149 break;
150
151 case IDC_SAVE_PAGE:
152 stock = GTK_STOCK_SAVE;
153 break;
154
155 case IDC_COPY:
156 case IDC_CONTENT_CONTEXT_COPYIMAGELOCATION:
157 case IDC_CONTENT_CONTEXT_COPYLINKLOCATION:
158 case IDC_CONTENT_CONTEXT_COPYAVLOCATION:
159 case IDC_CONTENT_CONTEXT_COPYEMAILADDRESS:
160 case IDC_CONTENT_CONTEXT_COPY:
161 stock = GTK_STOCK_COPY;
162 break;
163
164 case IDC_CUT:
165 case IDC_CONTENT_CONTEXT_CUT:
166 stock = GTK_STOCK_CUT;
167 break;
168
169 case IDC_PASTE:
170 case IDC_CONTENT_CONTEXT_PASTE:
171 stock = GTK_STOCK_PASTE;
172 break;
173
174 case IDC_CONTENT_CONTEXT_DELETE:
175 case IDC_BOOKMARK_BAR_REMOVE:
176 stock = GTK_STOCK_DELETE;
177 break;
178
179 case IDC_CONTENT_CONTEXT_UNDO:
180 stock = GTK_STOCK_UNDO;
181 break;
182
183 case IDC_CONTENT_CONTEXT_REDO:
184 stock = GTK_STOCK_REDO;
185 break;
186
187 case IDC_SEARCH:
188 case IDC_FIND:
189 case IDC_CONTENT_CONTEXT_SEARCHWEBFOR:
190 stock = GTK_STOCK_FIND;
191 break;
192
193 case IDC_CONTENT_CONTEXT_SELECTALL:
194 stock = GTK_STOCK_SELECT_ALL;
195 break;
196
197 case IDC_CLEAR_BROWSING_DATA:
198 stock = GTK_STOCK_CLEAR;
199 break;
200
201 case IDC_BACK:
202 stock = GTK_STOCK_GO_BACK;
203 break;
204
205 case IDC_RELOAD:
206 stock = GTK_STOCK_REFRESH;
207 break;
208
209 case IDC_FORWARD:
210 stock = GTK_STOCK_GO_FORWARD;
211 break;
212
213 case IDC_PRINT:
214 stock = GTK_STOCK_PRINT;
215 break;
216
217 case IDC_CONTENT_CONTEXT_VIEWPAGEINFO:
218 stock = GTK_STOCK_INFO;
219 break;
220
221 case IDC_SPELLCHECK_MENU:
222 stock = GTK_STOCK_SPELL_CHECK;
223 break;
224
225 case IDC_RESTORE_TAB:
226 stock = GTK_STOCK_UNDELETE;
227 break;
228
229 case IDC_HOME:
230 stock = GTK_STOCK_HOME;
231 break;
232
233 case IDC_STOP:
234 stock = GTK_STOCK_STOP;
235 break;
236
237 case IDC_ABOUT:
238 stock = GTK_STOCK_ABOUT;
239 break;
240
241 case IDC_EXIT:
242 stock = GTK_STOCK_QUIT;
243 break;
244
245 case IDC_HELP_PAGE_VIA_MENU:
246 stock = GTK_STOCK_HELP;
247 break;
248
249 case IDC_OPTIONS:
250 stock = GTK_STOCK_PREFERENCES;
251 break;
252
253 case IDC_CONTENT_CONTEXT_GOTOURL:
254 stock = GTK_STOCK_JUMP_TO;
255 break;
256
257 case IDC_DEV_TOOLS_INSPECT:
258 case IDC_CONTENT_CONTEXT_INSPECTELEMENT:
259 stock = GTK_STOCK_PROPERTIES;
260 break;
261
262 case IDC_BOOKMARK_BAR_ADD_NEW_BOOKMARK:
263 stock = GTK_STOCK_ADD;
264 break;
265
266 case IDC_BOOKMARK_BAR_RENAME_FOLDER:
267 case IDC_BOOKMARK_BAR_EDIT:
268 stock = GTK_STOCK_EDIT;
269 break;
270
271 case IDC_BOOKMARK_BAR_NEW_FOLDER:
272 stock = GTK_STOCK_DIRECTORY;
273 break;
274
275 case IDC_BOOKMARK_BAR_OPEN_ALL:
276 stock = GTK_STOCK_OPEN;
277 break;
278
279 default:
280 stock = NULL;
281 }
282
283 return stock ? gtk_image_new_from_stock(stock, GTK_ICON_SIZE_MENU) : NULL;
284 }
285
GetImageForCommandId(int command_id) const286 GtkWidget* MenuGtk::Delegate::GetImageForCommandId(int command_id) const {
287 return GetDefaultImageForCommandId(command_id);
288 }
289
MenuGtk(MenuGtk::Delegate * delegate,ui::MenuModel * model)290 MenuGtk::MenuGtk(MenuGtk::Delegate* delegate,
291 ui::MenuModel* model)
292 : delegate_(delegate),
293 model_(model),
294 dummy_accel_group_(gtk_accel_group_new()),
295 menu_(gtk_custom_menu_new()),
296 weak_factory_(this) {
297 DCHECK(model);
298 g_object_ref_sink(menu_);
299 ConnectSignalHandlers();
300 BuildMenuFromModel();
301 }
302
~MenuGtk()303 MenuGtk::~MenuGtk() {
304 Cancel();
305
306 gtk_widget_destroy(menu_);
307 g_object_unref(menu_);
308
309 g_object_unref(dummy_accel_group_);
310 }
311
ConnectSignalHandlers()312 void MenuGtk::ConnectSignalHandlers() {
313 // We connect afterwards because OnMenuShow calls SetMenuItemInfo, which may
314 // take a long time or even start a nested message loop.
315 g_signal_connect(menu_, "show", G_CALLBACK(OnMenuShowThunk), this);
316 g_signal_connect(menu_, "hide", G_CALLBACK(OnMenuHiddenThunk), this);
317 GtkWidget* toplevel_window = gtk_widget_get_toplevel(menu_);
318 signal_.Connect(toplevel_window, "focus-out-event",
319 G_CALLBACK(OnMenuFocusOutThunk), this);
320 }
321
AppendMenuItemWithLabel(int command_id,const std::string & label)322 GtkWidget* MenuGtk::AppendMenuItemWithLabel(int command_id,
323 const std::string& label) {
324 std::string converted_label = ui::ConvertAcceleratorsFromWindowsStyle(label);
325 GtkWidget* menu_item = BuildMenuItemWithLabel(converted_label, command_id);
326 return AppendMenuItem(command_id, menu_item);
327 }
328
AppendMenuItemWithIcon(int command_id,const std::string & label,const gfx::Image & icon)329 GtkWidget* MenuGtk::AppendMenuItemWithIcon(int command_id,
330 const std::string& label,
331 const gfx::Image& icon) {
332 std::string converted_label = ui::ConvertAcceleratorsFromWindowsStyle(label);
333 GtkWidget* menu_item = BuildMenuItemWithImage(converted_label, icon);
334 return AppendMenuItem(command_id, menu_item);
335 }
336
AppendCheckMenuItemWithLabel(int command_id,const std::string & label)337 GtkWidget* MenuGtk::AppendCheckMenuItemWithLabel(int command_id,
338 const std::string& label) {
339 std::string converted_label = ui::ConvertAcceleratorsFromWindowsStyle(label);
340 GtkWidget* menu_item =
341 gtk_check_menu_item_new_with_mnemonic(converted_label.c_str());
342 return AppendMenuItem(command_id, menu_item);
343 }
344
AppendSeparator()345 GtkWidget* MenuGtk::AppendSeparator() {
346 GtkWidget* menu_item = gtk_separator_menu_item_new();
347 gtk_widget_show(menu_item);
348 gtk_menu_shell_append(GTK_MENU_SHELL(menu_), menu_item);
349 return menu_item;
350 }
351
InsertSeparator(int position)352 GtkWidget* MenuGtk::InsertSeparator(int position) {
353 GtkWidget* menu_item = gtk_separator_menu_item_new();
354 gtk_widget_show(menu_item);
355 gtk_menu_shell_insert(GTK_MENU_SHELL(menu_), menu_item, position);
356 return menu_item;
357 }
358
AppendMenuItem(int command_id,GtkWidget * menu_item)359 GtkWidget* MenuGtk::AppendMenuItem(int command_id, GtkWidget* menu_item) {
360 if (delegate_ && delegate_->AlwaysShowIconForCmd(command_id) &&
361 GTK_IS_IMAGE_MENU_ITEM(menu_item))
362 gtk_util::SetAlwaysShowImage(menu_item);
363
364 return AppendMenuItemToMenu(command_id, NULL, menu_item, menu_, true);
365 }
366
InsertMenuItem(int command_id,GtkWidget * menu_item,int position)367 GtkWidget* MenuGtk::InsertMenuItem(int command_id, GtkWidget* menu_item,
368 int position) {
369 if (delegate_ && delegate_->AlwaysShowIconForCmd(command_id) &&
370 GTK_IS_IMAGE_MENU_ITEM(menu_item))
371 gtk_util::SetAlwaysShowImage(menu_item);
372
373 return InsertMenuItemToMenu(command_id, NULL, menu_item, menu_, position,
374 true);
375 }
376
AppendMenuItemToMenu(int index,ui::MenuModel * model,GtkWidget * menu_item,GtkWidget * menu,bool connect_to_activate)377 GtkWidget* MenuGtk::AppendMenuItemToMenu(int index,
378 ui::MenuModel* model,
379 GtkWidget* menu_item,
380 GtkWidget* menu,
381 bool connect_to_activate) {
382 int children_count = g_list_length(GTK_MENU_SHELL(menu)->children);
383 return InsertMenuItemToMenu(index, model, menu_item, menu,
384 children_count, connect_to_activate);
385 }
386
InsertMenuItemToMenu(int index,ui::MenuModel * model,GtkWidget * menu_item,GtkWidget * menu,int position,bool connect_to_activate)387 GtkWidget* MenuGtk::InsertMenuItemToMenu(int index,
388 ui::MenuModel* model,
389 GtkWidget* menu_item,
390 GtkWidget* menu,
391 int position,
392 bool connect_to_activate) {
393 SetMenuItemID(menu_item, index);
394
395 // Native menu items do their own thing, so only selectively listen for the
396 // activate signal.
397 if (connect_to_activate) {
398 g_signal_connect(menu_item, "activate",
399 G_CALLBACK(OnMenuItemActivatedThunk), this);
400 }
401
402 // AppendMenuItemToMenu is used both internally when we control menu creation
403 // from a model (where the model can choose to hide certain menu items), and
404 // with immediate commands which don't provide the option.
405 if (model) {
406 if (model->IsVisibleAt(index))
407 gtk_widget_show(menu_item);
408 } else {
409 gtk_widget_show(menu_item);
410 }
411 gtk_menu_shell_insert(GTK_MENU_SHELL(menu), menu_item, position);
412 return menu_item;
413 }
414
PopupForWidget(GtkWidget * widget,int button,guint32 event_time)415 void MenuGtk::PopupForWidget(GtkWidget* widget, int button,
416 guint32 event_time) {
417 gtk_menu_popup(GTK_MENU(menu_), NULL, NULL,
418 WidgetMenuPositionFunc,
419 widget,
420 button, event_time);
421 }
422
PopupAsContext(const gfx::Point & point,guint32 event_time)423 void MenuGtk::PopupAsContext(const gfx::Point& point, guint32 event_time) {
424 // gtk_menu_popup doesn't like the "const" qualifier on point.
425 gfx::Point nonconst_point(point);
426 gtk_menu_popup(GTK_MENU(menu_), NULL, NULL,
427 PointMenuPositionFunc, &nonconst_point,
428 3, event_time);
429 }
430
PopupAsContextForStatusIcon(guint32 event_time,guint32 button,GtkStatusIcon * icon)431 void MenuGtk::PopupAsContextForStatusIcon(guint32 event_time, guint32 button,
432 GtkStatusIcon* icon) {
433 gtk_menu_popup(GTK_MENU(menu_), NULL, NULL, gtk_status_icon_position_menu,
434 icon, button, event_time);
435 }
436
PopupAsFromKeyEvent(GtkWidget * widget)437 void MenuGtk::PopupAsFromKeyEvent(GtkWidget* widget) {
438 PopupForWidget(widget, 0, gtk_get_current_event_time());
439 gtk_menu_shell_select_first(GTK_MENU_SHELL(menu_), FALSE);
440 }
441
Cancel()442 void MenuGtk::Cancel() {
443 gtk_menu_popdown(GTK_MENU(menu_));
444 }
445
UpdateMenu()446 void MenuGtk::UpdateMenu() {
447 gtk_container_foreach(GTK_CONTAINER(menu_), SetMenuItemInfo, this);
448 }
449
BuildMenuItemWithImage(const std::string & label,GtkWidget * image)450 GtkWidget* MenuGtk::BuildMenuItemWithImage(const std::string& label,
451 GtkWidget* image) {
452 GtkWidget* menu_item =
453 gtk_image_menu_item_new_with_mnemonic(label.c_str());
454 gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(menu_item), image);
455 return menu_item;
456 }
457
BuildMenuItemWithImage(const std::string & label,const gfx::Image & icon)458 GtkWidget* MenuGtk::BuildMenuItemWithImage(const std::string& label,
459 const gfx::Image& icon) {
460 GtkWidget* menu_item = BuildMenuItemWithImage(label,
461 gtk_image_new_from_pixbuf(icon.ToGdkPixbuf()));
462 return menu_item;
463 }
464
BuildMenuItemWithLabel(const std::string & label,int command_id)465 GtkWidget* MenuGtk::BuildMenuItemWithLabel(const std::string& label,
466 int command_id) {
467 GtkWidget* img =
468 delegate_ ? delegate_->GetImageForCommandId(command_id) :
469 MenuGtk::Delegate::GetDefaultImageForCommandId(command_id);
470 return img ? BuildMenuItemWithImage(label, img) :
471 gtk_menu_item_new_with_mnemonic(label.c_str());
472 }
473
BuildMenuFromModel()474 void MenuGtk::BuildMenuFromModel() {
475 BuildSubmenuFromModel(model_, menu_);
476 }
477
BuildSubmenuFromModel(ui::MenuModel * model,GtkWidget * menu)478 void MenuGtk::BuildSubmenuFromModel(ui::MenuModel* model, GtkWidget* menu) {
479 std::map<int, GtkWidget*> radio_groups;
480 GtkWidget* menu_item = NULL;
481 for (int i = 0; i < model->GetItemCount(); ++i) {
482 gfx::Image icon;
483 std::string label = ui::ConvertAcceleratorsFromWindowsStyle(
484 UTF16ToUTF8(model->GetLabelAt(i)));
485 bool connect_to_activate = true;
486
487 switch (model->GetTypeAt(i)) {
488 case ui::MenuModel::TYPE_SEPARATOR:
489 menu_item = gtk_separator_menu_item_new();
490 break;
491
492 case ui::MenuModel::TYPE_CHECK:
493 menu_item = gtk_check_menu_item_new_with_mnemonic(label.c_str());
494 break;
495
496 case ui::MenuModel::TYPE_RADIO: {
497 std::map<int, GtkWidget*>::iterator iter =
498 radio_groups.find(model->GetGroupIdAt(i));
499
500 if (iter == radio_groups.end()) {
501 menu_item = gtk_radio_menu_item_new_with_mnemonic(
502 NULL, label.c_str());
503 radio_groups[model->GetGroupIdAt(i)] = menu_item;
504 } else {
505 menu_item = gtk_radio_menu_item_new_with_mnemonic_from_widget(
506 GTK_RADIO_MENU_ITEM(iter->second), label.c_str());
507 }
508 break;
509 }
510 case ui::MenuModel::TYPE_BUTTON_ITEM: {
511 ui::ButtonMenuItemModel* button_menu_item_model =
512 model->GetButtonMenuItemAt(i);
513 menu_item = BuildButtonMenuItem(button_menu_item_model, menu);
514 connect_to_activate = false;
515 break;
516 }
517 case ui::MenuModel::TYPE_SUBMENU:
518 case ui::MenuModel::TYPE_COMMAND: {
519 int command_id = model->GetCommandIdAt(i);
520 if (model->GetIconAt(i, &icon))
521 menu_item = BuildMenuItemWithImage(label, icon);
522 else
523 menu_item = BuildMenuItemWithLabel(label, command_id);
524 if (delegate_ && delegate_->AlwaysShowIconForCmd(command_id) &&
525 GTK_IS_IMAGE_MENU_ITEM(menu_item)) {
526 gtk_util::SetAlwaysShowImage(menu_item);
527 }
528 break;
529 }
530
531 default:
532 NOTREACHED();
533 }
534
535 if (model->GetTypeAt(i) == ui::MenuModel::TYPE_SUBMENU) {
536 GtkWidget* submenu = gtk_menu_new();
537 g_object_set_data(G_OBJECT(submenu), "menu-item", menu_item);
538 ui::MenuModel* submenu_model = model->GetSubmenuModelAt(i);
539 g_object_set_data(G_OBJECT(menu_item), "submenu-model", submenu_model);
540 gtk_menu_item_set_submenu(GTK_MENU_ITEM(menu_item), submenu);
541 // We will populate the submenu on demand when shown.
542 g_signal_connect(submenu, "show", G_CALLBACK(OnSubMenuShowThunk), this);
543 g_signal_connect(submenu, "hide", G_CALLBACK(OnSubMenuHiddenThunk), this);
544 connect_to_activate = false;
545 }
546
547 ui::Accelerator accelerator;
548 if (model->GetAcceleratorAt(i, &accelerator)) {
549 gtk_widget_add_accelerator(menu_item,
550 "activate",
551 dummy_accel_group_,
552 ui::GetGdkKeyCodeForAccelerator(accelerator),
553 ui::GetGdkModifierForAccelerator(accelerator),
554 GTK_ACCEL_VISIBLE);
555 }
556
557 g_object_set_data(G_OBJECT(menu_item), "model", model);
558 AppendMenuItemToMenu(i, model, menu_item, menu, connect_to_activate);
559
560 menu_item = NULL;
561 }
562 }
563
BuildButtonMenuItem(ui::ButtonMenuItemModel * model,GtkWidget * menu)564 GtkWidget* MenuGtk::BuildButtonMenuItem(ui::ButtonMenuItemModel* model,
565 GtkWidget* menu) {
566 GtkWidget* menu_item = gtk_custom_menu_item_new(
567 ui::RemoveWindowsStyleAccelerators(UTF16ToUTF8(model->label())).c_str());
568
569 // Set up the callback to the model for when it is clicked.
570 g_object_set_data(G_OBJECT(menu_item), "button-model", model);
571 g_signal_connect(menu_item, "button-pushed",
572 G_CALLBACK(OnMenuButtonPressedThunk), this);
573 g_signal_connect(menu_item, "try-button-pushed",
574 G_CALLBACK(OnMenuTryButtonPressedThunk), this);
575
576 GtkSizeGroup* group = NULL;
577 for (int i = 0; i < model->GetItemCount(); ++i) {
578 GtkWidget* button = NULL;
579
580 switch (model->GetTypeAt(i)) {
581 case ui::ButtonMenuItemModel::TYPE_SPACE: {
582 gtk_custom_menu_item_add_space(GTK_CUSTOM_MENU_ITEM(menu_item));
583 break;
584 }
585 case ui::ButtonMenuItemModel::TYPE_BUTTON: {
586 button = gtk_custom_menu_item_add_button(
587 GTK_CUSTOM_MENU_ITEM(menu_item),
588 model->GetCommandIdAt(i));
589
590 int icon_idr;
591 if (model->GetIconAt(i, &icon_idr)) {
592 SetupImageIcon(button, menu, icon_idr, delegate_);
593 } else {
594 gtk_button_set_label(
595 GTK_BUTTON(button),
596 ui::RemoveWindowsStyleAccelerators(
597 UTF16ToUTF8(model->GetLabelAt(i))).c_str());
598 }
599
600 SetUpButtonShowHandler(button, model, i);
601 break;
602 }
603 case ui::ButtonMenuItemModel::TYPE_BUTTON_LABEL: {
604 button = gtk_custom_menu_item_add_button_label(
605 GTK_CUSTOM_MENU_ITEM(menu_item),
606 model->GetCommandIdAt(i));
607 gtk_button_set_label(
608 GTK_BUTTON(button),
609 ui::RemoveWindowsStyleAccelerators(
610 UTF16ToUTF8(model->GetLabelAt(i))).c_str());
611 SetUpButtonShowHandler(button, model, i);
612 break;
613 }
614 }
615
616 if (button && model->PartOfGroup(i)) {
617 if (!group)
618 group = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL);
619
620 gtk_size_group_add_widget(group, button);
621 }
622 }
623
624 if (group)
625 g_object_unref(group);
626
627 return menu_item;
628 }
629
OnMenuItemActivated(GtkWidget * menu_item)630 void MenuGtk::OnMenuItemActivated(GtkWidget* menu_item) {
631 if (block_activation_)
632 return;
633
634 ui::MenuModel* model = ModelForMenuItem(GTK_MENU_ITEM(menu_item));
635
636 if (!model) {
637 // There won't be a model for "native" submenus like the "Input Methods"
638 // context menu. We don't need to handle activation messages for submenus
639 // anyway, so we can just return here.
640 DCHECK(gtk_menu_item_get_submenu(GTK_MENU_ITEM(menu_item)));
641 return;
642 }
643
644 // The activate signal is sent to radio items as they get deselected;
645 // ignore it in this case.
646 if (GTK_IS_RADIO_MENU_ITEM(menu_item) &&
647 !gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(menu_item))) {
648 return;
649 }
650
651 int id;
652 if (!GetMenuItemID(menu_item, &id))
653 return;
654
655 // The menu item can still be activated by hotkeys even if it is disabled.
656 if (model->IsEnabledAt(id))
657 ExecuteCommand(model, id);
658 }
659
OnMenuButtonPressed(GtkWidget * menu_item,int command_id)660 void MenuGtk::OnMenuButtonPressed(GtkWidget* menu_item, int command_id) {
661 ui::ButtonMenuItemModel* model =
662 reinterpret_cast<ui::ButtonMenuItemModel*>(
663 g_object_get_data(G_OBJECT(menu_item), "button-model"));
664 if (model && model->IsCommandIdEnabled(command_id)) {
665 if (delegate_)
666 delegate_->CommandWillBeExecuted();
667
668 model->ActivatedCommand(command_id);
669 }
670 }
671
OnMenuTryButtonPressed(GtkWidget * menu_item,int command_id)672 gboolean MenuGtk::OnMenuTryButtonPressed(GtkWidget* menu_item,
673 int command_id) {
674 gboolean pressed = FALSE;
675 ui::ButtonMenuItemModel* model =
676 reinterpret_cast<ui::ButtonMenuItemModel*>(
677 g_object_get_data(G_OBJECT(menu_item), "button-model"));
678 if (model &&
679 model->IsCommandIdEnabled(command_id) &&
680 !model->DoesCommandIdDismissMenu(command_id)) {
681 if (delegate_)
682 delegate_->CommandWillBeExecuted();
683
684 model->ActivatedCommand(command_id);
685 pressed = TRUE;
686 }
687
688 return pressed;
689 }
690
691 // static
WidgetMenuPositionFunc(GtkMenu * menu,int * x,int * y,gboolean * push_in,void * void_widget)692 void MenuGtk::WidgetMenuPositionFunc(GtkMenu* menu,
693 int* x,
694 int* y,
695 gboolean* push_in,
696 void* void_widget) {
697 GtkWidget* widget = GTK_WIDGET(void_widget);
698 GtkRequisition menu_req;
699
700 gtk_widget_size_request(GTK_WIDGET(menu), &menu_req);
701
702 gdk_window_get_origin(gtk_widget_get_window(widget), x, y);
703 GdkScreen *screen = gtk_widget_get_screen(widget);
704 gint monitor = gdk_screen_get_monitor_at_point(screen, *x, *y);
705
706 GdkRectangle screen_rect;
707 gdk_screen_get_monitor_geometry(screen, monitor,
708 &screen_rect);
709
710 GtkAllocation allocation;
711 gtk_widget_get_allocation(widget, &allocation);
712
713 if (!gtk_widget_get_has_window(widget)) {
714 *x += allocation.x;
715 *y += allocation.y;
716 }
717 *y += allocation.height;
718
719 bool start_align =
720 !!g_object_get_data(G_OBJECT(widget), "left-align-popup");
721 if (base::i18n::IsRTL())
722 start_align = !start_align;
723
724 if (!start_align)
725 *x += allocation.width - menu_req.width;
726
727 *y = CalculateMenuYPosition(&screen_rect, &menu_req, widget, *y);
728
729 *push_in = FALSE;
730 }
731
732 // static
PointMenuPositionFunc(GtkMenu * menu,int * x,int * y,gboolean * push_in,gpointer userdata)733 void MenuGtk::PointMenuPositionFunc(GtkMenu* menu,
734 int* x,
735 int* y,
736 gboolean* push_in,
737 gpointer userdata) {
738 *push_in = TRUE;
739
740 gfx::Point* point = reinterpret_cast<gfx::Point*>(userdata);
741 *x = point->x();
742 *y = point->y();
743
744 GtkRequisition menu_req;
745 gtk_widget_size_request(GTK_WIDGET(menu), &menu_req);
746 GdkScreen* screen;
747 gdk_display_get_pointer(gdk_display_get_default(), &screen, NULL, NULL, NULL);
748 gint monitor = gdk_screen_get_monitor_at_point(screen, *x, *y);
749
750 GdkRectangle screen_rect;
751 gdk_screen_get_monitor_geometry(screen, monitor, &screen_rect);
752
753 *y = CalculateMenuYPosition(&screen_rect, &menu_req, NULL, *y);
754 }
755
ExecuteCommand(ui::MenuModel * model,int id)756 void MenuGtk::ExecuteCommand(ui::MenuModel* model, int id) {
757 if (delegate_)
758 delegate_->CommandWillBeExecuted();
759
760 GdkEvent* event = gtk_get_current_event();
761 int event_flags = 0;
762
763 if (event && event->type == GDK_BUTTON_RELEASE)
764 event_flags = event_utils::EventFlagsFromGdkState(event->button.state);
765 model->ActivatedAt(id, event_flags);
766
767 if (event)
768 gdk_event_free(event);
769 }
770
OnMenuShow(GtkWidget * widget)771 void MenuGtk::OnMenuShow(GtkWidget* widget) {
772 model_->MenuWillShow();
773 base::MessageLoop::current()->PostTask(
774 FROM_HERE, base::Bind(&MenuGtk::UpdateMenu, weak_factory_.GetWeakPtr()));
775 }
776
OnMenuHidden(GtkWidget * widget)777 void MenuGtk::OnMenuHidden(GtkWidget* widget) {
778 if (delegate_)
779 delegate_->StoppedShowing();
780 model_->MenuClosed();
781 }
782
OnMenuFocusOut(GtkWidget * widget,GdkEventFocus * event)783 gboolean MenuGtk::OnMenuFocusOut(GtkWidget* widget, GdkEventFocus* event) {
784 gtk_widget_hide(menu_);
785 return TRUE;
786 }
787
OnSubMenuShow(GtkWidget * submenu)788 void MenuGtk::OnSubMenuShow(GtkWidget* submenu) {
789 GtkWidget* menu_item = static_cast<GtkWidget*>(
790 g_object_get_data(G_OBJECT(submenu), "menu-item"));
791 // TODO(mdm): Figure out why this can sometimes be NULL. See bug 131974.
792 CHECK(menu_item);
793 // Notify the submenu model that the menu will be shown.
794 ui::MenuModel* submenu_model = static_cast<ui::MenuModel*>(
795 g_object_get_data(G_OBJECT(menu_item), "submenu-model"));
796 // We're extra cautious here, and bail out if the submenu model is NULL. In
797 // some cases we clear it out from a parent menu; we shouldn't ever show the
798 // menu after that, but we play it safe since we're dealing with wacky
799 // injected libraries that toy with our menus. (See comments below.)
800 if (!submenu_model)
801 return;
802
803 // If the submenu is already built, then return right away. This means we
804 // recently showed this submenu, and have not yet processed the fact that it
805 // was hidden before being shown again.
806 if (g_object_get_data(G_OBJECT(submenu), "submenu-built"))
807 return;
808 g_object_set_data(G_OBJECT(submenu), "submenu-built", GINT_TO_POINTER(1));
809
810 submenu_model->MenuWillShow();
811
812 // Actually build the submenu and attach it to the parent menu item.
813 BuildSubmenuFromModel(submenu_model, submenu);
814 gtk_menu_item_set_submenu(GTK_MENU_ITEM(menu_item), submenu);
815
816 // Update all the menu item info in the newly-generated menu.
817 gtk_container_foreach(GTK_CONTAINER(submenu), SetMenuItemInfo, this);
818 }
819
OnSubMenuHidden(GtkWidget * submenu)820 void MenuGtk::OnSubMenuHidden(GtkWidget* submenu) {
821 // Increase the reference count of the old submenu, and schedule it to be
822 // deleted later. We get this hide notification before we've processed menu
823 // activations, so if we were to delete the submenu now, we might lose the
824 // activation. This also lets us reuse the menu if it is shown again before
825 // it gets deleted; in that case, OnSubMenuHiddenCallback() just decrements
826 // the reference count again. Note that the delay is just an optimization; we
827 // could use PostTask() and this would still work correctly.
828 g_object_ref(G_OBJECT(submenu));
829 base::MessageLoop::current()->PostDelayedTask(
830 FROM_HERE,
831 base::Bind(&MenuGtk::OnSubMenuHiddenCallback, submenu),
832 base::TimeDelta::FromSeconds(2));
833 }
834
835 namespace {
836
837 // Remove all descendant submenu-model data pointers.
RemoveSubMenuModels(GtkWidget * menu_item,void * unused)838 void RemoveSubMenuModels(GtkWidget* menu_item, void* unused) {
839 if (!GTK_IS_MENU_ITEM(menu_item))
840 return;
841 g_object_steal_data(G_OBJECT(menu_item), "submenu-model");
842 GtkWidget* submenu = gtk_menu_item_get_submenu(GTK_MENU_ITEM(menu_item));
843 if (submenu)
844 gtk_container_foreach(GTK_CONTAINER(submenu), RemoveSubMenuModels, NULL);
845 }
846
847 } // namespace
848
849 // static
OnSubMenuHiddenCallback(GtkWidget * submenu)850 void MenuGtk::OnSubMenuHiddenCallback(GtkWidget* submenu) {
851 if (!gtk_widget_get_visible(submenu)) {
852 // Remove all the children of this menu, clearing out their submenu-model
853 // pointers in case they have pending calls to OnSubMenuHiddenCallback().
854 // (Normally that won't happen: we'd have hidden them first, and so they'd
855 // have already been deleted. But in some cases [e.g. on Ubuntu 12.04],
856 // GTK menu operations may be hooked to allow external applications to
857 // mirror the menu structure, and the hooks may show and hide menus in
858 // order to trigger exactly the kind of dynamic menu building we're doing.
859 // The result is that we see show and hide events in strange orders.)
860 GList* children = gtk_container_get_children(GTK_CONTAINER(submenu));
861 for (GList* child = children; child; child = g_list_next(child)) {
862 RemoveSubMenuModels(GTK_WIDGET(child->data), NULL);
863 gtk_container_remove(GTK_CONTAINER(submenu), GTK_WIDGET(child->data));
864 }
865 g_list_free(children);
866
867 // Clear out the bit that says the menu is built.
868 // We'll rebuild it next time it is shown.
869 g_object_steal_data(G_OBJECT(submenu), "submenu-built");
870
871 // Notify the submenu model that the menu has been hidden. This may cause
872 // it to delete descendant submenu models, which is why we cleared those
873 // pointers out above.
874 GtkWidget* menu_item = static_cast<GtkWidget*>(
875 g_object_get_data(G_OBJECT(submenu), "menu-item"));
876 // TODO(mdm): Figure out why this can sometimes be NULL. See bug 124110.
877 CHECK(menu_item);
878 ui::MenuModel* submenu_model = static_cast<ui::MenuModel*>(
879 g_object_get_data(G_OBJECT(menu_item), "submenu-model"));
880 if (submenu_model)
881 submenu_model->MenuClosed();
882 }
883
884 // Remove the reference we grabbed in OnSubMenuHidden() above.
885 g_object_unref(G_OBJECT(submenu));
886 }
887
888 // static
SetButtonItemInfo(GtkWidget * button,gpointer userdata)889 void MenuGtk::SetButtonItemInfo(GtkWidget* button, gpointer userdata) {
890 ui::ButtonMenuItemModel* model =
891 reinterpret_cast<ui::ButtonMenuItemModel*>(
892 g_object_get_data(G_OBJECT(button), "button-model"));
893 int index = GPOINTER_TO_INT(g_object_get_data(
894 G_OBJECT(button), "button-model-id"));
895
896 if (model->IsItemDynamicAt(index)) {
897 std::string label = ui::ConvertAcceleratorsFromWindowsStyle(
898 UTF16ToUTF8(model->GetLabelAt(index)));
899 gtk_button_set_label(GTK_BUTTON(button), label.c_str());
900 }
901
902 gtk_widget_set_sensitive(GTK_WIDGET(button), model->IsEnabledAt(index));
903 }
904
905 // static
SetMenuItemInfo(GtkWidget * widget,gpointer userdata)906 void MenuGtk::SetMenuItemInfo(GtkWidget* widget, gpointer userdata) {
907 if (GTK_IS_SEPARATOR_MENU_ITEM(widget)) {
908 // We need to explicitly handle this case because otherwise we'll ask the
909 // menu delegate about something with an invalid id.
910 return;
911 }
912
913 int id;
914 if (!GetMenuItemID(widget, &id))
915 return;
916
917 ui::MenuModel* model = ModelForMenuItem(GTK_MENU_ITEM(widget));
918 if (!model) {
919 // If we're not providing the sub menu, then there's no model. For
920 // example, the IME submenu doesn't have a model.
921 return;
922 }
923
924 if (GTK_IS_CHECK_MENU_ITEM(widget)) {
925 GtkCheckMenuItem* item = GTK_CHECK_MENU_ITEM(widget);
926
927 // gtk_check_menu_item_set_active() will send the activate signal. Touching
928 // the underlying "active" property will also call the "activate" handler
929 // for this menu item. So we prevent the "activate" handler from
930 // being called while we set the checkbox.
931 // Why not use one of the glib signal-blocking functions? Because when we
932 // toggle a radio button, it will deactivate one of the other radio buttons,
933 // which we don't have a pointer to.
934 // Wny not make this a member variable? Because "menu" is a pointer to the
935 // root of the MenuGtk and we want to disable *all* MenuGtks, including
936 // submenus.
937 block_activation_ = true;
938 gtk_check_menu_item_set_active(item, model->IsItemCheckedAt(id));
939 block_activation_ = false;
940 }
941
942 if (GTK_IS_CUSTOM_MENU_ITEM(widget)) {
943 // Iterate across all the buttons to update their visible properties.
944 gtk_custom_menu_item_foreach_button(GTK_CUSTOM_MENU_ITEM(widget),
945 SetButtonItemInfo,
946 userdata);
947 }
948
949 if (GTK_IS_MENU_ITEM(widget)) {
950 gtk_widget_set_sensitive(widget, model->IsEnabledAt(id));
951
952 if (model->IsVisibleAt(id)) {
953 // Update the menu item label if it is dynamic.
954 if (model->IsItemDynamicAt(id)) {
955 std::string label = ui::ConvertAcceleratorsFromWindowsStyle(
956 UTF16ToUTF8(model->GetLabelAt(id)));
957
958 gtk_menu_item_set_label(GTK_MENU_ITEM(widget), label.c_str());
959 if (GTK_IS_IMAGE_MENU_ITEM(widget)) {
960 gfx::Image icon;
961 if (model->GetIconAt(id, &icon)) {
962 gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(widget),
963 gtk_image_new_from_pixbuf(
964 icon.ToGdkPixbuf()));
965 } else {
966 gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(widget), NULL);
967 }
968 }
969 }
970
971 gtk_widget_show(widget);
972 } else {
973 gtk_widget_hide(widget);
974 }
975
976 GtkWidget* submenu = gtk_menu_item_get_submenu(GTK_MENU_ITEM(widget));
977 if (submenu) {
978 gtk_container_foreach(GTK_CONTAINER(submenu), &SetMenuItemInfo,
979 userdata);
980 }
981 }
982 }
983