OLD | NEW |
| (Empty) |
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. | |
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. | |
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 | |
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 | |
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 | |
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 | |
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. | |
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 | |
124 bool MenuGtk::Delegate::AlwaysShowIconForCmd(int command_id) const { | |
125 return false; | |
126 } | |
127 | |
128 GtkIconSet* MenuGtk::Delegate::GetIconSetForId(int idr) { return NULL; } | |
129 | |
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 | |
286 GtkWidget* MenuGtk::Delegate::GetImageForCommandId(int command_id) const { | |
287 return GetDefaultImageForCommandId(command_id); | |
288 } | |
289 | |
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 | |
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 | |
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 | |
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 | |
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 | |
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 | |
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 | |
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 | |
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 | |
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 | |
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 | |
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 | |
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 | |
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 | |
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 | |
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 | |
442 void MenuGtk::Cancel() { | |
443 gtk_menu_popdown(GTK_MENU(menu_)); | |
444 } | |
445 | |
446 void MenuGtk::UpdateMenu() { | |
447 gtk_container_foreach(GTK_CONTAINER(menu_), SetMenuItemInfo, this); | |
448 } | |
449 | |
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 | |
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 | |
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 | |
474 void MenuGtk::BuildMenuFromModel() { | |
475 BuildSubmenuFromModel(model_, menu_); | |
476 } | |
477 | |
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 base::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 | |
564 GtkWidget* MenuGtk::BuildButtonMenuItem(ui::ButtonMenuItemModel* model, | |
565 GtkWidget* menu) { | |
566 GtkWidget* menu_item = gtk_custom_menu_item_new( | |
567 ui::RemoveWindowsStyleAccelerators( | |
568 base::UTF16ToUTF8(model->label())).c_str()); | |
569 | |
570 // Set up the callback to the model for when it is clicked. | |
571 g_object_set_data(G_OBJECT(menu_item), "button-model", model); | |
572 g_signal_connect(menu_item, "button-pushed", | |
573 G_CALLBACK(OnMenuButtonPressedThunk), this); | |
574 g_signal_connect(menu_item, "try-button-pushed", | |
575 G_CALLBACK(OnMenuTryButtonPressedThunk), this); | |
576 | |
577 GtkSizeGroup* group = NULL; | |
578 for (int i = 0; i < model->GetItemCount(); ++i) { | |
579 GtkWidget* button = NULL; | |
580 | |
581 switch (model->GetTypeAt(i)) { | |
582 case ui::ButtonMenuItemModel::TYPE_SPACE: { | |
583 gtk_custom_menu_item_add_space(GTK_CUSTOM_MENU_ITEM(menu_item)); | |
584 break; | |
585 } | |
586 case ui::ButtonMenuItemModel::TYPE_BUTTON: { | |
587 button = gtk_custom_menu_item_add_button( | |
588 GTK_CUSTOM_MENU_ITEM(menu_item), | |
589 model->GetCommandIdAt(i)); | |
590 | |
591 int icon_idr; | |
592 if (model->GetIconAt(i, &icon_idr)) { | |
593 SetupImageIcon(button, menu, icon_idr, delegate_); | |
594 } else { | |
595 gtk_button_set_label( | |
596 GTK_BUTTON(button), | |
597 ui::RemoveWindowsStyleAccelerators( | |
598 base::UTF16ToUTF8(model->GetLabelAt(i))).c_str()); | |
599 } | |
600 | |
601 SetUpButtonShowHandler(button, model, i); | |
602 break; | |
603 } | |
604 case ui::ButtonMenuItemModel::TYPE_BUTTON_LABEL: { | |
605 button = gtk_custom_menu_item_add_button_label( | |
606 GTK_CUSTOM_MENU_ITEM(menu_item), | |
607 model->GetCommandIdAt(i)); | |
608 gtk_button_set_label( | |
609 GTK_BUTTON(button), | |
610 ui::RemoveWindowsStyleAccelerators( | |
611 base::UTF16ToUTF8(model->GetLabelAt(i))).c_str()); | |
612 SetUpButtonShowHandler(button, model, i); | |
613 break; | |
614 } | |
615 } | |
616 | |
617 if (button && model->PartOfGroup(i)) { | |
618 if (!group) | |
619 group = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL); | |
620 | |
621 gtk_size_group_add_widget(group, button); | |
622 } | |
623 } | |
624 | |
625 if (group) | |
626 g_object_unref(group); | |
627 | |
628 return menu_item; | |
629 } | |
630 | |
631 void MenuGtk::OnMenuItemActivated(GtkWidget* menu_item) { | |
632 if (block_activation_) | |
633 return; | |
634 | |
635 ui::MenuModel* model = ModelForMenuItem(GTK_MENU_ITEM(menu_item)); | |
636 | |
637 if (!model) { | |
638 // There won't be a model for "native" submenus like the "Input Methods" | |
639 // context menu. We don't need to handle activation messages for submenus | |
640 // anyway, so we can just return here. | |
641 DCHECK(gtk_menu_item_get_submenu(GTK_MENU_ITEM(menu_item))); | |
642 return; | |
643 } | |
644 | |
645 // The activate signal is sent to radio items as they get deselected; | |
646 // ignore it in this case. | |
647 if (GTK_IS_RADIO_MENU_ITEM(menu_item) && | |
648 !gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(menu_item))) { | |
649 return; | |
650 } | |
651 | |
652 int id; | |
653 if (!GetMenuItemID(menu_item, &id)) | |
654 return; | |
655 | |
656 // The menu item can still be activated by hotkeys even if it is disabled. | |
657 if (model->IsEnabledAt(id)) | |
658 ExecuteCommand(model, id); | |
659 } | |
660 | |
661 void MenuGtk::OnMenuButtonPressed(GtkWidget* menu_item, int command_id) { | |
662 ui::ButtonMenuItemModel* model = | |
663 reinterpret_cast<ui::ButtonMenuItemModel*>( | |
664 g_object_get_data(G_OBJECT(menu_item), "button-model")); | |
665 if (model && model->IsCommandIdEnabled(command_id)) { | |
666 if (delegate_) | |
667 delegate_->CommandWillBeExecuted(); | |
668 | |
669 model->ActivatedCommand(command_id); | |
670 } | |
671 } | |
672 | |
673 gboolean MenuGtk::OnMenuTryButtonPressed(GtkWidget* menu_item, | |
674 int command_id) { | |
675 gboolean pressed = FALSE; | |
676 ui::ButtonMenuItemModel* model = | |
677 reinterpret_cast<ui::ButtonMenuItemModel*>( | |
678 g_object_get_data(G_OBJECT(menu_item), "button-model")); | |
679 if (model && | |
680 model->IsCommandIdEnabled(command_id) && | |
681 !model->DoesCommandIdDismissMenu(command_id)) { | |
682 if (delegate_) | |
683 delegate_->CommandWillBeExecuted(); | |
684 | |
685 model->ActivatedCommand(command_id); | |
686 pressed = TRUE; | |
687 } | |
688 | |
689 return pressed; | |
690 } | |
691 | |
692 // static | |
693 void MenuGtk::WidgetMenuPositionFunc(GtkMenu* menu, | |
694 int* x, | |
695 int* y, | |
696 gboolean* push_in, | |
697 void* void_widget) { | |
698 GtkWidget* widget = GTK_WIDGET(void_widget); | |
699 GtkRequisition menu_req; | |
700 | |
701 gtk_widget_size_request(GTK_WIDGET(menu), &menu_req); | |
702 | |
703 gdk_window_get_origin(gtk_widget_get_window(widget), x, y); | |
704 GdkScreen *screen = gtk_widget_get_screen(widget); | |
705 gint monitor = gdk_screen_get_monitor_at_point(screen, *x, *y); | |
706 | |
707 GdkRectangle screen_rect; | |
708 gdk_screen_get_monitor_geometry(screen, monitor, | |
709 &screen_rect); | |
710 | |
711 GtkAllocation allocation; | |
712 gtk_widget_get_allocation(widget, &allocation); | |
713 | |
714 if (!gtk_widget_get_has_window(widget)) { | |
715 *x += allocation.x; | |
716 *y += allocation.y; | |
717 } | |
718 *y += allocation.height; | |
719 | |
720 bool start_align = | |
721 !!g_object_get_data(G_OBJECT(widget), "left-align-popup"); | |
722 if (base::i18n::IsRTL()) | |
723 start_align = !start_align; | |
724 | |
725 if (!start_align) | |
726 *x += allocation.width - menu_req.width; | |
727 | |
728 *y = CalculateMenuYPosition(&screen_rect, &menu_req, widget, *y); | |
729 | |
730 *push_in = FALSE; | |
731 } | |
732 | |
733 // static | |
734 void MenuGtk::PointMenuPositionFunc(GtkMenu* menu, | |
735 int* x, | |
736 int* y, | |
737 gboolean* push_in, | |
738 gpointer userdata) { | |
739 *push_in = TRUE; | |
740 | |
741 gfx::Point* point = reinterpret_cast<gfx::Point*>(userdata); | |
742 *x = point->x(); | |
743 *y = point->y(); | |
744 | |
745 GtkRequisition menu_req; | |
746 gtk_widget_size_request(GTK_WIDGET(menu), &menu_req); | |
747 GdkScreen* screen; | |
748 gdk_display_get_pointer(gdk_display_get_default(), &screen, NULL, NULL, NULL); | |
749 gint monitor = gdk_screen_get_monitor_at_point(screen, *x, *y); | |
750 | |
751 GdkRectangle screen_rect; | |
752 gdk_screen_get_monitor_geometry(screen, monitor, &screen_rect); | |
753 | |
754 *y = CalculateMenuYPosition(&screen_rect, &menu_req, NULL, *y); | |
755 } | |
756 | |
757 void MenuGtk::ExecuteCommand(ui::MenuModel* model, int id) { | |
758 if (delegate_) | |
759 delegate_->CommandWillBeExecuted(); | |
760 | |
761 GdkEvent* event = gtk_get_current_event(); | |
762 int event_flags = 0; | |
763 | |
764 if (event && event->type == GDK_BUTTON_RELEASE) | |
765 event_flags = event_utils::EventFlagsFromGdkState(event->button.state); | |
766 model->ActivatedAt(id, event_flags); | |
767 | |
768 if (event) | |
769 gdk_event_free(event); | |
770 } | |
771 | |
772 void MenuGtk::OnMenuShow(GtkWidget* widget) { | |
773 model_->MenuWillShow(); | |
774 base::MessageLoop::current()->PostTask( | |
775 FROM_HERE, base::Bind(&MenuGtk::UpdateMenu, weak_factory_.GetWeakPtr())); | |
776 } | |
777 | |
778 void MenuGtk::OnMenuHidden(GtkWidget* widget) { | |
779 if (delegate_) | |
780 delegate_->StoppedShowing(); | |
781 model_->MenuClosed(); | |
782 } | |
783 | |
784 gboolean MenuGtk::OnMenuFocusOut(GtkWidget* widget, GdkEventFocus* event) { | |
785 gtk_widget_hide(menu_); | |
786 return TRUE; | |
787 } | |
788 | |
789 void MenuGtk::OnSubMenuShow(GtkWidget* submenu) { | |
790 GtkWidget* menu_item = static_cast<GtkWidget*>( | |
791 g_object_get_data(G_OBJECT(submenu), "menu-item")); | |
792 // TODO(mdm): Figure out why this can sometimes be NULL. See bug 131974. | |
793 CHECK(menu_item); | |
794 // Notify the submenu model that the menu will be shown. | |
795 ui::MenuModel* submenu_model = static_cast<ui::MenuModel*>( | |
796 g_object_get_data(G_OBJECT(menu_item), "submenu-model")); | |
797 // We're extra cautious here, and bail out if the submenu model is NULL. In | |
798 // some cases we clear it out from a parent menu; we shouldn't ever show the | |
799 // menu after that, but we play it safe since we're dealing with wacky | |
800 // injected libraries that toy with our menus. (See comments below.) | |
801 if (!submenu_model) | |
802 return; | |
803 | |
804 // If the submenu is already built, then return right away. This means we | |
805 // recently showed this submenu, and have not yet processed the fact that it | |
806 // was hidden before being shown again. | |
807 if (g_object_get_data(G_OBJECT(submenu), "submenu-built")) | |
808 return; | |
809 g_object_set_data(G_OBJECT(submenu), "submenu-built", GINT_TO_POINTER(1)); | |
810 | |
811 submenu_model->MenuWillShow(); | |
812 | |
813 // Actually build the submenu and attach it to the parent menu item. | |
814 BuildSubmenuFromModel(submenu_model, submenu); | |
815 gtk_menu_item_set_submenu(GTK_MENU_ITEM(menu_item), submenu); | |
816 | |
817 // Update all the menu item info in the newly-generated menu. | |
818 gtk_container_foreach(GTK_CONTAINER(submenu), SetMenuItemInfo, this); | |
819 } | |
820 | |
821 void MenuGtk::OnSubMenuHidden(GtkWidget* submenu) { | |
822 // Increase the reference count of the old submenu, and schedule it to be | |
823 // deleted later. We get this hide notification before we've processed menu | |
824 // activations, so if we were to delete the submenu now, we might lose the | |
825 // activation. This also lets us reuse the menu if it is shown again before | |
826 // it gets deleted; in that case, OnSubMenuHiddenCallback() just decrements | |
827 // the reference count again. Note that the delay is just an optimization; we | |
828 // could use PostTask() and this would still work correctly. | |
829 g_object_ref(G_OBJECT(submenu)); | |
830 base::MessageLoop::current()->PostDelayedTask( | |
831 FROM_HERE, | |
832 base::Bind(&MenuGtk::OnSubMenuHiddenCallback, submenu), | |
833 base::TimeDelta::FromSeconds(2)); | |
834 } | |
835 | |
836 namespace { | |
837 | |
838 // Remove all descendant submenu-model data pointers. | |
839 void RemoveSubMenuModels(GtkWidget* menu_item, void* unused) { | |
840 if (!GTK_IS_MENU_ITEM(menu_item)) | |
841 return; | |
842 g_object_steal_data(G_OBJECT(menu_item), "submenu-model"); | |
843 GtkWidget* submenu = gtk_menu_item_get_submenu(GTK_MENU_ITEM(menu_item)); | |
844 if (submenu) | |
845 gtk_container_foreach(GTK_CONTAINER(submenu), RemoveSubMenuModels, NULL); | |
846 } | |
847 | |
848 } // namespace | |
849 | |
850 // static | |
851 void MenuGtk::OnSubMenuHiddenCallback(GtkWidget* submenu) { | |
852 if (!gtk_widget_get_visible(submenu)) { | |
853 // Remove all the children of this menu, clearing out their submenu-model | |
854 // pointers in case they have pending calls to OnSubMenuHiddenCallback(). | |
855 // (Normally that won't happen: we'd have hidden them first, and so they'd | |
856 // have already been deleted. But in some cases [e.g. on Ubuntu 12.04], | |
857 // GTK menu operations may be hooked to allow external applications to | |
858 // mirror the menu structure, and the hooks may show and hide menus in | |
859 // order to trigger exactly the kind of dynamic menu building we're doing. | |
860 // The result is that we see show and hide events in strange orders.) | |
861 GList* children = gtk_container_get_children(GTK_CONTAINER(submenu)); | |
862 for (GList* child = children; child; child = g_list_next(child)) { | |
863 RemoveSubMenuModels(GTK_WIDGET(child->data), NULL); | |
864 gtk_container_remove(GTK_CONTAINER(submenu), GTK_WIDGET(child->data)); | |
865 } | |
866 g_list_free(children); | |
867 | |
868 // Clear out the bit that says the menu is built. | |
869 // We'll rebuild it next time it is shown. | |
870 g_object_steal_data(G_OBJECT(submenu), "submenu-built"); | |
871 | |
872 // Notify the submenu model that the menu has been hidden. This may cause | |
873 // it to delete descendant submenu models, which is why we cleared those | |
874 // pointers out above. | |
875 GtkWidget* menu_item = static_cast<GtkWidget*>( | |
876 g_object_get_data(G_OBJECT(submenu), "menu-item")); | |
877 // TODO(mdm): Figure out why this can sometimes be NULL. See bug 124110. | |
878 CHECK(menu_item); | |
879 ui::MenuModel* submenu_model = static_cast<ui::MenuModel*>( | |
880 g_object_get_data(G_OBJECT(menu_item), "submenu-model")); | |
881 if (submenu_model) | |
882 submenu_model->MenuClosed(); | |
883 } | |
884 | |
885 // Remove the reference we grabbed in OnSubMenuHidden() above. | |
886 g_object_unref(G_OBJECT(submenu)); | |
887 } | |
888 | |
889 // static | |
890 void MenuGtk::SetButtonItemInfo(GtkWidget* button, gpointer userdata) { | |
891 ui::ButtonMenuItemModel* model = | |
892 reinterpret_cast<ui::ButtonMenuItemModel*>( | |
893 g_object_get_data(G_OBJECT(button), "button-model")); | |
894 int index = GPOINTER_TO_INT(g_object_get_data( | |
895 G_OBJECT(button), "button-model-id")); | |
896 | |
897 if (model->IsItemDynamicAt(index)) { | |
898 std::string label = ui::ConvertAcceleratorsFromWindowsStyle( | |
899 base::UTF16ToUTF8(model->GetLabelAt(index))); | |
900 gtk_button_set_label(GTK_BUTTON(button), label.c_str()); | |
901 } | |
902 | |
903 gtk_widget_set_sensitive(GTK_WIDGET(button), model->IsEnabledAt(index)); | |
904 } | |
905 | |
906 // static | |
907 void MenuGtk::SetMenuItemInfo(GtkWidget* widget, gpointer userdata) { | |
908 if (GTK_IS_SEPARATOR_MENU_ITEM(widget)) { | |
909 // We need to explicitly handle this case because otherwise we'll ask the | |
910 // menu delegate about something with an invalid id. | |
911 return; | |
912 } | |
913 | |
914 int id; | |
915 if (!GetMenuItemID(widget, &id)) | |
916 return; | |
917 | |
918 ui::MenuModel* model = ModelForMenuItem(GTK_MENU_ITEM(widget)); | |
919 if (!model) { | |
920 // If we're not providing the sub menu, then there's no model. For | |
921 // example, the IME submenu doesn't have a model. | |
922 return; | |
923 } | |
924 | |
925 if (GTK_IS_CHECK_MENU_ITEM(widget)) { | |
926 GtkCheckMenuItem* item = GTK_CHECK_MENU_ITEM(widget); | |
927 | |
928 // gtk_check_menu_item_set_active() will send the activate signal. Touching | |
929 // the underlying "active" property will also call the "activate" handler | |
930 // for this menu item. So we prevent the "activate" handler from | |
931 // being called while we set the checkbox. | |
932 // Why not use one of the glib signal-blocking functions? Because when we | |
933 // toggle a radio button, it will deactivate one of the other radio buttons, | |
934 // which we don't have a pointer to. | |
935 // Wny not make this a member variable? Because "menu" is a pointer to the | |
936 // root of the MenuGtk and we want to disable *all* MenuGtks, including | |
937 // submenus. | |
938 block_activation_ = true; | |
939 gtk_check_menu_item_set_active(item, model->IsItemCheckedAt(id)); | |
940 block_activation_ = false; | |
941 } | |
942 | |
943 if (GTK_IS_CUSTOM_MENU_ITEM(widget)) { | |
944 // Iterate across all the buttons to update their visible properties. | |
945 gtk_custom_menu_item_foreach_button(GTK_CUSTOM_MENU_ITEM(widget), | |
946 SetButtonItemInfo, | |
947 userdata); | |
948 } | |
949 | |
950 if (GTK_IS_MENU_ITEM(widget)) { | |
951 gtk_widget_set_sensitive(widget, model->IsEnabledAt(id)); | |
952 | |
953 if (model->IsVisibleAt(id)) { | |
954 // Update the menu item label if it is dynamic. | |
955 if (model->IsItemDynamicAt(id)) { | |
956 std::string label = ui::ConvertAcceleratorsFromWindowsStyle( | |
957 base::UTF16ToUTF8(model->GetLabelAt(id))); | |
958 | |
959 gtk_menu_item_set_label(GTK_MENU_ITEM(widget), label.c_str()); | |
960 if (GTK_IS_IMAGE_MENU_ITEM(widget)) { | |
961 gfx::Image icon; | |
962 if (model->GetIconAt(id, &icon)) { | |
963 gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(widget), | |
964 gtk_image_new_from_pixbuf( | |
965 icon.ToGdkPixbuf())); | |
966 } else { | |
967 gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(widget), NULL); | |
968 } | |
969 } | |
970 } | |
971 | |
972 gtk_widget_show(widget); | |
973 } else { | |
974 gtk_widget_hide(widget); | |
975 } | |
976 | |
977 GtkWidget* submenu = gtk_menu_item_get_submenu(GTK_MENU_ITEM(widget)); | |
978 if (submenu) { | |
979 gtk_container_foreach(GTK_CONTAINER(submenu), &SetMenuItemInfo, | |
980 userdata); | |
981 } | |
982 } | |
983 } | |
OLD | NEW |