Merge remote-tracking branch 'burton/aggregator'
authorGuillaume Desmottes <guillaume.desmottes@collabora.co.uk>
Fri, 10 Aug 2012 15:23:52 +0000 (17:23 +0200)
committerGuillaume Desmottes <guillaume.desmottes@collabora.co.uk>
Fri, 10 Aug 2012 15:23:52 +0000 (17:23 +0200)
libempathy-gtk/Makefile.am
libempathy-gtk/empathy-roster-model-aggregator.c [new file with mode: 0644]
libempathy-gtk/empathy-roster-model-aggregator.h [new file with mode: 0644]
tests/interactive/Makefile.am
tests/interactive/test-empathy-roster-model-aggregator.c [new file with mode: 0644]

index 87d7875..902a5e5 100644 (file)
@@ -77,6 +77,7 @@ libempathy_gtk_handwritten_source =                   \
        empathy-roster-contact.c                        \
        empathy-roster-group.c                  \
        empathy-roster-model.c                  \
+       empathy-roster-model-aggregator.c                       \
        empathy-roster-model-manager.c                  \
        empathy-roster-view.c                   \
        empathy-search-bar.c                    \
@@ -148,6 +149,7 @@ libempathy_gtk_headers =                    \
        empathy-roster-contact.h                        \
        empathy-roster-group.h                  \
        empathy-roster-model.h                  \
+       empathy-roster-model-aggregator.h                       \
        empathy-roster-model-manager.h                  \
        empathy-roster-view.h                   \
        empathy-search-bar.h                    \
