Blob Blame History Raw
/*
 * gnome-keyring
 *
 * Copyright (C) 2010 Stefan Walter
 * Copyright (C) 2011 Collabora Ltd.
 *
 * This program 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 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this program; if not, see <http://www.gnu.org/licenses/>.
 *
 * Author: Stef Walter <stefw@collabora.co.uk>
 */

#include "config.h"

#include "gcr-collection-model.h"

#include "ui/gcr-enum-types.h"

#include <gtk/gtk.h>

#include <string.h>
#include <unistd.h>

/**
 * SECTION:gcr-collection-model
 * @title: GcrCollectionModel
 * @short_description: A GtkTreeModel that represents a collection
 *
 * This is an implementation of #GtkTreeModel which represents the objects in
 * the a #GcrCollection. As objects are added or removed from the collection,
 * rows are added and removed from this model.
 *
 * The row values come from the properties of the objects in the collection. Use
 * gcr_collection_model_new() to create a new collection model. To have more
 * control over the values use a set of #GcrColumn structures to define the
 * columns. This can be done with gcr_collection_model_new_full() or
 * gcr_collection_model_set_columns().
 *
 * Each row can have a selected state, which is represented by a boolean column.
 * The selected state can be toggled with gcr_collection_model_toggle_selected()
 * or set with gcr_collection_model_set_selected_objects() and retrieved with
 * gcr_collection_model_get_selected_objects().
 *
 * To determine which object a row represents and vice versa, use the
 * gcr_collection_model_iter_for_object() or gcr_collection_model_object_for_iter()
 * functions.
 */

/**
 * GcrCollectionModel:
 *
 * A #GtkTreeModel which contains a row for each object in a #GcrCollection.
 */

/**
 * GcrCollectionModelClass:
 * @parent_class: The parent class
 *
 * The class for #GcrCollectionModel.
 */

/**
 * GcrCollectionModelMode:
 * @GCR_COLLECTION_MODEL_LIST: only objects in the top collection, no child objects
 * @GCR_COLLECTION_MODEL_TREE: show objects in the collection, and child objects in a tree form
 *
 * If set GcrCollectionModel is created with a mode of %GCR_COLLECTION_MODEL_TREE,
 * then any included objects that are themselves a #GcrCollection, will have all child
 * objects include as child rows in a tree form.
 */

#define COLLECTION_MODEL_STAMP 0xAABBCCDD

enum {
	PROP_0,
	PROP_COLLECTION,
	PROP_COLUMNS,
	PROP_MODE
};

typedef struct {
	GObject *object;
	GSequenceIter *parent;
	GSequence *children;
} GcrCollectionRow;

typedef struct {
	GtkTreeIterCompareFunc sort_func;
	gpointer user_data;
	GDestroyNotify destroy_func;
} GcrCollectionSortClosure;

typedef struct _GcrCollectionColumn {
	gchar *property;
	GType *type;
	GtkTreeIterCompareFunc sort_func;
	gpointer sort_data;
	GDestroyNotify sort_destroy;
} GcrCollectionColumn;

struct _GcrCollectionModelPrivate {
	GcrCollectionModelMode mode;
	GcrCollection *collection;
	GHashTable *selected;
	GSequence *root_sequence;
	GHashTable *object_to_seq;

	const GcrColumn *columns;
	guint n_columns;

	/* Sort information */
	gint sort_column_id;
	GtkSortType sort_order_type;
	GcrCollectionSortClosure *column_sort_closures;
	GcrCollectionSortClosure default_sort_closure;

	/* Sequence ordering information */
	GCompareDataFunc order_current;
	gpointer order_argument;
};

/* Forward declarations */
static void gcr_collection_model_tree_model_init (GtkTreeModelIface *iface);
static void gcr_collection_model_tree_sortable_init (GtkTreeSortableIface *iface);

G_DEFINE_TYPE_EXTENDED (GcrCollectionModel, gcr_collection_model, G_TYPE_OBJECT, 0,
                        G_IMPLEMENT_INTERFACE (GTK_TYPE_TREE_MODEL, gcr_collection_model_tree_model_init)
                        G_IMPLEMENT_INTERFACE (GTK_TYPE_TREE_SORTABLE, gcr_collection_model_tree_sortable_init)
);

typedef gint (*CompareValueFunc) (const GValue *va,
                                  const GValue *vb);

static gint
compare_int_value (const GValue *va,
                   const GValue *vb)
{
	gint a = g_value_get_int (va);
	gint b = g_value_get_int (vb);
	if (a > b) return 1;
	else if (a < b) return -1;
	return 0;
}

static gint
compare_uint_value (const GValue *va,
                    const GValue *vb)
{
	guint a = g_value_get_uint (va);
	guint b = g_value_get_uint (vb);
	if (a > b) return 1;
	else if (a < b) return -1;
	return 0;
}

static gint
compare_long_value (const GValue *va,
                    const GValue *vb)
{
	glong a = g_value_get_long (va);
	glong b = g_value_get_long (vb);
	if (a > b) return 1;
	else if (a < b) return -1;
	return 0;
}

static gint
compare_ulong_value (const GValue *va,
                     const GValue *vb)
{
	gulong a = g_value_get_ulong (va);
	gulong b = g_value_get_ulong (vb);
	if (a > b) return 1;
	else if (a < b) return -1;
	return 0;
}

static gint
compare_string_value (const GValue *va,
                      const GValue *vb)
{
	const gchar *a = g_value_get_string (va);
	const gchar *b = g_value_get_string (vb);
	gchar *case_a;
	gchar *case_b;
	gboolean ret;

	if (a == b)
		return 0;
	else if (!a)
		return -1;
	else if (!b)
		return 1;

	case_a = g_utf8_casefold (a, -1);
	case_b = g_utf8_casefold (b, -1);
	ret = g_utf8_collate (case_a, case_b);
	g_free (case_a);
	g_free (case_b);

	return ret;
}

static gint
compare_date_value (const GValue *va,
                    const GValue *vb)
{
	GDate *a = g_value_get_boxed (va);
	GDate *b = g_value_get_boxed (vb);

	if (a == b)
		return 0;
	else if (!a)
		return -1;
	else if (!b)
		return 1;
	else
		return g_date_compare (a, b);
}

static CompareValueFunc
lookup_compare_func (GType type)
{
	switch (type) {
	case G_TYPE_INT:
		return compare_int_value;
	case G_TYPE_UINT:
		return compare_uint_value;
	case G_TYPE_LONG:
		return compare_long_value;
	case G_TYPE_ULONG:
		return compare_ulong_value;
	case G_TYPE_STRING:
		return compare_string_value;
	}

	if (type == G_TYPE_DATE)
		return compare_date_value;

	return NULL;
}

