Home | History | Annotate | Download | only in gtk
      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.
     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 
     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.)
     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 
    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 
    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 
    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 
    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 
    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                 &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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    446 gboolean gtk_custom_menu_item_is_in_clickable_region(
    447     GtkCustomMenuItem* menu_item) {
    448   return menu_item->currently_selected_button != NULL;
    449 }
    450 
    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 
    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