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