// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "chrome/browser/ui/gtk/gtk_custom_menu_item.h" #include "base/i18n/rtl.h" #include "chrome/browser/ui/gtk/gtk_custom_menu.h" #include "ui/gfx/gtk_compat.h" // This method was autogenerated by the program glib-genmarshall, which // generated it from the line "BOOL:INT". Two different attempts at getting gyp // to autogenerate this didn't work. If we need more non-standard marshallers, // this should be deleted, and an actual build step should be added. void 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) { typedef gboolean(*GMarshalFunc_BOOLEAN__INT)(gpointer data1, gint arg_1, gpointer data2); register GMarshalFunc_BOOLEAN__INT callback; register GCClosure *cc = (GCClosure*)closure; register gpointer data1, data2; gboolean v_return; g_return_if_fail(return_value != NULL); g_return_if_fail(n_param_values == 2); if (G_CCLOSURE_SWAP_DATA(closure)) { data1 = closure->data; // Note: This line (and the line setting data1 in the other if branch) // were macros in the original autogenerated output. This is with the // macro resolved for release mode. In debug mode, it uses an accessor // that asserts saying that the object pointed to by param_values doesn't // hold a pointer. This appears to be the cause of http://crbug.com/58945. // // This is more than a little odd because the gtype on this first param // isn't set correctly by the time we get here, while I watched it // explicitly set upstack. I verified that v_pointer is still set // correctly. I'm not sure what's going on. :( data2 = (param_values + 0)->data[0].v_pointer; } else { data1 = (param_values + 0)->data[0].v_pointer; data2 = closure->data; } callback = (GMarshalFunc_BOOLEAN__INT)(marshal_data ? marshal_data : cc->callback); v_return = callback(data1, g_value_get_int(param_values + 1), data2); g_value_set_boolean(return_value, v_return); } enum { BUTTON_PUSHED, TRY_BUTTON_PUSHED, LAST_SIGNAL }; static guint custom_menu_item_signals[LAST_SIGNAL] = { 0 }; G_DEFINE_TYPE(GtkCustomMenuItem, gtk_custom_menu_item, GTK_TYPE_MENU_ITEM) static void set_selected(GtkCustomMenuItem* item, GtkWidget* selected) { if (selected != item->currently_selected_button) { if (item->currently_selected_button) { gtk_widget_set_state(item->currently_selected_button, GTK_STATE_NORMAL); gtk_widget_set_state( gtk_bin_get_child(GTK_BIN(item->currently_selected_button)), GTK_STATE_NORMAL); } item->currently_selected_button = selected; if (item->currently_selected_button) { gtk_widget_set_state(item->currently_selected_button, GTK_STATE_SELECTED); gtk_widget_set_state( gtk_bin_get_child(GTK_BIN(item->currently_selected_button)), GTK_STATE_PRELIGHT); } } } // When GtkButtons set the label text, they rebuild the widget hierarchy each // and every time. Therefore, we can't just fish out the label from the button // and set some properties; we have to create this callback function that // listens on the button's "notify" signal, which is emitted right after the // label has been (re)created. (Label values can change dynamically.) static void on_button_label_set(GObject* object) { GtkButton* button = GTK_BUTTON(object); GtkWidget* child = gtk_bin_get_child(GTK_BIN(button)); gtk_widget_set_sensitive(child, FALSE); gtk_misc_set_padding(GTK_MISC(child), 2, 0); } static void gtk_custom_menu_item_finalize(GObject *object); static gint gtk_custom_menu_item_expose(GtkWidget* widget, GdkEventExpose* event); static gboolean gtk_custom_menu_item_hbox_expose(GtkWidget* widget, GdkEventExpose* event, GtkCustomMenuItem* menu_item); static void gtk_custom_menu_item_select(GtkItem *item); static void gtk_custom_menu_item_deselect(GtkItem *item); static void gtk_custom_menu_item_activate(GtkMenuItem* menu_item); static void gtk_custom_menu_item_init(GtkCustomMenuItem* item) { item->all_widgets = NULL; item->button_widgets = NULL; item->currently_selected_button = NULL; item->previously_selected_button = NULL; GtkWidget* menu_hbox = gtk_hbox_new(FALSE, 0); gtk_container_add(GTK_CONTAINER(item), menu_hbox); item->label = gtk_label_new(NULL); gtk_misc_set_alignment(GTK_MISC(item->label), 0.0, 0.5); gtk_box_pack_start(GTK_BOX(menu_hbox), item->label, TRUE, TRUE, 0); item->hbox = gtk_hbox_new(FALSE, 0); gtk_box_pack_end(GTK_BOX(menu_hbox), item->hbox, FALSE, FALSE, 0); g_signal_connect(item->hbox, "expose-event", G_CALLBACK(gtk_custom_menu_item_hbox_expose), item); gtk_widget_show_all(menu_hbox); } static void gtk_custom_menu_item_class_init(GtkCustomMenuItemClass* klass) { GObjectClass* gobject_class = G_OBJECT_CLASS(klass); GtkWidgetClass* widget_class = GTK_WIDGET_CLASS(klass); GtkItemClass* item_class = GTK_ITEM_CLASS(klass); GtkMenuItemClass* menu_item_class = GTK_MENU_ITEM_CLASS(klass); gobject_class->finalize = gtk_custom_menu_item_finalize; widget_class->expose_event = gtk_custom_menu_item_expose; item_class->select = gtk_custom_menu_item_select; item_class->deselect = gtk_custom_menu_item_deselect; menu_item_class->activate = gtk_custom_menu_item_activate; custom_menu_item_signals[BUTTON_PUSHED] = g_signal_new("button-pushed", G_TYPE_FROM_CLASS(gobject_class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, g_cclosure_marshal_VOID__INT, G_TYPE_NONE, 1, G_TYPE_INT); custom_menu_item_signals[TRY_BUTTON_PUSHED] = g_signal_new("try-button-pushed", G_TYPE_FROM_CLASS(gobject_class), G_SIGNAL_RUN_LAST, 0, NULL, NULL, chrome_marshall_BOOLEAN__INT, G_TYPE_BOOLEAN, 1, G_TYPE_INT); } static void gtk_custom_menu_item_finalize(GObject *object) { GtkCustomMenuItem* item = GTK_CUSTOM_MENU_ITEM(object); g_list_free(item->all_widgets); g_list_free(item->button_widgets); G_OBJECT_CLASS(gtk_custom_menu_item_parent_class)->finalize(object); } static gint gtk_custom_menu_item_expose(GtkWidget* widget, GdkEventExpose* event) { if (gtk_widget_get_visible(widget) && gtk_widget_get_mapped(widget) && gtk_bin_get_child(GTK_BIN(widget))) { // We skip the drawing in the GtkMenuItem class it draws the highlighted // background and we don't want that. gtk_container_propagate_expose(GTK_CONTAINER(widget), gtk_bin_get_child(GTK_BIN(widget)), event); } return FALSE; } static void gtk_custom_menu_item_expose_button(GtkWidget* hbox, GdkEventExpose* event, GList* button_item) { // We search backwards to find the leftmost and rightmost buttons. The // current button may be that button. GtkWidget* current_button = GTK_WIDGET(button_item->data); GtkWidget* first_button = current_button; for (GList* i = button_item; i && GTK_IS_BUTTON(i->data); i = g_list_previous(i)) { first_button = GTK_WIDGET(i->data); } GtkWidget* last_button = current_button; for (GList* i = button_item; i && GTK_IS_BUTTON(i->data); i = g_list_next(i)) { last_button = GTK_WIDGET(i->data); } if (base::i18n::IsRTL()) std::swap(first_button, last_button); GtkAllocation first_allocation; gtk_widget_get_allocation(first_button, &first_allocation); GtkAllocation current_allocation; gtk_widget_get_allocation(current_button, ¤t_allocation); GtkAllocation last_allocation; gtk_widget_get_allocation(last_button, &last_allocation); int x = first_allocation.x; int y = first_allocation.y; int width = last_allocation.width + last_allocation.x - first_allocation.x; int height = last_allocation.height; gtk_paint_box(gtk_widget_get_style(hbox), gtk_widget_get_window(hbox), gtk_widget_get_state(current_button), GTK_SHADOW_OUT, ¤t_allocation, hbox, "button", x, y, width, height); // Propagate to the button's children. gtk_container_propagate_expose( GTK_CONTAINER(current_button), gtk_bin_get_child(GTK_BIN(current_button)), event); } static gboolean gtk_custom_menu_item_hbox_expose(GtkWidget* widget, GdkEventExpose* event, GtkCustomMenuItem* menu_item) { // First render all the buttons that aren't the currently selected item. for (GList* current_item = menu_item->all_widgets; current_item != NULL; current_item = g_list_next(current_item)) { if (GTK_IS_BUTTON(current_item->data)) { if (GTK_WIDGET(current_item->data) != menu_item->currently_selected_button) { gtk_custom_menu_item_expose_button(widget, event, current_item); } } } // As a separate pass, draw the buton separators above. We need to draw the // separators in a separate pass because we are drawing on top of the // buttons. Otherwise, the vlines are overwritten by the next button. for (GList* current_item = menu_item->all_widgets; current_item != NULL; current_item = g_list_next(current_item)) { if (GTK_IS_BUTTON(current_item->data)) { // Check to see if this is the last button in a run. GList* next_item = g_list_next(current_item); if (next_item && GTK_IS_BUTTON(next_item->data)) { GtkWidget* current_button = GTK_WIDGET(current_item->data); GtkAllocation button_allocation; gtk_widget_get_allocation(current_button, &button_allocation); GtkAllocation child_alloc; gtk_widget_get_allocation(gtk_bin_get_child(GTK_BIN(current_button)), &child_alloc); GtkStyle* style = gtk_widget_get_style(widget); int half_offset = style->xthickness / 2; gtk_paint_vline(style, gtk_widget_get_window(widget), gtk_widget_get_state(current_button), &event->area, widget, "button", child_alloc.y, child_alloc.y + child_alloc.height, button_allocation.x + button_allocation.width - half_offset); } } } // Finally, draw the selected item on top of the separators so there are no // artifacts inside the button area. GList* selected = g_list_find(menu_item->all_widgets, menu_item->currently_selected_button); if (selected) { gtk_custom_menu_item_expose_button(widget, event, selected); } return TRUE; } static void gtk_custom_menu_item_select(GtkItem* item) { GtkCustomMenuItem* custom_item = GTK_CUSTOM_MENU_ITEM(item); // When we are selected, the only thing we do is clear information from // previous selections. Actual selection of a button is done either in the // "mouse-motion-event" or is manually set from GtkCustomMenu's overridden // "move-current" handler. custom_item->previously_selected_button = NULL; gtk_widget_queue_draw(GTK_WIDGET(item)); } static void gtk_custom_menu_item_deselect(GtkItem* item) { GtkCustomMenuItem* custom_item = GTK_CUSTOM_MENU_ITEM(item); // When we are deselected, we store the item that was currently selected so // that it can be acted on. Menu items are first deselected before they are // activated. custom_item->previously_selected_button = custom_item->currently_selected_button; if (custom_item->currently_selected_button) set_selected(custom_item, NULL); gtk_widget_queue_draw(GTK_WIDGET(item)); } static void gtk_custom_menu_item_activate(GtkMenuItem* menu_item) { GtkCustomMenuItem* custom_item = GTK_CUSTOM_MENU_ITEM(menu_item); // We look at |previously_selected_button| because by the time we've been // activated, we've already gone through our deselect handler. if (custom_item->previously_selected_button) { gpointer id_ptr = g_object_get_data( G_OBJECT(custom_item->previously_selected_button), "command-id"); if (id_ptr != NULL) { int command_id = GPOINTER_TO_INT(id_ptr); g_signal_emit(custom_item, custom_menu_item_signals[BUTTON_PUSHED], 0, command_id); set_selected(custom_item, NULL); } } } GtkWidget* gtk_custom_menu_item_new(const char* title) { GtkCustomMenuItem* item = GTK_CUSTOM_MENU_ITEM( g_object_new(GTK_TYPE_CUSTOM_MENU_ITEM, NULL)); gtk_label_set_text(GTK_LABEL(item->label), title); return GTK_WIDGET(item); } GtkWidget* gtk_custom_menu_item_add_button(GtkCustomMenuItem* menu_item, int command_id) { GtkWidget* button = gtk_button_new(); g_object_set_data(G_OBJECT(button), "command-id", GINT_TO_POINTER(command_id)); gtk_box_pack_start(GTK_BOX(menu_item->hbox), button, FALSE, FALSE, 0); gtk_widget_show(button); menu_item->all_widgets = g_list_append(menu_item->all_widgets, button); menu_item->button_widgets = g_list_append(menu_item->button_widgets, button); return button; } GtkWidget* gtk_custom_menu_item_add_button_label(GtkCustomMenuItem* menu_item, int command_id) { GtkWidget* button = gtk_button_new_with_label(""); g_object_set_data(G_OBJECT(button), "command-id", GINT_TO_POINTER(command_id)); gtk_box_pack_start(GTK_BOX(menu_item->hbox), button, FALSE, FALSE, 0); g_signal_connect(button, "notify::label", G_CALLBACK(on_button_label_set), NULL); gtk_widget_show(button); menu_item->all_widgets = g_list_append(menu_item->all_widgets, button); return button; } void gtk_custom_menu_item_add_space(GtkCustomMenuItem* menu_item) { GtkWidget* fixed = gtk_fixed_new(); gtk_widget_set_size_request(fixed, 5, -1); gtk_box_pack_start(GTK_BOX(menu_item->hbox), fixed, FALSE, FALSE, 0); gtk_widget_show(fixed); menu_item->all_widgets = g_list_append(menu_item->all_widgets, fixed); } void gtk_custom_menu_item_receive_motion_event(GtkCustomMenuItem* menu_item, gdouble x, gdouble y) { GtkWidget* new_selected_widget = NULL; GList* current = menu_item->button_widgets; for (; current != NULL; current = current->next) { GtkWidget* current_widget = GTK_WIDGET(current->data); GtkAllocation alloc; gtk_widget_get_allocation(current_widget, &alloc); int offset_x, offset_y; gtk_widget_translate_coordinates(current_widget, GTK_WIDGET(menu_item), 0, 0, &offset_x, &offset_y); if (x >= offset_x && x < (offset_x + alloc.width) && y >= offset_y && y < (offset_y + alloc.height)) { new_selected_widget = current_widget; break; } } set_selected(menu_item, new_selected_widget); } gboolean gtk_custom_menu_item_handle_move(GtkCustomMenuItem* menu_item, GtkMenuDirectionType direction) { GtkWidget* current = menu_item->currently_selected_button; if (menu_item->button_widgets && current) { switch (direction) { case GTK_MENU_DIR_PREV: { if (g_list_first(menu_item->button_widgets)->data == current) return FALSE; set_selected(menu_item, GTK_WIDGET(g_list_previous(g_list_find( menu_item->button_widgets, current))->data)); break; } case GTK_MENU_DIR_NEXT: { if (g_list_last(menu_item->button_widgets)->data == current) return FALSE; set_selected(menu_item, GTK_WIDGET(g_list_next(g_list_find( menu_item->button_widgets, current))->data)); break; } default: break; } } return TRUE; } void gtk_custom_menu_item_select_item_by_direction( GtkCustomMenuItem* menu_item, GtkMenuDirectionType direction) { menu_item->previously_selected_button = NULL; // If we're just told to be selected by the menu system, select the first // item. if (menu_item->button_widgets) { switch (direction) { case GTK_MENU_DIR_PREV: { GtkWidget* last_button = GTK_WIDGET(g_list_last(menu_item->button_widgets)->data); if (last_button) set_selected(menu_item, last_button); break; } case GTK_MENU_DIR_NEXT: { GtkWidget* first_button = GTK_WIDGET(g_list_first(menu_item->button_widgets)->data); if (first_button) set_selected(menu_item, first_button); break; } default: break; } } gtk_widget_queue_draw(GTK_WIDGET(menu_item)); } gboolean gtk_custom_menu_item_is_in_clickable_region( GtkCustomMenuItem* menu_item) { return menu_item->currently_selected_button != NULL; } gboolean gtk_custom_menu_item_try_no_dismiss_command( GtkCustomMenuItem* menu_item) { GtkCustomMenuItem* custom_item = GTK_CUSTOM_MENU_ITEM(menu_item); gboolean activated = TRUE; // We work with |currently_selected_button| instead of // |previously_selected_button| since we haven't been "deselect"ed yet. gpointer id_ptr = g_object_get_data( G_OBJECT(custom_item->currently_selected_button), "command-id"); if (id_ptr != NULL) { int command_id = GPOINTER_TO_INT(id_ptr); g_signal_emit(custom_item, custom_menu_item_signals[TRY_BUTTON_PUSHED], 0, command_id, &activated); } return activated; } void gtk_custom_menu_item_foreach_button(GtkCustomMenuItem* menu_item, GtkCallback callback, gpointer callback_data) { // Even though we're filtering |all_widgets| on GTK_IS_BUTTON(), this isn't // equivalent to |button_widgets| because we also want the button-labels. for (GList* i = menu_item->all_widgets; i && GTK_IS_BUTTON(i->data); i = g_list_next(i)) { if (GTK_IS_BUTTON(i->data)) { callback(GTK_WIDGET(i->data), callback_data); } } }