[gnome-chess/chess-telepathy-networking-support-664946-rebase: 15/64] [libgames-contacts] Add view to display folks individuals



commit 0db172fff39fdbe807339bbe6b6a1f3db015c59c
Author: Chandni Verma <chandniverma2112 gmail com>
Date:   Wed Jul 11 07:53:58 2012 +0530

    [libgames-contacts] Add view to display folks individuals
    
    Supports:
     - Avatars
     - Presence
     - Individual Groups
     - Individual filteration based of gaming capabilities
     - Contact search

 libgames-contacts/Makefile.am                    |   15 +-
 libgames-contacts/games-cell-renderer-expander.c |  344 ++++++
 libgames-contacts/games-cell-renderer-expander.h |   65 ++
 libgames-contacts/games-cell-renderer-text.c     |  376 +++++++
 libgames-contacts/games-cell-renderer-text.h     |   59 +
 libgames-contacts/games-contact.c                |   13 +-
 libgames-contacts/games-individual-view.c        | 1238 ++++++++++++++++++++++
 libgames-contacts/games-individual-view.h        |  127 +++
 libgames-contacts/games-live-search.c            |  704 ++++++++++++
 libgames-contacts/games-live-search.h            |   81 ++
 libgames-contacts/games-ui-utils.c               |   65 ++
 libgames-contacts/games-ui-utils.h               |    7 +-
 12 files changed, 3085 insertions(+), 9 deletions(-)
---
diff --git a/libgames-contacts/Makefile.am b/libgames-contacts/Makefile.am
index b2e7788..9b3f6f7 100644
--- a/libgames-contacts/Makefile.am
+++ b/libgames-contacts/Makefile.am
@@ -15,7 +15,12 @@ networking_headers = \
 	games-individual-store-manager.h \
 	games-individual-groups.h \
 	games-ui-utils.h \
-	games-contact.h
+	games-contact.h \
+	games-individual-view.h \
+	games-cell-renderer-text.h \
+	games-cell-renderer-expander.h \
+	games-live-search.h
+
 
 networking_sources = \
 	$(networking_headers) \
@@ -24,7 +29,12 @@ networking_sources = \
 	games-individual-store-manager.c \
 	games-individual-groups.c \
 	games-ui-utils.c \
-	games-contact.c
+	games-contact.c \
+	games-individual-view.c \
+	games-cell-renderer-text.c \
+	games-cell-renderer-expander.c \
+	games-live-search.c
+
 
 libgames_contacts_la_SOURCES = \
 	$(networking_sources)
@@ -39,6 +49,7 @@ libgames_contacts_la_CPPFLAGS = \
 	-DICON_THEME_DIRECTORY="\"$(datadir)/icons\""				\
 	-DLOCALE_DIRECTORY="\"$(datadir)/locale\""				\
 	-DGETTEXT_PACKAGE="\"gnome-chess\""				\
+	-DG_LOG_DOMAIN="\"Games\""
 	$(AM_CPPFLAGS)
 
 libgames_contacts_la_CFLAGS = \
