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
29bool MenuGtk::block_activation_ = false;
30
31namespace {
32
33// Sets the ID of a menu item.
34void 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.
44bool 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
54ui::MenuModel* ModelForMenuItem(GtkMenuItem* menu_item) {
55  return reinterpret_cast<ui::MenuModel*>(
56      g_object_get_data(G_OBJECT(menu_item), "model"));
57}
58
59void 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
68void 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
82void 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.
99int 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
124bool MenuGtk::Delegate::AlwaysShowIconForCmd(int command_id) const {
125  return false;
126}
127
128GtkIconSet* MenuGtk::Delegate::GetIconSetForId(int idr) { return NULL; }
129
130GtkWidget* 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
286GtkWidget* MenuGtk::Delegate::GetImageForCommandId(int command_id) const {
287  return GetDefaultImageForCommandId(command_id);
288}
289
290MenuGtk::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
303MenuGtk::~MenuGtk() {
304  Cancel();
305
306  gtk_widget_destroy(menu_);
307  g_object_unref(menu_);
308
309  g_object_unref(dummy_accel_group_);
310}
311
312void 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
322GtkWidget* 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
329GtkWidget* 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
337GtkWidget* 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
345GtkWidget* 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
352GtkWidget* MenuGtk::AppendMenuItem(int command_id, GtkWidget* menu_item) {
353  if (delegate_ && delegate_->AlwaysShowIconForCmd(command_id) &&
354      GTK_IS_IMAGE_MENU_ITEM(menu_item))
355    gtk_util::SetAlwaysShowImage(menu_item);
356
357  return AppendMenuItemToMenu(command_id, NULL, menu_item, menu_, true);
358}
359
360GtkWidget* MenuGtk::AppendMenuItemToMenu(int index,
361                                         ui::MenuModel* model,
362                                         GtkWidget* menu_item,
363                                         GtkWidget* menu,
364                                         bool connect_to_activate) {
365  SetMenuItemID(menu_item, index);
366
367  // Native menu items do their own thing, so only selectively listen for the
368  // activate signal.
369  if (connect_to_activate) {
370    g_signal_connect(menu_item, "activate",
371                     G_CALLBACK(OnMenuItemActivatedThunk), this);
372  }
373
374  // AppendMenuItemToMenu is used both internally when we control menu creation
375  // from a model (where the model can choose to hide certain menu items), and
376  // with immediate commands which don't provide the option.
377  if (model) {
378    if (model->IsVisibleAt(index))
379      gtk_widget_show(menu_item);
380  } else {
381    gtk_widget_show(menu_item);
382  }
383  gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item);
384  return menu_item;
385}
386
387void MenuGtk::PopupForWidget(GtkWidget* widget, int button,
388                             guint32 event_time) {
389  gtk_menu_popup(GTK_MENU(menu_), NULL, NULL,
390                 WidgetMenuPositionFunc,
391                 widget,
392                 button, event_time);
393}
394
395void MenuGtk::PopupAsContext(const gfx::Point& point, guint32 event_time) {
396  // gtk_menu_popup doesn't like the "const" qualifier on point.
397  gfx::Point nonconst_point(point);
398  gtk_menu_popup(GTK_MENU(menu_), NULL, NULL,
399                 PointMenuPositionFunc, &nonconst_point,
400                 3, event_time);
401}
402
403void MenuGtk::PopupAsContextForStatusIcon(guint32 event_time, guint32 button,
404                                          GtkStatusIcon* icon) {
405  gtk_menu_popup(GTK_MENU(menu_), NULL, NULL, gtk_status_icon_position_menu,
406                 icon, button, event_time);
407}
408
409void MenuGtk::PopupAsFromKeyEvent(GtkWidget* widget) {
410  PopupForWidget(widget, 0, gtk_get_current_event_time());
411  gtk_menu_shell_select_first(GTK_MENU_SHELL(menu_), FALSE);
412}
413
414void MenuGtk::Cancel() {
415  gtk_menu_popdown(GTK_MENU(menu_));
416}
417
418void MenuGtk::UpdateMenu() {
419  gtk_container_foreach(GTK_CONTAINER(menu_), SetMenuItemInfo, this);
420}
421
422GtkWidget* MenuGtk::BuildMenuItemWithImage(const std::string& label,
423                                           GtkWidget* image) {
424  GtkWidget* menu_item =
425      gtk_image_menu_item_new_with_mnemonic(label.c_str());
426  gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(menu_item), image);
427  return menu_item;
428}
429
430GtkWidget* MenuGtk::BuildMenuItemWithImage(const std::string& label,
431                                           const gfx::Image& icon) {
432  GtkWidget* menu_item = BuildMenuItemWithImage(label,
433      gtk_image_new_from_pixbuf(icon.ToGdkPixbuf()));
434  return menu_item;
435}
436
437GtkWidget* MenuGtk::BuildMenuItemWithLabel(const std::string& label,
438                                           int command_id) {
439  GtkWidget* img =
440      delegate_ ? delegate_->GetImageForCommandId(command_id) :
441                  MenuGtk::Delegate::GetDefaultImageForCommandId(command_id);
442  return img ? BuildMenuItemWithImage(label, img) :
443               gtk_menu_item_new_with_mnemonic(label.c_str());
444}
445
446void MenuGtk::BuildMenuFromModel() {
447  BuildSubmenuFromModel(model_, menu_);
448}
449
450void MenuGtk::BuildSubmenuFromModel(ui::MenuModel* model, GtkWidget* menu) {
451  std::map<int, GtkWidget*> radio_groups;
452  GtkWidget* menu_item = NULL;
453  for (int i = 0; i < model->GetItemCount(); ++i) {
454    gfx::Image icon;
455    std::string label = ui::ConvertAcceleratorsFromWindowsStyle(
456        UTF16ToUTF8(model->GetLabelAt(i)));
457    bool connect_to_activate = true;
458
459    switch (model->GetTypeAt(i)) {
460      case ui::MenuModel::TYPE_SEPARATOR:
461        menu_item = gtk_separator_menu_item_new();
462        break;
463
464      case ui::MenuModel::TYPE_CHECK:
465        menu_item = gtk_check_menu_item_new_with_mnemonic(label.c_str());
466        break;
467
468      case ui::MenuModel::TYPE_RADIO: {
469        std::map<int, GtkWidget*>::iterator iter =
470            radio_groups.find(model->GetGroupIdAt(i));
471
472        if (iter == radio_groups.end()) {
473          menu_item = gtk_radio_menu_item_new_with_mnemonic(
474              NULL, label.c_str());
475          radio_groups[model->GetGroupIdAt(i)] = menu_item;
476        } else {
477          menu_item = gtk_radio_menu_item_new_with_mnemonic_from_widget(
478              GTK_RADIO_MENU_ITEM(iter->second), label.c_str());
479        }
480        break;
481      }
482      case ui::MenuModel::TYPE_BUTTON_ITEM: {
483        ui::ButtonMenuItemModel* button_menu_item_model =
484            model->GetButtonMenuItemAt(i);
485        menu_item = BuildButtonMenuItem(button_menu_item_model, menu);
486        connect_to_activate = false;
487        break;
488      }
489      case ui::MenuModel::TYPE_SUBMENU:
490      case ui::MenuModel::TYPE_COMMAND: {
491        int command_id = model->GetCommandIdAt(i);
492        if (model->GetIconAt(i, &icon))
493          menu_item = BuildMenuItemWithImage(label, icon);
494        else
495          menu_item = BuildMenuItemWithLabel(label, command_id);
496        if (delegate_ && delegate_->AlwaysShowIconForCmd(command_id) &&
497            GTK_IS_IMAGE_MENU_ITEM(menu_item)) {
498          gtk_util::SetAlwaysShowImage(menu_item);
499        }
500        break;
501      }
502
503      default:
504        NOTREACHED();
505    }
506
507    if (model->GetTypeAt(i) == ui::MenuModel::TYPE_SUBMENU) {
508      GtkWidget* submenu = gtk_menu_new();
509      g_object_set_data(G_OBJECT(submenu), "menu-item", menu_item);
510      ui::MenuModel* submenu_model = model->GetSubmenuModelAt(i);
511      g_object_set_data(G_OBJECT(menu_item), "submenu-model", submenu_model);
512      gtk_menu_item_set_submenu(GTK_MENU_ITEM(menu_item), submenu);
513      // We will populate the submenu on demand when shown.
514      g_signal_connect(submenu, "show", G_CALLBACK(OnSubMenuShowThunk), this);
515      g_signal_connect(submenu, "hide", G_CALLBACK(OnSubMenuHiddenThunk), this);
516      connect_to_activate = false;
517    }
518
519    ui::Accelerator accelerator;
520    if (model->GetAcceleratorAt(i, &accelerator)) {
521      gtk_widget_add_accelerator(menu_item,
522                                 "activate",
523                                 dummy_accel_group_,
524                                 ui::GetGdkKeyCodeForAccelerator(accelerator),
525                                 ui::GetGdkModifierForAccelerator(accelerator),
526                                 GTK_ACCEL_VISIBLE);
527    }
528
529    g_object_set_data(G_OBJECT(menu_item), "model", model);
530    AppendMenuItemToMenu(i, model, menu_item, menu, connect_to_activate);
531
532    menu_item = NULL;
533  }
534}
535
536GtkWidget* MenuGtk::BuildButtonMenuItem(ui::ButtonMenuItemModel* model,
537                                        GtkWidget* menu) {
538  GtkWidget* menu_item = gtk_custom_menu_item_new(
539      ui::RemoveWindowsStyleAccelerators(UTF16ToUTF8(model->label())).c_str());
540
541  // Set up the callback to the model for when it is clicked.
542  g_object_set_data(G_OBJECT(menu_item), "button-model", model);
543  g_signal_connect(menu_item, "button-pushed",
544                   G_CALLBACK(OnMenuButtonPressedThunk), this);
545  g_signal_connect(menu_item, "try-button-pushed",
546                   G_CALLBACK(OnMenuTryButtonPressedThunk), this);
547
548  GtkSizeGroup* group = NULL;
549  for (int i = 0; i < model->GetItemCount(); ++i) {
550    GtkWidget* button = NULL;
551
552    switch (model->GetTypeAt(i)) {
553      case ui::ButtonMenuItemModel::TYPE_SPACE: {
554        gtk_custom_menu_item_add_space(GTK_CUSTOM_MENU_ITEM(menu_item));
555        break;
556      }
557      case ui::ButtonMenuItemModel::TYPE_BUTTON: {
558        button = gtk_custom_menu_item_add_button(
559            GTK_CUSTOM_MENU_ITEM(menu_item),
560            model->GetCommandIdAt(i));
561
562        int icon_idr;
563        if (model->GetIconAt(i, &icon_idr)) {
564          SetupImageIcon(button, menu, icon_idr, delegate_);
565        } else {
566          gtk_button_set_label(
567              GTK_BUTTON(button),
568              ui::RemoveWindowsStyleAccelerators(
569                  UTF16ToUTF8(model->GetLabelAt(i))).c_str());
570        }
571
572        SetUpButtonShowHandler(button, model, i);
573        break;
574      }
575      case ui::ButtonMenuItemModel::TYPE_BUTTON_LABEL: {
576        button = gtk_custom_menu_item_add_button_label(
577            GTK_CUSTOM_MENU_ITEM(menu_item),
578            model->GetCommandIdAt(i));
579        gtk_button_set_label(
580            GTK_BUTTON(button),
581            ui::RemoveWindowsStyleAccelerators(
582                UTF16ToUTF8(model->GetLabelAt(i))).c_str());
583        SetUpButtonShowHandler(button, model, i);
584        break;
585      }
586    }
587
588    if (button && model->PartOfGroup(i)) {
589      if (!group)
590        group = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL);
591
592      gtk_size_group_add_widget(group, button);
593    }
594  }
595
596  if (group)
597    g_object_unref(group);
598
599  return menu_item;
600}
601
602void MenuGtk::OnMenuItemActivated(GtkWidget* menu_item) {
603  if (block_activation_)
604    return;
605
606  ui::MenuModel* model = ModelForMenuItem(GTK_MENU_ITEM(menu_item));
607
608  if (!model) {
609    // There won't be a model for "native" submenus like the "Input Methods"
610    // context menu. We don't need to handle activation messages for submenus
611    // anyway, so we can just return here.
612    DCHECK(gtk_menu_item_get_submenu(GTK_MENU_ITEM(menu_item)));
613    return;
614  }
615
616  // The activate signal is sent to radio items as they get deselected;
617  // ignore it in this case.
618  if (GTK_IS_RADIO_MENU_ITEM(menu_item) &&
619      !gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(menu_item))) {
620    return;
621  }
622
623  int id;
624  if (!GetMenuItemID(menu_item, &id))
625    return;
626
627  // The menu item can still be activated by hotkeys even if it is disabled.
628  if (model->IsEnabledAt(id))
629    ExecuteCommand(model, id);
630}
631
632void MenuGtk::OnMenuButtonPressed(GtkWidget* menu_item, int command_id) {
633  ui::ButtonMenuItemModel* model =
634      reinterpret_cast<ui::ButtonMenuItemModel*>(
635          g_object_get_data(G_OBJECT(menu_item), "button-model"));
636  if (model && model->IsCommandIdEnabled(command_id)) {
637    if (delegate_)
638      delegate_->CommandWillBeExecuted();
639
640    model->ActivatedCommand(command_id);
641  }
642}
643
644gboolean MenuGtk::OnMenuTryButtonPressed(GtkWidget* menu_item,
645                                         int command_id) {
646  gboolean pressed = FALSE;
647  ui::ButtonMenuItemModel* model =
648      reinterpret_cast<ui::ButtonMenuItemModel*>(
649          g_object_get_data(G_OBJECT(menu_item), "button-model"));
650  if (model &&
651      model->IsCommandIdEnabled(command_id) &&
652      !model->DoesCommandIdDismissMenu(command_id)) {
653    if (delegate_)
654      delegate_->CommandWillBeExecuted();
655
656    model->ActivatedCommand(command_id);
657    pressed = TRUE;
658  }
659
660  return pressed;
661}
662
663// static
664void MenuGtk::WidgetMenuPositionFunc(GtkMenu* menu,
665                                     int* x,
666                                     int* y,
667                                     gboolean* push_in,
668                                     void* void_widget) {
669  GtkWidget* widget = GTK_WIDGET(void_widget);
670  GtkRequisition menu_req;
671
672  gtk_widget_size_request(GTK_WIDGET(menu), &menu_req);
673
674  gdk_window_get_origin(gtk_widget_get_window(widget), x, y);
675  GdkScreen *screen = gtk_widget_get_screen(widget);
676  gint monitor = gdk_screen_get_monitor_at_point(screen, *x, *y);
677
678  GdkRectangle screen_rect;
679  gdk_screen_get_monitor_geometry(screen, monitor,
680                                  &screen_rect);
681
682  GtkAllocation allocation;
683  gtk_widget_get_allocation(widget, &allocation);
684
685  if (!gtk_widget_get_has_window(widget)) {
686    *x += allocation.x;
687    *y += allocation.y;
688  }
689  *y += allocation.height;
690
691  bool start_align =
692    !!g_object_get_data(G_OBJECT(widget), "left-align-popup");
693  if (base::i18n::IsRTL())
694    start_align = !start_align;
695
696  if (!start_align)
697    *x += allocation.width - menu_req.width;
698
699  *y = CalculateMenuYPosition(&screen_rect, &menu_req, widget, *y);
700
701  *push_in = FALSE;
702}
703
704// static
705void MenuGtk::PointMenuPositionFunc(GtkMenu* menu,
706                                    int* x,
707                                    int* y,
708                                    gboolean* push_in,
709                                    gpointer userdata) {
710  *push_in = TRUE;
711
712  gfx::Point* point = reinterpret_cast<gfx::Point*>(userdata);
713  *x = point->x();
714  *y = point->y();
715
716  GtkRequisition menu_req;
717  gtk_widget_size_request(GTK_WIDGET(menu), &menu_req);
718  GdkScreen* screen;
719  gdk_display_get_pointer(gdk_display_get_default(), &screen, NULL, NULL, NULL);
720  gint monitor = gdk_screen_get_monitor_at_point(screen, *x, *y);
721
722  GdkRectangle screen_rect;
723  gdk_screen_get_monitor_geometry(screen, monitor, &screen_rect);
724
725  *y = CalculateMenuYPosition(&screen_rect, &menu_req, NULL, *y);
726}
727
728void MenuGtk::ExecuteCommand(ui::MenuModel* model, int id) {
729  if (delegate_)
730    delegate_->CommandWillBeExecuted();
731
732  GdkEvent* event = gtk_get_current_event();
733  int event_flags = 0;
734
735  if (event && event->type == GDK_BUTTON_RELEASE)
736    event_flags = event_utils::EventFlagsFromGdkState(event->button.state);
737  model->ActivatedAt(id, event_flags);
738
739  if (event)
740    gdk_event_free(event);
741}
742
743void MenuGtk::OnMenuShow(GtkWidget* widget) {
744  model_->MenuWillShow();
745  base::MessageLoop::current()->PostTask(
746      FROM_HERE, base::Bind(&MenuGtk::UpdateMenu, weak_factory_.GetWeakPtr()));
747}
748
749void MenuGtk::OnMenuHidden(GtkWidget* widget) {
750  if (delegate_)
751    delegate_->StoppedShowing();
752  model_->MenuClosed();
753}
754
755gboolean MenuGtk::OnMenuFocusOut(GtkWidget* widget, GdkEventFocus* event) {
756  gtk_widget_hide(menu_);
757  return TRUE;
758}
759
760void MenuGtk::OnSubMenuShow(GtkWidget* submenu) {
761  GtkWidget* menu_item = static_cast<GtkWidget*>(
762      g_object_get_data(G_OBJECT(submenu), "menu-item"));
763  // TODO(mdm): Figure out why this can sometimes be NULL. See bug 131974.
764  CHECK(menu_item);
765  // Notify the submenu model that the menu will be shown.
766  ui::MenuModel* submenu_model = static_cast<ui::MenuModel*>(
767      g_object_get_data(G_OBJECT(menu_item), "submenu-model"));
768  // We're extra cautious here, and bail out if the submenu model is NULL. In
769  // some cases we clear it out from a parent menu; we shouldn't ever show the
770  // menu after that, but we play it safe since we're dealing with wacky
771  // injected libraries that toy with our menus. (See comments below.)
772  if (!submenu_model)
773    return;
774
775  // If the submenu is already built, then return right away. This means we
776  // recently showed this submenu, and have not yet processed the fact that it
777  // was hidden before being shown again.
778  if (g_object_get_data(G_OBJECT(submenu), "submenu-built"))
779    return;
780  g_object_set_data(G_OBJECT(submenu), "submenu-built", GINT_TO_POINTER(1));
781
782  submenu_model->MenuWillShow();
783
784  // Actually build the submenu and attach it to the parent menu item.
785  BuildSubmenuFromModel(submenu_model, submenu);
786  gtk_menu_item_set_submenu(GTK_MENU_ITEM(menu_item), submenu);
787
788  // Update all the menu item info in the newly-generated menu.
789  gtk_container_foreach(GTK_CONTAINER(submenu), SetMenuItemInfo, this);
790}
791
792void MenuGtk::OnSubMenuHidden(GtkWidget* submenu) {
793  // Increase the reference count of the old submenu, and schedule it to be
794  // deleted later. We get this hide notification before we've processed menu
795  // activations, so if we were to delete the submenu now, we might lose the
796  // activation. This also lets us reuse the menu if it is shown again before
797  // it gets deleted; in that case, OnSubMenuHiddenCallback() just decrements
798  // the reference count again. Note that the delay is just an optimization; we
799  // could use PostTask() and this would still work correctly.
800  g_object_ref(G_OBJECT(submenu));
801  base::MessageLoop::current()->PostDelayedTask(
802      FROM_HERE,
803      base::Bind(&MenuGtk::OnSubMenuHiddenCallback, submenu),
804      base::TimeDelta::FromSeconds(2));
805}
806
807namespace {
808
809// Remove all descendant submenu-model data pointers.
810void RemoveSubMenuModels(GtkWidget* menu_item, void* unused) {
811  if (!GTK_IS_MENU_ITEM(menu_item))
812    return;
813  g_object_steal_data(G_OBJECT(menu_item), "submenu-model");
814  GtkWidget* submenu = gtk_menu_item_get_submenu(GTK_MENU_ITEM(menu_item));
815  if (submenu)
816    gtk_container_foreach(GTK_CONTAINER(submenu), RemoveSubMenuModels, NULL);
817}
818
819}  // namespace
820
821// static
822void MenuGtk::OnSubMenuHiddenCallback(GtkWidget* submenu) {
823  if (!gtk_widget_get_visible(submenu)) {
824    // Remove all the children of this menu, clearing out their submenu-model
825    // pointers in case they have pending calls to OnSubMenuHiddenCallback().
826    // (Normally that won't happen: we'd have hidden them first, and so they'd
827    // have already been deleted. But in some cases [e.g. on Ubuntu 12.04],
828    // GTK menu operations may be hooked to allow external applications to
829    // mirror the menu structure, and the hooks may show and hide menus in
830    // order to trigger exactly the kind of dynamic menu building we're doing.
831    // The result is that we see show and hide events in strange orders.)
832    GList* children = gtk_container_get_children(GTK_CONTAINER(submenu));
833    for (GList* child = children; child; child = g_list_next(child)) {
834      RemoveSubMenuModels(GTK_WIDGET(child->data), NULL);
835      gtk_container_remove(GTK_CONTAINER(submenu), GTK_WIDGET(child->data));
836    }
837    g_list_free(children);
838
839    // Clear out the bit that says the menu is built.
840    // We'll rebuild it next time it is shown.
841    g_object_steal_data(G_OBJECT(submenu), "submenu-built");
842
843    // Notify the submenu model that the menu has been hidden. This may cause
844    // it to delete descendant submenu models, which is why we cleared those
845    // pointers out above.
846    GtkWidget* menu_item = static_cast<GtkWidget*>(
847        g_object_get_data(G_OBJECT(submenu), "menu-item"));
848    // TODO(mdm): Figure out why this can sometimes be NULL. See bug 124110.
849    CHECK(menu_item);
850    ui::MenuModel* submenu_model = static_cast<ui::MenuModel*>(
851        g_object_get_data(G_OBJECT(menu_item), "submenu-model"));
852    if (submenu_model)
853      submenu_model->MenuClosed();
854  }
855
856  // Remove the reference we grabbed in OnSubMenuHidden() above.
857  g_object_unref(G_OBJECT(submenu));
858}
859
860// static
861void MenuGtk::SetButtonItemInfo(GtkWidget* button, gpointer userdata) {
862  ui::ButtonMenuItemModel* model =
863      reinterpret_cast<ui::ButtonMenuItemModel*>(
864          g_object_get_data(G_OBJECT(button), "button-model"));
865  int index = GPOINTER_TO_INT(g_object_get_data(
866      G_OBJECT(button), "button-model-id"));
867
868  if (model->IsItemDynamicAt(index)) {
869    std::string label = ui::ConvertAcceleratorsFromWindowsStyle(
870        UTF16ToUTF8(model->GetLabelAt(index)));
871    gtk_button_set_label(GTK_BUTTON(button), label.c_str());
872  }
873
874  gtk_widget_set_sensitive(GTK_WIDGET(button), model->IsEnabledAt(index));
875}
876
877// static
878void MenuGtk::SetMenuItemInfo(GtkWidget* widget, gpointer userdata) {
879  if (GTK_IS_SEPARATOR_MENU_ITEM(widget)) {
880    // We need to explicitly handle this case because otherwise we'll ask the
881    // menu delegate about something with an invalid id.
882    return;
883  }
884
885  int id;
886  if (!GetMenuItemID(widget, &id))
887    return;
888
889  ui::MenuModel* model = ModelForMenuItem(GTK_MENU_ITEM(widget));
890  if (!model) {
891    // If we're not providing the sub menu, then there's no model.  For
892    // example, the IME submenu doesn't have a model.
893    return;
894  }
895
896  if (GTK_IS_CHECK_MENU_ITEM(widget)) {
897    GtkCheckMenuItem* item = GTK_CHECK_MENU_ITEM(widget);
898
899    // gtk_check_menu_item_set_active() will send the activate signal. Touching
900    // the underlying "active" property will also call the "activate" handler
901    // for this menu item. So we prevent the "activate" handler from
902    // being called while we set the checkbox.
903    // Why not use one of the glib signal-blocking functions?  Because when we
904    // toggle a radio button, it will deactivate one of the other radio buttons,
905    // which we don't have a pointer to.
906    // Wny not make this a member variable?  Because "menu" is a pointer to the
907    // root of the MenuGtk and we want to disable *all* MenuGtks, including
908    // submenus.
909    block_activation_ = true;
910    gtk_check_menu_item_set_active(item, model->IsItemCheckedAt(id));
911    block_activation_ = false;
912  }
913
914  if (GTK_IS_CUSTOM_MENU_ITEM(widget)) {
915    // Iterate across all the buttons to update their visible properties.
916    gtk_custom_menu_item_foreach_button(GTK_CUSTOM_MENU_ITEM(widget),
917                                        SetButtonItemInfo,
918                                        userdata);
919  }
920
921  if (GTK_IS_MENU_ITEM(widget)) {
922    gtk_widget_set_sensitive(widget, model->IsEnabledAt(id));
923
924    if (model->IsVisibleAt(id)) {
925      // Update the menu item label if it is dynamic.
926      if (model->IsItemDynamicAt(id)) {
927        std::string label = ui::ConvertAcceleratorsFromWindowsStyle(
928            UTF16ToUTF8(model->GetLabelAt(id)));
929
930        gtk_menu_item_set_label(GTK_MENU_ITEM(widget), label.c_str());
931        if (GTK_IS_IMAGE_MENU_ITEM(widget)) {
932          gfx::Image icon;
933          if (model->GetIconAt(id, &icon)) {
934            gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(widget),
935                                          gtk_image_new_from_pixbuf(
936                                              icon.ToGdkPixbuf()));
937          } else {
938            gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(widget), NULL);
939          }
940        }
941      }
942
943      gtk_widget_show(widget);
944    } else {
945      gtk_widget_hide(widget);
946    }
947
948    GtkWidget* submenu = gtk_menu_item_get_submenu(GTK_MENU_ITEM(widget));
949    if (submenu) {
950      gtk_container_foreach(GTK_CONTAINER(submenu), &SetMenuItemInfo,
951                            userdata);
952    }
953  }
954}
955