1 /**
2  * @file pidgintooltip.c Pidgin Tooltip API
3  * @ingroup pidgin
4  */
5 
6 /* pidgin
7  *
8  * Pidgin is the legal property of its developers, whose names are too numerous
9  * to list here.  Please refer to the COPYRIGHT file distributed with this
10  * source distribution.
11  *
12  * This program is free software; you can redistribute it and/or modify
13  * it under the terms of the GNU General Public License as published by
14  * the Free Software Foundation; either version 2 of the License, or
15  * (at your option) any later version.
16  *
17  * This program is distributed in the hope that it will be useful,
18  * but WITHOUT ANY WARRANTY; without even the implied warranty of
19  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20  * GNU General Public License for more details.
21  *
22  * You should have received a copy of the GNU General Public License
23  * along with this program; if not, write to the Free Software
24  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02111-1301  USA
25  */
26 
27 #include "internal.h"
28 #include "prefs.h"
29 #include "pidgin.h"
30 #include "pidgintooltip.h"
31 #include "debug.h"
32 
33 static gboolean enable_tooltips;
34 static int tooltip_delay = -1;
35 
36 struct
37 {
38 	GtkWidget *widget;
39 	int timeout;
40 	GdkRectangle tip_rect;
41 	GtkWidget *tipwindow;
42 	PidginTooltipPaint paint_tooltip;
43 } pidgin_tooltip;
44 
45 typedef struct
46 {
47 	GtkWidget *widget;
48 	gpointer userdata;
49 	PidginTooltipPaint paint_tooltip;
50 	union {
51 		struct {
52 			PidginTooltipCreateForTree create_tooltip;
53 			GtkTreePath *path;
54 		} treeview;
55 		struct {
56 			PidginTooltipCreate create_tooltip;
57 		} widget;
58 	} common;
59 } PidginTooltipData;
60 
61 static void
initialize_tooltip_delay()62 initialize_tooltip_delay()
63 {
64 #if GTK_CHECK_VERSION(2,14,0)
65 	GtkSettings *settings;
66 #endif
67 
68 	if (tooltip_delay != -1)
69 		return;
70 
71 #if GTK_CHECK_VERSION(2,14,0)
72 	settings = gtk_settings_get_default();
73 
74 	g_object_get(settings, "gtk-enable-tooltips", &enable_tooltips, NULL);
75 	g_object_get(settings, "gtk-tooltip-timeout", &tooltip_delay, NULL);
76 #else
77 	tooltip_delay = purple_prefs_get_int(PIDGIN_PREFS_ROOT "/blist/tooltip_delay");
78 	enable_tooltips = (tooltip_delay != 0);
79 #endif
80 }
81 
82 static void
destroy_tooltip_data(PidginTooltipData * data)83 destroy_tooltip_data(PidginTooltipData *data)
84 {
85 	if (data->common.treeview.path)
86 		gtk_tree_path_free(data->common.treeview.path);
87 	pidgin_tooltip_destroy();
88 	g_free(data);
89 }
90 
pidgin_tooltip_destroy()91 void pidgin_tooltip_destroy()
92 {
93 	if (pidgin_tooltip.timeout > 0) {
94 		g_source_remove(pidgin_tooltip.timeout);
95 		pidgin_tooltip.timeout = 0;
96 	}
97 	if (pidgin_tooltip.tipwindow) {
98 		gtk_widget_destroy(pidgin_tooltip.tipwindow);
99 		pidgin_tooltip.tipwindow = NULL;
100 	}
101 }
102 
103 static gboolean
pidgin_tooltip_expose_event(GtkWidget * widget,GdkEventExpose * event,gpointer data)104 pidgin_tooltip_expose_event(GtkWidget *widget, GdkEventExpose *event, gpointer data)
105 {
106 	if (pidgin_tooltip.paint_tooltip) {
107 		gtk_paint_flat_box(widget->style, widget->window, GTK_STATE_NORMAL, GTK_SHADOW_OUT,
108 				NULL, widget, "tooltip", 0, 0, -1, -1);
109 		pidgin_tooltip.paint_tooltip(widget, data);
110 	}
111 	return FALSE;
112 }
113 
114 static GtkWidget*
setup_tooltip_window(void)115 setup_tooltip_window(void)
116 {
117 	const char *name;
118 	GtkWidget *tipwindow;
119 
120 	tipwindow = gtk_window_new(GTK_WINDOW_POPUP);
121 	name = gtk_window_get_title(GTK_WINDOW(pidgin_tooltip.widget));
122 	gtk_window_set_type_hint(GTK_WINDOW(tipwindow), GDK_WINDOW_TYPE_HINT_TOOLTIP);
123 	gtk_widget_set_app_paintable(tipwindow, TRUE);
124 	gtk_window_set_title(GTK_WINDOW(tipwindow), name ? name : _("Pidgin Tooltip"));
125 	gtk_window_set_resizable(GTK_WINDOW(tipwindow), FALSE);
126 	gtk_widget_set_name(tipwindow, "gtk-tooltips");
127 	gtk_widget_ensure_style(tipwindow);
128 	gtk_widget_realize(tipwindow);
129 	return tipwindow;
130 }
131 
132 static void
setup_tooltip_window_position(gpointer data,int w,int h)133 setup_tooltip_window_position(gpointer data, int w, int h)
134 {
135 	int sig;
136 	int scr_w, scr_h, x, y, dy;
137 	int preserved_x, preserved_y;
138 	int mon_num;
139 	GdkScreen *screen = NULL;
140 	GdkRectangle mon_size;
141 	GtkWidget *tipwindow = pidgin_tooltip.tipwindow;
142 
143 	gdk_display_get_pointer(gdk_display_get_default(), &screen, &x, &y, NULL);
144 	mon_num = gdk_screen_get_monitor_at_point(screen, x, y);
145 	gdk_screen_get_monitor_geometry(screen, mon_num, &mon_size);
146 
147 	scr_w = mon_size.width + mon_size.x;
148 	scr_h = mon_size.height + mon_size.y;
149 
150 	dy = gdk_display_get_default_cursor_size(gdk_display_get_default()) / 2;
151 
152 	if (w > mon_size.width)
153 		w = mon_size.width - 10;
154 
155 	if (h > mon_size.height)
156 		h = mon_size.height - 10;
157 
158 	preserved_x = x;
159 	preserved_y = y;
160 
161 	x -= ((w >> 1) + 4);
162 
163 	if ((y + h + 4) > scr_h)
164 		y = y - h - dy - 5;
165 	else
166 		y = y + dy + 6;
167 
168 	if (y < mon_size.y)
169 		y = mon_size.y;
170 
171 	if (y != mon_size.y) {
172 		if ((x + w) > scr_w)
173 			x -= (x + w + 5) - scr_w;
174 		else if (x < mon_size.x)
175 			x = mon_size.x;
176 	} else {
177 		x -= (w / 2 + 10);
178 		if (x < mon_size.x)
179 			x = mon_size.x;
180 	}
181 
182 	/* If the mouse covered by the tipwindow, move the tipwindow
183 	 * to the righ side of the it */
184 	if ((preserved_x >= x) && (preserved_x <= (x + w))
185 			&& (preserved_y >= y) && (preserved_y <= (y + h)))
186 		x = preserved_x + dy;
187 
188 	gtk_widget_set_size_request(tipwindow, w, h);
189 	gtk_window_move(GTK_WINDOW(tipwindow), x, y);
190 	gtk_widget_show(tipwindow);
191 
192 	g_signal_connect(G_OBJECT(tipwindow), "expose_event",
193 			G_CALLBACK(pidgin_tooltip_expose_event), data);
194 
195 	/* Hide the tooltip when the widget is destroyed */
196 	sig = g_signal_connect(G_OBJECT(pidgin_tooltip.widget), "destroy", G_CALLBACK(pidgin_tooltip_destroy), NULL);
197 	g_signal_connect_swapped(G_OBJECT(tipwindow), "destroy", G_CALLBACK(g_source_remove), GINT_TO_POINTER(sig));
198 }
199 
pidgin_tooltip_show(GtkWidget * widget,gpointer userdata,PidginTooltipCreate create_tooltip,PidginTooltipPaint paint_tooltip)200 void pidgin_tooltip_show(GtkWidget *widget, gpointer userdata,
201 		PidginTooltipCreate create_tooltip, PidginTooltipPaint paint_tooltip)
202 {
203 	GtkWidget *tipwindow;
204 	int w, h;
205 
206 	pidgin_tooltip_destroy();
207 
208 	pidgin_tooltip.widget = gtk_widget_get_toplevel(widget);
209 	pidgin_tooltip.tipwindow = tipwindow = setup_tooltip_window();
210 	pidgin_tooltip.paint_tooltip = paint_tooltip;
211 
212 	if (!create_tooltip(tipwindow, userdata, &w, &h)) {
213 		pidgin_tooltip_destroy();
214 		return;
215 	}
216 	setup_tooltip_window_position(userdata, w, h);
217 }
218 
219 static void
reset_data_treepath(PidginTooltipData * data)220 reset_data_treepath(PidginTooltipData *data)
221 {
222 	gtk_tree_path_free(data->common.treeview.path);
223 	data->common.treeview.path = NULL;
224 }
225 
226 static void
pidgin_tooltip_draw(PidginTooltipData * data)227 pidgin_tooltip_draw(PidginTooltipData *data)
228 {
229 	GtkWidget *tipwindow;
230 	int w, h;
231 
232 	pidgin_tooltip_destroy();
233 
234 	pidgin_tooltip.widget = gtk_widget_get_toplevel(data->widget);
235 	pidgin_tooltip.tipwindow = tipwindow = setup_tooltip_window();
236 	pidgin_tooltip.paint_tooltip = data->paint_tooltip;
237 
238 	if (!data->common.widget.create_tooltip(tipwindow, data->userdata, &w, &h)) {
239 		if (tipwindow == pidgin_tooltip.tipwindow)
240 			pidgin_tooltip_destroy();
241 		return;
242 	}
243 
244 	setup_tooltip_window_position(data->userdata, w, h);
245 }
246 
247 static void
pidgin_tooltip_draw_tree(PidginTooltipData * data)248 pidgin_tooltip_draw_tree(PidginTooltipData *data)
249 {
250 	GtkWidget *tipwindow;
251 	GtkTreePath *path = NULL;
252 	int w, h;
253 
254 	if (!gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(data->widget),
255 				pidgin_tooltip.tip_rect.x,
256 				pidgin_tooltip.tip_rect.y + (pidgin_tooltip.tip_rect.height/2),
257 				&path, NULL, NULL, NULL)) {
258 		pidgin_tooltip_destroy();
259 		return;
260 	}
261 
262 	if (data->common.treeview.path) {
263 		if (gtk_tree_path_compare(data->common.treeview.path, path) == 0) {
264 			gtk_tree_path_free(path);
265 			return;
266 		}
267 		gtk_tree_path_free(data->common.treeview.path);
268 		data->common.treeview.path = NULL;
269 	}
270 
271 	pidgin_tooltip_destroy();
272 
273 	pidgin_tooltip.widget = gtk_widget_get_toplevel(data->widget);
274 	pidgin_tooltip.tipwindow = tipwindow = setup_tooltip_window();
275 	pidgin_tooltip.paint_tooltip = data->paint_tooltip;
276 
277 	if (!data->common.treeview.create_tooltip(tipwindow, path, data->userdata, &w, &h)) {
278 		if (tipwindow == pidgin_tooltip.tipwindow)
279 			pidgin_tooltip_destroy();
280 		gtk_tree_path_free(path);
281 		return;
282 	}
283 
284 	setup_tooltip_window_position(data->userdata, w, h);
285 
286 	data->common.treeview.path = path;
287 	g_signal_connect_swapped(G_OBJECT(pidgin_tooltip.tipwindow), "destroy",
288 			G_CALLBACK(reset_data_treepath), data);
289 }
290 
291 static gboolean
pidgin_tooltip_timeout(gpointer data)292 pidgin_tooltip_timeout(gpointer data)
293 {
294 	PidginTooltipData *tdata = data;
295 	pidgin_tooltip.timeout = 0;
296 	if (GTK_IS_TREE_VIEW(tdata->widget))
297 		pidgin_tooltip_draw_tree(data);
298 	else
299 		pidgin_tooltip_draw(data);
300 	return FALSE;
301 }
302 
303 static gboolean
row_motion_cb(GtkWidget * tv,GdkEventMotion * event,gpointer userdata)304 row_motion_cb(GtkWidget *tv, GdkEventMotion *event, gpointer userdata)
305 {
306 	GtkTreePath *path;
307 
308 	if (event->window != gtk_tree_view_get_bin_window(GTK_TREE_VIEW(tv)))
309 		return FALSE;    /* The cursor is probably on the TreeView's header. */
310 
311 	initialize_tooltip_delay();
312 	if (!enable_tooltips)
313 		return FALSE;
314 
315 	if (pidgin_tooltip.timeout) {
316 		if ((event->y >= pidgin_tooltip.tip_rect.y) && ((event->y - pidgin_tooltip.tip_rect.height) <= pidgin_tooltip.tip_rect.y))
317 			return FALSE;
318 		/* We've left the cell.  Remove the timeout and create a new one below */
319 		pidgin_tooltip_destroy();
320 	}
321 
322 	gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(tv), event->x, event->y, &path, NULL, NULL, NULL);
323 
324 	if (path == NULL) {
325 		pidgin_tooltip_destroy();
326 		return FALSE;
327 	}
328 
329 	gtk_tree_view_get_cell_area(GTK_TREE_VIEW(tv), path, NULL, &pidgin_tooltip.tip_rect);
330 	gtk_tree_path_free(path);
331 
332 	pidgin_tooltip.timeout = g_timeout_add(tooltip_delay, (GSourceFunc)pidgin_tooltip_timeout, userdata);
333 
334 	return FALSE;
335 }
336 
337 static gboolean
widget_leave_cb(GtkWidget * tv,GdkEvent * event,gpointer userdata)338 widget_leave_cb(GtkWidget *tv, GdkEvent *event, gpointer userdata)
339 {
340 	pidgin_tooltip_destroy();
341 	return FALSE;
342 }
343 
pidgin_tooltip_setup_for_treeview(GtkWidget * tree,gpointer userdata,PidginTooltipCreateForTree create_tooltip,PidginTooltipPaint paint_tooltip)344 gboolean pidgin_tooltip_setup_for_treeview(GtkWidget *tree, gpointer userdata,
345 		PidginTooltipCreateForTree create_tooltip, PidginTooltipPaint paint_tooltip)
346 {
347 	PidginTooltipData *tdata = g_new0(PidginTooltipData, 1);
348 	tdata->widget = tree;
349 	tdata->userdata = userdata;
350 	tdata->common.treeview.create_tooltip = create_tooltip;
351 	tdata->paint_tooltip = paint_tooltip;
352 
353 	g_signal_connect(G_OBJECT(tree), "motion-notify-event", G_CALLBACK(row_motion_cb), tdata);
354 	g_signal_connect(G_OBJECT(tree), "leave-notify-event", G_CALLBACK(widget_leave_cb), NULL);
355 	g_signal_connect(G_OBJECT(tree), "scroll-event", G_CALLBACK(widget_leave_cb), NULL);
356 	g_signal_connect_swapped(G_OBJECT(tree), "destroy", G_CALLBACK(destroy_tooltip_data), tdata);
357 	return TRUE;
358 }
359 
360 static gboolean
widget_motion_cb(GtkWidget * widget,GdkEvent * event,gpointer data)361 widget_motion_cb(GtkWidget *widget, GdkEvent *event, gpointer data)
362 {
363 	initialize_tooltip_delay();
364 
365 	pidgin_tooltip_destroy();
366 	if (!enable_tooltips)
367 		return FALSE;
368 
369 	pidgin_tooltip.timeout = g_timeout_add(tooltip_delay, (GSourceFunc)pidgin_tooltip_timeout, data);
370 	return FALSE;
371 }
372 
pidgin_tooltip_setup_for_widget(GtkWidget * widget,gpointer userdata,PidginTooltipCreate create_tooltip,PidginTooltipPaint paint_tooltip)373 gboolean pidgin_tooltip_setup_for_widget(GtkWidget *widget, gpointer userdata,
374 		PidginTooltipCreate create_tooltip, PidginTooltipPaint paint_tooltip)
375 {
376 	PidginTooltipData *wdata = g_new0(PidginTooltipData, 1);
377 	wdata->widget = widget;
378 	wdata->userdata = userdata;
379 	wdata->common.widget.create_tooltip = create_tooltip;
380 	wdata->paint_tooltip = paint_tooltip;
381 
382 	g_signal_connect(G_OBJECT(widget), "motion-notify-event", G_CALLBACK(widget_motion_cb), wdata);
383 	g_signal_connect(G_OBJECT(widget), "leave-notify-event", G_CALLBACK(widget_leave_cb), NULL);
384 	g_signal_connect(G_OBJECT(widget), "scroll-event", G_CALLBACK(widget_leave_cb), NULL);
385 	g_signal_connect_swapped(G_OBJECT(widget), "destroy", G_CALLBACK(destroy_tooltip_data), wdata);
386 	return TRUE;
387 }
388 
389