]> git.0d.be Git - empathy.git/blob - libempathy-gtk/empathy-roster-view.c
roster-view: factor out contact_should_be_displayed()
[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 /**
707  * check if @contact should be displayed according to @self's current status
708  * and without consideration for the state of @contact's groups.
709  */
710 static gboolean
711 contact_should_be_displayed (EmpathyRosterView *self,
712     EmpathyRosterContact *contact)
713 {
714   if (is_searching (self))
715     {
716       FolksIndividual *individual;
717
718       individual = empathy_roster_contact_get_individual (contact);
719
720       return empathy_individual_match_string (individual,
721           empathy_live_search_get_text (self->priv->search),
722           empathy_live_search_get_words (self->priv->search));
723     }
724   else
725     {
726       if (self->priv->show_offline)
727         {
728           return TRUE;
729         }
730       else
731         {
732           return empathy_roster_contact_is_online (contact);
733         }
734     }
735 }
736
737 static gboolean
738 filter_contact (EmpathyRosterView *self,
739     EmpathyRosterContact *contact)
740 {
741   gboolean displayed;
742
743   displayed = contact_should_be_displayed (self, contact);
744
745   if (self->priv->show_groups)
746     {
747       const gchar *group_name;
748       EmpathyRosterGroup *group;
749
750       group_name = empathy_roster_contact_get_group (contact);
751       group = lookup_roster_group (self, group_name);
752
753       if (group != NULL)
754         {
755           update_group_widgets_count (self, group, contact, displayed);
756
757           /* When searching, always display even if the group is closed */
758           if (!is_searching (self) &&
759               !gtk_expander_get_expanded (GTK_EXPANDER (group)))
760             displayed = FALSE;
761         }
762     }
763
764   if (displayed)
765     {
766       add_to_displayed (self, contact);
767     }
768   else
769     {
770       remove_from_displayed (self, contact);
771     }
772
773   return displayed;
774 }
775
776 static gboolean
777 filter_group (EmpathyRosterView *self,
778     EmpathyRosterGroup *group)
779 {
780   return empathy_roster_group_get_widgets_count (group);
781 }
782
783 static gboolean
784 filter_list (GtkWidget *child,
785     gpointer user_data)
786 {
787   EmpathyRosterView *self = user_data;
788
789   if (EMPATHY_IS_ROSTER_CONTACT (child))
790     return filter_contact (self, EMPATHY_ROSTER_CONTACT (child));
791
792   else if (EMPATHY_IS_ROSTER_GROUP (child))
793     return filter_group (self, EMPATHY_ROSTER_GROUP (child));
794
795   g_return_val_if_reached (FALSE);
796 }
797
798 /* @list: GList of EmpathyRosterContact
799  *
800  * Returns: %TRUE if @list contains an EmpathyRosterContact associated with
801  * @individual */
802 static gboolean
803 individual_in_list (FolksIndividual *individual,
804     GList *list)
805 {
806   GList *l;
807
808   for (l = list; l != NULL; l = g_list_next (l))
809     {
810       EmpathyRosterContact *contact = l->data;
811
812       if (empathy_roster_contact_get_individual (contact) == individual)
813         return TRUE;
814     }
815
816   return FALSE;
817 }
818
819 static void
820 populate_view (EmpathyRosterView *self)
821 {
822   GList *individuals, *l;
823
824   individuals = empathy_individual_manager_get_members (self->priv->manager);
825   for (l = individuals; l != NULL; l = g_list_next (l))
826     {
827       FolksIndividual *individual = l->data;
828
829       individual_added (self, individual);
830     }
831
832   g_list_free (individuals);
833 }
834
835 static void
836 remove_from_group (EmpathyRosterView *self,
837     FolksIndividual *individual,
838     const gchar *group)
839 {
840   GHashTable *contacts;
841   GtkWidget *contact;
842   EmpathyRosterGroup *roster_group;
843
844   contacts = g_hash_table_lookup (self->priv->roster_contacts, individual);
845   if (contacts == NULL)
846     return;
847
848   contact = g_hash_table_lookup (contacts, group);
849   if (contact == NULL)
850     return;
851
852   g_hash_table_remove (contacts, group);
853
854   if (g_hash_table_size (contacts) == 0)
855     {
856       add_to_group (self, individual, UNGROUPPED);
857     }
858
859   roster_group = lookup_roster_group (self, group);
860
861   if (roster_group != NULL)
862     {
863       update_group_widgets_count (self, roster_group,
864           EMPATHY_ROSTER_CONTACT (contact), FALSE);
865     }
866
867   gtk_container_remove (GTK_CONTAINER (self), contact);
868 }
869
870 static void
871 update_top_contacts (EmpathyRosterView *self)
872 {
873   GList *tops, *l;
874   GList *to_add = NULL, *to_remove = NULL;
875   EmpathyRosterGroup *group;
876
877   if (!self->priv->show_groups)
878     {
879       egg_list_box_resort (EGG_LIST_BOX (self));
880       return;
881     }
882
883   tops = empathy_individual_manager_get_top_individuals (self->priv->manager);
884
885   group = g_hash_table_lookup (self->priv->roster_groups, TOP_GROUP);
886   if (group == NULL)
887     {
888       to_add = g_list_copy (tops);
889     }
890   else
891     {
892       GList *contacts;
893
894       contacts = empathy_roster_group_get_widgets (group);
895
896       /* Check which EmpathyRosterContact have to be removed */
897       for (l = contacts; l != NULL; l = g_list_next (l))
898         {
899           EmpathyRosterContact *contact = l->data;
900           FolksIndividual *individual;
901
902           individual = empathy_roster_contact_get_individual (contact);
903
904           if (g_list_find (tops, individual) == NULL)
905             to_remove = g_list_prepend (to_remove, individual);
906         }
907
908       /* Check which EmpathyRosterContact have to be added */
909       for (l = tops; l != NULL; l = g_list_next (l))
910         {
911           FolksIndividual *individual = l->data;
912
913           if (!individual_in_list (individual, contacts))
914             to_add = g_list_prepend (to_add, individual);
915         }
916     }
917
918   for (l = to_add; l != NULL; l = g_list_next (l))
919     add_to_group (self, l->data, TOP_GROUP);
920
921   for (l = to_remove; l != NULL; l = g_list_next (l))
922     remove_from_group (self, l->data, TOP_GROUP);
923
924   g_list_free (to_add);
925   g_list_free (to_remove);
926 }
927
928 static void
929 groups_changed_cb (EmpathyIndividualManager *manager,
930     FolksIndividual *individual,
931     gchar *group,
932     gboolean is_member,
933     EmpathyRosterView *self)
934 {
935   if (!self->priv->show_groups)
936     return;
937
938   if (is_member)
939     {
940       add_to_group (self, individual, group);
941     }
942   else
943     {
944       remove_from_group (self, individual, group);
945     }
946 }
947
948 static void
949 top_individuals_changed_cb (EmpathyIndividualManager *manager,
950     GParamSpec *spec,
951     EmpathyRosterView *self)
952 {
953   update_top_contacts (self);
954 }
955
956 static void
957 empathy_roster_view_constructed (GObject *object)
958 {
959   EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (object);
960   void (*chain_up) (GObject *) =
961       ((GObjectClass *) empathy_roster_view_parent_class)->constructed;
962
963   if (chain_up != NULL)
964     chain_up (object);
965
966   g_assert (EMPATHY_IS_INDIVIDUAL_MANAGER (self->priv->manager));
967
968   populate_view (self);
969
970   tp_g_signal_connect_object (self->priv->manager, "members-changed",
971       G_CALLBACK (members_changed_cb), self, 0);
972   tp_g_signal_connect_object (self->priv->manager, "groups-changed",
973       G_CALLBACK (groups_changed_cb), self, 0);
974   tp_g_signal_connect_object (self->priv->manager, "notify::top-individuals",
975       G_CALLBACK (top_individuals_changed_cb), self, 0);
976
977   egg_list_box_set_sort_func (EGG_LIST_BOX (self),
978       roster_view_sort, self, NULL);
979
980   egg_list_box_set_separator_funcs (EGG_LIST_BOX (self), update_separator,
981       self, NULL);
982
983   egg_list_box_set_filter_func (EGG_LIST_BOX (self), filter_list, self, NULL);
984
985   egg_list_box_set_activate_on_single_click (EGG_LIST_BOX (self), FALSE);
986 }
987
988 static void
989 empathy_roster_view_dispose (GObject *object)
990 {
991   EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (object);
992   void (*chain_up) (GObject *) =
993       ((GObjectClass *) empathy_roster_view_parent_class)->dispose;
994
995   stop_flashing (self);
996
997   empathy_roster_view_set_live_search (self, NULL);
998   g_clear_object (&self->priv->manager);
999
1000   if (chain_up != NULL)
1001     chain_up (object);
1002 }
1003
1004 static void
1005 empathy_roster_view_finalize (GObject *object)
1006 {
1007   EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (object);
1008   void (*chain_up) (GObject *) =
1009       ((GObjectClass *) empathy_roster_view_parent_class)->finalize;
1010
1011   g_hash_table_unref (self->priv->roster_contacts);
1012   g_hash_table_unref (self->priv->roster_groups);
1013   g_hash_table_unref (self->priv->displayed_contacts);
1014   g_queue_free_full (self->priv->events, event_free);
1015
1016   if (chain_up != NULL)
1017     chain_up (object);
1018 }
1019
1020 static void
1021 empathy_roster_view_child_activated (EggListBox *box,
1022     GtkWidget *child)
1023 {
1024   EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (box);
1025   EmpathyRosterContact *contact;
1026   FolksIndividual *individual;
1027   GList *l;
1028
1029   if (!EMPATHY_IS_ROSTER_CONTACT (child))
1030     return;
1031
1032   contact = EMPATHY_ROSTER_CONTACT (child);
1033   individual = empathy_roster_contact_get_individual (contact);
1034
1035   /* Activate the oldest event associated with this contact, if any */
1036   for (l = g_queue_peek_tail_link (self->priv->events); l != NULL;
1037       l = g_list_previous (l))
1038     {
1039       Event *event = l->data;
1040
1041       if (event->individual == individual)
1042         {
1043           g_signal_emit (box, signals[SIG_EVENT_ACTIVATED], 0, individual,
1044               event->user_data);
1045           return;
1046         }
1047     }
1048
1049   g_signal_emit (box, signals[SIG_INDIVIDUAL_ACTIVATED], 0, individual);
1050 }
1051
1052 static void
1053 fire_popup_individual_menu (EmpathyRosterView *self,
1054     GtkWidget *child,
1055     guint button,
1056     guint time)
1057 {
1058   EmpathyRosterContact *contact;
1059   FolksIndividual *individual;
1060
1061   if (!EMPATHY_IS_ROSTER_CONTACT (child))
1062     return;
1063
1064   contact = EMPATHY_ROSTER_CONTACT (child);
1065   individual = empathy_roster_contact_get_individual (contact);
1066
1067   g_signal_emit (self, signals[SIG_POPUP_INDIVIDUAL_MENU], 0,
1068       individual, button, time);
1069 }
1070
1071 static gboolean
1072 empathy_roster_view_button_press_event (GtkWidget *widget,
1073     GdkEventButton *event)
1074 {
1075   EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (widget);
1076   gboolean (*chain_up) (GtkWidget *, GdkEventButton *) =
1077       ((GtkWidgetClass *) empathy_roster_view_parent_class)->button_press_event;
1078
1079   if (event->button == 3)
1080     {
1081       GtkWidget *child;
1082
1083       child = egg_list_box_get_child_at_y (EGG_LIST_BOX (self), event->y);
1084
1085       if (child != NULL)
1086         fire_popup_individual_menu (self, child, event->button, event->time);
1087     }
1088
1089   return chain_up (widget, event);
1090 }
1091
1092 static gboolean
1093 empathy_roster_view_key_press_event (GtkWidget *widget,
1094     GdkEventKey *event)
1095 {
1096   EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (widget);
1097   gboolean (*chain_up) (GtkWidget *, GdkEventKey *) =
1098       ((GtkWidgetClass *) empathy_roster_view_parent_class)->key_press_event;
1099
1100   if (event->keyval == GDK_KEY_Menu)
1101     {
1102       GtkWidget *child;
1103
1104       child = egg_list_box_get_selected_child (EGG_LIST_BOX (self));
1105
1106       if (child != NULL)
1107         fire_popup_individual_menu (self, child, 0, event->time);
1108     }
1109
1110   return chain_up (widget, event);
1111 }
1112
1113 static gboolean
1114 empathy_roster_view_query_tooltip (GtkWidget *widget,
1115     gint x,
1116     gint y,
1117     gboolean keyboard_mode,
1118     GtkTooltip *tooltip)
1119 {
1120   EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (widget);
1121   GtkWidget *child;
1122   EmpathyRosterContact *contact;
1123   FolksIndividual *individual;
1124
1125   if (self->priv->individual_tooltip_cb == NULL)
1126     return FALSE;
1127
1128   child = egg_list_box_get_child_at_y (EGG_LIST_BOX (self), y);
1129   if (!EMPATHY_IS_ROSTER_CONTACT (child))
1130     return FALSE;
1131
1132   contact = EMPATHY_ROSTER_CONTACT (child);
1133   individual = empathy_roster_contact_get_individual (contact);
1134
1135   return self->priv->individual_tooltip_cb (self, individual, keyboard_mode,
1136       tooltip, self->priv->individual_tooltip_data);
1137 }
1138
1139 void
1140 empathy_roster_view_set_individual_tooltip_cb (EmpathyRosterView *self,
1141     EmpathyRosterViewIndividualTooltipCb callback,
1142     gpointer user_data)
1143 {
1144   self->priv->individual_tooltip_cb = callback;
1145   self->priv->individual_tooltip_data = user_data;
1146
1147   gtk_widget_set_has_tooltip (GTK_WIDGET (self), callback != NULL);
1148 }
1149
1150 static void
1151 empathy_roster_view_remove (GtkContainer *container,
1152     GtkWidget *widget)
1153 {
1154   EmpathyRosterView *self = EMPATHY_ROSTER_VIEW (container);
1155   void (*chain_up) (GtkContainer *, GtkWidget *) =
1156       ((GtkContainerClass *) empathy_roster_view_parent_class)->remove;
1157
1158   chain_up (container, widget);
1159
1160   if (EMPATHY_IS_ROSTER_CONTACT (widget))
1161     remove_from_displayed (self, (EmpathyRosterContact *) widget);
1162 }
1163
1164 static void
1165 empathy_roster_view_class_init (
1166     EmpathyRosterViewClass *klass)
1167 {
1168   GObjectClass *oclass = G_OBJECT_CLASS (klass);
1169   EggListBoxClass *box_class = EGG_LIST_BOX_CLASS (klass);
1170   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
1171   GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
1172   GParamSpec *spec;
1173
1174   oclass->get_property = empathy_roster_view_get_property;
1175   oclass->set_property = empathy_roster_view_set_property;
1176   oclass->constructed = empathy_roster_view_constructed;
1177   oclass->dispose = empathy_roster_view_dispose;
1178   oclass->finalize = empathy_roster_view_finalize;
1179
1180   widget_class->button_press_event = empathy_roster_view_button_press_event;
1181   widget_class->key_press_event = empathy_roster_view_key_press_event;
1182   widget_class->query_tooltip = empathy_roster_view_query_tooltip;
1183
1184   container_class->remove = empathy_roster_view_remove;
1185
1186   box_class->child_activated = empathy_roster_view_child_activated;
1187
1188   spec = g_param_spec_object ("manager", "Manager",
1189       "EmpathyIndividualManager",
1190       EMPATHY_TYPE_INDIVIDUAL_MANAGER,
1191       G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
1192   g_object_class_install_property (oclass, PROP_MANAGER, spec);
1193
1194   spec = g_param_spec_boolean ("show-offline", "Show Offline",
1195       "Show offline contacts",
1196       FALSE,
1197       G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
1198   g_object_class_install_property (oclass, PROP_SHOW_OFFLINE, spec);
1199
1200   spec = g_param_spec_boolean ("show-groups", "Show Groups",
1201       "Show groups",
1202       FALSE,
1203       G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
1204   g_object_class_install_property (oclass, PROP_SHOW_GROUPS, spec);
1205
1206   spec = g_param_spec_boolean ("empty", "Empty",
1207       "Is the view currently empty?",
1208       FALSE,
1209       G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
1210   g_object_class_install_property (oclass, PROP_EMPTY, spec);
1211
1212   signals[SIG_INDIVIDUAL_ACTIVATED] = g_signal_new ("individual-activated",
1213       G_OBJECT_CLASS_TYPE (klass),
1214       G_SIGNAL_RUN_LAST,
1215       0, NULL, NULL, NULL,
1216       G_TYPE_NONE,
1217       1, FOLKS_TYPE_INDIVIDUAL);
1218
1219   signals[SIG_POPUP_INDIVIDUAL_MENU] = g_signal_new ("popup-individual-menu",
1220       G_OBJECT_CLASS_TYPE (klass),
1221       G_SIGNAL_RUN_LAST,
1222       0, NULL, NULL, NULL,
1223       G_TYPE_NONE,
1224       3, FOLKS_TYPE_INDIVIDUAL, G_TYPE_UINT, G_TYPE_UINT);
1225
1226   signals[SIG_EVENT_ACTIVATED] = g_signal_new ("event-activated",
1227       G_OBJECT_CLASS_TYPE (klass),
1228       G_SIGNAL_RUN_LAST,
1229       0, NULL, NULL, NULL,
1230       G_TYPE_NONE,
1231       2, FOLKS_TYPE_INDIVIDUAL, G_TYPE_POINTER);
1232
1233   g_type_class_add_private (klass, sizeof (EmpathyRosterViewPriv));
1234 }
1235
1236 static void
1237 empathy_roster_view_init (EmpathyRosterView *self)
1238 {
1239   self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
1240       EMPATHY_TYPE_ROSTER_VIEW, EmpathyRosterViewPriv);
1241
1242   self->priv->roster_contacts = g_hash_table_new_full (NULL, NULL,
1243       NULL, (GDestroyNotify) g_hash_table_unref);
1244   self->priv->roster_groups = g_hash_table_new_full (g_str_hash, g_str_equal,
1245       g_free, NULL);
1246   self->priv->displayed_contacts = g_hash_table_new (NULL, NULL);
1247
1248   self->priv->events = g_queue_new ();
1249
1250   self->priv->empty = TRUE;
1251 }
1252
1253 GtkWidget *
1254 empathy_roster_view_new (EmpathyIndividualManager *manager)
1255 {
1256   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_MANAGER (manager), NULL);
1257
1258   return g_object_new (EMPATHY_TYPE_ROSTER_VIEW,
1259       "manager", manager,
1260       NULL);
1261 }
1262
1263 EmpathyIndividualManager *
1264 empathy_roster_view_get_manager (EmpathyRosterView *self)
1265 {
1266   return self->priv->manager;
1267 }
1268
1269 void
1270 empathy_roster_view_show_offline (EmpathyRosterView *self,
1271     gboolean show)
1272 {
1273   if (self->priv->show_offline == show)
1274     return;
1275
1276   self->priv->show_offline = show;
1277   egg_list_box_refilter (EGG_LIST_BOX (self));
1278
1279   g_object_notify (G_OBJECT (self), "show-offline");
1280 }
1281
1282 static void
1283 clear_view (EmpathyRosterView *self)
1284 {
1285   gtk_container_foreach (GTK_CONTAINER (self),
1286       (GtkCallback) gtk_widget_destroy, NULL);
1287
1288   g_hash_table_remove_all (self->priv->roster_contacts);
1289 }
1290
1291 void
1292 empathy_roster_view_show_groups (EmpathyRosterView *self,
1293     gboolean show)
1294 {
1295   if (self->priv->show_groups == show)
1296     return;
1297
1298   self->priv->show_groups = show;
1299
1300   /* TODO: block sort/filter? */
1301   clear_view (self);
1302   populate_view (self);
1303
1304   g_object_notify (G_OBJECT (self), "show-groups");
1305 }
1306
1307 static void
1308 select_first_contact (EmpathyRosterView *self)
1309 {
1310   GList *children, *l;
1311
1312   children = gtk_container_get_children (GTK_CONTAINER (self));
1313   for (l = children; l != NULL; l = g_list_next (l))
1314     {
1315       GtkWidget *child = l->data;
1316
1317       if (!gtk_widget_get_child_visible (child))
1318         continue;
1319
1320       if (!EMPATHY_IS_ROSTER_CONTACT (child))
1321         continue;
1322
1323       egg_list_box_select_child (EGG_LIST_BOX (self), child);
1324       break;
1325     }
1326
1327   g_list_free (children);
1328 }
1329
1330 static void
1331 search_text_notify_cb (EmpathyLiveSearch *search,
1332     GParamSpec *pspec,
1333     EmpathyRosterView *self)
1334 {
1335   egg_list_box_refilter (EGG_LIST_BOX (self));
1336
1337   select_first_contact (self);
1338 }
1339
1340 static void
1341 search_activate_cb (GtkWidget *search,
1342   EmpathyRosterView *self)
1343 {
1344   EggListBox *box = EGG_LIST_BOX (self);
1345   GtkWidget *child;
1346
1347   child = egg_list_box_get_selected_child (box);
1348   if (child == NULL)
1349     return;
1350
1351   empathy_roster_view_child_activated (box, child);
1352 }
1353
1354 void
1355 empathy_roster_view_set_live_search (EmpathyRosterView *self,
1356     EmpathyLiveSearch *search)
1357 {
1358   if (self->priv->search != NULL)
1359     {
1360       g_signal_handlers_disconnect_by_func (self->priv->search,
1361           search_text_notify_cb, self);
1362       g_signal_handlers_disconnect_by_func (self->priv->search,
1363           search_activate_cb, self);
1364
1365       g_clear_object (&self->priv->search);
1366     }
1367
1368   if (search == NULL)
1369     return;
1370
1371   self->priv->search = g_object_ref (search);
1372
1373   g_signal_connect (self->priv->search, "notify::text",
1374       G_CALLBACK (search_text_notify_cb), self);
1375   g_signal_connect (self->priv->search, "activate",
1376       G_CALLBACK (search_activate_cb), self);
1377 }
1378
1379 gboolean
1380 empathy_roster_view_is_empty (EmpathyRosterView *self)
1381 {
1382   return self->priv->empty;
1383 }
1384
1385 gboolean
1386 empathy_roster_view_is_searching (EmpathyRosterView *self)
1387 {
1388   return (self->priv->search != NULL &&
1389       gtk_widget_get_visible (GTK_WIDGET (self->priv->search)));
1390 }
1391
1392 /* Don't use EmpathyEvent as I prefer to keep this object not too specific to
1393  * Empathy's internals. */
1394 guint
1395 empathy_roster_view_add_event (EmpathyRosterView *self,
1396     FolksIndividual *individual,
1397     const gchar *icon,
1398     gpointer user_data)
1399 {
1400   GHashTable *contacts;
1401
1402   contacts = g_hash_table_lookup (self->priv->roster_contacts, individual);
1403   if (contacts == NULL)
1404     return 0;
1405
1406   self->priv->last_event_id++;
1407
1408   g_queue_push_head (self->priv->events,
1409       event_new (self->priv->last_event_id, individual, icon, user_data));
1410
1411   start_flashing (self);
1412
1413   return self->priv->last_event_id;
1414 }
1415
1416 void
1417 empathy_roster_view_remove_event (EmpathyRosterView *self,
1418     guint event_id)
1419 {
1420   GList *l;
1421
1422   for (l = g_queue_peek_head_link (self->priv->events); l != NULL;
1423       l = g_list_next (l))
1424     {
1425       Event *event = l->data;
1426
1427       if (event->id == event_id)
1428         {
1429           remove_event (self, event);
1430           return;
1431         }
1432     }
1433 }