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 }