• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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