src/mn-tooltips.gob (16645B) - raw
1 /* 2 * MNTooltips - a tooltips implementation allowing to use an arbitrary 3 * widget as tooltip. Update: this functionality is now supported by 4 * GTK+ (as of version 2.12), but unfortunately it is broken 5 * (http://bugzilla.gnome.org/show_bug.cgi?id=504087). 6 * 7 * Heavily based on GtkTooltips, 8 * Copyright (C) 1995-1997 Peter Mattis, Spencer Kimball and Josh MacDonald 9 * 10 * Mail Notification 11 * Copyright (C) 2003-2008 Jean-Yves Lefort <jylefort@brutele.be> 12 * 13 * This program is free software; you can redistribute it and/or modify 14 * it under the terms of the GNU General Public License as published by 15 * the Free Software Foundation; either version 3 of the License, or 16 * (at your option) any later version. 17 * 18 * This program is distributed in the hope that it will be useful, 19 * but WITHOUT ANY WARRANTY; without even the implied warranty of 20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 21 * GNU General Public License for more details. 22 * 23 * You should have received a copy of the GNU General Public License along 24 * with this program; if not, write to the Free Software Foundation, Inc., 25 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 26 */ 27 28 %headertop{ 29 #include <gtk/gtk.h> 30 %} 31 32 %privateheader{ 33 typedef struct 34 { 35 MNTooltips *self; 36 GtkWidget *widget; 37 GtkWidget *tip_widget; 38 } TooltipsData; 39 %} 40 41 %{ 42 #include "mn-util.h" 43 44 #define TOOLTIPS_DATA "mn-tooltips-data" 45 #define TOOLTIPS_INFO "mn-tooltips-info" 46 #define TOOLTIPS_KEYBOARD_MODE "gtk-tooltips-keyboard-mode" /* compatible with GtkTooltips */ 47 48 #define DELAY 500 /* Default delay in ms */ 49 #define STICKY_DELAY 0 /* Delay before popping up next tip 50 * if we're sticky 51 */ 52 #define STICKY_REVERT_DELAY 1000 /* Delay before sticky tooltips revert 53 * to normal 54 */ 55 56 /* The private flags that are used in the private_flags member of GtkWidget. 57 */ 58 typedef enum 59 { 60 PRIVATE_GTK_LEAVE_PENDING = 1 << 4 61 } GtkPrivateFlags; 62 63 /* Macros for extracting a widgets private_flags from GtkWidget. 64 */ 65 #define GTK_PRIVATE_FLAGS(wid) (GTK_WIDGET (wid)->private_flags) 66 67 /* Macros for setting and clearing private widget flags. 68 * we use a preprocessor string concatenation here for a clear 69 * flags/private_flags distinction at the cost of single flag operations. 70 */ 71 #define GTK_PRIVATE_SET_FLAG(wid,flag) G_STMT_START{ (GTK_PRIVATE_FLAGS (wid) |= (PRIVATE_ ## flag)); }G_STMT_END 72 %} 73 74 class MN:Tooltips from G:Object 75 { 76 private GtkWidget *window; 77 private TooltipsData *active_data; 78 private GSList *data_list; 79 80 private gboolean use_sticky_delay; 81 private GTimeVal last_popdown; 82 private unsigned int timeout_id; 83 84 private int border_width = 4; 85 86 finalize (self) 87 { 88 GSList *l; 89 90 if (selfp->timeout_id) 91 g_source_remove(selfp->timeout_id); 92 93 MN_LIST_FOREACH(l, selfp->data_list) 94 { 95 TooltipsData *data = l->data; 96 self_widget_remove(data->widget, data); 97 } 98 99 self_unset_window(self); 100 } 101 102 private void 103 destroy_data (TooltipsData *data) 104 { 105 g_object_disconnect(data->widget, 106 "any-signal", self_event_after_h, data, 107 "any-signal", self_widget_unmap, data, 108 "any-signal", self_widget_remove, data, 109 NULL); 110 111 g_object_set_data(G_OBJECT(data->widget), TOOLTIPS_DATA, NULL); 112 g_object_unref(data->widget); 113 g_object_unref(data->tip_widget); 114 g_free(data); 115 } 116 117 private void 118 display_closed_h (GdkDisplay *display, 119 gboolean is_error, 120 gpointer user_data) 121 { 122 Self *self = SELF(user_data); 123 self_unset_window(self); 124 } 125 126 private void 127 disconnect_display_closed (self) 128 { 129 g_signal_handlers_disconnect_by_func(gtk_widget_get_display(selfp->window), 130 self_display_closed_h, 131 self); 132 } 133 134 private void 135 unset_window (self) 136 { 137 if (selfp->window) 138 { 139 self_disconnect_display_closed(self); 140 gtk_widget_destroy(selfp->window); 141 } 142 } 143 144 private void 145 update_screen (self, gboolean new_window) 146 { 147 gboolean screen_changed = FALSE; 148 149 if (selfp->active_data && selfp->active_data->widget) 150 { 151 GdkScreen *screen = gtk_widget_get_screen(selfp->active_data->widget); 152 153 screen_changed = (screen != gtk_widget_get_screen(selfp->window)); 154 155 if (screen_changed) 156 { 157 if (! new_window) 158 self_disconnect_display_closed(self); 159 160 gtk_window_set_screen(GTK_WINDOW(selfp->window), screen); 161 } 162 } 163 164 if (screen_changed || new_window) 165 g_signal_connect(gtk_widget_get_display(selfp->window), 166 "closed", 167 G_CALLBACK(self_display_closed_h), 168 self); 169 } 170 171 private void 172 force_window (self) 173 { 174 if (! selfp->window) 175 { 176 selfp->window = gtk_window_new(GTK_WINDOW_POPUP); 177 self_update_screen(self, TRUE); 178 gtk_widget_set_app_paintable(selfp->window, TRUE); 179 gtk_window_set_resizable(GTK_WINDOW(selfp->window), FALSE); 180 gtk_widget_set_name(selfp->window, "gtk-tooltips"); 181 gtk_container_set_border_width(GTK_CONTAINER(selfp->window), selfp->border_width); 182 183 g_signal_connect_swapped(selfp->window, 184 "expose-event", 185 G_CALLBACK(self_paint_window), 186 self); 187 188 mn_add_weak_pointer(&selfp->window); 189 } 190 } 191 192 private TooltipsData * 193 get_data (Gtk:Widget *widget (check null type)) 194 { 195 return g_object_get_data(G_OBJECT(widget), TOOLTIPS_DATA); 196 } 197 198 private void 199 set_tip_widget_real (self, 200 Gtk:Widget *widget (check null type), 201 Gtk:Widget *tip_widget, 202 int border_width) 203 { 204 TooltipsData *data; 205 206 data = self_get_data(widget); 207 208 if (! tip_widget) 209 { 210 if (data) 211 self_widget_remove(data->widget, data); 212 return; 213 } 214 215 if (selfp->active_data 216 && selfp->active_data->widget == widget 217 && GTK_WIDGET_DRAWABLE(selfp->active_data->widget)) 218 { 219 if (data->tip_widget) 220 g_object_unref(data->tip_widget); 221 222 data->tip_widget = tip_widget; 223 224 if (data->tip_widget) 225 g_object_ref_sink(data->tip_widget); 226 227 self_draw_tips(self); 228 } 229 else 230 { 231 g_object_ref(widget); 232 233 if (data) 234 self_widget_remove(data->widget, data); 235 236 data = g_new0(TooltipsData, 1); 237 data->self = self; 238 data->widget = widget; 239 data->tip_widget = tip_widget; 240 241 if (data->tip_widget) 242 g_object_ref_sink(data->tip_widget); 243 244 selfp->data_list = g_slist_append(selfp->data_list, data); 245 g_signal_connect_after(widget, "event-after", G_CALLBACK(self_event_after_h), data); 246 247 g_object_set_data(G_OBJECT(widget), TOOLTIPS_DATA, data); 248 249 g_object_connect(widget, 250 "signal::unmap", self_widget_unmap, data, 251 "signal::unrealize", self_widget_unmap, data, 252 "signal::destroy", self_widget_remove, data, 253 NULL); 254 } 255 256 selfp->border_width = border_width; 257 if (selfp->window) 258 gtk_container_set_border_width(GTK_CONTAINER(selfp->window), border_width); 259 } 260 261 public void 262 set_tip (self, 263 Gtk:Widget *widget (check null type), 264 const char *tip_text) 265 { 266 GtkWidget *label = NULL; 267 268 if (tip_text) 269 { 270 label = gtk_label_new(tip_text); 271 gtk_label_set_line_wrap(GTK_LABEL(label), TRUE); 272 gtk_misc_set_alignment(GTK_MISC(label), 0.5, 0.5); 273 gtk_widget_show(label); 274 } 275 276 self_set_tip_widget_real(self, widget, label, 4); 277 } 278 279 public void 280 set_tip_widget (self, 281 Gtk:Widget *widget (check null type), 282 Gtk:Widget *tip_widget) 283 { 284 self_set_tip_widget_real(self, widget, tip_widget, 12); 285 } 286 287 private gboolean 288 paint_window (self) 289 { 290 GtkRequisition req; 291 292 gtk_widget_size_request(selfp->window, &req); 293 gtk_paint_flat_box(selfp->window->style, 294 selfp->window->window, 295 GTK_STATE_NORMAL, 296 GTK_SHADOW_OUT, 297 NULL, 298 selfp->window, 299 "tooltip", 300 0, 301 0, 302 req.width, 303 req.height); 304 305 return FALSE; 306 } 307 308 private void 309 draw_tips (self) 310 { 311 GtkRequisition requisition; 312 GtkWidget *widget; 313 gint x, y, w, h; 314 TooltipsData *data; 315 GtkWidget *child; 316 gboolean keyboard_mode; 317 GdkScreen *screen; 318 GdkScreen *pointer_screen; 319 gint monitor_num, px, py; 320 GdkRectangle monitor; 321 int screen_width; 322 323 if (! selfp->window) 324 self_force_window(self); 325 else if (GTK_WIDGET_VISIBLE(selfp->window)) 326 g_get_current_time(&selfp->last_popdown); 327 328 gtk_widget_ensure_style(selfp->window); 329 330 widget = selfp->active_data->widget; 331 g_object_set_data(G_OBJECT(selfp->window), TOOLTIPS_INFO, self); 332 333 keyboard_mode = self_get_keyboard_mode(widget); 334 335 self_update_screen(self, FALSE); 336 337 screen = gtk_widget_get_screen(widget); 338 339 data = selfp->active_data; 340 341 child = GTK_BIN(selfp->window)->child; 342 if (child) 343 gtk_container_remove(GTK_CONTAINER(selfp->window), child); 344 345 if (data->tip_widget) 346 { 347 gtk_container_add(GTK_CONTAINER(selfp->window), data->tip_widget); 348 gtk_widget_show(data->tip_widget); 349 } 350 351 gtk_widget_size_request(selfp->window, &requisition); 352 w = requisition.width; 353 h = requisition.height; 354 355 gdk_window_get_origin(widget->window, &x, &y); 356 if (GTK_WIDGET_NO_WINDOW(widget)) 357 { 358 x += widget->allocation.x; 359 y += widget->allocation.y; 360 } 361 362 x += widget->allocation.width / 2; 363 364 if (! keyboard_mode) 365 gdk_window_get_pointer(gdk_screen_get_root_window(screen), &x, NULL, NULL); 366 367 x -= (w / 2 + 4); 368 369 gdk_display_get_pointer(gdk_screen_get_display(screen), &pointer_screen, &px, &py, NULL); 370 if (pointer_screen != screen) 371 { 372 px = x; 373 py = y; 374 } 375 monitor_num = gdk_screen_get_monitor_at_point(screen, px, py); 376 gdk_screen_get_monitor_geometry(screen, monitor_num, &monitor); 377 378 if ((x + w) > monitor.x + monitor.width) 379 x -= (x + w) - (monitor.x + monitor.width); 380 else if (x < monitor.x) 381 x = monitor.x; 382 383 if ((y + h + widget->allocation.height + 4) > monitor.y + monitor.height 384 && (y - 4) > monitor.y) 385 y = y - h - 4; 386 else 387 y = y + widget->allocation.height + 4; 388 389 /* 390 * The following block is not part of GTK+ and has been added to 391 * make sure that the tooltip will not go beyond the screen edges 392 * (horizontally). 393 */ 394 screen_width = gdk_screen_get_width(screen); 395 if (x < 0 || x + w > screen_width) 396 { 397 x = 0; 398 gtk_widget_set_size_request(selfp->window, MIN(w, screen_width), -1); 399 } 400 401 /* 402 * The following block ensures that the top of the tooltip is 403 * visible, but it corrupts the tip widget (the mail summary is 404 * not properly positioned). A fix is welcome. 405 */ 406 /* 407 if (y < 0) 408 { 409 gtk_widget_set_size_request(selfp->window, -1, y + h); 410 y = 0; 411 } 412 */ 413 414 gtk_window_move(GTK_WINDOW(selfp->window), x, y); 415 gtk_widget_show(selfp->window); 416 } 417 418 private gboolean 419 timeout_cb (gpointer data) 420 { 421 Self *self = SELF(data); 422 423 if (selfp->active_data && GTK_WIDGET_DRAWABLE(selfp->active_data->widget)) 424 self_draw_tips(self); 425 426 selfp->timeout_id = 0; 427 return FALSE; /* remove timeout */ 428 } 429 430 private void 431 set_active_widget (self, Gtk:Widget *widget) 432 { 433 if (selfp->window) 434 { 435 if (GTK_WIDGET_VISIBLE(selfp->window)) 436 g_get_current_time(&selfp->last_popdown); 437 gtk_widget_hide(selfp->window); 438 } 439 440 mn_source_clear(&selfp->timeout_id); 441 442 selfp->active_data = NULL; 443 444 if (widget) 445 { 446 GSList *l; 447 448 MN_LIST_FOREACH(l, selfp->data_list) 449 { 450 TooltipsData *data = l->data; 451 452 if (data->widget == widget && GTK_WIDGET_DRAWABLE(widget)) 453 { 454 selfp->active_data = data; 455 break; 456 } 457 } 458 } 459 else 460 selfp->use_sticky_delay = FALSE; 461 } 462 463 private void 464 show_tip (Gtk:Widget *widget (check null type)) 465 { 466 TooltipsData *data; 467 468 data = self_get_data(widget); 469 470 if (data && 471 (! data->self->_priv->active_data || 472 data->self->_priv->active_data->widget != widget)) 473 { 474 self_set_active_widget(data->self, widget); 475 self_draw_tips(data->self); 476 } 477 } 478 479 private void 480 hide_tip (Gtk:Widget *widget (check null type)) 481 { 482 TooltipsData *data; 483 484 data = self_get_data(widget); 485 486 if (data && 487 (data->self->_priv->active_data && 488 data->self->_priv->active_data->widget == widget)) 489 self_set_active_widget(data->self, NULL); 490 } 491 492 private gboolean 493 recently_shown (self) 494 { 495 GTimeVal now; 496 glong msec; 497 498 g_get_current_time (&now); 499 msec = (now.tv_sec - selfp->last_popdown.tv_sec) * 1000 + 500 (now.tv_usec - selfp->last_popdown.tv_usec) / 1000; 501 return (msec < STICKY_REVERT_DELAY); 502 } 503 504 private gboolean 505 get_keyboard_mode (Gtk:Widget *widget (check null type)) 506 { 507 GtkWidget *toplevel = gtk_widget_get_toplevel(widget); 508 509 if (GTK_IS_WINDOW(toplevel)) 510 return GPOINTER_TO_INT(g_object_get_data(G_OBJECT(toplevel), TOOLTIPS_KEYBOARD_MODE)); 511 else 512 return FALSE; 513 } 514 515 private void 516 start_keyboard_mode (Gtk:Widget *widget (check null type)) 517 { 518 GtkWidget *toplevel = gtk_widget_get_toplevel(widget); 519 520 if (GTK_IS_WINDOW(toplevel)) 521 { 522 GtkWidget *focus = GTK_WINDOW(toplevel)->focus_widget; 523 524 g_object_set_data(G_OBJECT(toplevel), TOOLTIPS_KEYBOARD_MODE, GINT_TO_POINTER(TRUE)); 525 526 if (focus) 527 self_show_tip(focus); 528 } 529 } 530 531 private void 532 stop_keyboard_mode (Gtk:Widget *widget (check null type)) 533 { 534 GtkWidget *toplevel = gtk_widget_get_toplevel(widget); 535 536 if (GTK_IS_WINDOW(toplevel)) 537 { 538 GtkWidget *focus = GTK_WINDOW(toplevel)->focus_widget; 539 540 if (focus) 541 self_hide_tip(focus); 542 543 g_object_set_data(G_OBJECT(toplevel), TOOLTIPS_KEYBOARD_MODE, GINT_TO_POINTER(FALSE)); 544 } 545 } 546 547 private void 548 start_delay (self, Gtk:Widget *widget) 549 { 550 TooltipsData *old_data; 551 552 old_data = selfp->active_data; 553 if (! old_data || old_data->widget != widget) 554 { 555 self_set_active_widget(self, widget); 556 selfp->timeout_id = gdk_threads_add_timeout((selfp->use_sticky_delay && self_recently_shown(self)) ? STICKY_DELAY : DELAY, 557 self_timeout_cb, 558 self); 559 } 560 } 561 562 private void 563 event_after_h (GtkWidget *widget, GdkEvent *event, gpointer user_data) 564 { 565 Self *self; 566 TooltipsData *old_data; 567 GtkWidget *event_widget; 568 gboolean keyboard_mode = self_get_keyboard_mode(widget); 569 570 if ((event->type == GDK_LEAVE_NOTIFY || event->type == GDK_ENTER_NOTIFY) && 571 event->crossing.detail == GDK_NOTIFY_INFERIOR) 572 return; 573 574 old_data = self_get_data(widget); 575 self = old_data->self; 576 577 if (keyboard_mode) 578 { 579 switch (event->type) 580 { 581 case GDK_FOCUS_CHANGE: 582 if (event->focus_change.in) 583 self_show_tip(widget); 584 else 585 self_hide_tip(widget); 586 break; 587 588 default: 589 break; 590 } 591 } 592 else 593 { 594 if (event->type != GDK_KEY_PRESS && event->type != GDK_KEY_RELEASE) 595 { 596 event_widget = gtk_get_event_widget(event); 597 if (event_widget != widget) 598 return; 599 } 600 601 switch (event->type) 602 { 603 case GDK_EXPOSE: 604 /* do nothing */ 605 break; 606 607 case GDK_ENTER_NOTIFY: 608 if (! (GTK_IS_MENU_ITEM(widget) && GTK_MENU_ITEM(widget)->submenu)) 609 self_start_delay(self, widget); 610 break; 611 612 case GDK_LEAVE_NOTIFY: 613 self_set_active_widget(self, NULL); 614 selfp->use_sticky_delay = selfp->window && GTK_WIDGET_VISIBLE(selfp->window); 615 break; 616 617 case GDK_MOTION_NOTIFY: 618 /* Handle menu items specially ... pend popup for each motion 619 * on other widgets, we ignore motion. 620 */ 621 if (GTK_IS_MENU_ITEM(widget) && ! GTK_MENU_ITEM(widget)->submenu) 622 { 623 /* Completely evil hack to make sure we get the LEAVE_NOTIFY 624 */ 625 GTK_PRIVATE_SET_FLAG(widget, GTK_LEAVE_PENDING); 626 self_set_active_widget(self, NULL); 627 self_start_delay(self, widget); 628 break; 629 } 630 break; /* ignore */ 631 632 case GDK_BUTTON_PRESS: 633 case GDK_BUTTON_RELEASE: 634 case GDK_KEY_PRESS: 635 case GDK_KEY_RELEASE: 636 case GDK_PROXIMITY_IN: 637 case GDK_SCROLL: 638 self_set_active_widget(self, NULL); 639 break; 640 641 default: 642 break; 643 } 644 } 645 } 646 647 private void 648 widget_unmap (Gtk:Widget *widget (check null type), gpointer user_data) 649 { 650 TooltipsData *data = user_data; 651 Self *self = data->self; 652 653 if (selfp->active_data && 654 (selfp->active_data->widget == widget)) 655 self_set_active_widget(self, NULL); 656 } 657 658 private void 659 widget_remove (Gtk:Widget *widget (check null type), gpointer user_data) 660 { 661 TooltipsData *data = user_data; 662 Self *self = data->self; 663 664 self_widget_unmap(widget, user_data); 665 selfp->data_list = g_slist_remove(selfp->data_list, data); 666 self_destroy_data(data); 667 } 668 669 public void 670 toggle_keyboard_mode (Gtk:Widget *widget (check null type)) 671 { 672 if (self_get_keyboard_mode(widget)) 673 self_stop_keyboard_mode(widget); 674 else 675 self_start_keyboard_mode(widget); 676 } 677 678 public MNTooltips * 679 new (void) 680 { 681 return GET_NEW; 682 } 683 }