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