1 /*
2  * gui_tray.c: support for system tray
3  * Copyright (C) 2003-2004 Saulius Menkevicius
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, write to the Free Software
17  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  *
19  * $Id: gui_tray.c,v 1.32 2004/12/30 19:15:22 bobas Exp $
20  */
21 
22 #include <glib.h>
23 #include <gtk/gtk.h>
24 
25 #include "main.h"
26 #include "prefs.h"
27 #include "sess.h"
28 #include "user.h"
29 #include "util.h"
30 #include "gui_misc.h"
31 #include "gui.h"
32 
33 #define GUI_TRAY_IMPL
34 #include "gui_tray.h"
35 
36 #define ICON_BLINK_TIMEOUT_MS	500
37 #define TOOLTIP_MAX_LEN		64	/* max chars in one tooltip line */
38 
39 /** forward references
40  */
41 static void update_tray_icon();
42 
43 /** static variables
44  */
45 static gboolean	tray_created,
46 		tray_embedded;
47 
48 static guint	tray_blink_timeout;
49 static gboolean	tray_blink_state;
50 static guint	tray_tooltip_strings_num;
51 static GList	* tray_tooltip_strings;
52 
53 static const struct tray_impl_ops * tray_impl;
54 
55 /* static routines
56  */
57 static gboolean
tray_blink_cb(gpointer data)58 tray_blink_cb(gpointer data)
59 {
60 	update_tray_icon();
61 	tray_blink_state = !tray_blink_state;
62 
63 	return TRUE;	/* ie. do not remove this event source */
64 }
65 
66 static void
tray_start_blinking()67 tray_start_blinking()
68 {
69 	g_assert(tray_created);
70 
71 	if(tray_blink_timeout)
72 		return;
73 
74 	tray_blink_state = TRUE;	/* show the "faded" icon first time it changes */
75 	tray_blink_timeout = g_timeout_add(ICON_BLINK_TIMEOUT_MS, &tray_blink_cb, NULL);
76 
77 	/* force icon update */
78 	tray_blink_cb(NULL);
79 }
80 
81 static void
tray_stop_blinking()82 tray_stop_blinking()
83 {
84 	g_assert(tray_created);
85 
86 	if(!tray_blink_timeout)
87 		return;
88 
89 	g_source_remove(tray_blink_timeout);
90 	tray_blink_timeout = 0;
91 
92 	/* set the icon to "normal" state
93 	 */
94 	tray_blink_state = FALSE;
95 	update_tray_icon();
96 }
97 
98 static void
tray_popup_mode_cb(GtkMenuItem * item_w,gpointer new_mode)99 tray_popup_mode_cb(GtkMenuItem * item_w, gpointer new_mode)
100 {
101 	raise_event(EVENT_IFACE_TRAY_UMODE, new_mode, 0);
102 }
103 
104 static GtkWidget *
tray_popup_menu_mode_submenu()105 tray_popup_menu_mode_submenu()
106 {
107 	GtkWidget * submenu_w, * item_w;
108 	enum user_mode_enum usermode;
109 
110 	submenu_w = gtk_menu_new();
111 
112 	for(usermode = UMODE_FIRST_VALID; usermode < UMODE_NUM_VALID; usermode++) {
113 		item_w = util_image_menu_item(
114 			util_user_state_stock(usermode, TRUE), user_mode_name(usermode),
115 			G_CALLBACK(tray_popup_mode_cb), GINT_TO_POINTER(usermode));
116 		if(prefs_int(PREFS_MAIN_MODE)==usermode)
117 			gtk_widget_set_sensitive(item_w, FALSE);
118 
119 		gtk_menu_shell_append(GTK_MENU_SHELL(submenu_w), item_w);
120 	}
121 
122 	return submenu_w;
123 }
124 
125 static void
tray_popup_message_cb(GtkMenuItem * item_w,gpointer dummy)126 tray_popup_message_cb(GtkMenuItem * item_w, gpointer dummy)
127 {
128 	gpointer user = user_by_name(g_object_get_data(G_OBJECT(item_w), "username"));
129 	if(user)
130 		raise_event(EVENT_IFACE_USER_MESSAGE_REQ, user, 0);
131 }
132 
133 static GtkWidget *
tray_popup_menu_message_submenu(gint * n_users)134 tray_popup_menu_message_submenu(gint * n_users)
135 {
136 	GtkWidget * submenu_w, * item_w;
137 	GList * list, * entry;
138 
139 	g_assert(n_users);
140 
141 	submenu_w = gtk_menu_new();
142 
143 	list = user_list(TRUE);
144 	for(entry = list, *n_users = 0; entry; entry = entry->next, (*n_users) ++) {
145 		enum user_mode_enum usermode = user_mode_of(entry->data);
146 
147 		item_w = util_image_menu_item(
148 			util_user_state_stock(usermode, TRUE), NULL,
149 			G_CALLBACK(tray_popup_message_cb), NULL);
150 		gtk_label_set_text(GTK_LABEL(GTK_BIN(item_w)->child), user_name_of(entry->data));
151 		gtk_menu_shell_append(GTK_MENU_SHELL(submenu_w), item_w);
152 
153 		if(IS_MESSAGEABLE_MODE(usermode)) {
154 			g_object_set_data_full(
155 				G_OBJECT(item_w), "username",
156 				g_strdup(user_name_of(entry->data)), (GDestroyNotify)g_free);
157 		} else {
158 			gtk_widget_set_sensitive(item_w, FALSE);
159 		}
160 	}
161 
162 	g_list_free(list);
163 
164 	return submenu_w;
165 }
166 
167 static void
tray_popup_bare_event_cb(GtkMenuItem * item_w,gpointer event)168 tray_popup_bare_event_cb(GtkMenuItem * item_w, gpointer event)
169 {
170 	raise_event(GPOINTER_TO_INT(event), NULL, 0);
171 }
172 
173 static void
tray_popup_menu(guint event_button,guint32 event_time)174 tray_popup_menu(guint event_button, guint32 event_time)
175 {
176 	GtkWidget * menu_w, * submenu_w, * item_w;
177 	gint user_count;
178 
179 	menu_w = gtk_menu_new();
180 
181 	/* "Send message to >" submenu */
182 	item_w = util_image_menu_item(NULL, _("Message to"), NULL, NULL);
183 	gtk_menu_shell_append(GTK_MENU_SHELL(menu_w), item_w);
184 
185 	submenu_w = tray_popup_menu_message_submenu(&user_count);
186 	gtk_menu_item_set_submenu(GTK_MENU_ITEM(item_w), submenu_w);
187 	if(!user_count)
188 		gtk_widget_set_sensitive(item_w, FALSE);
189 
190 	/* "Change mode to >" submenu */
191 	item_w = gtk_menu_item_new_with_label(_("Set mode"));
192 	gtk_menu_item_set_submenu(GTK_MENU_ITEM(item_w), tray_popup_menu_mode_submenu());
193 	gtk_menu_shell_append(GTK_MENU_SHELL(menu_w), item_w);
194 
195 	/* "Preferences.." */
196 	gtk_menu_shell_append(GTK_MENU_SHELL(menu_w), gtk_separator_menu_item_new());
197 
198 	item_w = util_image_menu_item(
199 		GTK_STOCK_PREFERENCES, _("Preferences.."),
200 		G_CALLBACK(tray_popup_bare_event_cb),
201 		GINT_TO_POINTER(EVENT_IFACE_SHOW_CONFIGURE_DLG));
202 	gtk_menu_shell_append(GTK_MENU_SHELL(menu_w), item_w);
203 
204 	gtk_menu_shell_append(GTK_MENU_SHELL(menu_w), gtk_separator_menu_item_new());
205 
206 	/* "Quit" */
207 	item_w = util_image_menu_item(
208 		GTK_STOCK_QUIT, _("Quit"),
209 		G_CALLBACK(tray_popup_bare_event_cb), GINT_TO_POINTER(EVENT_IFACE_EXIT));
210 	gtk_menu_shell_append(GTK_MENU_SHELL(menu_w), item_w);
211 
212 	/* show menu */
213 	gtk_widget_show_all(menu_w);
214 	gtk_menu_popup(GTK_MENU(menu_w), NULL, NULL, NULL, NULL, event_button, event_time);
215 }
216 
217 /**
218  * update_tray_icon:
219  *	updates tray icon to current state
220  */
221 static void
update_tray_icon()222 update_tray_icon()
223 {
224 	g_assert(tray_created);
225 
226 	tray_impl->set_icon(tray_blink_state, my_mode());
227 }
228 
229 static void
create_tray()230 create_tray()
231 {
232 	/* tooltip strings */
233 	tray_tooltip_strings_num = 0;
234 	tray_tooltip_strings = NULL;
235 	tray_blink_timeout = 0;
236 
237 	/* create and embed the widget itself */
238 	tray_embedded = FALSE;
239 	tray_impl->create(PACKAGE);
240 	tray_created = TRUE;
241 
242 	/* update tray icon contents */
243 	tray_blink_state = FALSE;
244 	update_tray_icon();
245 }
246 
247 static void
destroy_tray()248 destroy_tray()
249 {
250 	if(tray_blink_timeout)
251 		tray_stop_blinking();
252 
253 	tray_impl->destroy();
254 
255 	/* delete tooltip structs */
256 	util_list_free_with_data(tray_tooltip_strings, (GDestroyNotify)g_free);
257 	tray_tooltip_strings = NULL;
258 	tray_tooltip_strings_num = 0;
259 }
260 
261 /**
262  * tray_blink_trigger:
263  *	triggers blinking of tray icon on specific session text events
264  */
265 static void
tray_blink_trigger(sess_id session,const char * text,enum session_text_type text_type)266 tray_blink_trigger(
267 	sess_id session,
268 	const char * text,
269 	enum session_text_type text_type)
270 {
271 	gboolean blink;
272 
273 	g_assert(tray_created);
274 
275 	switch(sess_type(session)) {
276 	case SESSTYPE_STATUS:
277 		blink = prefs_bool(PREFS_GUI_TRAY_TRIGGERS_STATUS);
278 		break;
279 	case SESSTYPE_PRIVATE:
280 		blink = (prefs_bool(PREFS_GUI_TRAY_TRIGGERS_PRIVATE)
281 				&& (text_type==SESSTEXT_THEIR_TEXT || text_type==SESSTEXT_THEIR_ME))
282 
283 			|| (prefs_bool(PREFS_GUI_TRAY_TRIGGERS_JOIN_LEAVE)
284 				&& (text_type==SESSTEXT_JOIN || text_type==SESSTEXT_LEAVE));
285 		break;
286 	case SESSTYPE_CHANNEL:
287 		blink = (prefs_bool(PREFS_GUI_TRAY_TRIGGERS_TOPIC)
288 				&& text_type==SESSTEXT_TOPIC)
289 
290 			|| (prefs_bool(PREFS_GUI_TRAY_TRIGGERS_JOIN_LEAVE)
291 				&& (text_type==SESSTEXT_JOIN || text_type==SESSTEXT_LEAVE))
292 
293 			|| (prefs_bool(PREFS_GUI_TRAY_TRIGGERS_CHANNEL)
294 				&& (text_type==SESSTEXT_THEIR_TEXT ||text_type==SESSTEXT_THEIR_ME));
295 		break;
296 	}
297 
298 	if(blink)
299 		tray_start_blinking();
300 }
301 
302 /**
303  * tray_update_tooltip
304  *	appends new line to tray icon popup
305  */
306 static void
tray_update_tooltip(gpointer session,const gchar * text)307 tray_update_tooltip(gpointer session, const gchar * text)
308 {
309 	GList * entry;
310 	GString * tooltip_text;
311 
312 	g_assert(tray_created);
313 	g_assert(session && text);
314 
315 	/* check if we can set the tooltip */
316 	if(! tray_impl->set_tooltip)
317 		return;
318 
319 	/* don't write the same line twice
320 	 *  if 'duplicate status messages on the active tab' is enabled
321 	 */
322 	if(prefs_bool(PREFS_MAIN_LOG_GLOBAL)
323 			&& sess_type(session)==SESSTYPE_STATUS
324 			&& sess_type(sess_current())!=SESSTYPE_STATUS) {
325 		return;
326 	}
327 
328 	/* remove old entries so we keep inside specified limit of `PREFS_TRAY_TOOLTIP_LINES' */
329 	while(tray_tooltip_strings
330 			&& tray_tooltip_strings_num > (prefs_int(
331 					PREFS_GUI_TRAY_TOOLTIP_LINE_NUM)-1)) {
332 		g_free(tray_tooltip_strings->data);
333 		tray_tooltip_strings = g_list_delete_link(tray_tooltip_strings, tray_tooltip_strings);
334 		tray_tooltip_strings_num --;
335 	}
336 
337 	if(prefs_int(PREFS_GUI_TRAY_TOOLTIP_LINE_NUM)) {
338 		/* append new line */
339 		tray_tooltip_strings = g_list_append(tray_tooltip_strings, g_strdup(text));
340 		tray_tooltip_strings_num ++;
341 
342 		tooltip_text = g_string_new(NULL);
343 
344 		for(entry = tray_tooltip_strings; entry; entry = entry->next) {
345 			/* ensure that tooltip line is not too long */
346 			gint line_len = g_utf8_strlen((const gchar*)entry->data, -1);
347 			if(line_len > TOOLTIP_MAX_LEN) {
348 				/* line is too long, append the beginning of the line only */
349 				g_string_append_len(
350 					tooltip_text, (const gchar*)entry->data,
351 					g_utf8_offset_to_pointer(
352 						(const gchar*)entry->data, TOOLTIP_MAX_LEN - 2)
353 					- (const gchar*)entry->data
354 				);
355 				g_string_append(tooltip_text, "..");
356 			} else {
357 				g_string_append(tooltip_text, (const gchar*)entry->data);
358 			}
359 			if(entry->next)
360 				g_string_append_c(tooltip_text, '\n');
361 		}
362 
363 		/* update tooltip text */
364 		tray_impl->set_tooltip(tooltip_text->str);
365 
366 		g_string_free(tooltip_text, TRUE);
367 	} else {
368 		/* disable the tooltip as there are no lines do display
369 		 */
370 		tray_impl->set_tooltip(NULL);
371 	}
372 }
373 
374 static void
tray_prefs_gui_tray_enable_changed_cb(const gchar * prefs_name)375 tray_prefs_gui_tray_enable_changed_cb(const gchar * prefs_name)
376 {
377 	if(app_status() >= APP_START) {
378 		if(prefs_bool(PREFS_GUI_TRAY_ENABLE))
379 			create_tray();
380 		else	destroy_tray();
381 	}
382 }
383 
384 static void
tray_register_prefs()385 tray_register_prefs()
386 {
387 	prefs_register(PREFS_GUI_TRAY_ENABLE, PREFS_TYPE_BOOL,
388 		_("Enable system tray icon"), NULL, NULL);
389 	prefs_register(PREFS_GUI_TRAY_TRIGGERS_JOIN_LEAVE, PREFS_TYPE_BOOL,
390 		_("Tray blinks when someone joins/leaves a channel"), NULL, NULL);
391 	prefs_register(PREFS_GUI_TRAY_TRIGGERS_CHANNEL, PREFS_TYPE_BOOL,
392 		_("Tray blinks on new channel text"), NULL, NULL);
393 	prefs_register(PREFS_GUI_TRAY_TRIGGERS_PRIVATE, PREFS_TYPE_BOOL,
394 		_("Tray blinks on new private text"), NULL, NULL);
395 	prefs_register(PREFS_GUI_TRAY_TRIGGERS_STATUS, PREFS_TYPE_BOOL,
396 		_("Tray blinks on new status text"), NULL, NULL);
397 	prefs_register(PREFS_GUI_TRAY_TRIGGERS_TOPIC, PREFS_TYPE_BOOL,
398 		_("Tray blinks on channel topic change"), NULL, NULL);
399 	prefs_register(PREFS_GUI_TRAY_HIDE_WND_ON_STARTUP, PREFS_TYPE_BOOL,
400 		_("Hide main window on startup"), NULL, NULL);
401 	prefs_register(PREFS_GUI_TRAY_TOOLTIP_LINE_NUM, PREFS_TYPE_UINT,
402 		_("Number of lines on tray icon tooltip"), NULL, NULL);
403 }
404 
405 static void
tray_embedded_cb()406 tray_embedded_cb()
407 {
408 	tray_embedded = TRUE;
409 	raise_event(EVENT_IFACE_TRAY_EMBEDDED, NULL, 0);
410 }
411 
412 static void
tray_removed_cb()413 tray_removed_cb()
414 {
415 	tray_embedded = FALSE;
416 	raise_event(EVENT_IFACE_TRAY_REMOVED, NULL, 0);
417 }
418 
419 static void
tray_clicked_cb(guint button,guint32 time)420 tray_clicked_cb(guint button, guint32 time)
421 {
422 	g_assert(tray_created && tray_embedded);
423 
424 	if(button==1) {
425 		/* show hide on left click */
426 		raise_event(EVENT_IFACE_TRAY_CLICK, NULL, 0);
427 	}
428 	else if(button==3) {
429 		tray_popup_menu(button, time);
430 	}
431 }
432 
433 static void
tray_prefs_main_mode_changed_cb(const gchar * prefs_name)434 tray_prefs_main_mode_changed_cb(const gchar * prefs_name)
435 {
436 	if(tray_created)
437 		update_tray_icon();
438 }
439 
440 /**
441  * tray_event_cb:
442  *	handles application events
443  */
444 static void
tray_event_cb(enum app_event_enum e,gpointer p,int i)445 tray_event_cb(enum app_event_enum e, gpointer p, int i)
446 {
447 	switch(e) {
448 	case EVENT_MAIN_INIT:
449 		tray_created = tray_embedded = FALSE;
450 
451 		tray_impl = tray_impl_init();
452 		tray_impl->set_embedded_notifier(tray_embedded_cb);
453 		tray_impl->set_removed_notifier(tray_removed_cb);
454 		tray_impl->set_clicked_notifier(tray_clicked_cb);
455 		break;
456 
457 	case EVENT_MAIN_REGISTER_PREFS:
458 		tray_register_prefs();
459 		break;
460 
461 	case EVENT_MAIN_PRESET_PREFS:
462 		prefs_add_notifier(PREFS_GUI_TRAY_ENABLE,
463 			(GHookFunc)tray_prefs_gui_tray_enable_changed_cb);
464 		prefs_add_notifier(PREFS_MAIN_MODE, (GHookFunc)tray_prefs_main_mode_changed_cb);
465 
466 		prefs_set(PREFS_GUI_TRAY_ENABLE, TRUE);
467 		prefs_set(PREFS_GUI_TRAY_TOOLTIP_LINE_NUM, 8);
468 		prefs_set(PREFS_GUI_TRAY_TRIGGERS_JOIN_LEAVE, TRUE);
469 		prefs_set(PREFS_GUI_TRAY_TRIGGERS_CHANNEL, TRUE);
470 		prefs_set(PREFS_GUI_TRAY_TRIGGERS_PRIVATE, TRUE);
471 		prefs_set(PREFS_GUI_TRAY_TRIGGERS_STATUS, TRUE);
472 		prefs_set(PREFS_GUI_TRAY_TRIGGERS_TOPIC, TRUE);
473 		break;
474 
475 	case EVENT_MAIN_START:
476 		if(prefs_bool(PREFS_GUI_TRAY_ENABLE))
477 			create_tray();
478 		break;
479 
480 	case EVENT_MAIN_PRECLOSE:
481 		tray_impl->destroy();
482 		break;
483 
484 	case EVENT_SESSION_TEXT:
485 		if(tray_embedded) {
486 			tray_update_tooltip(EVENT_V(p, 0), EVENT_V(p, 1));
487 
488 			if(!gui_is_active())
489 				tray_blink_trigger(EVENT_V(p, 0), EVENT_V(p, 1), (enum session_text_type)i);
490 		}
491 		break;
492 	case EVENT_IFACE_ACTIVE_CHANGE:
493 		if(tray_embedded && i)
494 			tray_stop_blinking();
495 		break;
496 	case EVENT_IFACE_TRAY_UMODE:
497 		prefs_set(PREFS_MAIN_MODE, GPOINTER_TO_INT(p));
498 		break;
499 	default:
500 		break;
501 	}
502 }
503 
504 /**
505  * gui_tray_is_embedded:
506  *	returns TRUE, if we have icon on system tray visible to the user
507  */
gui_tray_is_embedded()508 gboolean gui_tray_is_embedded()
509 {
510 	return tray_embedded;
511 }
512 
513 /**
514  * gui_tray_register:
515  *	registers gui_tray module for some events
516  */
gui_tray_register()517 void gui_tray_register()
518 {
519 	register_event_cb(tray_event_cb, EVENT_MAIN|EVENT_IFACE|EVENT_SESSION);
520 }
521