static gint
order_sequence_by_closure (gconstpointer a,
                           gconstpointer b,
                           gpointer user_data)
{
	GcrCollectionModel *self = GCR_COLLECTION_MODEL (user_data);
	GcrCollectionSortClosure *closure = self->pv->order_argument;
	const GcrCollectionRow *row_a = a;
	const GcrCollectionRow *row_b = b;
	GtkTreeIter iter_a;
	GtkTreeIter iter_b;

	g_assert (closure);
	g_assert (closure->sort_func);

	if (!gcr_collection_model_iter_for_object (self, row_a->object, &iter_a))
		g_return_val_if_reached (0);
	if (!gcr_collection_model_iter_for_object (self, row_b->object, &iter_b))
		g_return_val_if_reached (0);

	return (closure->sort_func) (GTK_TREE_MODEL (self),
	                             &iter_a, &iter_b, closure->user_data);
}

static gint
order_sequence_by_closure_reverse (gconstpointer a,
                                   gconstpointer b,
                                   gpointer user_data)
{
	return 0 - order_sequence_by_closure (a, b, user_data);
}

static gint
order_sequence_as_unsorted (gconstpointer a,
                            gconstpointer b,
                            gpointer user_data)
{
	const GcrCollectionRow *row_a = a;
	const GcrCollectionRow *row_b = b;
	return GPOINTER_TO_INT (row_a->object) - GPOINTER_TO_INT (row_b->object);
}

static gint
order_sequence_as_unsorted_reverse (gconstpointer a,
                                    gconstpointer b,
                                    gpointer user_data)
{
	const GcrCollectionRow *row_a = a;
	const GcrCollectionRow *row_b = b;
	return GPOINTER_TO_INT (row_b->object) - GPOINTER_TO_INT (row_a->object);
}

static void
lookup_object_property (GObject *object,
                        const gchar *property_name,
                        GValue *value)
{
	if (g_object_class_find_property (G_OBJECT_GET_CLASS (object), property_name))
		g_object_get_property (object, property_name, value);

	/* Other types have sane defaults */
	else if (G_VALUE_TYPE (value) == G_TYPE_STRING)
		g_value_set_string (value, "");
}

static gint
order_sequence_by_property (gconstpointer a,
                            gconstpointer b,
                            gpointer user_data)
{
	const GcrCollectionRow *row_a = a;
	const GcrCollectionRow *row_b = b;
	GcrCollectionModel *self = GCR_COLLECTION_MODEL (user_data);
	const GcrColumn *column = self->pv->order_argument;
	GValue value_a = { 0, };
	GValue value_b = { 0, };
	CompareValueFunc compare;
	gint ret;

	g_assert (column);

	/* Sort according to property values */
	column = &self->pv->columns[self->pv->sort_column_id];
	g_value_init (&value_a, column->property_type);
	lookup_object_property (row_a->object, column->property_name, &value_a);
	g_value_init (&value_b, column->property_type);
	lookup_object_property (row_b->object, column->property_name, &value_b);

	compare = lookup_compare_func (column->property_type);
	g_assert (compare != NULL);

	ret = (compare) (&value_a, &value_b);

	g_value_unset (&value_a);
	g_value_unset (&value_b);

	return ret;
}

static gint
order_sequence_by_property_reverse (gconstpointer a,
                                    gconstpointer b,
                                    gpointer user_data)
{
	return 0 - order_sequence_by_property (a, b, user_data);
}

static GHashTable*
selected_hash_table_new (void)
{
	return g_hash_table_new (g_direct_hash, g_direct_equal);
}

static gboolean
sequence_iter_to_tree (GcrCollectionModel *self,
                       GSequenceIter *seq,
                       GtkTreeIter *iter)
{
	GcrCollectionRow *row;

	g_return_val_if_fail (seq != NULL, FALSE);

	if (g_sequence_iter_is_end (seq))
		return FALSE;

	row = g_sequence_get (seq);
	g_return_val_if_fail (row != NULL && G_IS_OBJECT (row->object), FALSE);

	memset (iter, 0, sizeof (*iter));
	iter->stamp = COLLECTION_MODEL_STAMP;
	iter->user_data = row->object;
	iter->user_data2 = seq;
	return TRUE;
}

static GSequenceIter *
sequence_iter_for_tree (GcrCollectionModel *self,
                        GtkTreeIter *iter)
{
	g_return_val_if_fail (iter != NULL, NULL);
	g_return_val_if_fail (iter->stamp == COLLECTION_MODEL_STAMP, NULL);
	return iter->user_data2;
}

static GtkTreePath *
sequence_iter_to_path (GcrCollectionModel *self,
                       GSequenceIter *seq)
{
	GcrCollectionRow *row;
	GtkTreePath *path;

	path = gtk_tree_path_new ();
	while (seq) {
		gtk_tree_path_prepend_index (path, g_sequence_iter_get_position (seq));
		row = g_sequence_get (seq);
		seq = row->parent;
	}
	return path;
}

static GSequence *
child_sequence_for_tree (GcrCollectionModel *self,
                         GtkTreeIter *iter)
{
	GcrCollectionRow *row;
	GSequenceIter *seq;

	if (iter == NULL) {
		return self->pv->root_sequence;
	} else {
		seq = sequence_iter_for_tree (self, iter);
		g_return_val_if_fail (seq != NULL, NULL);
		row = g_sequence_get (seq);
		return row->children;
	}
}

static void
on_object_notify (GObject *object, GParamSpec *spec, GcrCollectionModel *self)
{
	GtkTreeIter iter;
	GtkTreePath *path;
	gboolean found = FALSE;
	guint i;

	g_return_if_fail (spec->name);

	for (i = 0; i < self->pv->n_columns - 1; ++i) {
		g_assert (self->pv->columns[i].property_name);
		if (g_str_equal (self->pv->columns[i].property_name, spec->name)) {
			found = TRUE;
			break;
		}
	}

	/* Tell the tree view that this row changed */
	if (found) {
		if (!gcr_collection_model_iter_for_object (self, object, &iter))
			g_return_if_reached ();
		path = gtk_tree_model_get_path (GTK_TREE_MODEL (self), &iter);
		g_return_if_fail (path);
		gtk_tree_model_row_changed (GTK_TREE_MODEL (self), path, &iter);
		gtk_tree_path_free (path);
	}
}

static void
on_object_gone (gpointer unused, GObject *was_object)
{
	g_warning ("object contained in GcrCollection and included in GcrCollectionModel "
	           "was destroyed before it was removed from the collection");
}

