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