diff --git a/libempathy-gtk/empathy-roster-model-aggregator.c b/libempathy-gtk/empathy-roster-model-aggregator.c
new file mode 100644 (file)
index 0000000..b0b6844
--- /dev/null
@@ -0,0 +1,425 @@
+/*
+ * empathy-roster-model-aggregator.c
+ *
+ * Implementation of EmpathyRosterModel using FolksIndividualAggregator as
+ * source.
+ *
+ * Copyright (C) 2012 Collabora Ltd. <http://www.collabora.co.uk/>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+
+#include "config.h"
+
+#include <folks/folks.h>
+#include <folks/folks-telepathy.h>
+
+#include "empathy-roster-model-aggregator.h"
+
+/**
+ * SECTION: empathy-roster-model-aggregator
+ * @title: EmpathyRosterModelAggregator
+ * @short_description: TODO
+ *
+ * TODO
+ */
+
+/**
+ * EmpathyRosterModelAggregator:
+ *
+ * Data structure representing a #EmpathyRosterModelAggregator.
+ *
+ * Since: UNRELEASED
+ */
+
+/**
+ * EmpathyRosterModelAggregatorClass:
+ *
+ * The class of a #EmpathyRosterModelAggregator.
+ *
+ * Since: UNRELEASED
+ */
+
+static void roster_model_iface_init (EmpathyRosterModelInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (EmpathyRosterModelAggregator,
+    empathy_roster_model_aggregator,
+    G_TYPE_OBJECT,
+    G_IMPLEMENT_INTERFACE (EMPATHY_TYPE_ROSTER_MODEL, roster_model_iface_init))
+
+enum
+{
+  PROP_AGGREGATOR = 1,
+  PROP_FILTER_FUNC,
+  PROP_FILTER_DATA,
+  N_PROPS
+};
+
+/*
+enum
+{
+  LAST_SIGNAL
+};
+
+static guint signals[LAST_SIGNAL];
+*/
+
+struct _EmpathyRosterModelAggregatorPriv
+{
+  FolksIndividualAggregator *aggregator;
+  GHashTable *filtered_individuals; /* Individual -> Individual */
+
+  EmpathyRosterModelAggregatorFilterFunc filter_func;
+  gpointer filter_data;
+};
+
+static void
+individual_group_changed_cb (FolksIndividual *individual,
+    gchar *group,
+    gboolean is_member,
+    EmpathyRosterModelAggregator *self)
+{
+  empathy_roster_model_fire_groups_changed (EMPATHY_ROSTER_MODEL (self),
+      individual, group, is_member);
+}
+
+static void
+add_to_filtered_individuals (EmpathyRosterModelAggregator *self,
+    FolksIndividual *individual)
+{
+  g_hash_table_add (self->priv->filtered_individuals,
+      g_object_ref (individual));
+
+  tp_g_signal_connect_object (individual, "group-changed",
+      G_CALLBACK (individual_group_changed_cb), self, 0);
+
+  empathy_roster_model_fire_individual_added (EMPATHY_ROSTER_MODEL (self),
+      individual);
+}
+
+static void
+remove_from_filtered_individuals (EmpathyRosterModelAggregator *self,
+    FolksIndividual *individual)
+{
+  g_signal_handlers_disconnect_by_func (individual,
+      individual_group_changed_cb, self);
+
+  g_hash_table_remove (self->priv->filtered_individuals, individual);
+
+  empathy_roster_model_fire_individual_removed (EMPATHY_ROSTER_MODEL (self),
+      individual);
+}
+
+static void
+individual_notify_cb (FolksIndividual *individual,
+    GParamSpec *param,
+    EmpathyRosterModelAggregator *self)
+{
+  if (!self->priv->filter_func (EMPATHY_ROSTER_MODEL (self), individual, self)
+      && g_hash_table_contains (self->priv->filtered_individuals, individual))
+    remove_from_filtered_individuals (self, individual);
+
+  if (self->priv->filter_func (EMPATHY_ROSTER_MODEL (self), individual, self)
+      && !g_hash_table_contains (self->priv->filtered_individuals, individual))
+    add_to_filtered_individuals (self, individual);
+}
+
+static void
+add_individual (EmpathyRosterModelAggregator *self,
+    FolksIndividual *individual)
+{
+  if (self->priv->filter_func != NULL)
+    {
+      tp_g_signal_connect_object (individual, "notify",
+          G_CALLBACK (individual_notify_cb), self, 0);
+
+      if (!self->priv->filter_func (EMPATHY_ROSTER_MODEL (self), individual,
+              self))
+        return;
+    }
+
+  add_to_filtered_individuals (self, individual);
+}
+
+static void
+remove_individual (EmpathyRosterModelAggregator *self,
+    FolksIndividual *individual)
+{
+  if (self->priv->filter_func != NULL)
+    g_signal_handlers_disconnect_by_func (individual,
+        individual_notify_cb, self);
+
+  if (g_hash_table_contains (self->priv->filtered_individuals,
+          individual))
+    remove_from_filtered_individuals (self, individual);
+}
+
+static void
+populate_individuals (EmpathyRosterModelAggregator *self)
+{
+  GeeMap *individuals;
+  GeeMapIterator *iter;
+
+  individuals = folks_individual_aggregator_get_individuals (
+      self->priv->aggregator);
+  iter = gee_map_map_iterator (individuals);
+  while (gee_map_iterator_next (iter))
+    {
+      add_individual (self, gee_map_iterator_get_value (iter));
+    }
+  g_clear_object (&iter);
+}
+
+static void
+aggregator_individuals_changed_cb (FolksIndividualAggregator *aggregator,
+    GeeSet *added,
+    GeeSet *removed,
+    gchar *message,
+    FolksPersona *actor,
+    FolksGroupDetailsChangeReason reason,
+    EmpathyRosterModelAggregator *self)
+{
+  if (gee_collection_get_size (GEE_COLLECTION (added)) > 0)
+    {
+      GeeIterator *iter = gee_iterable_iterator (GEE_ITERABLE (added));
+
+      while (iter != NULL && gee_iterator_next (iter))
+        {
+          add_individual (self, gee_iterator_get (iter));
+        }
+      g_clear_object (&iter);
+    }
+
+  if (gee_collection_get_size (GEE_COLLECTION (removed)) > 0)
+    {
+      GeeIterator *iter = gee_iterable_iterator (GEE_ITERABLE (removed));
+
+      while (iter != NULL && gee_iterator_next (iter))
+        {
+          remove_individual (self, gee_iterator_get (iter));
+        }
+      g_clear_object (&iter);
+    }
+}
+
+static void
+empathy_roster_model_aggregator_get_property (GObject *object,
+    guint property_id,
+    GValue *value,
+    GParamSpec *pspec)
+{
+  EmpathyRosterModelAggregator *self = EMPATHY_ROSTER_MODEL_AGGREGATOR (object);
+
+  switch (property_id)
+    {
+      case PROP_AGGREGATOR:
+        g_value_set_object (value, self->priv->aggregator);
+        break;
+      case PROP_FILTER_FUNC:
+        g_value_set_pointer (value, self->priv->filter_func);
+        break;
+      case PROP_FILTER_DATA:
+        g_value_set_pointer (value, self->priv->filter_data);
+        break;
+      default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+        break;
+    }
+}
+
+static void
+empathy_roster_model_aggregator_set_property (GObject *object,
+    guint property_id,
+    const GValue *value,
+    GParamSpec *pspec)
+{
+  EmpathyRosterModelAggregator *self = EMPATHY_ROSTER_MODEL_AGGREGATOR (object);
+
+  switch (property_id)
+    {
+      case PROP_AGGREGATOR:
+        g_assert (self->priv->aggregator == NULL); /* construct only */
+        self->priv->aggregator = g_value_dup_object (value);
+        break;
+      case PROP_FILTER_FUNC:
+        g_assert (self->priv->filter_func == NULL); /* construct only */
+        self->priv->filter_func = g_value_get_pointer (value);
+        break;
+      case PROP_FILTER_DATA:
+        g_assert (self->priv->filter_data == NULL); /* construct only */
+        self->priv->filter_data = g_value_get_pointer (value);
+        break;
+      default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+        break;
+    }
+}
+
+static void
+empathy_roster_model_aggregator_constructed (GObject *object)
+{
+  EmpathyRosterModelAggregator *self = EMPATHY_ROSTER_MODEL_AGGREGATOR (object);
+  void (*chain_up) (GObject *) =
+      ((GObjectClass *) empathy_roster_model_aggregator_parent_class)->constructed;
+
+  if (chain_up != NULL)
+    chain_up (object);
+
+  if (self->priv->aggregator == NULL)
+    self->priv->aggregator = folks_individual_aggregator_new ();
+
+  g_assert (FOLKS_IS_INDIVIDUAL_AGGREGATOR (self->priv->aggregator));
+
+  tp_g_signal_connect_object (self->priv->aggregator, "individuals-changed",
+      G_CALLBACK (aggregator_individuals_changed_cb), self, 0);
+
+  folks_individual_aggregator_prepare (self->priv->aggregator, NULL, NULL);
+
+  populate_individuals (self);
+}
+
+static void
+empathy_roster_model_aggregator_dispose (GObject *object)
+{
+  EmpathyRosterModelAggregator *self = EMPATHY_ROSTER_MODEL_AGGREGATOR (object);
+  void (*chain_up) (GObject *) =
+      ((GObjectClass *) empathy_roster_model_aggregator_parent_class)->dispose;
+
+  g_clear_object (&self->priv->aggregator);
+  g_clear_pointer (&self->priv->filtered_individuals, g_hash_table_unref);
+
+  if (chain_up != NULL)
+    chain_up (object);
+}
+
+static void
+empathy_roster_model_aggregator_finalize (GObject *object)
+{
+  //EmpathyRosterModelAggregator *self = EMPATHY_ROSTER_MODEL_AGGREGATOR (object);
+  void (*chain_up) (GObject *) =
+      ((GObjectClass *) empathy_roster_model_aggregator_parent_class)->finalize;
+
+  if (chain_up != NULL)
+    chain_up (object);
+}
+
+static void
+empathy_roster_model_aggregator_class_init (
+    EmpathyRosterModelAggregatorClass *klass)
+{
+  GObjectClass *oclass = G_OBJECT_CLASS (klass);
+  GParamSpec *spec;
+
+  oclass->get_property = empathy_roster_model_aggregator_get_property;
+  oclass->set_property = empathy_roster_model_aggregator_set_property;
+  oclass->constructed = empathy_roster_model_aggregator_constructed;
+  oclass->dispose = empathy_roster_model_aggregator_dispose;
+  oclass->finalize = empathy_roster_model_aggregator_finalize;
+
+  spec = g_param_spec_object ("aggregator", "Aggregator",
+      "FolksIndividualAggregator",
+      FOLKS_TYPE_INDIVIDUAL_AGGREGATOR,
+      G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (oclass, PROP_AGGREGATOR, spec);
+
+  spec = g_param_spec_pointer ("filter-func", "Filter-Func",
+      "EmpathyRosterModelAggregatorFilterFunc",
+      G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (oclass, PROP_FILTER_FUNC, spec);
+
+  spec = g_param_spec_pointer ("filter-data", "Filter-Data",
+      "GPointer",
+      G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (oclass, PROP_FILTER_DATA, spec);
+
+  g_type_class_add_private (klass, sizeof (EmpathyRosterModelAggregatorPriv));
+}
+
+static void
+empathy_roster_model_aggregator_init (EmpathyRosterModelAggregator *self)
+{
+  self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
+      EMPATHY_TYPE_ROSTER_MODEL_AGGREGATOR, EmpathyRosterModelAggregatorPriv);
+
+  self->priv->filtered_individuals = g_hash_table_new_full (NULL, NULL, NULL,
+      g_object_unref);
+}
+
+EmpathyRosterModelAggregator *
+empathy_roster_model_aggregator_new (
+    EmpathyRosterModelAggregatorFilterFunc filter_func,
+    gpointer user_data)
+{
+  return g_object_new (EMPATHY_TYPE_ROSTER_MODEL_AGGREGATOR,
+      "filter-func", filter_func,
+      "filter-data", user_data,
+      NULL);
+}
+
+EmpathyRosterModelAggregator *
+empathy_roster_model_aggregator_new_with_aggregator (
+    FolksIndividualAggregator *aggregator,
+    EmpathyRosterModelAggregatorFilterFunc filter_func,
+    gpointer user_data)
+{
+  g_return_val_if_fail (FOLKS_IS_INDIVIDUAL_AGGREGATOR (aggregator), NULL);
+
+  return g_object_new (EMPATHY_TYPE_ROSTER_MODEL_AGGREGATOR,
+      "aggregator", aggregator,
+      "filter-func", filter_func,
+      "filter-data", user_data,
+      NULL);
+}
+
+static GList *
+empathy_roster_model_aggregator_get_individuals (EmpathyRosterModel *model)
+{
+  EmpathyRosterModelAggregator *self = EMPATHY_ROSTER_MODEL_AGGREGATOR (model);
+
+  return g_hash_table_get_values (self->priv->filtered_individuals);
+}
+
+static GList *
+empathy_roster_model_aggregator_get_groups_for_individual (
+    EmpathyRosterModel *model,
+    FolksIndividual *individual)
+{
+  GList *groups_list = NULL;
+  GeeSet *groups_set;
+
+  groups_set = folks_group_details_get_groups (
+      FOLKS_GROUP_DETAILS (individual));
+  if (gee_collection_get_size (GEE_COLLECTION (groups_set)) > 0)
+    {
+      GeeIterator *iter = gee_iterable_iterator (GEE_ITERABLE (groups_set));
+
+      while (iter != NULL && gee_iterator_next (iter))
+        {
+          groups_list = g_list_prepend (groups_list, gee_iterator_get (iter));
+        }
+      g_clear_object (&iter);
+    }
+
+  return groups_list;
+}
+
+static void
+roster_model_iface_init (EmpathyRosterModelInterface *iface)
+{
+  iface->get_individuals = empathy_roster_model_aggregator_get_individuals;
+  iface->get_groups_for_individual =
+    empathy_roster_model_aggregator_get_groups_for_individual;
+}
diff --git a/libempathy-gtk/empathy-roster-model-aggregator.h b/libempathy-gtk/empathy-roster-model-aggregator.h
new file mode 100644 (file)
index 0000000..e29e36e
--- /dev/null
@@ -0,0 +1,93 @@
+/*
+ * empathy-roster-model-aggregator.h
+ *
+ * Copyright (C) 2012 Collabora Ltd. <http://www.collabora.co.uk/>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+
+#ifndef __EMPATHY_ROSTER_MODEL_AGGREGATOR_H__
+#define __EMPATHY_ROSTER_MODEL_AGGREGATOR_H__
+
+#include <glib-object.h>
+
+#include <folks/folks.h>
+
+#include "empathy-roster-model.h"
+
+G_BEGIN_DECLS
+
+typedef struct _EmpathyRosterModelAggregator EmpathyRosterModelAggregator;
+typedef struct _EmpathyRosterModelAggregatorClass
+EmpathyRosterModelAggregatorClass;
+typedef struct _EmpathyRosterModelAggregatorPriv
+EmpathyRosterModelAggregatorPriv;
+
+struct _EmpathyRosterModelAggregatorClass
+{
+  /*<private>*/
+  GObjectClass parent_class;
+};
+
+struct _EmpathyRosterModelAggregator
+{
+  /*<private>*/
+  GObject parent;
+  EmpathyRosterModelAggregatorPriv *priv;
+};
+
+typedef gboolean (* EmpathyRosterModelAggregatorFilterFunc) (
+    EmpathyRosterModel *model,
+    FolksIndividual *individual,
+    gpointer user_data);
+
+GType empathy_roster_model_aggregator_get_type (void);
+
+/* TYPE MACROS */
+#define EMPATHY_TYPE_ROSTER_MODEL_AGGREGATOR \
+  (empathy_roster_model_aggregator_get_type ())
+#define EMPATHY_ROSTER_MODEL_AGGREGATOR(obj) \
+  (G_TYPE_CHECK_INSTANCE_CAST((obj), \
+    EMPATHY_TYPE_ROSTER_MODEL_AGGREGATOR, \
+    EmpathyRosterModelAggregator))
+#define EMPATHY_ROSTER_MODEL_AGGREGATOR_CLASS(klass) \
+  (G_TYPE_CHECK_CLASS_CAST((klass), \
+    EMPATHY_TYPE_ROSTER_MODEL_AGGREGATOR, \
+    EmpathyRosterModelAggregatorClass))
+#define EMPATHY_IS_ROSTER_MODEL_AGGREGATOR(obj) \
+  (G_TYPE_CHECK_INSTANCE_TYPE((obj), \
+    EMPATHY_TYPE_ROSTER_MODEL_AGGREGATOR))
+#define EMPATHY_IS_ROSTER_MODEL_AGGREGATOR_CLASS(klass) \
+  (G_TYPE_CHECK_CLASS_TYPE((klass), \
+    EMPATHY_TYPE_ROSTER_MODEL_AGGREGATOR))
+#define EMPATHY_ROSTER_MODEL_AGGREGATOR_GET_CLASS(obj) \
+  (G_TYPE_INSTANCE_GET_CLASS ((obj), \
+    EMPATHY_TYPE_ROSTER_MODEL_AGGREGATOR, \
+    EmpathyRosterModelAggregatorClass))
+
+EmpathyRosterModelAggregator * empathy_roster_model_aggregator_new (
+    EmpathyRosterModelAggregatorFilterFunc filter_func,
+    gpointer user_data);
+
+EmpathyRosterModelAggregator *
+empathy_roster_model_aggregator_new_with_aggregator (
+    FolksIndividualAggregator *aggregator,
+    EmpathyRosterModelAggregatorFilterFunc filter_func,
+    gpointer user_data);
+
+G_END_DECLS
+
+#endif /* #ifndef __EMPATHY_ROSTER_MODEL_AGGREGATOR_H__*/
index d741f24..27fdbf7 100644 (file)
@@ -20,7 +20,8 @@ noinst_PROGRAMS =                     \
        test-empathy-account-chooser \
        test-empathy-calendar-button \
        test-empathy-roster-view \
-       test-empathy-dual-roster-view
+       test-empathy-dual-roster-view \
+       test-empathy-roster-model-aggregator
 
 empathy_logs_SOURCES = empathy-logs.c
 test_empathy_contact_blocking_dialog_SOURCES = test-empathy-contact-blocking-dialog.c
@@ -31,3 +32,4 @@ test_empathy_account_chooser_SOURCES = test-empathy-account-chooser.c
 test_empathy_calendar_button_SOURCES = test-empathy-calendar-button.c
 test_empathy_roster_view_SOURCES = test-empathy-roster-view.c
 test_empathy_dual_roster_view_SOURCES = test-empathy-dual-roster-view.c
+test_empathy_roster_model_aggregator_SOURCES = test-empathy-roster-model-aggregator.c
diff --git a/tests/interactive/test-empathy-roster-model-aggregator.c b/tests/interactive/test-empathy-roster-model-aggregator.c
new file mode 100644 (file)
index 0000000..49f2012
--- /dev/null
@@ -0,0 +1,164 @@
+#include <config.h>
+
+#include <libempathy-gtk/empathy-roster-model.h>
+#include <libempathy-gtk/empathy-roster-model-aggregator.h>
+
+#include <libempathy-gtk/empathy-roster-view.h>
+#include <libempathy-gtk/empathy-ui-utils.h>
+
+static gboolean show_offline = FALSE;
+static gboolean show_groups = FALSE;
+
+static GOptionEntry entries[] =
+  {
+    { "offline", 0, 0, G_OPTION_ARG_NONE, &show_offline, "Show offline contacts", NULL },
+    { "groups", 0, 0, G_OPTION_ARG_NONE, &show_groups, "Show groups", NULL },
+    { NULL }
+  };
+
+static void
+individual_activated_cb (EmpathyRosterView *self,
+    FolksIndividual *individual,
+    gpointer user_data)
+{
+  g_assert (FOLKS_IS_INDIVIDUAL (individual));
+
+  g_print ("'%s' activated\n",
+      folks_alias_details_get_alias (FOLKS_ALIAS_DETAILS (individual)));
+}
+
+static void
+popup_individual_menu_cb (EmpathyRosterView *self,
+    FolksIndividual *individual,
+    guint button,
+    guint time,
+    gpointer user_data)
+{
+  GtkWidget *menu, *item;
+
+  g_print ("'%s' popup menu\n",
+      folks_alias_details_get_alias (FOLKS_ALIAS_DETAILS (individual)));
+
+  menu = gtk_menu_new ();
+
+  g_signal_connect (menu, "deactivate",
+      G_CALLBACK (gtk_widget_destroy), NULL);
+
+  item = gtk_menu_item_new_with_label (folks_alias_details_get_alias (
+          FOLKS_ALIAS_DETAILS (individual)));
+  gtk_widget_show (item);
+
+  gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
+
+  gtk_menu_attach_to_widget (GTK_MENU (menu), GTK_WIDGET (self), NULL);
+
+  gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL, button, time);
+}
+
+static gboolean
+individual_tooltip_cb (EmpathyRosterView *view,
+    FolksIndividual *individual,
+    gboolean keyboard_mode,
+    GtkTooltip *tooltip,
+    gpointer user_data)
+{
+  gtk_tooltip_set_text (tooltip,
+      folks_alias_details_get_alias (FOLKS_ALIAS_DETAILS (individual)));
+
+  return TRUE;
+}
+
+static void
+empty_cb (EmpathyRosterView *view,
+    GParamSpec *spec,
+    gpointer user_data)
+{
+  if (empathy_roster_view_is_empty (view))
+    g_print ("view is now empty\n");
+  else
+    g_print ("view is no longer empty\n");
+}
+
+static gboolean
+filter (EmpathyRosterModel *model,
+    FolksIndividual *individual,
+    gpointer user_data)
+{
+  if (folks_avatar_details_get_avatar (FOLKS_AVATAR_DETAILS (individual))
+      == NULL)
+    return FALSE;
+
+  return TRUE;
+}
+
+int
+main (int argc,
+    char **argv)
+{
+  GtkWidget *window, *view, *scrolled, *box, *search;
+  GError *error = NULL;
+  GOptionContext *context;
+  EmpathyRosterModel *model;
+
+  gtk_init (&argc, &argv);
+  empathy_gtk_init ();
+
+  context = g_option_context_new ("- test tree model performance");
+  g_option_context_add_main_entries (context, entries, GETTEXT_PACKAGE);
+  g_option_context_add_group (context, gtk_get_option_group (TRUE));
+  if (!g_option_context_parse (context, &argc, &argv, &error))
+    {
+      g_print ("option parsing failed: %s\n", error->message);
+      return 1;
+    }
+
+  window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
+
+  empathy_set_css_provider (window);
+
+  box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 8);
+
+  model = EMPATHY_ROSTER_MODEL (empathy_roster_model_aggregator_new (
+          filter, NULL));
+  view = empathy_roster_view_new (model);
+
+  g_object_unref (model);
+  g_signal_connect (view, "individual-activated",
+      G_CALLBACK (individual_activated_cb), NULL);
+  g_signal_connect (view, "popup-individual-menu",
+      G_CALLBACK (popup_individual_menu_cb), NULL);
+  g_signal_connect (view, "notify::empty",
+      G_CALLBACK (empty_cb), NULL);
+  g_signal_connect (view, "individual-tooltip",
+      G_CALLBACK (individual_tooltip_cb), NULL);
+
+  gtk_widget_set_has_tooltip (view, TRUE);
+
+  empathy_roster_view_show_offline (EMPATHY_ROSTER_VIEW (view), show_offline);
+  empathy_roster_view_show_groups (EMPATHY_ROSTER_VIEW (view), show_groups);
+
+  search = empathy_live_search_new (view);
+  empathy_roster_view_set_live_search (EMPATHY_ROSTER_VIEW (view),
+      EMPATHY_LIVE_SEARCH (search));
+
+  scrolled = gtk_scrolled_window_new (NULL, NULL);
+  gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled),
+      GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
+
+  egg_list_box_add_to_scrolled (EGG_LIST_BOX (view),
+      GTK_SCROLLED_WINDOW (scrolled));
+
+  gtk_box_pack_start (GTK_BOX (box), search, FALSE, TRUE, 0);
+  gtk_box_pack_start (GTK_BOX (box), scrolled, TRUE, TRUE, 0);
+  gtk_container_add (GTK_CONTAINER (window), box);
+
+  gtk_window_set_default_size (GTK_WINDOW (window), 300, 600);
+  gtk_widget_show_all (window);
+
+  g_signal_connect_swapped (window, "destroy",
+      G_CALLBACK (gtk_main_quit), NULL);
+
+  gtk_main ();
+
+  return 0;
+}