1 // Copyright (c) 2011 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/gtk_custom_menu_item.h"
6
7 #include "base/i18n/rtl.h"
8 #include "chrome/browser/ui/gtk/gtk_custom_menu.h"
9
10 // This method was autogenerated by the program glib-genmarshall, which
11 // generated it from the line "BOOL:INT". Two different attempts at getting gyp
12 // to autogenerate this didn't work. If we need more non-standard marshallers,
13 // this should be deleted, and an actual build step should be added.
chrome_marshall_BOOLEAN__INT(GClosure * closure,GValue * return_value G_GNUC_UNUSED,guint n_param_values,const GValue * param_values,gpointer invocation_hint G_GNUC_UNUSED,gpointer marshal_data)14 void chrome_marshall_BOOLEAN__INT(GClosure* closure,
15 GValue* return_value G_GNUC_UNUSED,
16 guint n_param_values,
17 const GValue* param_values,
18 gpointer invocation_hint G_GNUC_UNUSED,
19 gpointer marshal_data) {
20 typedef gboolean(*GMarshalFunc_BOOLEAN__INT)(gpointer data1,
21 gint arg_1,
22 gpointer data2);
23 register GMarshalFunc_BOOLEAN__INT callback;
24 register GCClosure *cc = (GCClosure*)closure;
25 register gpointer data1, data2;
26 gboolean v_return;
27
28 g_return_if_fail(return_value != NULL);
29 g_return_if_fail(n_param_values == 2);
30
31 if (G_CCLOSURE_SWAP_DATA(closure)) {
32 data1 = closure->data;
33 // Note: This line (and the line setting data1 in the other if branch)
34 // were macros in the original autogenerated output. This is with the
35 // macro resolved for release mode. In debug mode, it uses an accessor
36 // that asserts saying that the object pointed to by param_values doesn't
37 // hold a pointer. This appears to be the cause of http://crbug.com/58945.
38 //
39 // This is more than a little odd because the gtype on this first param
40 // isn't set correctly by the time we get here, while I watched it
41 // explicitly set upstack. I verified that v_pointer is still set
42 // correctly. I'm not sure what's going on. :(
43 data2 = (param_values + 0)->data[0].v_pointer;
44 } else {
45 data1 = (param_values + 0)->data[0].v_pointer;
46 data2 = closure->data;
47 }
48 callback = (GMarshalFunc_BOOLEAN__INT)(marshal_data ? marshal_data :
49 cc->callback);
50
51 v_return = callback(data1,
52 g_value_get_int(param_values + 1),
53 data2);
54
55 g_value_set_boolean(return_value, v_return);
56 }
57
58 enum {
59 BUTTON_PUSHED,
60 TRY_BUTTON_PUSHED,
61 LAST_SIGNAL
62 };
63
64 static guint custom_menu_item_signals[LAST_SIGNAL] = { 0 };
65
G_DEFINE_TYPE(GtkCustomMenuItem,gtk_custom_menu_item,GTK_TYPE_MENU_ITEM)66 G_DEFINE_TYPE(GtkCustomMenuItem, gtk_custom_menu_item, GTK_TYPE_MENU_ITEM)
67
68 static void set_selected(GtkCustomMenuItem* item, GtkWidget* selected) {
69 if (selected != item->currently_selected_button) {
70 if (item->currently_selected_button) {
71 gtk_widget_set_state(item->currently_selected_button, GTK_STATE_NORMAL);
72 gtk_widget_set_state(
73 gtk_bin_get_child(GTK_BIN(item->currently_selected_button)),
74 GTK_STATE_NORMAL);
75 }
76
77 item->currently_selected_button = selected;
78 if (item->currently_selected_button) {
79 gtk_widget_set_state(item->currently_selected_button, GTK_STATE_SELECTED);
80 gtk_widget_set_state(
81 gtk_bin_get_child(GTK_BIN(item->currently_selected_button)),
82 GTK_STATE_PRELIGHT);
83 }
84 }
85 }
86
87 // When GtkButtons set the label text, they rebuild the widget hierarchy each
88 // and every time. Therefore, we can't just fish out the label from the button
89 // and set some properties; we have to create this callback function that
90 // listens on the button's "notify" signal, which is emitted right after the
91 // label has been (re)created. (Label values can change dynamically.)
on_button_label_set(GObject * object)92 static void on_button_label_set(GObject* object) {
93 GtkButton* button = GTK_BUTTON(object);
94 gtk_widget_set_sensitive(GTK_BIN(button)->child, FALSE);
95 gtk_misc_set_padding(GTK_MISC(GTK_BIN(button)->child), 2, 0);
96 }
97
98 static void gtk_custom_menu_item_finalize(GObject *object);
99 static gint gtk_custom_menu_item_expose(GtkWidget* widget,
100 GdkEventExpose* event);
101 static gboolean gtk_custom_menu_item_hbox_expose(GtkWidget* widget,
102 GdkEventExpose* event,
103 GtkCustomMenuItem* menu_item);
104 static void gtk_custom_menu_item_select(GtkItem *item);
105 static void gtk_custom_menu_item_deselect(GtkItem *item);
106 static void gtk_custom_menu_item_activate(GtkMenuItem* menu_item);
107
gtk_custom_menu_item_init(GtkCustomMenuItem * item)108 static void gtk_custom_menu_item_init(GtkCustomMenuItem* item) {
109 item->all_widgets = NULL;
110 item->button_widgets = NULL;
111 item->currently_selected_button = NULL;
112 item->previously_selected_button = NULL;
113
114 GtkWidget* menu_hbox = gtk_hbox_new(FALSE, 0);
115 gtk_container_add(GTK_CONTAINER(item), menu_hbox);
116
117 item->label = gtk_label_new(NULL);
118 gtk_misc_set_alignment(GTK_MISC(item->label), 0.0, 0.5);
119 gtk_box_pack_start(GTK_BOX(menu_hbox), item->label, TRUE, TRUE, 0);
120
121 item->hbox = gtk_hbox_new(FALSE, 0);
122 gtk_box_pack_end(GTK_BOX(menu_hbox), item->hbox, FALSE, FALSE, 0);
123
124 g_signal_connect(item->hbox, "expose-event",
125 G_CALLBACK(gtk_custom_menu_item_hbox_expose),
126 item);
127
128 gtk_widget_show_all(menu_hbox);
129 }
130
gtk_custom_menu_item_class_init(GtkCustomMenuItemClass * klass)131 static void gtk_custom_menu_item_class_init(GtkCustomMenuItemClass* klass) {
132 GObjectClass* gobject_class = G_OBJECT_CLASS(klass);
133 GtkWidgetClass* widget_class = GTK_WIDGET_CLASS(klass);
134 GtkItemClass* item_class = GTK_ITEM_CLASS(klass);
135 GtkMenuItemClass* menu_item_class = GTK_MENU_ITEM_CLASS(klass);
136
137 gobject_class->finalize = gtk_custom_menu_item_finalize;
138
139 widget_class->expose_event = gtk_custom_menu_item_expose;
140
141 item_class->select = gtk_custom_menu_item_select;
142 item_class->deselect = gtk_custom_menu_item_deselect;
143
144 menu_item_class->activate = gtk_custom_menu_item_activate;
145
146 custom_menu_item_signals[BUTTON_PUSHED] =
147 g_signal_new("button-pushed",
148 G_TYPE_FROM_CLASS(gobject_class),
149 G_SIGNAL_RUN_FIRST,
150 0,
151 NULL, NULL,
152 gtk_marshal_NONE__INT,
153 G_TYPE_NONE, 1, GTK_TYPE_INT);
154 custom_menu_item_signals[TRY_BUTTON_PUSHED] =
155 g_signal_new("try-button-pushed",
156 G_TYPE_FROM_CLASS(gobject_class),
157 G_SIGNAL_RUN_LAST,
158 0,
159 NULL, NULL,
160 chrome_marshall_BOOLEAN__INT,
161 G_TYPE_BOOLEAN, 1, GTK_TYPE_INT);
162 }
163
gtk_custom_menu_item_finalize(GObject * object)164 static void gtk_custom_menu_item_finalize(GObject *object) {
165 GtkCustomMenuItem* item = GTK_CUSTOM_MENU_ITEM(object);
166 g_list_free(item->all_widgets);
167 g_list_free(item->button_widgets);
168
169 G_OBJECT_CLASS(gtk_custom_menu_item_parent_class)->finalize(object);
170 }
171
gtk_custom_menu_item_expose(GtkWidget * widget,GdkEventExpose * event)172 static gint gtk_custom_menu_item_expose(GtkWidget* widget,
173 GdkEventExpose* event) {
174 if (GTK_WIDGET_VISIBLE(widget) &&
175 GTK_WIDGET_MAPPED(widget) &&
176 gtk_bin_get_child(GTK_BIN(widget))) {
177 // We skip the drawing in the GtkMenuItem class it draws the highlighted
178 // background and we don't want that.
179 gtk_container_propagate_expose(GTK_CONTAINER(widget),
180 gtk_bin_get_child(GTK_BIN(widget)),
181 event);
182 }
183
184 return FALSE;
185 }
186
gtk_custom_menu_item_expose_button(GtkWidget * hbox,GdkEventExpose * event,GList * button_item)187 static void gtk_custom_menu_item_expose_button(GtkWidget* hbox,
188 GdkEventExpose* event,
189 GList* button_item) {
190 // We search backwards to find the leftmost and rightmost buttons. The
191 // current button may be that button.
192 GtkWidget* current_button = GTK_WIDGET(button_item->data);
193 GtkWidget* first_button = current_button;
194 for (GList* i = button_item; i && GTK_IS_BUTTON(i->data);
195 i = g_list_previous(i)) {
196 first_button = GTK_WIDGET(i->data);
197 }
198
199 GtkWidget* last_button = current_button;
200 for (GList* i = button_item; i && GTK_IS_BUTTON(i->data);
201 i = g_list_next(i)) {
202 last_button = GTK_WIDGET(i->data);
203 }
204
205 if (base::i18n::IsRTL())
206 std::swap(first_button, last_button);
207
208 int x = first_button->allocation.x;
209 int y = first_button->allocation.y;
210 int width = last_button->allocation.width + last_button->allocation.x -
211 first_button->allocation.x;
212 int height = last_button->allocation.height;
213
214 gtk_paint_box(hbox->style, hbox->window,
215 static_cast<GtkStateType>(
216 GTK_WIDGET_STATE(current_button)),
217 GTK_SHADOW_OUT,
218 ¤t_button->allocation, hbox, "button",
219 x, y, width, height);
220
221 // Propagate to the button's children.
222 gtk_container_propagate_expose(
223 GTK_CONTAINER(current_button),
224 gtk_bin_get_child(GTK_BIN(current_button)),
225 event);
226 }
227
gtk_custom_menu_item_hbox_expose(GtkWidget * widget,GdkEventExpose * event,GtkCustomMenuItem * menu_item)228 static gboolean gtk_custom_menu_item_hbox_expose(GtkWidget* widget,
229 GdkEventExpose* event,
230 GtkCustomMenuItem* menu_item) {
231 // First render all the buttons that aren't the currently selected item.
232 for (GList* current_item = menu_item->all_widgets;
233 current_item != NULL; current_item = g_list_next(current_item)) {
234 if (GTK_IS_BUTTON(current_item->data)) {
235 if (GTK_WIDGET(current_item->data) !=
236 menu_item->currently_selected_button) {
237 gtk_custom_menu_item_expose_button(widget, event, current_item);
238 }
239 }
240 }
241
242 // As a separate pass, draw the buton separators above. We need to draw the
243 // separators in a separate pass because we are drawing on top of the
244 // buttons. Otherwise, the vlines are overwritten by the next button.
245 for (GList* current_item = menu_item->all_widgets;
246 current_item != NULL; current_item = g_list_next(current_item)) {
247 if (GTK_IS_BUTTON(current_item->data)) {
248 // Check to see if this is the last button in a run.
249 GList* next_item = g_list_next(current_item);
250 if (next_item && GTK_IS_BUTTON(next_item->data)) {
251 GtkWidget* current_button = GTK_WIDGET(current_item->data);
252 GtkAllocation child_alloc =
253 gtk_bin_get_child(GTK_BIN(current_button))->allocation;
254 int half_offset = widget->style->xthickness / 2;
255 gtk_paint_vline(widget->style, widget->window,
256 static_cast<GtkStateType>(
257 GTK_WIDGET_STATE(current_button)),
258 &event->area, widget, "button",
259 child_alloc.y,
260 child_alloc.y + child_alloc.height,
261 current_button->allocation.x +
262 current_button->allocation.width - half_offset);
263 }
264 }
265 }
266
267 // Finally, draw the selected item on top of the separators so there are no
268 // artifacts inside the button area.
269 GList* selected = g_list_find(menu_item->all_widgets,
270 menu_item->currently_selected_button);
271 if (selected) {
272 gtk_custom_menu_item_expose_button(widget, event, selected);
273 }
274
275 return TRUE;
276 }
277
gtk_custom_menu_item_select(GtkItem * item)278 static void gtk_custom_menu_item_select(GtkItem* item) {
279 GtkCustomMenuItem* custom_item = GTK_CUSTOM_MENU_ITEM(item);
280
281 // When we are selected, the only thing we do is clear information from
282 // previous selections. Actual selection of a button is done either in the
283 // "mouse-motion-event" or is manually set from GtkCustomMenu's overridden
284 // "move-current" handler.
285 custom_item->previously_selected_button = NULL;
286
287 gtk_widget_queue_draw(GTK_WIDGET(item));
288 }
289
gtk_custom_menu_item_deselect(GtkItem * item)290 static void gtk_custom_menu_item_deselect(GtkItem* item) {
291 GtkCustomMenuItem* custom_item = GTK_CUSTOM_MENU_ITEM(item);
292
293 // When we are deselected, we store the item that was currently selected so
294 // that it can be acted on. Menu items are first deselected before they are
295 // activated.
296 custom_item->previously_selected_button =
297 custom_item->currently_selected_button;
298 if (custom_item->currently_selected_button)
299 set_selected(custom_item, NULL);
300
301 gtk_widget_queue_draw(GTK_WIDGET(item));
302 }
303
gtk_custom_menu_item_activate(GtkMenuItem * menu_item)304 static void gtk_custom_menu_item_activate(GtkMenuItem* menu_item) {
305 GtkCustomMenuItem* custom_item = GTK_CUSTOM_MENU_ITEM(menu_item);
306
307 // We look at |previously_selected_button| because by the time we've been
308 // activated, we've already gone through our deselect handler.
309 if (custom_item->previously_selected_button) {
310 gpointer id_ptr = g_object_get_data(
311 G_OBJECT(custom_item->previously_selected_button), "command-id");
312 if (id_ptr != NULL) {
313 int command_id = GPOINTER_TO_INT(id_ptr);
314 g_signal_emit(custom_item, custom_menu_item_signals[BUTTON_PUSHED], 0,
315 command_id);
316 set_selected(custom_item, NULL);
317 }
318 }
319 }
320
gtk_custom_menu_item_new(const char * title)321 GtkWidget* gtk_custom_menu_item_new(const char* title) {
322 GtkCustomMenuItem* item = GTK_CUSTOM_MENU_ITEM(
323 g_object_new(GTK_TYPE_CUSTOM_MENU_ITEM, NULL));
324 gtk_label_set_text(GTK_LABEL(item->label), title);
325 return GTK_WIDGET(item);
326 }
327
gtk_custom_menu_item_add_button(GtkCustomMenuItem * menu_item,int command_id)328 GtkWidget* gtk_custom_menu_item_add_button(GtkCustomMenuItem* menu_item,
329 int command_id) {
330 GtkWidget* button = gtk_button_new();
331 g_object_set_data(G_OBJECT(button), "command-id",
332 GINT_TO_POINTER(command_id));
333 gtk_box_pack_start(GTK_BOX(menu_item->hbox), button, FALSE, FALSE, 0);
334 gtk_widget_show(button);
335
336 menu_item->all_widgets = g_list_append(menu_item->all_widgets, button);
337 menu_item->button_widgets = g_list_append(menu_item->button_widgets, button);
338
339 return button;
340 }
341
gtk_custom_menu_item_add_button_label(GtkCustomMenuItem * menu_item,int command_id)342 GtkWidget* gtk_custom_menu_item_add_button_label(GtkCustomMenuItem* menu_item,
343 int command_id) {
344 GtkWidget* button = gtk_button_new_with_label("");
345 g_object_set_data(G_OBJECT(button), "command-id",
346 GINT_TO_POINTER(command_id));
347 gtk_box_pack_start(GTK_BOX(menu_item->hbox), button, FALSE, FALSE, 0);
348 g_signal_connect(button, "notify::label",
349 G_CALLBACK(on_button_label_set), NULL);
350 gtk_widget_show(button);
351
352 menu_item->all_widgets = g_list_append(menu_item->all_widgets, button);
353
354 return button;
355 }
356
gtk_custom_menu_item_add_space(GtkCustomMenuItem * menu_item)357 void gtk_custom_menu_item_add_space(GtkCustomMenuItem* menu_item) {
358 GtkWidget* fixed = gtk_fixed_new();
359 gtk_widget_set_size_request(fixed, 5, -1);
360
361 gtk_box_pack_start(GTK_BOX(menu_item->hbox), fixed, FALSE, FALSE, 0);
362 gtk_widget_show(fixed);
363
364 menu_item->all_widgets = g_list_append(menu_item->all_widgets, fixed);
365 }
366
gtk_custom_menu_item_receive_motion_event(GtkCustomMenuItem * menu_item,gdouble x,gdouble y)367 void gtk_custom_menu_item_receive_motion_event(GtkCustomMenuItem* menu_item,
368 gdouble x, gdouble y) {
369 GtkWidget* new_selected_widget = NULL;
370 GList* current = menu_item->button_widgets;
371 for (; current != NULL; current = current->next) {
372 GtkWidget* current_widget = GTK_WIDGET(current->data);
373 GtkAllocation alloc = current_widget->allocation;
374 int offset_x, offset_y;
375 gtk_widget_translate_coordinates(current_widget, GTK_WIDGET(menu_item),
376 0, 0, &offset_x, &offset_y);
377 if (x >= offset_x && x < (offset_x + alloc.width) &&
378 y >= offset_y && y < (offset_y + alloc.height)) {
379 new_selected_widget = current_widget;
380 break;
381 }
382 }
383
384 set_selected(menu_item, new_selected_widget);
385 }
386
gtk_custom_menu_item_handle_move(GtkCustomMenuItem * menu_item,GtkMenuDirectionType direction)387 gboolean gtk_custom_menu_item_handle_move(GtkCustomMenuItem* menu_item,
388 GtkMenuDirectionType direction) {
389 GtkWidget* current = menu_item->currently_selected_button;
390 if (menu_item->button_widgets && current) {
391 switch (direction) {
392 case GTK_MENU_DIR_PREV: {
393 if (g_list_first(menu_item->button_widgets)->data == current)
394 return FALSE;
395
396 set_selected(menu_item, GTK_WIDGET(g_list_previous(g_list_find(
397 menu_item->button_widgets, current))->data));
398 break;
399 }
400 case GTK_MENU_DIR_NEXT: {
401 if (g_list_last(menu_item->button_widgets)->data == current)
402 return FALSE;
403
404 set_selected(menu_item, GTK_WIDGET(g_list_next(g_list_find(
405 menu_item->button_widgets, current))->data));
406 break;
407 }
408 default:
409 break;
410 }
411 }
412
413 return TRUE;
414 }
415
gtk_custom_menu_item_select_item_by_direction(GtkCustomMenuItem * menu_item,GtkMenuDirectionType direction)416 void gtk_custom_menu_item_select_item_by_direction(
417 GtkCustomMenuItem* menu_item, GtkMenuDirectionType direction) {
418 menu_item->previously_selected_button = NULL;
419
420 // If we're just told to be selected by the menu system, select the first
421 // item.
422 if (menu_item->button_widgets) {
423 switch (direction) {
424 case GTK_MENU_DIR_PREV: {
425 GtkWidget* last_button =
426 GTK_WIDGET(g_list_last(menu_item->button_widgets)->data);
427 if (last_button)
428 set_selected(menu_item, last_button);
429 break;
430 }
431 case GTK_MENU_DIR_NEXT: {
432 GtkWidget* first_button =
433 GTK_WIDGET(g_list_first(menu_item->button_widgets)->data);
434 if (first_button)
435 set_selected(menu_item, first_button);
436 break;
437 }
438 default:
439 break;
440 }
441 }
442
443 gtk_widget_queue_draw(GTK_WIDGET(menu_item));
444 }
445
gtk_custom_menu_item_is_in_clickable_region(GtkCustomMenuItem * menu_item)446 gboolean gtk_custom_menu_item_is_in_clickable_region(
447 GtkCustomMenuItem* menu_item) {
448 return menu_item->currently_selected_button != NULL;
449 }
450
gtk_custom_menu_item_try_no_dismiss_command(GtkCustomMenuItem * menu_item)451 gboolean gtk_custom_menu_item_try_no_dismiss_command(
452 GtkCustomMenuItem* menu_item) {
453 GtkCustomMenuItem* custom_item = GTK_CUSTOM_MENU_ITEM(menu_item);
454 gboolean activated = TRUE;
455
456 // We work with |currently_selected_button| instead of
457 // |previously_selected_button| since we haven't been "deselect"ed yet.
458 gpointer id_ptr = g_object_get_data(
459 G_OBJECT(custom_item->currently_selected_button), "command-id");
460 if (id_ptr != NULL) {
461 int command_id = GPOINTER_TO_INT(id_ptr);
462 g_signal_emit(custom_item, custom_menu_item_signals[TRY_BUTTON_PUSHED], 0,
463 command_id, &activated);
464 }
465
466 return activated;
467 }
468
gtk_custom_menu_item_foreach_button(GtkCustomMenuItem * menu_item,GtkCallback callback,gpointer callback_data)469 void gtk_custom_menu_item_foreach_button(GtkCustomMenuItem* menu_item,
470 GtkCallback callback,
471 gpointer callback_data) {
472 // Even though we're filtering |all_widgets| on GTK_IS_BUTTON(), this isn't
473 // equivalent to |button_widgets| because we also want the button-labels.
474 for (GList* i = menu_item->all_widgets; i && GTK_IS_BUTTON(i->data);
475 i = g_list_next(i)) {
476 if (GTK_IS_BUTTON(i->data)) {
477 callback(GTK_WIDGET(i->data), callback_data);
478 }
479 }
480 }
481