]> git.0d.be Git - empathy.git/blob - libempathy-gtk/gossip-contact-list-store.c
1536262bbaa54662733531d4cd2d323d702c379f
[empathy.git] / libempathy-gtk / gossip-contact-list-store.c
1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 /*
3  * Copyright (C) 2005-2007 Imendio AB
4  * Copyright (C) 2007 Collabora Ltd.
5  *
6  * This program is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU General Public License as
8  * published by the Free Software Foundation; either version 2 of the
9  * License, or (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14  * General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public
17  * License along with this program; if not, write to the
18  * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
19  * Boston, MA 02111-1307, USA.
20  *
21  * Authors: Mikael Hallendal <micke@imendio.com>
22  *          Martyn Russell <martyn@imendio.com>
23  *          Xavier Claessens <xclaesse@gmail.com>
24  */
25
26 #include "config.h"
27
28 #include <string.h>
29
30 #include <glib.h>
31 #include <gtk/gtk.h>
32
33 #include <libempathy/gossip-debug.h>
34
35 #include "gossip-contact-list-store.h"
36 #include "gossip-contact-groups.h"
37 #include "gossip-ui-utils.h"
38
39 #define DEBUG_DOMAIN "ContactListStore"
40
41 /* Active users are those which have recently changed state
42  * (e.g. online, offline or from normal to a busy state).
43  */
44
45 /* Time user is shown as active */
46 #define ACTIVE_USER_SHOW_TIME 7000
47
48 /* Time after connecting which we wait before active users are enabled */
49 #define ACTIVE_USER_WAIT_TO_ENABLE_TIME 5000
50
51 #define GET_PRIV(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), GOSSIP_TYPE_CONTACT_LIST_STORE, GossipContactListStorePriv))
52
53 struct _GossipContactListStorePriv {
54         EmpathyContactList         *list;
55         gboolean                    show_offline;
56         gboolean                    show_avatars;
57         gboolean                    is_compact;
58         gboolean                    show_active;
59         GossipContactListStoreSort  sort_criterium;
60 };
61
62 typedef struct {
63         GtkTreeIter  iter;
64         const gchar *name;
65         gboolean     found;
66 } FindGroup;
67
68 typedef struct {
69         GossipContact *contact;
70         gboolean       found;
71         GList         *iters;
72 } FindContact;
73
74 typedef struct {
75         GossipContactListStore *store;
76         GossipContact          *contact;
77         gboolean                remove;
78 } ShowActiveData;
79
80 static void             gossip_contact_list_store_class_init         (GossipContactListStoreClass *klass);
81 static void             gossip_contact_list_store_init               (GossipContactListStore      *list);
82 static void             contact_list_store_finalize                  (GObject                     *object);
83 static void             contact_list_store_get_property              (GObject                     *object,
84                                                                       guint                        param_id,
85                                                                       GValue                      *value,
86                                                                       GParamSpec                  *pspec);
87 static void             contact_list_store_set_property              (GObject                     *object,
88                                                                       guint                        param_id,
89                                                                       const GValue                *value,
90                                                                       GParamSpec                  *pspec);
91 static void             contact_list_store_setup                     (GossipContactListStore      *store);
92 static void             contact_list_store_contact_added_cb          (EmpathyContactList          *list_iface,
93                                                                       GossipContact               *contact,
94                                                                       GossipContactListStore      *store);
95 static void             contact_list_store_add_contact               (GossipContactListStore      *store,
96                                                                       GossipContact               *contact);
97 static void             contact_list_store_contact_removed_cb        (EmpathyContactList          *list_iface,
98                                                                       GossipContact               *contact,
99                                                                       GossipContactListStore      *store);
100 static void             contact_list_store_remove_contact            (GossipContactListStore      *store,
101                                                                       GossipContact               *contact);
102 static void             contact_list_store_contact_update            (GossipContactListStore      *store,
103                                                                       GossipContact               *contact);
104 static void             contact_list_store_contact_groups_updated_cb (GossipContact               *contact,
105                                                                       GParamSpec                  *param,
106                                                                       GossipContactListStore      *store);
107 static void             contact_list_store_contact_updated_cb        (GossipContact               *contact,
108                                                                       GParamSpec                  *param,
109                                                                       GossipContactListStore      *store);
110 static void             contact_list_store_contact_set_active        (GossipContactListStore      *store,
111                                                                       GossipContact               *contact,
112                                                                       gboolean                     active,
113                                                                       gboolean                     set_changed);
114 static ShowActiveData * contact_list_store_contact_active_new        (GossipContactListStore      *store,
115                                                                       GossipContact               *contact,
116                                                                       gboolean                     remove);
117 static void             contact_list_store_contact_active_free       (ShowActiveData              *data);
118 static gboolean         contact_list_store_contact_active_cb         (ShowActiveData              *data);
119 static gboolean         contact_list_store_get_group_foreach         (GtkTreeModel                *model,
120                                                                       GtkTreePath                 *path,
121                                                                       GtkTreeIter                 *iter,
122                                                                       FindGroup                   *fg);
123 static void             contact_list_store_get_group                 (GossipContactListStore      *store,
124                                                                       const gchar                 *name,
125                                                                       GtkTreeIter                 *iter_group_to_set,
126                                                                       GtkTreeIter                 *iter_separator_to_set,
127                                                                       gboolean                    *created);
128 static gint             contact_list_store_state_sort_func           (GtkTreeModel                *model,
129                                                                       GtkTreeIter                 *iter_a,
130                                                                       GtkTreeIter                 *iter_b,
131                                                                       gpointer                     user_data);
132 static gint             contact_list_store_name_sort_func            (GtkTreeModel                *model,
133                                                                       GtkTreeIter                 *iter_a,
134                                                                       GtkTreeIter                 *iter_b,
135                                                                       gpointer                     user_data);
136 static gboolean         contact_list_store_find_contact_foreach      (GtkTreeModel                *model,
137                                                                       GtkTreePath                 *path,
138                                                                       GtkTreeIter                 *iter,
139                                                                       FindContact                 *fc);
140 static GList *          contact_list_store_find_contact              (GossipContactListStore      *store,
141                                                                       GossipContact               *contact);
142 static gboolean         contact_list_store_update_list_mode_foreach  (GtkTreeModel                *model,
143                                                                       GtkTreePath                 *path,
144                                                                       GtkTreeIter                 *iter,
145                                                                       GossipContactListStore      *store);
146
147 enum {
148         PROP_0,
149         PROP_SHOW_OFFLINE,
150         PROP_SHOW_AVATARS,
151         PROP_IS_COMPACT,
152         PROP_SORT_CRITERIUM
153 };
154
155 GType
156 gossip_contact_list_store_sort_get_type (void)
157 {
158         static GType etype = 0;
159
160         if (etype == 0) {
161                 static const GEnumValue values[] = {
162                         { GOSSIP_CONTACT_LIST_STORE_SORT_NAME, 
163                           "GOSSIP_CONTACT_LIST_STORE_SORT_NAME", 
164                           "name" },
165                         { GOSSIP_CONTACT_LIST_STORE_SORT_STATE, 
166                           "GOSSIP_CONTACT_LIST_STORE_SORT_STATE", 
167                           "state" },
168                         { 0, NULL, NULL }
169                 };
170
171                 etype = g_enum_register_static ("GossipContactListStoreSort", values);
172         }
173
174         return etype;
175 }
176
177 G_DEFINE_TYPE (GossipContactListStore, gossip_contact_list_store, GTK_TYPE_TREE_STORE);
178
179 static void
180 gossip_contact_list_store_class_init (GossipContactListStoreClass *klass)
181 {
182         GObjectClass *object_class = G_OBJECT_CLASS (klass);
183
184         object_class->finalize = contact_list_store_finalize;
185         object_class->get_property = contact_list_store_get_property;
186         object_class->set_property = contact_list_store_set_property;
187
188         g_object_class_install_property (object_class,
189                                          PROP_SHOW_OFFLINE,
190                                          g_param_spec_boolean ("show-offline",
191                                                                "Show Offline",
192                                                                "Whether contact list should display "
193                                                                "offline contacts",
194                                                                FALSE,
195                                                                G_PARAM_READWRITE));
196          g_object_class_install_property (object_class,
197                                           PROP_SHOW_AVATARS,
198                                           g_param_spec_boolean ("show-avatars",
199                                                                 "Show Avatars",
200                                                                 "Whether contact list should display "
201                                                                 "avatars for contacts",
202                                                                 TRUE,
203                                                                 G_PARAM_READWRITE));
204         g_object_class_install_property (object_class,
205                                          PROP_IS_COMPACT,
206                                          g_param_spec_boolean ("is-compact",
207                                                                "Is Compact",
208                                                                "Whether the contact list is in compact mode or not",
209                                                                FALSE,
210                                                                G_PARAM_READWRITE));
211
212         g_object_class_install_property (object_class,
213                                          PROP_SORT_CRITERIUM,
214                                          g_param_spec_enum ("sort-criterium",
215                                                             "Sort citerium",
216                                                             "The sort criterium to use for sorting the contact list",
217                                                             GOSSIP_TYPE_CONTACT_LIST_STORE_SORT,
218                                                             GOSSIP_CONTACT_LIST_STORE_SORT_NAME,
219                                                             G_PARAM_READWRITE));
220
221         g_type_class_add_private (object_class, sizeof (GossipContactListStorePriv));
222 }
223
224 static void
225 gossip_contact_list_store_init (GossipContactListStore *store)
226 {
227         GossipContactListStorePriv *priv;
228
229         priv = GET_PRIV (store);
230
231         priv->is_compact = FALSE;
232         priv->show_active = TRUE;
233         priv->show_avatars = TRUE;
234 }
235
236 static void
237 contact_list_store_finalize (GObject *object)
238 {
239         GossipContactListStorePriv *priv;
240
241         priv = GET_PRIV (object);
242
243         /* FIXME: disconnect all signals on the list and contacts */
244
245         if (priv->list) {
246                 g_object_unref (priv->list);
247         }
248
249         G_OBJECT_CLASS (gossip_contact_list_store_parent_class)->finalize (object);
250 }
251
252 static void
253 contact_list_store_get_property (GObject    *object,
254                                  guint       param_id,
255                                  GValue     *value,
256                                  GParamSpec *pspec)
257 {
258         GossipContactListStorePriv *priv;
259
260         priv = GET_PRIV (object);
261
262         switch (param_id) {
263         case PROP_SHOW_OFFLINE:
264                 g_value_set_boolean (value, priv->show_offline);
265                 break;
266         case PROP_SHOW_AVATARS:
267                 g_value_set_boolean (value, priv->show_avatars);
268                 break;
269         case PROP_IS_COMPACT:
270                 g_value_set_boolean (value, priv->is_compact);
271                 break;
272         case PROP_SORT_CRITERIUM:
273                 g_value_set_enum (value, priv->sort_criterium);
274                 break;
275         default:
276                 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
277                 break;
278         };
279 }
280
281 static void
282 contact_list_store_set_property (GObject      *object,
283                                  guint         param_id,
284                                  const GValue *value,
285                                  GParamSpec   *pspec)
286 {
287         GossipContactListStorePriv *priv;
288
289         priv = GET_PRIV (object);
290
291         switch (param_id) {
292         case PROP_SHOW_OFFLINE:
293                 gossip_contact_list_store_set_show_offline (GOSSIP_CONTACT_LIST_STORE (object),
294                                                             g_value_get_boolean (value));
295                 break;
296         case PROP_SHOW_AVATARS:
297                 gossip_contact_list_store_set_show_avatars (GOSSIP_CONTACT_LIST_STORE (object),
298                                                             g_value_get_boolean (value));
299                 break;
300         case PROP_IS_COMPACT:
301                 gossip_contact_list_store_set_is_compact (GOSSIP_CONTACT_LIST_STORE (object),
302                                                           g_value_get_boolean (value));
303                 break;
304         case PROP_SORT_CRITERIUM:
305                 gossip_contact_list_store_set_sort_criterium (GOSSIP_CONTACT_LIST_STORE (object),
306                                                               g_value_get_enum (value));
307                 break;
308         default:
309                 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
310                 break;
311         };
312 }
313
314 GossipContactListStore *
315 gossip_contact_list_store_new (EmpathyContactList *list_iface)
316 {
317         GossipContactListStore     *store;
318         GossipContactListStorePriv *priv;
319         GList                      *contacts, *l;
320
321         g_return_val_if_fail (EMPATHY_IS_CONTACT_LIST (list_iface), NULL);
322
323         store = g_object_new (GOSSIP_TYPE_CONTACT_LIST_STORE, NULL);
324         priv = GET_PRIV (store);
325
326         contact_list_store_setup (store);
327         priv->list = g_object_ref (list_iface);
328
329         /* Signal connection. */
330         g_signal_connect (priv->list,
331                           "contact-added",
332                           G_CALLBACK (contact_list_store_contact_added_cb),
333                           store);
334         g_signal_connect (priv->list,
335                           "contact-removed",
336                           G_CALLBACK (contact_list_store_contact_removed_cb),
337                           store);
338
339         /* Add contacts already created */
340         contacts = empathy_contact_list_get_contacts (priv->list);
341         for (l = contacts; l; l = l->next) {
342                 GossipContact *contact;
343
344                 contact = l->data;
345
346                 contact_list_store_contact_added_cb (priv->list, contact, store);
347
348                 g_object_unref (contact);
349         }
350         g_list_free (contacts);
351
352         return store;
353 }
354
355 EmpathyContactList *
356 gossip_contact_list_store_get_list_iface (GossipContactListStore *store)
357 {
358         GossipContactListStorePriv *priv;
359
360         g_return_val_if_fail (GOSSIP_IS_CONTACT_LIST_STORE (store), FALSE);
361
362         priv = GET_PRIV (store);
363
364         return priv->list;
365 }
366
367 gboolean
368 gossip_contact_list_store_get_show_offline (GossipContactListStore *store)
369 {
370         GossipContactListStorePriv *priv;
371
372         g_return_val_if_fail (GOSSIP_IS_CONTACT_LIST_STORE (store), FALSE);
373
374         priv = GET_PRIV (store);
375
376         return priv->show_offline;
377 }
378
379 void
380 gossip_contact_list_store_set_show_offline (GossipContactListStore *store,
381                                             gboolean                show_offline)
382 {
383         GossipContactListStorePriv *priv;
384         GList                      *contacts, *l;
385         gboolean                    show_active;
386
387         g_return_if_fail (GOSSIP_IS_CONTACT_LIST_STORE (store));
388
389         priv = GET_PRIV (store);
390
391         priv->show_offline = show_offline;
392         show_active = priv->show_active;
393
394         /* Disable temporarily. */
395         priv->show_active = FALSE;
396
397         contacts = empathy_contact_list_get_contacts (priv->list);
398         for (l = contacts; l; l = l->next) {
399                 GossipContact *contact;
400
401                 contact = GOSSIP_CONTACT (l->data);
402
403                 contact_list_store_contact_update (store, contact);
404                 
405                 g_object_unref (contact);
406         }
407         g_list_free (contacts);
408
409         /* Restore to original setting. */
410         priv->show_active = show_active;
411 }
412
413 gboolean
414 gossip_contact_list_store_get_show_avatars (GossipContactListStore *store)
415 {
416         GossipContactListStorePriv *priv;
417
418         g_return_val_if_fail (GOSSIP_IS_CONTACT_LIST_STORE (store), TRUE);
419
420         priv = GET_PRIV (store);
421
422         return priv->show_avatars;
423 }
424
425 void
426 gossip_contact_list_store_set_show_avatars (GossipContactListStore *store,
427                                             gboolean                show_avatars)
428 {
429         GossipContactListStorePriv *priv;
430         GtkTreeModel               *model;
431
432         g_return_if_fail (GOSSIP_IS_CONTACT_LIST_STORE (store));
433
434         priv = GET_PRIV (store);
435
436         priv->show_avatars = show_avatars;
437
438         model = GTK_TREE_MODEL (store);
439
440         gtk_tree_model_foreach (model,
441                                 (GtkTreeModelForeachFunc)
442                                 contact_list_store_update_list_mode_foreach,
443                                 store);
444 }
445
446 gboolean
447 gossip_contact_list_store_get_is_compact (GossipContactListStore *store)
448 {
449         GossipContactListStorePriv *priv;
450
451         g_return_val_if_fail (GOSSIP_IS_CONTACT_LIST_STORE (store), TRUE);
452
453         priv = GET_PRIV (store);
454
455         return priv->is_compact;
456 }
457
458 void
459 gossip_contact_list_store_set_is_compact (GossipContactListStore *store,
460                                           gboolean                is_compact)
461 {
462         GossipContactListStorePriv *priv;
463         GtkTreeModel               *model;
464
465         g_return_if_fail (GOSSIP_IS_CONTACT_LIST_STORE (store));
466
467         priv = GET_PRIV (store);
468
469         priv->is_compact = is_compact;
470
471         model = GTK_TREE_MODEL (store);
472
473         gtk_tree_model_foreach (model,
474                                 (GtkTreeModelForeachFunc)
475                                 contact_list_store_update_list_mode_foreach,
476                                 store);
477 }
478
479 GossipContactListStoreSort
480 gossip_contact_list_store_get_sort_criterium (GossipContactListStore *store)
481 {
482         GossipContactListStorePriv *priv;
483
484         g_return_val_if_fail (GOSSIP_IS_CONTACT_LIST_STORE (store), 0);
485
486         priv = GET_PRIV (store);
487
488         return priv->sort_criterium;
489 }
490
491 void
492 gossip_contact_list_store_set_sort_criterium (GossipContactListStore     *store,
493                                               GossipContactListStoreSort  sort_criterium)
494 {
495         GossipContactListStorePriv *priv;
496
497         g_return_if_fail (GOSSIP_IS_CONTACT_LIST_STORE (store));
498
499         priv = GET_PRIV (store);
500
501         priv->sort_criterium = sort_criterium;
502
503         switch (sort_criterium) {
504         case GOSSIP_CONTACT_LIST_STORE_SORT_STATE:
505                 gtk_tree_sortable_set_sort_column_id (GTK_TREE_SORTABLE (store),
506                                                       COL_STATUS,
507                                                       GTK_SORT_ASCENDING);
508                 break;
509                 
510         case GOSSIP_CONTACT_LIST_STORE_SORT_NAME:
511                 gtk_tree_sortable_set_sort_column_id (GTK_TREE_SORTABLE (store),
512                                                       COL_NAME,
513                                                       GTK_SORT_ASCENDING);
514                 break;
515         }
516 }
517
518 gboolean
519 gossip_contact_list_store_row_separator_func (GtkTreeModel *model,
520                                               GtkTreeIter  *iter,
521                                               gpointer      data)
522 {
523         gboolean is_separator = FALSE;
524
525         g_return_val_if_fail (GTK_IS_TREE_MODEL (model), FALSE);
526
527         gtk_tree_model_get (model, iter,
528                             COL_IS_SEPARATOR, &is_separator,
529                             -1);
530
531         return is_separator;
532 }
533
534 gchar *
535 gossip_contact_list_store_get_parent_group (GtkTreeModel *model,
536                                             GtkTreePath  *path,
537                                             gboolean     *path_is_group)
538 {
539         GtkTreeIter  parent_iter, iter;
540         gchar       *name = NULL;
541         gboolean     is_group;
542
543         g_return_val_if_fail (GTK_IS_TREE_MODEL (model), NULL);
544
545         if (path_is_group) {
546                 *path_is_group = FALSE;
547         }
548
549         if (!gtk_tree_model_get_iter (model, &iter, path)) {
550                 return NULL;
551         }
552
553         gtk_tree_model_get (model, &iter,
554                             COL_IS_GROUP, &is_group,
555                             COL_NAME, &name,
556                             -1);
557
558         if (!is_group) {
559                 g_free (name);
560                 name = NULL;
561
562                 if (!gtk_tree_model_iter_parent (model, &parent_iter, &iter)) {
563                         return NULL;
564                 }
565
566                 iter = parent_iter;
567
568                 gtk_tree_model_get (model, &iter,
569                                     COL_IS_GROUP, &is_group,
570                                     COL_NAME, &name,
571                                     -1);
572                 if (!is_group) {
573                         g_free (name);
574                         return NULL;
575                 }
576         }
577
578         if (path_is_group) {
579                 *path_is_group = TRUE;
580         }
581
582         return name;
583 }
584
585 gboolean
586 gossip_contact_list_store_search_equal_func (GtkTreeModel *model,
587                                              gint          column,
588                                              const gchar  *key,
589                                              GtkTreeIter  *iter,
590                                              gpointer      search_data)
591 {
592         gchar    *name, *name_folded;
593         gchar    *key_folded;
594         gboolean  ret;
595
596         g_return_val_if_fail (GTK_IS_TREE_MODEL (model), FALSE);
597
598         if (!key) {
599                 return FALSE;
600         }
601
602         gtk_tree_model_get (model, iter, COL_NAME, &name, -1);
603
604         if (!name) {
605                 return FALSE;
606         }
607
608         name_folded = g_utf8_casefold (name, -1);
609         key_folded = g_utf8_casefold (key, -1);
610
611         if (name_folded && key_folded && 
612             strstr (name_folded, key_folded)) {
613                 ret = FALSE;
614         } else {
615                 ret = TRUE;
616         }
617
618         g_free (name);
619         g_free (name_folded);
620         g_free (key_folded);
621
622         return ret;
623 }
624
625 static void
626 contact_list_store_setup (GossipContactListStore *store)
627 {
628         GossipContactListStorePriv *priv;
629         GType                       types[] = {G_TYPE_STRING,       /* Status icon-name */
630                                                GDK_TYPE_PIXBUF,     /* Avatar pixbuf */
631                                                G_TYPE_BOOLEAN,      /* Avatar pixbuf visible */
632                                                G_TYPE_STRING,       /* Name */
633                                                G_TYPE_STRING,       /* Status string */
634                                                G_TYPE_BOOLEAN,      /* Show status */
635                                                GOSSIP_TYPE_CONTACT, /* Contact type */
636                                                G_TYPE_BOOLEAN,      /* Is group */
637                                                G_TYPE_BOOLEAN,      /* Is active */
638                                                G_TYPE_BOOLEAN,      /* Is online */
639                                                G_TYPE_BOOLEAN};     /* Is separator */
640         
641         priv = GET_PRIV (store);
642
643         gtk_tree_store_set_column_types (GTK_TREE_STORE (store), COL_COUNT, types);
644
645         /* Set up sorting */
646         gtk_tree_sortable_set_sort_func (GTK_TREE_SORTABLE (store),
647                                          COL_NAME,
648                                          contact_list_store_name_sort_func,
649                                          store, NULL);
650         gtk_tree_sortable_set_sort_func (GTK_TREE_SORTABLE (store),
651                                          COL_STATUS,
652                                          contact_list_store_state_sort_func,
653                                          store, NULL);
654
655         gossip_contact_list_store_set_sort_criterium (store, priv->sort_criterium);
656 }
657
658 static void
659 contact_list_store_contact_added_cb (EmpathyContactList     *list_iface,
660                                      GossipContact          *contact,
661                                      GossipContactListStore *store)
662 {
663         GossipContactListStorePriv *priv;
664
665         priv = GET_PRIV (store);
666
667         gossip_debug (DEBUG_DOMAIN, 
668                       "Contact:'%s' added",
669                       gossip_contact_get_name (contact));
670
671         g_signal_connect (contact, "notify::groups",
672                           G_CALLBACK (contact_list_store_contact_groups_updated_cb),
673                           store);
674         g_signal_connect (contact, "notify::presence",
675                           G_CALLBACK (contact_list_store_contact_updated_cb),
676                           store);
677         g_signal_connect (contact, "notify::name",
678                           G_CALLBACK (contact_list_store_contact_updated_cb),
679                           store);
680         g_signal_connect (contact, "notify::avatar",
681                           G_CALLBACK (contact_list_store_contact_updated_cb),
682                           store);
683         g_signal_connect (contact, "notify::type",
684                           G_CALLBACK (contact_list_store_contact_updated_cb),
685                           store);
686
687         contact_list_store_add_contact (store, contact);
688 }
689
690 static void
691 contact_list_store_add_contact (GossipContactListStore *store,
692                                 GossipContact          *contact)
693 {
694         GossipContactListStorePriv *priv;
695         GtkTreeIter                 iter;
696         GList                      *groups, *l;
697
698         priv = GET_PRIV (store);
699         
700         if (!priv->show_offline && !gossip_contact_is_online (contact)) {
701                 return;
702         }
703
704         /* If no groups just add it at the top level. */
705         groups = gossip_contact_get_groups (contact);
706         if (!groups) {
707                 gtk_tree_store_append (GTK_TREE_STORE (store), &iter, NULL);
708                 gtk_tree_store_set (GTK_TREE_STORE (store), &iter,
709                                     COL_CONTACT, contact,
710                                     -1);
711         }
712
713         /* Else add to each group. */
714         for (l = groups; l; l = l->next) {
715                 GtkTreeIter  iter_group;
716                 const gchar *name;
717
718                 name = l->data;
719                 if (!name) {
720                         continue;
721                 }
722
723                 contact_list_store_get_group (store, name, &iter_group, NULL, NULL);
724
725                 gtk_tree_store_insert_after (GTK_TREE_STORE (store), &iter,
726                                              &iter_group, NULL);
727                 gtk_tree_store_set (GTK_TREE_STORE (store), &iter,
728                                     COL_CONTACT, contact,
729                                     -1);
730         }
731
732         contact_list_store_contact_update (store, contact);
733 }
734
735 static void
736 contact_list_store_contact_removed_cb (EmpathyContactList     *list_iface,
737                                        GossipContact          *contact,
738                                        GossipContactListStore *store)
739 {
740         gossip_debug (DEBUG_DOMAIN, "Contact:'%s' removed",
741                       gossip_contact_get_name (contact));
742
743         /* Disconnect signals */
744         g_signal_handlers_disconnect_by_func (contact, 
745                                               G_CALLBACK (contact_list_store_contact_groups_updated_cb),
746                                               store);
747         g_signal_handlers_disconnect_by_func (contact,
748                                               G_CALLBACK (contact_list_store_contact_updated_cb),
749                                               store);
750
751         contact_list_store_remove_contact (store, contact);
752 }
753
754 static void
755 contact_list_store_remove_contact (GossipContactListStore *store,
756                                    GossipContact          *contact)
757 {
758         GossipContactListStorePriv *priv;
759         GtkTreeModel               *model;
760         GList                      *iters, *l;
761
762         priv = GET_PRIV (store);
763
764         iters = contact_list_store_find_contact (store, contact);
765         if (!iters) {
766                 return;
767         }
768         
769         /* Clean up model */
770         model = GTK_TREE_MODEL (store);
771
772         for (l = iters; l; l = l->next) {
773                 GtkTreeIter parent;
774
775                 /* NOTE: it is only <= 2 here because we have
776                  * separators after the group name, otherwise it
777                  * should be 1. 
778                  */
779                 if (gtk_tree_model_iter_parent (model, &parent, l->data) &&
780                     gtk_tree_model_iter_n_children (model, &parent) <= 2) {
781                         gtk_tree_store_remove (GTK_TREE_STORE (store), &parent);
782                 } else {
783                         gtk_tree_store_remove (GTK_TREE_STORE (store), l->data);
784                 }
785         }
786
787         g_list_foreach (iters, (GFunc) gtk_tree_iter_free, NULL);
788         g_list_free (iters);
789 }
790
791 static void
792 contact_list_store_contact_update (GossipContactListStore *store,
793                                    GossipContact          *contact)
794 {
795         GossipContactListStorePriv *priv;
796         ShowActiveData             *data;
797         GtkTreeModel               *model;
798         GList                      *iters, *l;
799         gboolean                    in_list;
800         gboolean                    should_be_in_list;
801         gboolean                    was_online = TRUE;
802         gboolean                    now_online = FALSE;
803         gboolean                    set_model = FALSE;
804         gboolean                    do_remove = FALSE;
805         gboolean                    do_set_active = FALSE;
806         gboolean                    do_set_refresh = FALSE;
807         GdkPixbuf                  *pixbuf_avatar;
808
809         priv = GET_PRIV (store);
810
811         model = GTK_TREE_MODEL (store);
812
813         iters = contact_list_store_find_contact (store, contact);
814         if (!iters) {
815                 in_list = FALSE;
816         } else {
817                 in_list = TRUE;
818         }
819
820         /* Get online state now. */
821         now_online = gossip_contact_is_online (contact);
822
823         if (priv->show_offline || now_online) {
824                 should_be_in_list = TRUE;
825         } else {
826                 should_be_in_list = FALSE;
827         }
828
829         if (!in_list && !should_be_in_list) {
830                 /* Nothing to do. */
831                 gossip_debug (DEBUG_DOMAIN,
832                               "Contact:'%s' in list:NO, should be:NO",
833                               gossip_contact_get_name (contact));
834
835                 g_list_foreach (iters, (GFunc) gtk_tree_iter_free, NULL);
836                 g_list_free (iters);
837                 return;
838         }
839         else if (in_list && !should_be_in_list) {
840                 gossip_debug (DEBUG_DOMAIN,
841                               "Contact:'%s' in list:YES, should be:NO",
842                               gossip_contact_get_name (contact));
843
844                 if (priv->show_active) {
845                         do_remove = TRUE;
846                         do_set_active = TRUE;
847                         do_set_refresh = TRUE;
848
849                         set_model = TRUE;
850                         gossip_debug (DEBUG_DOMAIN, "Remove item (after timeout)");
851                 } else {
852                         gossip_debug (DEBUG_DOMAIN, "Remove item (now)!");
853                         contact_list_store_remove_contact (store, contact);
854                 }
855         }
856         else if (!in_list && should_be_in_list) {
857                 gossip_debug (DEBUG_DOMAIN,
858                               "Contact:'%s' in list:NO, should be:YES",
859                               gossip_contact_get_name (contact));
860
861                 contact_list_store_add_contact (store, contact);
862
863                 if (priv->show_active) {
864                         do_set_active = TRUE;
865
866                         gossip_debug (DEBUG_DOMAIN, "Set active (contact added)");
867                 }
868         } else {
869                 gossip_debug (DEBUG_DOMAIN,
870                               "Contact:'%s' in list:YES, should be:YES",
871                               gossip_contact_get_name (contact));
872
873                 /* Get online state before. */
874                 if (iters && g_list_length (iters) > 0) {
875                         gtk_tree_model_get (model, iters->data,
876                                             COL_IS_ONLINE, &was_online,
877                                             -1);
878                 }
879
880                 /* Is this really an update or an online/offline. */
881                 if (priv->show_active) {
882                         if (was_online != now_online) {
883                                 do_set_active = TRUE;
884                                 do_set_refresh = TRUE;
885
886                                 gossip_debug (DEBUG_DOMAIN, "Set active (contact updated %s)",
887                                               was_online ? "online  -> offline" :
888                                                            "offline -> online");
889                         } else {
890                                 /* Was TRUE for presence updates. */
891                                 /* do_set_active = FALSE;  */
892                                 do_set_refresh = TRUE;
893
894                                 gossip_debug (DEBUG_DOMAIN, "Set active (contact updated)");
895                         }
896                 }
897
898                 set_model = TRUE;
899         }
900
901         pixbuf_avatar = gossip_pixbuf_avatar_from_contact_scaled (contact, 32, 32);
902         for (l = iters; l && set_model; l = l->next) {
903                 gtk_tree_store_set (GTK_TREE_STORE (store), l->data,
904                                     COL_ICON_STATUS, gossip_icon_name_for_contact (contact),
905                                     COL_PIXBUF_AVATAR, pixbuf_avatar,
906                                     COL_PIXBUF_AVATAR_VISIBLE, priv->show_avatars,
907                                     COL_NAME, gossip_contact_get_name (contact),
908                                     COL_STATUS, gossip_contact_get_status (contact),
909                                     COL_STATUS_VISIBLE, !priv->is_compact,
910                                     COL_IS_GROUP, FALSE,
911                                     COL_IS_ONLINE, now_online,
912                                     COL_IS_SEPARATOR, FALSE,
913                                     -1);
914         }
915
916         if (pixbuf_avatar) {
917                 g_object_unref (pixbuf_avatar);
918         }
919
920         if (priv->show_active && do_set_active) {
921                 contact_list_store_contact_set_active (store, contact, do_set_active, do_set_refresh);
922
923                 if (do_set_active) {
924                         data = contact_list_store_contact_active_new (store, contact, do_remove);
925                         g_timeout_add (ACTIVE_USER_SHOW_TIME,
926                                        (GSourceFunc) contact_list_store_contact_active_cb,
927                                        data);
928                 }
929         }
930
931         /* FIXME: when someone goes online then offline quickly, the
932          * first timeout sets the user to be inactive and the second
933          * timeout removes the user from the contact list, really we
934          * should remove the first timeout.
935          */
936         g_list_foreach (iters, (GFunc) gtk_tree_iter_free, NULL);
937         g_list_free (iters);
938 }
939
940 static void
941 contact_list_store_contact_groups_updated_cb (GossipContact          *contact,
942                                               GParamSpec             *param,
943                                               GossipContactListStore *store)
944 {
945         gossip_debug (DEBUG_DOMAIN, "Contact:'%s' groups updated",
946                       gossip_contact_get_name (contact));
947
948         /* We do this to make sure the groups are correct, if not, we
949          * would have to check the groups already set up for each
950          * contact and then see what has been updated.
951          */
952         contact_list_store_remove_contact (store, contact);
953         contact_list_store_add_contact (store, contact);
954 }
955
956 static void
957 contact_list_store_contact_updated_cb (GossipContact          *contact,
958                                        GParamSpec             *param,
959                                        GossipContactListStore *store)
960 {
961         gossip_debug (DEBUG_DOMAIN,
962                       "Contact:'%s' updated, checking roster is in sync...",
963                       gossip_contact_get_name (contact));
964
965         contact_list_store_contact_update (store, contact);
966 }
967
968 static void
969 contact_list_store_contact_set_active (GossipContactListStore *store,
970                                        GossipContact          *contact,
971                                        gboolean                active,
972                                        gboolean                set_changed)
973 {
974         GossipContactListStorePriv *priv;
975         GtkTreeModel               *model;
976         GList                      *iters, *l;
977
978         priv = GET_PRIV (store);
979         model = GTK_TREE_MODEL (store);
980
981         iters = contact_list_store_find_contact (store, contact);
982         for (l = iters; l; l = l->next) {
983                 GtkTreePath *path;
984
985                 gtk_tree_store_set (GTK_TREE_STORE (store), l->data,
986                                     COL_IS_ACTIVE, active,
987                                     -1);
988
989                 gossip_debug (DEBUG_DOMAIN, "Set item %s", active ? "active" : "inactive");
990
991                 if (set_changed) {
992                         path = gtk_tree_model_get_path (model, l->data);
993                         gtk_tree_model_row_changed (model, path, l->data);
994                         gtk_tree_path_free (path);
995                 }
996         }
997
998         g_list_foreach (iters, (GFunc) gtk_tree_iter_free, NULL);
999         g_list_free (iters);
1000
1001 }
1002
1003 static ShowActiveData *
1004 contact_list_store_contact_active_new (GossipContactListStore *store,
1005                                        GossipContact          *contact,
1006                                        gboolean                remove)
1007 {
1008         ShowActiveData *data;
1009
1010         gossip_debug (DEBUG_DOMAIN, 
1011                       "Contact:'%s' now active, and %s be removed",
1012                       gossip_contact_get_name (contact), 
1013                       remove ? "WILL" : "WILL NOT");
1014         
1015         data = g_slice_new0 (ShowActiveData);
1016
1017         data->store = g_object_ref (store);
1018         data->contact = g_object_ref (contact);
1019         data->remove = remove;
1020
1021         return data;
1022 }
1023
1024 static void
1025 contact_list_store_contact_active_free (ShowActiveData *data)
1026 {
1027         g_object_unref (data->contact);
1028         g_object_unref (data->store);
1029
1030         g_slice_free (ShowActiveData, data);
1031 }
1032
1033 static gboolean
1034 contact_list_store_contact_active_cb (ShowActiveData *data)
1035 {
1036         GossipContactListStorePriv *priv;
1037
1038         priv = GET_PRIV (data->store);
1039
1040         if (data->remove &&
1041             !priv->show_offline &&
1042             !gossip_contact_is_online (data->contact)) {
1043                 gossip_debug (DEBUG_DOMAIN, 
1044                               "Contact:'%s' active timeout, removing item",
1045                               gossip_contact_get_name (data->contact));
1046                 contact_list_store_remove_contact (data->store, data->contact);
1047         }
1048
1049         gossip_debug (DEBUG_DOMAIN, 
1050                       "Contact:'%s' no longer active",
1051                       gossip_contact_get_name (data->contact));
1052
1053         contact_list_store_contact_set_active (data->store,
1054                                                data->contact,
1055                                                FALSE,
1056                                                TRUE);
1057
1058         contact_list_store_contact_active_free (data);
1059
1060         return FALSE;
1061 }
1062
1063 static gboolean
1064 contact_list_store_get_group_foreach (GtkTreeModel *model,
1065                                       GtkTreePath  *path,
1066                                       GtkTreeIter  *iter,
1067                                       FindGroup    *fg)
1068 {
1069         gchar    *str;
1070         gboolean  is_group;
1071
1072         /* Groups are only at the top level. */
1073         if (gtk_tree_path_get_depth (path) != 1) {
1074                 return FALSE;
1075         }
1076
1077         gtk_tree_model_get (model, iter,
1078                             COL_NAME, &str,
1079                             COL_IS_GROUP, &is_group,
1080                             -1);
1081
1082         if (is_group && strcmp (str, fg->name) == 0) {
1083                 fg->found = TRUE;
1084                 fg->iter = *iter;
1085         }
1086
1087         g_free (str);
1088
1089         return fg->found;
1090 }
1091
1092 static void
1093 contact_list_store_get_group (GossipContactListStore *store,
1094                               const gchar       *name,
1095                               GtkTreeIter       *iter_group_to_set,
1096                               GtkTreeIter       *iter_separator_to_set,
1097                               gboolean          *created)
1098 {
1099         GossipContactListStorePriv *priv;
1100         GtkTreeModel               *model;
1101         GtkTreeIter                 iter_group;
1102         GtkTreeIter                 iter_separator;
1103         FindGroup                   fg;
1104
1105         priv = GET_PRIV (store);
1106
1107         memset (&fg, 0, sizeof (fg));
1108
1109         fg.name = name;
1110
1111         model = GTK_TREE_MODEL (store);
1112         gtk_tree_model_foreach (model,
1113                                 (GtkTreeModelForeachFunc) contact_list_store_get_group_foreach,
1114                                 &fg);
1115
1116         if (!fg.found) {
1117                 if (created) {
1118                         *created = TRUE;
1119                 }
1120
1121                 gtk_tree_store_append (GTK_TREE_STORE (store), &iter_group, NULL);
1122                 gtk_tree_store_set (GTK_TREE_STORE (store), &iter_group,
1123                                     COL_ICON_STATUS, NULL,
1124                                     COL_NAME, name,
1125                                     COL_IS_GROUP, TRUE,
1126                                     COL_IS_ACTIVE, FALSE,
1127                                     COL_IS_SEPARATOR, FALSE,
1128                                     -1);
1129
1130                 if (iter_group_to_set) {
1131                         *iter_group_to_set = iter_group;
1132                 }
1133
1134                 gtk_tree_store_append (GTK_TREE_STORE (store),
1135                                        &iter_separator, 
1136                                        &iter_group);
1137                 gtk_tree_store_set (GTK_TREE_STORE (store), &iter_separator,
1138                                     COL_IS_SEPARATOR, TRUE,
1139                                     -1);
1140
1141                 if (iter_separator_to_set) {
1142                         *iter_separator_to_set = iter_separator;
1143                 }
1144         } else {
1145                 if (created) {
1146                         *created = FALSE;
1147                 }
1148
1149                 if (iter_group_to_set) {
1150                         *iter_group_to_set = fg.iter;
1151                 }
1152
1153                 iter_separator = fg.iter;
1154
1155                 if (gtk_tree_model_iter_next (model, &iter_separator)) {
1156                         gboolean is_separator;
1157
1158                         gtk_tree_model_get (model, &iter_separator,
1159                                             COL_IS_SEPARATOR, &is_separator,
1160                                             -1);
1161
1162                         if (is_separator && iter_separator_to_set) {
1163                                 *iter_separator_to_set = iter_separator;
1164                         }
1165                 }
1166         }
1167 }
1168
1169 static gint
1170 contact_list_store_state_sort_func (GtkTreeModel *model,
1171                                     GtkTreeIter  *iter_a,
1172                                     GtkTreeIter  *iter_b,
1173                                     gpointer      user_data)
1174 {
1175         gint            ret_val = 0;
1176         gchar          *name_a, *name_b;
1177         gboolean        is_separator_a, is_separator_b;
1178         GossipContact  *contact_a, *contact_b;
1179         GossipPresence *presence_a, *presence_b;
1180         McPresence      state_a, state_b;
1181
1182         gtk_tree_model_get (model, iter_a,
1183                             COL_NAME, &name_a,
1184                             COL_CONTACT, &contact_a,
1185                             COL_IS_SEPARATOR, &is_separator_a,
1186                             -1);
1187         gtk_tree_model_get (model, iter_b,
1188                             COL_NAME, &name_b,
1189                             COL_CONTACT, &contact_b,
1190                             COL_IS_SEPARATOR, &is_separator_b,
1191                             -1);
1192
1193         /* Separator or group? */
1194         if (is_separator_a || is_separator_b) {
1195                 if (is_separator_a) {
1196                         ret_val = -1;
1197                 } else if (is_separator_b) {
1198                         ret_val = 1;
1199                 }
1200         } else if (!contact_a && contact_b) {
1201                 ret_val = 1;
1202         } else if (contact_a && !contact_b) {
1203                 ret_val = -1;
1204         } else if (!contact_a && !contact_b) {
1205                 /* Handle groups */
1206                 ret_val = g_utf8_collate (name_a, name_b);
1207         }
1208
1209         if (ret_val) {
1210                 goto free_and_out;
1211         }
1212
1213         /* If we managed to get this far, we can start looking at
1214          * the presences.
1215          */
1216         presence_a = gossip_contact_get_presence (GOSSIP_CONTACT (contact_a));
1217         presence_b = gossip_contact_get_presence (GOSSIP_CONTACT (contact_b));
1218
1219         if (!presence_a && presence_b) {
1220                 ret_val = 1;
1221         } else if (presence_a && !presence_b) {
1222                 ret_val = -1;
1223         } else if (!presence_a && !presence_b) {
1224                 /* Both offline, sort by name */
1225                 ret_val = g_utf8_collate (name_a, name_b);
1226         } else {
1227                 state_a = gossip_presence_get_state (presence_a);
1228                 state_b = gossip_presence_get_state (presence_b);
1229
1230                 if (state_a < state_b) {
1231                         ret_val = -1;
1232                 } else if (state_a > state_b) {
1233                         ret_val = 1;
1234                 } else {
1235                         /* Fallback: compare by name */
1236                         ret_val = g_utf8_collate (name_a, name_b);
1237                 }
1238         }
1239
1240 free_and_out:
1241         g_free (name_a);
1242         g_free (name_b);
1243
1244         if (contact_a) {
1245                 g_object_unref (contact_a);
1246         }
1247
1248         if (contact_b) {
1249                 g_object_unref (contact_b);
1250         }
1251
1252         return ret_val;
1253 }
1254
1255 static gint
1256 contact_list_store_name_sort_func (GtkTreeModel *model,
1257                                    GtkTreeIter  *iter_a,
1258                                    GtkTreeIter  *iter_b,
1259                                    gpointer      user_data)
1260 {
1261         gchar         *name_a, *name_b;
1262         GossipContact *contact_a, *contact_b;
1263         gboolean       is_separator_a, is_separator_b;
1264         gint           ret_val;
1265
1266         gtk_tree_model_get (model, iter_a,
1267                             COL_NAME, &name_a,
1268                             COL_CONTACT, &contact_a,
1269                             COL_IS_SEPARATOR, &is_separator_a,
1270                             -1);
1271         gtk_tree_model_get (model, iter_b,
1272                             COL_NAME, &name_b,
1273                             COL_CONTACT, &contact_b,
1274                             COL_IS_SEPARATOR, &is_separator_b,
1275                             -1);
1276
1277         /* If contact is NULL it means it's a group. */
1278
1279         if (is_separator_a || is_separator_b) {
1280                 if (is_separator_a) {
1281                         ret_val = -1;
1282                 } else if (is_separator_b) {
1283                         ret_val = 1;
1284                 }
1285         } else if (!contact_a && contact_b) {
1286                 ret_val = 1;
1287         } else if (contact_a && !contact_b) {
1288                 ret_val = -1;
1289         } else {
1290                 ret_val = g_utf8_collate (name_a, name_b);
1291         }
1292
1293         g_free (name_a);
1294         g_free (name_b);
1295
1296         if (contact_a) {
1297                 g_object_unref (contact_a);
1298         }
1299
1300         if (contact_b) {
1301                 g_object_unref (contact_b);
1302         }
1303
1304         return ret_val;
1305 }
1306
1307 static gboolean
1308 contact_list_store_find_contact_foreach (GtkTreeModel *model,
1309                                          GtkTreePath  *path,
1310                                          GtkTreeIter  *iter,
1311                                          FindContact  *fc)
1312 {
1313         GossipContact *contact;
1314
1315         gtk_tree_model_get (model, iter,
1316                             COL_CONTACT, &contact,
1317                             -1);
1318
1319         if (!contact) {
1320                 return FALSE;
1321         }
1322
1323         if (gossip_contact_equal (contact, fc->contact)) {
1324                 fc->found = TRUE;
1325                 fc->iters = g_list_append (fc->iters, gtk_tree_iter_copy (iter));
1326         }
1327         g_object_unref (contact);
1328
1329         return FALSE;
1330 }
1331
1332 static GList *
1333 contact_list_store_find_contact (GossipContactListStore *store,
1334                                  GossipContact          *contact)
1335 {
1336         GossipContactListStorePriv *priv;
1337         GtkTreeModel              *model;
1338         GList                     *l = NULL;
1339         FindContact                fc;
1340
1341         priv = GET_PRIV (store);
1342
1343         memset (&fc, 0, sizeof (fc));
1344
1345         fc.contact = contact;
1346
1347         model = GTK_TREE_MODEL (store);
1348         gtk_tree_model_foreach (model,
1349                                 (GtkTreeModelForeachFunc) contact_list_store_find_contact_foreach,
1350                                 &fc);
1351
1352         if (fc.found) {
1353                 l = fc.iters;
1354         }
1355
1356         return l;
1357 }
1358
1359 static gboolean
1360 contact_list_store_update_list_mode_foreach (GtkTreeModel           *model,
1361                                              GtkTreePath            *path,
1362                                              GtkTreeIter            *iter,
1363                                              GossipContactListStore *store)
1364 {
1365         GossipContactListStorePriv *priv;
1366         gboolean                    show_avatar = FALSE;
1367
1368         priv = GET_PRIV (store);
1369
1370         if (priv->show_avatars && !priv->is_compact) {
1371                 show_avatar = TRUE;
1372         }
1373
1374         gtk_tree_store_set (GTK_TREE_STORE (store), iter,
1375                             COL_PIXBUF_AVATAR_VISIBLE, show_avatar,
1376                             COL_STATUS_VISIBLE, !priv->is_compact,
1377                             -1);
1378
1379         return FALSE;
1380 }
1381