static void      on_collection_added              (GcrCollection *collection,
                                                   GObject *object,
                                                   gpointer user_data);

static void      on_collection_removed            (GcrCollection *collection,
                                                   GObject *object,
                                                   gpointer user_data);

static void      add_object_to_sequence           (GcrCollectionModel *self,
                                                   GSequence *sequence,
                                                   GSequenceIter *parent,
                                                   GObject *object,
                                                   gboolean emit);

static void      remove_object_from_sequence      (GcrCollectionModel *self,
                                                   GSequence *sequence,
                                                   GSequenceIter *seq,
                                                   GObject *object,
                                                   gboolean emit);

static void
add_children_to_sequence (GcrCollectionModel *self,
                          GSequence *sequence,
                          GSequenceIter *parent,
                          GcrCollection *collection,
                          GList *children,
                          GHashTable *exclude,
                          gboolean emit)
{
	GList *l;

	for (l = children; l; l = g_list_next (l)) {
		if (!exclude || g_hash_table_lookup (exclude, l->data) == NULL)
			add_object_to_sequence (self, sequence, parent, l->data, emit);
	}

	/* Now listen in for any changes */
	g_signal_connect_after (collection, "added", G_CALLBACK (on_collection_added), self);
	g_signal_connect_after (collection, "removed", G_CALLBACK (on_collection_removed), self);
}

static void
add_object_to_sequence (GcrCollectionModel *self,
                        GSequence *sequence,
                        GSequenceIter *parent,
                        GObject *object,
                        gboolean emit)
{
	GcrCollectionRow *row;
	GcrCollection *collection;
	GSequenceIter *seq;
	GtkTreeIter iter;
	GtkTreePath *path;
	GList *children;

	g_assert (GCR_IS_COLLECTION_MODEL (self));
	g_assert (G_IS_OBJECT (object));
	g_assert (self->pv->order_current);

	if (g_hash_table_lookup (self->pv->object_to_seq, object)) {
		g_warning ("object was already added to the GcrCollectionModel. Perhaps "
		           "a loop exists in a tree structure?");
		return;
	}

	row = g_slice_new0 (GcrCollectionRow);
	row->object = object;
	row->parent = parent;
	row->children = NULL;

	seq = g_sequence_insert_sorted (sequence, row, self->pv->order_current, self);
	g_hash_table_insert (self->pv->object_to_seq, object, seq);
	g_object_weak_ref (G_OBJECT (object), (GWeakNotify)on_object_gone, self);
	g_signal_connect (object, "notify", G_CALLBACK (on_object_notify), self);

	if (emit) {
		if (!sequence_iter_to_tree (self, seq, &iter))
			g_assert_not_reached ();
		path = sequence_iter_to_path (self, seq);
		g_assert (path != NULL);
		gtk_tree_model_row_inserted (GTK_TREE_MODEL (self), path, &iter);
		gtk_tree_path_free (path);
	}

	if (self->pv->mode == GCR_COLLECTION_MODEL_TREE &&
	    GCR_IS_COLLECTION (object)) {
		row->children = g_sequence_new (NULL);
		collection = GCR_COLLECTION (object);
		children = gcr_collection_get_objects (collection);
		add_children_to_sequence (self, row->children, seq,
		                          collection, children, NULL, emit);
		g_list_free (children);
	}
}

static void
on_collection_added (GcrCollection *collection,
                     GObject *object,
                     gpointer user_data)
{
	GcrCollectionModel *self = GCR_COLLECTION_MODEL (user_data);
	GSequence *sequence;
	GSequenceIter *parent;
	GcrCollectionRow *row;

	if (collection == self->pv->collection) {
		sequence = self->pv->root_sequence;
		parent = NULL;
	} else {
		parent = g_hash_table_lookup (self->pv->object_to_seq, G_OBJECT (collection));
		row = g_sequence_get (parent);
		g_assert (row->children);
		sequence = row->children;
	}

	add_object_to_sequence (self, sequence, parent, object, TRUE);
}

static void
remove_children_from_sequence (GcrCollectionModel *self,
                               GSequence *sequence,
                               GcrCollection *collection,
                               GHashTable *exclude,
                               gboolean emit)
{
	GSequenceIter *seq, *next;
	GcrCollectionRow *row;

	g_signal_handlers_disconnect_by_func (collection, on_collection_added, self);
	g_signal_handlers_disconnect_by_func (collection, on_collection_removed, self);

	for (seq = g_sequence_get_begin_iter (sequence);
	     !g_sequence_iter_is_end (seq); seq = next) {
		next = g_sequence_iter_next (seq);
		row = g_sequence_get (seq);
		if (!exclude || g_hash_table_lookup (exclude, row->object) == NULL)
			remove_object_from_sequence (self, sequence, seq, row->object, emit);
	}
}

static void
remove_object_from_sequence (GcrCollectionModel *self,
                             GSequence *sequence,
                             GSequenceIter *seq,
                             GObject *object,
                             gboolean emit)
{
	GcrCollectionRow *row;
	GtkTreePath *path = NULL;

	if (emit) {
		path = sequence_iter_to_path (self, seq);
		g_assert (path != NULL);
	}

	row = g_sequence_get (seq);
	g_assert (row->object == object);

	g_object_weak_unref (object, on_object_gone, self);
	g_signal_handlers_disconnect_by_func (object, on_object_notify, self);

	if (row->children) {
		g_assert (self->pv->mode == GCR_COLLECTION_MODEL_TREE);
		g_assert (GCR_IS_COLLECTION (object));
		remove_children_from_sequence (self, row->children,
		                               GCR_COLLECTION (object), NULL, emit);
		g_assert (g_sequence_get_length (row->children) == 0);
		g_sequence_free (row->children);
		row->children = NULL;
	}

	if (self->pv->selected)
		g_hash_table_remove (self->pv->selected, object);
	if (!g_hash_table_remove (self->pv->object_to_seq, object))
		g_assert_not_reached ();

	g_sequence_remove (seq);
	g_slice_free (GcrCollectionRow, row);

	/* Fire signal for this removed row */
	if (path != NULL) {
		gtk_tree_model_row_deleted (GTK_TREE_MODEL (self), path);
		gtk_tree_path_free (path);
	}

}

static void
on_collection_removed (GcrCollection *collection,
                       GObject *object,
                       gpointer user_data)
{
	GcrCollectionModel *self = GCR_COLLECTION_MODEL (user_data);
	GSequenceIter *seq;
	GSequence *sequence;

	seq = g_hash_table_lookup (self->pv->object_to_seq, object);
	g_return_if_fail (seq != NULL);

	sequence = g_sequence_iter_get_sequence (seq);
	g_assert (sequence != NULL);

	remove_object_from_sequence (self, sequence, seq, object, TRUE);
}

