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