]> git.0d.be Git - empathy.git/blob - src/empathy-roster-window.c
roster-window: use self->priv pattern
[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_NO_MATCH
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 roster_window_row_deleted_cb (GtkTreeModel *model,
612     GtkTreePath *path,
613     EmpathyRosterWindow *self)
614 {
615   GtkTreeIter help_iter;
616
617   if (!gtk_tree_model_get_iter_first (model, &help_iter))
618     {
619       self->priv->empty = TRUE;
620
621       if (empathy_individual_view_is_searching (self->priv->individual_view))
622         {
623           gchar *tmp;
624
625           tmp = g_strdup_printf ("<b><span size='xx-large'>%s</span></b>",
626               _("No match found"));
627
628           gtk_label_set_markup (GTK_LABEL (self->priv->no_entry_label), tmp);
629           g_free (tmp);
630
631           gtk_label_set_line_wrap (GTK_LABEL (self->priv->no_entry_label),
632               TRUE);
633
634           gtk_notebook_set_current_page (GTK_NOTEBOOK (self->priv->notebook),
635               PAGE_NO_MATCH);
636         }
637     }
638 }
639
640 static void
641 roster_window_row_inserted_cb (GtkTreeModel      *model,
642     GtkTreePath *path,
643     GtkTreeIter *iter,
644     EmpathyRosterWindow *self)
645 {
646   if (self->priv->empty)
647     {
648       self->priv->empty = FALSE;
649       gtk_notebook_set_current_page (GTK_NOTEBOOK (self->priv->notebook),
650           PAGE_CONTACT_LIST);
651       gtk_widget_grab_focus (GTK_WIDGET (self->priv->individual_view));
652
653       /* The store is being filled, it will be done after an idle cb.
654        * So we can then get events. If we do that too soon, event's
655        * contact is not yet in the store and it won't get marked as
656        * having events. */
657       g_idle_add (roster_window_load_events_idle_cb, self);
658     }
659 }
660
661 static void
662 roster_window_remove_error (EmpathyRosterWindow *self,
663     TpAccount *account)
664 {
665   GtkWidget *error_widget;
666
667   error_widget = g_hash_table_lookup (self->priv->errors, account);
668   if (error_widget != NULL)
669     {
670       gtk_widget_destroy (error_widget);
671       g_hash_table_remove (self->priv->errors, account);
672     }
673 }
674
675 static void
676 roster_window_account_disabled_cb (TpAccountManager  *manager,
677     TpAccount *account,
678     EmpathyRosterWindow *self)
679 {
680   roster_window_remove_error (self, account);
681 }
682
683 static void
684 roster_window_error_retry_clicked_cb (GtkButton *button,
685     EmpathyRosterWindow *self)
686 {
687   TpAccount *account;
688
689   account = g_object_get_data (G_OBJECT (button), "account");
690   tp_account_reconnect_async (account, NULL, NULL);
691
692   roster_window_remove_error (self, account);
693 }
694
695 static void
696 roster_window_error_edit_clicked_cb (GtkButton *button,
697     EmpathyRosterWindow *self)
698 {
699   TpAccount *account;
700
701   account = g_object_get_data (G_OBJECT (button), "account");
702
703   empathy_accounts_dialog_show_application (
704       gtk_widget_get_screen (GTK_WIDGET (button)),
705       account, FALSE, FALSE);
706
707   roster_window_remove_error (self, account);
708 }
709
710 static void
711 roster_window_error_close_clicked_cb (GtkButton *button,
712     EmpathyRosterWindow *self)
713 {
714   TpAccount *account;
715
716   account = g_object_get_data (G_OBJECT (button), "account");
717   roster_window_remove_error (self, account);
718 }
719
720 static void
721 roster_window_error_upgrade_sw_clicked_cb (GtkButton *button,
722     EmpathyRosterWindow *self)
723 {
724   TpAccount *account;
725   GtkWidget *dialog;
726
727   account = g_object_get_data (G_OBJECT (button), "account");
728   roster_window_remove_error (self, account);
729
730   dialog = gtk_message_dialog_new (GTK_WINDOW (self),
731       GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR,
732       GTK_BUTTONS_OK,
733       _("Sorry, %s accounts can’t be used until your %s software is updated."),
734       tp_account_get_protocol (account),
735       tp_account_get_protocol (account));
736
737   g_signal_connect_swapped (dialog, "response",
738       G_CALLBACK (gtk_widget_destroy),
739       dialog);
740
741   gtk_widget_show (dialog);
742 }
743
744 static void
745 roster_window_upgrade_software_error (EmpathyRosterWindow *self,
746     TpAccount *account)
747 {
748   GtkWidget *info_bar;
749   GtkWidget *content_area;
750   GtkWidget *label;
751   GtkWidget *image;
752   GtkWidget *upgrade_button;
753   GtkWidget *close_button;
754   GtkWidget *action_area;
755   GtkWidget *action_grid;
756   gchar *str;
757   const gchar *icon_name;
758   const gchar *error_message;
759   gboolean user_requested;
760
761   error_message =
762     empathy_account_get_error_message (account, &user_requested);
763
764   if (user_requested)
765     return;
766
767   str = g_markup_printf_escaped ("<b>%s</b>\n%s",
768       tp_account_get_display_name (account),
769       error_message);
770
771   /* If there are other errors, remove them */
772   roster_window_remove_error (self, account);
773
774   info_bar = gtk_info_bar_new ();
775   gtk_info_bar_set_message_type (GTK_INFO_BAR (info_bar), GTK_MESSAGE_ERROR);
776
777   gtk_widget_set_no_show_all (info_bar, TRUE);
778   gtk_box_pack_start (GTK_BOX (self->priv->errors_vbox), info_bar, FALSE, TRUE, 0);
779   gtk_widget_show (info_bar);
780
781   icon_name = tp_account_get_icon_name (account);
782   image = gtk_image_new_from_icon_name (icon_name, GTK_ICON_SIZE_SMALL_TOOLBAR);
783   gtk_widget_show (image);
784
785   label = gtk_label_new (str);
786   gtk_label_set_use_markup (GTK_LABEL (label), TRUE);
787   gtk_label_set_ellipsize (GTK_LABEL (label), PANGO_ELLIPSIZE_END);
788   gtk_misc_set_alignment (GTK_MISC (label), 0, 0.5);
789   gtk_widget_show (label);
790   g_free (str);
791
792   content_area = gtk_info_bar_get_content_area (GTK_INFO_BAR (info_bar));
793   gtk_box_pack_start (GTK_BOX (content_area), image, FALSE, FALSE, 0);
794   gtk_box_pack_start (GTK_BOX (content_area), label, TRUE, TRUE, 0);
795
796   image = gtk_image_new_from_stock (GTK_STOCK_REFRESH, GTK_ICON_SIZE_BUTTON);
797   upgrade_button = gtk_button_new ();
798   gtk_button_set_image (GTK_BUTTON (upgrade_button), image);
799   gtk_widget_set_tooltip_text (upgrade_button, _("Update software..."));
800   gtk_widget_show (upgrade_button);
801
802   image = gtk_image_new_from_stock (GTK_STOCK_CLOSE, GTK_ICON_SIZE_BUTTON);
803   close_button = gtk_button_new ();
804   gtk_button_set_image (GTK_BUTTON (close_button), image);
805   gtk_widget_set_tooltip_text (close_button, _("Close"));
806   gtk_widget_show (close_button);
807
808   action_grid = gtk_grid_new ();
809   gtk_grid_set_column_spacing (GTK_GRID (action_grid), 2);
810   gtk_widget_show (action_grid);
811
812   action_area = gtk_info_bar_get_action_area (GTK_INFO_BAR (info_bar));
813   gtk_box_pack_start (GTK_BOX (action_area), action_grid, FALSE, FALSE, 0);
814
815   gtk_grid_attach (GTK_GRID (action_grid), upgrade_button, 0, 0, 1, 1);
816   gtk_grid_attach (GTK_GRID (action_grid), close_button, 1, 0, 1, 1);
817
818   g_object_set_data (G_OBJECT (info_bar), "label", label);
819   g_object_set_data_full (G_OBJECT (info_bar),
820       "account", g_object_ref (account),
821       g_object_unref);
822   g_object_set_data_full (G_OBJECT (upgrade_button),
823       "account", g_object_ref (account),
824       g_object_unref);
825   g_object_set_data_full (G_OBJECT (close_button),
826       "account", g_object_ref (account),
827       g_object_unref);
828
829   g_signal_connect (upgrade_button, "clicked",
830       G_CALLBACK (roster_window_error_upgrade_sw_clicked_cb), self);
831   g_signal_connect (close_button, "clicked",
832       G_CALLBACK (roster_window_error_close_clicked_cb), self);
833
834   gtk_widget_set_tooltip_text (self->priv->errors_vbox, error_message);
835   gtk_widget_show (self->priv->errors_vbox);
836
837   g_hash_table_insert (self->priv->errors, g_object_ref (account), info_bar);
838 }
839
840 static void
841 roster_window_error_display (EmpathyRosterWindow *self,
842     TpAccount *account)
843 {
844   GtkWidget *info_bar;
845   GtkWidget *content_area;
846   GtkWidget *label;
847   GtkWidget *image;
848   GtkWidget *retry_button;
849   GtkWidget *edit_button;
850   GtkWidget *close_button;
851   GtkWidget *action_area;
852   GtkWidget *action_grid;
853   gchar *str;
854   const gchar *icon_name;
855   const gchar *error_message;
856   gboolean user_requested;
857
858   if (!tp_strdiff (TP_ERROR_STR_SOFTWARE_UPGRADE_REQUIRED,
859        tp_account_get_detailed_error (account, NULL)))
860     {
861       roster_window_upgrade_software_error (self, account);
862       return;
863     }
864
865   error_message = empathy_account_get_error_message (account, &user_requested);
866
867   if (user_requested)
868     return;
869
870   str = g_markup_printf_escaped ("<b>%s</b>\n%s",
871       tp_account_get_display_name (account), error_message);
872
873   info_bar = g_hash_table_lookup (self->priv->errors, account);
874   if (info_bar)
875     {
876       label = g_object_get_data (G_OBJECT (info_bar), "label");
877
878       /* Just set the latest error and return */
879       gtk_label_set_markup (GTK_LABEL (label), str);
880       g_free (str);
881
882       return;
883     }
884
885   info_bar = gtk_info_bar_new ();
886   gtk_info_bar_set_message_type (GTK_INFO_BAR (info_bar), GTK_MESSAGE_ERROR);
887
888   gtk_widget_set_no_show_all (info_bar, TRUE);
889   gtk_box_pack_start (GTK_BOX (self->priv->errors_vbox), info_bar, FALSE, TRUE, 0);
890   gtk_widget_show (info_bar);
891
892   icon_name = tp_account_get_icon_name (account);
893   image = gtk_image_new_from_icon_name (icon_name, GTK_ICON_SIZE_SMALL_TOOLBAR);
894   gtk_widget_show (image);
895
896   label = gtk_label_new (str);
897   gtk_label_set_use_markup (GTK_LABEL (label), TRUE);
898   gtk_label_set_ellipsize (GTK_LABEL (label), PANGO_ELLIPSIZE_END);
899   gtk_misc_set_alignment (GTK_MISC (label), 0, 0.5);
900   gtk_widget_show (label);
901   g_free (str);
902
903   content_area = gtk_info_bar_get_content_area (GTK_INFO_BAR (info_bar));
904   gtk_box_pack_start (GTK_BOX (content_area), image, FALSE, FALSE, 0);
905   gtk_box_pack_start (GTK_BOX (content_area), label, TRUE, TRUE, 0);
906
907   image = gtk_image_new_from_stock (GTK_STOCK_REFRESH, GTK_ICON_SIZE_BUTTON);
908   retry_button = gtk_button_new ();
909   gtk_button_set_image (GTK_BUTTON (retry_button), image);
910   gtk_widget_set_tooltip_text (retry_button, _("Reconnect"));
911   gtk_widget_show (retry_button);
912
913   image = gtk_image_new_from_stock (GTK_STOCK_EDIT, GTK_ICON_SIZE_BUTTON);
914   edit_button = gtk_button_new ();
915   gtk_button_set_image (GTK_BUTTON (edit_button), image);
916   gtk_widget_set_tooltip_text (edit_button, _("Edit Account"));
917   gtk_widget_show (edit_button);
918
919   image = gtk_image_new_from_stock (GTK_STOCK_CLOSE, GTK_ICON_SIZE_BUTTON);
920   close_button = gtk_button_new ();
921   gtk_button_set_image (GTK_BUTTON (close_button), image);
922   gtk_widget_set_tooltip_text (close_button, _("Close"));
923   gtk_widget_show (close_button);
924
925   action_grid = gtk_grid_new ();
926   gtk_grid_set_column_spacing (GTK_GRID (action_grid), 2);
927   gtk_widget_show (action_grid);
928
929   action_area = gtk_info_bar_get_action_area (GTK_INFO_BAR (info_bar));
930   gtk_box_pack_start (GTK_BOX (action_area), action_grid, FALSE, FALSE, 0);
931
932   gtk_grid_attach (GTK_GRID (action_grid), retry_button, 0, 0, 1, 1);
933   gtk_grid_attach (GTK_GRID (action_grid), edit_button, 1, 0, 1, 1);
934   gtk_grid_attach (GTK_GRID (action_grid), close_button, 2, 0, 1, 1);
935
936   g_object_set_data (G_OBJECT (info_bar), "label", label);
937   g_object_set_data_full (G_OBJECT (info_bar),
938       "account", g_object_ref (account),
939       g_object_unref);
940   g_object_set_data_full (G_OBJECT (edit_button),
941       "account", g_object_ref (account),
942       g_object_unref);
943   g_object_set_data_full (G_OBJECT (close_button),
944       "account", g_object_ref (account),
945       g_object_unref);
946   g_object_set_data_full (G_OBJECT (retry_button),
947       "account", g_object_ref (account),
948       g_object_unref);
949
950   g_signal_connect (edit_button, "clicked",
951       G_CALLBACK (roster_window_error_edit_clicked_cb), self);
952   g_signal_connect (close_button, "clicked",
953       G_CALLBACK (roster_window_error_close_clicked_cb), self);
954   g_signal_connect (retry_button, "clicked",
955       G_CALLBACK (roster_window_error_retry_clicked_cb), self);
956
957   gtk_widget_set_tooltip_text (self->priv->errors_vbox, error_message);
958   gtk_widget_show (self->priv->errors_vbox);
959
960   g_hash_table_insert (self->priv->errors, g_object_ref (account), info_bar);
961 }
962
963 static void
964 roster_window_update_status (EmpathyRosterWindow *self)
965 {
966   gboolean connected, connecting;
967   GList *l, *children;
968
969   connected = empathy_account_manager_get_accounts_connected (&connecting);
970
971   /* Update the spinner state */
972   if (connecting)
973     {
974       gtk_spinner_start (GTK_SPINNER (self->priv->throbber));
975       gtk_widget_show (self->priv->throbber_tool_item);
976     }
977   else
978     {
979       gtk_spinner_stop (GTK_SPINNER (self->priv->throbber));
980       gtk_widget_hide (self->priv->throbber_tool_item);
981     }
982
983   /* Update widgets sensibility */
984   for (l = self->priv->actions_connected; l; l = l->next)
985     gtk_action_set_sensitive (l->data, connected);
986
987   /* Update favourite rooms sensitivity */
988   children = gtk_container_get_children (GTK_CONTAINER (self->priv->room_menu));
989   for (l = children; l != NULL; l = l->next)
990     {
991       if (g_object_get_data (G_OBJECT (l->data), "is_favorite") != NULL)
992         gtk_widget_set_sensitive (GTK_WIDGET (l->data), connected);
993     }
994
995   g_list_free (children);
996 }
997
998 static char *
999 roster_window_account_to_action_name (TpAccount *account)
1000 {
1001   char *r;
1002
1003   /* action names can't have '/' in them, replace it with '.' */
1004   r = g_strdup (tp_account_get_path_suffix (account));
1005   r = g_strdelimit (r, "/", '.');
1006
1007   return r;
1008 }
1009
1010 static void
1011 roster_window_balance_activate_cb (GtkAction *action,
1012     EmpathyRosterWindow *self)
1013 {
1014   const char *uri;
1015
1016   uri = g_object_get_data (G_OBJECT (action), "manage-credit-uri");
1017
1018   if (!tp_str_empty (uri))
1019     {
1020       DEBUG ("Top-up credit URI: %s", uri);
1021       empathy_url_show (GTK_WIDGET (self), uri);
1022     }
1023   else
1024     {
1025       DEBUG ("unknown protocol for top-up");
1026     }
1027 }
1028
1029 static void
1030 roster_window_balance_update_balance (GtkAction *action,
1031     TpConnection *conn)
1032 {
1033   TpAccount *account = tp_connection_get_account (conn);
1034   GtkWidget *label;
1035   int amount = 0;
1036   guint scale = G_MAXINT32;
1037   const gchar *currency = "";
1038   char *money, *str;
1039
1040   if (!tp_connection_get_balance (conn, &amount, &scale, &currency))
1041     return;
1042
1043   if (amount == 0 &&
1044       scale == G_MAXINT32 &&
1045       tp_str_empty (currency))
1046     {
1047       /* unknown balance */
1048       money = g_strdup ("--");
1049     }
1050   else
1051     {
1052       char *tmp = empathy_format_currency (amount, scale, currency);
1053
1054       money = g_strdup_printf ("%s %s", currency, tmp);
1055       g_free (tmp);
1056     }
1057
1058   /* Translators: this string will be something like:
1059    * Top up My Account ($1.23)..." */
1060   str = g_strdup_printf (_("Top up %s (%s)..."),
1061     tp_account_get_display_name (account), money);
1062
1063   gtk_action_set_label (action, str);
1064   g_free (str);
1065
1066   /* update the money label in the roster */
1067   label = g_object_get_data (G_OBJECT (action), "money-label");
1068
1069   gtk_label_set_text (GTK_LABEL (label), money);
1070   g_free (money);
1071 }
1072
1073 static void
1074 roster_window_balance_changed_cb (TpConnection *conn,
1075     guint balance,
1076     guint scale,
1077     const gchar *currency,
1078     GtkAction *action)
1079 {
1080   roster_window_balance_update_balance (action, conn);
1081 }
1082
1083 static GtkAction *
1084 roster_window_setup_balance_create_action (EmpathyRosterWindow *self,
1085     TpAccount *account)
1086 {
1087   GtkAction *action;
1088   char *name, *ui;
1089   guint merge_id;
1090   GError *error = NULL;
1091
1092   /* create the action group if required */
1093   if (self->priv->balance_action_group == NULL)
1094     {
1095       self->priv->balance_action_group =
1096         gtk_action_group_new ("balance-action-group");
1097
1098       gtk_ui_manager_insert_action_group (self->priv->ui_manager,
1099         self->priv->balance_action_group, -1);
1100     }
1101
1102   /* create the action */
1103   name = roster_window_account_to_action_name (account);
1104   action = gtk_action_new (name,
1105       tp_account_get_display_name (account),
1106       _("Top up account credit"), NULL);
1107
1108   g_object_bind_property (account, "icon-name", action, "icon-name",
1109       G_BINDING_SYNC_CREATE);
1110
1111   g_signal_connect (action, "activate",
1112       G_CALLBACK (roster_window_balance_activate_cb), self);
1113
1114   gtk_action_group_add_action (self->priv->balance_action_group, action);
1115   g_object_unref (action);
1116
1117   ui = g_strdup_printf (
1118       "<ui>"
1119       " <menubar name='menubar'>"
1120       "  <menu action='view'>"
1121       "   <placeholder name='view_balance_placeholder'>"
1122       "    <menuitem action='%s'/>"
1123       "   </placeholder>"
1124       "  </menu>"
1125       " </menubar>"
1126       "</ui>",
1127       name);
1128
1129   merge_id = gtk_ui_manager_add_ui_from_string (self->priv->ui_manager,
1130       ui, -1, &error);
1131
1132   if (error != NULL)
1133     {
1134       DEBUG ("Failed to add balance UI for %s: %s",
1135         tp_account_get_display_name (account),
1136         error->message);
1137       g_error_free (error);
1138     }
1139
1140   g_object_set_data (G_OBJECT (action),
1141       "merge-id", GUINT_TO_POINTER (merge_id));
1142
1143   g_free (name);
1144   g_free (ui);
1145
1146   return action;
1147 }
1148
1149 static GtkWidget *
1150 roster_window_setup_balance_create_widget (EmpathyRosterWindow *self,
1151     GtkAction *action,
1152     TpAccount *account)
1153 {
1154   GtkWidget *hbox, *image, *label, *button;
1155
1156   hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6);
1157
1158   /* protocol icon */
1159   image = gtk_image_new ();
1160   gtk_box_pack_start (GTK_BOX (hbox), image, FALSE, TRUE, 0);
1161   g_object_bind_property (action, "icon-name", image, "icon-name",
1162       G_BINDING_SYNC_CREATE);
1163
1164   /* account name label */
1165   label = gtk_label_new ("");
1166   gtk_misc_set_alignment (GTK_MISC (label), 0.0, 0.5);
1167   gtk_box_pack_start (GTK_BOX (hbox), label, TRUE, TRUE, 0);
1168   g_object_bind_property (account, "display-name", label, "label",
1169       G_BINDING_SYNC_CREATE);
1170
1171   /* balance label */
1172   label = gtk_label_new ("");
1173   gtk_misc_set_alignment (GTK_MISC (label), 0.0, 0.5);
1174   gtk_box_pack_start (GTK_BOX (hbox), label, FALSE, TRUE, 0);
1175   g_object_set_data (G_OBJECT (action), "money-label", label);
1176
1177   /* top up button */
1178   button = gtk_button_new_with_label (_("Top Up..."));
1179   gtk_box_pack_start (GTK_BOX (hbox), button, FALSE, TRUE, 0);
1180   g_signal_connect_swapped (button, "clicked",
1181       G_CALLBACK (gtk_action_activate), action);
1182
1183   gtk_box_pack_start (GTK_BOX (self->priv->balance_vbox), hbox, FALSE, TRUE, 0);
1184   gtk_widget_show_all (hbox);
1185
1186   /* tie the lifetime of the widget to the lifetime of the action */
1187   g_object_weak_ref (G_OBJECT (action),
1188       (GWeakNotify) gtk_widget_destroy, hbox);
1189
1190   return hbox;
1191 }
1192
1193 static void
1194 roster_window_setup_balance (EmpathyRosterWindow *self,
1195     TpAccount *account)
1196 {
1197   TpConnection *conn = tp_account_get_connection (account);
1198   GtkAction *action;
1199   const gchar *uri;
1200
1201   if (conn == NULL)
1202     return;
1203
1204   if (!tp_proxy_is_prepared (conn, TP_CONNECTION_FEATURE_BALANCE))
1205     return;
1206
1207   DEBUG ("Setting up balance for acct: %s",
1208       tp_account_get_display_name (account));
1209
1210   /* create the action */
1211   action = roster_window_setup_balance_create_action (self, account);
1212
1213   if (action == NULL)
1214     return;
1215
1216   gtk_action_set_visible (self->priv->view_balance_show_in_roster, TRUE);
1217
1218   /* create the display widget */
1219   roster_window_setup_balance_create_widget (self, action, account);
1220
1221   /* check the current balance and monitor for any changes */
1222   uri = tp_connection_get_balance_uri (conn);
1223
1224   g_object_set_data_full (G_OBJECT (action), "manage-credit-uri",
1225       g_strdup (uri), g_free);
1226   gtk_action_set_sensitive (GTK_ACTION (action), !tp_str_empty (uri));
1227
1228   roster_window_balance_update_balance (GTK_ACTION (action), conn);
1229
1230   g_signal_connect (conn, "balance-changed",
1231       G_CALLBACK (roster_window_balance_changed_cb), action);
1232 }
1233
1234 static void
1235 roster_window_remove_balance_action (EmpathyRosterWindow *self,
1236     TpAccount *account)
1237 {
1238   GtkAction *action;
1239   char *name;
1240   GList *a;
1241
1242   if (self->priv->balance_action_group == NULL)
1243     return;
1244
1245   name = roster_window_account_to_action_name (account);
1246
1247   action = gtk_action_group_get_action (
1248       self->priv->balance_action_group, name);
1249
1250   if (action != NULL)
1251     {
1252       guint merge_id;
1253
1254       DEBUG ("Removing action");
1255
1256       merge_id = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (action),
1257             "merge-id"));
1258
1259       gtk_ui_manager_remove_ui (self->priv->ui_manager,
1260           merge_id);
1261       gtk_action_group_remove_action (
1262           self->priv->balance_action_group, action);
1263     }
1264
1265   g_free (name);
1266
1267   a = gtk_action_group_list_actions (
1268       self->priv->balance_action_group);
1269
1270   gtk_action_set_visible (self->priv->view_balance_show_in_roster,
1271       g_list_length (a) > 0);
1272
1273   g_list_free (a);
1274 }
1275
1276 static void
1277 roster_window_connection_changed_cb (TpAccount  *account,
1278     guint old_status,
1279     guint current,
1280     guint reason,
1281     gchar *dbus_error_name,
1282     GHashTable *details,
1283     EmpathyRosterWindow *self)
1284 {
1285   roster_window_update_status (self);
1286
1287   if (current == TP_CONNECTION_STATUS_DISCONNECTED &&
1288       reason != TP_CONNECTION_STATUS_REASON_REQUESTED)
1289     {
1290       roster_window_error_display (self, account);
1291     }
1292
1293   if (current == TP_CONNECTION_STATUS_DISCONNECTED)
1294     {
1295       empathy_sound_manager_play (self->priv->sound_mgr, GTK_WIDGET (self),
1296               EMPATHY_SOUND_ACCOUNT_DISCONNECTED);
1297
1298       /* remove balance action if required */
1299       roster_window_remove_balance_action (self, account);
1300     }
1301
1302   if (current == TP_CONNECTION_STATUS_CONNECTED)
1303     {
1304       empathy_sound_manager_play (self->priv->sound_mgr, GTK_WIDGET (self),
1305               EMPATHY_SOUND_ACCOUNT_CONNECTED);
1306
1307       /* Account connected without error, remove error message if any */
1308       roster_window_remove_error (self, account);
1309       roster_window_setup_balance (self, account);
1310     }
1311 }
1312
1313 static void
1314 roster_window_accels_load (void)
1315 {
1316   gchar *filename;
1317
1318   filename = g_build_filename (g_get_user_config_dir (),
1319       PACKAGE_NAME, ACCELS_FILENAME, NULL);
1320   if (g_file_test (filename, G_FILE_TEST_EXISTS))
1321     {
1322       DEBUG ("Loading from:'%s'", filename);
1323       gtk_accel_map_load (filename);
1324     }
1325
1326   g_free (filename);
1327 }
1328
1329 static void
1330 roster_window_accels_save (void)
1331 {
1332   gchar *dir;
1333   gchar *file_with_path;
1334
1335   dir = g_build_filename (g_get_user_config_dir (), PACKAGE_NAME, NULL);
1336   g_mkdir_with_parents (dir, S_IRUSR | S_IWUSR | S_IXUSR);
1337   file_with_path = g_build_filename (dir, ACCELS_FILENAME, NULL);
1338   g_free (dir);
1339
1340   DEBUG ("Saving to:'%s'", file_with_path);
1341   gtk_accel_map_save (file_with_path);
1342
1343   g_free (file_with_path);
1344 }
1345
1346 static void
1347 empathy_roster_window_finalize (GObject *window)
1348 {
1349   EmpathyRosterWindow *self = EMPATHY_ROSTER_WINDOW (window);
1350   GHashTableIter iter;
1351   gpointer key, value;
1352
1353   /* Save user-defined accelerators. */
1354   roster_window_accels_save ();
1355
1356   g_list_free (self->priv->actions_connected);
1357
1358   g_object_unref (self->priv->account_manager);
1359   g_object_unref (self->priv->individual_store);
1360   g_object_unref (self->priv->sound_mgr);
1361   g_hash_table_unref (self->priv->errors);
1362   g_hash_table_unref (self->priv->auths);
1363
1364   /* disconnect all handlers of status-changed signal */
1365   g_hash_table_iter_init (&iter, self->priv->status_changed_handlers);
1366   while (g_hash_table_iter_next (&iter, &key, &value))
1367     g_signal_handler_disconnect (TP_ACCOUNT (key), GPOINTER_TO_UINT (value));
1368
1369   g_hash_table_unref (self->priv->status_changed_handlers);
1370
1371   g_signal_handlers_disconnect_by_func (self->priv->event_manager,
1372       roster_window_event_added_cb, self);
1373   g_signal_handlers_disconnect_by_func (self->priv->event_manager,
1374       roster_window_event_removed_cb, self);
1375
1376   g_object_unref (self->priv->call_observer);
1377   g_object_unref (self->priv->event_manager);
1378   g_object_unref (self->priv->ui_manager);
1379   g_object_unref (self->priv->chatroom_manager);
1380
1381   g_object_unref (self->priv->gsettings_ui);
1382   g_object_unref (self->priv->gsettings_contacts);
1383
1384   G_OBJECT_CLASS (empathy_roster_window_parent_class)->finalize (window);
1385 }
1386
1387 static gboolean
1388 roster_window_key_press_event_cb  (GtkWidget *window,
1389     GdkEventKey *event,
1390     gpointer user_data)
1391 {
1392   if (event->keyval == GDK_KEY_T
1393       && event->state & GDK_SHIFT_MASK
1394       && event->state & GDK_CONTROL_MASK)
1395     empathy_chat_manager_call_undo_closed_chat ();
1396
1397   return FALSE;
1398 }
1399
1400 static void
1401 roster_window_chat_quit_cb (GtkAction *action,
1402     EmpathyRosterWindow *self)
1403 {
1404   gtk_widget_destroy (GTK_WIDGET (self));
1405 }
1406
1407 static void
1408 roster_window_view_history_cb (GtkAction *action,
1409     EmpathyRosterWindow *self)
1410 {
1411   empathy_log_window_show (NULL, NULL, FALSE, GTK_WINDOW (self));
1412 }
1413
1414 static void
1415 roster_window_chat_new_message_cb (GtkAction *action,
1416     EmpathyRosterWindow *self)
1417 {
1418   empathy_new_message_dialog_show (GTK_WINDOW (self));
1419 }
1420
1421 static void
1422 roster_window_chat_new_call_cb (GtkAction *action,
1423     EmpathyRosterWindow *self)
1424 {
1425   empathy_new_call_dialog_show (GTK_WINDOW (self));
1426 }
1427
1428 static void
1429 roster_window_chat_add_contact_cb (GtkAction *action,
1430     EmpathyRosterWindow *self)
1431 {
1432   empathy_new_individual_dialog_show (GTK_WINDOW (self));
1433 }
1434
1435 static void
1436 roster_window_chat_search_contacts_cb (GtkAction *action,
1437     EmpathyRosterWindow *self)
1438 {
1439   GtkWidget *dialog = empathy_contact_search_dialog_new (
1440       GTK_WINDOW (self));
1441
1442   gtk_widget_show (dialog);
1443 }
1444
1445 static void
1446 roster_window_view_show_ft_manager (GtkAction *action,
1447     EmpathyRosterWindow *self)
1448 {
1449   empathy_ft_manager_show ();
1450 }
1451
1452 static void
1453 roster_window_view_show_offline_cb (GtkToggleAction   *action,
1454     EmpathyRosterWindow *self)
1455 {
1456   gboolean current;
1457
1458   current = gtk_toggle_action_get_active (action);
1459   g_settings_set_boolean (self->priv->gsettings_ui,
1460       EMPATHY_PREFS_UI_SHOW_OFFLINE,
1461       current);
1462
1463   empathy_individual_view_set_show_offline (self->priv->individual_view,
1464       current);
1465 }
1466
1467 static void
1468 roster_window_notify_sort_contact_cb (GSettings         *gsettings,
1469     const gchar *key,
1470     EmpathyRosterWindow *self)
1471 {
1472   gchar *str;
1473
1474   str = g_settings_get_string (gsettings, key);
1475
1476   if (str != NULL)
1477     {
1478       GType       type;
1479       GEnumClass *enum_class;
1480       GEnumValue *enum_value;
1481
1482       type = empathy_individual_store_sort_get_type ();
1483       enum_class = G_ENUM_CLASS (g_type_class_peek (type));
1484       enum_value = g_enum_get_value_by_nick (enum_class, str);
1485       if (enum_value)
1486         {
1487           /* By changing the value of the GtkRadioAction,
1488              it emits a signal that calls roster_window_view_sort_contacts_cb
1489              which updates the contacts list */
1490           gtk_radio_action_set_current_value (self->priv->sort_by_name,
1491                       enum_value->value);
1492         }
1493       else
1494         {
1495           g_warning ("Wrong value for sort_criterium configuration : %s", str);
1496         }
1497     g_free (str);
1498   }
1499 }
1500
1501 static void
1502 roster_window_view_sort_contacts_cb (GtkRadioAction *action,
1503     GtkRadioAction *current,
1504     EmpathyRosterWindow *self)
1505 {
1506   EmpathyIndividualStoreSort value;
1507   GSList *group;
1508   GType type;
1509   GEnumClass *enum_class;
1510   GEnumValue *enum_value;
1511
1512   value = gtk_radio_action_get_current_value (action);
1513   group = gtk_radio_action_get_group (action);
1514
1515   /* Get string from index */
1516   type = empathy_individual_store_sort_get_type ();
1517   enum_class = G_ENUM_CLASS (g_type_class_peek (type));
1518   enum_value = g_enum_get_value (enum_class, g_slist_index (group, current));
1519
1520   if (!enum_value)
1521     g_warning ("No GEnumValue for EmpathyContactListSort with GtkRadioAction index:%d",
1522         g_slist_index (group, action));
1523   else
1524     g_settings_set_string (self->priv->gsettings_contacts,
1525         EMPATHY_PREFS_CONTACTS_SORT_CRITERIUM,
1526         enum_value->value_nick);
1527
1528   empathy_individual_store_set_sort_criterium (self->priv->individual_store,
1529       value);
1530 }
1531
1532 static void
1533 roster_window_view_show_protocols_cb (GtkToggleAction *action,
1534     EmpathyRosterWindow *self)
1535 {
1536   gboolean value;
1537
1538   value = gtk_toggle_action_get_active (action);
1539
1540   g_settings_set_boolean (self->priv->gsettings_ui,
1541       EMPATHY_PREFS_UI_SHOW_PROTOCOLS,
1542       value);
1543
1544   empathy_individual_store_set_show_protocols (self->priv->individual_store,
1545       value);
1546 }
1547
1548 /* Matches GtkRadioAction values set in empathy-roster-window.ui */
1549 #define CONTACT_LIST_NORMAL_SIZE_WITH_AVATARS   0
1550 #define CONTACT_LIST_NORMAL_SIZE      1
1551 #define CONTACT_LIST_COMPACT_SIZE     2
1552
1553 static void
1554 roster_window_view_contacts_list_size_cb (GtkRadioAction *action,
1555     GtkRadioAction *current,
1556     EmpathyRosterWindow *self)
1557 {
1558   GSettings *gsettings_ui;
1559   gint value;
1560
1561   value = gtk_radio_action_get_current_value (action);
1562   /* create a new GSettings, so we can delay the setting until both
1563    * values are set */
1564   gsettings_ui = g_settings_new (EMPATHY_PREFS_UI_SCHEMA);
1565
1566   DEBUG ("radio button toggled, value = %i", value);
1567
1568   g_settings_delay (gsettings_ui);
1569   g_settings_set_boolean (gsettings_ui, EMPATHY_PREFS_UI_SHOW_AVATARS,
1570       value == CONTACT_LIST_NORMAL_SIZE_WITH_AVATARS);
1571
1572   g_settings_set_boolean (gsettings_ui, EMPATHY_PREFS_UI_COMPACT_CONTACT_LIST,
1573       value == CONTACT_LIST_COMPACT_SIZE);
1574   g_settings_apply (gsettings_ui);
1575
1576   /* FIXME: these enums probably have the wrong namespace */
1577   empathy_individual_store_set_show_avatars (self->priv->individual_store,
1578       value == CONTACT_LIST_NORMAL_SIZE_WITH_AVATARS);
1579   empathy_individual_store_set_is_compact (self->priv->individual_store,
1580       value == CONTACT_LIST_COMPACT_SIZE);
1581
1582   g_object_unref (gsettings_ui);
1583 }
1584
1585 static void roster_window_notify_show_protocols_cb (GSettings *gsettings,
1586     const gchar *key,
1587     EmpathyRosterWindow *self)
1588 {
1589   gtk_toggle_action_set_active (self->priv->show_protocols,
1590       g_settings_get_boolean (gsettings, EMPATHY_PREFS_UI_SHOW_PROTOCOLS));
1591 }
1592
1593
1594 static void
1595 roster_window_notify_contact_list_size_cb (GSettings *gsettings,
1596     const gchar *key,
1597     EmpathyRosterWindow *self)
1598 {
1599   gint value;
1600
1601   if (g_settings_get_boolean (gsettings,
1602       EMPATHY_PREFS_UI_COMPACT_CONTACT_LIST))
1603     value = CONTACT_LIST_COMPACT_SIZE;
1604   else if (g_settings_get_boolean (gsettings,
1605       EMPATHY_PREFS_UI_SHOW_AVATARS))
1606     value = CONTACT_LIST_NORMAL_SIZE_WITH_AVATARS;
1607   else
1608     value = CONTACT_LIST_NORMAL_SIZE;
1609
1610   DEBUG ("setting changed, value = %i", value);
1611
1612   /* By changing the value of the GtkRadioAction,
1613      it emits a signal that calls roster_window_view_contacts_list_size_cb
1614      which updates the contacts list */
1615   gtk_radio_action_set_current_value (self->priv->normal_with_avatars, value);
1616 }
1617
1618 static void
1619 roster_window_edit_search_contacts_cb (GtkCheckMenuItem  *item,
1620     EmpathyRosterWindow *self)
1621 {
1622   empathy_individual_view_start_search (self->priv->individual_view);
1623 }
1624
1625 static void
1626 roster_window_view_show_map_cb (GtkCheckMenuItem  *item,
1627     EmpathyRosterWindow *self)
1628 {
1629 #ifdef HAVE_LIBCHAMPLAIN
1630   empathy_map_view_show ();
1631 #endif
1632 }
1633
1634 static void
1635 join_chatroom (EmpathyChatroom *chatroom,
1636     gint64 timestamp)
1637 {
1638   TpAccount *account;
1639   const gchar *room;
1640
1641   account = empathy_chatroom_get_account (chatroom);
1642   room = empathy_chatroom_get_room (chatroom);
1643
1644   DEBUG ("Requesting channel for '%s'", room);
1645   empathy_join_muc (account, room, timestamp);
1646 }
1647
1648 typedef struct
1649 {
1650   TpAccount *account;
1651   EmpathyChatroom *chatroom;
1652   gint64 timestamp;
1653   glong sig_id;
1654   guint timeout;
1655 } join_fav_account_sig_ctx;
1656
1657 static join_fav_account_sig_ctx *
1658 join_fav_account_sig_ctx_new (TpAccount *account,
1659     EmpathyChatroom *chatroom,
1660     gint64 timestamp)
1661 {
1662   join_fav_account_sig_ctx *ctx = g_slice_new0 (
1663       join_fav_account_sig_ctx);
1664
1665   ctx->account = g_object_ref (account);
1666   ctx->chatroom = g_object_ref (chatroom);
1667   ctx->timestamp = timestamp;
1668   return ctx;
1669 }
1670
1671 static void
1672 join_fav_account_sig_ctx_free (join_fav_account_sig_ctx *ctx)
1673 {
1674   g_object_unref (ctx->account);
1675   g_object_unref (ctx->chatroom);
1676   g_slice_free (join_fav_account_sig_ctx, ctx);
1677 }
1678
1679 static void
1680 account_status_changed_cb (TpAccount  *account,
1681     TpConnectionStatus old_status,
1682     TpConnectionStatus new_status,
1683     guint reason,
1684     gchar *dbus_error_name,
1685     GHashTable *details,
1686     gpointer user_data)
1687 {
1688   join_fav_account_sig_ctx *ctx = user_data;
1689
1690   switch (new_status)
1691     {
1692       case TP_CONNECTION_STATUS_DISCONNECTED:
1693         /* Don't wait any longer */
1694         goto finally;
1695         break;
1696
1697       case TP_CONNECTION_STATUS_CONNECTING:
1698         /* Wait a bit */
1699         return;
1700
1701       case TP_CONNECTION_STATUS_CONNECTED:
1702         /* We can join the room */
1703         break;
1704
1705       default:
1706         g_assert_not_reached ();
1707     }
1708
1709   join_chatroom (ctx->chatroom, ctx->timestamp);
1710
1711 finally:
1712   g_source_remove (ctx->timeout);
1713   g_signal_handler_disconnect (account, ctx->sig_id);
1714 }
1715
1716 #define JOIN_FAVORITE_TIMEOUT 5
1717
1718 static gboolean
1719 join_favorite_timeout_cb (gpointer data)
1720 {
1721   join_fav_account_sig_ctx *ctx = data;
1722
1723   /* stop waiting for joining the favorite room */
1724   g_signal_handler_disconnect (ctx->account, ctx->sig_id);
1725   return FALSE;
1726 }
1727
1728 static void
1729 roster_window_favorite_chatroom_join (EmpathyChatroom *chatroom)
1730 {
1731   TpAccount *account;
1732
1733   account = empathy_chatroom_get_account (chatroom);
1734   if (tp_account_get_connection_status (account, NULL) !=
1735                TP_CONNECTION_STATUS_CONNECTED)
1736     {
1737       join_fav_account_sig_ctx *ctx;
1738
1739       ctx = join_fav_account_sig_ctx_new (account, chatroom,
1740         empathy_get_current_action_time ());
1741
1742       ctx->sig_id = g_signal_connect_data (account, "status-changed",
1743         G_CALLBACK (account_status_changed_cb), ctx,
1744         (GClosureNotify) join_fav_account_sig_ctx_free, 0);
1745
1746       ctx->timeout = g_timeout_add_seconds (JOIN_FAVORITE_TIMEOUT,
1747         join_favorite_timeout_cb, ctx);
1748       return;
1749     }
1750
1751   join_chatroom (chatroom, empathy_get_current_action_time ());
1752 }
1753
1754 static void
1755 roster_window_favorite_chatroom_menu_activate_cb (GtkMenuItem *menu_item,
1756     EmpathyChatroom *chatroom)
1757 {
1758   roster_window_favorite_chatroom_join (chatroom);
1759 }
1760
1761 static void
1762 roster_window_favorite_chatroom_menu_add (EmpathyRosterWindow *self,
1763     EmpathyChatroom *chatroom)
1764 {
1765   GtkWidget *menu_item;
1766   const gchar *name, *account_name;
1767   gchar *label;
1768
1769   if (g_object_get_data (G_OBJECT (chatroom), "menu_item"))
1770     return;
1771
1772   name = empathy_chatroom_get_name (chatroom);
1773   account_name = tp_account_get_display_name (
1774       empathy_chatroom_get_account (chatroom));
1775   label = g_strdup_printf ("%s (%s)", name, account_name);
1776   menu_item = gtk_menu_item_new_with_label (label);
1777   g_free (label);
1778   g_object_set_data (G_OBJECT (menu_item), "is_favorite",
1779       GUINT_TO_POINTER (TRUE));
1780
1781   g_object_set_data (G_OBJECT (chatroom), "menu_item", menu_item);
1782   g_signal_connect (menu_item, "activate",
1783       G_CALLBACK (roster_window_favorite_chatroom_menu_activate_cb),
1784       chatroom);
1785
1786   gtk_menu_shell_insert (GTK_MENU_SHELL (self->priv->room_menu),
1787       menu_item, 4);
1788
1789   gtk_widget_show (menu_item);
1790 }
1791
1792 static void
1793 roster_window_favorite_chatroom_menu_added_cb (EmpathyChatroomManager *manager,
1794     EmpathyChatroom *chatroom,
1795     EmpathyRosterWindow *self)
1796 {
1797   roster_window_favorite_chatroom_menu_add (self, chatroom);
1798   gtk_widget_show (self->priv->room_separator);
1799   gtk_action_set_sensitive (self->priv->room_join_favorites, TRUE);
1800 }
1801
1802 static void
1803 roster_window_favorite_chatroom_menu_removed_cb (
1804     EmpathyChatroomManager *manager,
1805     EmpathyChatroom *chatroom,
1806     EmpathyRosterWindow *self)
1807 {
1808   GtkWidget *menu_item;
1809   GList *chatrooms;
1810
1811   menu_item = g_object_get_data (G_OBJECT (chatroom), "menu_item");
1812   g_object_set_data (G_OBJECT (chatroom), "menu_item", NULL);
1813   gtk_widget_destroy (menu_item);
1814
1815   chatrooms = empathy_chatroom_manager_get_chatrooms (self->priv->chatroom_manager,
1816       NULL);
1817   if (chatrooms)
1818     gtk_widget_show (self->priv->room_separator);
1819   else
1820     gtk_widget_hide (self->priv->room_separator);
1821
1822   gtk_action_set_sensitive (self->priv->room_join_favorites, chatrooms != NULL);
1823   g_list_free (chatrooms);
1824 }
1825
1826 static void
1827 roster_window_favorite_chatroom_menu_setup (EmpathyRosterWindow *self)
1828 {
1829   GList *chatrooms, *l;
1830   GtkWidget *room;
1831
1832   self->priv->chatroom_manager = empathy_chatroom_manager_dup_singleton (NULL);
1833   chatrooms = empathy_chatroom_manager_get_chatrooms (
1834     self->priv->chatroom_manager, NULL);
1835   room = gtk_ui_manager_get_widget (self->priv->ui_manager,
1836     "/menubar/room");
1837   self->priv->room_menu = gtk_menu_item_get_submenu (GTK_MENU_ITEM (room));
1838   self->priv->room_separator = gtk_ui_manager_get_widget (self->priv->ui_manager,
1839     "/menubar/room/room_separator");
1840
1841   for (l = chatrooms; l; l = l->next)
1842     roster_window_favorite_chatroom_menu_add (self, l->data);
1843
1844   if (!chatrooms)
1845     gtk_widget_hide (self->priv->room_separator);
1846
1847   gtk_action_set_sensitive (self->priv->room_join_favorites, chatrooms != NULL);
1848
1849   g_signal_connect (self->priv->chatroom_manager, "chatroom-added",
1850       G_CALLBACK (roster_window_favorite_chatroom_menu_added_cb),
1851       self);
1852
1853   g_signal_connect (self->priv->chatroom_manager, "chatroom-removed",
1854       G_CALLBACK (roster_window_favorite_chatroom_menu_removed_cb),
1855       self);
1856
1857   g_list_free (chatrooms);
1858 }
1859
1860 static void
1861 roster_window_room_join_new_cb (GtkAction *action,
1862     EmpathyRosterWindow *self)
1863 {
1864   empathy_new_chatroom_dialog_show (GTK_WINDOW (self));
1865 }
1866
1867 static void
1868 roster_window_room_join_favorites_cb (GtkAction *action,
1869     EmpathyRosterWindow *self)
1870 {
1871   GList *chatrooms, *l;
1872
1873   chatrooms = empathy_chatroom_manager_get_chatrooms (self->priv->chatroom_manager,
1874       NULL);
1875
1876   for (l = chatrooms; l; l = l->next)
1877     roster_window_favorite_chatroom_join (l->data);
1878
1879   g_list_free (chatrooms);
1880 }
1881
1882 static void
1883 roster_window_room_manage_favorites_cb (GtkAction *action,
1884     EmpathyRosterWindow *self)
1885 {
1886   empathy_chatrooms_window_show (GTK_WINDOW (self));
1887 }
1888
1889 static void
1890 roster_window_edit_cb (GtkAction *action,
1891     EmpathyRosterWindow *self)
1892 {
1893   GtkWidget *submenu;
1894
1895   /* FIXME: It should use the UIManager to merge the contact/group submenu */
1896   submenu = empathy_individual_view_get_individual_menu (
1897       self->priv->individual_view);
1898   if (submenu)
1899     {
1900       GtkMenuItem *item;
1901       GtkWidget   *label;
1902
1903       item = GTK_MENU_ITEM (self->priv->edit_context);
1904       label = gtk_bin_get_child (GTK_BIN (item));
1905       gtk_label_set_text (GTK_LABEL (label), _("Contact"));
1906
1907       gtk_widget_show (self->priv->edit_context);
1908       gtk_widget_show (self->priv->edit_context_separator);
1909
1910       gtk_menu_item_set_submenu (item, submenu);
1911
1912       return;
1913     }
1914
1915   submenu = empathy_individual_view_get_group_menu (self->priv->individual_view);
1916   if (submenu)
1917     {
1918       GtkMenuItem *item;
1919       GtkWidget   *label;
1920
1921       item = GTK_MENU_ITEM (self->priv->edit_context);
1922       label = gtk_bin_get_child (GTK_BIN (item));
1923       gtk_label_set_text (GTK_LABEL (label), _("Group"));
1924
1925       gtk_widget_show (self->priv->edit_context);
1926       gtk_widget_show (self->priv->edit_context_separator);
1927
1928       gtk_menu_item_set_submenu (item, submenu);
1929
1930       return;
1931     }
1932
1933   gtk_widget_hide (self->priv->edit_context);
1934   gtk_widget_hide (self->priv->edit_context_separator);
1935
1936   return;
1937 }
1938
1939 static void
1940 roster_window_edit_accounts_cb (GtkAction *action,
1941     EmpathyRosterWindow *self)
1942 {
1943   empathy_accounts_dialog_show_application (gdk_screen_get_default (),
1944       NULL, FALSE, FALSE);
1945 }
1946
1947 static void
1948 roster_window_edit_personal_information_cb (GtkAction *action,
1949     EmpathyRosterWindow *self)
1950 {
1951   empathy_contact_personal_dialog_show (GTK_WINDOW (self));
1952 }
1953
1954 static void
1955 roster_window_edit_blocked_contacts_cb (GtkAction *action,
1956     EmpathyRosterWindow *self)
1957 {
1958   GtkWidget *dialog;
1959
1960   dialog = empathy_contact_blocking_dialog_new (GTK_WINDOW (self));
1961   gtk_widget_show (dialog);
1962
1963   g_signal_connect (dialog, "response",
1964       G_CALLBACK (gtk_widget_destroy), NULL);
1965 }
1966
1967 void
1968 empathy_roster_window_show_preferences (EmpathyRosterWindow *self,
1969     const gchar *tab)
1970 {
1971   if (self->priv->preferences == NULL)
1972     {
1973       self->priv->preferences = empathy_preferences_new (GTK_WINDOW (self),
1974                                                    self->priv->shell_running);
1975       g_object_add_weak_pointer (G_OBJECT (self->priv->preferences),
1976                (gpointer) &self->priv->preferences);
1977
1978       gtk_widget_show (self->priv->preferences);
1979     }
1980   else
1981     {
1982       gtk_window_present (GTK_WINDOW (self->priv->preferences));
1983     }
1984
1985   if (tab != NULL)
1986     empathy_preferences_show_tab (
1987       EMPATHY_PREFERENCES (self->priv->preferences), tab);
1988 }
1989
1990 static void
1991 roster_window_edit_preferences_cb (GtkAction *action,
1992     EmpathyRosterWindow *self)
1993 {
1994   empathy_roster_window_show_preferences (self, NULL);
1995 }
1996
1997 static void
1998 roster_window_help_about_cb (GtkAction *action,
1999     EmpathyRosterWindow *self)
2000 {
2001   empathy_about_dialog_new (GTK_WINDOW (self));
2002 }
2003
2004 static void
2005 roster_window_help_debug_cb (GtkAction *action,
2006     EmpathyRosterWindow *self)
2007 {
2008   empathy_launch_program (BIN_DIR, "empathy-debugger", NULL);
2009 }
2010
2011 static void
2012 roster_window_help_contents_cb (GtkAction *action,
2013     EmpathyRosterWindow *self)
2014 {
2015   empathy_url_show (GTK_WIDGET (self), "ghelp:empathy");
2016 }
2017
2018 static gboolean
2019 roster_window_throbber_button_press_event_cb (GtkWidget *throbber,
2020     GdkEventButton *event,
2021     EmpathyRosterWindow *self)
2022 {
2023   if (event->type != GDK_BUTTON_PRESS ||
2024       event->button != 1)
2025     return FALSE;
2026
2027   empathy_accounts_dialog_show_application (
2028       gtk_widget_get_screen (GTK_WIDGET (throbber)),
2029       NULL, FALSE, FALSE);
2030
2031   return FALSE;
2032 }
2033
2034 static void
2035 roster_window_account_removed_cb (TpAccountManager  *manager,
2036     TpAccount *account,
2037     EmpathyRosterWindow *self)
2038 {
2039   GList *a;
2040
2041   a = tp_account_manager_get_valid_accounts (manager);
2042
2043   gtk_action_set_sensitive (self->priv->view_history,
2044     g_list_length (a) > 0);
2045
2046   g_list_free (a);
2047
2048   /* remove errors if any */
2049   roster_window_remove_error (self, account);
2050
2051   /* remove the balance action if required */
2052   roster_window_remove_balance_action (self, account);
2053 }
2054
2055 static void
2056 roster_window_account_validity_changed_cb (TpAccountManager  *manager,
2057     TpAccount *account,
2058     gboolean valid,
2059     EmpathyRosterWindow *self)
2060 {
2061   if (valid)
2062     {
2063       gulong handler_id;
2064       handler_id = GPOINTER_TO_UINT (g_hash_table_lookup (
2065         self->priv->status_changed_handlers, account));
2066
2067       /* connect signal only if it was not connected yet */
2068       if (handler_id == 0)
2069         {
2070           handler_id = g_signal_connect (account,
2071             "status-changed",
2072             G_CALLBACK (roster_window_connection_changed_cb),
2073             self);
2074           g_hash_table_insert (self->priv->status_changed_handlers,
2075             account, GUINT_TO_POINTER (handler_id));
2076         }
2077     }
2078
2079   roster_window_account_removed_cb (manager, account, self);
2080 }
2081
2082 static void
2083 roster_window_notify_show_offline_cb (GSettings *gsettings,
2084     const gchar *key,
2085     gpointer toggle_action)
2086 {
2087   gtk_toggle_action_set_active (toggle_action,
2088       g_settings_get_boolean (gsettings, key));
2089 }
2090
2091 static void
2092 roster_window_connection_items_setup (EmpathyRosterWindow *self,
2093     GtkBuilder *gui)
2094 {
2095   GList *list;
2096   GObject *action;
2097   guint i;
2098   const gchar *actions_connected[] = {
2099       "room_join_new",
2100       "room_join_favorites",
2101       "chat_new_message",
2102       "chat_new_call",
2103       "chat_search_contacts",
2104       "chat_add_contact",
2105       "edit_personal_information",
2106       "edit_blocked_contacts",
2107       "edit_search_contacts"
2108   };
2109
2110   for (i = 0, list = NULL; i < G_N_ELEMENTS (actions_connected); i++)
2111     {
2112       action = gtk_builder_get_object (gui, actions_connected[i]);
2113       list = g_list_prepend (list, action);
2114     }
2115
2116   self->priv->actions_connected = list;
2117 }
2118
2119 static void
2120 account_manager_prepared_cb (GObject *source_object,
2121     GAsyncResult *result,
2122     gpointer user_data)
2123 {
2124   GList *accounts, *j;
2125   TpAccountManager *manager = TP_ACCOUNT_MANAGER (source_object);
2126   EmpathyRosterWindow *self = user_data;
2127   GError *error = NULL;
2128
2129   if (!tp_proxy_prepare_finish (manager, result, &error))
2130     {
2131       DEBUG ("Failed to prepare account manager: %s", error->message);
2132       g_error_free (error);
2133       return;
2134     }
2135
2136   accounts = tp_account_manager_get_valid_accounts (self->priv->account_manager);
2137   for (j = accounts; j != NULL; j = j->next)
2138     {
2139       TpAccount *account = TP_ACCOUNT (j->data);
2140       gulong handler_id;
2141
2142       handler_id = g_signal_connect (account, "status-changed",
2143           G_CALLBACK (roster_window_connection_changed_cb),
2144           self);
2145       g_hash_table_insert (self->priv->status_changed_handlers,
2146           account, GUINT_TO_POINTER (handler_id));
2147
2148       roster_window_setup_balance (self, account);
2149     }
2150
2151   g_signal_connect (manager, "account-validity-changed",
2152       G_CALLBACK (roster_window_account_validity_changed_cb), self);
2153
2154   roster_window_update_status (self);
2155
2156   /* Disable the "Previous Conversations" menu entry if there is no account */
2157   gtk_action_set_sensitive (self->priv->view_history,
2158       g_list_length (accounts) > 0);
2159
2160   g_list_free (accounts);
2161 }
2162
2163 void
2164 empathy_roster_window_set_shell_running (EmpathyRosterWindow *self,
2165     gboolean shell_running)
2166 {
2167   if (self->priv->shell_running == shell_running)
2168     return;
2169
2170   self->priv->shell_running = shell_running;
2171   g_object_notify (G_OBJECT (self), "shell-running");
2172 }
2173
2174 static GObject *
2175 empathy_roster_window_constructor (GType type,
2176     guint n_construct_params,
2177     GObjectConstructParam *construct_params)
2178 {
2179   static GObject *window = NULL;
2180
2181   if (window != NULL)
2182     return g_object_ref (window);
2183
2184   window = G_OBJECT_CLASS (empathy_roster_window_parent_class)->constructor (
2185     type, n_construct_params, construct_params);
2186
2187   g_object_add_weak_pointer (window, (gpointer) &window);
2188
2189   return window;
2190 }
2191
2192 static void
2193 empathy_roster_window_set_property (GObject *object,
2194     guint property_id,
2195     const GValue *value,
2196     GParamSpec *pspec)
2197 {
2198   EmpathyRosterWindow *self = EMPATHY_ROSTER_WINDOW (object);
2199
2200   switch (property_id)
2201     {
2202       case PROP_SHELL_RUNNING:
2203         self->priv->shell_running = g_value_get_boolean (value);
2204         break;
2205       default:
2206         G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
2207         break;
2208     }
2209 }
2210
2211 static void
2212 empathy_roster_window_get_property (GObject    *object,
2213     guint property_id,
2214     GValue *value,
2215     GParamSpec *pspec)
2216 {
2217   EmpathyRosterWindow *self = EMPATHY_ROSTER_WINDOW (object);
2218
2219   switch (property_id)
2220     {
2221       case PROP_SHELL_RUNNING:
2222         g_value_set_boolean (value, self->priv->shell_running);
2223         break;
2224       default:
2225         G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
2226         break;
2227     }
2228 }
2229
2230 static void
2231 empathy_roster_window_class_init (EmpathyRosterWindowClass *klass)
2232 {
2233   GObjectClass *object_class = G_OBJECT_CLASS (klass);
2234   GParamSpec *pspec;
2235
2236   object_class->finalize = empathy_roster_window_finalize;
2237   object_class->constructor = empathy_roster_window_constructor;
2238
2239   object_class->set_property = empathy_roster_window_set_property;
2240   object_class->get_property = empathy_roster_window_get_property;
2241
2242   pspec = g_param_spec_boolean ("shell-running",
2243       "Shell running",
2244       "Whether the Shell is running or not",
2245       FALSE,
2246       G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
2247   g_object_class_install_property (object_class, PROP_SHELL_RUNNING, pspec);
2248
2249   g_type_class_add_private (object_class, sizeof (EmpathyRosterWindowPriv));
2250 }
2251
2252 static void
2253 empathy_roster_window_init (EmpathyRosterWindow *self)
2254 {
2255   EmpathyIndividualManager *individual_manager;
2256   GtkBuilder *gui, *gui_mgr;
2257   GtkWidget *sw;
2258   GtkToggleAction *show_offline_widget;
2259   GtkAction *show_map_widget;
2260   GtkToolItem *item;
2261   gboolean show_offline;
2262   gchar *filename;
2263   GtkTreeModel *model;
2264   GtkWidget *search_vbox;
2265   GtkWidget *menubar;
2266
2267   self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
2268       EMPATHY_TYPE_ROSTER_WINDOW, EmpathyRosterWindowPriv);
2269
2270   self->priv->gsettings_ui = g_settings_new (EMPATHY_PREFS_UI_SCHEMA);
2271   self->priv->gsettings_contacts = g_settings_new (EMPATHY_PREFS_CONTACTS_SCHEMA);
2272
2273   self->priv->sound_mgr = empathy_sound_manager_dup_singleton ();
2274
2275   gtk_window_set_title (GTK_WINDOW (self), _("Contact List"));
2276   gtk_window_set_role (GTK_WINDOW (self), "contact_list");
2277   gtk_window_set_default_size (GTK_WINDOW (self), 225, 325);
2278
2279   /* don't finalize the widget on delete-event, just hide it */
2280   g_signal_connect (self, "delete-event",
2281     G_CALLBACK (gtk_widget_hide_on_delete), NULL);
2282
2283   /* Set up interface */
2284   filename = empathy_file_lookup ("empathy-roster-window.ui", "src");
2285   gui = empathy_builder_get_file (filename,
2286       "main_vbox", &self->priv->main_vbox,
2287       "balance_vbox", &self->priv->balance_vbox,
2288       "errors_vbox", &self->priv->errors_vbox,
2289       "auth_vbox", &self->priv->auth_vbox,
2290       "search_vbox", &search_vbox,
2291       "presence_toolbar", &self->priv->presence_toolbar,
2292       "notebook", &self->priv->notebook,
2293       "no_entry_label", &self->priv->no_entry_label,
2294       "roster_scrolledwindow", &sw,
2295       NULL);
2296   g_free (filename);
2297
2298   /* Set UI manager */
2299   filename = empathy_file_lookup ("empathy-roster-window-menubar.ui", "src");
2300   gui_mgr = empathy_builder_get_file (filename,
2301       "ui_manager", &self->priv->ui_manager,
2302       "view_show_offline", &show_offline_widget,
2303       "view_show_protocols", &self->priv->show_protocols,
2304       "view_sort_by_name", &self->priv->sort_by_name,
2305       "view_sort_by_status", &self->priv->sort_by_status,
2306       "view_normal_size_with_avatars", &self->priv->normal_with_avatars,
2307       "view_normal_size", &self->priv->normal_size,
2308       "view_compact_size", &self->priv->compact_size,
2309       "view_history", &self->priv->view_history,
2310       "view_show_map", &show_map_widget,
2311       "room_join_favorites", &self->priv->room_join_favorites,
2312       "view_balance_show_in_roster", &self->priv->view_balance_show_in_roster,
2313       "menubar", &menubar,
2314       NULL);
2315   g_free (filename);
2316
2317   /* The UI manager is living in its own .ui file as Glade doesn't support
2318    * those. The GtkMenubar has to be in this file as well to we manually add
2319    * it to the first position of the vbox. */
2320   gtk_box_pack_start (GTK_BOX (self->priv->main_vbox), menubar, FALSE, FALSE, 0);
2321   gtk_box_reorder_child (GTK_BOX (self->priv->main_vbox), menubar, 0);
2322
2323   gtk_container_add (GTK_CONTAINER (self), self->priv->main_vbox);
2324   gtk_widget_show (self->priv->main_vbox);
2325
2326   g_signal_connect (self, "key-press-event",
2327       G_CALLBACK (roster_window_key_press_event_cb), NULL);
2328
2329   empathy_builder_connect (gui_mgr, self,
2330       "chat_quit", "activate", roster_window_chat_quit_cb,
2331       "chat_new_message", "activate", roster_window_chat_new_message_cb,
2332       "chat_new_call", "activate", roster_window_chat_new_call_cb,
2333       "view_history", "activate", roster_window_view_history_cb,
2334       "room_join_new", "activate", roster_window_room_join_new_cb,
2335       "room_join_favorites", "activate", roster_window_room_join_favorites_cb,
2336       "room_manage_favorites", "activate", roster_window_room_manage_favorites_cb,
2337       "chat_add_contact", "activate", roster_window_chat_add_contact_cb,
2338       "chat_search_contacts", "activate", roster_window_chat_search_contacts_cb,
2339       "view_show_ft_manager", "activate", roster_window_view_show_ft_manager,
2340       "view_show_offline", "toggled", roster_window_view_show_offline_cb,
2341       "view_show_protocols", "toggled", roster_window_view_show_protocols_cb,
2342       "view_sort_by_name", "changed", roster_window_view_sort_contacts_cb,
2343       "view_normal_size_with_avatars", "changed", roster_window_view_contacts_list_size_cb,
2344       "view_show_map", "activate", roster_window_view_show_map_cb,
2345       "edit", "activate", roster_window_edit_cb,
2346       "edit_accounts", "activate", roster_window_edit_accounts_cb,
2347       "edit_personal_information", "activate", roster_window_edit_personal_information_cb,
2348       "edit_blocked_contacts", "activate", roster_window_edit_blocked_contacts_cb,
2349       "edit_preferences", "activate", roster_window_edit_preferences_cb,
2350       "edit_search_contacts", "activate", roster_window_edit_search_contacts_cb,
2351       "help_about", "activate", roster_window_help_about_cb,
2352       "help_debug", "activate", roster_window_help_debug_cb,
2353       "help_contents", "activate", roster_window_help_contents_cb,
2354       NULL);
2355
2356   /* Set up connection related widgets. */
2357   roster_window_connection_items_setup (self, gui_mgr);
2358
2359   g_object_ref (self->priv->ui_manager);
2360   g_object_unref (gui);
2361   g_object_unref (gui_mgr);
2362
2363 #ifndef HAVE_LIBCHAMPLAIN
2364   gtk_action_set_visible (show_map_widget, FALSE);
2365 #endif
2366
2367   self->priv->account_manager = tp_account_manager_dup ();
2368
2369   tp_proxy_prepare_async (self->priv->account_manager, NULL,
2370       account_manager_prepared_cb, self);
2371
2372   self->priv->errors = g_hash_table_new_full (g_direct_hash, g_direct_equal,
2373       g_object_unref, NULL);
2374
2375   self->priv->auths = g_hash_table_new (NULL, NULL);
2376
2377   self->priv->status_changed_handlers = g_hash_table_new_full (g_direct_hash,
2378       g_direct_equal, NULL, NULL);
2379
2380   /* Set up menu */
2381   roster_window_favorite_chatroom_menu_setup (self);
2382
2383   self->priv->edit_context = gtk_ui_manager_get_widget (self->priv->ui_manager,
2384       "/menubar/edit/edit_context");
2385   self->priv->edit_context_separator = gtk_ui_manager_get_widget (
2386       self->priv->ui_manager,
2387       "/menubar/edit/edit_context_separator");
2388   gtk_widget_hide (self->priv->edit_context);
2389   gtk_widget_hide (self->priv->edit_context_separator);
2390
2391   /* Set up contact list. */
2392   empathy_status_presets_get_all ();
2393
2394   /* Set up presence chooser */
2395   self->priv->presence_chooser = empathy_presence_chooser_new ();
2396   gtk_widget_show (self->priv->presence_chooser);
2397   item = gtk_tool_item_new ();
2398   gtk_widget_show (GTK_WIDGET (item));
2399   gtk_widget_set_size_request (self->priv->presence_chooser, 10, -1);
2400   gtk_container_add (GTK_CONTAINER (item), self->priv->presence_chooser);
2401   gtk_tool_item_set_is_important (item, TRUE);
2402   gtk_tool_item_set_expand (item, TRUE);
2403   gtk_toolbar_insert (GTK_TOOLBAR (self->priv->presence_toolbar), item, -1);
2404
2405   /* Set up the throbber */
2406   self->priv->throbber = gtk_spinner_new ();
2407   gtk_widget_set_size_request (self->priv->throbber, 16, -1);
2408   gtk_widget_set_events (self->priv->throbber, GDK_BUTTON_PRESS_MASK);
2409   g_signal_connect (self->priv->throbber, "button-press-event",
2410     G_CALLBACK (roster_window_throbber_button_press_event_cb),
2411     self);
2412   gtk_widget_show (self->priv->throbber);
2413
2414   item = gtk_tool_item_new ();
2415   gtk_container_set_border_width (GTK_CONTAINER (item), 6);
2416   gtk_toolbar_insert (GTK_TOOLBAR (self->priv->presence_toolbar), item, -1);
2417   gtk_container_add (GTK_CONTAINER (item), self->priv->throbber);
2418   self->priv->throbber_tool_item = GTK_WIDGET (item);
2419
2420   /* XXX: this class is designed to live for the duration of the program,
2421    * so it's got a race condition between its signal handlers and its
2422    * finalization. The class is planned to be removed, so we won't fix
2423    * this before then. */
2424   individual_manager = empathy_individual_manager_dup_singleton ();
2425   self->priv->individual_store = EMPATHY_INDIVIDUAL_STORE (
2426       empathy_individual_store_manager_new (individual_manager));
2427   g_object_unref (individual_manager);
2428
2429   /* For the moment, we disallow Persona drops onto the roster contact list
2430    * (e.g. from things such as the EmpathyPersonaView in the linking dialogue).
2431    * No code is hooked up to do anything on a Persona drop, so allowing them
2432    * would achieve nothing except confusion. */
2433   self->priv->individual_view = empathy_individual_view_new (
2434       self->priv->individual_store,
2435       /* EmpathyIndividualViewFeatureFlags */
2436       EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_SAVE |
2437       EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_RENAME |
2438       EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_REMOVE |
2439       EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_CHANGE |
2440       EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_REMOVE |
2441       EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_DROP |
2442       EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_DRAG |
2443       EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_TOOLTIP |
2444       EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_CALL |
2445       EMPATHY_INDIVIDUAL_VIEW_FEATURE_FILE_DROP,
2446       /* EmpathyIndividualFeatureFlags  */
2447       EMPATHY_INDIVIDUAL_FEATURE_CHAT |
2448       EMPATHY_INDIVIDUAL_FEATURE_CALL |
2449       EMPATHY_INDIVIDUAL_FEATURE_EDIT |
2450       EMPATHY_INDIVIDUAL_FEATURE_INFO |
2451       EMPATHY_INDIVIDUAL_FEATURE_LINK |
2452       EMPATHY_INDIVIDUAL_FEATURE_SMS |
2453       EMPATHY_INDIVIDUAL_FEATURE_CALL_PHONE);
2454
2455   gtk_widget_show (GTK_WIDGET (self->priv->individual_view));
2456   gtk_container_add (GTK_CONTAINER (sw),
2457       GTK_WIDGET (self->priv->individual_view));
2458   g_signal_connect (self->priv->individual_view, "row-activated",
2459      G_CALLBACK (roster_window_row_activated_cb), self);
2460
2461   /* Set up search bar */
2462   self->priv->search_bar = empathy_live_search_new (
2463       GTK_WIDGET (self->priv->individual_view));
2464   empathy_individual_view_set_live_search (self->priv->individual_view,
2465       EMPATHY_LIVE_SEARCH (self->priv->search_bar));
2466   gtk_box_pack_start (GTK_BOX (search_vbox), self->priv->search_bar,
2467       FALSE, TRUE, 0);
2468
2469   g_signal_connect_swapped (self, "map",
2470       G_CALLBACK (gtk_widget_grab_focus), self->priv->individual_view);
2471
2472   /* Connect to proper signals to check if contact list is empty or not */
2473   model = gtk_tree_view_get_model (GTK_TREE_VIEW (self->priv->individual_view));
2474   self->priv->empty = TRUE;
2475   g_signal_connect (model, "row-inserted",
2476       G_CALLBACK (roster_window_row_inserted_cb), self);
2477   g_signal_connect (model, "row-deleted",
2478       G_CALLBACK (roster_window_row_deleted_cb), self);
2479
2480   /* Load user-defined accelerators. */
2481   roster_window_accels_load ();
2482
2483   /* Set window size. */
2484   empathy_geometry_bind (GTK_WINDOW (self), GEOMETRY_NAME);
2485
2486   /* bind view_balance_show_in_roster */
2487   g_settings_bind (self->priv->gsettings_ui, "show-balance-in-roster",
2488       self->priv->view_balance_show_in_roster, "active",
2489       G_SETTINGS_BIND_DEFAULT);
2490   g_object_bind_property (self->priv->view_balance_show_in_roster, "active",
2491       self->priv->balance_vbox, "visible",
2492       G_BINDING_SYNC_CREATE);
2493
2494   /* Enable event handling */
2495   self->priv->call_observer = empathy_call_observer_dup_singleton ();
2496   self->priv->event_manager = empathy_event_manager_dup_singleton ();
2497
2498   g_signal_connect (self->priv->event_manager, "event-added",
2499       G_CALLBACK (roster_window_event_added_cb), self);
2500   g_signal_connect (self->priv->event_manager, "event-removed",
2501       G_CALLBACK (roster_window_event_removed_cb), self);
2502   g_signal_connect (self->priv->account_manager, "account-validity-changed",
2503       G_CALLBACK (roster_window_account_validity_changed_cb), self);
2504   g_signal_connect (self->priv->account_manager, "account-removed",
2505       G_CALLBACK (roster_window_account_removed_cb), self);
2506   g_signal_connect (self->priv->account_manager, "account-disabled",
2507       G_CALLBACK (roster_window_account_disabled_cb), self);
2508
2509   /* Show offline ? */
2510   show_offline = g_settings_get_boolean (self->priv->gsettings_ui,
2511       EMPATHY_PREFS_UI_SHOW_OFFLINE);
2512
2513   g_signal_connect (self->priv->gsettings_ui,
2514       "changed::" EMPATHY_PREFS_UI_SHOW_OFFLINE,
2515       G_CALLBACK (roster_window_notify_show_offline_cb), show_offline_widget);
2516
2517   gtk_toggle_action_set_active (show_offline_widget, show_offline);
2518
2519   /* Show protocol ? */
2520   g_signal_connect (self->priv->gsettings_ui,
2521       "changed::" EMPATHY_PREFS_UI_SHOW_PROTOCOLS,
2522       G_CALLBACK (roster_window_notify_show_protocols_cb), self);
2523
2524   roster_window_notify_show_protocols_cb (self->priv->gsettings_ui,
2525       EMPATHY_PREFS_UI_SHOW_PROTOCOLS, self);
2526
2527   /* Sort by name / by status ? */
2528   g_signal_connect (self->priv->gsettings_contacts,
2529       "changed::" EMPATHY_PREFS_CONTACTS_SORT_CRITERIUM,
2530       G_CALLBACK (roster_window_notify_sort_contact_cb), self);
2531
2532   roster_window_notify_sort_contact_cb (self->priv->gsettings_contacts,
2533       EMPATHY_PREFS_CONTACTS_SORT_CRITERIUM, self);
2534
2535   /* Contacts list size */
2536   g_signal_connect (self->priv->gsettings_ui,
2537       "changed::" EMPATHY_PREFS_UI_COMPACT_CONTACT_LIST,
2538       G_CALLBACK (roster_window_notify_contact_list_size_cb), self);
2539
2540   g_signal_connect (self->priv->gsettings_ui,
2541       "changed::" EMPATHY_PREFS_UI_SHOW_AVATARS,
2542       G_CALLBACK (roster_window_notify_contact_list_size_cb),
2543       self);
2544
2545   roster_window_notify_contact_list_size_cb (self->priv->gsettings_ui,
2546       EMPATHY_PREFS_UI_SHOW_AVATARS, self);
2547 }
2548
2549 GtkWidget *
2550 empathy_roster_window_dup (void)
2551 {
2552   return g_object_new (EMPATHY_TYPE_ROSTER_WINDOW, NULL);
2553 }