static void
free_owned_columns (gpointer data)
{
	GcrColumn *columns;
	g_assert (data);

	/* Only the property column is in use */
	for (columns = data; columns->property_name; ++columns)
		g_free ((gchar*)columns->property_name);
	g_free (data);
}

static GtkTreeModelFlags
gcr_collection_model_real_get_flags (GtkTreeModel *model)
{
	return GTK_TREE_MODEL_ITERS_PERSIST;
}

static gint
gcr_collection_model_real_get_n_columns (GtkTreeModel *model)
{
	GcrCollectionModel *self = GCR_COLLECTION_MODEL (model);
	return self->pv->n_columns;
}

static GType
gcr_collection_model_real_get_column_type (GtkTreeModel *model,
                                           gint column_id)
{
	GcrCollectionModel *self = GCR_COLLECTION_MODEL (model);
	g_return_val_if_fail (column_id >= 0 && column_id <= self->pv->n_columns, 0);

	/* The last is the selected column */
	if (column_id == self->pv->n_columns)
		return G_TYPE_BOOLEAN;

	return self->pv->columns[column_id].column_type;
}

static gboolean
gcr_collection_model_real_get_iter (GtkTreeModel *model,
                                    GtkTreeIter *iter,
                                    GtkTreePath *path)
{
	GcrCollectionModel *self = GCR_COLLECTION_MODEL (model);
	const gint *indices;
	GSequence *sequence;
	GSequenceIter *seq;
	GcrCollectionRow *row;
	gint count;
	gint i;

	sequence = self->pv->root_sequence;
	seq = NULL;

	indices = gtk_tree_path_get_indices_with_depth (path, &count);
	if (count == 0)
		return FALSE;

	for (i = 0; i < count; i++) {
		if (!sequence)
			return FALSE;
		seq = g_sequence_get_iter_at_pos (sequence, indices[i]);
		if (g_sequence_iter_is_end (seq))
			return FALSE;
		row = g_sequence_get (seq);
		sequence = row->children;
	}

	return sequence_iter_to_tree (self, seq, iter);
}

static GtkTreePath*
gcr_collection_model_real_get_path (GtkTreeModel *model,
                                    GtkTreeIter *iter)
{
	GcrCollectionModel *self = GCR_COLLECTION_MODEL (model);
	GSequenceIter *seq;

	if (iter == NULL)
		return gtk_tree_path_new ();

	seq = sequence_iter_for_tree (self, iter);
	g_return_val_if_fail (seq != NULL, NULL);
	return sequence_iter_to_path (self, seq);
}

static void
gcr_collection_model_real_get_value (GtkTreeModel *model,
                                     GtkTreeIter *iter,
                                     gint column_id,
                                     GValue *value)
{
	GcrCollectionModel *self = GCR_COLLECTION_MODEL (model);
	GObject *object;
	GValue original;
	const GcrColumn *column;
	GParamSpec *spec;

	object = gcr_collection_model_object_for_iter (self, iter);
	g_return_if_fail (G_IS_OBJECT (object));
	g_return_if_fail (column_id >= 0 && column_id < self->pv->n_columns);

	/* The selected column? Last one */
	if (column_id == self->pv->n_columns - 1) {
		g_value_init (value, G_TYPE_BOOLEAN);
		g_value_set_boolean (value, gcr_collection_model_is_selected (self, iter));
		return;
	}

	/* Figure out which property */
	column = &self->pv->columns[column_id];
	g_assert (column->property_name);
	g_value_init (value, column->column_type);

	/* Lookup the property on the object */
	spec = g_object_class_find_property (G_OBJECT_GET_CLASS (object), column->property_name);
	if (spec != NULL) {
		/* A transformer is specified, or mismatched types */
		if (column->transformer || column->column_type != column->property_type) {
			memset (&original, 0, sizeof (original));
			g_value_init (&original, column->property_type);
			g_object_get_property (object, column->property_name, &original);

			if (column->transformer) {
				(column->transformer) (&original, value);
			} else {
				g_warning ("%s property of %s class was of type %s instead of type %s"
				           " and cannot be converted due to lack of transformer",
				           column->property_name, G_OBJECT_TYPE_NAME (object),
				           g_type_name (column->property_type),
				           g_type_name (column->column_type));
				spec = NULL;
			}

		/* Simple, no transformation necessary */
		} else {
			g_object_get_property (object, column->property_name, value);
		}
	}

	if (spec == NULL) {

		/* All the number types have sane defaults */
		if (column->column_type == G_TYPE_STRING)
			g_value_set_string (value, "");
	}
}

static gboolean
gcr_collection_model_real_iter_next (GtkTreeModel *model,
                                     GtkTreeIter *iter)
{
	GcrCollectionModel *self = GCR_COLLECTION_MODEL (model);
	GSequenceIter *seq = sequence_iter_for_tree (self, iter);
	g_return_val_if_fail (seq != NULL, FALSE);
	return sequence_iter_to_tree (self, g_sequence_iter_next (seq), iter);
}

static gboolean
gcr_collection_model_real_iter_children (GtkTreeModel *model,
                                         GtkTreeIter *iter,
                                         GtkTreeIter *parent)
{
	GcrCollectionModel *self = GCR_COLLECTION_MODEL (model);
	GSequence *sequence = child_sequence_for_tree (self, parent);
	return sequence && sequence_iter_to_tree (self, g_sequence_get_begin_iter (sequence), iter);
}

static gboolean
gcr_collection_model_real_iter_has_child (GtkTreeModel *model,
                                          GtkTreeIter *iter)
{
	GcrCollectionModel *self = GCR_COLLECTION_MODEL (model);
	GSequence *sequence = child_sequence_for_tree (self, iter);
	return sequence && !g_sequence_iter_is_end (g_sequence_get_begin_iter (sequence));
}

static gint
gcr_collection_model_real_iter_n_children (GtkTreeModel *model,
                                           GtkTreeIter *iter)
{
	GcrCollectionModel *self = GCR_COLLECTION_MODEL (model);
	GSequence *sequence = child_sequence_for_tree (self, iter);
	return sequence ? g_sequence_get_length (sequence) : 0;
}

static gboolean
gcr_collection_model_real_iter_nth_child (GtkTreeModel *model,
                                          GtkTreeIter *iter,
                                          GtkTreeIter *parent,
                                          gint n)
{
	GcrCollectionModel *self = GCR_COLLECTION_MODEL (model);
	GSequence *sequence;
	GSequenceIter *seq;

	sequence = child_sequence_for_tree (self, parent);
	if (sequence == NULL)
		return FALSE;
	seq = g_sequence_get_iter_at_pos (sequence, n);
	return sequence_iter_to_tree (self, seq, iter);
}

