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