diff --git a/libgames-contacts/games-cell-renderer-expander.c b/libgames-contacts/games-cell-renderer-expander.c
new file mode 100644
index 0000000..ec96570
--- /dev/null
+++ b/libgames-contacts/games-cell-renderer-expander.c
@@ -0,0 +1,344 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2012 Chandni Verma
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA  02110-1301  USA
+ */
+
+/*
+ * This code has been influenced by contacts in Empathy, a chat 
+ * application based on Telepathy (https://live.gnome.org/Empathy)
+ * Copyright (C) 2005-2007 Imendio ABdd
+ * Copyright (C) 2007-2011 Collabora Ltd.
+ */
+
+
+#include <gtk/gtk.h>
+
+#include <libgames-contacts/games-ui-utils.h>
+#include "games-cell-renderer-expander.h"
+
+struct _GamesCellRendererExpanderPriv{
+	GtkExpanderStyle     expander_style;
+	gint                 expander_size;
+
+	guint                activatable : 1;
+};
+
+enum {
+	PROP_0,
+	PROP_EXPANDER_STYLE,
+	PROP_EXPANDER_SIZE,
+	PROP_ACTIVATABLE
+};
+
+static void     games_cell_renderer_expander_get_property (GObject                         *object,
+							    guint                            param_id,
+							    GValue                          *value,
+							    GParamSpec                      *pspec);
+static void     games_cell_renderer_expander_set_property (GObject                         *object,
+							    guint                            param_id,
+							    const GValue                    *value,
+							    GParamSpec                      *pspec);
+static void     games_cell_renderer_expander_finalize     (GObject                         *object);
+static void     games_cell_renderer_expander_get_size     (GtkCellRenderer                 *cell,
+							    GtkWidget                       *widget,
+							    const GdkRectangle              *cell_area,
+							    gint                            *x_offset,
+							    gint                            *y_offset,
+							    gint                            *width,
+							    gint                            *height);
+static void     games_cell_renderer_expander_render       (GtkCellRenderer                 *cell,
+							    cairo_t *cr,
+							    GtkWidget                       *widget,
+							    const GdkRectangle              *background_area,
+							    const GdkRectangle              *cell_area,
+							    GtkCellRendererState             flags);
+static gboolean games_cell_renderer_expander_activate     (GtkCellRenderer                 *cell,
+							    GdkEvent                        *event,
+							    GtkWidget                       *widget,
+							    const gchar                     *path,
+							    const GdkRectangle              *background_area,
+							    const GdkRectangle              *cell_area,
+							    GtkCellRendererState             flags);
+
+G_DEFINE_TYPE (GamesCellRendererExpander, games_cell_renderer_expander, GTK_TYPE_CELL_RENDERER)
+
+static void
+games_cell_renderer_expander_init (GamesCellRendererExpander *expander)
+{
+	GamesCellRendererExpanderPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (expander,
+		GAMES_TYPE_CELL_RENDERER_EXPANDER, GamesCellRendererExpanderPriv);
+
+	expander->priv = priv;
+	priv->expander_style = GTK_EXPANDER_COLLAPSED;
+	priv->expander_size = 12;
+	priv->activatable = TRUE;
+
+	g_object_set (expander,
+		      "xpad", 2,
+		      "ypad", 2,
+		      "mode", GTK_CELL_RENDERER_MODE_ACTIVATABLE,
+		      NULL);
+}
+
+static void
+games_cell_renderer_expander_class_init (GamesCellRendererExpanderClass *klass)
+{
+	GObjectClass         *object_class;
+	GtkCellRendererClass *cell_class;
+
+	object_class  = G_OBJECT_CLASS (klass);
+	cell_class = GTK_CELL_RENDERER_CLASS (klass);
+
+	object_class->finalize = games_cell_renderer_expander_finalize;
+
+	object_class->get_property = games_cell_renderer_expander_get_property;
+	object_class->set_property = games_cell_renderer_expander_set_property;
+
+	cell_class->get_size = games_cell_renderer_expander_get_size;
+	cell_class->render = games_cell_renderer_expander_render;
+	cell_class->activate = games_cell_renderer_expander_activate;
+
+	g_object_class_install_property (object_class,
+					 PROP_EXPANDER_STYLE,
+					 g_param_spec_enum ("expander-style",
+							    "Expander Style",
+							    "Style to use when painting the expander",
+							    GTK_TYPE_EXPANDER_STYLE,
+							    GTK_EXPANDER_COLLAPSED,
+							    G_PARAM_READWRITE));
+
+	g_object_class_install_property (object_class,
+					 PROP_EXPANDER_SIZE,
+					 g_param_spec_int ("expander-size",
+							   "Expander Size",
+							   "The size of the expander",
+							   0,
+							   G_MAXINT,
+							   12,
+							   G_PARAM_READWRITE));
+
+	g_object_class_install_property (object_class,
+					 PROP_ACTIVATABLE,
+					 g_param_spec_boolean ("activatable",
+							       "Activatable",
+							       "The expander can be activated",
+							       TRUE,
+							       G_PARAM_READWRITE));
+
+	g_type_class_add_private (object_class, sizeof (GamesCellRendererExpanderPriv));
+}
+
+static void
+games_cell_renderer_expander_get_property (GObject    *object,
+					    guint       param_id,
+					    GValue     *value,
+					    GParamSpec *pspec)
+{
+	GamesCellRendererExpander     *expander;
+
+	expander = GAMES_CELL_RENDERER_EXPANDER (object);
+
+	switch (param_id) {
+	case PROP_EXPANDER_STYLE:
+		g_value_set_enum (value, expander->priv->expander_style);
+		break;
+
+	case PROP_EXPANDER_SIZE:
+		g_value_set_int (value, expander->priv->expander_size);
+		break;
+
+	case PROP_ACTIVATABLE:
+		g_value_set_boolean (value, expander->priv->activatable);
+		break;
+
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
+		break;
+	}
+}
+
+static void
+games_cell_renderer_expander_set_property (GObject      *object,
+					    guint         param_id,
+					    const GValue *value,
+					    GParamSpec   *pspec)
+{
+	GamesCellRendererExpander     *expander;
+
+	expander = GAMES_CELL_RENDERER_EXPANDER (object);
+      
+	switch (param_id) {
+	case PROP_EXPANDER_STYLE:
+		expander->priv->expander_style = g_value_get_enum (value);
+		break;
+
+	case PROP_EXPANDER_SIZE:
+		expander->priv->expander_size = g_value_get_int (value);
+		break;
+
+	case PROP_ACTIVATABLE:
+		expander->priv->activatable = g_value_get_boolean (value);
+		break;
+
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
+		break;
+	}
+}
+
+static void
+games_cell_renderer_expander_finalize (GObject *object)
+{
+	(* G_OBJECT_CLASS (games_cell_renderer_expander_parent_class)->finalize) (object);
+}
+
+GtkCellRenderer *
+games_cell_renderer_expander_new (void)
+{
+	return g_object_new (GAMES_TYPE_CELL_RENDERER_EXPANDER, NULL);
+}
+
+static void
+games_cell_renderer_expander_get_size (GtkCellRenderer    *cell,
+					GtkWidget          *widget,
+					const GdkRectangle *cell_area,
+					gint               *x_offset,
+					gint               *y_offset,
+					gint               *width,
+					gint               *height)
+{
+	GamesCellRendererExpander     *expander;
+	gfloat xalign, yalign;
+	guint xpad, ypad;
+
+	expander = (GamesCellRendererExpander *) cell;
+
+	g_object_get (cell,
+		      "xalign", &xalign,
+		      "yalign", &yalign,
+		      "xpad", &xpad,
+		      "ypad", &ypad,
+		      NULL);
+
+	if (cell_area) {
+		if (x_offset) {
+			*x_offset = xalign * (cell_area->width - (expander->priv->expander_size + (2 * xpad)));
+			*x_offset = MAX (*x_offset, 0);
+		}
+
+		if (y_offset) {
+			*y_offset = yalign * (cell_area->height - (expander->priv->expander_size + (2 * ypad)));
+			*y_offset = MAX (*y_offset, 0);
+		}
+	} else {
+		if (x_offset)
+			*x_offset = 0;
+
+		if (y_offset)
+			*y_offset = 0;
+	}
+
+	if (width)
+		*width = xpad * 2 + expander->priv->expander_size;
+
+	if (height)
+		*height = ypad * 2 + expander->priv->expander_size;
+}
+
+static void
+games_cell_renderer_expander_render (GtkCellRenderer      *cell,
+				      cairo_t *cr,
+				      GtkWidget            *widget,
+				      const GdkRectangle   *background_area,
+				      const GdkRectangle   *cell_area,
+				      GtkCellRendererState  flags)
+{
+	GamesCellRendererExpander     *expander;
+	gint                            x_offset, y_offset;
+	guint                           xpad, ypad;
+	GtkStyleContext                 *style;
+	GtkStateFlags                    state;
+
+	expander = (GamesCellRendererExpander *) cell;
+
+	games_cell_renderer_expander_get_size (cell, widget,
+						(GdkRectangle *) cell_area,
+						&x_offset, &y_offset,
+						NULL, NULL);
+
+	g_object_get (cell,
+		      "xpad", &xpad,
+		      "ypad", &ypad,
+		      NULL);
+
+	style = gtk_widget_get_style_context (widget);
+
+	gtk_style_context_save (style);
+	gtk_style_context_add_class (style, GTK_STYLE_CLASS_EXPANDER);
+
+	state = gtk_cell_renderer_get_state (cell, widget, flags);
+
+	if (expander->priv->expander_style == GTK_EXPANDER_COLLAPSED)
+		state |= GTK_STATE_FLAG_NORMAL;
+	else
+		state |= GTK_STATE_FLAG_ACTIVE;
+
+	gtk_style_context_set_state (style, state);
+
+	gtk_render_expander (style,
+			     cr,
+			     cell_area->x + x_offset + xpad,
+			     cell_area->y + y_offset + ypad,
+			     expander->priv->expander_size,
+			     expander->priv->expander_size);
+
+	gtk_style_context_restore (style);
+}
+
+static gboolean
+games_cell_renderer_expander_activate (GtkCellRenderer      *cell,
+					GdkEvent             *event,
+					GtkWidget            *widget,
+					const gchar          *path_string,
+					const GdkRectangle   *background_area,
+					const GdkRectangle   *cell_area,
+					GtkCellRendererState  flags)
+{
+	GamesCellRendererExpander *expander = GAMES_CELL_RENDERER_EXPANDER (cell);
+	GtkTreePath               *path;
+
+	if (!GTK_IS_TREE_VIEW (widget) || !expander->priv->activatable)
+		return FALSE;
+
+	path = gtk_tree_path_new_from_string (path_string);
+
+	if (gtk_tree_path_get_depth (path) > 1) {
+		gtk_tree_path_free (path);
+		return TRUE;
+	}
+
+	if (gtk_tree_view_row_expanded (GTK_TREE_VIEW (widget), path)) {
+		gtk_tree_view_collapse_row (GTK_TREE_VIEW (widget), path);
+	} else {
+		gtk_tree_view_expand_row (GTK_TREE_VIEW (widget), path, FALSE);
+	}
+
+	gtk_tree_path_free (path);
+
+	return TRUE;
+}
diff --git a/libgames-contacts/games-cell-renderer-expander.h b/libgames-contacts/games-cell-renderer-expander.h
new file mode 100644
index 0000000..4a86722
--- /dev/null
+++ b/libgames-contacts/games-cell-renderer-expander.h
@@ -0,0 +1,65 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2012 Chandni Verma
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA  02110-1301  USA
+ */
+
+/*
+ * This code has been influenced by contacts in Empathy, a chat 
+ * application based on Telepathy (https://live.gnome.org/Empathy)
+ * Copyright (C) 2006-2007 Imendio AB
+ */
+
+#ifndef __GAMES_CELL_RENDERER_EXPANDER_H__
+#define __GAMES_CELL_RENDERER_EXPANDER_H__
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define GAMES_TYPE_CELL_RENDERER_EXPANDER		(games_cell_renderer_expander_get_type ())
+#define GAMES_CELL_RENDERER_EXPANDER(obj)		(G_TYPE_CHECK_INSTANCE_CAST ((obj), GAMES_TYPE_CELL_RENDERER_EXPANDER, GamesCellRendererExpander))
+#define GAMES_CELL_RENDERER_EXPANDER_CLASS(klass)	(G_TYPE_CHECK_CLASS_CAST ((klass), GAMES_TYPE_CELL_RENDERER_EXPANDER, GamesCellRendererExpanderClass))
+#define GAMES_IS_CELL_RENDERER_EXPANDER(obj)		(G_TYPE_CHECK_INSTANCE_TYPE ((obj), GAMES_TYPE_CELL_RENDERER_EXPANDER))
+#define GAMES_IS_CELL_RENDERER_EXPANDER_CLASS(klass)	(G_TYPE_CHECK_CLASS_TYPE ((klass), GAMES_TYPE_CELL_RENDERER_EXPANDER))
+#define GAMES_CELL_RENDERER_EXPANDER_GET_CLASS(obj)	(G_TYPE_INSTANCE_GET_CLASS ((obj), GAMES_TYPE_CELL_RENDERER_EXPANDER, GamesCellRendererExpanderClass))
+
+typedef struct _GamesCellRendererExpander GamesCellRendererExpander;
+typedef struct _GamesCellRendererExpanderClass GamesCellRendererExpanderClass;
+typedef struct _GamesCellRendererExpanderPriv GamesCellRendererExpanderPriv;
+
+struct _GamesCellRendererExpander {
+  GtkCellRenderer parent;
+  GamesCellRendererExpanderPriv *priv;
+};
+
+struct _GamesCellRendererExpanderClass {
+  GtkCellRendererClass parent_class;
+
+  /* Padding for future expansion */
+  void (*_gtk_reserved1) (void);
+  void (*_gtk_reserved2) (void);
+  void (*_gtk_reserved3) (void);
+  void (*_gtk_reserved4) (void);
+};
+
+GType            games_cell_renderer_expander_get_type (void) G_GNUC_CONST;
+GtkCellRenderer *games_cell_renderer_expander_new      (void);
+
+G_END_DECLS
+
+#endif /* __GAMES_CELL_RENDERER_EXPANDER_H__ */
diff --git a/libgames-contacts/games-cell-renderer-text.c b/libgames-contacts/games-cell-renderer-text.c
new file mode 100644
index 0000000..4308e14
--- /dev/null
+++ b/libgames-contacts/games-cell-renderer-text.c
@@ -0,0 +1,376 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2012 Chandni Verma
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA  02110-1301  USA
+ */
+
+/*
+ * This code has been influenced by contacts in Empathy, a chat 
+ * application based on Telepathy (https://live.gnome.org/Empathy)
+ * Copyright (C) 2004-2007 Imendio ABdd
+ * Copyright (C) 2010 Collabora Ltd.
+ */
+
+
+#include "config.h"
+
+#include <string.h>
+
+#include <libgames-contacts/games-ui-utils.h>
+#include "games-cell-renderer-text.h"
+
+struct _GamesCellRendererTextPriv{
+	gchar    *name;
+	TpConnectionPresenceType presence_type;
+	gchar    *status;
+	gboolean  is_group;
+
+	gboolean  is_valid;
+	gboolean  is_selected;
+
+	gboolean  compact;
+};
+
+static void cell_renderer_text_dispose           (GObject                     *object);
+static void cell_renderer_text_finalize          (GObject                     *object);
+static void cell_renderer_text_get_property      (GObject                     *object,
+						  guint                        param_id,
+						  GValue                      *value,
+						  GParamSpec                  *pspec);
+static void cell_renderer_text_set_property      (GObject                     *object,
+						  guint                        param_id,
+						  const GValue                *value,
+						  GParamSpec                  *pspec);
+static void cell_renderer_text_render            (GtkCellRenderer             *cell,
+						  cairo_t *cr,
+						  GtkWidget                   *widget,
+						  const GdkRectangle          *background_area,
+						  const GdkRectangle          *cell_area,
+						  GtkCellRendererState         flags);
+static void cell_renderer_text_update_text       (GamesCellRendererText      *cell,
+						  GtkWidget                   *widget,
+						  gboolean                     selected);
+
+/* Properties */
+enum {
+	PROP_0,
+	PROP_NAME,
+	PROP_PRESENCE_TYPE,
+	PROP_STATUS,
+	PROP_IS_GROUP,
+	PROP_COMPACT,
+};
+
+G_DEFINE_TYPE (GamesCellRendererText, games_cell_renderer_text, GTK_TYPE_CELL_RENDERER_TEXT);
+
+static void
+cell_renderer_text_get_preferred_height_for_width (GtkCellRenderer *renderer,
+								GtkWidget *widget,
+								gint width,
+								gint *minimum_size,
+								gint *natural_size)
+{
+	GamesCellRendererText *self = GAMES_CELL_RENDERER_TEXT (renderer);
+
+	/* Only update if not already valid so we get the right size. */
+	cell_renderer_text_update_text (self, widget, self->priv->is_selected);
+
+	GTK_CELL_RENDERER_CLASS (games_cell_renderer_text_parent_class)->
+			get_preferred_height_for_width (renderer, widget, width,
+					minimum_size, natural_size);
+}
+
+
+static void
+games_cell_renderer_text_class_init (GamesCellRendererTextClass *klass)
+{
+	GObjectClass         *object_class;
+	GtkCellRendererClass *cell_class;
+	GParamSpec           *spec;
+
+	object_class = G_OBJECT_CLASS (klass);
+	cell_class = GTK_CELL_RENDERER_CLASS (klass);
+
+	object_class->dispose = cell_renderer_text_dispose;
+	object_class->finalize = cell_renderer_text_finalize;
+
+	object_class->get_property = cell_renderer_text_get_property;
+	object_class->set_property = cell_renderer_text_set_property;
+
+	cell_class->get_preferred_height_for_width = cell_renderer_text_get_preferred_height_for_width;
+	cell_class->render = cell_renderer_text_render;
+
+	spec = g_param_spec_string ("name", "Name", "Contact name", NULL,
+		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+	g_object_class_install_property (object_class, PROP_NAME, spec);
+
+	spec = g_param_spec_uint ("presence-type", "TpConnectionPresenceType",
+		"The contact's presence type",
+		0, G_MAXUINT, /* Telepathy enum, can be extended */
+		TP_CONNECTION_PRESENCE_TYPE_UNKNOWN,
+		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+	g_object_class_install_property (object_class, PROP_PRESENCE_TYPE,
+		spec);
+
+	spec = g_param_spec_string ("status", "Status message",
+		"Contact's custom status message", NULL,
+		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+	g_object_class_install_property (object_class, PROP_STATUS, spec);
+
+	spec = g_param_spec_boolean ("is-group", "Is group",
+		"Whether this cell is a group", FALSE,
+		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+	g_object_class_install_property (object_class, PROP_IS_GROUP, spec);
+
+	spec = g_param_spec_boolean ("compact", "Compact",
+		"TRUE to show the status alongside the contact name;"
+		"FALSE to show it on its own line",
+		FALSE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+	g_object_class_install_property (object_class, PROP_COMPACT, spec);
+
+	g_type_class_add_private (object_class, sizeof (GamesCellRendererTextPriv));
+}
+
+static void
+games_cell_renderer_text_init (GamesCellRendererText *cell)
+{
+	GamesCellRendererTextPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (cell,
+		GAMES_TYPE_CELL_RENDERER_TEXT, GamesCellRendererTextPriv);
+
+	cell->priv = priv;
+	g_object_set (cell,
+		      "ellipsize", PANGO_ELLIPSIZE_END,
+		      NULL);
+
+	priv->name = g_strdup ("");
+	priv->status = g_strdup ("");
+	priv->compact = FALSE;
+}
+
+static void
+cell_renderer_text_dispose (GObject *object)
+{
+	GamesCellRendererText *cell = GAMES_CELL_RENDERER_TEXT (object);
+
+	g_free (cell->priv->name);
+	g_free (cell->priv->status);
+
+	(G_OBJECT_CLASS (games_cell_renderer_text_parent_class)->dispose) (object);
+}
+
+static void
+cell_renderer_text_finalize (GObject *object)
+{
+	(G_OBJECT_CLASS (games_cell_renderer_text_parent_class)->finalize) (object);
+}
+
+static void
+cell_renderer_text_get_property (GObject    *object,
+				 guint       param_id,
+				 GValue     *value,
+				 GParamSpec *pspec)
+{
+	GamesCellRendererText     *cell;
+
+	cell = GAMES_CELL_RENDERER_TEXT (object);
+
+	switch (param_id) {
+	case PROP_NAME:
+		g_value_set_string (value, cell->priv->name);
+		break;
+	case PROP_PRESENCE_TYPE:
+		g_value_set_uint (value, cell->priv->presence_type);
+		break;
+	case PROP_STATUS:
+		g_value_set_string (value, cell->priv->status);
+		break;
+	case PROP_IS_GROUP:
+		g_value_set_boolean (value, cell->priv->is_group);
+		break;
+	case PROP_COMPACT:
+		g_value_set_boolean (value, cell->priv->compact);
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
+		break;
+	}
+}
+
+static void
+cell_renderer_text_set_property (GObject      *object,
+				 guint         param_id,
+				 const GValue *value,
+				 GParamSpec   *pspec)
+{
+	GamesCellRendererText     *cell;
+	const gchar                *str;
+
+	cell = GAMES_CELL_RENDERER_TEXT (object);
+
+	switch (param_id) {
+	case PROP_NAME:
+		g_free (cell->priv->name);
+		str = g_value_get_string (value);
+		cell->priv->name = g_strdup (str ? str : "");
+		g_strdelimit (cell->priv->name, "\n\r\t", ' ');
+		cell->priv->is_valid = FALSE;
+		break;
+	case PROP_PRESENCE_TYPE:
+		cell->priv->presence_type = g_value_get_uint (value);
+		cell->priv->is_valid = FALSE;
+		break;
+	case PROP_STATUS:
+		g_free (cell->priv->status);
+		str = g_value_get_string (value);
+		cell->priv->status = g_strdup (str ? str : "");
+		g_strdelimit (cell->priv->status, "\n\r\t", ' ');
+		cell->priv->is_valid = FALSE;
+		break;
+	case PROP_IS_GROUP:
+		cell->priv->is_group = g_value_get_boolean (value);
+		cell->priv->is_valid = FALSE;
+		break;
+	case PROP_COMPACT:
+		cell->priv->compact = g_value_get_boolean (value);
+		cell->priv->is_valid = FALSE;
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
+		break;
+	}
+}
+
+static void
+cell_renderer_text_render (GtkCellRenderer      *cell,
+			   cairo_t *cr,
+			   GtkWidget            *widget,
+			   const GdkRectangle   *background_area,
+			   const GdkRectangle   *cell_area,
+			   GtkCellRendererState  flags)
+{
+	GamesCellRendererText *celltext;
+
+	celltext = GAMES_CELL_RENDERER_TEXT (cell);
+
+	cell_renderer_text_update_text (celltext,
+					widget,
+					(flags & GTK_CELL_RENDERER_SELECTED));
+
+	(GTK_CELL_RENDERER_CLASS (games_cell_renderer_text_parent_class)->render) (
+		cell, cr,
+		widget,
+		background_area,
+		cell_area,
+		flags);
+}
+
+static void
+cell_renderer_text_update_text (GamesCellRendererText *cell,
+				GtkWidget              *widget,
+				gboolean                selected)
+{
+	GamesCellRendererText *self = GAMES_CELL_RENDERER_TEXT (cell);
+	const PangoFontDescription *font_desc;
+	PangoAttrList              *attr_list;
+	PangoAttribute             *attr_color = NULL, *attr_size;
+	GtkStyleContext            *style;
+	gchar                      *str;
+	gint                        font_size;
+
+	if (self->priv->is_valid && self->priv->is_selected == selected) {
+		return;
+	}
+
+	if (self->priv->is_group) {
+		g_object_set (cell,
+			      "visible", TRUE,
+			      "weight", PANGO_WEIGHT_BOLD,
+			      "text", self->priv->name,
+			      "attributes", NULL,
+			      "xpad", 1,
+			      "ypad", 1,
+			      NULL);
+
+		self->priv->is_selected = selected;
+		self->priv->is_valid = TRUE;
+		return;
+	}
+
+	style = gtk_widget_get_style_context (widget);
+
+	attr_list = pango_attr_list_new ();
+
+	font_desc = gtk_style_context_get_font (style, GTK_STATE_FLAG_NORMAL);
+	font_size = pango_font_description_get_size (font_desc);
+	attr_size = pango_attr_size_new (font_size / 1.2);
+	attr_size->start_index = strlen (self->priv->name) + 1;
+	attr_size->end_index = -1;
+	pango_attr_list_insert (attr_list, attr_size);
+
+	if (!selected) {
+		GdkRGBA color;
+
+		gtk_style_context_get_color (style, 0, &color);
+
+		attr_color = pango_attr_foreground_new (color.red * 0xffff,
+							color.green * 0xffff,
+							color.blue * 0xffff);
+		attr_color->start_index = attr_size->start_index;
+		attr_color->end_index = -1;
+		pango_attr_list_insert (attr_list, attr_color);
+	}
+
+	if (self->priv->compact) {
+		if (STR_EMPTY (self->priv->status)) {
+			str = g_strdup (self->priv->name);
+		} else {
+			str = g_strdup_printf ("%s %s", self->priv->name, self->priv->status);
+		}
+	} else {
+		const gchar *status = self->priv->status;
+
+		if (STR_EMPTY (self->priv->status)) {
+			status = games_presence_get_default_message (self->priv->presence_type);
+		}
+
+		if (status == NULL)
+			str = g_strdup (self->priv->name);
+		else
+			str = g_strdup_printf ("%s\n%s", self->priv->name, status);
+	}
+
+	g_object_set (cell,
+		      "visible", TRUE,
+		      "weight", PANGO_WEIGHT_NORMAL,
+		      "text", str,
+		      "attributes", attr_list,
+		      "xpad", 0,
+		      "ypad", 1,
+		      NULL);
+
+	g_free (str);
+	pango_attr_list_unref (attr_list);
+
+	self->priv->is_selected = selected;
+	self->priv->is_valid = TRUE;
+}
+
+GtkCellRenderer *
+games_cell_renderer_text_new (void)
+{
+	return g_object_new (GAMES_TYPE_CELL_RENDERER_TEXT, NULL);
+}
diff --git a/libgames-contacts/games-cell-renderer-text.h b/libgames-contacts/games-cell-renderer-text.h
new file mode 100644
index 0000000..b71ff29
--- /dev/null
+++ b/libgames-contacts/games-cell-renderer-text.h
@@ -0,0 +1,59 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2012 Chandni Verma
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA  02110-1301  USA
+ */
+
+/*
+ * This code has been influenced by contacts in Empathy, a chat 
+ * application based on Telepathy (https://live.gnome.org/Empathy)
+ * Copyright (C) 2004-2007 Imendio AB
+ */
+
+#ifndef __GAMES_CELL_RENDERER_TEXT_H__
+#define __GAMES_CELL_RENDERER_TEXT_H__
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define GAMES_TYPE_CELL_RENDERER_TEXT         (games_cell_renderer_text_get_type ())
+#define GAMES_CELL_RENDERER_TEXT(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), GAMES_TYPE_CELL_RENDERER_TEXT, GamesCellRendererText))
+#define GAMES_CELL_RENDERER_TEXT_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST((k), GAMES_TYPE_CELL_RENDERER_TEXT, GamesCellRendererTextClass))
+#define GAMES_IS_CELL_RENDERER_TEXT(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), GAMES_TYPE_CELL_RENDERER_TEXT))
+#define GAMES_IS_CELL_RENDERER_TEXT_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), GAMES_TYPE_CELL_RENDERER_TEXT))
+#define GAMES_CELL_RENDERER_TEXT_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GAMES_TYPE_CELL_RENDERER_TEXT, GamesCellRendererTextClass))
+
+typedef struct _GamesCellRendererText      GamesCellRendererText;
+typedef struct _GamesCellRendererTextClass GamesCellRendererTextClass;
+typedef struct _GamesCellRendererTextPriv GamesCellRendererTextPriv;
+
+struct _GamesCellRendererText {
+	GtkCellRendererText parent;
+	GamesCellRendererTextPriv *priv;
+};
+
+struct _GamesCellRendererTextClass {
+	GtkCellRendererTextClass    parent_class;
+};
+
+GType             games_cell_renderer_text_get_type (void) G_GNUC_CONST;
+GtkCellRenderer * games_cell_renderer_text_new      (void);
+
+G_END_DECLS
+
+#endif /* __GAMES_CELL_RENDERER_TEXT_H__ */
diff --git a/libgames-contacts/games-contact.c b/libgames-contacts/games-contact.c
index d7b7ad3..9efcae6 100644
--- a/libgames-contacts/games-contact.c
+++ b/libgames-contacts/games-contact.c
@@ -1104,13 +1104,17 @@ games_contact_equal (gconstpointer contact1,
 }
 
 static GamesCapabilities
-tp_caps_to_capabilities (TpCapabilities *caps)
+tp_caps_to_capabilities (GamesContact *self,
+    TpCapabilities *caps)
 {
   GamesCapabilities capabilities = 0;
 
   if (tp_capabilities_supports_dbus_tubes (caps, TP_HANDLE_TYPE_CONTACT,
-      "org.gnome.glchess"))
+      "org.gnome.glchess")) {
+    g_debug ("Contact %s(%s) has glchess playing capabilities",
+        games_contact_get_alias (self), games_contact_get_id (self));
     capabilities |= GAMES_CAPABILITIES_TUBE_GLCHESS;
+  }
 
   return capabilities;
 }
@@ -1121,10 +1125,13 @@ set_capabilities_from_tp_caps (GamesContact *self,
 {
   GamesCapabilities capabilities;
 
+  g_debug ("Got capabilities for contact %s(%s)",
+      games_contact_get_alias (self), games_contact_get_id (self));
+
   if (caps == NULL)
     return;
 
-  capabilities = tp_caps_to_capabilities (caps);
+  capabilities = tp_caps_to_capabilities (self, caps);
   games_contact_set_capabilities (self, capabilities);
 }
 
diff --git a/libgames-contacts/games-individual-view.c b/libgames-contacts/games-individual-view.c
new file mode 100644
index 0000000..36a536b
--- /dev/null
+++ b/libgames-contacts/games-individual-view.c
@@ -0,0 +1,1238 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2012 Chandni Verma
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA  02110-1301  USA
+ *
+ * Author: Chandni Verma <chandniverma2112 gmail com>
+ */
+
+/* Much of code here is a simplified and adapted version of the way
+ * contacts are displayed in Empathy, a chat application using Folks
+ * (https://live.gnome.org/Empathy)
+ * Copyright (C) 2005-2007 Imendio AB
+ * Copyright (C) 2007-2010 Collabora Ltd.
+ */
+
+#include "config.h"
+
+#include <string.h>
+
+#include <glib/gi18n-lib.h>
+#include <gdk/gdkkeysyms.h>
+#include <gtk/gtk.h>
+
+#include <telepathy-glib/account-manager.h>
+#include <telepathy-glib/util.h>
+
+#include <folks/folks.h>
+#include <folks/folks-telepathy.h>
+
+#include <libgames-contacts/games-individual-manager.h>
+#include <libgames-contacts/games-individual-groups.h>
+#include <libgames-contacts/games-ui-utils.h>
+
+#include "games-individual-view.h"
+#include "games-individual-store.h"
+#include "games-cell-renderer-expander.h"
+#include "games-cell-renderer-text.h"
+#include "games-ui-utils.h"
+#include "games-enum-types.h"
+
+/* Active users are those which have recently changed state
+ * (e.g. online, offline or from normal to a busy state).
+ */
+
+struct _GamesIndividualViewPriv
+{
+  GamesIndividualStore *store;
+  GamesIndividualViewFeatureFlags view_features;
+
+  gboolean show_untrusted;
+  gboolean show_uninteresting;
+
+  GtkTreeModelFilter *filter;
+  GtkWidget *search_widget;
+
+  guint expand_groups_idle_handler;
+  /* owned string (group name) -> bool (whether to expand/contract) */
+  GHashTable *expand_groups;
+
+  GtkTreeModelFilterVisibleFunc custom_filter;
+  gpointer custom_filter_data;
+
+  GtkCellRenderer *text_renderer;
+};
+
+enum
+{
+  PROP_0,
+  PROP_STORE,
+  PROP_VIEW_FEATURES,
+  PROP_SHOW_UNTRUSTED,
+  PROP_SHOW_UNINTERESTING,
+};
+
+G_DEFINE_TYPE (GamesIndividualView, games_individual_view,
+    GTK_TYPE_TREE_VIEW);
+
+
+static void
+individual_view_cell_set_background (GamesIndividualView *view,
+    GtkCellRenderer *cell,
+    gboolean is_group,
+    gboolean is_active)
+{
+  if (!is_group && is_active)
+    {
+      GtkStyleContext *style;
+      GdkRGBA color;
+
+      style = gtk_widget_get_style_context (GTK_WIDGET (view));
+
+      gtk_style_context_get_background_color (style, GTK_STATE_FLAG_SELECTED,
+          &color);
+
+      /* Here we take the current theme colour and add it to
+       * the colour for white and average the two. This
+       * gives a colour which is inline with the theme but
+       * slightly whiter.
+       */
+      const GdkRGBA white = { 1.0, 1.0, 1.0, 1.0 };
+
+     	color.red = (color.red + white.red) / 2;
+      color.green = (color.green + white.green) / 2;
+      color.blue = (color.blue + white.blue) / 2;
+
+      g_object_set (cell, "cell-background-rgba", &color, NULL);
+    }
+  else
+    g_object_set (cell, "cell-background-rgba", NULL, NULL);
+}
+
+static void
+individual_view_pixbuf_cell_data_func (GtkTreeViewColumn *tree_column,
+    GtkCellRenderer *cell,
+    GtkTreeModel *model,
+    GtkTreeIter *iter,
+    GamesIndividualView *view)
+{
+  GdkPixbuf *pixbuf;
+  gboolean is_group;
+  gboolean is_active;
+
+  gtk_tree_model_get (model, iter,
+      GAMES_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
+      GAMES_INDIVIDUAL_STORE_COL_IS_ACTIVE, &is_active,
+      GAMES_INDIVIDUAL_STORE_COL_ICON_STATUS, &pixbuf, -1);
+
+  g_object_set (cell,
+      "visible", !is_group,
+      "pixbuf", pixbuf,
+      NULL);
+
+  tp_clear_object (&pixbuf);
+
+  individual_view_cell_set_background (view, cell, is_group, is_active);
+}
+
+static void
+individual_view_group_icon_cell_data_func (GtkTreeViewColumn *tree_column,
+    GtkCellRenderer *cell,
+    GtkTreeModel *model,
+    GtkTreeIter *iter,
+    GamesIndividualView *view)
+{
+  GdkPixbuf *pixbuf = NULL;
+  gboolean is_group;
+  gchar *name;
+
+  gtk_tree_model_get (model, iter,
+      GAMES_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
+      GAMES_INDIVIDUAL_STORE_COL_NAME, &name, -1);
+
+  if (!is_group)
+    goto out;
+
+  if (!tp_strdiff (name, GAMES_INDIVIDUAL_STORE_FAVORITE))
+    {
+      pixbuf = games_pixbuf_from_icon_name ("emblem-favorite",
+          GTK_ICON_SIZE_MENU);
+    }
+  else if (!tp_strdiff (name, GAMES_INDIVIDUAL_STORE_PEOPLE_NEARBY))
+    {
+      pixbuf = games_pixbuf_from_icon_name ("im-local-xmpp",
+          GTK_ICON_SIZE_MENU);
+    }
+
+out:
+  g_object_set (cell,
+      "visible", pixbuf != NULL,
+      "pixbuf", pixbuf,
+      NULL);
+
+  tp_clear_object (&pixbuf);
+
+  g_free (name);
+}
+
+static void
+individual_view_avatar_cell_data_func (GtkTreeViewColumn *tree_column,
+    GtkCellRenderer *cell,
+    GtkTreeModel *model,
+    GtkTreeIter *iter,
+    GamesIndividualView *view)
+{
+  GdkPixbuf *pixbuf;
+  gboolean show_avatar;
+  gboolean is_group;
+  gboolean is_active;
+
+  gtk_tree_model_get (model, iter,
+      GAMES_INDIVIDUAL_STORE_COL_PIXBUF_AVATAR, &pixbuf,
+      GAMES_INDIVIDUAL_STORE_COL_PIXBUF_AVATAR_VISIBLE, &show_avatar,
+      GAMES_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
+      GAMES_INDIVIDUAL_STORE_COL_IS_ACTIVE, &is_active, -1);
+
+  g_object_set (cell,
+      "visible", !is_group && show_avatar,
+      "pixbuf", pixbuf,
+      NULL);
+
+  tp_clear_object (&pixbuf);
+
+  individual_view_cell_set_background (view, cell, is_group, is_active);
+}
+
+static void
+individual_view_text_cell_data_func (GtkTreeViewColumn *tree_column,
+    GtkCellRenderer *cell,
+    GtkTreeModel *model,
+    GtkTreeIter *iter,
+    GamesIndividualView *view)
+{
+  gboolean is_group;
+  gboolean is_active;
+
+  gtk_tree_model_get (model, iter,
+      GAMES_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
+      GAMES_INDIVIDUAL_STORE_COL_IS_ACTIVE, &is_active, -1);
+
+  individual_view_cell_set_background (view, cell, is_group, is_active);
+}
+
+static void
+individual_view_expander_cell_data_func (GtkTreeViewColumn *column,
+    GtkCellRenderer *cell,
+    GtkTreeModel *model,
+    GtkTreeIter *iter,
+    GamesIndividualView *view)
+{
+  gboolean is_group;
+  gboolean is_active;
+
+  gtk_tree_model_get (model, iter,
+      GAMES_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
+      GAMES_INDIVIDUAL_STORE_COL_IS_ACTIVE, &is_active, -1);
+
+  if (gtk_tree_model_iter_has_child (model, iter))
+    {
+      GtkTreePath *path;
+      gboolean row_expanded;
+
+      path = gtk_tree_model_get_path (model, iter);
+      row_expanded =
+          gtk_tree_view_row_expanded (GTK_TREE_VIEW
+          (gtk_tree_view_column_get_tree_view (column)), path);
+      gtk_tree_path_free (path);
+
+      g_object_set (cell,
+          "visible", TRUE,
+          "expander-style",
+          row_expanded ? GTK_EXPANDER_EXPANDED : GTK_EXPANDER_COLLAPSED,
+          NULL);
+    }
+  else
+    g_object_set (cell, "visible", FALSE, NULL);
+
+  individual_view_cell_set_background (view, cell, is_group, is_active);
+}
+
+static void
+individual_view_row_expand_or_collapse_cb (GamesIndividualView *view,
+    GtkTreeIter *iter,
+    GtkTreePath *path,
+    gpointer user_data)
+{
+  GtkTreeModel *model;
+  gchar *name;
+  gboolean expanded;
+
+  if (!(view->priv->view_features & GAMES_INDIVIDUAL_VIEW_FEATURE_GROUPS_SAVE))
+    return;
+
+  model = gtk_tree_view_get_model (GTK_TREE_VIEW (view));
+
+  gtk_tree_model_get (model, iter,
+      GAMES_INDIVIDUAL_STORE_COL_NAME, &name, -1);
+
+  expanded = GPOINTER_TO_INT (user_data);
+  games_individual_group_set_expanded (name, expanded);
+
+  g_free (name);
+}
+
+static gboolean
+individual_view_start_search_cb (GamesIndividualView *view,
+    gpointer data)
+{
+  if (view->priv->search_widget == NULL)
+    return FALSE;
+
+  games_individual_view_start_search (view);
+
+  return TRUE;
+}
+
+static void
+individual_view_search_text_notify_cb (GamesLiveSearch *search,
+    GParamSpec *pspec,
+    GamesIndividualView *view)
+{
+  GtkTreePath *path;
+  GtkTreeViewColumn *focus_column;
+  GtkTreeModel *model;
+  GtkTreeIter iter;
+  gboolean set_cursor = FALSE;
+
+  gtk_tree_model_filter_refilter (view->priv->filter);
+
+  /* Set cursor on the first contact. If it is already set on a group,
+   * set it on its first child contact. Note that first child of a group
+   * is its separator, that's why we actually set to the 2nd
+   */
+
+  model = gtk_tree_view_get_model (GTK_TREE_VIEW (view));
+  gtk_tree_view_get_cursor (GTK_TREE_VIEW (view), &path, &focus_column);
+
+  if (path == NULL)
+    {
+      path = gtk_tree_path_new_from_string ("0:1");
+      set_cursor = TRUE;
+    }
+  else if (gtk_tree_path_get_depth (path) < 2)
+    {
+      gboolean is_group;
+
+      gtk_tree_model_get_iter (model, &iter, path);
+      gtk_tree_model_get (model, &iter,
+          GAMES_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
+          -1);
+
+      if (is_group)
+        {
+          gtk_tree_path_down (path);
+          gtk_tree_path_next (path);
+          set_cursor = TRUE;
+        }
+    }
+
+  if (set_cursor)
+    {
+      /* Make sure the path is valid. */
+      if (gtk_tree_model_get_iter (model, &iter, path))
+        {
+          gtk_tree_view_set_cursor (GTK_TREE_VIEW (view), path, focus_column,
+              FALSE);
+        }
+    }
+
+  gtk_tree_path_free (path);
+}
+
+static void
+individual_view_search_activate_cb (GtkWidget *search,
+  GamesIndividualView *view)
+{
+  GtkTreePath *path;
+  GtkTreeViewColumn *focus_column;
+
+  gtk_tree_view_get_cursor (GTK_TREE_VIEW (view), &path, &focus_column);
+  if (path != NULL)
+    {
+      gtk_tree_view_row_activated (GTK_TREE_VIEW (view), path, focus_column);
+      gtk_tree_path_free (path);
+
+      gtk_widget_hide (search);
+    }
+}
+
+static gboolean
+individual_view_search_key_navigation_cb (GtkWidget *search,
+  GdkEvent *event,
+  GamesIndividualView *view)
+{
+  GdkEvent *new_event;
+  gboolean ret = FALSE;
+
+  new_event = gdk_event_copy (event);
+  gtk_widget_grab_focus (GTK_WIDGET (view));
+  ret = gtk_widget_event (GTK_WIDGET (view), new_event);
+  gtk_widget_grab_focus (search);
+
+  gdk_event_free (new_event);
+
+  return ret;
+}
+
+static void
+individual_view_search_hide_cb (GamesLiveSearch *search,
+    GamesIndividualView *view)
+{
+  GtkTreeModel *model;
+  GtkTreePath *cursor_path;
+  GtkTreeIter iter;
+  gboolean valid = FALSE;
+
+  /* block expand or collapse handlers, they would write the
+   * expand or collapsed setting to file otherwise */
+  g_signal_handlers_block_by_func (view,
+      individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (TRUE));
+  g_signal_handlers_block_by_func (view,
+    individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (FALSE));
+
+  /* restore which groups are expanded and which are not */
+  model = gtk_tree_view_get_model (GTK_TREE_VIEW (view));
+  for (valid = gtk_tree_model_get_iter_first (model, &iter);
+       valid; valid = gtk_tree_model_iter_next (model, &iter))
+    {
+      gboolean is_group;
+      gchar *name = NULL;
+      GtkTreePath *path;
+
+      gtk_tree_model_get (model, &iter,
+          GAMES_INDIVIDUAL_STORE_COL_NAME, &name,
+          GAMES_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
+          -1);
+
+      if (!is_group)
+        {
+          g_free (name);
+          continue;
+        }
+
+      path = gtk_tree_model_get_path (model, &iter);
+      if ((view->priv->view_features &
+            GAMES_INDIVIDUAL_VIEW_FEATURE_GROUPS_SAVE) == 0 ||
+          games_individual_group_get_expanded (name))
+        {
+          gtk_tree_view_expand_row (GTK_TREE_VIEW (view), path, TRUE);
+        }
+      else
+        {
+          gtk_tree_view_collapse_row (GTK_TREE_VIEW (view), path);
+        }
+
+      gtk_tree_path_free (path);
+      g_free (name);
+    }
+
+  /* unblock expand or collapse handlers */
+  g_signal_handlers_unblock_by_func (view,
+      individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (TRUE));
+  g_signal_handlers_unblock_by_func (view,
+      individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (FALSE));
+
+  /* keep the selected contact visible */
+  gtk_tree_view_get_cursor (GTK_TREE_VIEW (view), &cursor_path, NULL);
+
+  if (cursor_path != NULL)
+    gtk_tree_view_scroll_to_cell (GTK_TREE_VIEW (view), cursor_path, NULL,
+        FALSE, 0, 0);
+
+  gtk_tree_path_free (cursor_path);
+}
+
+static void
+individual_view_search_show_cb (GamesLiveSearch *search,
+    GamesIndividualView *view)
+{
+  /* block expand or collapse handlers during expand all, they would
+   * write the expand or collapsed setting to file otherwise */
+  g_signal_handlers_block_by_func (view,
+      individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (TRUE));
+
+  gtk_tree_view_expand_all (GTK_TREE_VIEW (view));
+
+  g_signal_handlers_unblock_by_func (view,
+      individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (TRUE));
+}
+
+static gboolean
+expand_idle_foreach_cb (GtkTreeModel *model,
+    GtkTreePath *path,
+    GtkTreeIter *iter,
+    GamesIndividualView *self)
+{
+  gboolean is_group;
+  gpointer should_expand;
+  gchar *name;
+
+  /* We only want groups */
+  if (gtk_tree_path_get_depth (path) > 1)
+    return FALSE;
+
+  gtk_tree_model_get (model, iter,
+      GAMES_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
+      GAMES_INDIVIDUAL_STORE_COL_NAME, &name,
+      -1);
+
+  if (!is_group)
+    {
+      g_free (name);
+      return FALSE;
+    }
+
+  if (g_hash_table_lookup_extended (self->priv->expand_groups, name, NULL,
+      &should_expand))
+    {
+      if (GPOINTER_TO_INT (should_expand))
+        gtk_tree_view_expand_row (GTK_TREE_VIEW (self), path, FALSE);
+      else
+        gtk_tree_view_collapse_row (GTK_TREE_VIEW (self), path);
+
+      g_hash_table_remove (self->priv->expand_groups, name);
+    }
+
+  g_free (name);
+
+  return FALSE;
+}
+
+static gboolean
+individual_view_expand_idle_cb (GamesIndividualView *self)
+{
+  g_signal_handlers_block_by_func (self,
+    individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (TRUE));
+  g_signal_handlers_block_by_func (self,
+    individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (FALSE));
+
+  /* The store/filter could've been removed while we were in the idle queue */
+  if (self->priv->filter != NULL)
+    {
+      gtk_tree_model_foreach (GTK_TREE_MODEL (self->priv->filter),
+          (GtkTreeModelForeachFunc) expand_idle_foreach_cb, self);
+    }
+
+  g_signal_handlers_unblock_by_func (self,
+      individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (FALSE));
+  g_signal_handlers_unblock_by_func (self,
+      individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (TRUE));
+
+  /* Empty the table of groups to expand/contract, since it may contain groups
+   * which no longer exist in the tree view. This can happen after going
+   * offline, for example. */
+  g_hash_table_remove_all (self->priv->expand_groups);
+  self->priv->expand_groups_idle_handler = 0;
+  g_object_unref (self);
+
+  return FALSE;
+}
+
+static void
+individual_view_row_has_child_toggled_cb (GtkTreeModel *model,
+    GtkTreePath *path,
+    GtkTreeIter *iter,
+    GamesIndividualView *view)
+{
+  gboolean should_expand, is_group = FALSE;
+  gchar *name = NULL;
+  gpointer will_expand;
+
+  gtk_tree_model_get (model, iter,
+      GAMES_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
+      GAMES_INDIVIDUAL_STORE_COL_NAME, &name,
+      -1);
+
+  if (!is_group || STR_EMPTY (name))
+    {
+      g_free (name);
+      return;
+    }
+
+  should_expand = (view->priv->view_features &
+          GAMES_INDIVIDUAL_VIEW_FEATURE_GROUPS_SAVE) == 0 ||
+      (view->priv->search_widget != NULL &&
+          gtk_widget_get_visible (view->priv->search_widget)) ||
+      games_individual_group_get_expanded (name);
+
+  /* FIXME: It doesn't work to call gtk_tree_view_expand_row () from within
+   * gtk_tree_model_filter_refilter (). We add the rows to expand/contract to
+   * a hash table, and expand or contract them as appropriate all at once in
+   * an idle handler which iterates over all the group rows. */
+  if (!g_hash_table_lookup_extended (view->priv->expand_groups, name, NULL,
+      &will_expand) ||
+      GPOINTER_TO_INT (will_expand) != should_expand)
+    {
+      g_hash_table_insert (view->priv->expand_groups, g_strdup (name),
+          GINT_TO_POINTER (should_expand));
+
+      if (view->priv->expand_groups_idle_handler == 0)
+        {
+          view->priv->expand_groups_idle_handler =
+              g_idle_add ((GSourceFunc) individual_view_expand_idle_cb,
+                  g_object_ref (view));
+        }
+    }
+
+  g_free (name);
+}
+
+static gboolean
+individual_view_is_visible_individual (GamesIndividualView *self,
+    FolksIndividual *individual,
+    gboolean is_online,
+    gboolean is_searching,
+    const gchar *group,
+    gboolean is_fake_group,
+    GamesCapabilities individual_caps,
+    GamesActionType interest)
+{
+  GamesLiveSearch *live = GAMES_LIVE_SEARCH (self->priv->search_widget);
+  gboolean is_favorite;
+
+  /* We're only giving the visibility wrt filtering here, not things like
+   * presence. */
+  if (!self->priv->show_untrusted &&
+      folks_individual_get_trust_level (individual) == FOLKS_TRUST_LEVEL_NONE)
+    {
+      return FALSE;
+    }
+
+  if (!self->priv->show_uninteresting)
+    {
+      /* Hide all uninteresting individuals */
+      switch (interest)
+      {
+        case GAMES_ACTION_CHAT:
+            /* All individuals in individuals contain TpContacts and hence
+             * support chats. Proceed to calculate visibility. */
+            break;
+
+        case GAMES_ACTION_PLAY_GLCHESS:
+            if ((individual_caps & GAMES_ACTION_PLAY_GLCHESS) == 0)
+              return FALSE;
+            /*else proceed */
+            break;
+
+        default:
+            return FALSE;
+      }
+    }
+
+  is_favorite = folks_favourite_details_get_is_favourite (
+      FOLKS_FAVOURITE_DETAILS (individual));
+  if (!is_searching) {
+    if (is_favorite && is_fake_group &&
+        !tp_strdiff (group, GAMES_INDIVIDUAL_STORE_FAVORITE))
+        /* Always display favorite contacts in the favorite group */
+        return TRUE;
+
+    return (is_online);
+  }
+
+  return games_individual_match_string (individual,
+      games_live_search_get_text (live),
+      games_live_search_get_words (live));
+}
+
+static gchar *
+get_group (GtkTreeModel *model,
+    GtkTreeIter *iter,
+    gboolean *is_fake)
+{
+  GtkTreeIter parent_iter;
+  gchar *name = NULL;
+
+  *is_fake = FALSE;
+
+  if (!gtk_tree_model_iter_parent (model, &parent_iter, iter))
+    return NULL;
+
+  gtk_tree_model_get (model, &parent_iter,
+      GAMES_INDIVIDUAL_STORE_COL_NAME, &name,
+      GAMES_INDIVIDUAL_STORE_COL_IS_FAKE_GROUP, is_fake,
+      -1);
+
+  return name;
+}
+
+gboolean
+individual_view_filter_default (GtkTreeModel *model,
+    GtkTreeIter *iter,
+    gpointer user_data,
+    GamesActionType interest)
+{
+  GamesIndividualView *self = GAMES_INDIVIDUAL_VIEW (user_data);
+  FolksIndividual *individual = NULL;
+  gboolean is_group, is_separator, valid;
+  GtkTreeIter child_iter;
+  gboolean visible, is_online;
+  gboolean is_searching = TRUE;
+  GamesCapabilities individual_caps;
+
+  if (self->priv->search_widget == NULL ||
+      !gtk_widget_get_visible (self->priv->search_widget))
+     is_searching = FALSE;
+
+  gtk_tree_model_get (model, iter,
+      GAMES_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
+      GAMES_INDIVIDUAL_STORE_COL_IS_SEPARATOR, &is_separator,
+      GAMES_INDIVIDUAL_STORE_COL_IS_ONLINE, &is_online,
+      GAMES_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual,
+      GAMES_INDIVIDUAL_STORE_COL_INDIVIDUAL_CAPABILITIES, &individual_caps,
+      -1);
+
+  if (individual != NULL)
+    {
+      gchar *group;
+      gboolean is_fake_group;
+
+      group = get_group (model, iter, &is_fake_group);
+
+      visible = individual_view_is_visible_individual (self, individual,
+          is_online, is_searching, group, is_fake_group, individual_caps,
+          interest);
+
+      g_object_unref (individual);
+      g_free (group);
+
+      return visible;
+    }
+
+  if (is_separator)
+    return TRUE;
+
+  /* Not a contact, not a separator, must be a group */
+  g_return_val_if_fail (is_group, FALSE);
+
+  /* only show groups which are not empty */
+  for (valid = gtk_tree_model_iter_children (model, &child_iter, iter);
+       valid; valid = gtk_tree_model_iter_next (model, &child_iter))
+    {
+      gchar *group;
+      gboolean is_fake_group;
+
+      gtk_tree_model_get (model, &child_iter,
+        GAMES_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual,
+        GAMES_INDIVIDUAL_STORE_COL_IS_ONLINE, &is_online,
+        GAMES_INDIVIDUAL_STORE_COL_INDIVIDUAL_CAPABILITIES,
+            &individual_caps,
+        -1);
+
+      if (individual == NULL)
+        continue;
+
+      group = get_group (model, &child_iter, &is_fake_group);
+
+      visible = individual_view_is_visible_individual (self, individual,
+          is_online, is_searching, group, is_fake_group, individual_caps,
+          interest);
+
+      g_object_unref (individual);
+      g_free (group);
+
+      /* show group if it has at least one visible contact in it */
+      if (visible)
+        return TRUE;
+    }
+
+  return FALSE;
+}
+
+static gboolean
+individual_view_filter_visible_func (GtkTreeModel *model,
+    GtkTreeIter *iter,
+    gpointer user_data)
+{
+  GamesIndividualView *self = GAMES_INDIVIDUAL_VIEW (user_data);
+
+  if (self->priv->custom_filter != NULL)
+    return self->priv->custom_filter (model, iter,
+        self->priv->custom_filter_data);
+  else
+    return individual_view_filter_default (model, iter, user_data,
+        GAMES_ACTION_CHAT);
+}
+
+static void
+individual_view_constructed (GObject *object)
+{
+  GamesIndividualView *view = GAMES_INDIVIDUAL_VIEW (object);
+  GtkCellRenderer *cell;
+  GtkTreeViewColumn *col;
+
+  /* Setup view */
+  g_object_set (view,
+      "headers-visible", FALSE,
+      "show-expanders", FALSE,
+      NULL);
+
+  col = gtk_tree_view_column_new ();
+
+  /* State */
+  cell = gtk_cell_renderer_pixbuf_new ();
+  gtk_tree_view_column_pack_start (col, cell, FALSE);
+  gtk_tree_view_column_set_cell_data_func (col, cell,
+      (GtkTreeCellDataFunc) individual_view_pixbuf_cell_data_func,
+      view, NULL);
+
+  g_object_set (cell,
+      "xpad", 5,
+      "ypad", 1,
+      "visible", FALSE,
+      NULL);
+
+  /* Group icon */
+  cell = gtk_cell_renderer_pixbuf_new ();
+  gtk_tree_view_column_pack_start (col, cell, FALSE);
+  gtk_tree_view_column_set_cell_data_func (col, cell,
+      (GtkTreeCellDataFunc) individual_view_group_icon_cell_data_func,
+      view, NULL);
+
+  g_object_set (cell,
+      "xpad", 0,
+      "ypad", 0,
+      "visible", FALSE,
+      "width", 16,
+      "height", 16,
+      NULL);
+
+  /* Name */
+  view->priv->text_renderer = games_cell_renderer_text_new ();
+  gtk_tree_view_column_pack_start (col, view->priv->text_renderer, TRUE);
+  gtk_tree_view_column_set_cell_data_func (col, view->priv->text_renderer,
+      (GtkTreeCellDataFunc) individual_view_text_cell_data_func, view, NULL);
+
+  gtk_tree_view_column_add_attribute (col, view->priv->text_renderer,
+      "name", GAMES_INDIVIDUAL_STORE_COL_NAME);
+  gtk_tree_view_column_add_attribute (col, view->priv->text_renderer,
+      "text", GAMES_INDIVIDUAL_STORE_COL_NAME);
+  gtk_tree_view_column_add_attribute (col, view->priv->text_renderer,
+      "presence-type", GAMES_INDIVIDUAL_STORE_COL_PRESENCE_TYPE);
+  gtk_tree_view_column_add_attribute (col, view->priv->text_renderer,
+      "status", GAMES_INDIVIDUAL_STORE_COL_STATUS);
+  gtk_tree_view_column_add_attribute (col, view->priv->text_renderer,
+      "is_group", GAMES_INDIVIDUAL_STORE_COL_IS_GROUP);
+  gtk_tree_view_column_add_attribute (col, view->priv->text_renderer,
+      "compact", GAMES_INDIVIDUAL_STORE_COL_COMPACT);
+
+  /* Avatar */
+  cell = gtk_cell_renderer_pixbuf_new ();
+  gtk_tree_view_column_pack_start (col, cell, FALSE);
+  gtk_tree_view_column_set_cell_data_func (col, cell,
+      (GtkTreeCellDataFunc) individual_view_avatar_cell_data_func,
+      view, NULL);
+
+  g_object_set (cell,
+      "xpad", 0,
+      "ypad", 0,
+      "visible", FALSE,
+      "width", 32,
+      "height", 32,
+      NULL);
+
+  /* Expander */
+  cell = games_cell_renderer_expander_new ();
+  gtk_tree_view_column_pack_end (col, cell, FALSE);
+  gtk_tree_view_column_set_cell_data_func (col, cell,
+      (GtkTreeCellDataFunc) individual_view_expander_cell_data_func,
+      view, NULL);
+
+  /* Actually add the column now we have added all cell renderers */
+  gtk_tree_view_append_column (GTK_TREE_VIEW (view), col);
+}
+
+static void
+individual_view_set_view_features (GamesIndividualView *view,
+    GamesIndividualViewFeatureFlags features)
+{
+  g_return_if_fail (GAMES_IS_INDIVIDUAL_VIEW (view));
+
+  view->priv->view_features = features;
+}
+
+static void
+individual_view_dispose (GObject *object)
+{
+  GamesIndividualView *view = GAMES_INDIVIDUAL_VIEW (object);
+
+  tp_clear_object (&view->priv->store);
+  tp_clear_object (&view->priv->filter);
+
+  games_individual_view_set_live_search (view, NULL);
+
+  G_OBJECT_CLASS (games_individual_view_parent_class)->dispose (object);
+}
+
+static void
+individual_view_finalize (GObject *object)
+{
+  GamesIndividualView *self = GAMES_INDIVIDUAL_VIEW (object);
+
+  if (self->priv->expand_groups_idle_handler != 0)
+    g_source_remove (self->priv->expand_groups_idle_handler);
+  g_hash_table_unref (self->priv->expand_groups);
+
+  G_OBJECT_CLASS (games_individual_view_parent_class)->finalize (object);
+}
+
+static void
+individual_view_get_property (GObject *object,
+    guint param_id,
+    GValue *value,
+    GParamSpec *pspec)
+{
+  GamesIndividualView *self = GAMES_INDIVIDUAL_VIEW (object);
+
+  switch (param_id)
+    {
+    case PROP_STORE:
+      g_value_set_object (value, self->priv->store);
+      break;
+    case PROP_VIEW_FEATURES:
+      g_value_set_flags (value, self->priv->view_features);
+      break;
+    case PROP_SHOW_UNTRUSTED:
+      g_value_set_boolean (value, self->priv->show_untrusted);
+      break;
+    case PROP_SHOW_UNINTERESTING:
+      g_value_set_boolean (value, self->priv->show_uninteresting);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
+      break;
+    };
+}
+
+static void
+individual_view_set_property (GObject *object,
+    guint param_id,
+    const GValue *value,
+    GParamSpec *pspec)
+{
+  GamesIndividualView *view = GAMES_INDIVIDUAL_VIEW (object);
+
+  switch (param_id)
+    {
+    case PROP_STORE:
+      games_individual_view_set_store (view, g_value_get_object (value));
+      break;
+    case PROP_VIEW_FEATURES:
+      individual_view_set_view_features (view, g_value_get_flags (value));
+      break;
+    case PROP_SHOW_UNTRUSTED:
+      games_individual_view_set_show_untrusted (view,
+          g_value_get_boolean (value));
+      break;
+    case PROP_SHOW_UNINTERESTING:
+      games_individual_view_set_show_uninteresting (view,
+          g_value_get_boolean (value));
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
+      break;
+    };
+}
+
+static void
+games_individual_view_class_init (GamesIndividualViewClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->constructed = individual_view_constructed;
+  object_class->dispose = individual_view_dispose;
+  object_class->finalize = individual_view_finalize;
+  object_class->get_property = individual_view_get_property;
+  object_class->set_property = individual_view_set_property;
+
+  g_object_class_install_property (object_class,
+      PROP_STORE,
+      g_param_spec_object ("store",
+          "The store of the view",
+          "The store of the view",
+          GAMES_TYPE_INDIVIDUAL_STORE,
+          G_PARAM_READWRITE));
+  g_object_class_install_property (object_class,
+      PROP_VIEW_FEATURES,
+      g_param_spec_flags ("view-features",
+          "Features of the view",
+          "Flags for all enabled features",
+          GAMES_TYPE_INDIVIDUAL_VIEW_FEATURE_FLAGS,
+          GAMES_INDIVIDUAL_VIEW_FEATURE_NONE, G_PARAM_READWRITE));
+  g_object_class_install_property (object_class,
+      PROP_SHOW_UNTRUSTED,
+      g_param_spec_boolean ("show-untrusted",
+          "Show Untrusted Individuals",
+          "Whether the view should display untrusted individuals; "
+          "those who could not be who they say they are.",
+          TRUE, G_PARAM_READWRITE));
+  g_object_class_install_property (object_class,
+      PROP_SHOW_UNINTERESTING,
+      g_param_spec_boolean ("show-uninteresting",
+          "Show Uninteresting Individuals",
+          "Whether the view should not filter out individuals using "
+          "capability checks against view's interest. ",
+          FALSE, G_PARAM_READWRITE));
+
+  g_type_class_add_private (object_class, sizeof (GamesIndividualViewPriv));
+}
+
+static void
+games_individual_view_init (GamesIndividualView *view)
+{
+  GamesIndividualViewPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (view,
+      GAMES_TYPE_INDIVIDUAL_VIEW, GamesIndividualViewPriv);
+
+  view->priv = priv;
+
+  priv->show_untrusted = TRUE;
+  priv->show_uninteresting = FALSE;
+
+  /* Get saved group states. */
+  games_individual_groups_get_all ();
+
+  priv->expand_groups = g_hash_table_new_full (g_str_hash, g_str_equal,
+      (GDestroyNotify) g_free, NULL);
+
+  gtk_tree_view_set_row_separator_func (GTK_TREE_VIEW (view),
+      games_individual_store_row_separator_func, NULL, NULL);
+
+  /* Connect to tree view signals rather than override. */
+  g_signal_connect (view, "row-expanded",
+      G_CALLBACK (individual_view_row_expand_or_collapse_cb),
+      GINT_TO_POINTER (TRUE));
+  g_signal_connect (view, "row-collapsed",
+      G_CALLBACK (individual_view_row_expand_or_collapse_cb),
+      GINT_TO_POINTER (FALSE));
+}
+
+GamesIndividualView *
+games_individual_view_new (GamesIndividualStore *store,
+    GamesIndividualViewFeatureFlags view_features)
+{
+  g_return_val_if_fail (GAMES_IS_INDIVIDUAL_STORE (store), NULL);
+
+  return g_object_new (GAMES_TYPE_INDIVIDUAL_VIEW,
+      "store", store,
+      "view-features", view_features, NULL);
+}
+
+FolksIndividual *
+games_individual_view_dup_selected (GamesIndividualView *view)
+{
+  GtkTreeSelection *selection;
+  GtkTreeIter iter;
+  GtkTreeModel *model;
+  FolksIndividual *individual;
+
+  g_return_val_if_fail (GAMES_IS_INDIVIDUAL_VIEW (view), NULL);
+
+  selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (view));
+  if (!gtk_tree_selection_get_selected (selection, &model, &iter))
+    return NULL;
+
+  gtk_tree_model_get (model, &iter,
+      GAMES_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual, -1);
+
+  return individual;
+}
+
+void
+games_individual_view_set_live_search (GamesIndividualView *view,
+    GamesLiveSearch *search)
+{
+  /* remove old handlers if old search was not null */
+  if (view->priv->search_widget != NULL)
+    {
+      g_signal_handlers_disconnect_by_func (view,
+          individual_view_start_search_cb, NULL);
+
+      g_signal_handlers_disconnect_by_func (view->priv->search_widget,
+          individual_view_search_text_notify_cb, view);
+      g_signal_handlers_disconnect_by_func (view->priv->search_widget,
+          individual_view_search_activate_cb, view);
+      g_signal_handlers_disconnect_by_func (view->priv->search_widget,
+          individual_view_search_key_navigation_cb, view);
+      g_signal_handlers_disconnect_by_func (view->priv->search_widget,
+          individual_view_search_hide_cb, view);
+      g_signal_handlers_disconnect_by_func (view->priv->search_widget,
+          individual_view_search_show_cb, view);
+      g_object_unref (view->priv->search_widget);
+      view->priv->search_widget = NULL;
+    }
+
+  /* connect handlers if new search is not null */
+  if (search != NULL)
+    {
+      view->priv->search_widget = g_object_ref (search);
+
+      g_signal_connect (view, "start-interactive-search",
+          G_CALLBACK (individual_view_start_search_cb), NULL);
+
+      g_signal_connect (view->priv->search_widget, "notify::text",
+          G_CALLBACK (individual_view_search_text_notify_cb), view);
+      g_signal_connect (view->priv->search_widget, "activate",
+          G_CALLBACK (individual_view_search_activate_cb), view);
+      g_signal_connect (view->priv->search_widget, "key-navigation",
+          G_CALLBACK (individual_view_search_key_navigation_cb), view);
+      g_signal_connect (view->priv->search_widget, "hide",
+          G_CALLBACK (individual_view_search_hide_cb), view);
+      g_signal_connect (view->priv->search_widget, "show",
+          G_CALLBACK (individual_view_search_show_cb), view);
+    }
+}
+
+gboolean
+games_individual_view_is_searching (GamesIndividualView *self)
+{
+  g_return_val_if_fail (GAMES_IS_INDIVIDUAL_VIEW (self), FALSE);
+
+  return (self->priv->search_widget != NULL &&
+          gtk_widget_get_visible (self->priv->search_widget));
+}
+
+gboolean
+games_individual_view_get_show_untrusted (GamesIndividualView *self)
+{
+  g_return_val_if_fail (GAMES_IS_INDIVIDUAL_VIEW (self), FALSE);
+
+  return self->priv->show_untrusted;
+}
+
+void
+games_individual_view_set_show_untrusted (GamesIndividualView *self,
+    gboolean show_untrusted)
+{
+  g_return_if_fail (GAMES_IS_INDIVIDUAL_VIEW (self));
+
+  self->priv->show_untrusted = show_untrusted;
+
+  g_object_notify (G_OBJECT (self), "show-untrusted");
+  gtk_tree_model_filter_refilter (self->priv->filter);
+}
+
+GamesIndividualStore *
+games_individual_view_get_store (GamesIndividualView *self)
+{
+  g_return_val_if_fail (GAMES_IS_INDIVIDUAL_VIEW (self), NULL);
+
+  return self->priv->store;
+}
+
+void
+games_individual_view_set_store (GamesIndividualView *self,
+    GamesIndividualStore *store)
+{
+  g_return_if_fail (GAMES_IS_INDIVIDUAL_VIEW (self));
+  g_return_if_fail (store == NULL || GAMES_IS_INDIVIDUAL_STORE (store));
+
+  /* Destroy the old filter and remove the old store */
+  if (self->priv->store != NULL)
+    {
+      g_signal_handlers_disconnect_by_func (self->priv->filter,
+          individual_view_row_has_child_toggled_cb, self);
+
+      gtk_tree_view_set_model (GTK_TREE_VIEW (self), NULL);
+    }
+
+  tp_clear_object (&self->priv->filter);
+  tp_clear_object (&self->priv->store);
+
+  /* Set the new store */
+  self->priv->store = store;
+
+  if (store != NULL)
+    {
+      g_object_ref (store);
+
+      /* Create a new filter */
+      self->priv->filter = GTK_TREE_MODEL_FILTER (gtk_tree_model_filter_new (
+          GTK_TREE_MODEL (self->priv->store), NULL));
+      gtk_tree_model_filter_set_visible_func (self->priv->filter,
+          individual_view_filter_visible_func, self, NULL);
+
+      g_signal_connect (self->priv->filter, "row-has-child-toggled",
+          G_CALLBACK (individual_view_row_has_child_toggled_cb), self);
+      gtk_tree_view_set_model (GTK_TREE_VIEW (self),
+          GTK_TREE_MODEL (self->priv->filter));
+    }
+}
+
+void
+games_individual_view_start_search (GamesIndividualView *self)
+{
+  g_return_if_fail (GAMES_IS_INDIVIDUAL_VIEW (self));
+  g_return_if_fail (self->priv->search_widget != NULL);
+
+  if (gtk_widget_get_visible (GTK_WIDGET (self->priv->search_widget)))
+    gtk_widget_grab_focus (GTK_WIDGET (self->priv->search_widget));
+  else
+    gtk_widget_show (GTK_WIDGET (self->priv->search_widget));
+}
+
+void
+games_individual_view_set_custom_filter (GamesIndividualView *self,
+    GtkTreeModelFilterVisibleFunc filter,
+    gpointer data)
+{
+  self->priv->custom_filter = filter;
+  self->priv->custom_filter_data = data;
+}
+
+void
+games_individual_view_refilter (GamesIndividualView *self)
+{
+  gtk_tree_model_filter_refilter (self->priv->filter);
+}
+
+void
+games_individual_view_select_first (GamesIndividualView *self)
+{
+  GtkTreeIter iter;
+
+  gtk_tree_model_filter_refilter (self->priv->filter);
+
+  if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (self->priv->filter), &iter))
+    {
+      GtkTreeSelection *selection = gtk_tree_view_get_selection (
+          GTK_TREE_VIEW (self));
+
+      gtk_tree_selection_select_iter (selection, &iter);
+    }
+}
+
+void
+games_individual_view_set_show_uninteresting (GamesIndividualView *self,
+    gboolean show_uninteresting)
+{
+  g_return_if_fail (GAMES_IS_INDIVIDUAL_VIEW (self));
+
+
+  self->priv->show_uninteresting = show_uninteresting;
+
+  g_object_notify (G_OBJECT (self), "show-uninteresting");
+  gtk_tree_model_filter_refilter (self->priv->filter);
+}
diff --git a/libgames-contacts/games-individual-view.h b/libgames-contacts/games-individual-view.h
new file mode 100644
index 0000000..109f3f5
--- /dev/null
+++ b/libgames-contacts/games-individual-view.h
@@ -0,0 +1,127 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2012 Chandni Verma
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA  02110-1301  USA
+ */
+
+/* Much of code here is a simplified adaptation of the way contacts
+ * are displayed in Empathy, a chat application using folks.
+ * (https://live.gnome.org/Empathy)
+ * Copyright (C) 2005-2007 Imendio AB
+ * Copyright (C) 2007-2010 Collabora Ltd.
+ */
+
+
+#ifndef __GAMES_INDIVIDUAL_VIEW_H__
+#define __GAMES_INDIVIDUAL_VIEW_H__
+
+#include <gtk/gtk.h>
+
+#include <folks/folks.h>
+
+#include "games-enum-types.h"
+
+#include "games-live-search.h"
+#include "games-individual-store.h"
+
+G_BEGIN_DECLS
+#define GAMES_TYPE_INDIVIDUAL_VIEW         (games_individual_view_get_type ())
+#define GAMES_INDIVIDUAL_VIEW(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), GAMES_TYPE_INDIVIDUAL_VIEW, GamesIndividualView))
+#define GAMES_INDIVIDUAL_VIEW_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST((k), GAMES_TYPE_INDIVIDUAL_VIEW, GamesIndividualViewClass))
+#define GAMES_IS_INDIVIDUAL_VIEW(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), GAMES_TYPE_INDIVIDUAL_VIEW))
+#define GAMES_IS_INDIVIDUAL_VIEW_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), GAMES_TYPE_INDIVIDUAL_VIEW))
+#define GAMES_INDIVIDUAL_VIEW_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GAMES_TYPE_INDIVIDUAL_VIEW, GamesIndividualViewClass))
+typedef struct _GamesIndividualView GamesIndividualView;
+typedef struct _GamesIndividualViewClass GamesIndividualViewClass;
+typedef struct _GamesIndividualViewPriv GamesIndividualViewPriv;
+
+typedef enum
+{
+  GAMES_INDIVIDUAL_VIEW_FEATURE_NONE = 0,
+  GAMES_INDIVIDUAL_VIEW_FEATURE_GROUPS_SAVE = 1 << 0,
+} GamesIndividualViewFeatureFlags;
+
+struct _GamesIndividualView
+{
+  GtkTreeView parent;
+  GamesIndividualViewPriv *priv;
+};
+
+struct _GamesIndividualViewClass
+{
+  GtkTreeViewClass parent_class;
+};
+
+GType games_individual_view_get_type (void) G_GNUC_CONST;
+
+GamesIndividualView *games_individual_view_new (
+    GamesIndividualStore *store,
+    GamesIndividualViewFeatureFlags view_features);
+
+FolksIndividual *games_individual_view_dup_selected (
+    GamesIndividualView *view);
+
+GtkWidget *games_individual_view_get_individual_menu (
+    GamesIndividualView *view);
+
+GtkWidget *games_individual_view_get_group_menu (GamesIndividualView *view);
+
+void games_individual_view_set_live_search (GamesIndividualView *view,
+    GamesLiveSearch *search);
+
+gboolean games_individual_view_get_show_offline (
+    GamesIndividualView *view);
+
+void games_individual_view_set_show_offline (
+    GamesIndividualView *view,
+    gboolean show_offline);
+
+gboolean games_individual_view_get_show_untrusted (
+    GamesIndividualView *self);
+
+void games_individual_view_set_show_untrusted (GamesIndividualView *self,
+    gboolean show_untrusted);
+
+void games_individual_view_set_show_uninteresting (
+    GamesIndividualView *view,
+    gboolean show_uninteresting);
+
+gboolean games_individual_view_is_searching (
+    GamesIndividualView *view);
+
+GamesIndividualStore *games_individual_view_get_store (
+    GamesIndividualView *self);
+void games_individual_view_set_store (GamesIndividualView *self,
+    GamesIndividualStore *store);
+
+void games_individual_view_start_search (GamesIndividualView *self);
+
+gboolean individual_view_filter_default (GtkTreeModel *model,
+    GtkTreeIter *iter,
+    gpointer user_data,
+    GamesActionType interest);
+
+void games_individual_view_set_custom_filter (GamesIndividualView *self,
+    GtkTreeModelFilterVisibleFunc filter,
+    gpointer data);
+
+void games_individual_view_refilter (GamesIndividualView *self);
+
+void games_individual_view_select_first (GamesIndividualView *self);
+
+G_END_DECLS
+#endif /* __GAMES_INDIVIDUAL_VIEW_H__ */
diff --git a/libgames-contacts/games-live-search.c b/libgames-contacts/games-live-search.c
new file mode 100644
index 0000000..59b1bfd
--- /dev/null
+++ b/libgames-contacts/games-live-search.c
@@ -0,0 +1,704 @@
+/*
+ * Copyright (C) 2012 Chandni Verma
+ *
+ * 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
+ */
+
+/*
+ * Live search is taken from Empathy (https://live.gnome.org/Empathy)
+ * Copyright (C) 2010 Collabora Ltd.
+ * Copyright (C) 2007-2010 Nokia Corporation.
+ */
+
+#include <config.h>
+#include <string.h>
+
+#include <gtk/gtk.h>
+#include <gdk/gdkkeysyms.h>
+
+#include <libgames-contacts/games-ui-utils.h>
+
+#include "games-live-search.h"
+
+G_DEFINE_TYPE (GamesLiveSearch, games_live_search, GTK_TYPE_HBOX)
+
+struct _GamesLiveSearchPriv
+{
+  GtkWidget *search_entry;
+  GtkWidget *hook_widget;
+
+  GPtrArray *stripped_words;
+};
+
+enum
+{
+  PROP_0,
+  PROP_HOOK_WIDGET,
+  PROP_TEXT
+};
+
+enum
+{
+  ACTIVATE,
+  KEYNAV,
+  LAST_SIGNAL
+};
+
+static guint signals[LAST_SIGNAL];
+
+static void live_search_hook_widget_destroy_cb (GtkWidget *object,
+    gpointer user_data);
+
+/**
+ * stripped_char:
+ *
+ * Returns a stripped version of @ch, removing any case, accentuation
+ * mark, or any special mark on it.
+ **/
+static gunichar
+stripped_char (gunichar ch)
+{
+  gunichar retval = 0;
+  GUnicodeType utype;
+
+  utype = g_unichar_type (ch);
+
+  switch (utype)
+    {
+    case G_UNICODE_CONTROL:
+    case G_UNICODE_FORMAT:
+    case G_UNICODE_UNASSIGNED:
+    case G_UNICODE_NON_SPACING_MARK:
+    case G_UNICODE_COMBINING_MARK:
+    case G_UNICODE_ENCLOSING_MARK:
+      /* Ignore those */
+      break;
+    case G_UNICODE_PRIVATE_USE:
+    case G_UNICODE_SURROGATE:
+    case G_UNICODE_LOWERCASE_LETTER:
+    case G_UNICODE_MODIFIER_LETTER:
+    case G_UNICODE_OTHER_LETTER:
+    case G_UNICODE_TITLECASE_LETTER:
+    case G_UNICODE_UPPERCASE_LETTER:
+    case G_UNICODE_DECIMAL_NUMBER:
+    case G_UNICODE_LETTER_NUMBER:
+    case G_UNICODE_OTHER_NUMBER:
+    case G_UNICODE_CONNECT_PUNCTUATION:
+    case G_UNICODE_DASH_PUNCTUATION:
+    case G_UNICODE_CLOSE_PUNCTUATION:
+    case G_UNICODE_FINAL_PUNCTUATION:
+    case G_UNICODE_INITIAL_PUNCTUATION:
+    case G_UNICODE_OTHER_PUNCTUATION:
+    case G_UNICODE_OPEN_PUNCTUATION:
+    case G_UNICODE_CURRENCY_SYMBOL:
+    case G_UNICODE_MODIFIER_SYMBOL:
+    case G_UNICODE_MATH_SYMBOL:
+    case G_UNICODE_OTHER_SYMBOL:
+    case G_UNICODE_LINE_SEPARATOR:
+    case G_UNICODE_PARAGRAPH_SEPARATOR:
+    case G_UNICODE_SPACE_SEPARATOR:
+    default:
+      ch = g_unichar_tolower (ch);
+      g_unichar_fully_decompose (ch, FALSE, &retval, 1);
+    }
+
+  return retval;
+}
+
+static void
+append_word (GPtrArray **word_array,
+    GString **word)
+{
+  if (*word != NULL)
+    {
+      if (*word_array == NULL)
+        *word_array = g_ptr_array_new_with_free_func (g_free);
+      g_ptr_array_add (*word_array, g_string_free (*word, FALSE));
+      *word = NULL;
+    }
+}
+
+GPtrArray *
+games_live_search_strip_utf8_string (const gchar *string)
+{
+  GPtrArray *word_array = NULL;
+  GString *word = NULL;
+  const gchar *p;
+
+  if (STR_EMPTY (string))
+    return NULL;
+
+  for (p = string; *p != '\0'; p = g_utf8_next_char (p))
+    {
+      gunichar sc;
+
+      /* Make the char lower-case, remove its accentuation marks, and ignore it
+       * if it is just unicode marks */
+      sc = stripped_char (g_utf8_get_char (p));
+      if (sc == 0)
+        continue;
+
+      /* If it is not alpha-num, it is separator between words */
+      if (!g_unichar_isalnum (sc))
+        {
+          append_word (&word_array, &word);
+          continue;
+        }
+
+      /* It is alpha-num, append this char to current word, or start new word */
+      if (word == NULL)
+        word = g_string_new (NULL);
+      g_string_append_unichar (word, sc);
+    }
+
+  append_word (&word_array, &word);
+
+  return word_array;
+}
+
+static gboolean
+live_search_match_prefix (const gchar *string,
+    const gchar *prefix)
+{
+  const gchar *p;
+  const gchar *prefix_p;
+  gboolean next_word = FALSE;
+
+  if (prefix == NULL || prefix[0] == 0)
+    return TRUE;
+
+  if (STR_EMPTY (string))
+    return FALSE;
+
+  prefix_p = prefix;
+  for (p = string; *p != '\0'; p = g_utf8_next_char (p))
+    {
+      gunichar sc;
+
+      /* Make the char lower-case, remove its accentuation marks, and ignore it
+       * if it is just unicode marks */
+      sc = stripped_char (g_utf8_get_char (p));
+      if (sc == 0)
+        continue;
+
+      /* If we want to go to next word, ignore alpha-num chars */
+      if (next_word && g_unichar_isalnum (sc))
+        continue;
+      next_word = FALSE;
+
+      /* Ignore word separators */
+      if (!g_unichar_isalnum (sc))
+        continue;
+
+      /* If this char does not match prefix_p, go to next word and start again
+       * from the beginning of prefix */
+      if (sc != g_utf8_get_char (prefix_p))
+        {
+          next_word = TRUE;
+          prefix_p = prefix;
+          continue;
+        }
+
+      /* prefix_p match, verify to next char. If this was the last of prefix,
+       * it means it completely machted and we are done. */
+      prefix_p = g_utf8_next_char (prefix_p);
+      if (*prefix_p == '\0')
+        return TRUE;
+    }
+
+  return FALSE;
+}
+
+gboolean
+games_live_search_match_words (const gchar *string,
+    GPtrArray *words)
+{
+  guint i;
+
+  if (words == NULL)
+    return TRUE;
+
+  for (i = 0; i < words->len; i++)
+    if (!live_search_match_prefix (string, g_ptr_array_index (words, i)))
+      return FALSE;
+
+  return TRUE;
+}
+
+static gboolean
+fire_key_navigation_sig (GamesLiveSearch *self,
+    GdkEventKey *event)
+{
+  gboolean ret;
+
+  g_signal_emit (self, signals[KEYNAV], 0, event, &ret);
+  return ret;
+}
+
+static gboolean
+live_search_entry_key_pressed_cb (GtkEntry *entry,
+    GdkEventKey *event,
+    gpointer user_data)
+{
+  GamesLiveSearch *self = GAMES_LIVE_SEARCH (user_data);
+
+  /* if esc key pressed, hide the search */
+  if (event->keyval == GDK_KEY_Escape)
+    {
+      gtk_widget_hide (GTK_WIDGET (self));
+      return TRUE;
+    }
+
+  /* emit key navigation signal, so other widgets can respond to it properly */
+  if (event->keyval == GDK_KEY_Up || event->keyval == GDK_KEY_Down
+      || event->keyval == GDK_KEY_Page_Up || event->keyval == GDK_KEY_Page_Down)
+     {
+       return fire_key_navigation_sig (self, event);
+     }
+
+  if (event->keyval == GDK_KEY_Home || event->keyval == GDK_KEY_End ||
+      event->keyval == GDK_KEY_space)
+    {
+      /* If the live search is visible, the entry should catch the Home/End
+       * and space events */
+      if (!gtk_widget_get_visible (GTK_WIDGET (self)))
+        {
+          return fire_key_navigation_sig (self, event);
+        }
+    }
+
+  return FALSE;
+}
+
+static void
+live_search_text_changed (GtkEntry *entry,
+    gpointer user_data)
+{
+  GamesLiveSearch *self = GAMES_LIVE_SEARCH (user_data);
+  const gchar *text;
+
+  text = gtk_entry_get_text (entry);
+
+  if (STR_EMPTY (text))
+    gtk_widget_hide (GTK_WIDGET (self));
+  else
+    gtk_widget_show (GTK_WIDGET (self));
+
+  if (self->priv->stripped_words != NULL)
+    g_ptr_array_unref (self->priv->stripped_words);
+
+  self->priv->stripped_words = games_live_search_strip_utf8_string (text);
+
+  g_object_notify (G_OBJECT (self), "text");
+}
+
+static void
+live_search_close_pressed (GtkEntry *entry,
+    GtkEntryIconPosition icon_pos,
+    GdkEvent *event,
+    gpointer user_data)
+{
+  GamesLiveSearch *self = GAMES_LIVE_SEARCH (user_data);
+
+  gtk_widget_hide (GTK_WIDGET (self));
+}
+
+static gboolean
+live_search_key_press_event_cb (GtkWidget *widget,
+    GdkEventKey *event,
+    gpointer user_data)
+{
+  GamesLiveSearch *self = GAMES_LIVE_SEARCH (user_data);
+  GdkEvent *new_event;
+  gboolean ret;
+
+  /* dont forward this event to the entry, else the event is consumed by the
+   * entry and does not close the window */
+  if (!gtk_widget_get_visible (GTK_WIDGET (self)) &&
+      event->keyval == GDK_KEY_Escape)
+    return FALSE;
+
+  /* do not show the search if CTRL and/or ALT are pressed with a key
+   * this is needed, because otherwise the CTRL + F accel would not work,
+   * because the entry consumes it */
+  if (event->state & (GDK_MOD1_MASK | GDK_CONTROL_MASK) ||
+      event->keyval == GDK_KEY_Control_L ||
+      event->keyval == GDK_KEY_Control_R)
+    return FALSE;
+
+  /* dont forward the up/down and Page Up/Down arrow keys to the entry,
+   * they are needed for navigation in the treeview and are not needed in
+   * the search entry */
+   if (event->keyval == GDK_KEY_Up || event->keyval == GDK_KEY_Down ||
+       event->keyval == GDK_KEY_Page_Up || event->keyval == GDK_KEY_Page_Down)
+     return FALSE;
+
+   if (event->keyval == GDK_KEY_Home || event->keyval == GDK_KEY_End ||
+       event->keyval == GDK_KEY_space)
+     {
+       /* Home/End and space keys have to be forwarded to the entry only if
+        * the live search is visible (to move the cursor inside the entry). */
+       if (!gtk_widget_get_visible (GTK_WIDGET (self)))
+         return FALSE;
+     }
+
+   /* Don't forward shift keys events as focusing the search entry would
+    * cancel an in-progress editing on a cell renderer (like when renaming a
+    * group). There is no point focusing it anyway as we don't display the
+    * search entry when only a shift key is pressed. */
+   if (event->keyval == GDK_KEY_Shift_L ||
+       event->keyval == GDK_KEY_Shift_R)
+       return FALSE;
+
+  /* realize the widget if it is not realized yet */
+  gtk_widget_realize (self->priv->search_entry);
+  if (!gtk_widget_has_focus (self->priv->search_entry))
+    {
+      gtk_widget_grab_focus (self->priv->search_entry);
+      gtk_editable_set_position (GTK_EDITABLE (self->priv->search_entry), -1);
+    }
+
+  /* forward the event to the search entry */
+  new_event = gdk_event_copy ((GdkEvent *) event);
+  ret = gtk_widget_event (self->priv->search_entry, new_event);
+  gdk_event_free (new_event);
+
+  return ret;
+}
+
+static void
+live_search_entry_activate_cb (GtkEntry *entry,
+    GamesLiveSearch *self)
+{
+  g_signal_emit (self, signals[ACTIVATE], 0);
+}
+
+static void
+live_search_release_hook_widget (GamesLiveSearch *self)
+{
+  /* remove old handlers if old source was not null */
+  if (self->priv->hook_widget != NULL)
+    {
+      g_signal_handlers_disconnect_by_func (self->priv->hook_widget,
+          live_search_key_press_event_cb, self);
+      g_signal_handlers_disconnect_by_func (self->priv->hook_widget,
+          live_search_hook_widget_destroy_cb, self);
+      g_object_unref (self->priv->hook_widget);
+      self->priv->hook_widget = NULL;
+    }
+}
+
+static void
+live_search_hook_widget_destroy_cb (GtkWidget *object,
+    gpointer user_data)
+{
+  GamesLiveSearch *self = GAMES_LIVE_SEARCH (user_data);
+
+  /* unref the hook widget and hide search */
+  gtk_widget_hide (GTK_WIDGET (self));
+  live_search_release_hook_widget (self);
+}
+
+static void
+live_search_dispose (GObject *obj)
+{
+  GamesLiveSearch *self = GAMES_LIVE_SEARCH (obj);
+
+  live_search_release_hook_widget (self);
+
+  if (G_OBJECT_CLASS (games_live_search_parent_class)->dispose != NULL)
+    G_OBJECT_CLASS (games_live_search_parent_class)->dispose (obj);
+}
+
+static void
+live_search_finalize (GObject *obj)
+{
+  GamesLiveSearch *self = GAMES_LIVE_SEARCH (obj);
+
+  if (self->priv->stripped_words != NULL)
+    g_ptr_array_unref (self->priv->stripped_words);
+
+  if (G_OBJECT_CLASS (games_live_search_parent_class)->finalize != NULL)
+    G_OBJECT_CLASS (games_live_search_parent_class)->finalize (obj);
+}
+
+static void
+live_search_get_property (GObject *object,
+    guint param_id,
+    GValue *value,
+    GParamSpec *pspec)
+{
+  GamesLiveSearch *self = GAMES_LIVE_SEARCH (object);
+
+  switch (param_id)
+    {
+    case PROP_HOOK_WIDGET:
+      g_value_set_object (value, games_live_search_get_hook_widget (self));
+      break;
+    case PROP_TEXT:
+      g_value_set_string (value, games_live_search_get_text (self));
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
+      break;
+    }
+}
+
+static void
+live_search_set_property (GObject *object,
+    guint param_id,
+    const GValue *value,
+    GParamSpec *pspec)
+{
+  GamesLiveSearch *self = GAMES_LIVE_SEARCH (object);
+
+  switch (param_id) {
+  case PROP_HOOK_WIDGET:
+    games_live_search_set_hook_widget (self, g_value_get_object (value));
+    break;
+  case PROP_TEXT:
+    games_live_search_set_text (self, g_value_get_string (value));
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
+    break;
+  };
+}
+
+static void
+live_search_unmap (GtkWidget *widget)
+{
+  GamesLiveSearch *self = GAMES_LIVE_SEARCH (widget);
+
+  GTK_WIDGET_CLASS (games_live_search_parent_class)->unmap (widget);
+
+  /* unmap can happen if a parent gets hidden, in that case we want to hide
+   * the live search as well, so when it gets mapped again, the live search
+   * won't be shown. */
+  gtk_widget_hide (widget);
+
+  gtk_entry_set_text (GTK_ENTRY (self->priv->search_entry), "");
+  gtk_widget_grab_focus (self->priv->hook_widget);
+}
+
+static void
+live_search_show (GtkWidget *widget)
+{
+  GamesLiveSearch *self = GAMES_LIVE_SEARCH (widget);
+
+  if (!gtk_widget_has_focus (self->priv->search_entry))
+    gtk_widget_grab_focus (self->priv->search_entry);
+
+  GTK_WIDGET_CLASS (games_live_search_parent_class)->show (widget);
+}
+
+static void
+live_search_grab_focus (GtkWidget *widget)
+{
+  GamesLiveSearch *self = GAMES_LIVE_SEARCH (widget);
+
+  if (!gtk_widget_has_focus (self->priv->search_entry))
+    {
+      gtk_widget_grab_focus (self->priv->search_entry);
+      gtk_editable_set_position (GTK_EDITABLE (self->priv->search_entry), -1);
+    }
+}
+
+static void
+games_live_search_class_init (GamesLiveSearchClass *klass)
+{
+  GObjectClass *object_class = (GObjectClass *) klass;
+  GtkWidgetClass *widget_class = (GtkWidgetClass *) klass;
+  GParamSpec *param_spec;
+
+  object_class->finalize = live_search_finalize;
+  object_class->dispose = live_search_dispose;
+  object_class->get_property = live_search_get_property;
+  object_class->set_property = live_search_set_property;
+
+  widget_class->unmap = live_search_unmap;
+  widget_class->show = live_search_show;
+  widget_class->grab_focus = live_search_grab_focus;
+
+  signals[ACTIVATE] = g_signal_new ("activate",
+      G_TYPE_FROM_CLASS (object_class),
+      G_SIGNAL_RUN_LAST,
+      0,
+      NULL, NULL,
+      g_cclosure_marshal_generic,
+      G_TYPE_NONE, 0);
+
+  signals[KEYNAV] = g_signal_new ("key-navigation",
+      G_TYPE_FROM_CLASS (object_class),
+      G_SIGNAL_RUN_LAST,
+      0,
+      g_signal_accumulator_true_handled, NULL,
+      g_cclosure_marshal_generic,
+      G_TYPE_BOOLEAN, 1, GDK_TYPE_EVENT | G_SIGNAL_TYPE_STATIC_SCOPE);
+
+  param_spec = g_param_spec_object ("hook-widget", "Live Search Hook Widget",
+      "The live search catches key-press-events on this widget",
+      GTK_TYPE_WIDGET, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (object_class, PROP_HOOK_WIDGET,
+      param_spec);
+
+  param_spec = g_param_spec_string ("text", "Live Search Text",
+      "The text of the live search entry",
+      "", G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (object_class, PROP_TEXT, param_spec);
+
+  g_type_class_add_private (klass, sizeof (GamesLiveSearchPriv));
+}
+
+static void
+games_live_search_init (GamesLiveSearch *self)
+{
+  GamesLiveSearchPriv *priv =
+    G_TYPE_INSTANCE_GET_PRIVATE ((self), GAMES_TYPE_LIVE_SEARCH,
+        GamesLiveSearchPriv);
+
+  gtk_widget_set_no_show_all (GTK_WIDGET (self), TRUE);
+
+  priv->search_entry = gtk_entry_new ();
+  gtk_entry_set_icon_from_stock (GTK_ENTRY (priv->search_entry),
+      GTK_ENTRY_ICON_SECONDARY, GTK_STOCK_CLOSE);
+  gtk_entry_set_icon_activatable (GTK_ENTRY (priv->search_entry),
+      GTK_ENTRY_ICON_SECONDARY, TRUE);
+  gtk_entry_set_icon_sensitive (GTK_ENTRY (priv->search_entry),
+      GTK_ENTRY_ICON_SECONDARY, TRUE);
+  gtk_widget_show (priv->search_entry);
+
+  gtk_box_pack_start (GTK_BOX (self), priv->search_entry, TRUE, TRUE, 0);
+
+  g_signal_connect (priv->search_entry, "icon_release",
+      G_CALLBACK (live_search_close_pressed), self);
+  g_signal_connect (priv->search_entry, "changed",
+      G_CALLBACK (live_search_text_changed), self);
+  g_signal_connect (priv->search_entry, "key-press-event",
+      G_CALLBACK (live_search_entry_key_pressed_cb), self);
+  g_signal_connect (priv->search_entry, "activate",
+      G_CALLBACK (live_search_entry_activate_cb), self);
+
+  priv->hook_widget = NULL;
+
+  self->priv = priv;
+}
+
+GtkWidget *
+games_live_search_new (GtkWidget *hook)
+{
+  g_return_val_if_fail (hook == NULL || GTK_IS_WIDGET (hook), NULL);
+
+  return g_object_new (GAMES_TYPE_LIVE_SEARCH,
+      "hook-widget", hook,
+      NULL);
+}
+
+/* public methods */
+
+GtkWidget *
+games_live_search_get_hook_widget (GamesLiveSearch *self)
+{
+  g_return_val_if_fail (GAMES_IS_LIVE_SEARCH (self), NULL);
+
+  return self->priv->hook_widget;
+}
+
+void
+games_live_search_set_hook_widget (GamesLiveSearch *self,
+    GtkWidget *hook)
+{
+  g_return_if_fail (GAMES_IS_LIVE_SEARCH (self));
+  g_return_if_fail (hook == NULL || GTK_IS_WIDGET (hook));
+
+  /* release the actual widget */
+  live_search_release_hook_widget (self);
+
+  /* connect handlers if new source is not null */
+  if (hook != NULL)
+    {
+      self->priv->hook_widget = g_object_ref (hook);
+      g_signal_connect (self->priv->hook_widget, "key-press-event",
+          G_CALLBACK (live_search_key_press_event_cb),
+          self);
+      g_signal_connect (self->priv->hook_widget, "destroy",
+          G_CALLBACK (live_search_hook_widget_destroy_cb),
+          self);
+    }
+}
+
+const gchar *
+games_live_search_get_text (GamesLiveSearch *self)
+{
+  g_return_val_if_fail (GAMES_IS_LIVE_SEARCH (self), NULL);
+
+  return gtk_entry_get_text (GTK_ENTRY (self->priv->search_entry));
+}
+
+void
+games_live_search_set_text (GamesLiveSearch *self,
+    const gchar *text)
+{
+  g_return_if_fail (GAMES_IS_LIVE_SEARCH (self));
+  g_return_if_fail (text != NULL);
+
+  gtk_entry_set_text (GTK_ENTRY (self->priv->search_entry), text);
+}
+
+/**
+ * games_live_search_match:
+ * @self: a #GamesLiveSearch
+ * @string: a string where to search, must be valid UTF-8.
+ *
+ * Search if one of the words in @string string starts with the current text
+ * of @self.
+ *
+ * Searching for "aba" in "Abasto" will match, searching in "Moraba" will not,
+ * and searching in "A tool (abacus)" will do.
+ *
+ * The match is not case-sensitive, and regardless of the accentuation marks.
+ *
+ * Returns: %TRUE if a match is found, %FALSE otherwise.
+ *
+ **/
+gboolean
+games_live_search_match (GamesLiveSearch *self,
+    const gchar *string)
+{
+  g_return_val_if_fail (GAMES_IS_LIVE_SEARCH (self), FALSE);
+
+  return games_live_search_match_words (string, self->priv->stripped_words);
+}
+
+gboolean
+games_live_search_match_string (const gchar *string,
+    const gchar *prefix)
+{
+  GPtrArray *words;
+  gboolean match;
+
+  words = games_live_search_strip_utf8_string (prefix);
+  match = games_live_search_match_words (string, words);
+  if (words != NULL)
+    g_ptr_array_unref (words);
+
+  return match;
+}
+
+GPtrArray *
+games_live_search_get_words (GamesLiveSearch *self)
+{
+  return self->priv->stripped_words;
+}
diff --git a/libgames-contacts/games-live-search.h b/libgames-contacts/games-live-search.h
new file mode 100644
index 0000000..7d4dd9b
--- /dev/null
+++ b/libgames-contacts/games-live-search.h
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2012 Chandni Verma
+ *
+ * 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
+ */
+
+ /*
+ * Live search is taken from Empathy (https://live.gnome.org/Empathy)
+ * Copyright (C) 2010 Collabora Ltd.
+ * Copyright (C) 2007-2010 Nokia Corporation.
+ */
+
+#ifndef __GAMES_LIVE_SEARCH_H__
+#define __GAMES_LIVE_SEARCH_H__
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define GAMES_TYPE_LIVE_SEARCH         (games_live_search_get_type ())
+#define GAMES_LIVE_SEARCH(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), GAMES_TYPE_LIVE_SEARCH, GamesLiveSearch))
+#define GAMES_LIVE_SEARCH_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST ((k), GAMES_TYPE_LIVE_SEARCH, GamesLiveSearchClass))
+#define GAMES_IS_LIVE_SEARCH(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), GAMES_TYPE_LIVE_SEARCH))
+#define GAMES_IS_LIVE_SEARCH_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), GAMES_TYPE_LIVE_SEARCH))
+#define GAMES_LIVE_SEARCH_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GAMES_TYPE_LIVE_SEARCH, GamesLiveSearchClass))
+
+typedef struct _GamesLiveSearch      GamesLiveSearch;
+typedef struct _GamesLiveSearchClass GamesLiveSearchClass;
+typedef struct _GamesLiveSearchPriv  GamesLiveSearchPriv;
+
+struct _GamesLiveSearch {
+  GtkHBox parent;
+
+  /*<private>*/
+  GamesLiveSearchPriv *priv;
+};
+
+struct _GamesLiveSearchClass {
+  GtkHBoxClass parent_class;
+};
+
+GType games_live_search_get_type (void) G_GNUC_CONST;
+GtkWidget *games_live_search_new (GtkWidget *hook);
+
+GtkWidget *games_live_search_get_hook_widget (GamesLiveSearch *self);
+void games_live_search_set_hook_widget (GamesLiveSearch *self,
+    GtkWidget *hook);
+
+const gchar *games_live_search_get_text (GamesLiveSearch *self);
+void games_live_search_set_text (GamesLiveSearch *self,
+    const gchar *text);
+
+gboolean games_live_search_match (GamesLiveSearch *self,
+    const gchar *string);
+
+GPtrArray * games_live_search_strip_utf8_string (const gchar *string);
+
+gboolean games_live_search_match_words (const gchar *string,
+    GPtrArray *words);
+
+GPtrArray * games_live_search_get_words (GamesLiveSearch *self);
+
+/* Made public for unit tests */
+gboolean games_live_search_match_string (const gchar *string,
+   const gchar *prefix);
+
+G_END_DECLS
+
+#endif /* __GAMES_LIVE_SEARCH_H__ */
diff --git a/libgames-contacts/games-ui-utils.c b/libgames-contacts/games-ui-utils.c
index 668cae7..9917996 100644
--- a/libgames-contacts/games-ui-utils.c
+++ b/libgames-contacts/games-ui-utils.c
@@ -39,6 +39,7 @@
 #include <folks/folks.h>
 
 #include "games-ui-utils.h"
+#include "games-live-search.h"
 
 /* Contact presence image names */
 
@@ -972,6 +973,70 @@ out:
   g_free (cmd);
 }
 
+/* @words = games_live_search_strip_utf8_string (@text);
+ *
+ * User has to pass both so we don't have to compute @words ourself each time
+ * this function is called. */
+gboolean
+games_individual_match_string (FolksIndividual *individual,
+    const char *text,
+    GPtrArray *words)
+{
+  const gchar *str;
+  GeeSet *personas;
+  GeeIterator *iter;
+  gboolean retval = FALSE;
+
+  /* check alias name */
+  str = folks_alias_details_get_alias (FOLKS_ALIAS_DETAILS (individual));
+
+  if (games_live_search_match_words (str, words))
+    return TRUE;
+
+  personas = folks_individual_get_personas (individual);
+
+  /* check contact id, remove the @server.com part */
+  iter = gee_iterable_iterator (GEE_ITERABLE (personas));
+  while (retval == FALSE && gee_iterator_next (iter))
+    {
+      FolksPersona *persona = gee_iterator_get (iter);
+      const gchar *p;
+
+      if (games_folks_persona_is_interesting (persona))
+        {
+          str = folks_persona_get_display_id (persona);
+
+          /* Accept the persona if @text is a full prefix of his ID; that allows
+           * user to find, say, a jabber contact by typing his JID. */
+          if (g_str_has_prefix (str, text))
+            {
+              retval = TRUE;
+            }
+          else
+            {
+              gchar *dup_str = NULL;
+              gboolean visible;
+
+              p = strstr (str, "@");
+              if (p != NULL)
+                str = dup_str = g_strndup (str, p - str);
+
+              visible = games_live_search_match_words (str, words);
+              g_free (dup_str);
+              if (visible)
+                retval = TRUE;
+            }
+        }
+      g_clear_object (&persona);
+    }
+  g_clear_object (&iter);
+
+  /* FIXME: Add more rules here, we could check phone numbers in
+   * contact's vCard for example. */
+  return retval;
+}
+
+
 /* Most of the workspace manipulation code has been copied from libwnck
  * Copyright (C) 2001 Havoc Pennington
  * Copyright (C) 2005-2007 Vincent Untz
diff --git a/libgames-contacts/games-ui-utils.h b/libgames-contacts/games-ui-utils.h
index 9fc7f54..e176d03 100644
--- a/libgames-contacts/games-ui-utils.h
+++ b/libgames-contacts/games-ui-utils.h
@@ -126,10 +126,9 @@ void        games_url_show                            (GtkWidget        *parent,
 /* Misc */
 gint64      games_get_current_action_time             (void);
 
-gboolean games_individual_match_string (
-    FolksIndividual *individual,
-    const gchar *text,
-    GPtrArray *words);
+gboolean    games_individual_match_string             (FolksIndividual *individual,
+                                                       const gchar     *text,
+                                                       GPtrArray       *words);
 
 void games_launch_program (const gchar *dir,
     const gchar *name,



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]