static gboolean
gcr_collection_model_real_iter_parent (GtkTreeModel *model,
                                       GtkTreeIter *iter,
                                       GtkTreeIter *child)
{
	GcrCollectionModel *self = GCR_COLLECTION_MODEL (model);
	GSequenceIter *seq;
	GcrCollectionRow *row;

	seq = sequence_iter_for_tree (self, child);
	g_return_val_if_fail (seq != NULL, FALSE);
	row = g_sequence_get (seq);
	if (row->parent == NULL)
		return FALSE;
	return sequence_iter_to_tree (self, row->parent, iter);
}

static void
gcr_collection_model_real_ref_node (GtkTreeModel *model,
                                    GtkTreeIter *iter)
{
	/* Nothing to do */
}

static void
gcr_collection_model_real_unref_node (GtkTreeModel *model,
                                      GtkTreeIter *iter)
{
	/* Nothing to do */
}

static void
gcr_collection_model_tree_model_init (GtkTreeModelIface *iface)
{
	iface->get_flags = gcr_collection_model_real_get_flags;
	iface->get_n_columns = gcr_collection_model_real_get_n_columns;
	iface->get_column_type = gcr_collection_model_real_get_column_type;
	iface->get_iter = gcr_collection_model_real_get_iter;
	iface->get_path = gcr_collection_model_real_get_path;
	iface->get_value = gcr_collection_model_real_get_value;
	iface->iter_next = gcr_collection_model_real_iter_next;
	iface->iter_children = gcr_collection_model_real_iter_children;
	iface->iter_has_child = gcr_collection_model_real_iter_has_child;
	iface->iter_n_children = gcr_collection_model_real_iter_n_children;
	iface->iter_nth_child = gcr_collection_model_real_iter_nth_child;
	iface->iter_parent = gcr_collection_model_real_iter_parent;
	iface->ref_node = gcr_collection_model_real_ref_node;
	iface->unref_node = gcr_collection_model_real_unref_node;
}

static void
collection_resort_sequence (GcrCollectionModel *self,
                            GSequenceIter *parent,
                            GSequence *sequence)
{
	GPtrArray *previous;
	GSequenceIter *seq, *next;
	gint *new_order;
	GtkTreePath *path;
	GtkTreeIter iter;
	GcrCollectionRow *row;
	gint index;
	gint i;

	/* Make note of how things stand, and at same time resort all kids */
	previous = g_ptr_array_new ();
	for (seq = g_sequence_get_begin_iter (sequence);
	     !g_sequence_iter_is_end (seq); seq = next) {
		next = g_sequence_iter_next (seq);
		row = g_sequence_get (seq);
		if (row->children)
			collection_resort_sequence (self, seq, row->children);
		g_ptr_array_add (previous, row->object);
	}

	if (previous->len == 0) {
		g_ptr_array_free (previous, TRUE);
		return;
	}

	/* Actually perform the sort */
	g_sequence_sort (sequence, self->pv->order_current, self);

	/* Now go through and map out how things changed */
	new_order = g_new0 (gint, previous->len);
	for (i = 0; i < previous->len; i++) {
		seq = g_hash_table_lookup (self->pv->object_to_seq, previous->pdata[i]);
		g_assert (seq != NULL);
		index = g_sequence_iter_get_position (seq);
		g_assert (index >= 0 && index < previous->len);
		new_order[index] = i;
	}

	g_ptr_array_free (previous, TRUE);

	path = sequence_iter_to_path (self, parent);
	if (parent == NULL) {
		gtk_tree_model_rows_reordered (GTK_TREE_MODEL (self), path, NULL, new_order);
	} else {
		if (!sequence_iter_to_tree (self, parent, &iter))
			g_assert_not_reached ();
		gtk_tree_model_rows_reordered (GTK_TREE_MODEL (self), path, &iter, new_order);
	}
	gtk_tree_path_free (path);
	g_free (new_order);
}

static gboolean
gcr_collection_model_get_sort_column_id (GtkTreeSortable *sortable,
                                         gint *sort_column_id,
                                         GtkSortType *order)
{
	GcrCollectionModel *self = GCR_COLLECTION_MODEL (sortable);

	if (order)
		*order = self->pv->sort_order_type;
	if (sort_column_id)
		*sort_column_id = self->pv->sort_column_id;
	return (self->pv->sort_column_id != GTK_TREE_SORTABLE_DEFAULT_SORT_COLUMN_ID &&
		self->pv->sort_column_id != GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID);
}

static void
gcr_collection_model_set_sort_column_id (GtkTreeSortable *sortable,
                                         gint sort_column_id,
                                         GtkSortType order)
{
	GcrCollectionModel *self = GCR_COLLECTION_MODEL (sortable);
	GCompareDataFunc func;
	gpointer argument;
	const GcrColumn *column;
	gboolean reverse;

	reverse = (order == GTK_SORT_DESCENDING);

	if (sort_column_id == GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID) {
		func = reverse ? order_sequence_as_unsorted_reverse : order_sequence_as_unsorted;
		argument = NULL;

	} else if (sort_column_id == GTK_TREE_SORTABLE_DEFAULT_SORT_COLUMN_ID) {
		func = reverse ? order_sequence_by_closure_reverse : order_sequence_by_closure;
		argument = &self->pv->default_sort_closure;

	} else if (sort_column_id >= 0 && sort_column_id < self->pv->n_columns) {
		if (self->pv->column_sort_closures[sort_column_id].sort_func) {
			func = reverse ? order_sequence_by_closure_reverse : order_sequence_by_closure;
			argument = &self->pv->column_sort_closures[sort_column_id];
		} else {
			column = &self->pv->columns[sort_column_id];
			if (!(column->flags & GCR_COLUMN_SORTABLE))
				return;
			if (!lookup_compare_func (column->property_type)) {
				g_warning ("no sort implementation defined for type '%s' on column '%s'",
				           g_type_name (column->property_type), column->property_name);
				return;
			}

			func = reverse ? order_sequence_by_property_reverse : order_sequence_by_property;
			argument = (gpointer)column;
		}
	} else {
		g_warning ("invalid sort_column_id passed to gtk_tree_sortable_set_sort_column_id(): %d",
		           sort_column_id);
		return;
	}

	if (sort_column_id != self->pv->sort_column_id ||
	    order != self->pv->sort_order_type) {
		self->pv->sort_column_id = sort_column_id;
		self->pv->sort_order_type = order;
		gtk_tree_sortable_sort_column_changed (sortable);
	}

	if (func != self->pv->order_current ||
	    argument != self->pv->order_argument) {
		self->pv->order_current = func;
		self->pv->order_argument = (gpointer)argument;
		collection_resort_sequence (self, NULL, self->pv->root_sequence);
	}
}

