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