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