static void
clear_sort_closure (GcrCollectionSortClosure *closure)
{
	if (closure->destroy_func)
		(closure->destroy_func) (closure->user_data);
	closure->sort_func = NULL;
	closure->destroy_func = NULL;
	closure->user_data = NULL;
}

static void
set_sort_closure (GcrCollectionSortClosure *closure,
                  GtkTreeIterCompareFunc func,
                  gpointer data,
                  GDestroyNotify destroy)
{
	clear_sort_closure (closure);
	closure->sort_func = func;
	closure->user_data = data;
	closure->destroy_func = destroy;
}

static void
gcr_collection_model_set_sort_func (GtkTreeSortable *sortable,
                                    gint sort_column_id,
                                    GtkTreeIterCompareFunc func,
                                    gpointer data,
                                    GDestroyNotify destroy)
{
	GcrCollectionModel *self = GCR_COLLECTION_MODEL (sortable);

	g_return_if_fail (sort_column_id >= 0 && sort_column_id < self->pv->n_columns);

	set_sort_closure (&self->pv->column_sort_closures[sort_column_id],
	                  func, data, destroy);

	/* Resorts if necessary */
	if (self->pv->sort_column_id == sort_column_id) {
		gcr_collection_model_set_sort_column_id (sortable,
		                                         self->pv->sort_column_id,
		                                         self->pv->sort_order_type);
	}
}

static void
gcr_collection_model_set_default_sort_func (GtkTreeSortable *sortable,
                                            GtkTreeIterCompareFunc func,
                                            gpointer data, GDestroyNotify destroy)
{
	GcrCollectionModel *self = GCR_COLLECTION_MODEL (sortable);

	set_sort_closure (&self->pv->default_sort_closure,
	                  func, data, destroy);

	/* Resorts if necessary */
	if (self->pv->sort_column_id == GTK_TREE_SORTABLE_DEFAULT_SORT_COLUMN_ID) {
		gcr_collection_model_set_sort_column_id (sortable,
		                                         self->pv->sort_column_id,
		                                         self->pv->sort_order_type);
	}
}

static gboolean
gcr_collection_model_has_default_sort_func (GtkTreeSortable *sortable)
{
	GcrCollectionModel *self = GCR_COLLECTION_MODEL (sortable);

	return (self->pv->default_sort_closure.sort_func != NULL);
}

static void
gcr_collection_model_tree_sortable_init (GtkTreeSortableIface *iface)
{
	iface->get_sort_column_id = gcr_collection_model_get_sort_column_id;
	iface->set_sort_column_id = gcr_collection_model_set_sort_column_id;
	iface->set_sort_func = gcr_collection_model_set_sort_func;
	iface->set_default_sort_func = gcr_collection_model_set_default_sort_func;
	iface->has_default_sort_func = gcr_collection_model_has_default_sort_func;
}

static void
gcr_collection_model_init (GcrCollectionModel *self)
{
	self->pv = G_TYPE_INSTANCE_GET_PRIVATE (self, GCR_TYPE_COLLECTION_MODEL, GcrCollectionModelPrivate);

	self->pv->root_sequence = g_sequence_new (NULL);
	self->pv->object_to_seq = g_hash_table_new (g_direct_hash, g_direct_equal);
	self->pv->sort_column_id = GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID;
	self->pv->sort_order_type = GTK_SORT_ASCENDING;
	self->pv->order_current = order_sequence_as_unsorted;
}

static void
gcr_collection_model_set_property (GObject *object, guint prop_id,
                                   const GValue *value, GParamSpec *pspec)
{
	GcrCollectionModel *self = GCR_COLLECTION_MODEL (object);
	GcrColumn *columns;

	switch (prop_id) {
	case PROP_MODE:
		self->pv->mode = g_value_get_enum (value);
		break;
	case PROP_COLLECTION:
		gcr_collection_model_set_collection (self, g_value_get_object (value));
		break;
	case PROP_COLUMNS:
		columns = g_value_get_pointer (value);
		if (columns)
			gcr_collection_model_set_columns (self, columns);
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
		break;
	}
}

static void
gcr_collection_model_get_property (GObject *object, guint prop_id,
                                   GValue *value, GParamSpec *pspec)
{
	GcrCollectionModel *self = GCR_COLLECTION_MODEL (object);

	switch (prop_id) {
	case PROP_MODE:
		g_value_set_enum (value, self->pv->mode);
		break;
	case PROP_COLLECTION:
		g_value_set_object (value, self->pv->collection);
		break;
	case PROP_COLUMNS:
		g_value_set_pointer (value, (gpointer)self->pv->columns);
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
		break;
	}
}

static void
gcr_collection_model_dispose (GObject *object)
{
	GcrCollectionModel *self = GCR_COLLECTION_MODEL (object);

	/* Disconnect from all rows */
	if (self->pv->collection) {
		remove_children_from_sequence (self, self->pv->root_sequence,
		                               self->pv->collection, NULL, FALSE);
		g_object_unref (self->pv->collection);
		self->pv->collection = NULL;
	}

	G_OBJECT_CLASS (gcr_collection_model_parent_class)->dispose (object);
}

static void
gcr_collection_model_finalize (GObject *object)
{
	GcrCollectionModel *self = GCR_COLLECTION_MODEL (object);
	guint i;

	g_assert (!self->pv->collection);

	g_assert (g_sequence_get_length (self->pv->root_sequence) == 0);
	g_sequence_free (self->pv->root_sequence);
	g_assert (g_hash_table_size (self->pv->object_to_seq) == 0);
	g_hash_table_destroy (self->pv->object_to_seq);

	if (self->pv->selected) {
		g_assert (g_hash_table_size (self->pv->selected) == 0);
		g_hash_table_destroy (self->pv->selected);
		self->pv->selected = NULL;
	}

	self->pv->columns = NULL;
	for (i = 0; i < self->pv->n_columns; i++)
		clear_sort_closure (&self->pv->column_sort_closures[i]);
	g_free (self->pv->column_sort_closures);
	clear_sort_closure (&self->pv->default_sort_closure);

	G_OBJECT_CLASS (gcr_collection_model_parent_class)->finalize (object);
}

