menu_util.cc 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. // Copyright 2013 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. #include "shell/browser/ui/gtk/menu_util.h"
  5. #include <gdk/gdk.h>
  6. #include <gtk/gtk.h>
  7. #include <string>
  8. #include "base/containers/flat_map.h"
  9. #include "base/strings/utf_string_conversions.h"
  10. #include "chrome/app/chrome_command_ids.h"
  11. #include "shell/browser/ui/gtk_util.h"
  12. #include "third_party/skia/include/core/SkBitmap.h"
  13. #include "third_party/skia/include/core/SkUnPreMultiply.h"
  14. #include "ui/base/accelerators/accelerator.h"
  15. #include "ui/base/accelerators/menu_label_accelerator_util_linux.h"
  16. #include "ui/base/models/image_model.h"
  17. #include "ui/base/models/menu_model.h"
  18. #include "ui/events/event_constants.h"
  19. #include "ui/events/keycodes/keyboard_code_conversion_x.h"
  20. #include "ui/ozone/public/ozone_platform.h"
  21. namespace electron::gtkui {
  22. namespace {
  23. int EventFlagsFromGdkState(guint state) {
  24. int flags = ui::EF_NONE;
  25. flags |= (state & GDK_SHIFT_MASK) ? ui::EF_SHIFT_DOWN : ui::EF_NONE;
  26. flags |= (state & GDK_LOCK_MASK) ? ui::EF_CAPS_LOCK_ON : ui::EF_NONE;
  27. flags |= (state & GDK_CONTROL_MASK) ? ui::EF_CONTROL_DOWN : ui::EF_NONE;
  28. flags |= (state & GDK_MOD1_MASK) ? ui::EF_ALT_DOWN : ui::EF_NONE;
  29. flags |= (state & GDK_BUTTON1_MASK) ? ui::EF_LEFT_MOUSE_BUTTON : ui::EF_NONE;
  30. flags |=
  31. (state & GDK_BUTTON2_MASK) ? ui::EF_MIDDLE_MOUSE_BUTTON : ui::EF_NONE;
  32. flags |= (state & GDK_BUTTON3_MASK) ? ui::EF_RIGHT_MOUSE_BUTTON : ui::EF_NONE;
  33. return flags;
  34. }
  35. guint GetGdkKeyCodeForAccelerator(const ui::Accelerator& accelerator) {
  36. // The second parameter is false because accelerator keys are expressed in
  37. // terms of the non-shift-modified key.
  38. return XKeysymForWindowsKeyCode(accelerator.key_code(), false);
  39. }
  40. GdkModifierType GetGdkModifierForAccelerator(
  41. const ui::Accelerator& accelerator) {
  42. int event_flag = accelerator.modifiers();
  43. int modifier = 0;
  44. if (event_flag & ui::EF_SHIFT_DOWN)
  45. modifier |= GDK_SHIFT_MASK;
  46. if (event_flag & ui::EF_CONTROL_DOWN)
  47. modifier |= GDK_CONTROL_MASK;
  48. if (event_flag & ui::EF_ALT_DOWN)
  49. modifier |= GDK_MOD1_MASK;
  50. return static_cast<GdkModifierType>(modifier);
  51. }
  52. } // namespace
  53. GtkWidget* BuildMenuItemWithImage(const std::string& label, GtkWidget* image) {
  54. // GTK4 removed support for image menu items.
  55. #if GTK_CHECK_VERSION(3, 90, 0)
  56. return gtk_menu_item_new_with_mnemonic(label.c_str());
  57. #else
  58. G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
  59. GtkWidget* menu_item = gtk_image_menu_item_new_with_mnemonic(label.c_str());
  60. gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(menu_item), image);
  61. G_GNUC_END_IGNORE_DEPRECATIONS;
  62. return menu_item;
  63. #endif
  64. }
  65. GtkWidget* BuildMenuItemWithImage(const std::string& label,
  66. const gfx::Image& icon) {
  67. GdkPixbuf* pixbuf = gtk_util::GdkPixbufFromSkBitmap(*icon.ToSkBitmap());
  68. GtkWidget* menu_item =
  69. BuildMenuItemWithImage(label, gtk_image_new_from_pixbuf(pixbuf));
  70. g_object_unref(pixbuf);
  71. return menu_item;
  72. }
  73. GtkWidget* BuildMenuItemWithLabel(const std::string& label) {
  74. return gtk_menu_item_new_with_mnemonic(label.c_str());
  75. }
  76. ui::MenuModel* ModelForMenuItem(GtkMenuItem* menu_item) {
  77. return reinterpret_cast<ui::MenuModel*>(
  78. g_object_get_data(G_OBJECT(menu_item), "model"));
  79. }
  80. GtkWidget* AppendMenuItemToMenu(int index,
  81. ui::MenuModel* model,
  82. GtkWidget* menu_item,
  83. GtkWidget* menu,
  84. bool connect_to_activate,
  85. MenuActivatedCallback item_activated_cb,
  86. std::vector<ScopedGSignal>* signals) {
  87. // Set the ID of a menu item.
  88. // Add 1 to the menu_id to avoid setting zero (null) to "menu-id".
  89. g_object_set_data(G_OBJECT(menu_item), "menu-id", GINT_TO_POINTER(index + 1));
  90. // Native menu items do their own thing, so only selectively listen for the
  91. // activate signal.
  92. if (connect_to_activate) {
  93. signals->emplace_back(menu_item, "activate", item_activated_cb);
  94. }
  95. // AppendMenuItemToMenu is used both internally when we control menu creation
  96. // from a model (where the model can choose to hide certain menu items), and
  97. // with immediate commands which don't provide the option.
  98. if (model) {
  99. if (model->IsVisibleAt(index))
  100. gtk_widget_show(menu_item);
  101. } else {
  102. gtk_widget_show(menu_item);
  103. }
  104. gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item);
  105. return menu_item;
  106. }
  107. bool GetMenuItemID(GtkWidget* menu_item, int* menu_id) {
  108. gpointer id_ptr = g_object_get_data(G_OBJECT(menu_item), "menu-id");
  109. if (id_ptr != nullptr) {
  110. *menu_id = GPOINTER_TO_INT(id_ptr) - 1;
  111. return true;
  112. }
  113. return false;
  114. }
  115. void ExecuteCommand(ui::MenuModel* model, int id) {
  116. GdkEvent* event = gtk_get_current_event();
  117. int event_flags = 0;
  118. if (event && event->type == GDK_BUTTON_RELEASE)
  119. event_flags = EventFlagsFromGdkState(event->button.state);
  120. model->ActivatedAt(static_cast<int>(id), event_flags);
  121. if (event)
  122. gdk_event_free(event);
  123. }
  124. void BuildSubmenuFromModel(ui::MenuModel* model,
  125. GtkWidget* menu,
  126. MenuActivatedCallback item_activated_cb,
  127. bool* block_activation,
  128. std::vector<ScopedGSignal>* signals) {
  129. base::flat_map<int, GtkWidget*> radio_groups;
  130. GtkWidget* menu_item = nullptr;
  131. for (size_t i = 0; i < model->GetItemCount(); ++i) {
  132. std::string label = ui::ConvertAcceleratorsFromWindowsStyle(
  133. base::UTF16ToUTF8(model->GetLabelAt(i)));
  134. bool connect_to_activate = true;
  135. switch (model->GetTypeAt(i)) {
  136. case ui::MenuModel::TYPE_SEPARATOR:
  137. menu_item = gtk_separator_menu_item_new();
  138. break;
  139. case ui::MenuModel::TYPE_CHECK:
  140. menu_item = gtk_check_menu_item_new_with_mnemonic(label.c_str());
  141. break;
  142. case ui::MenuModel::TYPE_RADIO: {
  143. auto iter = radio_groups.find(model->GetGroupIdAt(i));
  144. if (iter == radio_groups.end()) {
  145. menu_item =
  146. gtk_radio_menu_item_new_with_mnemonic(nullptr, label.c_str());
  147. radio_groups[model->GetGroupIdAt(i)] = menu_item;
  148. } else {
  149. menu_item = gtk_radio_menu_item_new_with_mnemonic_from_widget(
  150. GTK_RADIO_MENU_ITEM(iter->second), label.c_str());
  151. }
  152. break;
  153. }
  154. case ui::MenuModel::TYPE_BUTTON_ITEM: {
  155. NOTIMPLEMENTED();
  156. break;
  157. }
  158. case ui::MenuModel::TYPE_SUBMENU:
  159. case ui::MenuModel::TYPE_COMMAND: {
  160. auto icon_model = model->GetIconAt(i);
  161. if (!icon_model.IsEmpty())
  162. menu_item = BuildMenuItemWithImage(label, icon_model.GetImage());
  163. else
  164. menu_item = BuildMenuItemWithLabel(label);
  165. #if !GTK_CHECK_VERSION(3, 90, 0)
  166. G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
  167. if (GTK_IS_IMAGE_MENU_ITEM(menu_item)) {
  168. gtk_image_menu_item_set_always_show_image(
  169. GTK_IMAGE_MENU_ITEM(menu_item), TRUE);
  170. }
  171. G_GNUC_END_IGNORE_DEPRECATIONS;
  172. #endif
  173. break;
  174. }
  175. default:
  176. NOTREACHED();
  177. }
  178. if (model->GetTypeAt(i) == ui::MenuModel::TYPE_SUBMENU) {
  179. GtkWidget* submenu = gtk_menu_new();
  180. ui::MenuModel* submenu_model = model->GetSubmenuModelAt(i);
  181. BuildSubmenuFromModel(submenu_model, submenu, item_activated_cb,
  182. block_activation, signals);
  183. gtk_menu_item_set_submenu(GTK_MENU_ITEM(menu_item), submenu);
  184. // Update all the menu item info in the newly-generated menu.
  185. gtk_container_foreach(GTK_CONTAINER(submenu), SetMenuItemInfo,
  186. block_activation);
  187. submenu_model->MenuWillShow();
  188. connect_to_activate = false;
  189. }
  190. if (ui::OzonePlatform::GetInstance()
  191. ->GetPlatformProperties()
  192. .electron_can_call_x11) {
  193. ui::Accelerator accelerator;
  194. if (model->GetAcceleratorAt(i, &accelerator)) {
  195. gtk_widget_add_accelerator(menu_item, "activate", nullptr,
  196. GetGdkKeyCodeForAccelerator(accelerator),
  197. GetGdkModifierForAccelerator(accelerator),
  198. GTK_ACCEL_VISIBLE);
  199. }
  200. }
  201. g_object_set_data(G_OBJECT(menu_item), "model", model);
  202. AppendMenuItemToMenu(i, model, menu_item, menu, connect_to_activate,
  203. item_activated_cb, signals);
  204. menu_item = nullptr;
  205. }
  206. }
  207. void SetMenuItemInfo(GtkWidget* widget, void* block_activation_ptr) {
  208. if (GTK_IS_SEPARATOR_MENU_ITEM(widget)) {
  209. // We need to explicitly handle this case because otherwise we'll ask the
  210. // menu delegate about something with an invalid id.
  211. return;
  212. }
  213. int id;
  214. if (!GetMenuItemID(widget, &id))
  215. return;
  216. ui::MenuModel* model = ModelForMenuItem(GTK_MENU_ITEM(widget));
  217. if (!model) {
  218. // If we're not providing the sub menu, then there's no model. For
  219. // example, the IME submenu doesn't have a model.
  220. return;
  221. }
  222. bool* block_activation = static_cast<bool*>(block_activation_ptr);
  223. if (GTK_IS_CHECK_MENU_ITEM(widget)) {
  224. GtkCheckMenuItem* item = GTK_CHECK_MENU_ITEM(widget);
  225. // gtk_check_menu_item_set_active() will send the activate signal. Touching
  226. // the underlying "active" property will also call the "activate" handler
  227. // for this menu item. So we prevent the "activate" handler from
  228. // being called while we set the checkbox.
  229. // Why not use one of the glib signal-blocking functions? Because when we
  230. // toggle a radio button, it will deactivate one of the other radio buttons,
  231. // which we don't have a pointer to.
  232. *block_activation = true;
  233. gtk_check_menu_item_set_active(item, model->IsItemCheckedAt(id));
  234. *block_activation = false;
  235. }
  236. if (GTK_IS_MENU_ITEM(widget)) {
  237. gtk_widget_set_sensitive(widget, model->IsEnabledAt(id));
  238. if (model->IsVisibleAt(id)) {
  239. // Update the menu item label if it is dynamic.
  240. if (model->IsItemDynamicAt(id)) {
  241. std::string label = ui::ConvertAcceleratorsFromWindowsStyle(
  242. base::UTF16ToUTF8(model->GetLabelAt(id)));
  243. gtk_menu_item_set_label(GTK_MENU_ITEM(widget), label.c_str());
  244. #if !GTK_CHECK_VERSION(3, 90, 0)
  245. G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
  246. if (GTK_IS_IMAGE_MENU_ITEM(widget)) {
  247. auto icon_model = model->GetIconAt(id);
  248. if (!icon_model.IsEmpty()) {
  249. GdkPixbuf* pixbuf = gtk_util::GdkPixbufFromSkBitmap(
  250. *icon_model.GetImage().ToSkBitmap());
  251. gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(widget),
  252. gtk_image_new_from_pixbuf(pixbuf));
  253. g_object_unref(pixbuf);
  254. } else {
  255. gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(widget), nullptr);
  256. }
  257. }
  258. G_GNUC_END_IGNORE_DEPRECATIONS;
  259. #endif
  260. }
  261. gtk_widget_show(widget);
  262. } else {
  263. gtk_widget_hide(widget);
  264. }
  265. GtkWidget* submenu = gtk_menu_item_get_submenu(GTK_MENU_ITEM(widget));
  266. if (submenu) {
  267. gtk_container_foreach(GTK_CONTAINER(submenu), &SetMenuItemInfo,
  268. block_activation_ptr);
  269. }
  270. }
  271. }
  272. } // namespace electron::gtkui