]> git.0d.be Git - empathy.git/blob - libempathy-gtk/empathy-roster-view.c
coding style fixes
[empathy.git] / libempathy-gtk / empathy-roster-view.c
1 #include "config.h"
2
3 #include "empathy-roster-view.h"
4
5 #include <glib/gi18n-lib.h>
6
7 #include <libempathy-gtk/empathy-roster-contact.h>
8 #include <libempathy-gtk/empathy-roster-group.h>
9 #include <libempathy-gtk/empathy-ui-utils.h>
10
11 #include <libempathy/empathy-utils.h>
12
13 G_DEFINE_TYPE (EmpathyRosterView, empathy_roster_view, EGG_TYPE_LIST_BOX)
14
15 /* Flashing delay for icons (milliseconds). */
16 #define FLASH_TIMEOUT 500
17
18 enum
19 {
20   PROP_MANAGER = 1,
21   PROP_MODEL,
22   PROP_SHOW_OFFLINE,
23   PROP_SHOW_GROUPS,
24   PROP_EMPTY,
25   N_PROPS
26 };
27
28 enum
29 {
30   SIG_INDIVIDUAL_ACTIVATED,
31   SIG_POPUP_INDIVIDUAL_MENU,
32   SIG_EVENT_ACTIVATED,
33   SIG_INDIVIDUAL_TOOLTIP,
34   LAST_SIGNAL
35 };
36
37 static guint signals[LAST_SIGNAL];
38
39 #define NO_GROUP "X-no-group"
40
41 struct _EmpathyRosterViewPriv
42 {
43   EmpathyIndividualManager *manager;
44
45   /* FolksIndividual (borrowed) -> GHashTable (
46    * (gchar * group_name) -> EmpathyRosterContact (borrowed))
47    *
48    * When not using groups, this hash just have one element mapped
49    * from the special NO_GROUP key. We could use it as a set but
50    * I prefer to stay coherent in the way this hash is managed.
51    */
52   GHashTable *roster_contacts;
53   /* (gchar *group_name) -> EmpathyRosterGroup (borrowed) */
54   GHashTable *roster_groups;
55   /* Hash of the EmpathyRosterContact currently displayed */
56   GHashTable *displayed_contacts;
57
58   guint last_event_id;
59   /* queue of (Event *). The most recent events are in the head of the queue
60    * so we always display the icon of the oldest one. */
61   GQueue *events;
62   guint flash_id;
63   gboolean display_flash_event;
64
65   gboolean show_offline;
66   gboolean show_groups;
67   gboolean empty;
68
69   EmpathyLiveSearch *search;
70
71   EmpathyRosterModel *model;
72 };
73
74 typedef struct
75 {
76   guint id;
77   FolksIndividual *individual;
78   gchar *icon;
79   gpointer user_data;
80 } Event;
81
82 static Event *
83 event_new (guint id,
84     FolksIndividual *individual,
85     const gchar *icon,
86     gpointer user_data)
87 {
88   Event *event = g_slice_new (Event);
89
90   event->id = id;
91   event->individual = g_object_ref (individual);
92   event->icon = g_strdup (icon);
93   event->user_data = user_data;
94   return event;
95 }
96
97 static void
98 event_free (gpointer data)
99 {
100   Event *event = data;
101   g_object_unref (event->individual);
102   g_free (event->icon);
103
104   g_slice_free (Event, event);
105 }
106
107 static void
108 empathy_roster_view_get_property (GObject *object,
109     guint property_id,
110     GValue *value,
111     GParamSpec *pspec)
112 {
113   EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (object);
114
115   switch (property_id)
116     {
117       case PROP_MANAGER:
118         g_value_set_object (value, self->priv->manager);
119         break;
120       case PROP_MODEL:
121         g_value_set_object (value, self->priv->model);
122         break;
123       case PROP_SHOW_OFFLINE:
124         g_value_set_boolean (value, self->priv->show_offline);
125         break;
126       case PROP_SHOW_GROUPS:
127         g_value_set_boolean (value, self->priv->show_groups);
128         break;
129       case PROP_EMPTY:
130         g_value_set_boolean (value, self->priv->empty);
131         break;
132       default:
133         G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
134         break;
135     }
136 }
137
138 static void
139 empathy_roster_view_set_property (GObject *object,
140     guint property_id,
141     const GValue *value,
142     GParamSpec *pspec)
143 {
144   EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (object);
145
146   switch (property_id)
147     {
148       case PROP_MANAGER:
149         g_assert (self->priv->manager == NULL); /* construct only */
150         self->priv->manager = g_value_dup_object (value);
151         break;
152       case PROP_MODEL:
153         g_assert (self->priv->model == NULL);
154         self->priv->model = g_value_dup_object (value);
155         break;
156       case PROP_SHOW_OFFLINE:
157         empathy_roster_view_show_offline (self, g_value_get_boolean (value));
158         break;
159       case PROP_SHOW_GROUPS:
160         empathy_roster_view_show_groups (self, g_value_get_boolean (value));
161         break;
162       default:
163         G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
164         break;
165     }
166 }
167
168 static void
169 roster_contact_changed_cb (GtkWidget *child,
170     GParamSpec *spec,
171     EmpathyRosterView *self)
172 {
173   egg_list_box_child_changed (EGG_LIST_BOX (self), child);
174 }
175
176 static gboolean
177 is_xmpp_local_contact (FolksIndividual *individual)
178 {
179   EmpathyContact *contact;
180   TpConnection *connection;
181   const gchar *protocol_name = NULL;
182   gboolean result;
183
184   contact = empathy_contact_dup_from_folks_individual (individual);
185
186   if (contact == NULL)
187     return FALSE;
188
189   connection = empathy_contact_get_connection (contact);
190   protocol_name = tp_connection_get_protocol_name (connection);
191   result = !tp_strdiff (protocol_name, "local-xmpp");
192   g_object_unref (contact);
193
194   return result;
195 }
196
197 static GtkWidget *
198 add_roster_contact (EmpathyRosterView *self,
199     FolksIndividual *individual,
200     const gchar *group)
201 {
202   GtkWidget *contact;
203
204   contact = empathy_roster_contact_new (individual, group);
205
206   /* Need to refilter if online is changed */
207   g_signal_connect (contact, "notify::online",
208       G_CALLBACK (roster_contact_changed_cb), self);
209
210   /* Need to resort if alias is changed */
211   g_signal_connect (contact, "notify::alias",
212       G_CALLBACK (roster_contact_changed_cb), self);
213
214   gtk_widget_show (contact);
215   gtk_container_add (GTK_CONTAINER (self), contact);
216
217   return contact;
218 }
219
220 static void
221 group_expanded_cb (EmpathyRosterGroup *group,
222     GParamSpec *spec,
223     EmpathyRosterView *self)
224 {
225   GList *widgets, *l;
226
227   widgets = empathy_roster_group_get_widgets (group);
228   for (l = widgets; l != NULL; l = g_list_next (l))
229     {
230       egg_list_box_child_changed (EGG_LIST_BOX (self), l->data);
231     }
232
233   g_list_free (widgets);
234 }
235
236 static EmpathyRosterGroup *
237 lookup_roster_group (EmpathyRosterView *self,
238     const gchar *group)
239 {
240   return g_hash_table_lookup (self->priv->roster_groups, group);
241 }
242
243 static EmpathyRosterGroup *
244 ensure_roster_group (EmpathyRosterView *self,
245     const gchar *group)
246 {
247   GtkWidget *roster_group;
248
249   roster_group = (GtkWidget *) lookup_roster_group (self, group);
250   if (roster_group != NULL)
251     return EMPATHY_ROSTER_GROUP (roster_group);
252
253   if (!tp_strdiff (group, EMPATHY_ROSTER_VIEW_GROUP_TOP_GROUP))
254     roster_group = empathy_roster_group_new (group, "emblem-favorite-symbolic");
255   else if (!tp_strdiff (group, EMPATHY_ROSTER_VIEW_GROUP_PEOPLE_NEARBY))
256     roster_group = empathy_roster_group_new (group, "im-local-xmpp");
257   else
258     roster_group = empathy_roster_group_new (group, NULL);
259
260   g_signal_connect (roster_group, "notify::expanded",
261       G_CALLBACK (group_expanded_cb), self);
262
263   gtk_widget_show (roster_group);
264   gtk_container_add (GTK_CONTAINER (self), roster_group);
265
266   g_hash_table_insert (self->priv->roster_groups, g_strdup (group),
267       roster_group);
268
269   return EMPATHY_ROSTER_GROUP (roster_group);
270 }
271
272 static void
273 update_group_widgets (EmpathyRosterView *self,
274     EmpathyRosterGroup *group,
275     EmpathyRosterContact *contact,
276     gboolean add)
277 {
278   guint old_count, count;
279
280   old_count = empathy_roster_group_get_widgets_count (group);
281
282   if (add)
283     count = empathy_roster_group_add_widget (group, GTK_WIDGET (contact));
284   else
285     count = empathy_roster_group_remove_widget (group, GTK_WIDGET (contact));
286
287   if (count != old_count)
288     egg_list_box_child_changed (EGG_LIST_BOX (self), GTK_WIDGET (group));
289 }
290
291 static void
292 add_to_group (EmpathyRosterView *self,
293     FolksIndividual *individual,
294     const gchar *group)
295 {
296   GtkWidget *contact;
297   GHashTable *contacts;
298   EmpathyRosterGroup *roster_group = NULL;
299
300   contacts = g_hash_table_lookup (self->priv->roster_contacts, individual);
301   if (contacts == NULL)
302     return;
303
304   if (tp_strdiff (group, NO_GROUP))
305     roster_group = ensure_roster_group (self, group);
306
307   contact = add_roster_contact (self, individual, group);
308   g_hash_table_insert (contacts, g_strdup (group), contact);
309
310   if (roster_group != NULL)
311     {
312       update_group_widgets (self, roster_group,
313           EMPATHY_ROSTER_CONTACT (contact), TRUE);
314     }
315 }
316
317 static void
318 individual_added (EmpathyRosterView *self,
319     FolksIndividual *individual)
320 {
321   GHashTable *contacts;
322
323   contacts = g_hash_table_lookup (self->priv->roster_contacts, individual);
324   if (contacts != NULL)
325     return;
326
327   contacts = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
328
329   g_hash_table_insert (self->priv->roster_contacts, individual, contacts);
330
331   if (!self->priv->show_groups)
332     {
333       add_to_group (self, individual, NO_GROUP);
334     }
335   else if (is_xmpp_local_contact (individual))
336     {
337       add_to_group (self, individual, EMPATHY_ROSTER_VIEW_GROUP_PEOPLE_NEARBY);
338     }
339   else
340     {
341       GeeSet *groups;
342       GList *tops;
343
344       tops = empathy_individual_manager_get_top_individuals (
345           self->priv->manager);
346
347       if (folks_favourite_details_get_is_favourite (
348             FOLKS_FAVOURITE_DETAILS (individual)) ||
349           g_list_index (tops, individual) != -1)
350         {
351           add_to_group (self, individual, EMPATHY_ROSTER_VIEW_GROUP_TOP_GROUP);
352         }
353
354       groups = folks_group_details_get_groups (
355           FOLKS_GROUP_DETAILS (individual));
356
357       if (gee_collection_get_size (GEE_COLLECTION (groups)) > 0)
358         {
359           GeeIterator *iter = gee_iterable_iterator (GEE_ITERABLE (groups));
360
361           while (iter != NULL && gee_iterator_next (iter))
362             {
363               gchar *group = gee_iterator_get (iter);
364
365               add_to_group (self, individual, group);
366
367               g_free (group);
368             }
369
370           g_clear_object (&iter);
371         }
372       else
373         {
374           /* No group, adds to Ungrouped */
375           add_to_group (self, individual, EMPATHY_ROSTER_VIEW_GROUP_UNGROUPED);
376         }
377     }
378 }
379
380 static void
381 set_event_icon_on_individual (EmpathyRosterView *self,
382     FolksIndividual *individual,
383     const gchar *icon)
384 {
385   GHashTable *contacts;
386   GHashTableIter iter;
387   gpointer v;
388
389   contacts = g_hash_table_lookup (self->priv->roster_contacts, individual);
390   if (contacts == NULL)
391     return;
392
393   g_hash_table_iter_init (&iter, contacts);
394   while (g_hash_table_iter_next (&iter, NULL, &v))
395     {
396       EmpathyRosterContact *contact =v;
397
398       empathy_roster_contact_set_event_icon (contact, icon);
399     }
400 }
401
402 static void
403 flash_event (Event *event,
404     EmpathyRosterView *self)
405 {
406   set_event_icon_on_individual (self, event->individual, event->icon);
407 }
408
409 static void
410 unflash_event (Event *event,
411     EmpathyRosterView *self)
412 {
413   set_event_icon_on_individual (self, event->individual, NULL);
414 }
415
416 static gboolean
417 flash_cb (gpointer data)
418 {
419   EmpathyRosterView *self = data;
420
421   if (self->priv->display_flash_event)
422     {
423       g_queue_foreach (self->priv->events, (GFunc) flash_event, self);
424       self->priv->display_flash_event = FALSE;
425     }
426   else
427     {
428       g_queue_foreach (self->priv->events, (GFunc) unflash_event, self);
429       self->priv->display_flash_event = TRUE;
430     }
431
432   return TRUE;
433 }
434
435 static void
436 start_flashing (EmpathyRosterView *self)
437 {
438   if (self->priv->flash_id != 0)
439     return;
440
441   self->priv->display_flash_event = TRUE;
442
443   self->priv->flash_id = g_timeout_add (FLASH_TIMEOUT,
444       flash_cb, self);
445 }
446
447 static void
448 stop_flashing (EmpathyRosterView *self)
449 {
450   if (self->priv->flash_id == 0)
451     return;
452
453   g_source_remove (self->priv->flash_id);
454   self->priv->flash_id = 0;
455 }
456
457 static void
458 remove_event (EmpathyRosterView *self,
459     Event *event)
460 {
461   unflash_event (event, self);
462   g_queue_remove (self->priv->events, event);
463
464   if (g_queue_get_length (self->priv->events) == 0)
465     {
466       stop_flashing (self);
467     }
468 }
469
470 static void
471 remove_all_individual_event (EmpathyRosterView *self,
472     FolksIndividual *individual)
473 {
474   GList *l;
475
476   for (l = g_queue_peek_head_link (self->priv->events); l != NULL;
477       l = g_list_next (l))
478     {
479       Event *event = l->data;
480
481       if (event->individual == individual)
482         {
483           remove_event (self, event);
484           return;
485         }
486     }
487 }
488
489 static void
490 individual_removed (EmpathyRosterView *self,
491     FolksIndividual *individual)
492 {
493   GHashTable *contacts;
494   GHashTableIter iter;
495   gpointer key, value;
496
497   contacts = g_hash_table_lookup (self->priv->roster_contacts, individual);
498   if (contacts == NULL)
499     return;
500
501   remove_all_individual_event (self, individual);
502
503   g_hash_table_iter_init (&iter, contacts);
504   while (g_hash_table_iter_next (&iter, &key, &value))
505     {
506       const gchar *group_name = key;
507       GtkWidget *contact = value;
508       EmpathyRosterGroup *group;
509
510       group = lookup_roster_group (self, group_name);
511       if (group != NULL)
512         {
513           update_group_widgets (self, group,
514               EMPATHY_ROSTER_CONTACT (contact), FALSE);
515         }
516
517       gtk_container_remove (GTK_CONTAINER (self), contact);
518     }
519
520   g_hash_table_remove (self->priv->roster_contacts, individual);
521 }
522
523 static void
524 individual_added_cb (EmpathyRosterModel *model,
525     FolksIndividual *individual,
526     EmpathyRosterView *self)
527 {
528   individual_added (self, individual);
529 }
530
531 static void
532 individual_removed_cb (EmpathyRosterModel *model,
533     FolksIndividual *individual,
534     EmpathyRosterView *self)
535 {
536   individual_removed (self, individual);
537 }
538
539 static gint
540 compare_roster_contacts_by_alias (EmpathyRosterContact *a,
541     EmpathyRosterContact *b)
542 {
543   FolksIndividual *ind_a, *ind_b;
544   const gchar *alias_a, *alias_b;
545
546   ind_a = empathy_roster_contact_get_individual (a);
547   ind_b = empathy_roster_contact_get_individual (b);
548
549   alias_a = folks_alias_details_get_alias (FOLKS_ALIAS_DETAILS (ind_a));
550   alias_b = folks_alias_details_get_alias (FOLKS_ALIAS_DETAILS (ind_b));
551
552   return g_ascii_strcasecmp (alias_a, alias_b);
553 }
554
555 static gboolean
556 contact_is_favourite (EmpathyRosterContact *contact)
557 {
558   FolksIndividual *individual;
559
560   individual = empathy_roster_contact_get_individual (contact);
561
562   return folks_favourite_details_get_is_favourite (
563       FOLKS_FAVOURITE_DETAILS (individual));
564 }
565
566 static gboolean
567 contact_in_top (EmpathyRosterView *self,
568     EmpathyRosterContact *contact)
569 {
570   FolksIndividual *individual;
571   GList *tops;
572
573   if (contact_is_favourite (contact))
574     return TRUE;
575
576   individual = empathy_roster_contact_get_individual (contact);
577
578   tops = empathy_individual_manager_get_top_individuals (self->priv->manager);
579
580   if (g_list_index (tops, individual) != -1)
581     return TRUE;
582
583   return FALSE;
584 }
585
586 static gint
587 compare_roster_contacts_no_group (EmpathyRosterView *self,
588     EmpathyRosterContact *a,
589     EmpathyRosterContact *b)
590 {
591   gboolean top_a, top_b;
592
593   top_a = contact_in_top (self, a);
594   top_b = contact_in_top (self, b);
595
596   if (top_a == top_b)
597     /* Both contacts are in the top of the roster (or not). Sort them
598      * alphabetically */
599     return compare_roster_contacts_by_alias (a, b);
600   else if (top_a)
601     return -1;
602   else
603     return 1;
604 }
605
606 static gint
607 compare_group_names (const gchar *group_a,
608     const gchar *group_b)
609 {
610   if (!tp_strdiff (group_a, EMPATHY_ROSTER_VIEW_GROUP_TOP_GROUP))
611     return -1;
612
613   if (!tp_strdiff (group_b, EMPATHY_ROSTER_VIEW_GROUP_TOP_GROUP))
614     return 1;
615
616   if (!tp_strdiff (group_a, EMPATHY_ROSTER_VIEW_GROUP_UNGROUPED))
617     return 1;
618   else if (!tp_strdiff (group_b, EMPATHY_ROSTER_VIEW_GROUP_UNGROUPED))
619     return -1;
620
621   return g_ascii_strcasecmp (group_a, group_b);
622 }
623
624 static gint
625 compare_roster_contacts_with_groups (EmpathyRosterView *self,
626     EmpathyRosterContact *a,
627     EmpathyRosterContact *b)
628 {
629   const gchar *group_a, *group_b;
630
631   group_a = empathy_roster_contact_get_group (a);
632   group_b = empathy_roster_contact_get_group (b);
633
634   if (!tp_strdiff (group_a, group_b))
635     /* Same group, compare the contacts */
636     return compare_roster_contacts_by_alias (a, b);
637
638   /* Sort by group */
639   return compare_group_names (group_a, group_b);
640 }
641
642 static gint
643 compare_roster_contacts (EmpathyRosterView *self,
644     EmpathyRosterContact *a,
645     EmpathyRosterContact *b)
646 {
647   if (!self->priv->show_groups)
648     return compare_roster_contacts_no_group (self, a, b);
649   else
650     return compare_roster_contacts_with_groups (self, a, b);
651 }
652
653 static gint
654 compare_roster_groups (EmpathyRosterGroup *a,
655     EmpathyRosterGroup *b)
656 {
657   const gchar *name_a, *name_b;
658
659   name_a = empathy_roster_group_get_name (a);
660   name_b = empathy_roster_group_get_name (b);
661
662   return compare_group_names (name_a, name_b);
663 }
664
665 static gint
666 compare_contact_group (EmpathyRosterContact *contact,
667     EmpathyRosterGroup *group)
668 {
669   const char *contact_group, *group_name;
670
671   contact_group = empathy_roster_contact_get_group (contact);
672   group_name = empathy_roster_group_get_name (group);
673
674   if (!tp_strdiff (contact_group, group_name))
675     /* @contact is in @group, @group has to be displayed first */
676     return 1;
677
678   /* @contact is in a different group, sort by group name */
679   return compare_group_names (contact_group, group_name);
680 }
681
682 static gint
683 roster_view_sort (gconstpointer a,
684     gconstpointer b,
685     gpointer user_data)
686 {
687   EmpathyRosterView *self = user_data;
688
689   if (EMPATHY_IS_ROSTER_CONTACT (a) && EMPATHY_IS_ROSTER_CONTACT (b))
690     return compare_roster_contacts (self, EMPATHY_ROSTER_CONTACT (a),
691         EMPATHY_ROSTER_CONTACT (b));
692   else if (EMPATHY_IS_ROSTER_GROUP (a) && EMPATHY_IS_ROSTER_GROUP (b))
693     return compare_roster_groups (EMPATHY_ROSTER_GROUP (a),
694         EMPATHY_ROSTER_GROUP (b));
695   else if (EMPATHY_IS_ROSTER_CONTACT (a) && EMPATHY_IS_ROSTER_GROUP (b))
696     return compare_contact_group (EMPATHY_ROSTER_CONTACT (a),
697         EMPATHY_ROSTER_GROUP (b));
698   else if (EMPATHY_IS_ROSTER_GROUP (a) && EMPATHY_IS_ROSTER_CONTACT (b))
699     return -1 * compare_contact_group (EMPATHY_ROSTER_CONTACT (b),
700         EMPATHY_ROSTER_GROUP (a));
701
702   g_return_val_if_reached (0);
703 }
704
705 static void
706 update_separator (GtkWidget **separator,
707     GtkWidget *child,
708     GtkWidget *before,
709     gpointer user_data)
710 {
711   if (before == NULL)
712     {
713       /* No separator before the first row */
714       g_clear_object (separator);
715       return;
716     }
717
718   if (*separator != NULL)
719     return;
720
721   *separator = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL);
722   g_object_ref_sink (*separator);
723 }
724
725 static gboolean
726 is_searching (EmpathyRosterView *self)
727 {
728   if (self->priv->search == NULL)
729     return FALSE;
730
731   return gtk_widget_get_visible (GTK_WIDGET (self->priv->search));
732 }
733
734 static void
735 update_empty (EmpathyRosterView *self,
736     gboolean empty)
737 {
738   if (self->priv->empty == empty)
739     return;
740
741   self->priv->empty = empty;
742   g_object_notify (G_OBJECT (self), "empty");
743 }
744
745 static void
746 add_to_displayed (EmpathyRosterView *self,
747     EmpathyRosterContact *contact)
748 {
749   FolksIndividual *individual;
750   GHashTable *contacts;
751   GHashTableIter iter;
752   gpointer k;
753
754   if (g_hash_table_lookup (self->priv->displayed_contacts, contact) != NULL)
755     return;
756
757   g_hash_table_add (self->priv->displayed_contacts, contact);
758   update_empty (self, FALSE);
759
760   /* Groups of this contact may now be displayed if we just displays the first
761    * child in this group. */
762
763   if (!self->priv->show_groups)
764     return;
765
766   individual = empathy_roster_contact_get_individual (contact);
767   contacts = g_hash_table_lookup (self->priv->roster_contacts, individual);
768   if (contacts == NULL)
769     return;
770
771   g_hash_table_iter_init (&iter, contacts);
772   while (g_hash_table_iter_next (&iter, &k, NULL))
773     {
774       const gchar *group_name = k;
775       GtkWidget *group;
776
777       group = g_hash_table_lookup (self->priv->roster_groups, group_name);
778       if (group == NULL)
779         continue;
780
781       egg_list_box_child_changed (EGG_LIST_BOX (self), group);
782     }
783 }
784
785 static void
786 remove_from_displayed (EmpathyRosterView *self,
787     EmpathyRosterContact *contact)
788 {
789   g_hash_table_remove (self->priv->displayed_contacts, contact);
790
791   if (g_hash_table_size (self->priv->displayed_contacts) == 0)
792     update_empty (self, TRUE);
793 }
794
795 /**
796  * check if @contact should be displayed according to @self's current status
797  * and without consideration for the state of @contact's groups.
798  */
799 static gboolean
800 contact_should_be_displayed (EmpathyRosterView *self,
801     EmpathyRosterContact *contact)
802 {
803   if (is_searching (self))
804     {
805       FolksIndividual *individual;
806
807       individual = empathy_roster_contact_get_individual (contact);
808
809       return empathy_individual_match_string (individual,
810           empathy_live_search_get_text (self->priv->search),
811           empathy_live_search_get_words (self->priv->search));
812     }
813
814   if (self->priv->show_offline)
815       return TRUE;
816
817   if (contact_is_favourite (contact))
818     {
819       const gchar *group_name;
820
821       if (!self->priv->show_groups)
822         /* Always display favourite contacts in non-group mode. */
823         return TRUE;
824
825       group_name = empathy_roster_contact_get_group (contact);
826
827       if (!tp_strdiff (group_name, EMPATHY_ROSTER_VIEW_GROUP_TOP_GROUP))
828         /* Always display favourite contact in group mode only in the
829          * 'top group'*/
830         return TRUE;
831     }
832
833   return empathy_roster_contact_is_online (contact);
834 }
835
836
837 static gboolean
838 filter_contact (EmpathyRosterView *self,
839     EmpathyRosterContact *contact)
840 {
841   gboolean displayed;
842
843   displayed = contact_should_be_displayed (self, contact);
844
845   if (self->priv->show_groups)
846     {
847       const gchar *group_name;
848       EmpathyRosterGroup *group;
849
850       group_name = empathy_roster_contact_get_group (contact);
851       group = lookup_roster_group (self, group_name);
852
853       if (group != NULL)
854         {
855           /* When searching, always display even if the group is closed */
856           if (!is_searching (self) &&
857               !gtk_expander_get_expanded (GTK_EXPANDER (group)))
858             displayed = FALSE;
859         }
860     }
861
862   if (displayed)
863     {
864       add_to_displayed (self, contact);
865     }
866   else
867     {
868       remove_from_displayed (self, contact);
869     }
870
871   return displayed;
872 }
873
874 static gboolean
875 filter_group (EmpathyRosterView *self,
876     EmpathyRosterGroup *group)
877 {
878   GList *widgets, *l;
879
880   /* Display the group if it contains at least one displayed contact */
881   widgets = empathy_roster_group_get_widgets (group);
882   for (l = widgets; l != NULL; l = g_list_next (l))
883     {
884       EmpathyRosterContact *contact = l->data;
885
886       if (contact_should_be_displayed (self, contact))
887         return TRUE;
888     }
889
890   return FALSE;
891 }
892
893 static gboolean
894 filter_list (GtkWidget *child,
895     gpointer user_data)
896 {
897   EmpathyRosterView *self = user_data;
898
899   if (EMPATHY_IS_ROSTER_CONTACT (child))
900     return filter_contact (self, EMPATHY_ROSTER_CONTACT (child));
901
902   else if (EMPATHY_IS_ROSTER_GROUP (child))
903     return filter_group (self, EMPATHY_ROSTER_GROUP (child));
904
905   g_return_val_if_reached (FALSE);
906 }
907
908 /* @list: GList of EmpathyRosterContact
909  *
910  * Returns: %TRUE if @list contains an EmpathyRosterContact associated with
911  * @individual */
912 static gboolean
913 individual_in_list (FolksIndividual *individual,
914     GList *list)
915 {
916   GList *l;
917
918   for (l = list; l != NULL; l = g_list_next (l))
919     {
920       EmpathyRosterContact *contact = l->data;
921
922       if (empathy_roster_contact_get_individual (contact) == individual)
923         return TRUE;
924     }
925
926   return FALSE;
927 }
928
929 static void
930 populate_view (EmpathyRosterView *self)
931 {
932   GList *individuals, *l;
933
934   individuals = empathy_roster_model_get_individuals (self->priv->model);
935   for (l = individuals; l != NULL; l = g_list_next (l))
936     {
937       FolksIndividual *individual = l->data;
938
939       individual_added (self, individual);
940     }
941
942   g_list_free (individuals);
943 }
944
945 static void
946 remove_from_group (EmpathyRosterView *self,
947     FolksIndividual *individual,
948     const gchar *group)
949 {
950   GHashTable *contacts;
951   GtkWidget *contact;
952   EmpathyRosterGroup *roster_group;
953
954   contacts = g_hash_table_lookup (self->priv->roster_contacts, individual);
955   if (contacts == NULL)
956     return;
957
958   contact = g_hash_table_lookup (contacts, group);
959   if (contact == NULL)
960     return;
961
962   g_hash_table_remove (contacts, group);
963
964   if (g_hash_table_size (contacts) == 0)
965     {
966       add_to_group (self, individual, EMPATHY_ROSTER_VIEW_GROUP_UNGROUPED);
967     }
968
969   roster_group = lookup_roster_group (self, group);
970
971   if (roster_group != NULL)
972     {
973       update_group_widgets (self, roster_group,
974           EMPATHY_ROSTER_CONTACT (contact), FALSE);
975     }
976
977   gtk_container_remove (GTK_CONTAINER (self), contact);
978 }
979
980 static void
981 update_top_contacts (EmpathyRosterView *self)
982 {
983   GList *tops, *l;
984   GList *to_add = NULL, *to_remove = NULL;
985   EmpathyRosterGroup *group;
986
987   if (!self->priv->show_groups)
988     {
989       egg_list_box_resort (EGG_LIST_BOX (self));
990       return;
991     }
992
993   tops = empathy_individual_manager_get_top_individuals (self->priv->manager);
994
995   group = g_hash_table_lookup (self->priv->roster_groups,
996       EMPATHY_ROSTER_VIEW_GROUP_TOP_GROUP);
997   if (group == NULL)
998     {
999       to_add = g_list_copy (tops);
1000     }
1001   else
1002     {
1003       GList *contacts;
1004
1005       contacts = empathy_roster_group_get_widgets (group);
1006
1007       /* Check which EmpathyRosterContact have to be removed */
1008       for (l = contacts; l != NULL; l = g_list_next (l))
1009         {
1010           EmpathyRosterContact *contact = l->data;
1011           FolksIndividual *individual;
1012
1013           if (contact_is_favourite (contact))
1014             continue;
1015
1016           individual = empathy_roster_contact_get_individual (contact);
1017
1018           if (g_list_find (tops, individual) == NULL)
1019             to_remove = g_list_prepend (to_remove, individual);
1020         }
1021
1022       /* Check which EmpathyRosterContact have to be added */
1023       for (l = tops; l != NULL; l = g_list_next (l))
1024         {
1025           FolksIndividual *individual = l->data;
1026
1027           if (!individual_in_list (individual, contacts))
1028             to_add = g_list_prepend (to_add, individual);
1029         }
1030     }
1031
1032   for (l = to_add; l != NULL; l = g_list_next (l))
1033     add_to_group (self, l->data, EMPATHY_ROSTER_VIEW_GROUP_TOP_GROUP);
1034
1035   for (l = to_remove; l != NULL; l = g_list_next (l))
1036     remove_from_group (self, l->data, EMPATHY_ROSTER_VIEW_GROUP_TOP_GROUP);
1037
1038   g_list_free (to_add);
1039   g_list_free (to_remove);
1040 }
1041
1042 static void
1043 groups_changed_cb (EmpathyIndividualManager *manager,
1044     FolksIndividual *individual,
1045     gchar *group,
1046     gboolean is_member,
1047     EmpathyRosterView *self)
1048 {
1049   if (!self->priv->show_groups)
1050     return;
1051
1052   if (is_member)
1053     {
1054       add_to_group (self, individual, group);
1055     }
1056   else
1057     {
1058       remove_from_group (self, individual, group);
1059     }
1060 }
1061
1062 static void
1063 top_individuals_changed_cb (EmpathyIndividualManager *manager,
1064     GParamSpec *spec,
1065     EmpathyRosterView *self)
1066 {
1067   update_top_contacts (self);
1068 }
1069
1070 static void
1071 favourites_changed_cb (EmpathyIndividualManager *manager,
1072     FolksIndividual *individual,
1073     gboolean favourite,
1074     EmpathyRosterView *self)
1075 {
1076   GHashTable *contacts;
1077
1078   contacts = g_hash_table_lookup (self->priv->roster_contacts, individual);
1079   if (contacts == NULL)
1080     return;
1081
1082   if (self->priv->show_groups)
1083     {
1084       if (favourite)
1085         add_to_group (self, individual, EMPATHY_ROSTER_VIEW_GROUP_TOP_GROUP);
1086       else
1087         remove_from_group (self, individual,
1088             EMPATHY_ROSTER_VIEW_GROUP_TOP_GROUP);
1089     }
1090   else
1091     {
1092       GtkWidget *contact;
1093
1094       contact = g_hash_table_lookup (contacts, NO_GROUP);
1095       if (contact == NULL)
1096         return;
1097
1098       egg_list_box_child_changed (EGG_LIST_BOX (self), contact);
1099     }
1100 }
1101
1102 static void
1103 empathy_roster_view_constructed (GObject *object)
1104 {
1105   EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (object);
1106   void (*chain_up) (GObject *) =
1107       ((GObjectClass *) empathy_roster_view_parent_class)->constructed;
1108
1109   if (chain_up != NULL)
1110     chain_up (object);
1111
1112   g_assert (EMPATHY_IS_INDIVIDUAL_MANAGER (self->priv->manager));
1113   g_assert (EMPATHY_IS_ROSTER_MODEL (self->priv->model));
1114
1115   populate_view (self);
1116
1117   tp_g_signal_connect_object (self->priv->model, "individual-added",
1118       G_CALLBACK (individual_added_cb), self, 0);
1119   tp_g_signal_connect_object (self->priv->model, "individual-removed",
1120       G_CALLBACK (individual_removed_cb), self, 0);
1121   tp_g_signal_connect_object (self->priv->manager, "groups-changed",
1122       G_CALLBACK (groups_changed_cb), self, 0);
1123   tp_g_signal_connect_object (self->priv->manager, "notify::top-individuals",
1124       G_CALLBACK (top_individuals_changed_cb), self, 0);
1125   tp_g_signal_connect_object (self->priv->manager, "notify::favourites-changed",
1126       G_CALLBACK (favourites_changed_cb), self, 0);
1127
1128   egg_list_box_set_sort_func (EGG_LIST_BOX (self),
1129       roster_view_sort, self, NULL);
1130
1131   egg_list_box_set_separator_funcs (EGG_LIST_BOX (self), update_separator,
1132       self, NULL);
1133
1134   egg_list_box_set_filter_func (EGG_LIST_BOX (self), filter_list, self, NULL);
1135
1136   egg_list_box_set_activate_on_single_click (EGG_LIST_BOX (self), FALSE);
1137 }
1138
1139 static void
1140 empathy_roster_view_dispose (GObject *object)
1141 {
1142   EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (object);
1143   void (*chain_up) (GObject *) =
1144       ((GObjectClass *) empathy_roster_view_parent_class)->dispose;
1145
1146   stop_flashing (self);
1147
1148   empathy_roster_view_set_live_search (self, NULL);
1149   g_clear_object (&self->priv->manager);
1150   g_clear_object (&self->priv->model);
1151
1152   if (chain_up != NULL)
1153     chain_up (object);
1154 }
1155
1156 static void
1157 empathy_roster_view_finalize (GObject *object)
1158 {
1159   EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (object);
1160   void (*chain_up) (GObject *) =
1161       ((GObjectClass *) empathy_roster_view_parent_class)->finalize;
1162
1163   g_hash_table_unref (self->priv->roster_contacts);
1164   g_hash_table_unref (self->priv->roster_groups);
1165   g_hash_table_unref (self->priv->displayed_contacts);
1166   g_queue_free_full (self->priv->events, event_free);
1167
1168   if (chain_up != NULL)
1169     chain_up (object);
1170 }
1171
1172 static void
1173 empathy_roster_view_child_activated (EggListBox *box,
1174     GtkWidget *child)
1175 {
1176   EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (box);
1177   EmpathyRosterContact *contact;
1178   FolksIndividual *individual;
1179   GList *l;
1180
1181   if (!EMPATHY_IS_ROSTER_CONTACT (child))
1182     return;
1183
1184   contact = EMPATHY_ROSTER_CONTACT (child);
1185   individual = empathy_roster_contact_get_individual (contact);
1186
1187   /* Activate the oldest event associated with this contact, if any */
1188   for (l = g_queue_peek_tail_link (self->priv->events); l != NULL;
1189       l = g_list_previous (l))
1190     {
1191       Event *event = l->data;
1192
1193       if (event->individual == individual)
1194         {
1195           g_signal_emit (box, signals[SIG_EVENT_ACTIVATED], 0, individual,
1196               event->user_data);
1197           return;
1198         }
1199     }
1200
1201   g_signal_emit (box, signals[SIG_INDIVIDUAL_ACTIVATED], 0, individual);
1202 }
1203
1204 static void
1205 fire_popup_individual_menu (EmpathyRosterView *self,
1206     GtkWidget *child,
1207     guint button,
1208     guint time)
1209 {
1210   EmpathyRosterContact *contact;
1211   FolksIndividual *individual;
1212
1213   if (!EMPATHY_IS_ROSTER_CONTACT (child))
1214     return;
1215
1216   contact = EMPATHY_ROSTER_CONTACT (child);
1217   individual = empathy_roster_contact_get_individual (contact);
1218
1219   g_signal_emit (self, signals[SIG_POPUP_INDIVIDUAL_MENU], 0,
1220       individual, button, time);
1221 }
1222
1223 static gboolean
1224 empathy_roster_view_button_press_event (GtkWidget *widget,
1225     GdkEventButton *event)
1226 {
1227   EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (widget);
1228   gboolean (*chain_up) (GtkWidget *, GdkEventButton *) =
1229       ((GtkWidgetClass *) empathy_roster_view_parent_class)->button_press_event;
1230
1231   if (event->button == 3)
1232     {
1233       GtkWidget *child;
1234
1235       child = egg_list_box_get_child_at_y (EGG_LIST_BOX (self), event->y);
1236
1237       if (child != NULL)
1238         {
1239           egg_list_box_select_child (EGG_LIST_BOX (self), child);
1240
1241           fire_popup_individual_menu (self, child, event->button, event->time);
1242         }
1243     }
1244
1245   return chain_up (widget, event);
1246 }
1247
1248 static gboolean
1249 empathy_roster_view_key_press_event (GtkWidget *widget,
1250     GdkEventKey *event)
1251 {
1252   EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (widget);
1253   gboolean (*chain_up) (GtkWidget *, GdkEventKey *) =
1254       ((GtkWidgetClass *) empathy_roster_view_parent_class)->key_press_event;
1255
1256   if (event->keyval == GDK_KEY_Menu)
1257     {
1258       GtkWidget *child;
1259
1260       child = egg_list_box_get_selected_child (EGG_LIST_BOX (self));
1261
1262       if (child != NULL)
1263         fire_popup_individual_menu (self, child, 0, event->time);
1264     }
1265
1266   return chain_up (widget, event);
1267 }
1268
1269 /**
1270  * @out_child: (out) (allow-none)
1271  */
1272 FolksIndividual *
1273 empathy_roster_view_get_individual_at_y (EmpathyRosterView *self,
1274     gint y,
1275     GtkWidget **out_child)
1276 {
1277   GtkWidget *child;
1278
1279   child = egg_list_box_get_child_at_y (EGG_LIST_BOX (self), y);
1280
1281   if (out_child != NULL)
1282     *out_child = child;
1283
1284   if (!EMPATHY_IS_ROSTER_CONTACT (child))
1285     return NULL;
1286
1287   return empathy_roster_contact_get_individual (EMPATHY_ROSTER_CONTACT (child));
1288 }
1289
1290 /**
1291  * @out_child: (out) (allow-none)
1292  */
1293 const gchar *
1294 empathy_roster_view_get_group_at_y (EmpathyRosterView *self,
1295     gint y)
1296 {
1297   GtkWidget *child;
1298
1299   child = egg_list_box_get_child_at_y (EGG_LIST_BOX (self), y);
1300
1301   if (EMPATHY_IS_ROSTER_CONTACT (child))
1302     return empathy_roster_contact_get_group (EMPATHY_ROSTER_CONTACT (child));
1303   else if (EMPATHY_IS_ROSTER_GROUP (child))
1304     return empathy_roster_group_get_name (EMPATHY_ROSTER_GROUP (child));
1305
1306   return NULL;
1307 }
1308
1309 static gboolean
1310 empathy_roster_view_query_tooltip (GtkWidget *widget,
1311     gint x,
1312     gint y,
1313     gboolean keyboard_mode,
1314     GtkTooltip *tooltip)
1315 {
1316   EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (widget);
1317   FolksIndividual *individual;
1318   gboolean result;
1319   GtkWidget *child;
1320
1321   individual = empathy_roster_view_get_individual_at_y (self, y, &child);
1322   if (individual == NULL)
1323     return FALSE;
1324
1325   g_signal_emit (self, signals[SIG_INDIVIDUAL_TOOLTIP], 0,
1326       individual, keyboard_mode, tooltip, &result);
1327
1328   if (result)
1329     {
1330       GtkAllocation allocation;
1331
1332       gtk_widget_get_allocation (child, &allocation);
1333       gtk_tooltip_set_tip_area (tooltip, (GdkRectangle *) &allocation);
1334     }
1335
1336   return result;
1337 }
1338
1339 static void
1340 empathy_roster_view_remove (GtkContainer *container,
1341     GtkWidget *widget)
1342 {
1343   EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (container);
1344   void (*chain_up) (GtkContainer *, GtkWidget *) =
1345       ((GtkContainerClass *) empathy_roster_view_parent_class)->remove;
1346
1347   chain_up (container, widget);
1348
1349   if (EMPATHY_IS_ROSTER_CONTACT (widget))
1350     remove_from_displayed (self, (EmpathyRosterContact *) widget);
1351 }
1352
1353 static void
1354 empathy_roster_view_class_init (
1355     EmpathyRosterViewClass *klass)
1356 {
1357   GObjectClass *oclass = G_OBJECT_CLASS (klass);
1358   EggListBoxClass *box_class = EGG_LIST_BOX_CLASS (klass);
1359   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
1360   GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
1361   GParamSpec *spec;
1362
1363   oclass->get_property = empathy_roster_view_get_property;
1364   oclass->set_property = empathy_roster_view_set_property;
1365   oclass->constructed = empathy_roster_view_constructed;
1366   oclass->dispose = empathy_roster_view_dispose;
1367   oclass->finalize = empathy_roster_view_finalize;
1368
1369   widget_class->button_press_event = empathy_roster_view_button_press_event;
1370   widget_class->key_press_event = empathy_roster_view_key_press_event;
1371   widget_class->query_tooltip = empathy_roster_view_query_tooltip;
1372
1373   container_class->remove = empathy_roster_view_remove;
1374
1375   box_class->child_activated = empathy_roster_view_child_activated;
1376
1377   spec = g_param_spec_object ("manager", "Manager",
1378       "EmpathyIndividualManager",
1379       EMPATHY_TYPE_INDIVIDUAL_MANAGER,
1380       G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
1381   g_object_class_install_property (oclass, PROP_MANAGER, spec);
1382
1383   spec = g_param_spec_object ("model", "Model",
1384       "EmpathyRosterModel",
1385       EMPATHY_TYPE_ROSTER_MODEL,
1386       G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
1387   g_object_class_install_property (oclass, PROP_MODEL, spec);
1388
1389   spec = g_param_spec_boolean ("show-offline", "Show Offline",
1390       "Show offline contacts",
1391       FALSE,
1392       G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
1393   g_object_class_install_property (oclass, PROP_SHOW_OFFLINE, spec);
1394
1395   spec = g_param_spec_boolean ("show-groups", "Show Groups",
1396       "Show groups",
1397       FALSE,
1398       G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
1399   g_object_class_install_property (oclass, PROP_SHOW_GROUPS, spec);
1400
1401   spec = g_param_spec_boolean ("empty", "Empty",
1402       "Is the view currently empty?",
1403       FALSE,
1404       G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
1405   g_object_class_install_property (oclass, PROP_EMPTY, spec);
1406
1407   signals[SIG_INDIVIDUAL_ACTIVATED] = g_signal_new ("individual-activated",
1408       G_OBJECT_CLASS_TYPE (klass),
1409       G_SIGNAL_RUN_LAST,
1410       0, NULL, NULL, NULL,
1411       G_TYPE_NONE,
1412       1, FOLKS_TYPE_INDIVIDUAL);
1413
1414   signals[SIG_POPUP_INDIVIDUAL_MENU] = g_signal_new ("popup-individual-menu",
1415       G_OBJECT_CLASS_TYPE (klass),
1416       G_SIGNAL_RUN_LAST,
1417       0, NULL, NULL, NULL,
1418       G_TYPE_NONE,
1419       3, FOLKS_TYPE_INDIVIDUAL, G_TYPE_UINT, G_TYPE_UINT);
1420
1421   signals[SIG_EVENT_ACTIVATED] = g_signal_new ("event-activated",
1422       G_OBJECT_CLASS_TYPE (klass),
1423       G_SIGNAL_RUN_LAST,
1424       0, NULL, NULL, NULL,
1425       G_TYPE_NONE,
1426       2, FOLKS_TYPE_INDIVIDUAL, G_TYPE_POINTER);
1427
1428   signals[SIG_INDIVIDUAL_TOOLTIP] = g_signal_new ("individual-tooltip",
1429       G_OBJECT_CLASS_TYPE (klass),
1430       G_SIGNAL_RUN_LAST,
1431       0, g_signal_accumulator_true_handled, NULL, NULL,
1432       G_TYPE_BOOLEAN,
1433       3, FOLKS_TYPE_INDIVIDUAL, G_TYPE_BOOLEAN, GTK_TYPE_TOOLTIP);
1434
1435   g_type_class_add_private (klass, sizeof (EmpathyRosterViewPriv));
1436 }
1437
1438 static void
1439 empathy_roster_view_init (EmpathyRosterView *self)
1440 {
1441   self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
1442       EMPATHY_TYPE_ROSTER_VIEW, EmpathyRosterViewPriv);
1443
1444   self->priv->roster_contacts = g_hash_table_new_full (NULL, NULL,
1445       NULL, (GDestroyNotify) g_hash_table_unref);
1446   self->priv->roster_groups = g_hash_table_new_full (g_str_hash, g_str_equal,
1447       g_free, NULL);
1448   self->priv->displayed_contacts = g_hash_table_new (NULL, NULL);
1449
1450   self->priv->events = g_queue_new ();
1451
1452   self->priv->empty = TRUE;
1453 }
1454
1455 GtkWidget *
1456 empathy_roster_view_new (EmpathyIndividualManager *manager,
1457     EmpathyRosterModel *model)
1458 {
1459   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_MANAGER (manager), NULL);
1460   g_return_val_if_fail (EMPATHY_IS_ROSTER_MODEL (model), NULL);
1461
1462   return g_object_new (EMPATHY_TYPE_ROSTER_VIEW,
1463       "manager", manager,
1464       "model", model,
1465       NULL);
1466 }
1467
1468 EmpathyIndividualManager *
1469 empathy_roster_view_get_manager (EmpathyRosterView *self)
1470 {
1471   return self->priv->manager;
1472 }
1473
1474 void
1475 empathy_roster_view_show_offline (EmpathyRosterView *self,
1476     gboolean show)
1477 {
1478   if (self->priv->show_offline == show)
1479     return;
1480
1481   self->priv->show_offline = show;
1482   egg_list_box_refilter (EGG_LIST_BOX (self));
1483
1484   g_object_notify (G_OBJECT (self), "show-offline");
1485 }
1486
1487 static void
1488 clear_view (EmpathyRosterView *self)
1489 {
1490   gtk_container_foreach (GTK_CONTAINER (self),
1491       (GtkCallback) gtk_widget_destroy, NULL);
1492
1493   g_hash_table_remove_all (self->priv->roster_contacts);
1494   g_hash_table_remove_all (self->priv->roster_groups);
1495   g_hash_table_remove_all (self->priv->displayed_contacts);
1496 }
1497
1498 void
1499 empathy_roster_view_show_groups (EmpathyRosterView *self,
1500     gboolean show)
1501 {
1502   if (self->priv->show_groups == show)
1503     return;
1504
1505   self->priv->show_groups = show;
1506
1507   /* TODO: block sort/filter? */
1508   clear_view (self);
1509   populate_view (self);
1510
1511   g_object_notify (G_OBJECT (self), "show-groups");
1512 }
1513
1514 static void
1515 select_first_contact (EmpathyRosterView *self)
1516 {
1517   GList *children, *l;
1518
1519   children = gtk_container_get_children (GTK_CONTAINER (self));
1520   for (l = children; l != NULL; l = g_list_next (l))
1521     {
1522       GtkWidget *child = l->data;
1523
1524       if (!gtk_widget_get_child_visible (child))
1525         continue;
1526
1527       if (!EMPATHY_IS_ROSTER_CONTACT (child))
1528         continue;
1529
1530       egg_list_box_select_child (EGG_LIST_BOX (self), child);
1531       break;
1532     }
1533
1534   g_list_free (children);
1535 }
1536
1537 static void
1538 search_text_notify_cb (EmpathyLiveSearch *search,
1539     GParamSpec *pspec,
1540     EmpathyRosterView *self)
1541 {
1542   egg_list_box_refilter (EGG_LIST_BOX (self));
1543
1544   select_first_contact (self);
1545 }
1546
1547 static void
1548 search_activate_cb (GtkWidget *search,
1549   EmpathyRosterView *self)
1550 {
1551   EggListBox *box = EGG_LIST_BOX (self);
1552   GtkWidget *child;
1553
1554   child = egg_list_box_get_selected_child (box);
1555   if (child == NULL)
1556     return;
1557
1558   empathy_roster_view_child_activated (box, child);
1559 }
1560
1561 void
1562 empathy_roster_view_set_live_search (EmpathyRosterView *self,
1563     EmpathyLiveSearch *search)
1564 {
1565   if (self->priv->search != NULL)
1566     {
1567       g_signal_handlers_disconnect_by_func (self->priv->search,
1568           search_text_notify_cb, self);
1569       g_signal_handlers_disconnect_by_func (self->priv->search,
1570           search_activate_cb, self);
1571
1572       g_clear_object (&self->priv->search);
1573     }
1574
1575   if (search == NULL)
1576     return;
1577
1578   self->priv->search = g_object_ref (search);
1579
1580   g_signal_connect (self->priv->search, "notify::text",
1581       G_CALLBACK (search_text_notify_cb), self);
1582   g_signal_connect (self->priv->search, "activate",
1583       G_CALLBACK (search_activate_cb), self);
1584 }
1585
1586 gboolean
1587 empathy_roster_view_is_empty (EmpathyRosterView *self)
1588 {
1589   return self->priv->empty;
1590 }
1591
1592 gboolean
1593 empathy_roster_view_is_searching (EmpathyRosterView *self)
1594 {
1595   return (self->priv->search != NULL &&
1596       gtk_widget_get_visible (GTK_WIDGET (self->priv->search)));
1597 }
1598
1599 /* Don't use EmpathyEvent as I prefer to keep this object not too specific to
1600  * Empathy's internals. */
1601 guint
1602 empathy_roster_view_add_event (EmpathyRosterView *self,
1603     FolksIndividual *individual,
1604     const gchar *icon,
1605     gpointer user_data)
1606 {
1607   GHashTable *contacts;
1608
1609   contacts = g_hash_table_lookup (self->priv->roster_contacts, individual);
1610   if (contacts == NULL)
1611     return 0;
1612
1613   self->priv->last_event_id++;
1614
1615   g_queue_push_head (self->priv->events,
1616       event_new (self->priv->last_event_id, individual, icon, user_data));
1617
1618   start_flashing (self);
1619
1620   return self->priv->last_event_id;
1621 }
1622
1623 void
1624 empathy_roster_view_remove_event (EmpathyRosterView *self,
1625     guint event_id)
1626 {
1627   GList *l;
1628
1629   for (l = g_queue_peek_head_link (self->priv->events); l != NULL;
1630       l = g_list_next (l))
1631     {
1632       Event *event = l->data;
1633
1634       if (event->id == event_id)
1635         {
1636           remove_event (self, event);
1637           return;
1638         }
1639     }
1640 }
1641
1642 FolksIndividual *
1643 empathy_roster_view_get_selected_individual (EmpathyRosterView *self)
1644 {
1645   GtkWidget *child;
1646
1647   child = egg_list_box_get_selected_child (EGG_LIST_BOX (self));
1648
1649   if (!EMPATHY_IS_ROSTER_CONTACT (child))
1650     return NULL;
1651
1652   return empathy_roster_contact_get_individual (EMPATHY_ROSTER_CONTACT (child));
1653 }