static void
gcr_collection_model_class_init (GcrCollectionModelClass *klass)
{
	GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
	gcr_collection_model_parent_class = g_type_class_peek_parent (klass);

	gobject_class->dispose = gcr_collection_model_dispose;
	gobject_class->finalize = gcr_collection_model_finalize;
	gobject_class->set_property = gcr_collection_model_set_property;
	gobject_class->get_property = gcr_collection_model_get_property;

	g_object_class_install_property (gobject_class, PROP_MODE,
	              g_param_spec_enum ("mode", "Mode", "Tree or list mode",
	                                 GCR_TYPE_COLLECTION_MODEL_MODE, GCR_COLLECTION_MODEL_TREE,
	                                 G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));

	g_object_class_install_property (gobject_class, PROP_COLLECTION,
	            g_param_spec_object ("collection", "Object Collection", "Collection to get objects from",
	                                 GCR_TYPE_COLLECTION, G_PARAM_READWRITE));

	g_object_class_install_property (gobject_class, PROP_COLUMNS,
		g_param_spec_pointer ("columns", "Columns", "Columns for the model",
		                      G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));

	g_type_class_add_private (klass, sizeof (GcrCollectionModelPrivate));
}

/**
 * gcr_collection_model_new: (skip)
 * @collection: the collection to represent
 * @mode: whether list or tree mode
 * @...: the column names and types
 *
 * Create a new #GcrCollectionModel. The variable argument list should contain
 * pairs of property names, and #GType values. The variable argument list should
 * be terminated with %NULL.
 *
 * Returns: (transfer full): a newly allocated model, which should be released
 *          with g_object_unref().
 */
GcrCollectionModel*
gcr_collection_model_new (GcrCollection *collection,
                          GcrCollectionModelMode mode,
                          ...)
{
	GcrColumn column;
	GcrCollectionModel *self;
	const gchar *arg;
	GArray *array;
	va_list va;

	/* With a null terminator */
	array = g_array_new (TRUE, TRUE, sizeof (GcrColumn));

	va_start (va, mode);
	while ((arg = va_arg (va, const gchar*)) != NULL) {
		memset (&column, 0, sizeof (column));
		column.property_name = g_strdup (arg);
		column.property_type = va_arg (va, GType);
		column.column_type = column.property_type;
		g_array_append_val (array, column);
	}
	va_end (va);

	self = gcr_collection_model_new_full (collection, mode, (GcrColumn*)array->data);
	g_object_set_data_full (G_OBJECT (self), "gcr_collection_model_new",
	                        g_array_free (array, FALSE), free_owned_columns);
	return self;
}

/**
 * gcr_collection_model_new_full: (skip)
 * @collection: the collection to represent
 * @mode: whether list or tree mode
 * @columns: the columns the model should contain
 *
 * Create a new #GcrCollectionModel.
 *
 * Returns: (transfer full): a newly allocated model, which should be released
 *          with g_object_unref()
 */
GcrCollectionModel*
gcr_collection_model_new_full (GcrCollection *collection,
                               GcrCollectionModelMode mode,
                               const GcrColumn *columns)
{
	GcrCollectionModel *self = g_object_new (GCR_TYPE_COLLECTION_MODEL,
	                                         "collection", collection,
	                                         "mode", mode,
	                                         NULL);
	gcr_collection_model_set_columns (self, columns);
	return self;
}

/**
 * gcr_collection_model_set_columns: (skip)
 * @self: The model
 * @columns: The columns the model should contain
 *
 * Set the columns that the model should contain. @columns is an array of
 * #GcrColumn structures, with the last one containing %NULL for all values.
 *
 * This function can only be called once, and only if the model was not created
 * without a set of columns. This function cannot be called after the model
 * has been added to a view.
 *
 * The columns are accessed as static data. They should continue to remain
 * in memory for longer than the GcrCollectionModel object.
 *
 * Returns: The number of columns
 */
guint
gcr_collection_model_set_columns (GcrCollectionModel *self,
                                  const GcrColumn *columns)
{
	const GcrColumn *col;
	guint n_columns;

	g_return_val_if_fail (GCR_IS_COLLECTION_MODEL (self), 0);
	g_return_val_if_fail (columns, 0);
	g_return_val_if_fail (self->pv->n_columns == 0, 0);

	/* Count the number of columns, extra column for selected */
	for (col = columns, n_columns = 1; col->property_name; ++col)
		++n_columns;

	/* We expect the columns to stay around */
	self->pv->columns = columns;
	self->pv->n_columns = n_columns;
	self->pv->column_sort_closures = g_new0 (GcrCollectionSortClosure, self->pv->n_columns);

	return n_columns - 1;
}

/**
 * gcr_collection_model_get_collection:
 * @self: a collection model
 *
 * Get the collection which this model represents
 *
 * Returns: (transfer none): the collection, owned by the model
 */
GcrCollection *
gcr_collection_model_get_collection (GcrCollectionModel *self)
{
	g_return_val_if_fail (GCR_IS_COLLECTION_MODEL (self), NULL);
	return self->pv->collection;
}

/**
 * gcr_collection_model_set_collection:
 * @self: a collection model
 * @collection: (allow-none): the collection or %NULL
 *
 * Set the collection which this model represents
 */
void
gcr_collection_model_set_collection (GcrCollectionModel *self,
                                     GcrCollection *collection)
{
	GcrCollection *previous;
	GHashTable *exclude;
	GList *children = NULL;
	GList *l;

	g_return_if_fail (GCR_IS_COLLECTION_MODEL (self));
	g_return_if_fail (collection == NULL || GCR_IS_COLLECTION (collection));

	if (collection == self->pv->collection)
		return;

	if (collection)
		g_object_ref (collection);
	previous = self->pv->collection;
	self->pv->collection = collection;

	if (collection)
		children = gcr_collection_get_objects (collection);

	if (previous) {
		exclude = g_hash_table_new (g_direct_hash, g_direct_equal);
		for (l = children; l != NULL; l = g_list_next (l))
			g_hash_table_insert (exclude, l->data, l->data);

		remove_children_from_sequence (self, self->pv->root_sequence,
		                               previous, exclude, TRUE);

		g_hash_table_destroy (exclude);
		g_object_unref (previous);
	}

	if (collection) {
		add_children_to_sequence (self, self->pv->root_sequence,
		                          NULL, collection, children,
		                          self->pv->object_to_seq, TRUE);
		g_list_free (children);
	}

	g_object_notify (G_OBJECT (self), "collection");
}

/**
 * gcr_collection_model_object_for_iter:
 * @self: The model
 * @iter: The row
 *
 * Get the object that is represented by the given row in the model.
 *
 * Returns: (transfer none): The object, owned by the model.
 */
