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 ¤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 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