gtk_custom_menu_item.cc revision ddb351dbec246cf1fab5ec20d2d5520909041de1
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.
14void 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
58enum {
59  BUTTON_PUSHED,
60  TRY_BUTTON_PUSHED,
61  LAST_SIGNAL
62};
63
64static guint custom_menu_item_signals[LAST_SIGNAL] = { 0 };
65
66G_DEFINE_TYPE(GtkCustomMenuItem, gtk_custom_menu_item, GTK_TYPE_MENU_ITEM)
67
68static 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.)
92static 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
98static void gtk_custom_menu_item_finalize(GObject *object);
99static gint gtk_custom_menu_item_expose(GtkWidget* widget,
100                                        GdkEventExpose* event);
101static gboolean gtk_custom_menu_item_hbox_expose(GtkWidget* widget,
102                                                 GdkEventExpose* event,
103                                                 GtkCustomMenuItem* menu_item);
104static void gtk_custom_menu_item_select(GtkItem *item);
105static void gtk_custom_menu_item_deselect(GtkItem *item);
106static void gtk_custom_menu_item_activate(GtkMenuItem* menu_item);
107
108static 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
131static 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
164static 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
172static 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
187static 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                &current_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
228static 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
278static 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
290static 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
304static 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
321GtkWidget* 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
328GtkWidget* 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
342GtkWidget* 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
357void 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
367void 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
387gboolean 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
416void 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
446gboolean gtk_custom_menu_item_is_in_clickable_region(
447    GtkCustomMenuItem* menu_item) {
448  return menu_item->currently_selected_button != NULL;
449}
450
451gboolean 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
469void 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