GObject *
gcr_collection_model_object_for_iter (GcrCollectionModel *self, const GtkTreeIter *iter)
{
	g_return_val_if_fail (GCR_IS_COLLECTION_MODEL (self), NULL);
	g_return_val_if_fail (iter != NULL, NULL);
	g_return_val_if_fail (iter->stamp == COLLECTION_MODEL_STAMP, NULL);
	g_return_val_if_fail (G_IS_OBJECT (iter->user_data), NULL);

	return G_OBJECT (iter->user_data);
}

/**
 * gcr_collection_model_iter_for_object:
 * @self: The model
 * @object: The object
 * @iter: The row for the object
 *
 * Set @iter to the row for the given object. If the object is not in this
 * model, then %FALSE will be returned.
 *
 * Returns: %TRUE if the object was present.
 */
gboolean
gcr_collection_model_iter_for_object (GcrCollectionModel *self, GObject *object,
                                      GtkTreeIter *iter)
{
	GSequenceIter *seq;

	g_return_val_if_fail (GCR_IS_COLLECTION_MODEL (self), FALSE);
	g_return_val_if_fail (G_IS_OBJECT (object), FALSE);
	g_return_val_if_fail (iter != NULL, FALSE);

	seq = g_hash_table_lookup (self->pv->object_to_seq, object);
	if (seq == NULL)
		return FALSE;

	return sequence_iter_to_tree (self, seq, iter);
}

/**
 * gcr_collection_model_column_for_selected:
 * @self: The model
 *
 * Get the column identifier for the column that contains the values
 * of the selected state.
 *
 * Returns: The column identifier.
 */
gint
gcr_collection_model_column_for_selected (GcrCollectionModel *self)
{
	g_return_val_if_fail (GCR_IS_COLLECTION_MODEL (self), 0);
	g_assert (self->pv->n_columns > 0);
	return self->pv->n_columns - 1;
}

/**
 * gcr_collection_model_toggle_selected:
 * @self: The model
 * @iter: The row
 *
 * Toggle the selected state of a given row.
 */
void
gcr_collection_model_toggle_selected (GcrCollectionModel *self, GtkTreeIter *iter)
{
	GObject *object;

	g_return_if_fail (GCR_IS_COLLECTION_MODEL (self));

	object = gcr_collection_model_object_for_iter (self, iter);
	g_return_if_fail (G_IS_OBJECT (object));

	if (!self->pv->selected)
		self->pv->selected = selected_hash_table_new ();

	if (g_hash_table_lookup (self->pv->selected, object))
		g_hash_table_remove (self->pv->selected, object);
	else
		g_hash_table_insert (self->pv->selected, object, object);
}

/**
 * gcr_collection_model_change_selected:
 * @self: The model
 * @iter: The row
 * @selected: Whether the row should be selected or not.
 *
 * Set whether a given row is toggled selected or not.
 */
void
gcr_collection_model_change_selected (GcrCollectionModel *self, GtkTreeIter *iter, gboolean selected)
{
	GtkTreePath *path;
	GObject *object;

	g_return_if_fail (GCR_IS_COLLECTION_MODEL (self));

	object = gcr_collection_model_object_for_iter (self, iter);
	g_return_if_fail (G_IS_OBJECT (object));

	if (!self->pv->selected)
		self->pv->selected = g_hash_table_new (g_direct_hash, g_direct_equal);

	if (selected)
		g_hash_table_insert (self->pv->selected, object, object);
	else
		g_hash_table_remove (self->pv->selected, object);

	/* Tell the view that this row changed */
	path = gtk_tree_model_get_path (GTK_TREE_MODEL (self), iter);
	g_return_if_fail (path);
	gtk_tree_model_row_changed (GTK_TREE_MODEL (self), path, iter);
	gtk_tree_path_free (path);
}

/**
 * gcr_collection_model_is_selected:
 * @self: The model
 * @iter: The row
 *
 * Check whether a given row has been toggled as selected.
 *
 * Returns: Whether the row has been selected.
 */
gboolean
gcr_collection_model_is_selected (GcrCollectionModel *self, GtkTreeIter *iter)
{
	GObject *object;

	g_return_val_if_fail (GCR_IS_COLLECTION_MODEL (self), FALSE);

	object = gcr_collection_model_object_for_iter (self, iter);
	g_return_val_if_fail (G_IS_OBJECT (object), FALSE);

	if (!self->pv->selected)
		return FALSE;

	return g_hash_table_lookup (self->pv->selected, object) ? TRUE : FALSE;
}

/**
 * gcr_collection_model_get_selected_objects:
 * @self: the collection model
 *
 * Get a list of checked/selected objects.
 *
 * Returns: (transfer container) (element-type GObject.Object): a list of selected
 *          objects, which should be freed with g_list_free()
 */
GList *
gcr_collection_model_get_selected_objects (GcrCollectionModel *self)
{
	GHashTableIter iter;
	GList *result = NULL;
	gpointer key;

	g_return_val_if_fail (GCR_IS_COLLECTION_MODEL (self), NULL);

	if (!self->pv->selected)
		return NULL;

	g_hash_table_iter_init (&iter, self->pv->selected);
	while (g_hash_table_iter_next (&iter, &key, NULL))
		result = g_list_prepend (result, key);
	return result;
}

/**
 * gcr_collection_model_set_selected_objects:
 * @self: the collection model
 * @selected: (element-type GObject.Object): a list of objects to select
 *
 * Set the checked/selected objects.
 */
void
gcr_collection_model_set_selected_objects (GcrCollectionModel *self,
                                           GList *selected)
{
	GHashTable *newly_selected;
	GList *old_selection;
	GtkTreeIter iter;
	GList *l;

	old_selection = gcr_collection_model_get_selected_objects (self);
	newly_selected = selected_hash_table_new ();

	/* Select all the objects in selected which aren't already selected */
	for (l = selected; l; l = g_list_next (l)) {
		if (!self->pv->selected || !g_hash_table_lookup (self->pv->selected, l->data)) {
			if (!gcr_collection_model_iter_for_object (self, l->data, &iter))
				g_return_if_reached ();
			gcr_collection_model_change_selected (self, &iter, TRUE);
		}

		/* Note that we've seen this one */
		g_hash_table_insert (newly_selected, l->data, l->data);
	}

	/* Unselect all the objects which aren't supposed to be selected */
	for (l = old_selection; l; l = g_list_next (l)) {
		if (!g_hash_table_lookup (newly_selected, l->data)) {
			if (!gcr_collection_model_iter_for_object (self, l->data, &iter))
				g_return_if_reached ();
			gcr_collection_model_change_selected (self, &iter, FALSE);
		}
	}

	g_list_free (old_selection);
	g_hash_table_destroy (newly_selected);
}