]> git.0d.be Git - empathy.git/blob - src/empathy-status-icon.c
0f3fff1b78c6e56048e05475d3569d09f73305f7
[empathy.git] / src / empathy-status-icon.c
1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 /*
3  * Copyright (C) 2007-2008 Collabora Ltd.
4  *
5  * This library is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU Lesser General Public
7  * License as published by the Free Software Foundation; either
8  * version 2.1 of the License, or (at your option) any later version.
9  *
10  * This library 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 GNU
13  * Lesser General Public License for more details.
14  *
15  * You should have received a copy of the GNU Lesser General Public
16  * License along with this library; if not, write to the Free Software
17  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
18  *
19  * Authors: Xavier Claessens <xclaesse@gmail.com>
20  */
21
22 #include <config.h>
23
24 #include <string.h>
25
26 #include <glib.h>
27
28 #include <gtk/gtk.h>
29 #include <gdk/gdkkeysyms.h>
30 #include <glib/gi18n.h>
31
32 #include <libnotify/notification.h>
33 #include <libnotify/notify.h>
34
35 #include <telepathy-glib/account-manager.h>
36 #include <telepathy-glib/util.h>
37
38 #include <libempathy/empathy-gsettings.h>
39 #include <libempathy/empathy-utils.h>
40
41 #include <libempathy-gtk/empathy-presence-chooser.h>
42 #include <libempathy-gtk/empathy-ui-utils.h>
43 #include <libempathy-gtk/empathy-images.h>
44 #include <libempathy-gtk/empathy-new-message-dialog.h>
45 #include <libempathy-gtk/empathy-new-call-dialog.h>
46 #include <libempathy-gtk/empathy-notify-manager.h>
47
48 #include "empathy-accounts-dialog.h"
49 #include "empathy-status-icon.h"
50 #include "empathy-preferences.h"
51 #include "empathy-event-manager.h"
52
53 #define DEBUG_FLAG EMPATHY_DEBUG_DISPATCHER
54 #include <libempathy/empathy-debug.h>
55
56 /* Number of ms to wait when blinking */
57 #define BLINK_TIMEOUT 500
58
59 #define GET_PRIV(obj) EMPATHY_GET_PRIV (obj, EmpathyStatusIcon)
60 typedef struct {
61         GtkStatusIcon       *icon;
62         TpAccountManager    *account_manager;
63         EmpathyNotifyManager *notify_mgr;
64         gboolean             showing_event_icon;
65         guint                blink_timeout;
66         EmpathyEventManager *event_manager;
67         EmpathyEvent        *event;
68         NotifyNotification  *notification;
69         GSettings           *gsettings_ui;
70
71         GtkWindow           *window;
72         GtkUIManager        *ui_manager;
73         GtkWidget           *popup_menu;
74         GtkAction           *show_window_item;
75         GtkAction           *new_message_item;
76         GtkAction           *status_item;
77 } EmpathyStatusIconPriv;
78
79 G_DEFINE_TYPE (EmpathyStatusIcon, empathy_status_icon, G_TYPE_OBJECT);
80
81 static void
82 status_icon_notification_closed_cb (NotifyNotification *notification,
83                                     EmpathyStatusIcon  *icon)
84 {
85         EmpathyStatusIconPriv *priv = GET_PRIV (icon);
86
87         g_object_unref (notification);
88
89         if (priv->notification == notification) {
90                 priv->notification = NULL;
91         }
92
93         if (!priv->event) {
94                 return;
95         }
96
97         /* inhibit other updates for this event */
98         empathy_event_inhibit_updates (priv->event);
99 }
100
101 static void
102 notification_close_helper (EmpathyStatusIconPriv *priv)
103 {
104         if (priv->notification != NULL) {
105                 notify_notification_close (priv->notification, NULL);
106                 priv->notification = NULL;
107         }
108 }
109
110 static void
111 notification_approve_cb (NotifyNotification *notification,
112                         gchar              *action,
113                         EmpathyStatusIcon  *icon)
114 {
115         EmpathyStatusIconPriv *priv = GET_PRIV (icon);
116
117         if (priv->event)
118                 empathy_event_approve (priv->event);
119 }
120
121 static void
122 add_notification_actions (EmpathyStatusIcon *self,
123                           NotifyNotification *notification)
124 {
125         EmpathyStatusIconPriv *priv = GET_PRIV (self);
126
127         switch (priv->event->type) {
128                 case EMPATHY_EVENT_TYPE_CHAT:
129                         notify_notification_add_action (notification,
130                                 "respond", _("Respond"), (NotifyActionCallback) notification_approve_cb,
131                                         self, NULL);
132                         break;
133
134                 default:
135                         break;
136         }
137 }
138
139 static void
140 status_icon_update_notification (EmpathyStatusIcon *icon)
141 {
142         EmpathyStatusIconPriv *priv = GET_PRIV (icon);
143         GdkPixbuf *pixbuf = NULL;
144
145         if (!empathy_notify_manager_notification_is_enabled (priv->notify_mgr)) {
146                 /* always close the notification if this happens */
147                 notification_close_helper (priv);
148                 return;
149         }
150
151         if (priv->event) {
152                 gchar *message_esc = NULL;
153                 gboolean has_x_canonical_append;
154                 NotifyNotification *notification = priv->notification;
155
156                 if (priv->event->message != NULL)
157                         message_esc = g_markup_escape_text (priv->event->message, -1);
158
159                 has_x_canonical_append =
160                                 empathy_notify_manager_has_capability (priv->notify_mgr,
161                                         EMPATHY_NOTIFY_MANAGER_CAP_X_CANONICAL_APPEND);
162
163                 if (notification != NULL && ! has_x_canonical_append) {
164                         /* if the notification server supports x-canonical-append, it is
165                            better to not use notify_notification_update to avoid
166                            overwriting the current notification message */
167                         notify_notification_update (notification,
168                                                     priv->event->header, message_esc,
169                                                     NULL);
170                 } else {
171                         /* if the notification server supports x-canonical-append,
172                            the hint will be added, so that the message from the
173                            just created notification will be automatically appended
174                            to an existing notification with the same title.
175                            In this way the previous message will not be lost: the new
176                            message will appear below it, in the same notification */
177                         notification = notify_notification_new_with_status_icon
178                                 (priv->event->header, message_esc, NULL, priv->icon);
179
180                         if (priv->notification == NULL) {
181                                 priv->notification = notification;
182                         }
183
184                         notify_notification_set_timeout (notification,
185                                                          NOTIFY_EXPIRES_DEFAULT);
186
187                         if (has_x_canonical_append) {
188                                 notify_notification_set_hint_string (notification,
189                                         EMPATHY_NOTIFY_MANAGER_CAP_X_CANONICAL_APPEND, "");
190                         }
191
192                         if (empathy_notify_manager_has_capability (priv->notify_mgr,
193                                    EMPATHY_NOTIFY_MANAGER_CAP_ACTIONS))
194                                 add_notification_actions (icon, notification);
195
196                         g_signal_connect (notification, "closed",
197                                           G_CALLBACK (status_icon_notification_closed_cb), icon);
198                 }
199
200                 pixbuf = empathy_notify_manager_get_pixbuf_for_notification (
201                                                                    priv->notify_mgr, priv->event->contact,
202                                                                    priv->event->icon_name);
203
204                 if (pixbuf != NULL) {
205                         notify_notification_set_icon_from_pixbuf (notification, pixbuf);
206                         g_object_unref (pixbuf);
207                 }
208
209                 notify_notification_show (notification, NULL);
210
211                 g_free (message_esc);
212         } else {
213                 notification_close_helper (priv);
214         }
215 }
216
217 static void
218 status_icon_update_tooltip (EmpathyStatusIcon *icon)
219 {
220         EmpathyStatusIconPriv *priv = GET_PRIV (icon);
221
222         if (priv->event) {
223                 gchar *tooltip = NULL;
224
225                 if (priv->event->message != NULL)
226                                 tooltip = g_markup_printf_escaped ("<i>%s</i>\n%s",
227                                                                    priv->event->header,
228                                                                    priv->event->message);
229                 else
230                                 tooltip = g_markup_printf_escaped ("<i>%s</i>",
231                                                                    priv->event->header);
232                 gtk_status_icon_set_tooltip_markup (priv->icon, tooltip);
233                 g_free (tooltip);
234         } else {
235                 TpConnectionPresenceType type;
236                 gchar *msg;
237
238                 type = tp_account_manager_get_most_available_presence (
239                         priv->account_manager, NULL, &msg);
240
241                 if (!EMP_STR_EMPTY (msg)) {
242                         gtk_status_icon_set_tooltip_text (priv->icon, msg);
243                 }
244                 else {
245                         gtk_status_icon_set_tooltip_text (priv->icon,
246                                                 empathy_presence_get_default_message (type));
247                 }
248
249                 g_free (msg);
250         }
251 }
252
253 static void
254 status_icon_update_icon (EmpathyStatusIcon *icon)
255 {
256         EmpathyStatusIconPriv *priv = GET_PRIV (icon);
257         const gchar           *icon_name;
258
259         if (priv->event && priv->showing_event_icon) {
260                 icon_name = priv->event->icon_name;
261         } else {
262                 TpConnectionPresenceType state;
263
264                 state = tp_account_manager_get_most_available_presence (
265                         priv->account_manager, NULL, NULL);
266
267                 /* An unset presence type here doesn't make sense. Force it
268                  * to be offline. */
269                 if (state == TP_CONNECTION_PRESENCE_TYPE_UNSET) {
270                         state = TP_CONNECTION_PRESENCE_TYPE_OFFLINE;
271                 }
272
273                 icon_name = empathy_icon_name_for_presence (state);
274         }
275
276         if (icon_name != NULL)
277                 gtk_status_icon_set_from_icon_name (priv->icon, icon_name);
278 }
279
280 static gboolean
281 status_icon_blink_timeout_cb (EmpathyStatusIcon *icon)
282 {
283         EmpathyStatusIconPriv *priv = GET_PRIV (icon);
284
285         priv->showing_event_icon = !priv->showing_event_icon;
286         status_icon_update_icon (icon);
287
288         return TRUE;
289 }
290 static void
291 status_icon_event_added_cb (EmpathyEventManager *manager,
292                             EmpathyEvent        *event,
293                             EmpathyStatusIcon   *icon)
294 {
295         EmpathyStatusIconPriv *priv = GET_PRIV (icon);
296
297         if (priv->event) {
298                 return;
299         }
300
301         DEBUG ("New event %p", event);
302
303         priv->event = event;
304         if (event->must_ack) {
305                 priv->showing_event_icon = TRUE;
306                 status_icon_update_icon (icon);
307                 status_icon_update_tooltip (icon);
308         }
309         status_icon_update_notification (icon);
310
311         if (!priv->blink_timeout && priv->showing_event_icon) {
312                 priv->blink_timeout = g_timeout_add (BLINK_TIMEOUT,
313                                                      (GSourceFunc) status_icon_blink_timeout_cb,
314                                                      icon);
315         }
316 }
317
318 static void
319 status_icon_event_removed_cb (EmpathyEventManager *manager,
320                               EmpathyEvent        *event,
321                               EmpathyStatusIcon   *icon)
322 {
323         EmpathyStatusIconPriv *priv = GET_PRIV (icon);
324
325         if (event != priv->event) {
326                 return;
327         }
328
329         priv->event = empathy_event_manager_get_top_event (priv->event_manager);
330
331         status_icon_update_tooltip (icon);
332         status_icon_update_icon (icon);
333
334         /* update notification anyway, as it's safe and we might have been
335          * changed presence in the meanwhile
336          */
337         status_icon_update_notification (icon);
338
339         if (!priv->event && priv->blink_timeout) {
340                 g_source_remove (priv->blink_timeout);
341                 priv->blink_timeout = 0;
342         }
343 }
344
345 static void
346 status_icon_event_updated_cb (EmpathyEventManager *manager,
347                               EmpathyEvent        *event,
348                               EmpathyStatusIcon   *icon)
349 {
350         EmpathyStatusIconPriv *priv = GET_PRIV (icon);
351
352         if (event != priv->event) {
353                 return;
354         }
355
356         if (empathy_notify_manager_notification_is_enabled (priv->notify_mgr)) {
357                 status_icon_update_notification (icon);
358         }
359
360         status_icon_update_tooltip (icon);
361 }
362
363 static void
364 status_icon_set_visibility (EmpathyStatusIcon *icon,
365                             gboolean           visible,
366                             gboolean           store)
367 {
368         EmpathyStatusIconPriv *priv = GET_PRIV (icon);
369
370         if (store) {
371                 g_settings_set_boolean (priv->gsettings_ui,
372                                         EMPATHY_PREFS_UI_MAIN_WINDOW_HIDDEN,
373                                         !visible);
374         }
375
376         if (!visible) {
377                 empathy_window_iconify (priv->window, priv->icon);
378         } else {
379                 empathy_window_present (GTK_WINDOW (priv->window));
380         }
381 }
382
383 static void
384 status_icon_notify_visibility_cb (GSettings   *gsettings,
385                                   const gchar *key,
386                                   gpointer     user_data)
387 {
388         EmpathyStatusIcon *icon = user_data;
389         gboolean           hidden = FALSE;
390
391         hidden = g_settings_get_boolean (gsettings, key);
392         status_icon_set_visibility (icon, !hidden, FALSE);
393 }
394
395 static void
396 status_icon_toggle_visibility (EmpathyStatusIcon *icon)
397 {
398         EmpathyStatusIconPriv *priv = GET_PRIV (icon);
399         gboolean               visible;
400
401         visible = gtk_window_is_active (priv->window);
402         status_icon_set_visibility (icon, !visible, TRUE);
403 }
404
405 static void
406 status_icon_presence_changed_cb (EmpathyStatusIcon *icon)
407 {
408         EmpathyStatusIconPriv *priv = GET_PRIV (icon);
409
410         status_icon_update_icon (icon);
411         status_icon_update_tooltip (icon);
412
413         if (!empathy_notify_manager_notification_is_enabled (priv->notify_mgr)) {
414                 /* dismiss the outstanding notification if present */
415
416                 if (priv->notification) {
417                         notify_notification_close (priv->notification, NULL);
418                         priv->notification = NULL;
419                 }
420         }
421 }
422
423 static gboolean
424 status_icon_delete_event_cb (GtkWidget         *widget,
425                              GdkEvent          *event,
426                              EmpathyStatusIcon *icon)
427 {
428         status_icon_set_visibility (icon, FALSE, TRUE);
429         return TRUE;
430 }
431
432 static gboolean
433 status_icon_key_press_event_cb  (GtkWidget *window,
434                                  GdkEventKey *event,
435                                  EmpathyStatusIcon *icon)
436 {
437         if (event->keyval == GDK_Escape) {
438                 status_icon_set_visibility (icon, FALSE, TRUE);
439         }
440         return FALSE;
441 }
442
443 static void
444 status_icon_activate_cb (GtkStatusIcon     *status_icon,
445                          EmpathyStatusIcon *icon)
446 {
447         EmpathyStatusIconPriv *priv = GET_PRIV (icon);
448
449         DEBUG ("%s", priv->event ? "event" : "toggle");
450
451         if (priv->event) {
452                 empathy_event_activate (priv->event);
453         } else {
454                 status_icon_toggle_visibility (icon);
455         }
456 }
457
458 static void
459 status_icon_show_hide_window_cb (GtkToggleAction   *action,
460                                  EmpathyStatusIcon *icon)
461 {
462         gboolean visible;
463
464         visible = gtk_toggle_action_get_active (action);
465         status_icon_set_visibility (icon, visible, TRUE);
466 }
467
468 static void
469 status_icon_new_message_cb (GtkAction         *action,
470                             EmpathyStatusIcon *icon)
471 {
472         empathy_new_message_dialog_show (NULL);
473 }
474
475 static void
476 status_icon_new_call_cb (GtkAction         *action,
477                             EmpathyStatusIcon *icon)
478 {
479         empathy_new_call_dialog_show (NULL);
480 }
481
482 static void
483 status_icon_quit_cb (GtkAction         *action,
484                      EmpathyStatusIcon *icon)
485 {
486         gtk_main_quit ();
487 }
488
489 static void
490 status_icon_popup_menu_cb (GtkStatusIcon     *status_icon,
491                            guint              button,
492                            guint              activate_time,
493                            EmpathyStatusIcon *icon)
494 {
495         EmpathyStatusIconPriv *priv = GET_PRIV (icon);
496         GtkWidget             *menu_item;
497         GtkWidget             *submenu;
498         gboolean               show;
499
500         show = empathy_window_get_is_visible (GTK_WINDOW (priv->window));
501
502         g_signal_handlers_block_by_func (priv->show_window_item,
503                                          status_icon_show_hide_window_cb,
504                                          icon);
505         gtk_toggle_action_set_active (GTK_TOGGLE_ACTION (priv->show_window_item),
506                                       show);
507         g_signal_handlers_unblock_by_func (priv->show_window_item,
508                                            status_icon_show_hide_window_cb,
509                                            icon);
510
511         menu_item = gtk_ui_manager_get_widget (priv->ui_manager, "/menu/status");
512         submenu = empathy_presence_chooser_create_menu ();
513         gtk_menu_item_set_submenu (GTK_MENU_ITEM (menu_item), submenu);
514
515         gtk_menu_popup (GTK_MENU (priv->popup_menu),
516                         NULL, NULL,
517                         gtk_status_icon_position_menu,
518                         priv->icon,
519                         button,
520                         activate_time);
521 }
522
523 static void
524 status_icon_create_menu (EmpathyStatusIcon *icon)
525 {
526         EmpathyStatusIconPriv *priv = GET_PRIV (icon);
527         GtkBuilder            *gui;
528         gchar                 *filename;
529
530         filename = empathy_file_lookup ("empathy-status-icon.ui", "src");
531         gui = empathy_builder_get_file (filename,
532                                         "ui_manager", &priv->ui_manager,
533                                         "menu", &priv->popup_menu,
534                                         "show_list", &priv->show_window_item,
535                                         "new_message", &priv->new_message_item,
536                                         "status", &priv->status_item,
537                                        NULL);
538         g_free (filename);
539
540         empathy_builder_connect (gui, icon,
541                               "show_list", "toggled", status_icon_show_hide_window_cb,
542                               "new_message", "activate", status_icon_new_message_cb,
543                               "new_call", "activate", status_icon_new_call_cb,
544                               "quit", "activate", status_icon_quit_cb,
545                               NULL);
546
547         g_object_ref (priv->ui_manager);
548         g_object_unref (gui);
549 }
550
551 static void
552 status_icon_status_changed_cb (TpAccount *account,
553                                TpConnectionStatus current,
554                                TpConnectionStatus previous,
555                                TpConnectionStatusReason reason,
556                                gchar *dbus_error_name,
557                                GHashTable *details,
558                                EmpathyStatusIcon *icon)
559 {
560         EmpathyStatusIconPriv *priv = GET_PRIV (icon);
561
562         gtk_action_set_sensitive (priv->new_message_item,
563                                   empathy_account_manager_get_accounts_connected (NULL));
564 }
565
566 static void
567 status_icon_finalize (GObject *object)
568 {
569         EmpathyStatusIconPriv *priv = GET_PRIV (object);
570
571         if (priv->blink_timeout) {
572                 g_source_remove (priv->blink_timeout);
573         }
574
575         if (priv->notification) {
576                 notify_notification_close (priv->notification, NULL);
577                 g_object_unref (priv->notification);
578                 priv->notification = NULL;
579         }
580
581         g_object_unref (priv->icon);
582         g_object_unref (priv->account_manager);
583         g_object_unref (priv->event_manager);
584         g_object_unref (priv->ui_manager);
585         g_object_unref (priv->notify_mgr);
586         g_object_unref (priv->gsettings_ui);
587         g_object_unref (priv->window);
588 }
589
590 static void
591 empathy_status_icon_class_init (EmpathyStatusIconClass *klass)
592 {
593         GObjectClass *object_class = G_OBJECT_CLASS (klass);
594
595         object_class->finalize = status_icon_finalize;
596
597         g_type_class_add_private (object_class, sizeof (EmpathyStatusIconPriv));
598 }
599
600 static void
601 account_manager_prepared_cb (GObject *source_object,
602                              GAsyncResult *result,
603                              gpointer user_data)
604 {
605         GList *list, *l;
606         TpAccountManager *account_manager = TP_ACCOUNT_MANAGER (source_object);
607         EmpathyStatusIcon *icon = user_data;
608         GError *error = NULL;
609
610         if (!tp_account_manager_prepare_finish (account_manager, result, &error)) {
611                 DEBUG ("Failed to prepare account manager: %s", error->message);
612                 g_error_free (error);
613                 return;
614         }
615
616         list = tp_account_manager_get_valid_accounts (account_manager);
617         for (l = list; l != NULL; l = l->next) {
618                 tp_g_signal_connect_object (l->data, "status-changed",
619                                              G_CALLBACK (status_icon_status_changed_cb),
620                                              icon, 0);
621         }
622         g_list_free (list);
623
624         status_icon_presence_changed_cb (icon);
625 }
626
627 static void
628 empathy_status_icon_init (EmpathyStatusIcon *icon)
629 {
630         EmpathyStatusIconPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (icon,
631                 EMPATHY_TYPE_STATUS_ICON, EmpathyStatusIconPriv);
632
633         icon->priv = priv;
634         priv->icon = gtk_status_icon_new ();
635         priv->account_manager = tp_account_manager_dup ();
636         priv->event_manager = empathy_event_manager_dup_singleton ();
637
638         tp_account_manager_prepare_async (priv->account_manager, NULL,
639             account_manager_prepared_cb, icon);
640
641         /* make icon listen and respond to MAIN_WINDOW_HIDDEN changes */
642         priv->gsettings_ui = g_settings_new (EMPATHY_PREFS_UI_SCHEMA);
643         g_signal_connect (priv->gsettings_ui,
644                           "changed::" EMPATHY_PREFS_UI_MAIN_WINDOW_HIDDEN,
645                           G_CALLBACK (status_icon_notify_visibility_cb),
646                           icon);
647
648         status_icon_create_menu (icon);
649
650         g_signal_connect_swapped (priv->account_manager,
651                                   "most-available-presence-changed",
652                                   G_CALLBACK (status_icon_presence_changed_cb),
653                                   icon);
654         g_signal_connect (priv->event_manager, "event-added",
655                           G_CALLBACK (status_icon_event_added_cb),
656                           icon);
657         g_signal_connect (priv->event_manager, "event-removed",
658                           G_CALLBACK (status_icon_event_removed_cb),
659                           icon);
660         g_signal_connect (priv->event_manager, "event-updated",
661                           G_CALLBACK (status_icon_event_updated_cb),
662                           icon);
663         g_signal_connect (priv->icon, "activate",
664                           G_CALLBACK (status_icon_activate_cb),
665                           icon);
666         g_signal_connect (priv->icon, "popup-menu",
667                           G_CALLBACK (status_icon_popup_menu_cb),
668                           icon);
669
670         priv->notification = NULL;
671         priv->notify_mgr = empathy_notify_manager_dup_singleton ();
672 }
673
674 EmpathyStatusIcon *
675 empathy_status_icon_new (GtkWindow *window, gboolean hide_contact_list)
676 {
677         EmpathyStatusIconPriv *priv;
678         EmpathyStatusIcon     *icon;
679         gboolean               should_hide;
680
681         g_return_val_if_fail (GTK_IS_WINDOW (window), NULL);
682
683         icon = g_object_new (EMPATHY_TYPE_STATUS_ICON, NULL);
684         priv = GET_PRIV (icon);
685
686         priv->window = g_object_ref (window);
687
688         g_signal_connect_after (priv->window, "key-press-event",
689                           G_CALLBACK (status_icon_key_press_event_cb),
690                           icon);
691
692         g_signal_connect (priv->window, "delete-event",
693                           G_CALLBACK (status_icon_delete_event_cb),
694                           icon);
695
696         should_hide = g_settings_get_boolean (priv->gsettings_ui,
697                         EMPATHY_PREFS_UI_MAIN_WINDOW_HIDDEN);
698
699         if (gtk_window_is_active (priv->window) == should_hide) {
700                 status_icon_set_visibility (icon, !should_hide, FALSE);
701         }
702
703         return icon;
704 }
705