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