]> git.0d.be Git - empathy.git/blob - src/empathy-roster-window.c
make code displaying the 'no match found' page more generic
[empathy.git] / src / empathy-roster-window.c
1 /*
2  * Copyright (C) 2002-2007 Imendio AB
3  * Copyright (C) 2007-2010 Collabora Ltd.
4  *
5  * This program is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU General Public License as
7  * published by the Free Software Foundation; either version 2 of the
8  * License, or (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 GNU
13  * General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public
16  * License along with this program; if not, write to the
17  * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
18  * Boston, MA  02110-1301  USA
19  *
20  * Authors: Xavier Claessens <xclaesse@gmail.com>
21  *          Danielle Madeley <danielle.madeley@collabora.co.uk>
22  */
23
24 #include <config.h>
25
26 #include <sys/stat.h>
27 #include <gtk/gtk.h>
28 #include <gdk/gdkkeysyms.h>
29 #include <glib/gi18n.h>
30
31 #include <telepathy-glib/account-manager.h>
32 #include <telepathy-glib/util.h>
33 #include <folks/folks.h>
34
35 #include <libempathy/empathy-contact.h>
36 #include <libempathy/empathy-utils.h>
37 #include <libempathy/empathy-request-util.h>
38 #include <libempathy/empathy-chatroom-manager.h>
39 #include <libempathy/empathy-chatroom.h>
40 #include <libempathy/empathy-contact-list.h>
41 #include <libempathy/empathy-gsettings.h>
42 #include <libempathy/empathy-individual-manager.h>
43 #include <libempathy/empathy-gsettings.h>
44 #include <libempathy/empathy-status-presets.h>
45 #include <libempathy/empathy-tp-contact-factory.h>
46
47 #include <libempathy-gtk/empathy-contact-dialogs.h>
48 #include <libempathy-gtk/empathy-live-search.h>
49 #include <libempathy-gtk/empathy-contact-blocking-dialog.h>
50 #include <libempathy-gtk/empathy-contact-search-dialog.h>
51 #include <libempathy-gtk/empathy-geometry.h>
52 #include <libempathy-gtk/empathy-gtk-enum-types.h>
53 #include <libempathy-gtk/empathy-individual-dialogs.h>
54 #include <libempathy-gtk/empathy-individual-store.h>
55 #include <libempathy-gtk/empathy-individual-store-manager.h>
56 #include <libempathy-gtk/empathy-individual-view.h>
57 #include <libempathy-gtk/empathy-new-message-dialog.h>
58 #include <libempathy-gtk/empathy-new-call-dialog.h>
59 #include <libempathy-gtk/empathy-log-window.h>
60 #include <libempathy-gtk/empathy-presence-chooser.h>
61 #include <libempathy-gtk/empathy-sound-manager.h>
62 #include <libempathy-gtk/empathy-ui-utils.h>
63
64 #include "empathy-accounts-dialog.h"
65 #include "empathy-call-observer.h"
66 #include "empathy-chat-manager.h"
67 #include "empathy-roster-window.h"
68 #include "empathy-preferences.h"
69 #include "empathy-about-dialog.h"
70 #include "empathy-debug-window.h"
71 #include "empathy-new-chatroom-dialog.h"
72 #include "empathy-map-view.h"
73 #include "empathy-chatrooms-window.h"
74 #include "empathy-event-manager.h"
75 #include "empathy-ft-manager.h"
76
77 #define DEBUG_FLAG EMPATHY_DEBUG_OTHER
78 #include <libempathy/empathy-debug.h>
79
80 /* Flashing delay for icons (milliseconds). */
81 #define FLASH_TIMEOUT 500
82
83 /* Minimum width of roster window if something goes wrong. */
84 #define MIN_WIDTH 50
85
86 /* Accels (menu shortcuts) can be configured and saved */
87 #define ACCELS_FILENAME "accels.txt"
88
89 /* Name in the geometry file */
90 #define GEOMETRY_NAME "roster-window"
91
92 enum
93 {
94   PAGE_CONTACT_LIST = 0,
95   PAGE_MESSAGE
96 };
97
98 enum
99 {
100   PROP_0,
101   PROP_SHELL_RUNNING
102 };
103
104 G_DEFINE_TYPE (EmpathyRosterWindow, empathy_roster_window, GTK_TYPE_WINDOW);
105
106 struct _EmpathyRosterWindowPriv {
107   EmpathyIndividualStore *individual_store;
108   EmpathyIndividualView *individual_view;
109   TpAccountManager *account_manager;
110   EmpathyChatroomManager *chatroom_manager;
111   EmpathyEventManager *event_manager;
112   EmpathySoundManager *sound_mgr;
113   EmpathyCallObserver *call_observer;
114   guint flash_timeout_id;
115   gboolean flash_on;
116   gboolean empty;
117
118   GSettings *gsettings_ui;
119   GSettings *gsettings_contacts;
120
121   GtkWidget *preferences;
122   GtkWidget *main_vbox;
123   GtkWidget *throbber;
124   GtkWidget *throbber_tool_item;
125   GtkWidget *presence_toolbar;
126   GtkWidget *presence_chooser;
127   GtkWidget *errors_vbox;
128   GtkWidget *auth_vbox;
129   GtkWidget *search_bar;
130   GtkWidget *notebook;
131   GtkWidget *no_entry_label;
132
133   GtkToggleAction *show_protocols;
134   GtkRadioAction *sort_by_name;
135   GtkRadioAction *sort_by_status;
136   GtkRadioAction *normal_with_avatars;
137   GtkRadioAction *normal_size;
138   GtkRadioAction *compact_size;
139
140   GtkUIManager *ui_manager;
141   GtkAction *view_history;
142   GtkAction *room_join_favorites;
143   GtkWidget *room_menu;
144   GtkWidget *room_separator;
145   GtkWidget *edit_context;
146   GtkWidget *edit_context_separator;
147
148   GtkActionGroup *balance_action_group;
149   GtkAction *view_balance_show_in_roster;
150   GtkWidget *balance_vbox;
151
152   guint size_timeout_id;
153
154   /* reffed TpAccount* => visible GtkInfoBar* */
155   GHashTable *errors;
156
157   /* EmpathyEvent* => visible GtkInfoBar* */
158   GHashTable *auths;
159
160   /* stores a mapping from TpAccount to Handler ID to prevent
161    * to listen more than once to the status-changed signal */
162   GHashTable *status_changed_handlers;
163
164   /* Actions that are enabled when there are connected accounts */
165   GList *actions_connected;
166
167   gboolean shell_running;
168 };
169
170 static void
171 roster_window_flash_stop (EmpathyRosterWindow *self)
172 {
173   if (self->priv->flash_timeout_id == 0)
174     return;
175
176   DEBUG ("Stop flashing");
177   g_source_remove (self->priv->flash_timeout_id);
178   self->priv->flash_timeout_id = 0;
179   self->priv->flash_on = FALSE;
180 }
181
182 typedef struct
183 {
184   EmpathyEvent *event;
185   gboolean on;
186   EmpathyRosterWindow *self;
187 } FlashForeachData;
188
189 static gboolean
190 roster_window_flash_foreach (GtkTreeModel *model,
191     GtkTreePath *path,
192     GtkTreeIter *iter,
193     gpointer user_data)
194 {
195   FlashForeachData *data = (FlashForeachData *) user_data;
196   FolksIndividual *individual;
197   EmpathyContact *contact;
198   const gchar *icon_name;
199   GtkTreePath *parent_path = NULL;
200   GtkTreeIter parent_iter;
201   GdkPixbuf *pixbuf = NULL;
202
203   gtk_tree_model_get (model, iter,
204           EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual,
205           -1);
206
207   if (individual == NULL)
208     return FALSE;
209
210   contact = empathy_contact_dup_from_folks_individual (individual);
211   if (contact != data->event->contact)
212     goto out;
213
214   if (data->on)
215     {
216       icon_name = data->event->icon_name;
217       pixbuf = empathy_pixbuf_from_icon_name (icon_name, GTK_ICON_SIZE_MENU);
218     }
219   else
220     {
221       pixbuf = empathy_individual_store_get_individual_status_icon (
222               data->self->priv->individual_store,
223               individual);
224       if (pixbuf != NULL)
225         g_object_ref (pixbuf);
226     }
227
228   gtk_tree_store_set (GTK_TREE_STORE (model), iter,
229       EMPATHY_INDIVIDUAL_STORE_COL_ICON_STATUS, pixbuf,
230       -1);
231
232   /* To make sure the parent is shown correctly, we emit
233    * the row-changed signal on the parent so it prompts
234    * it to be refreshed by the filter func.
235    */
236   if (gtk_tree_model_iter_parent (model, &parent_iter, iter))
237     {
238       parent_path = gtk_tree_model_get_path (model, &parent_iter);
239     }
240
241   if (parent_path != NULL)
242     {
243       gtk_tree_model_row_changed (model, parent_path, &parent_iter);
244       gtk_tree_path_free (parent_path);
245     }
246
247 out:
248   g_object_unref (individual);
249   tp_clear_object (&contact);
250   tp_clear_object (&pixbuf);
251
252   return FALSE;
253 }
254
255 static gboolean
256 roster_window_flash_cb (EmpathyRosterWindow *self)
257 {
258   GtkTreeModel *model;
259   GSList *events, *l;
260   gboolean found_event = FALSE;
261   FlashForeachData  data;
262
263   self->priv->flash_on = !self->priv->flash_on;
264   data.on = self->priv->flash_on;
265   model = GTK_TREE_MODEL (self->priv->individual_store);
266
267   events = empathy_event_manager_get_events (self->priv->event_manager);
268   for (l = events; l; l = l->next)
269     {
270       data.event = l->data;
271       data.self = self;
272       if (!data.event->contact || !data.event->must_ack)
273         continue;
274
275       found_event = TRUE;
276       gtk_tree_model_foreach (model,
277           roster_window_flash_foreach,
278           &data);
279     }
280
281   if (!found_event)
282     roster_window_flash_stop (self);
283
284   return TRUE;
285 }
286
287 static void
288 roster_window_flash_start (EmpathyRosterWindow *self)
289 {
290   if (self->priv->flash_timeout_id != 0)
291     return;
292
293   DEBUG ("Start flashing");
294   self->priv->flash_timeout_id = g_timeout_add (FLASH_TIMEOUT,
295       (GSourceFunc) roster_window_flash_cb, self);
296
297   roster_window_flash_cb (self);
298 }
299
300 static void
301 roster_window_remove_auth (EmpathyRosterWindow *self,
302     EmpathyEvent *event)
303 {
304   GtkWidget *error_widget;
305
306   error_widget = g_hash_table_lookup (self->priv->auths, event);
307   if (error_widget != NULL)
308     {
309       gtk_widget_destroy (error_widget);
310       g_hash_table_remove (self->priv->auths, event);
311     }
312 }
313
314 static void
315 roster_window_auth_add_clicked_cb (GtkButton *button,
316     EmpathyRosterWindow *self)
317 {
318   EmpathyEvent *event;
319
320   event = g_object_get_data (G_OBJECT (button), "event");
321
322   empathy_event_approve (event);
323
324   roster_window_remove_auth (self, event);
325 }
326
327 static void
328 roster_window_auth_close_clicked_cb (GtkButton *button,
329     EmpathyRosterWindow *self)
330 {
331   EmpathyEvent *event;
332
333   event = g_object_get_data (G_OBJECT (button), "event");
334
335   empathy_event_decline (event);
336   roster_window_remove_auth (self, event);
337 }
338
339 static void
340 roster_window_auth_display (EmpathyRosterWindow *self,
341     EmpathyEvent *event)
342 {
343   TpAccount *account = event->account;
344   GtkWidget *info_bar;
345   GtkWidget *content_area;
346   GtkWidget *image;
347   GtkWidget *label;
348   GtkWidget *add_button;
349   GtkWidget *close_button;
350   GtkWidget *action_area;
351   GtkWidget *action_grid;
352   const gchar *icon_name;
353   gchar *str;
354
355   if (g_hash_table_lookup (self->priv->auths, event) != NULL)
356     return;
357
358   info_bar = gtk_info_bar_new ();
359   gtk_info_bar_set_message_type (GTK_INFO_BAR (info_bar), GTK_MESSAGE_QUESTION);
360
361   gtk_widget_set_no_show_all (info_bar, TRUE);
362   gtk_box_pack_start (GTK_BOX (self->priv->auth_vbox), info_bar, FALSE, TRUE, 0);
363   gtk_widget_show (info_bar);
364
365   icon_name = tp_account_get_icon_name (account);
366   image = gtk_image_new_from_icon_name (icon_name, GTK_ICON_SIZE_SMALL_TOOLBAR);
367   gtk_widget_show (image);
368
369   str = g_markup_printf_escaped ("<b>%s</b>\n%s",
370       tp_account_get_display_name (account),
371       _("Password required"));
372
373   label = gtk_label_new (str);
374   gtk_label_set_use_markup (GTK_LABEL (label), TRUE);
375   gtk_label_set_ellipsize (GTK_LABEL (label), PANGO_ELLIPSIZE_END);
376   gtk_misc_set_alignment (GTK_MISC (label), 0, 0.5);
377   gtk_widget_show (label);
378
379   g_free (str);
380
381   content_area = gtk_info_bar_get_content_area (GTK_INFO_BAR (info_bar));
382   gtk_box_pack_start (GTK_BOX (content_area), image, FALSE, FALSE, 0);
383   gtk_box_pack_start (GTK_BOX (content_area), label, TRUE, TRUE, 0);
384
385   image = gtk_image_new_from_stock (GTK_STOCK_ADD, GTK_ICON_SIZE_BUTTON);
386   add_button = gtk_button_new ();
387   gtk_button_set_image (GTK_BUTTON (add_button), image);
388   gtk_widget_set_tooltip_text (add_button, _("Provide Password"));
389   gtk_widget_show (add_button);
390
391   image = gtk_image_new_from_stock (GTK_STOCK_CLOSE, GTK_ICON_SIZE_BUTTON);
392   close_button = gtk_button_new ();
393   gtk_button_set_image (GTK_BUTTON (close_button), image);
394   gtk_widget_set_tooltip_text (close_button, _("Disconnect"));
395   gtk_widget_show (close_button);
396
397   action_grid = gtk_grid_new ();
398   gtk_grid_set_column_spacing (GTK_GRID (action_grid), 6);
399   gtk_widget_show (action_grid);
400
401   action_area = gtk_info_bar_get_action_area (GTK_INFO_BAR (info_bar));
402   gtk_box_pack_start (GTK_BOX (action_area), action_grid, FALSE, FALSE, 0);
403
404   gtk_grid_attach (GTK_GRID (action_grid), add_button, 0, 0, 1, 1);
405   gtk_grid_attach (GTK_GRID (action_grid), close_button, 1, 0, 1, 1);
406
407   g_object_set_data_full (G_OBJECT (info_bar),
408       "event", event, NULL);
409   g_object_set_data_full (G_OBJECT (add_button),
410       "event", event, NULL);
411   g_object_set_data_full (G_OBJECT (close_button),
412       "event", event, NULL);
413
414   g_signal_connect (add_button, "clicked",
415       G_CALLBACK (roster_window_auth_add_clicked_cb), self);
416   g_signal_connect (close_button, "clicked",
417       G_CALLBACK (roster_window_auth_close_clicked_cb), self);
418
419   gtk_widget_show (self->priv->auth_vbox);
420
421   g_hash_table_insert (self->priv->auths, event, info_bar);
422 }
423
424 static void
425 modify_event_count (GtkTreeModel *model,
426     GtkTreeIter *iter,
427     EmpathyEvent *event,
428     gboolean increase)
429 {
430   FolksIndividual *individual;
431   EmpathyContact *contact;
432   guint count;
433
434   gtk_tree_model_get (model, iter,
435       EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual,
436       EMPATHY_INDIVIDUAL_STORE_COL_EVENT_COUNT, &count,
437       -1);
438
439   if (individual == NULL)
440     return;
441
442   increase ? count++ : count--;
443
444   contact = empathy_contact_dup_from_folks_individual (individual);
445   if (contact == event->contact)
446     gtk_tree_store_set (GTK_TREE_STORE (model), iter,
447         EMPATHY_INDIVIDUAL_STORE_COL_EVENT_COUNT, count, -1);
448
449   tp_clear_object (&contact);
450   g_object_unref (individual);
451 }
452
453 static gboolean
454 increase_event_count_foreach (GtkTreeModel *model,
455     GtkTreePath *path,
456     GtkTreeIter *iter,
457     gpointer user_data)
458 {
459   EmpathyEvent *event = user_data;
460
461   modify_event_count (model, iter, event, TRUE);
462
463   return FALSE;
464 }
465
466 static void
467 increase_event_count (EmpathyRosterWindow *self,
468     EmpathyEvent *event)
469 {
470   GtkTreeModel *model;
471
472   model = GTK_TREE_MODEL (self->priv->individual_store);
473
474   gtk_tree_model_foreach (model, increase_event_count_foreach, event);
475 }
476
477 static gboolean
478 decrease_event_count_foreach (GtkTreeModel *model,
479     GtkTreePath *path,
480     GtkTreeIter *iter,
481     gpointer user_data)
482 {
483   EmpathyEvent *event = user_data;
484
485   modify_event_count (model, iter, event, FALSE);
486
487   return FALSE;
488 }
489
490 static void
491 decrease_event_count (EmpathyRosterWindow *self,
492     EmpathyEvent *event)
493 {
494   GtkTreeModel *model;
495
496   model = GTK_TREE_MODEL (self->priv->individual_store);
497
498   gtk_tree_model_foreach (model, decrease_event_count_foreach, event);
499 }
500
501 static void
502 roster_window_event_added_cb (EmpathyEventManager *manager,
503     EmpathyEvent *event,
504     EmpathyRosterWindow *self)
505 {
506   if (event->contact)
507     {
508       increase_event_count (self, event);
509
510       roster_window_flash_start (self);
511     }
512   else if (event->type == EMPATHY_EVENT_TYPE_AUTH)
513     {
514       roster_window_auth_display (self, event);
515     }
516 }
517
518 static void
519 roster_window_event_removed_cb (EmpathyEventManager *manager,
520     EmpathyEvent *event,
521     EmpathyRosterWindow *self)
522 {
523   FlashForeachData data;
524
525   if (event->type == EMPATHY_EVENT_TYPE_AUTH)
526     {
527       roster_window_remove_auth (self, event);
528       return;
529     }
530
531   if (!event->contact)
532     return;
533
534   decrease_event_count (self, event);
535
536   data.on = FALSE;
537   data.event = event;
538   data.self = self;
539
540   gtk_tree_model_foreach (GTK_TREE_MODEL (self->priv->individual_store),
541       roster_window_flash_foreach,
542       &data);
543 }
544
545 static gboolean
546 roster_window_load_events_idle_cb (gpointer user_data)
547 {
548   EmpathyRosterWindow *self = user_data;
549   GSList *l;
550
551   l = empathy_event_manager_get_events (self->priv->event_manager);
552   while (l != NULL)
553     {
554       roster_window_event_added_cb (self->priv->event_manager, l->data,
555           self);
556       l = l->next;
557     }
558
559   return FALSE;
560 }
561
562 static void
563 roster_window_row_activated_cb (EmpathyIndividualView *view,
564     GtkTreePath *path,
565     GtkTreeViewColumn *col,
566     EmpathyRosterWindow *self)
567 {
568   EmpathyContact *contact = NULL;
569   FolksIndividual *individual;
570   GtkTreeModel *model;
571   GtkTreeIter iter;
572   GSList *events, *l;
573
574   model = gtk_tree_view_get_model (GTK_TREE_VIEW (self->priv->individual_view));
575   gtk_tree_model_get_iter (model, &iter, path);
576
577   gtk_tree_model_get (model, &iter, EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL,
578       &individual, -1);
579
580   if (individual != NULL)
581     contact = empathy_contact_dup_from_folks_individual (individual);
582
583   if (contact == NULL)
584     goto OUT;
585
586   /* If the contact has an event activate it, otherwise the
587    * default handler of row-activated will be called. */
588   events = empathy_event_manager_get_events (self->priv->event_manager);
589   for (l = events; l; l = l->next)
590     {
591       EmpathyEvent *event = l->data;
592
593       if (event->contact == contact)
594         {
595           DEBUG ("Activate event");
596           empathy_event_activate (event);
597
598           /* We don't want the default handler of this signal
599            * (e.g. open a chat) */
600           g_signal_stop_emission_by_name (view, "row-activated");
601           break;
602         }
603     }
604
605   g_object_unref (contact);
606 OUT:
607   tp_clear_object (&individual);
608 }
609
610 static void
611 display_page_message (EmpathyRosterWindow *self,
612     const gchar *msg)
613 {
614   gchar *tmp;
615
616   tmp = g_strdup_printf ("<b><span size='xx-large'>%s</span></b>", msg);
617
618   gtk_label_set_markup (GTK_LABEL (self->priv->no_entry_label), tmp);
619   g_free (tmp);
620
621   gtk_label_set_line_wrap (GTK_LABEL (self->priv->no_entry_label), TRUE);
622
623   gtk_notebook_set_current_page (GTK_NOTEBOOK (self->priv->notebook),
624       PAGE_MESSAGE);
625 }
626
627 static void
628 roster_window_row_deleted_cb (GtkTreeModel *model,
629     GtkTreePath *path,
630     EmpathyRosterWindow *self)
631 {
632   GtkTreeIter help_iter;
633
634   if (!gtk_tree_model_get_iter_first (model, &help_iter))
635     {
636       self->priv->empty = TRUE;
637
638       if (empathy_individual_view_is_searching (self->priv->individual_view))
639         {
640           display_page_message (self, _("No match found"));
641         }
642     }
643 }
644
645 static void
646 display_page_contact_list (EmpathyRosterWindow *self)
647 {
648   gtk_notebook_set_current_page (GTK_NOTEBOOK (self->priv->notebook),
649       PAGE_CONTACT_LIST);
650 }
651
652 static void
653 roster_window_row_inserted_cb (GtkTreeModel      *model,
654     GtkTreePath *path,
655     GtkTreeIter *iter,
656     EmpathyRosterWindow *self)
657 {
658   if (self->priv->empty)
659     {
660       self->priv->empty = FALSE;
661
662       display_page_contact_list (self);
663       gtk_widget_grab_focus (GTK_WIDGET (self->priv->individual_view));
664
665       /* The store is being filled, it will be done after an idle cb.
666        * So we can then get events. If we do that too soon, event's
667        * contact is not yet in the store and it won't get marked as
668        * having events. */
669       g_idle_add (roster_window_load_events_idle_cb, self);
670     }
671 }
672
673 static void
674 roster_window_remove_error (EmpathyRosterWindow *self,
675     TpAccount *account)
676 {
677   GtkWidget *error_widget;
678
679   error_widget = g_hash_table_lookup (self->priv->errors, account);
680   if (error_widget != NULL)
681     {
682       gtk_widget_destroy (error_widget);
683       g_hash_table_remove (self->priv->errors, account);
684     }
685 }
686
687 static void
688 roster_window_account_disabled_cb (TpAccountManager  *manager,
689     TpAccount *account,
690     EmpathyRosterWindow *self)
691 {
692   roster_window_remove_error (self, account);
693 }
694
695 static void
696 roster_window_error_retry_clicked_cb (GtkButton *button,
697     EmpathyRosterWindow *self)
698 {
699   TpAccount *account;
700
701   account = g_object_get_data (G_OBJECT (button), "account");
702   tp_account_reconnect_async (account, NULL, NULL);
703
704   roster_window_remove_error (self, account);
705 }
706
707 static void
708 roster_window_error_edit_clicked_cb (GtkButton *button,
709     EmpathyRosterWindow *self)
710 {
711   TpAccount *account;
712
713   account = g_object_get_data (G_OBJECT (button), "account");
714
715   empathy_accounts_dialog_show_application (
716       gtk_widget_get_screen (GTK_WIDGET (button)),
717       account, FALSE, FALSE);
718
719   roster_window_remove_error (self, account);
720 }
721
722 static void
723 roster_window_error_close_clicked_cb (GtkButton *button,
724     EmpathyRosterWindow *self)
725 {
726   TpAccount *account;
727
728   account = g_object_get_data (G_OBJECT (button), "account");
729   roster_window_remove_error (self, account);
730 }
731
732 static void
733 roster_window_error_upgrade_sw_clicked_cb (GtkButton *button,
734     EmpathyRosterWindow *self)
735 {
736   TpAccount *account;
737   GtkWidget *dialog;
738
739   account = g_object_get_data (G_OBJECT (button), "account");
740   roster_window_remove_error (self, account);
741
742   dialog = gtk_message_dialog_new (GTK_WINDOW (self),
743       GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR,
744       GTK_BUTTONS_OK,
745       _("Sorry, %s accounts can’t be used until your %s software is updated."),
746       tp_account_get_protocol (account),
747       tp_account_get_protocol (account));
748
749   g_signal_connect_swapped (dialog, "response",
750       G_CALLBACK (gtk_widget_destroy),
751       dialog);
752
753   gtk_widget_show (dialog);
754 }
755
756 static void
757 roster_window_upgrade_software_error (EmpathyRosterWindow *self,
758     TpAccount *account)
759 {
760   GtkWidget *info_bar;
761   GtkWidget *content_area;
762   GtkWidget *label;
763   GtkWidget *image;
764   GtkWidget *upgrade_button;
765   GtkWidget *close_button;
766   GtkWidget *action_area;
767   GtkWidget *action_grid;
768   gchar *str;
769   const gchar *icon_name;
770   const gchar *error_message;
771   gboolean user_requested;
772
773   error_message =
774     empathy_account_get_error_message (account, &user_requested);
775
776   if (user_requested)
777     return;
778
779   str = g_markup_printf_escaped ("<b>%s</b>\n%s",
780       tp_account_get_display_name (account),
781       error_message);
782
783   /* If there are other errors, remove them */
784   roster_window_remove_error (self, account);
785
786   info_bar = gtk_info_bar_new ();
787   gtk_info_bar_set_message_type (GTK_INFO_BAR (info_bar), GTK_MESSAGE_ERROR);
788
789   gtk_widget_set_no_show_all (info_bar, TRUE);
790   gtk_box_pack_start (GTK_BOX (self->priv->errors_vbox), info_bar, FALSE, TRUE, 0);
791   gtk_widget_show (info_bar);
792
793   icon_name = tp_account_get_icon_name (account);
794   image = gtk_image_new_from_icon_name (icon_name, GTK_ICON_SIZE_SMALL_TOOLBAR);
795   gtk_widget_show (image);
796
797   label = gtk_label_new (str);
798   gtk_label_set_use_markup (GTK_LABEL (label), TRUE);
799   gtk_label_set_ellipsize (GTK_LABEL (label), PANGO_ELLIPSIZE_END);
800   gtk_misc_set_alignment (GTK_MISC (label), 0, 0.5);
801   gtk_widget_show (label);
802   g_free (str);
803
804   content_area = gtk_info_bar_get_content_area (GTK_INFO_BAR (info_bar));
805   gtk_box_pack_start (GTK_BOX (content_area), image, FALSE, FALSE, 0);
806   gtk_box_pack_start (GTK_BOX (content_area), label, TRUE, TRUE, 0);
807
808   image = gtk_image_new_from_stock (GTK_STOCK_REFRESH, GTK_ICON_SIZE_BUTTON);
809   upgrade_button = gtk_button_new ();
810   gtk_button_set_image (GTK_BUTTON (upgrade_button), image);
811   gtk_widget_set_tooltip_text (upgrade_button, _("Update software..."));
812   gtk_widget_show (upgrade_button);
813
814   image = gtk_image_new_from_stock (GTK_STOCK_CLOSE, GTK_ICON_SIZE_BUTTON);
815   close_button = gtk_button_new ();
816   gtk_button_set_image (GTK_BUTTON (close_button), image);
817   gtk_widget_set_tooltip_text (close_button, _("Close"));
818   gtk_widget_show (close_button);
819
820   action_grid = gtk_grid_new ();
821   gtk_grid_set_column_spacing (GTK_GRID (action_grid), 2);
822   gtk_widget_show (action_grid);
823
824   action_area = gtk_info_bar_get_action_area (GTK_INFO_BAR (info_bar));
825   gtk_box_pack_start (GTK_BOX (action_area), action_grid, FALSE, FALSE, 0);
826
827   gtk_grid_attach (GTK_GRID (action_grid), upgrade_button, 0, 0, 1, 1);
828   gtk_grid_attach (GTK_GRID (action_grid), close_button, 1, 0, 1, 1);
829
830   g_object_set_data (G_OBJECT (info_bar), "label", label);
831   g_object_set_data_full (G_OBJECT (info_bar),
832       "account", g_object_ref (account),
833       g_object_unref);
834   g_object_set_data_full (G_OBJECT (upgrade_button),
835       "account", g_object_ref (account),
836       g_object_unref);
837   g_object_set_data_full (G_OBJECT (close_button),
838       "account", g_object_ref (account),
839       g_object_unref);
840
841   g_signal_connect (upgrade_button, "clicked",
842       G_CALLBACK (roster_window_error_upgrade_sw_clicked_cb), self);
843   g_signal_connect (close_button, "clicked",
844       G_CALLBACK (roster_window_error_close_clicked_cb), self);
845
846   gtk_widget_set_tooltip_text (self->priv->errors_vbox, error_message);
847   gtk_widget_show (self->priv->errors_vbox);
848
849   g_hash_table_insert (self->priv->errors, g_object_ref (account), info_bar);
850 }
851
852 static void
853 roster_window_error_display (EmpathyRosterWindow *self,
854     TpAccount *account)
855 {
856   GtkWidget *info_bar;
857   GtkWidget *content_area;
858   GtkWidget *label;
859   GtkWidget *image;
860   GtkWidget *retry_button;
861   GtkWidget *edit_button;
862   GtkWidget *close_button;
863   GtkWidget *action_area;
864   GtkWidget *action_grid;
865   gchar *str;
866   const gchar *icon_name;
867   const gchar *error_message;
868   gboolean user_requested;
869
870   if (!tp_strdiff (TP_ERROR_STR_SOFTWARE_UPGRADE_REQUIRED,
871        tp_account_get_detailed_error (account, NULL)))
872     {
873       roster_window_upgrade_software_error (self, account);
874       return;
875     }
876
877   error_message = empathy_account_get_error_message (account, &user_requested);
878
879   if (user_requested)
880     return;
881
882   str = g_markup_printf_escaped ("<b>%s</b>\n%s",
883       tp_account_get_display_name (account), error_message);
884
885   info_bar = g_hash_table_lookup (self->priv->errors, account);
886   if (info_bar)
887     {
888       label = g_object_get_data (G_OBJECT (info_bar), "label");
889
890       /* Just set the latest error and return */
891       gtk_label_set_markup (GTK_LABEL (label), str);
892       g_free (str);
893
894       return;
895     }
896
897   info_bar = gtk_info_bar_new ();
898   gtk_info_bar_set_message_type (GTK_INFO_BAR (info_bar), GTK_MESSAGE_ERROR);
899
900   gtk_widget_set_no_show_all (info_bar, TRUE);
901   gtk_box_pack_start (GTK_BOX (self->priv->errors_vbox), info_bar, FALSE, TRUE, 0);
902   gtk_widget_show (info_bar);
903
904   icon_name = tp_account_get_icon_name (account);
905   image = gtk_image_new_from_icon_name (icon_name, GTK_ICON_SIZE_SMALL_TOOLBAR);
906   gtk_widget_show (image);
907
908   label = gtk_label_new (str);
909   gtk_label_set_use_markup (GTK_LABEL (label), TRUE);
910   gtk_label_set_ellipsize (GTK_LABEL (label), PANGO_ELLIPSIZE_END);
911   gtk_misc_set_alignment (GTK_MISC (label), 0, 0.5);
912   gtk_widget_show (label);
913   g_free (str);
914
915   content_area = gtk_info_bar_get_content_area (GTK_INFO_BAR (info_bar));
916   gtk_box_pack_start (GTK_BOX (content_area), image, FALSE, FALSE, 0);
917   gtk_box_pack_start (GTK_BOX (content_area), label, TRUE, TRUE, 0);
918
919   image = gtk_image_new_from_stock (GTK_STOCK_REFRESH, GTK_ICON_SIZE_BUTTON);
920   retry_button = gtk_button_new ();
921   gtk_button_set_image (GTK_BUTTON (retry_button), image);
922   gtk_widget_set_tooltip_text (retry_button, _("Reconnect"));
923   gtk_widget_show (retry_button);
924
925   image = gtk_image_new_from_stock (GTK_STOCK_EDIT, GTK_ICON_SIZE_BUTTON);
926   edit_button = gtk_button_new ();
927   gtk_button_set_image (GTK_BUTTON (edit_button), image);
928   gtk_widget_set_tooltip_text (edit_button, _("Edit Account"));
929   gtk_widget_show (edit_button);
930
931   image = gtk_image_new_from_stock (GTK_STOCK_CLOSE, GTK_ICON_SIZE_BUTTON);
932   close_button = gtk_button_new ();
933   gtk_button_set_image (GTK_BUTTON (close_button), image);
934   gtk_widget_set_tooltip_text (close_button, _("Close"));
935   gtk_widget_show (close_button);
936
937   action_grid = gtk_grid_new ();
938   gtk_grid_set_column_spacing (GTK_GRID (action_grid), 2);
939   gtk_widget_show (action_grid);
940
941   action_area = gtk_info_bar_get_action_area (GTK_INFO_BAR (info_bar));
942   gtk_box_pack_start (GTK_BOX (action_area), action_grid, FALSE, FALSE, 0);
943
944   gtk_grid_attach (GTK_GRID (action_grid), retry_button, 0, 0, 1, 1);
945   gtk_grid_attach (GTK_GRID (action_grid), edit_button, 1, 0, 1, 1);
946   gtk_grid_attach (GTK_GRID (action_grid), close_button, 2, 0, 1, 1);
947
948   g_object_set_data (G_OBJECT (info_bar), "label", label);
949   g_object_set_data_full (G_OBJECT (info_bar),
950       "account", g_object_ref (account),
951       g_object_unref);
952   g_object_set_data_full (G_OBJECT (edit_button),
953       "account", g_object_ref (account),
954       g_object_unref);
955   g_object_set_data_full (G_OBJECT (close_button),
956       "account", g_object_ref (account),
957       g_object_unref);
958   g_object_set_data_full (G_OBJECT (retry_button),
959       "account", g_object_ref (account),
960       g_object_unref);
961
962   g_signal_connect (edit_button, "clicked",
963       G_CALLBACK (roster_window_error_edit_clicked_cb), self);
964   g_signal_connect (close_button, "clicked",
965       G_CALLBACK (roster_window_error_close_clicked_cb), self);
966   g_signal_connect (retry_button, "clicked",
967       G_CALLBACK (roster_window_error_retry_clicked_cb), self);
968
969   gtk_widget_set_tooltip_text (self->priv->errors_vbox, error_message);
970   gtk_widget_show (self->priv->errors_vbox);
971
972   g_hash_table_insert (self->priv->errors, g_object_ref (account), info_bar);
973 }
974
975 static void
976 roster_window_update_status (EmpathyRosterWindow *self)
977 {
978   gboolean connected, connecting;
979   GList *l, *children;
980
981   connected = empathy_account_manager_get_accounts_connected (&connecting);
982
983   /* Update the spinner state */
984   if (connecting)
985     {
986       gtk_spinner_start (GTK_SPINNER (self->priv->throbber));
987       gtk_widget_show (self->priv->throbber_tool_item);
988     }
989   else
990     {
991       gtk_spinner_stop (GTK_SPINNER (self->priv->throbber));
992       gtk_widget_hide (self->priv->throbber_tool_item);
993     }
994
995   /* Update widgets sensibility */
996   for (l = self->priv->actions_connected; l; l = l->next)
997     gtk_action_set_sensitive (l->data, connected);
998
999   /* Update favourite rooms sensitivity */
1000   children = gtk_container_get_children (GTK_CONTAINER (self->priv->room_menu));
1001   for (l = children; l != NULL; l = l->next)
1002     {
1003       if (g_object_get_data (G_OBJECT (l->data), "is_favorite") != NULL)
1004         gtk_widget_set_sensitive (GTK_WIDGET (l->data), connected);
1005     }
1006
1007   g_list_free (children);
1008 }
1009
1010 static char *
1011 roster_window_account_to_action_name (TpAccount *account)
1012 {
1013   char *r;
1014
1015   /* action names can't have '/' in them, replace it with '.' */
1016   r = g_strdup (tp_account_get_path_suffix (account));
1017   r = g_strdelimit (r, "/", '.');
1018
1019   return r;
1020 }
1021
1022 static void
1023 roster_window_balance_activate_cb (GtkAction *action,
1024     EmpathyRosterWindow *self)
1025 {
1026   const char *uri;
1027
1028   uri = g_object_get_data (G_OBJECT (action), "manage-credit-uri");
1029
1030   if (!tp_str_empty (uri))
1031     {
1032       DEBUG ("Top-up credit URI: %s", uri);
1033       empathy_url_show (GTK_WIDGET (self), uri);
1034     }
1035   else
1036     {
1037       DEBUG ("unknown protocol for top-up");
1038     }
1039 }
1040
1041 static void
1042 roster_window_balance_update_balance (GtkAction *action,
1043     TpConnection *conn)
1044 {
1045   TpAccount *account = tp_connection_get_account (conn);
1046   GtkWidget *label;
1047   int amount = 0;
1048   guint scale = G_MAXINT32;
1049   const gchar *currency = "";
1050   char *money, *str;
1051
1052   if (!tp_connection_get_balance (conn, &amount, &scale, &currency))
1053     return;
1054
1055   if (amount == 0 &&
1056       scale == G_MAXINT32 &&
1057       tp_str_empty (currency))
1058     {
1059       /* unknown balance */
1060       money = g_strdup ("--");
1061     }
1062   else
1063     {
1064       char *tmp = empathy_format_currency (amount, scale, currency);
1065
1066       money = g_strdup_printf ("%s %s", currency, tmp);
1067       g_free (tmp);
1068     }
1069
1070   /* Translators: this string will be something like:
1071    * Top up My Account ($1.23)..." */
1072   str = g_strdup_printf (_("Top up %s (%s)..."),
1073     tp_account_get_display_name (account), money);
1074
1075   gtk_action_set_label (action, str);
1076   g_free (str);
1077
1078   /* update the money label in the roster */
1079   label = g_object_get_data (G_OBJECT (action), "money-label");
1080
1081   gtk_label_set_text (GTK_LABEL (label), money);
1082   g_free (money);
1083 }
1084
1085 static void
1086 roster_window_balance_changed_cb (TpConnection *conn,
1087     guint balance,
1088     guint scale,
1089     const gchar *currency,
1090     GtkAction *action)
1091 {
1092   roster_window_balance_update_balance (action, conn);
1093 }
1094
1095 static GtkAction *
1096 roster_window_setup_balance_create_action (EmpathyRosterWindow *self,
1097     TpAccount *account)
1098 {
1099   GtkAction *action;
1100   char *name, *ui;
1101   guint merge_id;
1102   GError *error = NULL;
1103
1104   /* create the action group if required */
1105   if (self->priv->balance_action_group == NULL)
1106     {
1107       self->priv->balance_action_group =
1108         gtk_action_group_new ("balance-action-group");
1109
1110       gtk_ui_manager_insert_action_group (self->priv->ui_manager,
1111         self->priv->balance_action_group, -1);
1112     }
1113
1114   /* create the action */
1115   name = roster_window_account_to_action_name (account);
1116   action = gtk_action_new (name,
1117       tp_account_get_display_name (account),
1118       _("Top up account credit"), NULL);
1119
1120   g_object_bind_property (account, "icon-name", action, "icon-name",
1121       G_BINDING_SYNC_CREATE);
1122
1123   g_signal_connect (action, "activate",
1124       G_CALLBACK (roster_window_balance_activate_cb), self);
1125
1126   gtk_action_group_add_action (self->priv->balance_action_group, action);
1127   g_object_unref (action);
1128
1129   ui = g_strdup_printf (
1130       "<ui>"
1131       " <menubar name='menubar'>"
1132       "  <menu action='view'>"
1133       "   <placeholder name='view_balance_placeholder'>"
1134       "    <menuitem action='%s'/>"
1135       "   </placeholder>"
1136       "  </menu>"
1137       " </menubar>"
1138       "</ui>",
1139       name);
1140
1141   merge_id = gtk_ui_manager_add_ui_from_string (self->priv->ui_manager,
1142       ui, -1, &error);
1143
1144   if (error != NULL)
1145     {
1146       DEBUG ("Failed to add balance UI for %s: %s",
1147         tp_account_get_display_name (account),
1148         error->message);
1149       g_error_free (error);
1150     }
1151
1152   g_object_set_data (G_OBJECT (action),
1153       "merge-id", GUINT_TO_POINTER (merge_id));
1154
1155   g_free (name);
1156   g_free (ui);
1157
1158   return action;
1159 }
1160
1161 static GtkWidget *
1162 roster_window_setup_balance_create_widget (EmpathyRosterWindow *self,
1163     GtkAction *action,
1164     TpAccount *account)
1165 {
1166   GtkWidget *hbox, *image, *label, *button;
1167
1168   hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6);
1169
1170   /* protocol icon */
1171   image = gtk_image_new ();
1172   gtk_box_pack_start (GTK_BOX (hbox), image, FALSE, TRUE, 0);
1173   g_object_bind_property (action, "icon-name", image, "icon-name",
1174       G_BINDING_SYNC_CREATE);
1175
1176   /* account name label */
1177   label = gtk_label_new ("");
1178   gtk_misc_set_alignment (GTK_MISC (label), 0.0, 0.5);
1179   gtk_box_pack_start (GTK_BOX (hbox), label, TRUE, TRUE, 0);
1180   g_object_bind_property (account, "display-name", label, "label",
1181       G_BINDING_SYNC_CREATE);
1182
1183   /* balance label */
1184   label = gtk_label_new ("");
1185   gtk_misc_set_alignment (GTK_MISC (label), 0.0, 0.5);
1186   gtk_box_pack_start (GTK_BOX (hbox), label, FALSE, TRUE, 0);
1187   g_object_set_data (G_OBJECT (action), "money-label", label);
1188
1189   /* top up button */
1190   button = gtk_button_new_with_label (_("Top Up..."));
1191   gtk_box_pack_start (GTK_BOX (hbox), button, FALSE, TRUE, 0);
1192   g_signal_connect_swapped (button, "clicked",
1193       G_CALLBACK (gtk_action_activate), action);
1194
1195   gtk_box_pack_start (GTK_BOX (self->priv->balance_vbox), hbox, FALSE, TRUE, 0);
1196   gtk_widget_show_all (hbox);
1197
1198   /* tie the lifetime of the widget to the lifetime of the action */
1199   g_object_weak_ref (G_OBJECT (action),
1200       (GWeakNotify) gtk_widget_destroy, hbox);
1201
1202   return hbox;
1203 }
1204
1205 static void
1206 roster_window_setup_balance (EmpathyRosterWindow *self,
1207     TpAccount *account)
1208 {
1209   TpConnection *conn = tp_account_get_connection (account);
1210   GtkAction *action;
1211   const gchar *uri;
1212
1213   if (conn == NULL)
1214     return;
1215
1216   if (!tp_proxy_is_prepared (conn, TP_CONNECTION_FEATURE_BALANCE))
1217     return;
1218
1219   DEBUG ("Setting up balance for acct: %s",
1220       tp_account_get_display_name (account));
1221
1222   /* create the action */
1223   action = roster_window_setup_balance_create_action (self, account);
1224
1225   if (action == NULL)
1226     return;
1227
1228   gtk_action_set_visible (self->priv->view_balance_show_in_roster, TRUE);
1229
1230   /* create the display widget */
1231   roster_window_setup_balance_create_widget (self, action, account);
1232
1233   /* check the current balance and monitor for any changes */
1234   uri = tp_connection_get_balance_uri (conn);
1235
1236   g_object_set_data_full (G_OBJECT (action), "manage-credit-uri",
1237       g_strdup (uri), g_free);
1238   gtk_action_set_sensitive (GTK_ACTION (action), !tp_str_empty (uri));
1239
1240   roster_window_balance_update_balance (GTK_ACTION (action), conn);
1241
1242   g_signal_connect (conn, "balance-changed",
1243       G_CALLBACK (roster_window_balance_changed_cb), action);
1244 }
1245
1246 static void
1247 roster_window_remove_balance_action (EmpathyRosterWindow *self,
1248     TpAccount *account)
1249 {
1250   GtkAction *action;
1251   char *name;
1252   GList *a;
1253
1254   if (self->priv->balance_action_group == NULL)
1255     return;
1256
1257   name = roster_window_account_to_action_name (account);
1258
1259   action = gtk_action_group_get_action (
1260       self->priv->balance_action_group, name);
1261
1262   if (action != NULL)
1263     {
1264       guint merge_id;
1265
1266       DEBUG ("Removing action");
1267
1268       merge_id = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (action),
1269             "merge-id"));
1270
1271       gtk_ui_manager_remove_ui (self->priv->ui_manager,
1272           merge_id);
1273       gtk_action_group_remove_action (
1274           self->priv->balance_action_group, action);
1275     }
1276
1277   g_free (name);
1278
1279   a = gtk_action_group_list_actions (
1280       self->priv->balance_action_group);
1281
1282   gtk_action_set_visible (self->priv->view_balance_show_in_roster,
1283       g_list_length (a) > 0);
1284
1285   g_list_free (a);
1286 }
1287
1288 static void
1289 roster_window_connection_changed_cb (TpAccount  *account,
1290     guint old_status,
1291     guint current,
1292     guint reason,
1293     gchar *dbus_error_name,
1294     GHashTable *details,
1295     EmpathyRosterWindow *self)
1296 {
1297   roster_window_update_status (self);
1298
1299   if (current == TP_CONNECTION_STATUS_DISCONNECTED &&
1300       reason != TP_CONNECTION_STATUS_REASON_REQUESTED)
1301     {
1302       roster_window_error_display (self, account);
1303     }
1304
1305   if (current == TP_CONNECTION_STATUS_DISCONNECTED)
1306     {
1307       empathy_sound_manager_play (self->priv->sound_mgr, GTK_WIDGET (self),
1308               EMPATHY_SOUND_ACCOUNT_DISCONNECTED);
1309     }
1310
1311   if (current == TP_CONNECTION_STATUS_CONNECTED)
1312     {
1313       empathy_sound_manager_play (self->priv->sound_mgr, GTK_WIDGET (self),
1314               EMPATHY_SOUND_ACCOUNT_CONNECTED);
1315
1316       /* Account connected without error, remove error message if any */
1317       roster_window_remove_error (self, account);
1318     }
1319 }
1320
1321 static void
1322 roster_window_accels_load (void)
1323 {
1324   gchar *filename;
1325
1326   filename = g_build_filename (g_get_user_config_dir (),
1327       PACKAGE_NAME, ACCELS_FILENAME, NULL);
1328   if (g_file_test (filename, G_FILE_TEST_EXISTS))
1329     {
1330       DEBUG ("Loading from:'%s'", filename);
1331       gtk_accel_map_load (filename);
1332     }
1333
1334   g_free (filename);
1335 }
1336
1337 static void
1338 roster_window_accels_save (void)
1339 {
1340   gchar *dir;
1341   gchar *file_with_path;
1342
1343   dir = g_build_filename (g_get_user_config_dir (), PACKAGE_NAME, NULL);
1344   g_mkdir_with_parents (dir, S_IRUSR | S_IWUSR | S_IXUSR);
1345   file_with_path = g_build_filename (dir, ACCELS_FILENAME, NULL);
1346   g_free (dir);
1347
1348   DEBUG ("Saving to:'%s'", file_with_path);
1349   gtk_accel_map_save (file_with_path);
1350
1351   g_free (file_with_path);
1352 }
1353
1354 static void
1355 empathy_roster_window_finalize (GObject *window)
1356 {
1357   EmpathyRosterWindow *self = EMPATHY_ROSTER_WINDOW (window);
1358   GHashTableIter iter;
1359   gpointer key, value;
1360
1361   /* Save user-defined accelerators. */
1362   roster_window_accels_save ();
1363
1364   g_list_free (self->priv->actions_connected);
1365
1366   g_object_unref (self->priv->account_manager);
1367   g_object_unref (self->priv->individual_store);
1368   g_object_unref (self->priv->sound_mgr);
1369   g_hash_table_unref (self->priv->errors);
1370   g_hash_table_unref (self->priv->auths);
1371
1372   /* disconnect all handlers of status-changed signal */
1373   g_hash_table_iter_init (&iter, self->priv->status_changed_handlers);
1374   while (g_hash_table_iter_next (&iter, &key, &value))
1375     g_signal_handler_disconnect (TP_ACCOUNT (key), GPOINTER_TO_UINT (value));
1376
1377   g_hash_table_unref (self->priv->status_changed_handlers);
1378
1379   g_signal_handlers_disconnect_by_func (self->priv->event_manager,
1380       roster_window_event_added_cb, self);
1381   g_signal_handlers_disconnect_by_func (self->priv->event_manager,
1382       roster_window_event_removed_cb, self);
1383
1384   g_object_unref (self->priv->call_observer);
1385   g_object_unref (self->priv->event_manager);
1386   g_object_unref (self->priv->ui_manager);
1387   g_object_unref (self->priv->chatroom_manager);
1388
1389   g_object_unref (self->priv->gsettings_ui);
1390   g_object_unref (self->priv->gsettings_contacts);
1391
1392   G_OBJECT_CLASS (empathy_roster_window_parent_class)->finalize (window);
1393 }
1394
1395 static gboolean
1396 roster_window_key_press_event_cb  (GtkWidget *window,
1397     GdkEventKey *event,
1398     gpointer user_data)
1399 {
1400   if (event->keyval == GDK_KEY_T
1401       && event->state & GDK_SHIFT_MASK
1402       && event->state & GDK_CONTROL_MASK)
1403     empathy_chat_manager_call_undo_closed_chat ();
1404
1405   return FALSE;
1406 }
1407
1408 static void
1409 roster_window_chat_quit_cb (GtkAction *action,
1410     EmpathyRosterWindow *self)
1411 {
1412   gtk_widget_destroy (GTK_WIDGET (self));
1413 }
1414
1415 static void
1416 roster_window_view_history_cb (GtkAction *action,
1417     EmpathyRosterWindow *self)
1418 {
1419   empathy_log_window_show (NULL, NULL, FALSE, GTK_WINDOW (self));
1420 }
1421
1422 static void
1423 roster_window_chat_new_message_cb (GtkAction *action,
1424     EmpathyRosterWindow *self)
1425 {
1426   empathy_new_message_dialog_show (GTK_WINDOW (self));
1427 }
1428
1429 static void
1430 roster_window_chat_new_call_cb (GtkAction *action,
1431     EmpathyRosterWindow *self)
1432 {
1433   empathy_new_call_dialog_show (GTK_WINDOW (self));
1434 }
1435
1436 static void
1437 roster_window_chat_add_contact_cb (GtkAction *action,
1438     EmpathyRosterWindow *self)
1439 {
1440   empathy_new_individual_dialog_show (GTK_WINDOW (self));
1441 }
1442
1443 static void
1444 roster_window_chat_search_contacts_cb (GtkAction *action,
1445     EmpathyRosterWindow *self)
1446 {
1447   GtkWidget *dialog = empathy_contact_search_dialog_new (
1448       GTK_WINDOW (self));
1449
1450   gtk_widget_show (dialog);
1451 }
1452
1453 static void
1454 roster_window_view_show_ft_manager (GtkAction *action,
1455     EmpathyRosterWindow *self)
1456 {
1457   empathy_ft_manager_show ();
1458 }
1459
1460 static void
1461 roster_window_view_show_offline_cb (GtkToggleAction   *action,
1462     EmpathyRosterWindow *self)
1463 {
1464   gboolean current;
1465
1466   current = gtk_toggle_action_get_active (action);
1467   g_settings_set_boolean (self->priv->gsettings_ui,
1468       EMPATHY_PREFS_UI_SHOW_OFFLINE,
1469       current);
1470
1471   empathy_individual_view_set_show_offline (self->priv->individual_view,
1472       current);
1473 }
1474
1475 static void
1476 roster_window_notify_sort_contact_cb (GSettings         *gsettings,
1477     const gchar *key,
1478     EmpathyRosterWindow *self)
1479 {
1480   gchar *str;
1481
1482   str = g_settings_get_string (gsettings, key);
1483
1484   if (str != NULL)
1485     {
1486       GType       type;
1487       GEnumClass *enum_class;
1488       GEnumValue *enum_value;
1489
1490       type = empathy_individual_store_sort_get_type ();
1491       enum_class = G_ENUM_CLASS (g_type_class_peek (type));
1492       enum_value = g_enum_get_value_by_nick (enum_class, str);
1493       if (enum_value)
1494         {
1495           /* By changing the value of the GtkRadioAction,
1496              it emits a signal that calls roster_window_view_sort_contacts_cb
1497              which updates the contacts list */
1498           gtk_radio_action_set_current_value (self->priv->sort_by_name,
1499                       enum_value->value);
1500         }
1501       else
1502         {
1503           g_warning ("Wrong value for sort_criterium configuration : %s", str);
1504         }
1505     g_free (str);
1506   }
1507 }
1508
1509 static void
1510 roster_window_view_sort_contacts_cb (GtkRadioAction *action,
1511     GtkRadioAction *current,
1512     EmpathyRosterWindow *self)
1513 {
1514   EmpathyIndividualStoreSort value;
1515   GSList *group;
1516   GType type;
1517   GEnumClass *enum_class;
1518   GEnumValue *enum_value;
1519
1520   value = gtk_radio_action_get_current_value (action);
1521   group = gtk_radio_action_get_group (action);
1522
1523   /* Get string from index */
1524   type = empathy_individual_store_sort_get_type ();
1525   enum_class = G_ENUM_CLASS (g_type_class_peek (type));
1526   enum_value = g_enum_get_value (enum_class, g_slist_index (group, current));
1527
1528   if (!enum_value)
1529     g_warning ("No GEnumValue for EmpathyContactListSort with GtkRadioAction index:%d",
1530         g_slist_index (group, action));
1531   else
1532     g_settings_set_string (self->priv->gsettings_contacts,
1533         EMPATHY_PREFS_CONTACTS_SORT_CRITERIUM,
1534         enum_value->value_nick);
1535
1536   empathy_individual_store_set_sort_criterium (self->priv->individual_store,
1537       value);
1538 }
1539
1540 static void
1541 roster_window_view_show_protocols_cb (GtkToggleAction *action,
1542     EmpathyRosterWindow *self)
1543 {
1544   gboolean value;
1545
1546   value = gtk_toggle_action_get_active (action);
1547
1548   g_settings_set_boolean (self->priv->gsettings_ui,
1549       EMPATHY_PREFS_UI_SHOW_PROTOCOLS,
1550       value);
1551
1552   empathy_individual_store_set_show_protocols (self->priv->individual_store,
1553       value);
1554 }
1555
1556 /* Matches GtkRadioAction values set in empathy-roster-window.ui */
1557 #define CONTACT_LIST_NORMAL_SIZE_WITH_AVATARS   0
1558 #define CONTACT_LIST_NORMAL_SIZE      1
1559 #define CONTACT_LIST_COMPACT_SIZE     2
1560
1561 static void
1562 roster_window_view_contacts_list_size_cb (GtkRadioAction *action,
1563     GtkRadioAction *current,
1564     EmpathyRosterWindow *self)
1565 {
1566   GSettings *gsettings_ui;
1567   gint value;
1568
1569   value = gtk_radio_action_get_current_value (action);
1570   /* create a new GSettings, so we can delay the setting until both
1571    * values are set */
1572   gsettings_ui = g_settings_new (EMPATHY_PREFS_UI_SCHEMA);
1573
1574   DEBUG ("radio button toggled, value = %i", value);
1575
1576   g_settings_delay (gsettings_ui);
1577   g_settings_set_boolean (gsettings_ui, EMPATHY_PREFS_UI_SHOW_AVATARS,
1578       value == CONTACT_LIST_NORMAL_SIZE_WITH_AVATARS);
1579
1580   g_settings_set_boolean (gsettings_ui, EMPATHY_PREFS_UI_COMPACT_CONTACT_LIST,
1581       value == CONTACT_LIST_COMPACT_SIZE);
1582   g_settings_apply (gsettings_ui);
1583
1584   /* FIXME: these enums probably have the wrong namespace */
1585   empathy_individual_store_set_show_avatars (self->priv->individual_store,
1586       value == CONTACT_LIST_NORMAL_SIZE_WITH_AVATARS);
1587   empathy_individual_store_set_is_compact (self->priv->individual_store,
1588       value == CONTACT_LIST_COMPACT_SIZE);
1589
1590   g_object_unref (gsettings_ui);
1591 }
1592
1593 static void roster_window_notify_show_protocols_cb (GSettings *gsettings,
1594     const gchar *key,
1595     EmpathyRosterWindow *self)
1596 {
1597   gtk_toggle_action_set_active (self->priv->show_protocols,
1598       g_settings_get_boolean (gsettings, EMPATHY_PREFS_UI_SHOW_PROTOCOLS));
1599 }
1600
1601
1602 static void
1603 roster_window_notify_contact_list_size_cb (GSettings *gsettings,
1604     const gchar *key,
1605     EmpathyRosterWindow *self)
1606 {
1607   gint value;
1608
1609   if (g_settings_get_boolean (gsettings,
1610       EMPATHY_PREFS_UI_COMPACT_CONTACT_LIST))
1611     value = CONTACT_LIST_COMPACT_SIZE;
1612   else if (g_settings_get_boolean (gsettings,
1613       EMPATHY_PREFS_UI_SHOW_AVATARS))
1614     value = CONTACT_LIST_NORMAL_SIZE_WITH_AVATARS;
1615   else
1616     value = CONTACT_LIST_NORMAL_SIZE;
1617
1618   DEBUG ("setting changed, value = %i", value);
1619
1620   /* By changing the value of the GtkRadioAction,
1621      it emits a signal that calls roster_window_view_contacts_list_size_cb
1622      which updates the contacts list */
1623   gtk_radio_action_set_current_value (self->priv->normal_with_avatars, value);
1624 }
1625
1626 static void
1627 roster_window_edit_search_contacts_cb (GtkCheckMenuItem  *item,
1628     EmpathyRosterWindow *self)
1629 {
1630   empathy_individual_view_start_search (self->priv->individual_view);
1631 }
1632
1633 static void
1634 roster_window_view_show_map_cb (GtkCheckMenuItem  *item,
1635     EmpathyRosterWindow *self)
1636 {
1637 #ifdef HAVE_LIBCHAMPLAIN
1638   empathy_map_view_show ();
1639 #endif
1640 }
1641
1642 static void
1643 join_chatroom (EmpathyChatroom *chatroom,
1644     gint64 timestamp)
1645 {
1646   TpAccount *account;
1647   const gchar *room;
1648
1649   account = empathy_chatroom_get_account (chatroom);
1650   room = empathy_chatroom_get_room (chatroom);
1651
1652   DEBUG ("Requesting channel for '%s'", room);
1653   empathy_join_muc (account, room, timestamp);
1654 }
1655
1656 typedef struct
1657 {
1658   TpAccount *account;
1659   EmpathyChatroom *chatroom;
1660   gint64 timestamp;
1661   glong sig_id;
1662   guint timeout;
1663 } join_fav_account_sig_ctx;
1664
1665 static join_fav_account_sig_ctx *
1666 join_fav_account_sig_ctx_new (TpAccount *account,
1667     EmpathyChatroom *chatroom,
1668     gint64 timestamp)
1669 {
1670   join_fav_account_sig_ctx *ctx = g_slice_new0 (
1671       join_fav_account_sig_ctx);
1672
1673   ctx->account = g_object_ref (account);
1674   ctx->chatroom = g_object_ref (chatroom);
1675   ctx->timestamp = timestamp;
1676   return ctx;
1677 }
1678
1679 static void
1680 join_fav_account_sig_ctx_free (join_fav_account_sig_ctx *ctx)
1681 {
1682   g_object_unref (ctx->account);
1683   g_object_unref (ctx->chatroom);
1684   g_slice_free (join_fav_account_sig_ctx, ctx);
1685 }
1686
1687 static void
1688 account_status_changed_cb (TpAccount  *account,
1689     TpConnectionStatus old_status,
1690     TpConnectionStatus new_status,
1691     guint reason,
1692     gchar *dbus_error_name,
1693     GHashTable *details,
1694     gpointer user_data)
1695 {
1696   join_fav_account_sig_ctx *ctx = user_data;
1697
1698   switch (new_status)
1699     {
1700       case TP_CONNECTION_STATUS_DISCONNECTED:
1701         /* Don't wait any longer */
1702         goto finally;
1703         break;
1704
1705       case TP_CONNECTION_STATUS_CONNECTING:
1706         /* Wait a bit */
1707         return;
1708
1709       case TP_CONNECTION_STATUS_CONNECTED:
1710         /* We can join the room */
1711         break;
1712
1713       default:
1714         g_assert_not_reached ();
1715     }
1716
1717   join_chatroom (ctx->chatroom, ctx->timestamp);
1718
1719 finally:
1720   g_source_remove (ctx->timeout);
1721   g_signal_handler_disconnect (account, ctx->sig_id);
1722 }
1723
1724 #define JOIN_FAVORITE_TIMEOUT 5
1725
1726 static gboolean
1727 join_favorite_timeout_cb (gpointer data)
1728 {
1729   join_fav_account_sig_ctx *ctx = data;
1730
1731   /* stop waiting for joining the favorite room */
1732   g_signal_handler_disconnect (ctx->account, ctx->sig_id);
1733   return FALSE;
1734 }
1735
1736 static void
1737 roster_window_favorite_chatroom_join (EmpathyChatroom *chatroom)
1738 {
1739   TpAccount *account;
1740
1741   account = empathy_chatroom_get_account (chatroom);
1742   if (tp_account_get_connection_status (account, NULL) !=
1743                TP_CONNECTION_STATUS_CONNECTED)
1744     {
1745       join_fav_account_sig_ctx *ctx;
1746
1747       ctx = join_fav_account_sig_ctx_new (account, chatroom,
1748         empathy_get_current_action_time ());
1749
1750       ctx->sig_id = g_signal_connect_data (account, "status-changed",
1751         G_CALLBACK (account_status_changed_cb), ctx,
1752         (GClosureNotify) join_fav_account_sig_ctx_free, 0);
1753
1754       ctx->timeout = g_timeout_add_seconds (JOIN_FAVORITE_TIMEOUT,
1755         join_favorite_timeout_cb, ctx);
1756       return;
1757     }
1758
1759   join_chatroom (chatroom, empathy_get_current_action_time ());
1760 }
1761
1762 static void
1763 roster_window_favorite_chatroom_menu_activate_cb (GtkMenuItem *menu_item,
1764     EmpathyChatroom *chatroom)
1765 {
1766   roster_window_favorite_chatroom_join (chatroom);
1767 }
1768
1769 static void
1770 roster_window_favorite_chatroom_menu_add (EmpathyRosterWindow *self,
1771     EmpathyChatroom *chatroom)
1772 {
1773   GtkWidget *menu_item;
1774   const gchar *name, *account_name;
1775   gchar *label;
1776
1777   if (g_object_get_data (G_OBJECT (chatroom), "menu_item"))
1778     return;
1779
1780   name = empathy_chatroom_get_name (chatroom);
1781   account_name = tp_account_get_display_name (
1782       empathy_chatroom_get_account (chatroom));
1783   label = g_strdup_printf ("%s (%s)", name, account_name);
1784   menu_item = gtk_menu_item_new_with_label (label);
1785   g_free (label);
1786   g_object_set_data (G_OBJECT (menu_item), "is_favorite",
1787       GUINT_TO_POINTER (TRUE));
1788
1789   g_object_set_data (G_OBJECT (chatroom), "menu_item", menu_item);
1790   g_signal_connect (menu_item, "activate",
1791       G_CALLBACK (roster_window_favorite_chatroom_menu_activate_cb),
1792       chatroom);
1793
1794   gtk_menu_shell_insert (GTK_MENU_SHELL (self->priv->room_menu),
1795       menu_item, 4);
1796
1797   gtk_widget_show (menu_item);
1798 }
1799
1800 static void
1801 roster_window_favorite_chatroom_menu_added_cb (EmpathyChatroomManager *manager,
1802     EmpathyChatroom *chatroom,
1803     EmpathyRosterWindow *self)
1804 {
1805   roster_window_favorite_chatroom_menu_add (self, chatroom);
1806   gtk_widget_show (self->priv->room_separator);
1807   gtk_action_set_sensitive (self->priv->room_join_favorites, TRUE);
1808 }
1809
1810 static void
1811 roster_window_favorite_chatroom_menu_removed_cb (
1812     EmpathyChatroomManager *manager,
1813     EmpathyChatroom *chatroom,
1814     EmpathyRosterWindow *self)
1815 {
1816   GtkWidget *menu_item;
1817   GList *chatrooms;
1818
1819   menu_item = g_object_get_data (G_OBJECT (chatroom), "menu_item");
1820   g_object_set_data (G_OBJECT (chatroom), "menu_item", NULL);
1821   gtk_widget_destroy (menu_item);
1822
1823   chatrooms = empathy_chatroom_manager_get_chatrooms (self->priv->chatroom_manager,
1824       NULL);
1825   if (chatrooms)
1826     gtk_widget_show (self->priv->room_separator);
1827   else
1828     gtk_widget_hide (self->priv->room_separator);
1829
1830   gtk_action_set_sensitive (self->priv->room_join_favorites, chatrooms != NULL);
1831   g_list_free (chatrooms);
1832 }
1833
1834 static void
1835 roster_window_favorite_chatroom_menu_setup (EmpathyRosterWindow *self)
1836 {
1837   GList *chatrooms, *l;
1838   GtkWidget *room;
1839
1840   self->priv->chatroom_manager = empathy_chatroom_manager_dup_singleton (NULL);
1841   chatrooms = empathy_chatroom_manager_get_chatrooms (
1842     self->priv->chatroom_manager, NULL);
1843   room = gtk_ui_manager_get_widget (self->priv->ui_manager,
1844     "/menubar/room");
1845   self->priv->room_menu = gtk_menu_item_get_submenu (GTK_MENU_ITEM (room));
1846   self->priv->room_separator = gtk_ui_manager_get_widget (self->priv->ui_manager,
1847     "/menubar/room/room_separator");
1848
1849   for (l = chatrooms; l; l = l->next)
1850     roster_window_favorite_chatroom_menu_add (self, l->data);
1851
1852   if (!chatrooms)
1853     gtk_widget_hide (self->priv->room_separator);
1854
1855   gtk_action_set_sensitive (self->priv->room_join_favorites, chatrooms != NULL);
1856
1857   g_signal_connect (self->priv->chatroom_manager, "chatroom-added",
1858       G_CALLBACK (roster_window_favorite_chatroom_menu_added_cb),
1859       self);
1860
1861   g_signal_connect (self->priv->chatroom_manager, "chatroom-removed",
1862       G_CALLBACK (roster_window_favorite_chatroom_menu_removed_cb),
1863       self);
1864
1865   g_list_free (chatrooms);
1866 }
1867
1868 static void
1869 roster_window_room_join_new_cb (GtkAction *action,
1870     EmpathyRosterWindow *self)
1871 {
1872   empathy_new_chatroom_dialog_show (GTK_WINDOW (self));
1873 }
1874
1875 static void
1876 roster_window_room_join_favorites_cb (GtkAction *action,
1877     EmpathyRosterWindow *self)
1878 {
1879   GList *chatrooms, *l;
1880
1881   chatrooms = empathy_chatroom_manager_get_chatrooms (self->priv->chatroom_manager,
1882       NULL);
1883
1884   for (l = chatrooms; l; l = l->next)
1885     roster_window_favorite_chatroom_join (l->data);
1886
1887   g_list_free (chatrooms);
1888 }
1889
1890 static void
1891 roster_window_room_manage_favorites_cb (GtkAction *action,
1892     EmpathyRosterWindow *self)
1893 {
1894   empathy_chatrooms_window_show (GTK_WINDOW (self));
1895 }
1896
1897 static void
1898 roster_window_edit_cb (GtkAction *action,
1899     EmpathyRosterWindow *self)
1900 {
1901   GtkWidget *submenu;
1902
1903   /* FIXME: It should use the UIManager to merge the contact/group submenu */
1904   submenu = empathy_individual_view_get_individual_menu (
1905       self->priv->individual_view);
1906   if (submenu)
1907     {
1908       GtkMenuItem *item;
1909       GtkWidget   *label;
1910
1911       item = GTK_MENU_ITEM (self->priv->edit_context);
1912       label = gtk_bin_get_child (GTK_BIN (item));
1913       gtk_label_set_text (GTK_LABEL (label), _("Contact"));
1914
1915       gtk_widget_show (self->priv->edit_context);
1916       gtk_widget_show (self->priv->edit_context_separator);
1917
1918       gtk_menu_item_set_submenu (item, submenu);
1919
1920       return;
1921     }
1922
1923   submenu = empathy_individual_view_get_group_menu (self->priv->individual_view);
1924   if (submenu)
1925     {
1926       GtkMenuItem *item;
1927       GtkWidget   *label;
1928
1929       item = GTK_MENU_ITEM (self->priv->edit_context);
1930       label = gtk_bin_get_child (GTK_BIN (item));
1931       gtk_label_set_text (GTK_LABEL (label), _("Group"));
1932
1933       gtk_widget_show (self->priv->edit_context);
1934       gtk_widget_show (self->priv->edit_context_separator);
1935
1936       gtk_menu_item_set_submenu (item, submenu);
1937
1938       return;
1939     }
1940
1941   gtk_widget_hide (self->priv->edit_context);
1942   gtk_widget_hide (self->priv->edit_context_separator);
1943
1944   return;
1945 }
1946
1947 static void
1948 roster_window_edit_accounts_cb (GtkAction *action,
1949     EmpathyRosterWindow *self)
1950 {
1951   empathy_accounts_dialog_show_application (gdk_screen_get_default (),
1952       NULL, FALSE, FALSE);
1953 }
1954
1955 static void
1956 roster_window_edit_blocked_contacts_cb (GtkAction *action,
1957     EmpathyRosterWindow *self)
1958 {
1959   GtkWidget *dialog;
1960
1961   dialog = empathy_contact_blocking_dialog_new (GTK_WINDOW (self));
1962   gtk_widget_show (dialog);
1963
1964   g_signal_connect (dialog, "response",
1965       G_CALLBACK (gtk_widget_destroy), NULL);
1966 }
1967
1968 void
1969 empathy_roster_window_show_preferences (EmpathyRosterWindow *self,
1970     const gchar *tab)
1971 {
1972   if (self->priv->preferences == NULL)
1973     {
1974       self->priv->preferences = empathy_preferences_new (GTK_WINDOW (self),
1975                                                    self->priv->shell_running);
1976       g_object_add_weak_pointer (G_OBJECT (self->priv->preferences),
1977                (gpointer) &self->priv->preferences);
1978
1979       gtk_widget_show (self->priv->preferences);
1980     }
1981   else
1982     {
1983       gtk_window_present (GTK_WINDOW (self->priv->preferences));
1984     }
1985
1986   if (tab != NULL)
1987     empathy_preferences_show_tab (
1988       EMPATHY_PREFERENCES (self->priv->preferences), tab);
1989 }
1990
1991 static void
1992 roster_window_edit_preferences_cb (GtkAction *action,
1993     EmpathyRosterWindow *self)
1994 {
1995   empathy_roster_window_show_preferences (self, NULL);
1996 }
1997
1998 static void
1999 roster_window_help_about_cb (GtkAction *action,
2000     EmpathyRosterWindow *self)
2001 {
2002   empathy_about_dialog_new (GTK_WINDOW (self));
2003 }
2004
2005 static void
2006 roster_window_help_debug_cb (GtkAction *action,
2007     EmpathyRosterWindow *self)
2008 {
2009   empathy_launch_program (BIN_DIR, "empathy-debugger", NULL);
2010 }
2011
2012 static void
2013 roster_window_help_contents_cb (GtkAction *action,
2014     EmpathyRosterWindow *self)
2015 {
2016   empathy_url_show (GTK_WIDGET (self), "ghelp:empathy");
2017 }
2018
2019 static gboolean
2020 roster_window_throbber_button_press_event_cb (GtkWidget *throbber,
2021     GdkEventButton *event,
2022     EmpathyRosterWindow *self)
2023 {
2024   if (event->type != GDK_BUTTON_PRESS ||
2025       event->button != 1)
2026     return FALSE;
2027
2028   empathy_accounts_dialog_show_application (
2029       gtk_widget_get_screen (GTK_WIDGET (throbber)),
2030       NULL, FALSE, FALSE);
2031
2032   return FALSE;
2033 }
2034
2035 static void
2036 roster_window_account_removed_cb (TpAccountManager  *manager,
2037     TpAccount *account,
2038     EmpathyRosterWindow *self)
2039 {
2040   GList *a;
2041
2042   a = tp_account_manager_get_valid_accounts (manager);
2043
2044   gtk_action_set_sensitive (self->priv->view_history,
2045     g_list_length (a) > 0);
2046
2047   g_list_free (a);
2048
2049   /* remove errors if any */
2050   roster_window_remove_error (self, account);
2051
2052   /* remove the balance action if required */
2053   roster_window_remove_balance_action (self, account);
2054 }
2055
2056 static void
2057 account_connection_notify_cb (TpAccount *account,
2058     GParamSpec *spec,
2059     EmpathyRosterWindow *self)
2060 {
2061   TpConnection *conn;
2062
2063   conn = tp_account_get_connection (account);
2064
2065   if (conn != NULL)
2066     {
2067       roster_window_setup_balance (self, account);
2068     }
2069   else
2070     {
2071       /* remove balance action if required */
2072       roster_window_remove_balance_action (self, account);
2073     }
2074 }
2075
2076 static void
2077 add_account (EmpathyRosterWindow *self,
2078     TpAccount *account)
2079 {
2080   gulong handler_id;
2081
2082   handler_id = GPOINTER_TO_UINT (g_hash_table_lookup (
2083     self->priv->status_changed_handlers, account));
2084
2085   /* connect signal only if it was not connected yet */
2086   if (handler_id != 0)
2087     return;
2088
2089   handler_id = g_signal_connect (account, "status-changed",
2090     G_CALLBACK (roster_window_connection_changed_cb), self);
2091
2092   g_hash_table_insert (self->priv->status_changed_handlers,
2093     account, GUINT_TO_POINTER (handler_id));
2094
2095   /* roster_window_setup_balance() relies on the TpConnection to be ready on
2096    * the TpAccount so we connect this signal as well. */
2097   tp_g_signal_connect_object (account, "notify::connection",
2098       G_CALLBACK (account_connection_notify_cb), self, 0);
2099
2100   roster_window_setup_balance (self, account);
2101 }
2102
2103 static void
2104 roster_window_account_validity_changed_cb (TpAccountManager  *manager,
2105     TpAccount *account,
2106     gboolean valid,
2107     EmpathyRosterWindow *self)
2108 {
2109   if (valid)
2110     add_account (self, account);
2111   else
2112     roster_window_account_removed_cb (manager, account, self);
2113 }
2114
2115 static void
2116 roster_window_notify_show_offline_cb (GSettings *gsettings,
2117     const gchar *key,
2118     gpointer toggle_action)
2119 {
2120   gtk_toggle_action_set_active (toggle_action,
2121       g_settings_get_boolean (gsettings, key));
2122 }
2123
2124 static void
2125 roster_window_connection_items_setup (EmpathyRosterWindow *self,
2126     GtkBuilder *gui)
2127 {
2128   GList *list;
2129   GObject *action;
2130   guint i;
2131   const gchar *actions_connected[] = {
2132       "room_join_new",
2133       "room_join_favorites",
2134       "chat_new_message",
2135       "chat_new_call",
2136       "chat_search_contacts",
2137       "chat_add_contact",
2138       "edit_blocked_contacts",
2139       "edit_search_contacts"
2140   };
2141
2142   for (i = 0, list = NULL; i < G_N_ELEMENTS (actions_connected); i++)
2143     {
2144       action = gtk_builder_get_object (gui, actions_connected[i]);
2145       list = g_list_prepend (list, action);
2146     }
2147
2148   self->priv->actions_connected = list;
2149 }
2150
2151 static void
2152 account_manager_prepared_cb (GObject *source_object,
2153     GAsyncResult *result,
2154     gpointer user_data)
2155 {
2156   GList *accounts, *j;
2157   TpAccountManager *manager = TP_ACCOUNT_MANAGER (source_object);
2158   EmpathyRosterWindow *self = user_data;
2159   GError *error = NULL;
2160
2161   if (!tp_proxy_prepare_finish (manager, result, &error))
2162     {
2163       DEBUG ("Failed to prepare account manager: %s", error->message);
2164       g_error_free (error);
2165       return;
2166     }
2167
2168   accounts = tp_account_manager_get_valid_accounts (self->priv->account_manager);
2169   for (j = accounts; j != NULL; j = j->next)
2170     {
2171       TpAccount *account = TP_ACCOUNT (j->data);
2172
2173       add_account (self, account);
2174     }
2175
2176   g_signal_connect (manager, "account-validity-changed",
2177       G_CALLBACK (roster_window_account_validity_changed_cb), self);
2178
2179   roster_window_update_status (self);
2180
2181   /* Disable the "Previous Conversations" menu entry if there is no account */
2182   gtk_action_set_sensitive (self->priv->view_history,
2183       g_list_length (accounts) > 0);
2184
2185   g_list_free (accounts);
2186 }
2187
2188 void
2189 empathy_roster_window_set_shell_running (EmpathyRosterWindow *self,
2190     gboolean shell_running)
2191 {
2192   if (self->priv->shell_running == shell_running)
2193     return;
2194
2195   self->priv->shell_running = shell_running;
2196   g_object_notify (G_OBJECT (self), "shell-running");
2197 }
2198
2199 static GObject *
2200 empathy_roster_window_constructor (GType type,
2201     guint n_construct_params,
2202     GObjectConstructParam *construct_params)
2203 {
2204   static GObject *window = NULL;
2205
2206   if (window != NULL)
2207     return g_object_ref (window);
2208
2209   window = G_OBJECT_CLASS (empathy_roster_window_parent_class)->constructor (
2210     type, n_construct_params, construct_params);
2211
2212   g_object_add_weak_pointer (window, (gpointer) &window);
2213
2214   return window;
2215 }
2216
2217 static void
2218 empathy_roster_window_set_property (GObject *object,
2219     guint property_id,
2220     const GValue *value,
2221     GParamSpec *pspec)
2222 {
2223   EmpathyRosterWindow *self = EMPATHY_ROSTER_WINDOW (object);
2224
2225   switch (property_id)
2226     {
2227       case PROP_SHELL_RUNNING:
2228         self->priv->shell_running = g_value_get_boolean (value);
2229         break;
2230       default:
2231         G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
2232         break;
2233     }
2234 }
2235
2236 static void
2237 empathy_roster_window_get_property (GObject    *object,
2238     guint property_id,
2239     GValue *value,
2240     GParamSpec *pspec)
2241 {
2242   EmpathyRosterWindow *self = EMPATHY_ROSTER_WINDOW (object);
2243
2244   switch (property_id)
2245     {
2246       case PROP_SHELL_RUNNING:
2247         g_value_set_boolean (value, self->priv->shell_running);
2248         break;
2249       default:
2250         G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
2251         break;
2252     }
2253 }
2254
2255 static void
2256 empathy_roster_window_class_init (EmpathyRosterWindowClass *klass)
2257 {
2258   GObjectClass *object_class = G_OBJECT_CLASS (klass);
2259   GParamSpec *pspec;
2260
2261   object_class->finalize = empathy_roster_window_finalize;
2262   object_class->constructor = empathy_roster_window_constructor;
2263
2264   object_class->set_property = empathy_roster_window_set_property;
2265   object_class->get_property = empathy_roster_window_get_property;
2266
2267   pspec = g_param_spec_boolean ("shell-running",
2268       "Shell running",
2269       "Whether the Shell is running or not",
2270       FALSE,
2271       G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
2272   g_object_class_install_property (object_class, PROP_SHELL_RUNNING, pspec);
2273
2274   g_type_class_add_private (object_class, sizeof (EmpathyRosterWindowPriv));
2275 }
2276
2277 static void
2278 empathy_roster_window_init (EmpathyRosterWindow *self)
2279 {
2280   EmpathyIndividualManager *individual_manager;
2281   GtkBuilder *gui, *gui_mgr;
2282   GtkWidget *sw;
2283   GtkToggleAction *show_offline_widget;
2284   GtkAction *show_map_widget;
2285   GtkToolItem *item;
2286   gboolean show_offline;
2287   gchar *filename;
2288   GtkTreeModel *model;
2289   GtkWidget *search_vbox;
2290   GtkWidget *menubar;
2291
2292   self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
2293       EMPATHY_TYPE_ROSTER_WINDOW, EmpathyRosterWindowPriv);
2294
2295   self->priv->gsettings_ui = g_settings_new (EMPATHY_PREFS_UI_SCHEMA);
2296   self->priv->gsettings_contacts = g_settings_new (EMPATHY_PREFS_CONTACTS_SCHEMA);
2297
2298   self->priv->sound_mgr = empathy_sound_manager_dup_singleton ();
2299
2300   gtk_window_set_title (GTK_WINDOW (self), _("Contact List"));
2301   gtk_window_set_role (GTK_WINDOW (self), "contact_list");
2302   gtk_window_set_default_size (GTK_WINDOW (self), 225, 325);
2303
2304   /* don't finalize the widget on delete-event, just hide it */
2305   g_signal_connect (self, "delete-event",
2306     G_CALLBACK (gtk_widget_hide_on_delete), NULL);
2307
2308   /* Set up interface */
2309   filename = empathy_file_lookup ("empathy-roster-window.ui", "src");
2310   gui = empathy_builder_get_file (filename,
2311       "main_vbox", &self->priv->main_vbox,
2312       "balance_vbox", &self->priv->balance_vbox,
2313       "errors_vbox", &self->priv->errors_vbox,
2314       "auth_vbox", &self->priv->auth_vbox,
2315       "search_vbox", &search_vbox,
2316       "presence_toolbar", &self->priv->presence_toolbar,
2317       "notebook", &self->priv->notebook,
2318       "no_entry_label", &self->priv->no_entry_label,
2319       "roster_scrolledwindow", &sw,
2320       NULL);
2321   g_free (filename);
2322
2323   /* Set UI manager */
2324   filename = empathy_file_lookup ("empathy-roster-window-menubar.ui", "src");
2325   gui_mgr = empathy_builder_get_file (filename,
2326       "ui_manager", &self->priv->ui_manager,
2327       "view_show_offline", &show_offline_widget,
2328       "view_show_protocols", &self->priv->show_protocols,
2329       "view_sort_by_name", &self->priv->sort_by_name,
2330       "view_sort_by_status", &self->priv->sort_by_status,
2331       "view_normal_size_with_avatars", &self->priv->normal_with_avatars,
2332       "view_normal_size", &self->priv->normal_size,
2333       "view_compact_size", &self->priv->compact_size,
2334       "view_history", &self->priv->view_history,
2335       "view_show_map", &show_map_widget,
2336       "room_join_favorites", &self->priv->room_join_favorites,
2337       "view_balance_show_in_roster", &self->priv->view_balance_show_in_roster,
2338       "menubar", &menubar,
2339       NULL);
2340   g_free (filename);
2341
2342   /* The UI manager is living in its own .ui file as Glade doesn't support
2343    * those. The GtkMenubar has to be in this file as well to we manually add
2344    * it to the first position of the vbox. */
2345   gtk_box_pack_start (GTK_BOX (self->priv->main_vbox), menubar, FALSE, FALSE, 0);
2346   gtk_box_reorder_child (GTK_BOX (self->priv->main_vbox), menubar, 0);
2347
2348   gtk_container_add (GTK_CONTAINER (self), self->priv->main_vbox);
2349   gtk_widget_show (self->priv->main_vbox);
2350
2351   g_signal_connect (self, "key-press-event",
2352       G_CALLBACK (roster_window_key_press_event_cb), NULL);
2353
2354   empathy_builder_connect (gui_mgr, self,
2355       "chat_quit", "activate", roster_window_chat_quit_cb,
2356       "chat_new_message", "activate", roster_window_chat_new_message_cb,
2357       "chat_new_call", "activate", roster_window_chat_new_call_cb,
2358       "view_history", "activate", roster_window_view_history_cb,
2359       "room_join_new", "activate", roster_window_room_join_new_cb,
2360       "room_join_favorites", "activate", roster_window_room_join_favorites_cb,
2361       "room_manage_favorites", "activate", roster_window_room_manage_favorites_cb,
2362       "chat_add_contact", "activate", roster_window_chat_add_contact_cb,
2363       "chat_search_contacts", "activate", roster_window_chat_search_contacts_cb,
2364       "view_show_ft_manager", "activate", roster_window_view_show_ft_manager,
2365       "view_show_offline", "toggled", roster_window_view_show_offline_cb,
2366       "view_show_protocols", "toggled", roster_window_view_show_protocols_cb,
2367       "view_sort_by_name", "changed", roster_window_view_sort_contacts_cb,
2368       "view_normal_size_with_avatars", "changed", roster_window_view_contacts_list_size_cb,
2369       "view_show_map", "activate", roster_window_view_show_map_cb,
2370       "edit", "activate", roster_window_edit_cb,
2371       "edit_accounts", "activate", roster_window_edit_accounts_cb,
2372       "edit_blocked_contacts", "activate", roster_window_edit_blocked_contacts_cb,
2373       "edit_preferences", "activate", roster_window_edit_preferences_cb,
2374       "edit_search_contacts", "activate", roster_window_edit_search_contacts_cb,
2375       "help_about", "activate", roster_window_help_about_cb,
2376       "help_debug", "activate", roster_window_help_debug_cb,
2377       "help_contents", "activate", roster_window_help_contents_cb,
2378       NULL);
2379
2380   /* Set up connection related widgets. */
2381   roster_window_connection_items_setup (self, gui_mgr);
2382
2383   g_object_ref (self->priv->ui_manager);
2384   g_object_unref (gui);
2385   g_object_unref (gui_mgr);
2386
2387 #ifndef HAVE_LIBCHAMPLAIN
2388   gtk_action_set_visible (show_map_widget, FALSE);
2389 #endif
2390
2391   self->priv->account_manager = tp_account_manager_dup ();
2392
2393   tp_proxy_prepare_async (self->priv->account_manager, NULL,
2394       account_manager_prepared_cb, self);
2395
2396   self->priv->errors = g_hash_table_new_full (g_direct_hash, g_direct_equal,
2397       g_object_unref, NULL);
2398
2399   self->priv->auths = g_hash_table_new (NULL, NULL);
2400
2401   self->priv->status_changed_handlers = g_hash_table_new_full (g_direct_hash,
2402       g_direct_equal, NULL, NULL);
2403
2404   /* Set up menu */
2405   roster_window_favorite_chatroom_menu_setup (self);
2406
2407   self->priv->edit_context = gtk_ui_manager_get_widget (self->priv->ui_manager,
2408       "/menubar/edit/edit_context");
2409   self->priv->edit_context_separator = gtk_ui_manager_get_widget (
2410       self->priv->ui_manager,
2411       "/menubar/edit/edit_context_separator");
2412   gtk_widget_hide (self->priv->edit_context);
2413   gtk_widget_hide (self->priv->edit_context_separator);
2414
2415   /* Set up contact list. */
2416   empathy_status_presets_get_all ();
2417
2418   /* Set up presence chooser */
2419   self->priv->presence_chooser = empathy_presence_chooser_new ();
2420   gtk_widget_show (self->priv->presence_chooser);
2421   item = gtk_tool_item_new ();
2422   gtk_widget_show (GTK_WIDGET (item));
2423   gtk_widget_set_size_request (self->priv->presence_chooser, 10, -1);
2424   gtk_container_add (GTK_CONTAINER (item), self->priv->presence_chooser);
2425   gtk_tool_item_set_is_important (item, TRUE);
2426   gtk_tool_item_set_expand (item, TRUE);
2427   gtk_toolbar_insert (GTK_TOOLBAR (self->priv->presence_toolbar), item, -1);
2428
2429   /* Set up the throbber */
2430   self->priv->throbber = gtk_spinner_new ();
2431   gtk_widget_set_size_request (self->priv->throbber, 16, -1);
2432   gtk_widget_set_events (self->priv->throbber, GDK_BUTTON_PRESS_MASK);
2433   g_signal_connect (self->priv->throbber, "button-press-event",
2434     G_CALLBACK (roster_window_throbber_button_press_event_cb),
2435     self);
2436   gtk_widget_show (self->priv->throbber);
2437
2438   item = gtk_tool_item_new ();
2439   gtk_container_set_border_width (GTK_CONTAINER (item), 6);
2440   gtk_toolbar_insert (GTK_TOOLBAR (self->priv->presence_toolbar), item, -1);
2441   gtk_container_add (GTK_CONTAINER (item), self->priv->throbber);
2442   self->priv->throbber_tool_item = GTK_WIDGET (item);
2443
2444   /* XXX: this class is designed to live for the duration of the program,
2445    * so it's got a race condition between its signal handlers and its
2446    * finalization. The class is planned to be removed, so we won't fix
2447    * this before then. */
2448   individual_manager = empathy_individual_manager_dup_singleton ();
2449   self->priv->individual_store = EMPATHY_INDIVIDUAL_STORE (
2450       empathy_individual_store_manager_new (individual_manager));
2451   g_object_unref (individual_manager);
2452
2453   /* For the moment, we disallow Persona drops onto the roster contact list
2454    * (e.g. from things such as the EmpathyPersonaView in the linking dialogue).
2455    * No code is hooked up to do anything on a Persona drop, so allowing them
2456    * would achieve nothing except confusion. */
2457   self->priv->individual_view = empathy_individual_view_new (
2458       self->priv->individual_store,
2459       /* EmpathyIndividualViewFeatureFlags */
2460       EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_SAVE |
2461       EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_RENAME |
2462       EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_REMOVE |
2463       EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_CHANGE |
2464       EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_REMOVE |
2465       EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_DROP |
2466       EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_DRAG |
2467       EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_TOOLTIP |
2468       EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_CALL |
2469       EMPATHY_INDIVIDUAL_VIEW_FEATURE_FILE_DROP,
2470       /* EmpathyIndividualFeatureFlags  */
2471       EMPATHY_INDIVIDUAL_FEATURE_CHAT |
2472       EMPATHY_INDIVIDUAL_FEATURE_CALL |
2473       EMPATHY_INDIVIDUAL_FEATURE_EDIT |
2474       EMPATHY_INDIVIDUAL_FEATURE_INFO |
2475       EMPATHY_INDIVIDUAL_FEATURE_LINK |
2476       EMPATHY_INDIVIDUAL_FEATURE_SMS |
2477       EMPATHY_INDIVIDUAL_FEATURE_CALL_PHONE);
2478
2479   gtk_widget_show (GTK_WIDGET (self->priv->individual_view));
2480   gtk_container_add (GTK_CONTAINER (sw),
2481       GTK_WIDGET (self->priv->individual_view));
2482   g_signal_connect (self->priv->individual_view, "row-activated",
2483      G_CALLBACK (roster_window_row_activated_cb), self);
2484
2485   /* Set up search bar */
2486   self->priv->search_bar = empathy_live_search_new (
2487       GTK_WIDGET (self->priv->individual_view));
2488   empathy_individual_view_set_live_search (self->priv->individual_view,
2489       EMPATHY_LIVE_SEARCH (self->priv->search_bar));
2490   gtk_box_pack_start (GTK_BOX (search_vbox), self->priv->search_bar,
2491       FALSE, TRUE, 0);
2492
2493   g_signal_connect_swapped (self, "map",
2494       G_CALLBACK (gtk_widget_grab_focus), self->priv->individual_view);
2495
2496   /* Connect to proper signals to check if contact list is empty or not */
2497   model = gtk_tree_view_get_model (GTK_TREE_VIEW (self->priv->individual_view));
2498   self->priv->empty = TRUE;
2499   g_signal_connect (model, "row-inserted",
2500       G_CALLBACK (roster_window_row_inserted_cb), self);
2501   g_signal_connect (model, "row-deleted",
2502       G_CALLBACK (roster_window_row_deleted_cb), self);
2503
2504   /* Load user-defined accelerators. */
2505   roster_window_accels_load ();
2506
2507   /* Set window size. */
2508   empathy_geometry_bind (GTK_WINDOW (self), GEOMETRY_NAME);
2509
2510   /* bind view_balance_show_in_roster */
2511   g_settings_bind (self->priv->gsettings_ui, "show-balance-in-roster",
2512       self->priv->view_balance_show_in_roster, "active",
2513       G_SETTINGS_BIND_DEFAULT);
2514   g_object_bind_property (self->priv->view_balance_show_in_roster, "active",
2515       self->priv->balance_vbox, "visible",
2516       G_BINDING_SYNC_CREATE);
2517
2518   /* Enable event handling */
2519   self->priv->call_observer = empathy_call_observer_dup_singleton ();
2520   self->priv->event_manager = empathy_event_manager_dup_singleton ();
2521
2522   g_signal_connect (self->priv->event_manager, "event-added",
2523       G_CALLBACK (roster_window_event_added_cb), self);
2524   g_signal_connect (self->priv->event_manager, "event-removed",
2525       G_CALLBACK (roster_window_event_removed_cb), self);
2526   g_signal_connect (self->priv->account_manager, "account-validity-changed",
2527       G_CALLBACK (roster_window_account_validity_changed_cb), self);
2528   g_signal_connect (self->priv->account_manager, "account-removed",
2529       G_CALLBACK (roster_window_account_removed_cb), self);
2530   g_signal_connect (self->priv->account_manager, "account-disabled",
2531       G_CALLBACK (roster_window_account_disabled_cb), self);
2532
2533   /* Show offline ? */
2534   show_offline = g_settings_get_boolean (self->priv->gsettings_ui,
2535       EMPATHY_PREFS_UI_SHOW_OFFLINE);
2536
2537   g_signal_connect (self->priv->gsettings_ui,
2538       "changed::" EMPATHY_PREFS_UI_SHOW_OFFLINE,
2539       G_CALLBACK (roster_window_notify_show_offline_cb), show_offline_widget);
2540
2541   gtk_toggle_action_set_active (show_offline_widget, show_offline);
2542
2543   /* Show protocol ? */
2544   g_signal_connect (self->priv->gsettings_ui,
2545       "changed::" EMPATHY_PREFS_UI_SHOW_PROTOCOLS,
2546       G_CALLBACK (roster_window_notify_show_protocols_cb), self);
2547
2548   roster_window_notify_show_protocols_cb (self->priv->gsettings_ui,
2549       EMPATHY_PREFS_UI_SHOW_PROTOCOLS, self);
2550
2551   /* Sort by name / by status ? */
2552   g_signal_connect (self->priv->gsettings_contacts,
2553       "changed::" EMPATHY_PREFS_CONTACTS_SORT_CRITERIUM,
2554       G_CALLBACK (roster_window_notify_sort_contact_cb), self);
2555
2556   roster_window_notify_sort_contact_cb (self->priv->gsettings_contacts,
2557       EMPATHY_PREFS_CONTACTS_SORT_CRITERIUM, self);
2558
2559   /* Contacts list size */
2560   g_signal_connect (self->priv->gsettings_ui,
2561       "changed::" EMPATHY_PREFS_UI_COMPACT_CONTACT_LIST,
2562       G_CALLBACK (roster_window_notify_contact_list_size_cb), self);
2563
2564   g_signal_connect (self->priv->gsettings_ui,
2565       "changed::" EMPATHY_PREFS_UI_SHOW_AVATARS,
2566       G_CALLBACK (roster_window_notify_contact_list_size_cb),
2567       self);
2568
2569   roster_window_notify_contact_list_size_cb (self->priv->gsettings_ui,
2570       EMPATHY_PREFS_UI_SHOW_AVATARS, self);
2571 }
2572
2573 GtkWidget *
2574 empathy_roster_window_dup (void)
2575 {
2576   return g_object_new (EMPATHY_TYPE_ROSTER_WINDOW, NULL);
2577 }