Index: rhythmdb/rhythmdb-keyword-model.c
===================================================================
--- rhythmdb/rhythmdb-keyword-model.c	(revision 0)
+++ rhythmdb/rhythmdb-keyword-model.c	(revision 0)
@@ -0,0 +1,1152 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ *  Copyright (C) 2007 James Livingston <doclivingston@gmail.com>
+ *
+ *  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.
+ *
+ */
+
+#include "config.h"
+
+#include <unistd.h>
+#include <stdlib.h>
+#include <string.h>
+#include <glib/gi18n.h>
+
+#include "rhythmdb-keyword-model.h"
+#include "rb-debug.h"
+#include "eggsequence.h"
+#include "rb-refstring.h"
+#include "rb-tree-dnd.h"
+
+static void rhythmdb_keyword_model_tree_model_init (GtkTreeModelIface *iface);
+static void rhythmdb_keyword_model_drag_source_init (RbTreeDragSourceIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE(RhythmDBKeywordModel, rhythmdb_keyword_model, G_TYPE_OBJECT,
+			G_IMPLEMENT_INTERFACE(GTK_TYPE_TREE_MODEL,
+					      rhythmdb_keyword_model_tree_model_init)
+			G_IMPLEMENT_INTERFACE(RB_TYPE_TREE_DRAG_SOURCE,
+					      rhythmdb_keyword_model_drag_source_init))
+
+typedef struct {
+	RBRefString *string;
+	guint refcount;
+} RhythmDBKeywordModelEntry;
+
+static void rhythmdb_keyword_model_dispose (GObject *object);
+static void rhythmdb_keyword_model_finalize (GObject *object);
+static void rhythmdb_keyword_model_set_property (GObject *object,
+					       guint prop_id,
+					       const GValue *value,
+					       GParamSpec *pspec);
+static void rhythmdb_keyword_model_get_property (GObject *object,
+					       guint prop_id,
+					       GValue *value,
+					       GParamSpec *pspec);
+static void rhythmdb_keyword_model_sync (RhythmDBKeywordModel *model);
+static void rhythmdb_keyword_model_row_inserted_cb (GtkTreeModel *model,
+						     GtkTreePath *path,
+						     GtkTreeIter *iter,
+						     RhythmDBKeywordModel *propmodel);
+static void rhythmdb_keyword_model_entry_removed_cb (RhythmDBQueryModel *model,
+						      RhythmDBEntry *entry,
+						      RhythmDBKeywordModel *propmodel);
+static void rhythmdb_keyword_model_prop_changed_cb (RhythmDB *db, RhythmDBEntry *entry,
+						     RhythmDBPropType prop, const GValue *old,
+						     const GValue *new,
+						     RhythmDBKeywordModel *propmodel);
+static void rhythmdb_keyword_model_insert_entry (RhythmDBKeywordModel *model,
+						 RhythmDBEntry *entry);
+static void rhythmdb_keyword_model_insert_keyword (RBRefString *keyword,
+						   RhythmDBKeywordModel *model);
+static void rhythmdb_keyword_model_delete_entry (RhythmDBKeywordModel *model,
+					    RhythmDBEntry *entry);
+static void rhythmdb_keyword_model_delete_keyword (RBRefString *keyword,
+						   RhythmDBKeywordModel *model);
+static GtkTreeModelFlags rhythmdb_keyword_model_get_flags (GtkTreeModel *model);
+static gint rhythmdb_keyword_model_get_n_columns (GtkTreeModel *tree_model);
+static GType rhythmdb_keyword_model_get_column_type (GtkTreeModel *tree_model, int index);
+static gboolean rhythmdb_keyword_model_get_iter (GtkTreeModel *tree_model, GtkTreeIter *iter,
+					       GtkTreePath  *path);
+static GtkTreePath * rhythmdb_keyword_model_get_path (GtkTreeModel *tree_model,
+						    GtkTreeIter  *iter);
+static void rhythmdb_keyword_model_get_value (GtkTreeModel *tree_model, GtkTreeIter *iter,
+					    gint column, GValue *value);
+static gboolean rhythmdb_keyword_model_iter_next (GtkTreeModel  *tree_model,
+						GtkTreeIter   *iter);
+static gboolean rhythmdb_keyword_model_iter_children (GtkTreeModel *tree_model,
+						    GtkTreeIter  *iter,
+						    GtkTreeIter  *parent);
+static gboolean rhythmdb_keyword_model_iter_has_child (GtkTreeModel *tree_model,
+						     GtkTreeIter  *iter);
+static gint rhythmdb_keyword_model_iter_n_children (GtkTreeModel *tree_model,
+						  GtkTreeIter  *iter);
+static gboolean rhythmdb_keyword_model_iter_nth_child (GtkTreeModel *tree_model,
+						     GtkTreeIter *iter, GtkTreeIter *parent,
+						     gint n);
+static gboolean rhythmdb_keyword_model_iter_parent (GtkTreeModel *tree_model,
+						  GtkTreeIter  *iter,
+						  GtkTreeIter  *child);
+
+static gboolean rhythmdb_keyword_model_drag_data_get (RbTreeDragSource *dragsource,
+						       GList *paths,
+						       GtkSelectionData *selection_data);
+static gboolean rhythmdb_keyword_model_drag_data_delete (RbTreeDragSource *dragsource,
+							  GList *paths);
+static gboolean rhythmdb_keyword_model_row_draggable (RbTreeDragSource *dragsource,
+						       GList *paths);
+
+enum {
+	TARGET_KEYWORDS,
+	TARGET_ENTRIES,
+	TARGET_URIS,
+};
+
+static const GtkTargetEntry targets_keyword  [] = {
+	{ "text/x-rhythmbox-keyword",  0, TARGET_KEYWORDS },
+	{ "application/x-rhythmbox-entry", 0, TARGET_ENTRIES },
+	{ "text/uri-list", 0, TARGET_URIS },
+};
+
+static GtkTargetList *rhythmdb_keyword_model_keyword_drag_target_list = NULL;
+
+struct RhythmDBKeywordModelPrivate
+{
+	RhythmDB *db;
+
+	RhythmDBQueryModel *query_model;
+	GHashTable *entries; /* entries that aren't present because they're hidden */
+
+	guint stamp;
+
+	EggSequence *keywords;
+	GHashTable *reverse_map;
+
+	RhythmDBKeywordModelEntry *all;
+
+	guint syncing_id;
+};
+
+#define RHYTHMDB_KEYWORD_MODEL_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), RHYTHMDB_TYPE_KEYWORD_MODEL, RhythmDBKeywordModelPrivate))
+
+enum
+{
+	PRE_ROW_DELETION,
+	LAST_SIGNAL
+};
+
+enum
+{
+	PROP_0,
+	PROP_RHYTHMDB,
+	PROP_QUERY_MODEL,
+};
+
+static guint rhythmdb_keyword_model_signals[LAST_SIGNAL] = { 0 };
+
+static void
+rhythmdb_keyword_model_class_init (RhythmDBKeywordModelClass *klass)
+{
+	GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+	if (!rhythmdb_keyword_model_keyword_drag_target_list)
+		rhythmdb_keyword_model_keyword_drag_target_list =
+			gtk_target_list_new (targets_keyword,
+					     G_N_ELEMENTS (targets_keyword));
+
+	object_class->set_property = rhythmdb_keyword_model_set_property;
+	object_class->get_property = rhythmdb_keyword_model_get_property;
+
+	object_class->dispose = rhythmdb_keyword_model_dispose;
+	object_class->finalize = rhythmdb_keyword_model_finalize;
+
+	rhythmdb_keyword_model_signals[PRE_ROW_DELETION] =
+		g_signal_new ("pre-row-deletion",
+			      G_OBJECT_CLASS_TYPE (object_class),
+			      G_SIGNAL_RUN_LAST,
+			      G_STRUCT_OFFSET (RhythmDBKeywordModelClass, pre_row_deletion),
+			      NULL, NULL,
+			      g_cclosure_marshal_VOID__VOID,
+			      G_TYPE_NONE,
+			      0);
+
+	g_object_class_install_property (object_class,
+					 PROP_RHYTHMDB,
+					 g_param_spec_object ("db",
+							      "RhythmDB",
+							      "RhythmDB object",
+							      RHYTHMDB_TYPE,
+							      G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
+
+	g_object_class_install_property (object_class,
+					 PROP_QUERY_MODEL,
+					 g_param_spec_object ("query-model",
+							      "RhythmDBQueryModel",
+							      "RhythmDBQueryModel object ",
+							      RHYTHMDB_TYPE_QUERY_MODEL,
+							      G_PARAM_READWRITE));
+
+	g_type_class_add_private (klass, sizeof (RhythmDBKeywordModelPrivate));
+}
+
+static void
+rhythmdb_keyword_model_tree_model_init (GtkTreeModelIface *iface)
+{
+	iface->get_flags = rhythmdb_keyword_model_get_flags;
+	iface->get_n_columns = rhythmdb_keyword_model_get_n_columns;
+	iface->get_column_type = rhythmdb_keyword_model_get_column_type;
+	iface->get_iter = rhythmdb_keyword_model_get_iter;
+	iface->get_path = rhythmdb_keyword_model_get_path;
+	iface->get_value = rhythmdb_keyword_model_get_value;
+	iface->iter_next = rhythmdb_keyword_model_iter_next;
+	iface->iter_children = rhythmdb_keyword_model_iter_children;
+	iface->iter_has_child = rhythmdb_keyword_model_iter_has_child;
+	iface->iter_n_children = rhythmdb_keyword_model_iter_n_children;
+	iface->iter_nth_child = rhythmdb_keyword_model_iter_nth_child;
+	iface->iter_parent = rhythmdb_keyword_model_iter_parent;
+}
+
+static void
+rhythmdb_keyword_model_drag_source_init (RbTreeDragSourceIface *iface)
+{
+	iface->rb_row_draggable = rhythmdb_keyword_model_row_draggable;
+	iface->rb_drag_data_delete = rhythmdb_keyword_model_drag_data_delete;
+	iface->rb_drag_data_get = rhythmdb_keyword_model_drag_data_get;
+}
+
+static gboolean
+_remove_entry_cb (GtkTreeModel *model,
+		  GtkTreePath *path,
+		  GtkTreeIter *iter,
+		  RhythmDBKeywordModel *propmodel)
+{
+	RhythmDBEntry *entry;
+
+	entry = rhythmdb_query_model_iter_to_entry (RHYTHMDB_QUERY_MODEL (model), iter);
+	rhythmdb_keyword_model_entry_removed_cb (RHYTHMDB_QUERY_MODEL (model),
+						  entry,
+						  propmodel);
+	return FALSE;
+}
+
+static gboolean
+_add_entry_cb (GtkTreeModel *model,
+	       GtkTreePath *path,
+	       GtkTreeIter *iter,
+	       RhythmDBKeywordModel *propmodel)
+{
+	rhythmdb_keyword_model_row_inserted_cb (model, path, iter, propmodel);
+	return FALSE;
+}
+
+static void
+rhythmdb_keyword_model_entry_keyword_added_cb (RhythmDB *db,
+					       RhythmDBEntry *entry,
+					       RBRefString *keyword,
+					       RhythmDBKeywordModel *model)
+{
+	rb_debug ("keyword '%s' added to entry %p", rb_refstring_get (keyword), entry);
+	rhythmdb_keyword_model_insert_keyword (keyword, model);
+}
+
+static void
+rhythmdb_keyword_model_entry_keyword_removed_cb (RhythmDB *db,
+						 RhythmDBEntry *entry,
+						 RBRefString *keyword,
+						 RhythmDBKeywordModel *model)
+{
+	rb_debug ("keyword '%s' removed from entry %p", rb_refstring_get (keyword), entry);
+	rhythmdb_keyword_model_delete_keyword (keyword, model);
+}
+
+static void
+rhythmdb_keyword_model_set_db_internal (RhythmDBKeywordModel *model,
+					RhythmDB *db)
+{
+	if (model->priv->db != NULL) {
+		g_signal_handlers_disconnect_by_func (model->priv->db,
+						      G_CALLBACK (rhythmdb_keyword_model_entry_keyword_added_cb),
+						      model);
+		g_signal_handlers_disconnect_by_func (model->priv->db,
+						      G_CALLBACK (rhythmdb_keyword_model_entry_keyword_removed_cb),
+						      model);
+
+		g_object_unref (model->priv->db);
+	}
+
+	model->priv->db = db;
+
+	if (model->priv->query_model != NULL) {
+		g_object_ref (model->priv->query_model);
+
+		g_signal_connect_object (model->priv->db,
+					 "entry-keyword-added",
+					 G_CALLBACK (rhythmdb_keyword_model_entry_keyword_added_cb),
+					 model,
+					 0);
+		g_signal_connect_object (model->priv->db,
+					 "entry-keyword-removed",
+					 G_CALLBACK (rhythmdb_keyword_model_entry_keyword_removed_cb),
+					 model,
+					 0);
+	}
+}
+
+static void
+rhythmdb_keyword_model_set_query_model_internal (RhythmDBKeywordModel *model,
+						  RhythmDBQueryModel    *query_model)
+{
+	if (model->priv->query_model != NULL) {
+		g_signal_handlers_disconnect_by_func (model->priv->query_model,
+						      G_CALLBACK (rhythmdb_keyword_model_row_inserted_cb),
+						      model);
+		g_signal_handlers_disconnect_by_func (model->priv->query_model,
+						      G_CALLBACK (rhythmdb_keyword_model_entry_removed_cb),
+						      model);
+		g_signal_handlers_disconnect_by_func (model->priv->query_model,
+					  	      G_CALLBACK (rhythmdb_keyword_model_prop_changed_cb),
+						      model);
+
+		gtk_tree_model_foreach (GTK_TREE_MODEL (model->priv->query_model),
+					(GtkTreeModelForeachFunc)_remove_entry_cb,
+					model);
+
+		g_object_unref (model->priv->query_model);
+	}
+
+	model->priv->query_model = query_model;
+	g_assert (rhythmdb_keyword_model_iter_n_children (GTK_TREE_MODEL (model), NULL) == 1);
+
+	if (model->priv->query_model != NULL) {
+		g_object_ref (model->priv->query_model);
+
+		g_signal_connect_object (model->priv->query_model,
+					 "row_inserted",
+					 G_CALLBACK (rhythmdb_keyword_model_row_inserted_cb),
+					 model,
+					 0);
+		g_signal_connect_object (model->priv->query_model,
+					 "post-entry-delete",
+					 G_CALLBACK (rhythmdb_keyword_model_entry_removed_cb),
+					 model,
+					 0);
+		g_signal_connect_object (model->priv->query_model,
+					 "entry-prop-changed",
+					 G_CALLBACK (rhythmdb_keyword_model_prop_changed_cb),
+					 model,
+					 0);
+		gtk_tree_model_foreach (GTK_TREE_MODEL (model->priv->query_model),
+					(GtkTreeModelForeachFunc)_add_entry_cb,
+					model);
+	}
+}
+
+static void
+rhythmdb_keyword_model_set_property (GObject *object,
+				     guint prop_id,
+				     const GValue *value,
+				     GParamSpec *pspec)
+{
+	RhythmDBKeywordModel *model = RHYTHMDB_KEYWORD_MODEL (object);
+
+	switch (prop_id) {
+	case PROP_RHYTHMDB:
+		rhythmdb_keyword_model_set_db_internal (model, g_value_get_object (value));
+		break;
+	case PROP_QUERY_MODEL:
+		rhythmdb_keyword_model_set_query_model_internal (model, g_value_get_object (value));
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+		break;
+	}
+}
+
+static void
+rhythmdb_keyword_model_get_property (GObject *object,
+				     guint prop_id,
+				     GValue *value,
+				     GParamSpec *pspec)
+{
+	RhythmDBKeywordModel *model = RHYTHMDB_KEYWORD_MODEL (object);
+
+	switch (prop_id) {
+	case PROP_RHYTHMDB:
+		g_value_set_object (value, model->priv->db);
+		break;
+	case PROP_QUERY_MODEL:
+		g_value_set_object (value, model->priv->query_model);
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+		break;
+	}
+}
+
+static void
+rhythmdb_keyword_model_init (RhythmDBKeywordModel *model)
+{
+	model->priv = RHYTHMDB_KEYWORD_MODEL_GET_PRIVATE (model);
+
+	model->priv->stamp = g_random_int ();
+
+	model->priv->keywords = egg_sequence_new (NULL);
+	model->priv->reverse_map = g_hash_table_new (rb_refstring_hash, rb_refstring_equal);
+	model->priv->entries = g_hash_table_new (g_direct_hash, g_direct_equal);
+
+	model->priv->all = g_new0 (RhythmDBKeywordModelEntry, 1);
+	model->priv->all->string = rb_refstring_new (_("All"));
+}
+
+static void
+_prop_model_entry_cleanup (RhythmDBKeywordModelEntry *prop, gpointer data)
+{
+	rb_refstring_unref (prop->string);
+	g_free (prop);
+}
+
+static void
+rhythmdb_keyword_model_dispose (GObject *object)
+{
+	RhythmDBKeywordModel *model;
+
+	g_return_if_fail (object != NULL);
+	g_return_if_fail (RHYTHMDB_IS_KEYWORD_MODEL (object));
+
+	model = RHYTHMDB_KEYWORD_MODEL (object);
+
+	rb_debug ("disposing keyword model %p", model);
+
+	g_return_if_fail (model->priv != NULL);
+
+	if (model->priv->syncing_id != 0) {
+		g_source_remove (model->priv->syncing_id);
+		model->priv->syncing_id = 0;
+	}
+
+	if (model->priv->query_model != NULL) {
+		g_object_unref (model->priv->query_model);
+		model->priv->query_model = NULL;
+	}
+
+	G_OBJECT_CLASS (rhythmdb_keyword_model_parent_class)->dispose (object);
+}
+
+static void
+rhythmdb_keyword_model_finalize (GObject *object)
+{
+	RhythmDBKeywordModel *model;
+
+	g_return_if_fail (object != NULL);
+	g_return_if_fail (RHYTHMDB_IS_KEYWORD_MODEL (object));
+
+	model = RHYTHMDB_KEYWORD_MODEL (object);
+
+	rb_debug ("finalizing keyword model %p", model);
+
+	g_return_if_fail (model->priv != NULL);
+
+	g_hash_table_destroy (model->priv->reverse_map);
+
+	egg_sequence_foreach (model->priv->keywords, (GFunc)_prop_model_entry_cleanup, NULL);
+	egg_sequence_free (model->priv->keywords);
+
+	g_hash_table_destroy (model->priv->entries);
+
+	g_free (model->priv->all);
+
+	G_OBJECT_CLASS (rhythmdb_keyword_model_parent_class)->finalize (object);
+}
+
+RhythmDBKeywordModel *
+rhythmdb_keyword_model_new (RhythmDB *db)
+{
+	return g_object_new (RHYTHMDB_TYPE_KEYWORD_MODEL, "db", db, NULL);
+}
+
+static void
+rhythmdb_keyword_model_row_inserted_cb (GtkTreeModel *model,
+					 GtkTreePath *path,
+					 GtkTreeIter *iter,
+					 RhythmDBKeywordModel *propmodel)
+{
+	RhythmDBEntry *entry;
+
+	entry = rhythmdb_query_model_iter_to_entry (RHYTHMDB_QUERY_MODEL (model), iter);
+
+	rhythmdb_keyword_model_insert_entry (propmodel, entry);
+	rhythmdb_keyword_model_sync (propmodel);
+
+	rhythmdb_entry_unref (entry);
+}
+
+static void
+rhythmdb_keyword_model_entry_removed_cb (RhythmDBQueryModel *model,
+					 RhythmDBEntry *entry,
+					 RhythmDBKeywordModel *propmodel)
+{
+	if (g_hash_table_remove (propmodel->priv->entries, entry))
+		return;
+
+	rhythmdb_keyword_model_delete_entry (propmodel, entry);
+	rhythmdb_keyword_model_sync (propmodel);
+}
+
+static gint
+rhythmdb_keyword_model_compare (RhythmDBKeywordModelEntry *a,
+				RhythmDBKeywordModelEntry *b,
+				RhythmDBKeywordModel *model)
+{
+	const char *a_str, *b_str;
+
+	a_str = rb_refstring_get_sort_key (a->string);
+	b_str = rb_refstring_get_sort_key (b->string);
+
+	return strcmp (a_str, b_str);
+}
+
+static void
+rhythmdb_keyword_model_insert_entry (RhythmDBKeywordModel *model,
+				RhythmDBEntry *entry)
+{
+	GList *keywords;
+
+	keywords = rhythmdb_entry_keywords_get (model->priv->db, entry);
+
+	rb_debug ("entry %p added with %d keywords", entry, g_list_length (keywords));
+	g_list_foreach (keywords, (GFunc)rhythmdb_keyword_model_insert_keyword, model);
+	g_list_foreach (keywords, (GFunc)rb_refstring_unref, NULL);
+	g_list_free (keywords);
+}
+
+static void
+rhythmdb_keyword_model_insert_keyword (RBRefString *keyword, RhythmDBKeywordModel *model)
+{
+	GtkTreeIter iter;
+	GtkTreePath *path;
+	EggSequenceIter *ptr;
+	RhythmDBKeywordModelEntry *prop;
+
+	model->priv->all->refcount++;
+	iter.stamp = model->priv->stamp;
+
+	if ((ptr = g_hash_table_lookup (model->priv->reverse_map, keyword))) {
+		prop = egg_sequence_get (ptr);
+		prop->refcount++;
+		rb_debug ("adding '%s', refcount %d, to model %p", rb_refstring_get (keyword), prop->refcount, model);
+
+		iter.user_data = ptr;
+		path = rhythmdb_keyword_model_get_path (GTK_TREE_MODEL (model), &iter);
+		gtk_tree_model_row_changed (GTK_TREE_MODEL (model), path, &iter);
+		gtk_tree_path_free (path);
+		return;
+	}
+	rb_debug ("adding new keyword '%s' to model %p", rb_refstring_get (keyword), model);
+
+	prop = g_new0 (RhythmDBKeywordModelEntry, 1);
+	prop->string = rb_refstring_ref (keyword);
+	prop->refcount = 1;
+
+	ptr = egg_sequence_insert_sorted (model->priv->keywords, prop,
+					(GCompareDataFunc) rhythmdb_keyword_model_compare,
+					model);
+	g_hash_table_insert (model->priv->reverse_map,
+			     (gpointer)keyword,
+			     ptr);
+
+	iter.user_data = ptr;
+	path = rhythmdb_keyword_model_get_path (GTK_TREE_MODEL (model), &iter);
+	gtk_tree_model_row_inserted (GTK_TREE_MODEL (model), path, &iter);
+	gtk_tree_path_free (path);
+}
+
+static void
+rhythmdb_keyword_model_delete_entry (RhythmDBKeywordModel *model,
+				     RhythmDBEntry *entry)
+{
+	GList *keywords;
+
+	if (g_hash_table_lookup (model->priv->entries, entry))
+		return;
+
+	keywords = rhythmdb_entry_keywords_get (model->priv->db, entry);
+	rb_debug ("entry %p deleted with %d keywords", entry, g_list_length (keywords));
+
+	g_list_foreach (keywords, (GFunc)rhythmdb_keyword_model_delete_keyword, model);
+	g_list_foreach (keywords, (GFunc)rb_refstring_unref, NULL);
+	g_list_free (keywords);
+}
+
+static void
+rhythmdb_keyword_model_delete_keyword (RBRefString *keyword, RhythmDBKeywordModel *model)
+{
+	EggSequenceIter *ptr;
+	RhythmDBKeywordModelEntry *prop;
+	GtkTreePath *path;
+	GtkTreeIter iter;
+
+	g_assert ((ptr = g_hash_table_lookup (model->priv->reverse_map, keyword)));
+
+	iter.stamp = model->priv->stamp;
+	iter.user_data = ptr;
+
+	model->priv->all->refcount--;
+
+	prop = egg_sequence_get (ptr);
+	rb_debug ("deleting '%s', refcount: %d, to model %p", rb_refstring_get (keyword), prop->refcount, model);
+	prop->refcount--;
+	if (prop->refcount > 0) {
+		path = rhythmdb_keyword_model_get_path (GTK_TREE_MODEL (model), &iter);
+		gtk_tree_model_row_changed (GTK_TREE_MODEL (model), path, &iter);
+		gtk_tree_path_free (path);
+		return;
+	}
+
+	path = rhythmdb_keyword_model_get_path (GTK_TREE_MODEL (model), &iter);
+	g_signal_emit (G_OBJECT (model), rhythmdb_keyword_model_signals[PRE_ROW_DELETION], 0);
+	gtk_tree_model_row_deleted (GTK_TREE_MODEL (model), path);
+	gtk_tree_path_free (path);
+
+	egg_sequence_remove (ptr);
+	g_hash_table_remove (model->priv->reverse_map, keyword);
+	rb_refstring_unref (prop->string);
+
+	g_free (prop);
+}
+
+gboolean
+rhythmdb_keyword_model_iter_from_string (RhythmDBKeywordModel *model,
+					  const char *name,
+					  GtkTreeIter *iter)
+{
+	EggSequenceIter *ptr;
+
+	if (name == NULL) {
+		if (iter) {
+			iter->stamp = model->priv->stamp;
+			iter->user_data = model->priv->all;
+		}
+		return TRUE;
+	}
+
+	ptr = g_hash_table_lookup (model->priv->reverse_map, name);
+	if (!ptr)
+		return FALSE;
+
+	if (iter) {
+		iter->stamp = model->priv->stamp;
+		iter->user_data = ptr;
+	}
+
+	return TRUE;
+}
+
+static GtkTreeModelFlags
+rhythmdb_keyword_model_get_flags (GtkTreeModel *model)
+{
+	return GTK_TREE_MODEL_ITERS_PERSIST | GTK_TREE_MODEL_LIST_ONLY;
+}
+
+static gint
+rhythmdb_keyword_model_get_n_columns (GtkTreeModel *tree_model)
+{
+	return RHYTHMDB_KEYWORD_MODEL_COLUMN_LAST;
+}
+
+static GType
+rhythmdb_keyword_model_get_column_type (GtkTreeModel *tree_model,
+					 int index)
+{
+	switch (index) {
+	case RHYTHMDB_KEYWORD_MODEL_COLUMN_TITLE:
+		return G_TYPE_STRING;
+	case RHYTHMDB_KEYWORD_MODEL_COLUMN_PRIORITY:
+		return G_TYPE_BOOLEAN;
+	case RHYTHMDB_KEYWORD_MODEL_COLUMN_NUMBER:
+		return G_TYPE_UINT;
+	default:
+		g_assert_not_reached ();
+		return G_TYPE_INVALID;
+	}
+}
+
+static gboolean
+rhythmdb_keyword_model_get_iter (GtkTreeModel *tree_model,
+				  GtkTreeIter *iter,
+				  GtkTreePath *path)
+{
+	RhythmDBKeywordModel *model = RHYTHMDB_KEYWORD_MODEL (tree_model);
+	guint index;
+	EggSequenceIter *ptr;
+
+	index = gtk_tree_path_get_indices (path)[0];
+
+	if (index == 0) {
+		iter->stamp = model->priv->stamp;
+		iter->user_data = model->priv->all;
+		return TRUE;
+	}
+
+	index--;
+	if (index >= egg_sequence_get_length (model->priv->keywords))
+		return FALSE;
+
+	ptr = egg_sequence_get_iter_at_pos (model->priv->keywords, index);
+
+	iter->stamp = model->priv->stamp;
+	iter->user_data = ptr;
+
+	return TRUE;
+}
+
+static GtkTreePath *
+rhythmdb_keyword_model_get_path (GtkTreeModel *tree_model,
+				  GtkTreeIter  *iter)
+{
+	RhythmDBKeywordModel *model = RHYTHMDB_KEYWORD_MODEL (tree_model);
+	GtkTreePath *path;
+
+	g_return_val_if_fail (iter->stamp == model->priv->stamp, NULL);
+
+	if (iter->user_data == model->priv->all) {
+		return gtk_tree_path_new_first ();
+	}
+
+	if (egg_sequence_iter_is_end (iter->user_data))
+		return NULL;
+
+	path = gtk_tree_path_new ();
+	if (iter->user_data == model->priv->all)
+		gtk_tree_path_append_index (path, 0);
+	else
+		gtk_tree_path_append_index (path, egg_sequence_iter_get_position (iter->user_data) + 1);
+	return path;
+}
+
+static void
+rhythmdb_keyword_model_get_value (GtkTreeModel *tree_model,
+				   GtkTreeIter *iter,
+				   gint column,
+				   GValue *value)
+{
+	RhythmDBKeywordModel *model = RHYTHMDB_KEYWORD_MODEL (tree_model);
+
+	g_return_if_fail (model->priv->stamp == iter->stamp);
+
+	if (iter->user_data == model->priv->all) {
+		switch (column) {
+		case RHYTHMDB_KEYWORD_MODEL_COLUMN_TITLE:
+			g_value_init (value, G_TYPE_STRING);
+			g_value_set_string (value, rb_refstring_get (model->priv->all->string));
+			break;
+		case RHYTHMDB_KEYWORD_MODEL_COLUMN_PRIORITY:
+			g_value_init (value, G_TYPE_BOOLEAN);
+			g_value_set_boolean (value, TRUE);
+			break;
+		case RHYTHMDB_KEYWORD_MODEL_COLUMN_NUMBER:
+			g_value_init (value, G_TYPE_UINT);
+			g_value_set_uint (value, model->priv->all->refcount);
+			break;
+		default:
+			g_assert_not_reached ();
+		}
+	} else {
+		RhythmDBKeywordModelEntry *prop;
+
+		g_return_if_fail (!egg_sequence_iter_is_end (iter->user_data));
+
+		prop = egg_sequence_get (iter->user_data);
+		switch (column) {
+		case RHYTHMDB_KEYWORD_MODEL_COLUMN_TITLE:
+			g_value_init (value, G_TYPE_STRING);
+			g_value_set_string (value, rb_refstring_get (prop->string));
+			break;
+		case RHYTHMDB_KEYWORD_MODEL_COLUMN_PRIORITY:
+			g_value_init (value, G_TYPE_BOOLEAN);
+			g_value_set_boolean (value, prop == model->priv->all);
+			break;
+		case RHYTHMDB_KEYWORD_MODEL_COLUMN_NUMBER:
+			g_value_init (value, G_TYPE_UINT);
+			g_value_set_uint (value, prop->refcount);
+			break;
+		default:
+			g_assert_not_reached ();
+		}
+	}
+}
+
+static gboolean
+rhythmdb_keyword_model_iter_next (GtkTreeModel  *tree_model,
+				   GtkTreeIter   *iter)
+{
+	RhythmDBKeywordModel *model = RHYTHMDB_KEYWORD_MODEL (tree_model);
+
+	g_return_val_if_fail (iter->stamp == model->priv->stamp, FALSE);
+
+	if (iter->user_data == model->priv->all) {
+		iter->user_data = egg_sequence_get_begin_iter (model->priv->keywords);
+	} else {
+		g_return_val_if_fail (!egg_sequence_iter_is_end (iter->user_data), FALSE);
+		iter->user_data = egg_sequence_iter_next (iter->user_data);
+	}
+
+	return !egg_sequence_iter_is_end (iter->user_data);
+}
+
+static gboolean
+rhythmdb_keyword_model_iter_children (GtkTreeModel *tree_model,
+				       GtkTreeIter  *iter,
+				       GtkTreeIter  *parent)
+{
+	RhythmDBKeywordModel *model = RHYTHMDB_KEYWORD_MODEL (tree_model);
+
+	if (parent != NULL)
+		return FALSE;
+
+	iter->stamp = model->priv->stamp;
+	iter->user_data = model->priv->all;
+
+	return TRUE;
+}
+
+static gboolean
+rhythmdb_keyword_model_iter_has_child (GtkTreeModel *tree_model,
+					GtkTreeIter *iter)
+{
+	return FALSE;
+}
+
+static gint
+rhythmdb_keyword_model_iter_n_children (GtkTreeModel *tree_model,
+					 GtkTreeIter *iter)
+{
+	RhythmDBKeywordModel *model = RHYTHMDB_KEYWORD_MODEL (tree_model);
+
+	if (iter)
+		g_return_val_if_fail (model->priv->stamp == iter->stamp, -1);
+
+	if (iter == NULL)
+		return 1 + egg_sequence_get_length (model->priv->keywords);
+
+	return 0;
+}
+
+static gboolean
+rhythmdb_keyword_model_iter_nth_child (GtkTreeModel *tree_model,
+					GtkTreeIter *iter,
+					GtkTreeIter *parent,
+					gint n)
+{
+	RhythmDBKeywordModel *model = RHYTHMDB_KEYWORD_MODEL (tree_model);
+	EggSequenceIter *child;
+
+	if (parent)
+		return FALSE;
+
+	if (n != 0) {
+		child = egg_sequence_get_iter_at_pos (model->priv->keywords, n);
+
+		if (egg_sequence_iter_is_end (child))
+			return FALSE;
+		iter->user_data = child;
+	} else {
+		iter->user_data = model->priv->all;
+	}
+
+	iter->stamp = model->priv->stamp;
+
+	return TRUE;
+}
+
+static gboolean
+rhythmdb_keyword_model_iter_parent (GtkTreeModel *tree_model,
+				     GtkTreeIter  *iter,
+				     GtkTreeIter  *child)
+{
+	return FALSE;
+}
+
+static gboolean
+rhythmdb_keyword_model_row_draggable (RbTreeDragSource *dragsource,
+				       GList *paths)
+{
+	return TRUE;
+}
+
+static gboolean
+rhythmdb_keyword_model_drag_data_delete (RbTreeDragSource *dragsource,
+					  GList *paths)
+{
+	/* not supported */
+	return TRUE;
+}
+
+/*Going through hoops to avoid nested functions*/
+struct QueryModelCbStruct {
+	RhythmDB *db;
+	GString *reply;
+	gint target;
+};
+
+static gboolean
+query_model_cb (GtkTreeModel *query_model,
+ 		GtkTreePath *path,
+		GtkTreeIter *iter,
+ 		struct QueryModelCbStruct *data)
+{
+ 	RhythmDBEntry *entry;
+
+ 	gtk_tree_model_get (query_model, iter, 0, &entry, -1);
+	if (data->target == TARGET_ENTRIES) {
+		g_string_append_printf (data->reply,
+					"%ld",
+					rhythmdb_entry_get_ulong (entry, RHYTHMDB_PROP_ENTRY_ID));
+	} else if (data->target == TARGET_URIS) {
+		const char *uri;
+		uri = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_LOCATION);
+		g_string_append (data->reply, uri);
+	}
+ 	g_string_append (data->reply, "\r\n");
+
+ 	rhythmdb_entry_unref (entry);
+ 	return FALSE;
+}
+
+static gboolean
+rhythmdb_keyword_model_drag_data_get (RbTreeDragSource *dragsource,
+				       GList *paths,
+				       GtkSelectionData *selection_data)
+{
+	RhythmDBKeywordModel *model = RHYTHMDB_KEYWORD_MODEL (dragsource);
+	guint target;
+
+	if (!gtk_target_list_find (rhythmdb_keyword_model_keyword_drag_target_list,
+				   selection_data->target,
+				   &target)) {
+		return FALSE;
+	}
+
+	if (target == TARGET_URIS || target == TARGET_ENTRIES) {
+		RhythmDB *db = model->priv->db;
+ 		RhythmDBQueryModel *query_model;
+ 		GString* reply = g_string_new ("");
+ 		GtkTreeIter iter;
+ 		gboolean is_all = FALSE;
+ 		struct QueryModelCbStruct tmp;
+		GtkTreePath *path;
+		GCompareDataFunc sort_func = NULL;
+		gpointer sort_data;
+		gboolean sort_reverse;
+
+		query_model = rhythmdb_query_model_new_empty (db);
+		/* FIXME the sort order on the query model at this point is usually
+		 * not the user's selected sort order.
+		 */
+		g_object_get (G_OBJECT (model->priv->query_model),
+			      "sort-func", &sort_func,
+			      "sort-data", &sort_data,
+			      "sort-reverse", &sort_reverse,
+			      NULL);
+		rhythmdb_query_model_set_sort_order (RHYTHMDB_QUERY_MODEL (query_model),
+						     sort_func, GUINT_TO_POINTER (sort_data), NULL, sort_reverse);
+
+		rb_debug ("getting drag data as uri list");
+		/* check if first selected row is 'All' */
+		path = gtk_tree_row_reference_get_path (paths->data);
+		if (path && gtk_tree_model_get_iter (GTK_TREE_MODEL (model), &iter, path))
+			gtk_tree_model_get (GTK_TREE_MODEL (model), &iter,
+					    RHYTHMDB_KEYWORD_MODEL_COLUMN_PRIORITY,
+					    &is_all, -1);
+		gtk_tree_path_free (path);
+		if (is_all) {
+			g_object_set (query_model,
+				      "base-model", model->priv->query_model,
+				      NULL);
+		} else {
+ 			GList *row;
+			GPtrArray *subquery = g_ptr_array_new ();
+
+ 			for (row = paths; row; row = row->next) {
+ 				char* name;
+				path = gtk_tree_row_reference_get_path (row->data);
+ 				if (path && gtk_tree_model_get_iter (GTK_TREE_MODEL (model), &iter, path)) {
+	 				gtk_tree_model_get (GTK_TREE_MODEL (model), &iter,
+	 						    RHYTHMDB_KEYWORD_MODEL_COLUMN_TITLE,
+							    &name, -1);
+	 				if (row == paths) {
+	 					rhythmdb_query_append (db, subquery,
+	 							       RHYTHMDB_QUERY_PROP_EQUALS,
+	 							       0, name,
+								       RHYTHMDB_QUERY_END);
+					} else {
+	 					rhythmdb_query_append (db, subquery,
+	 							       RHYTHMDB_QUERY_DISJUNCTION,
+	 							       RHYTHMDB_QUERY_PROP_EQUALS,
+	 							       0, name,
+	 							       RHYTHMDB_QUERY_END);
+	 				}
+				}
+
+				gtk_tree_path_free (path);
+ 				g_free (name);
+ 			}
+
+			g_object_set (query_model,
+				      "query", subquery,
+				      "base-model", model->priv->query_model,
+				      NULL);
+			rhythmdb_query_free (subquery);
+		}
+
+		tmp.db = db;
+ 		tmp.reply = reply;
+		tmp.target = target;
+ 		/* Too bad that we're on the main thread. Why doesn't gtk call us async?
+ 		 * How does file-roller manage? - it seems it refuses the drop when it isn't
+		 * done unpacking. In which case, we should tweak the drop acknowledgement,
+		 * and prepare the query using do_full_query_async. The query would be
+		 * hooked to the drag context.
+		 */
+ 		gtk_tree_model_foreach (GTK_TREE_MODEL (query_model),
+ 					(GtkTreeModelForeachFunc) query_model_cb,
+ 					&tmp);
+
+		g_object_unref (query_model);
+
+ 		gtk_selection_data_set (selection_data,
+ 		                        selection_data->target,
+ 		                        8, (guchar *)reply->str,
+ 		                        reply->len);
+ 		g_string_free (reply, TRUE);
+
+	} else {
+		char* title;
+		GList *p;
+		GString* reply = g_string_new ("");
+
+		rb_debug ("getting drag data as list of keyword values");
+
+		for (p = paths; p; p = p->next) {
+			GtkTreeIter iter;
+			GtkTreePath *path;
+
+			path = gtk_tree_row_reference_get_path (p->data);
+			if (path && gtk_tree_model_get_iter (GTK_TREE_MODEL (model), &iter, path)) {
+				gtk_tree_model_get (GTK_TREE_MODEL (model), &iter,
+						    RHYTHMDB_KEYWORD_MODEL_COLUMN_TITLE, &title, -1);
+				g_string_append (reply, title);
+				if (p->next)
+					g_string_append (reply, "\r\n");
+				g_free (title);
+			}
+			gtk_tree_path_free (path);
+		}
+		gtk_selection_data_set (selection_data,
+					selection_data->target,
+					8, (guchar *)reply->str,
+					reply->len);
+		g_string_free (reply, TRUE);
+	}
+
+	return TRUE;
+}
+
+void
+rhythmdb_keyword_model_enable_drag (RhythmDBKeywordModel *model,
+				     GtkTreeView *view)
+{
+	rb_tree_dnd_add_drag_source_support (view,
+					     GDK_BUTTON1_MASK,
+					     targets_keyword, G_N_ELEMENTS (targets_keyword),
+					     GDK_ACTION_COPY);
+}
+
+static gboolean
+rhythmdb_keyword_model_perform_sync (RhythmDBKeywordModel *model)
+{
+	GtkTreeIter iter;
+	GtkTreePath *path;
+
+	GDK_THREADS_ENTER ();
+
+	iter.stamp = model->priv->stamp;
+	iter.user_data = model->priv->all;
+	path = rhythmdb_keyword_model_get_path (GTK_TREE_MODEL (model), &iter);
+	gtk_tree_model_row_changed (GTK_TREE_MODEL (model), path, &iter);
+	gtk_tree_path_free (path);
+
+	model->priv->syncing_id = 0;
+	GDK_THREADS_LEAVE ();
+	return FALSE;
+}
+
+static void
+rhythmdb_keyword_model_sync (RhythmDBKeywordModel *model)
+{
+	if (model->priv->syncing_id != 0)
+		return;
+
+	model->priv->syncing_id = g_idle_add ((GSourceFunc)rhythmdb_keyword_model_perform_sync, model);
+}
+
+static void
+rhythmdb_keyword_model_prop_changed_cb (RhythmDB *db,
+					 RhythmDBEntry *entry,
+					 RhythmDBPropType propid,
+					 const GValue *old,
+					 const GValue *new,
+					 RhythmDBKeywordModel *model)
+{
+	if (propid == RHYTHMDB_PROP_HIDDEN) {
+		gboolean old_val = g_value_get_boolean (old);
+		gboolean new_val = g_value_get_boolean (new);
+
+		if (old_val != new_val) {
+			if (new_val == FALSE) {
+				g_assert (g_hash_table_remove (model->priv->entries, entry));
+				rhythmdb_keyword_model_insert_entry (model, entry);
+			} else {
+				g_assert (g_hash_table_lookup (model->priv->entries, entry) == NULL);
+
+				rhythmdb_keyword_model_delete_entry (model, entry);
+				g_hash_table_insert (model->priv->entries, entry, GINT_TO_POINTER (1));
+			}
+		}
+	}
+
+	rhythmdb_keyword_model_sync (model);
+}
+
+/* This should really be standard. */
+#define ENUM_ENTRY(NAME, DESC) { NAME, "" #NAME "", DESC }
+
+GType
+rhythmdb_keyword_model_column_get_type (void)
+{
+	static GType etype = 0;
+
+	if (etype == 0)	{
+		static const GEnumValue values[] = {
+			ENUM_ENTRY (RHYTHMDB_KEYWORD_MODEL_COLUMN_TITLE, "Keyword title"),
+			ENUM_ENTRY (RHYTHMDB_KEYWORD_MODEL_COLUMN_PRIORITY, "Value priority"),
+			ENUM_ENTRY (RHYTHMDB_KEYWORD_MODEL_COLUMN_NUMBER, "Track count"),
+			{ 0, 0, 0 }
+		};
+
+		etype = g_enum_register_static ("RhythmDBKeywordModelColumn", values);
+	}
+
+	return etype;
+}
Index: rhythmdb/rhythmdb-property-model.c
===================================================================
--- rhythmdb/rhythmdb-property-model.c	(revision 4845)
+++ rhythmdb/rhythmdb-property-model.c	(working copy)
@@ -149,7 +149,7 @@ struct RhythmDBPropertyModelPrivate
 	RhythmDB *db;
 
 	RhythmDBQueryModel *query_model;
-	GHashTable *entries;
+	GHashTable *entries; /* entries that aren't present because they're hidden */
 
 	RhythmDBPropType propid;
 	RhythmDBPropType sort_propid;
Index: rhythmdb/rb-refstring.c
===================================================================
--- rhythmdb/rb-refstring.c	(revision 4845)
+++ rhythmdb/rb-refstring.c	(working copy)
@@ -218,3 +218,17 @@ rb_refstring_equal (gconstpointer ap, gc
 {
 	return (ap == bp);
 }
+
+GType
+rb_refstring_get_type (void)
+{
+	static GType type = 0;
+
+	if (G_UNLIKELY (type == 0)) {
+		type = g_boxed_type_register_static ("RBRefString",
+						     (GBoxedCopyFunc)rb_refstring_ref,
+						     (GBoxedFreeFunc)rb_refstring_unref);
+	}
+
+	return type;
+}
Index: rhythmdb/rhythmdb-keyword-model.h
===================================================================
--- rhythmdb/rhythmdb-keyword-model.h	(revision 0)
+++ rhythmdb/rhythmdb-keyword-model.h	(revision 0)
@@ -0,0 +1,77 @@
+/*
+ *  Copyright (C) 2007 James Livingston <doclivingston@gmail.com>
+ *
+ *  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.
+ *
+ */
+
+#include "config.h"
+#include <glib-object.h>
+#include <glib.h>
+#include <gtk/gtktreeview.h>
+
+#include "rhythmdb.h"
+#include "rhythmdb-query-model.h"
+
+#ifndef RHYTHMDB_KEYWORD_MODEL_H
+#define RHYTHMDB_KEYWORD_MODEL_H
+
+G_BEGIN_DECLS
+
+#define RHYTHMDB_TYPE_KEYWORD_MODEL         (rhythmdb_keyword_model_get_type ())
+#define RHYTHMDB_KEYWORD_MODEL(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), RHYTHMDB_TYPE_KEYWORD_MODEL, RhythmDBKeywordModel))
+#define RHYTHMDB_KEYWORD_MODEL_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST((k), RHYTHMDB_KEYWORD_MODEL_TYPE, RhythmDBKeywordModelClass))
+#define RHYTHMDB_IS_KEYWORD_MODEL(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), RHYTHMDB_TYPE_KEYWORD_MODEL))
+#define RHYTHMDB_IS_KEYWORD_MODEL_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), RHYTHMDB_TYPE_KEYWORD_MODEL))
+#define RHYTHMDB_KEYWORD_MODEL_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), RHYTHMDB_TYPE_KEYWORD_MODEL, RhythmDBKeywordModelClass))
+
+typedef enum
+{
+	RHYTHMDB_KEYWORD_MODEL_COLUMN_TITLE,
+	RHYTHMDB_KEYWORD_MODEL_COLUMN_PRIORITY,
+	RHYTHMDB_KEYWORD_MODEL_COLUMN_NUMBER,
+	RHYTHMDB_KEYWORD_MODEL_COLUMN_LAST,
+} RhythmDBKeywordModelColumn;
+
+GType rhythmdb_keyword_model_column_get_type (void);
+#define RHYTHMDB_TYPE_KEYWORD_MODEL_COLUMN (rhythmdb_keyword_model_column_get_type ())
+
+typedef struct RhythmDBKeywordModelPrivate RhythmDBKeywordModelPrivate;
+
+typedef struct
+{
+	GObject parent;
+
+	RhythmDBKeywordModelPrivate *priv;
+} RhythmDBKeywordModel;
+
+typedef struct
+{
+	GObjectClass parent;
+
+	void (*pre_row_deletion) (RhythmDBKeywordModel *model);
+} RhythmDBKeywordModelClass;
+
+GType			rhythmdb_keyword_model_get_type	(void);
+
+RhythmDBKeywordModel *	rhythmdb_keyword_model_new		(RhythmDB *db);
+
+gboolean		rhythmdb_keyword_model_iter_from_string(RhythmDBKeywordModel *model, const char *name, GtkTreeIter *iter);
+
+void			rhythmdb_keyword_model_enable_drag	(RhythmDBKeywordModel *model, GtkTreeView *view);
+
+G_END_DECLS
+
+#endif /* __RHYTHMBDB_KEYWORD_MODEL_H */
Index: rhythmdb/rb-refstring.h
===================================================================
--- rhythmdb/rb-refstring.h	(revision 4845)
+++ rhythmdb/rb-refstring.h	(working copy)
@@ -24,6 +24,8 @@
 #ifndef __RB_REFSTRING_H
 #define __RB_REFSTRING_H
 
+#define RB_TYPE_REFSTRING      (rb_refstring_get_type ())
+
 typedef struct RBRefString RBRefString;
 
 void		rb_refstring_system_init (void);
@@ -42,4 +44,6 @@ const char *	rb_refstring_get_sort_key (
 guint rb_refstring_hash (gconstpointer p);
 gboolean rb_refstring_equal (gconstpointer ap, gconstpointer bp);
 
+GType rb_refstring_get_type (void);
+
 #endif
Index: rhythmdb/rhythmdb-tree.c
===================================================================
--- rhythmdb/rhythmdb-tree.c	(revision 4845)
+++ rhythmdb/rhythmdb-tree.c	(working copy)
@@ -79,6 +79,10 @@ static void rhythmdb_tree_entry_foreach 
 static gint64 rhythmdb_tree_entry_count (RhythmDB *adb);
 static void rhythmdb_tree_entry_foreach_by_type (RhythmDB *adb, RhythmDBEntryType type, GFunc func, gpointer user_data);
 static gint64 rhythmdb_tree_entry_count_by_type (RhythmDB *adb, RhythmDBEntryType type);
+static gboolean rhythmdb_tree_entry_keyword_add (RhythmDB *adb, RhythmDBEntry *entry, RBRefString *keyword);
+static gboolean rhythmdb_tree_entry_keyword_remove (RhythmDB *adb, RhythmDBEntry *entry, RBRefString *keyword);
+static gboolean rhythmdb_tree_entry_keyword_has (RhythmDB *adb, RhythmDBEntry *entry, RBRefString *keyword);
+static GList* rhythmdb_tree_entry_keywords_get (RhythmDB *adb, RhythmDBEntry *entry);
 static void rhythmdb_tree_do_full_query (RhythmDB *db, GPtrArray *query,
 					 RhythmDBQueryResults *results,
 					 gboolean *cancel);
@@ -114,6 +118,7 @@ static RhythmDBTreeProperty *get_or_crea
 							 RBRefString *name);
 
 static void remove_entry_from_album (RhythmDBTree *db, RhythmDBEntry *entry);
+static void remove_entry_from_keywords (RhythmDBTree *db, RhythmDBEntry *entry);
 
 static GList *split_query_by_disjunctions (RhythmDBTree *db, GPtrArray *query);
 static gboolean evaluate_conjunctive_subquery (RhythmDBTree *db, GPtrArray *query,
@@ -125,6 +130,9 @@ struct RhythmDBTreePrivate
 	GHashTable *entry_ids;
 	GMutex *entries_lock;
 
+	GHashTable *keywords; /* GHashTable<RBRefString, GHashTable<RhyhmDBEntry, 1>> */
+	GMutex *keywords_lock;
+
 	GHashTable *genres;
 	GHashTable *unknown_entry_types;
 	GMutex *genres_lock;
@@ -184,6 +192,10 @@ rhythmdb_tree_class_init (RhythmDBTreeCl
 	rhythmdb_class->impl_entry_count = rhythmdb_tree_entry_count;
 	rhythmdb_class->impl_entry_foreach_by_type = rhythmdb_tree_entry_foreach_by_type;
 	rhythmdb_class->impl_entry_count_by_type = rhythmdb_tree_entry_count_by_type;
+	rhythmdb_class->impl_entry_keyword_add = rhythmdb_tree_entry_keyword_add;
+	rhythmdb_class->impl_entry_keyword_remove = rhythmdb_tree_entry_keyword_remove;
+	rhythmdb_class->impl_entry_keyword_has = rhythmdb_tree_entry_keyword_has;
+	rhythmdb_class->impl_entry_keywords_get = rhythmdb_tree_entry_keywords_get;
 	rhythmdb_class->impl_evaluate_query = rhythmdb_tree_evaluate_query;
 	rhythmdb_class->impl_do_full_query = rhythmdb_tree_do_full_query;
 	rhythmdb_class->impl_entry_type_registered = rhythmdb_tree_entry_type_registered;
@@ -197,15 +209,18 @@ rhythmdb_tree_init (RhythmDBTree *db)
 	db->priv = RHYTHMDB_TREE_GET_PRIVATE (db);
 
 	db->priv->entries = g_hash_table_new (rb_refstring_hash, rb_refstring_equal);
-
 	db->priv->entry_ids = g_hash_table_new (g_direct_hash, g_direct_equal);
+	db->priv->entries_lock = g_mutex_new();
+
+	db->priv->keywords = g_hash_table_new_full (rb_refstring_hash, rb_refstring_equal,
+						    (GDestroyNotify)rb_refstring_unref, (GDestroyNotify)g_hash_table_destroy);
+	db->priv->keywords_lock = g_mutex_new();
 
+	db->priv->genres_lock = g_mutex_new();
 	db->priv->genres = g_hash_table_new_full (g_direct_hash, g_direct_equal,
 						  NULL, (GDestroyNotify)g_hash_table_destroy);
-	db->priv->unknown_entry_types = g_hash_table_new (rb_refstring_hash, rb_refstring_equal);
 
-	db->priv->entries_lock = g_mutex_new();
-	db->priv->genres_lock = g_mutex_new();
+	db->priv->unknown_entry_types = g_hash_table_new (rb_refstring_hash, rb_refstring_equal);
 }
 
 static void
@@ -261,6 +276,9 @@ rhythmdb_tree_finalize (GObject *object)
 	g_hash_table_destroy (db->priv->entry_ids);
 	g_mutex_free (db->priv->entries_lock);
 
+	g_hash_table_destroy (db->priv->keywords);
+	g_mutex_free (db->priv->keywords_lock);
+
 	g_hash_table_destroy (db->priv->genres);
 	g_mutex_free (db->priv->genres_lock);
 
@@ -282,6 +300,7 @@ struct RhythmDBTreeLoadContext
 		RHYTHMDB_TREE_PARSER_STATE_RHYTHMDB,
 		RHYTHMDB_TREE_PARSER_STATE_ENTRY,
 		RHYTHMDB_TREE_PARSER_STATE_ENTRY_PROPERTY,
+		RHYTHMDB_TREE_PARSER_STATE_ENTRY_KEYWORD,
 		RHYTHMDB_TREE_PARSER_STATE_UNKNOWN_ENTRY,
 		RHYTHMDB_TREE_PARSER_STATE_UNKNOWN_ENTRY_PROPERTY,
 		RHYTHMDB_TREE_PARSER_STATE_END,
@@ -384,18 +403,20 @@ rhythmdb_tree_parser_start_element (stru
 		break;
 	}
 	case RHYTHMDB_TREE_PARSER_STATE_ENTRY:
-	{
-		int val = rhythmdb_propid_from_nice_elt_name (RHYTHMDB (ctx->db), BAD_CAST name);
-		if (val < 0) {
-			ctx->in_unknown_elt++;
-			break;
-		}
+		if (strcmp (name, "keyword") == 0) {
+			ctx->state = RHYTHMDB_TREE_PARSER_STATE_ENTRY_KEYWORD;
+		} else {
+			int val = rhythmdb_propid_from_nice_elt_name (RHYTHMDB (ctx->db), BAD_CAST name);
+			if (val < 0) {
+				ctx->in_unknown_elt++;
+				break;
+			}
 
-		ctx->state = RHYTHMDB_TREE_PARSER_STATE_ENTRY_PROPERTY;
-		ctx->propid = val;
+			ctx->state = RHYTHMDB_TREE_PARSER_STATE_ENTRY_PROPERTY;
+			ctx->propid = val;
+		}
 		g_string_truncate (ctx->buf, 0);
 		break;
-	}
 	case RHYTHMDB_TREE_PARSER_STATE_UNKNOWN_ENTRY:
 	{
 		RhythmDBUnknownEntryProperty *prop;
@@ -410,6 +431,7 @@ rhythmdb_tree_parser_start_element (stru
 	}
 	case RHYTHMDB_TREE_PARSER_STATE_UNKNOWN_ENTRY_PROPERTY:
 	case RHYTHMDB_TREE_PARSER_STATE_ENTRY_PROPERTY:
+	case RHYTHMDB_TREE_PARSER_STATE_ENTRY_KEYWORD:
 	case RHYTHMDB_TREE_PARSER_STATE_END:
 	break;
 	}
@@ -553,6 +575,17 @@ rhythmdb_tree_parser_end_element (struct
 		ctx->state = RHYTHMDB_TREE_PARSER_STATE_ENTRY;
 		break;
 	}
+	case RHYTHMDB_TREE_PARSER_STATE_ENTRY_KEYWORD:
+	{
+		RBRefString *keyword;
+
+		keyword = rb_refstring_new (ctx->buf->str);
+		rhythmdb_entry_keyword_add (RHYTHMDB(ctx->db), ctx->entry, keyword);
+		rb_refstring_unref (keyword);
+
+		ctx->state = RHYTHMDB_TREE_PARSER_STATE_ENTRY;
+		break;
+	}
 	case RHYTHMDB_TREE_PARSER_STATE_UNKNOWN_ENTRY_PROPERTY:
 	{
 		RhythmDBUnknownEntryProperty *prop;
@@ -585,6 +618,7 @@ rhythmdb_tree_parser_characters (struct 
 	switch (ctx->state)
 	{
 	case RHYTHMDB_TREE_PARSER_STATE_ENTRY_PROPERTY:
+	case RHYTHMDB_TREE_PARSER_STATE_ENTRY_KEYWORD:
 	case RHYTHMDB_TREE_PARSER_STATE_UNKNOWN_ENTRY_PROPERTY:
 		g_string_append_len (ctx->buf, data, len);
 		break;
@@ -802,6 +836,7 @@ save_entry (RhythmDBTree *db,
 	RhythmDBPropType i;
 	RhythmDBPodcastFields *podcast = NULL;
 	xmlChar *encoded;
+	GList *keywords, *l;
 
 	if (ctx->error)
 		return;
@@ -967,6 +1002,23 @@ save_entry (RhythmDBTree *db,
 		}
 	}
 
+	/* save keywords */
+	keywords = rhythmdb_entry_keywords_get (RHYTHMDB (db), entry);
+
+	for (l = keywords; l != NULL; l = g_list_next (l)) {
+		RBRefString *keyword = (RBRefString*)l->data;
+
+		RHYTHMDB_FWRITE_STATICSTR ("    <keyword>", ctx->handle, ctx->error);
+		encoded	= xmlEncodeEntitiesReentrant (NULL, BAD_CAST rb_refstring_get (keyword));
+		RHYTHMDB_FWRITE (encoded, 1, xmlStrlen (encoded), ctx->handle, ctx->error);
+		g_free (encoded);
+		RHYTHMDB_FWRITE_STATICSTR ("</keyword>\n", ctx->handle, ctx->error);
+
+		rb_refstring_unref (keyword);
+	}
+
+	g_list_free (keywords);
+
 	RHYTHMDB_FWRITE_STATICSTR ("  </entry>\n", ctx->handle, ctx->error);
 }
 
@@ -1461,6 +1513,11 @@ rhythmdb_tree_entry_delete (RhythmDB *ad
 
 	remove_entry_from_album (db, entry);
 
+	/* remove all keywords */
+	g_mutex_lock (db->priv->keywords_lock);
+	remove_entry_from_keywords (db, entry);
+	g_mutex_unlock (db->priv->keywords_lock);
+
 	g_mutex_lock (db->priv->entries_lock);
 	g_assert (g_hash_table_remove (db->priv->entries, entry->location));
 	g_assert (g_hash_table_remove (db->priv->entry_ids, GINT_TO_POINTER (entry->id)));
@@ -1478,12 +1535,16 @@ remove_one_song (gpointer key,
 		 RhythmDBEntry *entry,
 		 RbEntryRemovalCtxt *ctxt)
 {
+	RhythmDBTree *db = RHYTHMDB_TREE(ctxt->db);
 	g_return_val_if_fail (entry != NULL, FALSE);
 
 	if (entry->type == ctxt->type) {
 		rhythmdb_emit_entry_deleted (ctxt->db, entry);
-		remove_entry_from_album (RHYTHMDB_TREE (ctxt->db), entry);
-		g_hash_table_remove (RHYTHMDB_TREE (ctxt->db)->priv->entry_ids, GINT_TO_POINTER (entry->id));
+		g_mutex_lock (db->priv->keywords_lock);
+		remove_entry_from_keywords (db, entry);
+		g_mutex_unlock (db->priv->keywords_lock);
+		remove_entry_from_album (db, entry);
+		g_hash_table_remove (db->priv->entry_ids, GINT_TO_POINTER (entry->id));
 		rhythmdb_entry_unref (entry);
 		return TRUE;
 	}
@@ -1741,6 +1802,26 @@ evaluate_conjunctive_subquery (RhythmDBT
 		case RHYTHMDB_QUERY_PROP_LESS:
 			RHYTHMDB_PROPERTY_COMPARE (>)
 			break;
+		case RHYTHMDB_QUERY_KEYWORD_HAS:
+		case RHYTHMDB_QUERY_KEYWORD_HASNT:
+		{
+			const char *str;
+			RBRefString *keyword;
+			gboolean has;
+
+			str = (const char*) g_value_get_boxed (data->val);
+			keyword = rb_refstring_find (str);
+			if (keyword != NULL) {
+				has = rhythmdb_tree_entry_keyword_has (db, entry, keyword);
+			} else {
+				has = FALSE;
+			}
+			if ((data->type == RHYTHMDB_QUERY_KEYWORD_HAS) ^ has)
+				return FALSE;
+			else
+				continue;
+			break;
+		}
 		case RHYTHMDB_QUERY_END:
 		case RHYTHMDB_QUERY_DISJUNCTION:
 		case RHYTHMDB_QUERY_PROP_YEAR_EQUALS:
@@ -2194,6 +2275,137 @@ rhythmdb_tree_entry_count_by_type (Rhyth
 }
 
 
+/* this is called with keywords_lock held */
+static gboolean
+remove_entry_from_keyword_table (RBRefString *keyword,
+				 GHashTable *table,
+				 RhythmDBEntry *entry)
+{
+	gboolean present;
+
+	present = g_hash_table_remove (table, entry);
+	/* TODO: remove entry hash tables */
+	return present;
+}
+
+static gboolean
+rhythmdb_tree_entry_keyword_add (RhythmDB *rdb,
+				 RhythmDBEntry *entry,
+				 RBRefString *keyword)
+{
+	RhythmDBTree *db = RHYTHMDB_TREE (rdb);
+	GHashTable *keyword_table;
+	gboolean present;
+
+	g_mutex_lock (db->priv->keywords_lock);
+	keyword_table = g_hash_table_lookup (db->priv->keywords, keyword);
+	if (keyword_table != NULL) {
+		/* it would be nice if _insert told us whether it was replacing a value */
+		present = (gboolean) g_hash_table_lookup (keyword_table, entry);
+		g_hash_table_insert (keyword_table, entry, GINT_TO_POINTER(1));
+	} else {
+		/* new keyword */
+		present = FALSE;
+
+		keyword_table = g_hash_table_new (g_direct_hash, g_direct_equal);
+		g_hash_table_insert (keyword_table, entry, GINT_TO_POINTER(1));
+
+		g_hash_table_insert (db->priv->keywords, rb_refstring_ref (keyword), keyword_table);
+	}
+
+	g_mutex_unlock (db->priv->keywords_lock);
+
+	return present;
+}
+
+static gboolean
+rhythmdb_tree_entry_keyword_remove (RhythmDB *rdb,
+				    RhythmDBEntry *entry,
+				    RBRefString *keyword)
+{
+	RhythmDBTree *db = RHYTHMDB_TREE (rdb);
+	GHashTable *keyword_table;
+	gboolean ret;
+
+	g_mutex_lock (db->priv->keywords_lock);
+	keyword_table = g_hash_table_lookup (db->priv->keywords, keyword);
+	if (keyword_table != NULL) {
+		ret = remove_entry_from_keyword_table (keyword, keyword_table, entry);
+	} else {
+		ret = FALSE;
+	}
+	g_mutex_unlock (db->priv->keywords_lock);
+
+	return ret;
+}
+
+static gboolean
+rhythmdb_tree_entry_keyword_has (RhythmDB *rdb,
+				 RhythmDBEntry *entry,
+				 RBRefString *keyword)
+{
+	RhythmDBTree *db = RHYTHMDB_TREE (rdb);
+	GHashTable *keyword_table;
+	gboolean ret;
+
+	g_mutex_lock (db->priv->keywords_lock);
+	keyword_table = g_hash_table_lookup (db->priv->keywords, keyword);
+	if (keyword_table != NULL) {
+		ret = (gboolean) g_hash_table_lookup (keyword_table, entry);
+	} else {
+		ret = FALSE;
+	}
+	g_mutex_unlock (db->priv->keywords_lock);
+
+	return ret;
+}
+
+/* this is called with keywords_lock held */
+static void
+remove_entry_from_keywords (RhythmDBTree *db,
+			    RhythmDBEntry *entry)
+{
+	g_hash_table_foreach (db->priv->keywords, (GHFunc)remove_entry_from_keyword_table, entry);
+}
+
+struct RhythmDBTreeKeywordsGetData {
+	RhythmDBTree *db;
+	RhythmDBEntry *entry;
+	GList *keywords;
+};
+
+static void
+check_entry_existance (RBRefString *keyword,
+		       GHashTable *keyword_table,
+		       struct RhythmDBTreeKeywordsGetData *data)
+{
+	gboolean present;
+
+	present = (gboolean) g_hash_table_lookup (keyword_table, data->entry);
+	if (present) {
+		data->keywords = g_list_prepend (data->keywords, rb_refstring_ref (keyword));
+	}
+}
+
+static GList*
+rhythmdb_tree_entry_keywords_get (RhythmDB *rdb,
+				  RhythmDBEntry *entry)
+{
+	RhythmDBTree *db = RHYTHMDB_TREE (rdb);
+	struct RhythmDBTreeKeywordsGetData data;
+	
+	data.db = db;
+	data.entry = entry;
+	data.keywords = NULL;
+
+	g_mutex_lock (db->priv->keywords_lock);
+	g_hash_table_foreach (db->priv->keywords, (GHFunc)check_entry_existance, &data);
+	g_mutex_unlock (db->priv->keywords_lock);
+
+	return data.keywords;
+}
+
+
 struct HashTreeIteratorCtxt {
 	RhythmDBTree *db;
 	RBTreeEntryItFunc entry_func;
Index: rhythmdb/rhythmdb.c
===================================================================
--- rhythmdb/rhythmdb.c	(revision 4845)
+++ rhythmdb/rhythmdb.c	(working copy)
@@ -137,6 +137,8 @@ enum
 	ENTRY_ADDED,
 	ENTRY_CHANGED,
 	ENTRY_DELETED,
+	ENTRY_KEYWORD_ADDED,
+	ENTRY_KEYWORD_REMOVED,
 	ENTRY_EXTRA_METADATA_REQUEST,
 	ENTRY_EXTRA_METADATA_NOTIFY,
 	ENTRY_EXTRA_METADATA_GATHER,
@@ -212,6 +214,26 @@ rhythmdb_class_init (RhythmDBClass *klas
 			      G_TYPE_NONE, 2,
 			      RHYTHMDB_TYPE_ENTRY, G_TYPE_POINTER);
 
+	rhythmdb_signals[ENTRY_KEYWORD_ADDED] =
+		g_signal_new ("entry_keyword_added",
+			      RHYTHMDB_TYPE,
+			      G_SIGNAL_RUN_LAST,
+			      G_STRUCT_OFFSET (RhythmDBClass, entry_added),
+			      NULL, NULL,
+			      rb_marshal_VOID__BOXED_BOXED,
+			      G_TYPE_NONE,
+			      2, RHYTHMDB_TYPE_ENTRY, RB_TYPE_REFSTRING);
+
+	rhythmdb_signals[ENTRY_KEYWORD_REMOVED] =
+		g_signal_new ("entry_keyword_removed",
+			      RHYTHMDB_TYPE,
+			      G_SIGNAL_RUN_LAST,
+			      G_STRUCT_OFFSET (RhythmDBClass, entry_deleted),
+			      NULL, NULL,
+			      rb_marshal_VOID__BOXED_BOXED,
+			      G_TYPE_NONE,
+			      2, RHYTHMDB_TYPE_ENTRY, RB_TYPE_REFSTRING);
+
 	rhythmdb_signals[ENTRY_EXTRA_METADATA_REQUEST] =
 		g_signal_new ("entry_extra_metadata_request",
 			      G_OBJECT_CLASS_TYPE (object_class),
@@ -775,17 +797,18 @@ rhythmdb_set_property (GObject *object,
 		       const GValue *value,
 		       GParamSpec *pspec)
 {
-	RhythmDB *source = RHYTHMDB (object);
+	RhythmDB *db = RHYTHMDB (object);
 
 	switch (prop_id) {
 	case PROP_NAME:
-		source->priv->name = g_strdup (g_value_get_string (value));
+		g_free (db->priv->name);
+		db->priv->name = g_value_dup_string (value);
 		break;
 	case PROP_DRY_RUN:
-		source->priv->dry_run = g_value_get_boolean (value);
+		db->priv->dry_run = g_value_get_boolean (value);
 		break;
 	case PROP_NO_UPDATE:
-		source->priv->no_update = g_value_get_boolean (value);
+		db->priv->no_update = g_value_get_boolean (value);
 		break;
 	default:
 		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
@@ -3530,6 +3553,8 @@ rhythmdb_query_type_get_type (void)
 			ENUM_ENTRY (RHYTHMDB_QUERY_PROP_YEAR_EQUALS, "Year equivalence: true if date within year"),
 			ENUM_ENTRY (RHYTHMDB_QUERY_PROP_YEAR_GREATER, "True if date greater than year"),
 			ENUM_ENTRY (RHYTHMDB_QUERY_PROP_YEAR_LESS, "True if date less than year"),
+			ENUM_ENTRY (RHYTHMDB_QUERY_KEYWORD_HAS, "True if the entry has the keyword"),
+			ENUM_ENTRY (RHYTHMDB_QUERY_KEYWORD_HASNT, "True if the entry doesn't have the keyword"),
 			{ 0, 0, 0 }
 		};
 
@@ -4558,6 +4583,94 @@ rhythmdb_entry_get_playback_uri (RhythmD
 		return rhythmdb_entry_dup_string (entry, RHYTHMDB_PROP_LOCATION);
 }
 
+
+/**
+ * rhythmdb_entry_keyword_add:
+ * @entry: a #RhythmDBEntry.
+ * @keyword: the keyword to add.
+ *
+ * Adds a keyword to an entry.
+ *
+ * Returns: whether the keyword was already on the entry
+ **/
+gboolean
+rhythmdb_entry_keyword_add	(RhythmDB *db,
+				 RhythmDBEntry *entry,
+				 RBRefString *keyword)
+{
+	RhythmDBClass *klass = RHYTHMDB_GET_CLASS (db);
+	gboolean ret;
+
+	ret = klass->impl_entry_keyword_add (db, entry, keyword);
+	if (!ret) {
+		g_signal_emit (G_OBJECT (db), rhythmdb_signals[ENTRY_KEYWORD_ADDED], 0, entry, keyword);
+	}
+	return ret;
+}
+
+/**
+ * rhythmdb_entry_keyword_remove:
+ * @entry: a #RhythmDBEntry.
+ * @keyword: the keyword to remove.
+ *
+ * Removed a keyword from an entry.
+ *
+ * Returns: whether the keyword had previously been added to the entry.
+ **/
+gboolean
+rhythmdb_entry_keyword_remove	(RhythmDB *db,
+				 RhythmDBEntry *entry,
+				 RBRefString *keyword)
+{
+	RhythmDBClass *klass = RHYTHMDB_GET_CLASS (db);
+	gboolean ret;
+
+	ret = klass->impl_entry_keyword_remove (db, entry, keyword);
+	if (ret) {
+		g_signal_emit (G_OBJECT (db), rhythmdb_signals[ENTRY_KEYWORD_REMOVED], 0, entry, keyword);
+	}
+	return ret;
+}
+
+/**
+ * rhythmdb_entry_keyword_has:
+ * @entry: a #RhythmDBEntry.
+ * @keyword: the keyword to check for.
+ *
+ * Checks whether a keyword is has been added to an entry.
+ *
+ * Returns: whether the keyword had been added to the entry.
+ **/
+gboolean
+rhythmdb_entry_keyword_has	(RhythmDB *db,
+				 RhythmDBEntry *entry,
+				 RBRefString *keyword)
+{
+	RhythmDBClass *klass = RHYTHMDB_GET_CLASS (db);
+
+	return klass->impl_entry_keyword_has (db, entry, keyword);
+}
+
+/**
+ * rhythmdb_entry_keywords_get:
+ * @entry: a #RhythmDBEntry.
+ *
+ * Gets the list ofkeywords that have been added to an entry.
+ *
+ * Returns: the list of keywords that have been added to the entry.
+ *          The caller is responsible for unref'ing the RBRefStrings and
+ *          freeing the list with g_list_free.
+ **/
+GList* /*<RBRefString>*/
+rhythmdb_entry_keywords_get	(RhythmDB *db,
+				 RhythmDBEntry *entry)
+{
+	RhythmDBClass *klass = RHYTHMDB_GET_CLASS (db);
+
+	return klass->impl_entry_keywords_get (db, entry);
+}
+
+
 GType
 rhythmdb_get_property_type (RhythmDB *db,
 			    guint property_id)
Index: rhythmdb/rhythmdb-query.c
===================================================================
--- rhythmdb/rhythmdb-query.c	(revision 4845)
+++ rhythmdb/rhythmdb-query.c	(working copy)
@@ -123,6 +123,8 @@ rhythmdb_query_parse_valist (RhythmDB *d
 		case RHYTHMDB_QUERY_PROP_YEAR_EQUALS:
 		case RHYTHMDB_QUERY_PROP_YEAR_GREATER:
 		case RHYTHMDB_QUERY_PROP_YEAR_LESS:
+		case RHYTHMDB_QUERY_KEYWORD_HAS:
+		case RHYTHMDB_QUERY_KEYWORD_HASNT:
 			data->propid = va_arg (args, guint);
 			data->val = g_new0 (GValue, 1);
 			g_value_init (data->val, rhythmdb_get_property_type (db, data->propid));
@@ -250,6 +252,8 @@ rhythmdb_query_append_params (RhythmDB *
 	case RHYTHMDB_QUERY_PROP_YEAR_EQUALS:
 	case RHYTHMDB_QUERY_PROP_YEAR_GREATER:
 	case RHYTHMDB_QUERY_PROP_YEAR_LESS:
+	case RHYTHMDB_QUERY_KEYWORD_HAS:
+	case RHYTHMDB_QUERY_KEYWORD_HASNT:
 		data->propid = prop;
 		data->val = g_new0 (GValue, 1);
 		g_value_init (data->val, rhythmdb_get_property_type (db, data->propid));
Index: rhythmdb/rhythmdb.h
===================================================================
--- rhythmdb/rhythmdb.h	(revision 4845)
+++ rhythmdb/rhythmdb.h	(working copy)
@@ -135,6 +135,10 @@ typedef enum
 	RHYTHMDB_QUERY_PROP_YEAR_EQUALS,
 	RHYTHMDB_QUERY_PROP_YEAR_GREATER,
 	RHYTHMDB_QUERY_PROP_YEAR_LESS,
+
+	/* keywords */
+	RHYTHMDB_QUERY_KEYWORD_HAS,
+	RHYTHMDB_QUERY_KEYWORD_HASNT,
 } RhythmDBQueryType;
 
 /* If you modify this enum, don't forget to modify rhythmdb_prop_get_type */
@@ -266,6 +270,8 @@ typedef struct
 	void	(*entry_added)		(RhythmDB *db, RhythmDBEntry *entry);
 	void	(*entry_changed)	(RhythmDB *db, RhythmDBEntry *entry, GSList *changes); /* list of RhythmDBEntryChanges */
 	void	(*entry_deleted)	(RhythmDB *db, RhythmDBEntry *entry);
+	void	(*entry_keyword_added)	(RhythmDB *db, RhythmDBEntry *entry, RBRefString *keyword);
+	void	(*entry_keyword_removed)(RhythmDB *db, RhythmDBEntry *entry, RBRefString *keyword);
 	GValue *(*entry_extra_metadata_request) (RhythmDB *db, RhythmDBEntry *entry);
 	void    (*entry_extra_metadata_gather) (RhythmDB *db, RhythmDBEntry *entry, GHashTable *data);
 	void	(*entry_extra_metadata_notify) (RhythmDB *db, RhythmDBEntry *entry, const char *field, GValue *metadata);
@@ -313,6 +319,18 @@ typedef struct
 	void		(*impl_entry_type_registered) (RhythmDB *db,
 						       const char *name,
 						       RhythmDBEntryType type);
+
+	gboolean	(*impl_entry_keyword_add)	(RhythmDB *db,
+							 RhythmDBEntry *entry,
+							 RBRefString *keyword);
+	gboolean	(*impl_entry_keyword_remove)	(RhythmDB *db,
+							 RhythmDBEntry *entry,
+							 RBRefString *keyword);
+	gboolean	(*impl_entry_keyword_has)	(RhythmDB *db,
+							 RhythmDBEntry *entry,
+							 RBRefString *keyword);
+	GList*		(*impl_entry_keywords_get)	(RhythmDB *db,
+							 RhythmDBEntry *entry);
 } RhythmDBClass;
 
 GType		rhythmdb_get_type	(void);
@@ -374,6 +392,18 @@ void		rhythmdb_entry_foreach_by_type  (R
 gint64		rhythmdb_entry_count_by_type	(RhythmDB *db,
 						 RhythmDBEntryType entry_type);
 
+gboolean	rhythmdb_entry_keyword_add	(RhythmDB *db,
+						 RhythmDBEntry *entry,
+						 RBRefString *keyword);
+gboolean	rhythmdb_entry_keyword_remove	(RhythmDB *db,
+						 RhythmDBEntry *entry,
+						 RBRefString *keyword);
+gboolean	rhythmdb_entry_keyword_has	(RhythmDB *db,
+						 RhythmDBEntry *entry,
+						 RBRefString *keyword);
+GList* /*<RBRefString>*/ rhythmdb_entry_keywords_get	(RhythmDB *db,
+							 RhythmDBEntry *entry);
+
 /**
  * Returns a freshly allocated GtkTreeModel which represents the query.
  * The extended arguments alternate between RhythmDBQueryType args
Index: rhythmdb/Makefile.am
===================================================================
--- rhythmdb/Makefile.am	(revision 4845)
+++ rhythmdb/Makefile.am	(working copy)
@@ -23,6 +23,8 @@ librhythmdb_la_SOURCES =				\
 	rhythmdb.h					\
 	rhythmdb-private.h				\
 	rhythmdb.c					\
+	rhythmdb-keyword-model.h			\
+	rhythmdb-keyword-model.c			\
 	rhythmdb-monitor.c				\
 	rhythmdb-query.c				\
 	rhythmdb-property-model.h			\
Index: bindings/python/rhythmdb.override
===================================================================
--- bindings/python/rhythmdb.override	(revision 4845)
+++ bindings/python/rhythmdb.override	(working copy)
@@ -9,6 +9,7 @@ headers
 
 #include "rhythmdb.h"
 #include "rhythmdb-property-model.h"
+#include "rhythmdb-keyword-model.h"
 #include "rhythmdb-query-model.h"
 #include "rhythmdb-query-results.h"
 
Index: bindings/python/rb.override
===================================================================
--- bindings/python/rb.override	(revision 4845)
+++ bindings/python/rb.override	(working copy)
@@ -16,6 +16,7 @@ headers
 #include "rb-playlist-source.h"
 #include "rb-plugin.h"
 #include "rb-property-view.h"
+#include "rb-keyword-view.h"
 #include "rb-removable-media-manager.h"
 #include "rb-removable-media-source.h"
 #include "rb-shell.h"
@@ -51,6 +52,8 @@ import rhythmdb.Entry as PyRhythmDBEntry
 import rhythmdb.EntryType as PyRhythmDBEntryType_Type
 import rhythmdb.PropertyModel as PyRhythmDBPropertyModel_Type
 import rhythmdb.PropertyModelColumn as PyRhythmDBPropertyModelColumn_Type
+import rhythmdb.KeywordModel as PyRhythmDBKeywordModel_Type
+import rhythmdb.KeywordModelColumn as PyRhythmDBKeywordModelColumn_Type
 import rhythmdb.PropType as PyRhythmDBPropType_Type
 import rhythmdb.Query as PyRhythmDBQuery_Type
 import rhythmdb.QueryModel as PyRhythmDBQueryModel_Type
Index: bindings/python/rhythmdb.defs
===================================================================
--- bindings/python/rhythmdb.defs	(revision 4845)
+++ bindings/python/rhythmdb.defs	(working copy)
@@ -47,6 +47,14 @@
   (gtype-id "RHYTHMDB_TYPE_PROPERTY_MODEL")
 )
 
+(define-object KeywordModel
+  (in-module "RhythmDB")
+  (parent "GObject")
+  (interface "GtkTreeModel")
+  (c-name "RhythmDBKeywordModel")
+  (gtype-id "RHYTHMDB_TYPE_KEYWORD_MODEL")
+)
+
 (define-object QueryModel
   (in-module "RhythmDB")
   (parent "GObject")
@@ -79,6 +87,8 @@
     '("prop-year-equals" "RHYTHMDB_QUERY_PROP_YEAR_EQUALS")
     '("prop-year-greater" "RHYTHMDB_QUERY_PROP_YEAR_GREATER")
     '("prop-year-less" "RHYTHMDB_QUERY_PROP_YEAR_LESS")
+    '("keyword-has" "RHYTHMDB_QUERY_KEYWORD_HAS")
+    '("keyword-hasnt" "RHYTHMDB_QUERY_KEYWORD_HASNT")
   )
 )
 
@@ -151,6 +161,18 @@
   )
 )
 
+(define-enum KeywordModelColumn
+  (in-module "RhythmDB")
+  (c-name "RhythmDBKeywordModelColumn")
+  (gtype-id "RHYTHMDB_TYPE_KEYWORD_MODEL_COLUMN")
+  (values
+    '("title" "RHYTHMDB_KEYWORD_MODEL_COLUMN_TITLE")
+    '("priority" "RHYTHMDB_KEYWORD_MODEL_COLUMN_PRIORITY")
+    '("number" "RHYTHMDB_KEYWORD_MODEL_COLUMN_NUMBER")
+    '("last" "RHYTHMDB_KEYWORD_MODEL_COLUMN_LAST")
+  )
+)
+
 
 ;; From ../../rhythmdb/rhythmdb-query-results.h
 
@@ -437,6 +459,71 @@
   )
 )
 
+(define-method entry_count
+  (of-object "RhythmDB")
+  (c-name "rhythmdb_entry_count")
+  (return-type "gint64")
+)
+
+(define-method entry_foreach_by_type
+  (of-object "RhythmDB")
+  (c-name "rhythmdb_entry_foreach_by_type")
+  (return-type "none")
+  (parameters
+    '("RhythmDBEntryType" "entry_type")
+    '("GFunc" "func")
+    '("gpointer" "data")
+  )
+)
+
+(define-method entry_count_by_type
+  (of-object "RhythmDB")
+  (c-name "rhythmdb_entry_count_by_type")
+  (return-type "gint64")
+  (parameters
+    '("RhythmDBEntryType" "entry_type")
+  )
+)
+
+(define-method entry_keyword_add
+  (of-object "RhythmDB")
+  (c-name "rhythmdb_entry_keyword_add")
+  (return-type "gboolean")
+  (parameters
+    '("RhythmDBEntry*" "entry")
+    '("RBRefString*" "keyword")
+  )
+)
+
+(define-method entry_keyword_remove
+  (of-object "RhythmDB")
+  (c-name "rhythmdb_entry_keyword_remove")
+  (return-type "gboolean")
+  (parameters
+    '("RhythmDBEntry*" "entry")
+    '("RBRefString*" "keyword")
+  )
+)
+
+(define-method entry_keyword_has
+  (of-object "RhythmDB")
+  (c-name "rhythmdb_entry_keyword_has")
+  (return-type "gboolean")
+  (parameters
+    '("RhythmDBEntry*" "entry")
+    '("RBRefString*" "keyword")
+  )
+)
+
+(define-method entry_keywords_get
+  (of-object "RhythmDB")
+  (c-name "rhythmdb_entry_keywords_get")
+  (return-type "GList*")
+  (parameters
+    '("RhythmDBEntry*" "entry")
+  )
+)
+
 (define-method entry_foreach
   (of-object "RhythmDB")
   (c-name "rhythmdb_entry_foreach")
@@ -945,3 +1032,40 @@
     '("RhythmDBQueryModel*" "model")
   )
 )
+
+;; From rhythmdb-keyword-model.h
+
+(define-function rhythmdb_keyword_model_column_get_type
+  (c-name "rhythmdb_keyword_model_column_get_type")
+  (return-type "GType")
+)
+
+(define-function rhythmdb_keyword_model_get_type
+  (c-name "rhythmdb_keyword_model_get_type")
+  (return-type "GType")
+)
+
+(define-method keyword_model_new
+  (of-object "RhythmDB")
+  (c-name "rhythmdb_keyword_model_new")
+  (return-type "RhythmDBKeywordModel*")
+)
+
+(define-method iter_from_string
+  (of-object "RhythmDBKeywordModel")
+  (c-name "rhythmdb_keyword_model_iter_from_string")
+  (return-type "gboolean")
+  (parameters
+    '("const-char*" "name")
+    '("GtkTreeIter*" "iter")
+  )
+)
+
+(define-method enable_drag
+  (of-object "RhythmDBKeywordModel")
+  (c-name "rhythmdb_keyword_model_enable_drag")
+  (return-type "none")
+  (parameters
+    '("GtkTreeView*" "view")
+  )
+)
Index: bindings/python/rb.defs
===================================================================
--- bindings/python/rb.defs	(revision 4845)
+++ bindings/python/rb.defs	(working copy)
@@ -92,6 +92,13 @@
   (gtype-id "RB_TYPE_PROPERTY_VIEW")
 )
 
+(define-object KeywordView
+  (in-module "RB")
+  (parent "GtkScrolledWindow")
+  (c-name "RBKeywordView")
+  (gtype-id "RB_TYPE_KEYWORD_VIEW")
+)
+
 (define-object LibraryBrowser
   (in-module "RB")
   (parent "GtkHBox")
@@ -1404,10 +1411,10 @@
   (c-name "rb_property_view_new")
   (is-constructor-of "RBPropertyView")
   (return-type "RBPropertyView*")
-  (parameters
-    '("RhythmDB*" "db")
-    '("guint" "propid")
-    '("const-char*" "title")
+  (properties
+    '("db" (argname "db"))
+    '("propid" (argment "propid"))
+    '("title" (argname "title"))
   )
 )
 
@@ -1990,3 +1997,87 @@
 )
 
 
+;; From rb-keyword-view.h
+
+(define-function rb_keyword_view_get_type
+  (c-name "rb_keyword_view_get_type")
+  (return-type "GType")
+)
+
+(define-function keyword_view_new
+  (in-module "rb")
+  (c-name "rb_keyword_view_new")
+  (is-constructor-of "RbKeywtitle")
+  (return-type "RBKeywordView*")
+  (properties
+    '("db" (argname "db"))
+    '("title" (argname "title"))
+  )
+)
+
+(define-method append_column_custom
+  (of-object "RBKeywordView")
+  (c-name "rb_keyword_view_append_column_custom")
+  (return-type "none")
+  (parameters
+    '("GtkTreeViewColumn*" "column")
+  )
+)
+
+(define-method set_selection_mode
+  (of-object "RBKeywordView")
+  (c-name "rb_keyword_view_set_selection_mode")
+  (return-type "none")
+  (parameters
+    '("GtkSelectionMode" "mode")
+  )
+)
+
+(define-method reset
+  (of-object "RBKeywordView")
+  (c-name "rb_keyword_view_reset")
+  (return-type "none")
+)
+
+(define-method set_selection
+  (of-object "RBKeywordView")
+  (c-name "rb_keyword_view_set_selection")
+  (return-type "none")
+  (parameters
+    '("const-GList*" "names")
+  )
+)
+
+(define-method get_model
+  (of-object "RBKeywordView")
+  (c-name "rb_keyword_view_get_model")
+  (return-type "RhythmDBKeywordModel*")
+)
+
+(define-method set_model
+  (of-object "RBKeywordView")
+  (c-name "rb_keyword_view_set_model")
+  (return-type "none")
+  (parameters
+    '("RhythmDBKeywordModel*" "model")
+  )
+)
+
+(define-method get_num_keywords
+  (of-object "RBKeywordView")
+  (c-name "rb_keyword_view_get_num_keywords")
+  (return-type "guint")
+)
+
+(define-method set_search_func
+  (of-object "RBKeywordView")
+  (c-name "rb_keyword_view_set_search_func")
+  (return-type "none")
+  (parameters
+    '("GtkTreeViewSearchEqualFunc" "func")
+    '("gpointer" "func_data")
+    '("GtkDestroyNotify" "notify")
+  )
+)
+
+
Index: bindings/python/Makefile.am
===================================================================
--- bindings/python/Makefile.am	(revision 4845)
+++ bindings/python/Makefile.am	(working copy)
@@ -49,6 +49,7 @@ BINDING_HEADERS_SRCDIR_IN = \
 	plugins/rb-plugin.h			\
 	rhythmdb/rhythmdb.h			\
 	rhythmdb/rhythmdb-property-model.h	\
+	rhythmdb/rhythmdb-keyword-model.h	\
 	rhythmdb/rhythmdb-query-model.h		\
 	rhythmdb/rhythmdb-query-results.h	\
 	shell/rb-shell.h			\
@@ -65,6 +66,7 @@ BINDING_HEADERS_SRCDIR_IN = \
 	widgets/rb-entry-view.h			\
 	widgets/rb-library-browser.h		\
 	widgets/rb-property-view.h		\
+	widgets/rb-keyword-view.h		\
 	widgets/rb-song-info.h			\
 	$(NULL)
 
Index: tests/test-rhythmdb.c
===================================================================
--- tests/test-rhythmdb.c	(revision 4845)
+++ tests/test-rhythmdb.c	(working copy)
@@ -204,6 +204,100 @@ START_TEST (test_rhythmdb_mirroring)
 }
 END_TEST
 
+static int
+count_and_free_refstring_list (GList *list)
+{
+	int count;
+
+	count = g_list_length (list);
+
+	g_list_foreach (list, (GFunc)rb_refstring_unref, NULL);
+	g_list_free (list);
+
+	return count;
+}
+
+START_TEST (test_rhythmdb_keywords)
+{
+	RhythmDBEntry *entry;
+	GList *list;
+	gboolean ret;
+	RBRefString *keyword_foo, *keyword_bar, *keyword_baz;
+
+	keyword_foo = rb_refstring_new ("foo");
+	keyword_bar = rb_refstring_new ("bar");
+	keyword_baz = rb_refstring_new ("baz");
+
+	entry = rhythmdb_entry_new (db, RHYTHMDB_ENTRY_TYPE_SONG, "file:///foo.mp3");
+	fail_unless (entry != NULL, "failed to create entry");
+
+	/* new entries should have 0 keywords */
+	list = rhythmdb_entry_keywords_get (db, entry);
+	fail_unless (count_and_free_refstring_list (list) == 0, "new entry had keywords");
+
+	/* adding one keyword */
+	ret = rhythmdb_entry_keyword_add (db, entry, keyword_foo);
+	fail_unless (ret == FALSE, "entry incorrectly reported as having keyword already");
+	list = rhythmdb_entry_keywords_get (db, entry);
+	fail_unless (count_and_free_refstring_list (list) == 1, "entry wrong number of keywords after one was added");
+
+	/* has added keyword */
+	ret = rhythmdb_entry_keyword_has (db, entry, keyword_foo);
+	fail_unless (ret == TRUE, "reported not having just-added keyword");
+
+	/* add keyword again */
+	ret = rhythmdb_entry_keyword_add (db, entry, keyword_foo);
+	fail_unless (ret == TRUE, "entry incorrectly reported as not keyword already");
+
+	/* check keyword count*/
+	list = rhythmdb_entry_keywords_get (db, entry);
+	fail_unless (count_and_free_refstring_list (list) == 1, "entry wrong number of keywords after one was re-added");
+
+	/* ensure it has only that keyword */
+	ret = rhythmdb_entry_keyword_has (db, entry, keyword_bar);
+	fail_unless (ret == FALSE, "reported having wrong keyword");
+
+	/* remove the keyword */
+	ret = rhythmdb_entry_keyword_remove (db, entry, keyword_foo);
+	fail_unless (ret == TRUE, "reported having not previously having keyword");
+
+	/* has removed keyword */
+	ret = rhythmdb_entry_keyword_has (db, entry, keyword_foo);
+	fail_unless (ret == FALSE, "reported having just-removed keyword");
+
+	/* check count is back to zero */
+	list = rhythmdb_entry_keywords_get (db, entry);
+	fail_unless (count_and_free_refstring_list (list) == 0, "entry has keywords after they were removed");
+
+	/* try removing keyword again */
+	ret = rhythmdb_entry_keyword_remove (db, entry, keyword_foo);
+	fail_unless (ret == FALSE, "reported previously having already removed keyword");
+
+	/* add and remove several keywords */
+	ret = rhythmdb_entry_keyword_add (db, entry, keyword_foo);
+	fail_unless (ret == FALSE, "reported previously having already removed keyword");
+	ret = rhythmdb_entry_keyword_add (db, entry, keyword_bar);
+	fail_unless (ret == FALSE, "reported previously having already never-added keyword");
+	ret = rhythmdb_entry_keyword_add (db, entry, keyword_baz);
+	fail_unless (ret == FALSE, "reported previously having already never-added keyword");
+
+	list = rhythmdb_entry_keywords_get (db, entry);
+	fail_unless (count_and_free_refstring_list (list) == 3, "entry wrong number of keywords after several were added");
+	ret = rhythmdb_entry_keyword_remove (db, entry, keyword_foo);
+	fail_unless (ret == TRUE, "reported previously not having added keyword");
+	list = rhythmdb_entry_keywords_get (db, entry);
+	fail_unless (count_and_free_refstring_list (list) == 2, "entry wrong number of keywords after several were added");
+	ret = rhythmdb_entry_keyword_remove (db, entry, keyword_bar);
+	fail_unless (ret == TRUE, "reported previously not having added keyword");
+	list = rhythmdb_entry_keywords_get (db, entry);
+	fail_unless (count_and_free_refstring_list (list) == 1, "entry wrong number of keywords after several were added");
+	ret = rhythmdb_entry_keyword_remove (db, entry, keyword_baz);
+	fail_unless (ret == TRUE, "reported previously not having added keyword");
+	list = rhythmdb_entry_keywords_get (db, entry);
+	fail_unless (count_and_free_refstring_list (list) == 0, "entry wrong number of keywords after several were added");
+}
+END_TEST
+
 START_TEST (test_rhythmdb_deserialisation1)
 {
 	RhythmDBQueryModel *model;
@@ -299,8 +393,10 @@ rhythmdb_suite (void)
 	tcase_add_test (tc_chain, test_rhythmdb_indexing);
 	tcase_add_test (tc_chain, test_rhythmdb_multiple);
 	tcase_add_test (tc_chain, test_rhythmdb_mirroring);
+	tcase_add_test (tc_chain, test_rhythmdb_keywords);
 	/*tcase_add_test (tc_chain, test_rhythmdb_signals);*/
 	/*tcase_add_test (tc_chain, test_rhythmdb_query);*/
+	/* FIXME: add some keywords to the deserialisation tests */
 	tcase_add_test (tc_chain, test_rhythmdb_deserialisation1);
 	tcase_add_test (tc_chain, test_rhythmdb_deserialisation2);
 	tcase_add_test (tc_chain, test_rhythmdb_deserialisation3);
Index: widgets/rb-keyword-view.h
===================================================================
--- widgets/rb-keyword-view.h	(revision 0)
+++ widgets/rb-keyword-view.h	(revision 0)
@@ -0,0 +1,89 @@
+/*
+ *  Copyright (C) 2002 Jorn Baayen <jorn@nl.linux.org>
+ *  Copyright (C) 2003 Colin Walters <walters@verbum.org>
+ *
+ *  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.
+ *
+ */
+
+#ifndef __RB_KEYWORD_VIEW_H
+#define __RB_KEYWORD_VIEW_H
+
+#include <gtk/gtkscrolledwindow.h>
+#include <gtk/gtkdnd.h>
+
+#include "rhythmdb.h"
+#include "rhythmdb-keyword-model.h"
+#include "rb-entry-view.h"
+
+G_BEGIN_DECLS
+
+#define RB_TYPE_KEYWORD_VIEW         (rb_keyword_view_get_type ())
+#define RB_KEYWORD_VIEW(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), RB_TYPE_KEYWORD_VIEW, RBKeywordView))
+#define RB_KEYWORD_VIEW_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST((k), RB_TYPE_KEYWORD_VIEW, RBKeywordViewClass))
+#define RB_IS_KEYWORD_VIEW(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), RB_TYPE_KEYWORD_VIEW))
+#define RB_IS_KEYWORD_VIEW_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), RB_TYPE_KEYWORD_VIEW))
+#define RB_KEYWORD_VIEW_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), RB_TYPE_KEYWORD_VIEW, RBKeywordViewClass))
+
+typedef struct RBKeywordViewPrivate RBKeywordViewPrivate;
+
+typedef struct
+{
+	GtkScrolledWindow parent;
+
+	RBKeywordViewPrivate *priv;
+} RBKeywordView;
+
+typedef struct
+{
+	GtkScrolledWindowClass parent;
+
+	void (*keyword_selected)	(RBKeywordView *view, const char *name);
+	void (*properties_selected)	(RBKeywordView *view, GList *properties);
+	void (*keyword_activated)	(RBKeywordView *view, const char *name);
+	void (*selection_reset)		(RBKeywordView *view);
+	void (*show_popup)		(RBKeywordView *view);
+} RBKeywordViewClass;
+
+GType		rb_keyword_view_get_type		(void);
+
+RBKeywordView *rb_keyword_view_new			(RhythmDB *db,
+							 const char *title);
+void		rb_keyword_view_append_column_custom	(RBKeywordView *view,
+							GtkTreeViewColumn *column);
+
+void		rb_keyword_view_set_selection_mode	(RBKeywordView *view,
+							 GtkSelectionMode mode);
+
+void		rb_keyword_view_reset			(RBKeywordView *view);
+
+void		rb_keyword_view_set_selection		(RBKeywordView *view,
+							 const GList *names);
+
+RhythmDBKeywordModel * rb_keyword_view_get_model	(RBKeywordView *view);
+
+void		rb_keyword_view_set_model		(RBKeywordView *view,
+							 RhythmDBKeywordModel *model);
+
+guint		rb_keyword_view_get_num_keywords	(RBKeywordView *view);
+
+void		rb_keyword_view_set_search_func	(RBKeywordView *view,
+						 GtkTreeViewSearchEqualFunc func,
+						 gpointer func_data,
+						 GtkDestroyNotify notify);
+
+G_END_DECLS
+
+#endif /* __RB_KEYWORD_VIEW_H */
Index: widgets/rb-library-browser.c
===================================================================
--- widgets/rb-library-browser.c	(revision 4845)
+++ widgets/rb-library-browser.c	(working copy)
@@ -35,6 +35,7 @@
 #include "rhythmdb-property-model.h"
 #include "rhythmdb-query-model.h"
 #include "rb-property-view.h"
+#include "rb-keyword-view.h"
 #include "rb-debug.h"
 #include "rb-util.h"
 
@@ -79,6 +80,8 @@ typedef struct
 
 	GHashTable *property_views;
 	GHashTable *selections;
+
+	RBKeywordView *keyword_view;
 } RBLibraryBrowserPrivate;
 
 enum
@@ -171,6 +174,12 @@ rb_library_browser_constructor (GType ty
 			constructor (type, n_construct_properties, construct_properties));
 	priv = RB_LIBRARY_BROWSER_GET_PRIVATE (browser);
 
+	priv->keyword_view = rb_keyword_view_new (priv->db, _("Keywords"));
+	rb_keyword_view_set_selection_mode (priv->keyword_view, GTK_SELECTION_MULTIPLE);
+	gtk_widget_show_all (GTK_WIDGET (priv->keyword_view));
+	gtk_widget_set_no_show_all (GTK_WIDGET (priv->keyword_view), TRUE);
+	gtk_box_pack_start_defaults (GTK_BOX (browser), GTK_WIDGET (priv->keyword_view));
+
 	for (i = 0; i < num_browser_properties; i++) {
 		RBPropertyView *view;
 
@@ -652,6 +661,7 @@ rb_library_browser_set_model (RBLibraryB
 	RBLibraryBrowserPrivate *priv = RB_LIBRARY_BROWSER_GET_PRIVATE (widget);
 	RBPropertyView *view;
 	RhythmDBPropertyModel *prop_model;
+	RhythmDBKeywordModel *keyword_model;
 
 	if (priv->input_model != NULL) {
 		g_object_unref (priv->input_model);
@@ -663,6 +673,10 @@ rb_library_browser_set_model (RBLibraryB
 		g_object_ref (priv->input_model);
 	}
 
+	/* FIXME: connect this up correctly */
+	keyword_model = rb_keyword_view_get_model (priv->keyword_view);
+	g_object_set (keyword_model, "query-model", priv->input_model, NULL);
+
 	view = g_hash_table_lookup (priv->property_views, (gpointer)browser_properties[0].type);
 	ignore_selection_changes (widget, view, TRUE);
 
Index: widgets/Makefile.am
===================================================================
--- widgets/Makefile.am	(revision 4845)
+++ widgets/Makefile.am	(working copy)
@@ -12,6 +12,8 @@ librbwidgets_la_SOURCES =				\
 	rb-entry-view.h					\
 	rb-property-view.c				\
 	rb-property-view.h				\
+	rb-keyword-view.c				\
+	rb-keyword-view.h				\
 	rb-search-entry.c				\
 	rb-search-entry.h				\
 	rb-header.c					\
Index: widgets/rb-keyword-view.c
===================================================================
--- widgets/rb-keyword-view.c	(revision 0)
+++ widgets/rb-keyword-view.c	(revision 0)
@@ -0,0 +1,740 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ *  Copyright (C) 2003 Colin Walters <walters@verbum.org>
+ *  Copyright (C) 2007 James Livingston <doclivingston@gmail.com>
+ *
+ *  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.
+ *
+ */
+
+#include "config.h"
+
+#include <string.h>
+#include <stdlib.h>
+
+#include <glib/gi18n.h>
+#include <gdk/gdkkeysyms.h>
+#include <gtk/gtk.h>
+#include <libgnomevfs/gnome-vfs-utils.h>
+
+#include "rb-keyword-view.h"
+#include "rb-dialog.h"
+#include "rb-debug.h"
+#include "rhythmdb.h"
+#include "rhythmdb-keyword-model.h"
+#include "rb-stock-icons.h"
+#include "eel-gconf-extensions.h"
+#include "rb-util.h"
+
+static void rb_keyword_view_class_init (RBKeywordViewClass *klass);
+static void rb_keyword_view_init (RBKeywordView *view);
+static void rb_keyword_view_dispose (GObject *object);
+static void rb_keyword_view_finalize (GObject *object);
+static void rb_keyword_view_set_property (GObject *object,
+					   guint prop_id,
+					   const GValue *value,
+					   GParamSpec *pspec);
+static void rb_keyword_view_get_property (GObject *object,
+					   guint prop_id,
+					   GValue *value,
+					   GParamSpec *pspec);
+static GObject * rb_keyword_view_constructor (GType type, guint n_construct_properties,
+					       GObjectConstructParam *construct_properties);
+static void rb_keyword_view_row_activated_cb (GtkTreeView *treeview,
+					       GtkTreePath *path,
+					       GtkTreeViewColumn *column,
+					       RBKeywordView *view);
+static void rb_keyword_view_selection_changed_cb (GtkTreeSelection *selection,
+						   RBKeywordView *view);
+static void rb_keyword_view_pre_row_deleted_cb (RhythmDBKeywordModel *model,
+						 RBKeywordView *view);
+static void rb_keyword_view_post_row_deleted_cb (GtkTreeModel *model,
+						  GtkTreePath *path,
+						  RBKeywordView *view);
+static gboolean rb_keyword_view_popup_menu_cb (GtkTreeView *treeview,
+						RBKeywordView *view);
+static gboolean rb_keyword_view_button_press_cb (GtkTreeView *tree,
+						  GdkEventButton *event,
+						  RBKeywordView *view);
+
+struct RBKeywordViewPrivate
+{
+	RhythmDB *db;
+
+	RhythmDBKeywordModel *prop_model;
+
+	char *title;
+
+	GtkWidget *treeview;
+	GtkTreeSelection *selection;
+
+	gboolean draggable;
+	gboolean handling_row_deletion;
+};
+
+#define RB_KEYWORD_VIEW_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), RB_TYPE_KEYWORD_VIEW, RBKeywordViewPrivate))
+
+enum
+{
+	KEYWORD_SELECTED,
+	KEYWORDS_SELECTED,
+	KEYWORD_ACTIVATED,
+	SELECTION_RESET,
+	SHOW_POPUP,
+	LAST_SIGNAL
+};
+
+enum
+{
+	PROP_0,
+	PROP_DB,
+	PROP_TITLE,
+	PROP_MODEL,
+	PROP_DRAGGABLE,
+};
+
+static guint rb_keyword_view_signals[LAST_SIGNAL] = { 0 };
+
+G_DEFINE_TYPE (RBKeywordView, rb_keyword_view, GTK_TYPE_SCROLLED_WINDOW)
+
+static void
+rb_keyword_view_class_init (RBKeywordViewClass *klass)
+{
+	GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+	object_class->dispose = rb_keyword_view_dispose;
+	object_class->finalize = rb_keyword_view_finalize;
+	object_class->constructor = rb_keyword_view_constructor;
+
+	object_class->set_property = rb_keyword_view_set_property;
+	object_class->get_property = rb_keyword_view_get_property;
+
+	g_object_class_install_property (object_class,
+					 PROP_DB,
+					 g_param_spec_object ("db",
+							      "RhythmDB",
+							      "RhythmDB database",
+							      RHYTHMDB_TYPE,
+							      G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
+
+	g_object_class_install_property (object_class,
+					 PROP_TITLE,
+					 g_param_spec_string ("title",
+							      "title",
+							      "title",
+							      "",
+							      G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
+	g_object_class_install_property (object_class,
+					 PROP_MODEL,
+					 g_param_spec_object ("keyword-model",
+							      "keyword model",
+							      "RhythmDBKeywordModel",
+							      RHYTHMDB_TYPE_KEYWORD_MODEL,
+							      G_PARAM_READWRITE));
+	g_object_class_install_property (object_class,
+					 PROP_DRAGGABLE,
+					 g_param_spec_boolean ("draggable",
+						 	       "draggable",
+							       "is a drag source",
+							       TRUE,
+							       G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
+
+	rb_keyword_view_signals[KEYWORD_ACTIVATED] =
+		g_signal_new ("keyword-activated",
+			      G_OBJECT_CLASS_TYPE (object_class),
+			      G_SIGNAL_RUN_LAST,
+			      G_STRUCT_OFFSET (RBKeywordViewClass, keyword_activated),
+			      NULL, NULL,
+			      g_cclosure_marshal_VOID__STRING,
+			      G_TYPE_NONE,
+			      1,
+			      G_TYPE_STRING);
+
+	rb_keyword_view_signals[KEYWORD_SELECTED] =
+		g_signal_new ("keyword-selected",
+			      G_OBJECT_CLASS_TYPE (object_class),
+			      G_SIGNAL_RUN_LAST,
+			      G_STRUCT_OFFSET (RBKeywordViewClass, keyword_selected),
+			      NULL, NULL,
+			      g_cclosure_marshal_VOID__STRING,
+			      G_TYPE_NONE,
+			      1,
+			      G_TYPE_STRING);
+
+	rb_keyword_view_signals[KEYWORDS_SELECTED] =
+		g_signal_new ("properties-selected",
+			      G_OBJECT_CLASS_TYPE (object_class),
+			      G_SIGNAL_RUN_LAST,
+			      G_STRUCT_OFFSET (RBKeywordViewClass, properties_selected),
+			      NULL, NULL,
+			      g_cclosure_marshal_VOID__POINTER,
+			      G_TYPE_NONE,
+			      1,
+			      G_TYPE_POINTER);
+
+	rb_keyword_view_signals[SELECTION_RESET] =
+		g_signal_new ("keyword-selection-reset",
+			      G_OBJECT_CLASS_TYPE (object_class),
+			      G_SIGNAL_RUN_LAST,
+			      G_STRUCT_OFFSET (RBKeywordViewClass, selection_reset),
+			      NULL, NULL,
+			      g_cclosure_marshal_VOID__VOID,
+			      G_TYPE_NONE,
+			      0);
+	rb_keyword_view_signals[SHOW_POPUP] =
+		g_signal_new ("show_popup",
+			      G_OBJECT_CLASS_TYPE (object_class),
+			      G_SIGNAL_RUN_LAST,
+			      G_STRUCT_OFFSET (RBKeywordViewClass, show_popup),
+			      NULL, NULL,
+			      g_cclosure_marshal_VOID__VOID,
+			      G_TYPE_NONE,
+			      0);
+
+	g_type_class_add_private (klass, sizeof (RBKeywordViewPrivate));
+}
+
+static void
+rb_keyword_view_init (RBKeywordView *view)
+{
+	view->priv = RB_KEYWORD_VIEW_GET_PRIVATE (view);
+}
+
+static void
+rb_keyword_view_set_model_internal (RBKeywordView *view,
+				     RhythmDBKeywordModel *model)
+{
+	if (view->priv->prop_model != NULL) {
+		g_signal_handlers_disconnect_by_func (view->priv->prop_model,
+						      G_CALLBACK (rb_keyword_view_pre_row_deleted_cb),
+						      view);
+		g_signal_handlers_disconnect_by_func (view->priv->prop_model,
+						      G_CALLBACK (rb_keyword_view_post_row_deleted_cb),
+						      view);
+		g_object_unref (view->priv->prop_model);
+	}
+
+	view->priv->prop_model = model;
+
+	if (view->priv->prop_model != NULL) {
+		GtkTreeIter iter;
+
+		g_object_ref (view->priv->prop_model);
+
+		gtk_tree_view_set_model (GTK_TREE_VIEW (view->priv->treeview),
+					 GTK_TREE_MODEL (view->priv->prop_model));
+
+		g_signal_connect_object (view->priv->prop_model,
+					 "pre-row-deletion",
+					 G_CALLBACK (rb_keyword_view_pre_row_deleted_cb),
+					 view,
+					 0);
+		g_signal_connect_object (view->priv->prop_model,
+					 "row_deleted",
+					 G_CALLBACK (rb_keyword_view_post_row_deleted_cb),
+					 view,
+					 G_CONNECT_AFTER);
+
+		g_signal_handlers_block_by_func (view->priv->selection,
+						 G_CALLBACK (rb_keyword_view_selection_changed_cb),
+						 view);
+
+		gtk_tree_selection_unselect_all (view->priv->selection);
+
+		if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (view->priv->prop_model), &iter))
+			gtk_tree_selection_select_iter (view->priv->selection, &iter);
+
+		g_signal_handlers_unblock_by_func (view->priv->selection,
+						   G_CALLBACK (rb_keyword_view_selection_changed_cb),
+						   view);
+	}
+}
+
+static void
+rb_keyword_view_dispose (GObject *object)
+{
+	RBKeywordView *view;
+
+	g_return_if_fail (object != NULL);
+	g_return_if_fail (RB_IS_KEYWORD_VIEW (object));
+
+	view = RB_KEYWORD_VIEW (object);
+
+	rb_keyword_view_set_model_internal (view, NULL);
+
+	G_OBJECT_CLASS (rb_keyword_view_parent_class)->dispose (object);
+}
+
+static void
+rb_keyword_view_finalize (GObject *object)
+{
+	RBKeywordView *view;
+
+	g_return_if_fail (object != NULL);
+	g_return_if_fail (RB_IS_KEYWORD_VIEW (object));
+
+	view = RB_KEYWORD_VIEW (object);
+
+	g_free (view->priv->title);
+
+	G_OBJECT_CLASS (rb_keyword_view_parent_class)->finalize (object);
+}
+
+static void
+rb_keyword_view_set_property (GObject *object,
+			       guint prop_id,
+			       const GValue *value,
+			       GParamSpec *pspec)
+{
+	RBKeywordView *view = RB_KEYWORD_VIEW (object);
+
+	switch (prop_id) {
+	case PROP_DB:
+		view->priv->db = g_value_get_object (value);
+		break;
+	case PROP_TITLE:
+		view->priv->title = g_value_dup_string (value);
+		break;
+	case PROP_MODEL:
+		rb_keyword_view_set_model_internal (view, g_value_get_object (value));
+		break;
+	case PROP_DRAGGABLE:
+		view->priv->draggable = g_value_get_boolean (value);
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+		break;
+	}
+}
+
+static void
+rb_keyword_view_get_property (GObject *object,
+			       guint prop_id,
+			       GValue *value,
+			       GParamSpec *pspec)
+{
+	RBKeywordView *view = RB_KEYWORD_VIEW (object);
+
+	switch (prop_id) {
+	case PROP_DB:
+		g_value_set_object (value, view->priv->db);
+		break;
+	case PROP_TITLE:
+		g_value_set_string (value, view->priv->title);
+		break;
+	case PROP_MODEL:
+		g_value_set_object (value, view->priv->prop_model);
+		break;
+	case PROP_DRAGGABLE:
+		g_value_set_boolean (value, view->priv->draggable);
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+		break;
+	}
+}
+
+RBKeywordView *
+rb_keyword_view_new (RhythmDB *db,
+		      const char *title)
+{
+	RBKeywordView *view;
+
+	view = RB_KEYWORD_VIEW (g_object_new (RB_TYPE_KEYWORD_VIEW,
+					       "hadjustment", NULL,
+					       "vadjustment", NULL,
+					       "hscrollbar_policy", GTK_POLICY_AUTOMATIC,
+					       "vscrollbar_policy", GTK_POLICY_ALWAYS,
+					       "shadow_type", GTK_SHADOW_IN,
+					       "db", db,
+					       "title", title,
+					       "draggable", TRUE,
+					       NULL));
+
+	return view;
+}
+
+void
+rb_keyword_view_set_selection_mode (RBKeywordView *view,
+				     GtkSelectionMode mode)
+{
+	g_return_if_fail (RB_IS_KEYWORD_VIEW (view));
+	g_return_if_fail (mode == GTK_SELECTION_SINGLE || mode == GTK_SELECTION_MULTIPLE);
+
+	gtk_tree_selection_set_mode (gtk_tree_view_get_selection (GTK_TREE_VIEW (view->priv->treeview)),
+				     mode);
+}
+
+void
+rb_keyword_view_reset (RBKeywordView *view)
+{
+	RhythmDBKeywordModel *model;
+
+	g_return_if_fail (RB_IS_KEYWORD_VIEW (view));
+
+	model = rhythmdb_keyword_model_new (view->priv->db);
+
+	rb_keyword_view_set_model_internal (view, model);
+	g_object_unref (model);
+}
+
+RhythmDBKeywordModel *
+rb_keyword_view_get_model (RBKeywordView *view)
+{
+	RhythmDBKeywordModel *model;
+
+	g_return_val_if_fail (RB_IS_KEYWORD_VIEW (view), NULL);
+
+	model = view->priv->prop_model;
+
+	return model;
+}
+
+void
+rb_keyword_view_set_model (RBKeywordView *view,
+			    RhythmDBKeywordModel *model)
+{
+	g_return_if_fail (RB_IS_KEYWORD_VIEW (view));
+
+	rb_keyword_view_set_model_internal (view, model);
+}
+
+static void
+rb_keyword_view_pre_row_deleted_cb (RhythmDBKeywordModel *model,
+				     RBKeywordView *view)
+{
+	view->priv->handling_row_deletion = TRUE;
+	rb_debug ("pre row deleted");
+}
+
+static void
+rb_keyword_view_post_row_deleted_cb (GtkTreeModel *model,
+				      GtkTreePath *path,
+				      RBKeywordView *view)
+{
+	view->priv->handling_row_deletion = FALSE;
+	rb_debug ("post row deleted");
+	if (gtk_tree_selection_count_selected_rows (view->priv->selection) == 0) {
+		GtkTreeIter first_iter;
+		rb_debug ("no rows selected, signalling reset");
+		if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (view->priv->prop_model), &first_iter)) {
+			g_signal_handlers_block_by_func (G_OBJECT (view->priv->selection),
+							 G_CALLBACK (rb_keyword_view_selection_changed_cb),
+							 view);
+			gtk_tree_selection_select_iter (view->priv->selection, &first_iter);
+			g_signal_emit (G_OBJECT (view), rb_keyword_view_signals[SELECTION_RESET], 0);
+			g_signal_handlers_unblock_by_func (G_OBJECT (view->priv->selection),
+							   G_CALLBACK (rb_keyword_view_selection_changed_cb),
+							   view);
+		}
+	}
+}
+
+guint
+rb_keyword_view_get_num_keywords (RBKeywordView *view)
+{
+	g_return_val_if_fail (RB_IS_KEYWORD_VIEW (view), 0);
+
+	return gtk_tree_model_iter_n_children (GTK_TREE_MODEL (view->priv->prop_model),
+					       NULL)-1;
+}
+
+static void
+rb_keyword_view_cell_data_func (GtkTreeViewColumn *column,
+				 GtkCellRenderer *renderer,
+				 GtkTreeModel *tree_model,
+				 GtkTreeIter *iter,
+				 RBKeywordView *view)
+{
+	char *title;
+	char *str;
+	guint number;
+	gboolean is_all;
+
+	gtk_tree_model_get (GTK_TREE_MODEL (tree_model), iter,
+			    RHYTHMDB_KEYWORD_MODEL_COLUMN_TITLE, &title,
+			    RHYTHMDB_KEYWORD_MODEL_COLUMN_PRIORITY, &is_all,
+			    RHYTHMDB_KEYWORD_MODEL_COLUMN_NUMBER, &number, -1);
+
+	if (is_all) {
+		int nodes;
+
+		nodes = gtk_tree_model_iter_n_children  (GTK_TREE_MODEL (tree_model), NULL);
+		/* Subtract one for the All node */
+		nodes--;
+
+		str = g_strdup_printf (_("All %d keywords (%d)"), nodes, number);
+	} else {
+		str = g_strdup_printf (_("%s (%d)"), title, number);
+	}
+
+	g_object_set (G_OBJECT (renderer), "text", str,
+		      "weight", G_UNLIKELY (is_all) ? PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL,
+		      NULL);
+	g_free (str);
+	g_free (title);
+}
+
+static GObject *
+rb_keyword_view_constructor (GType type,
+			      guint n_construct_properties,
+			      GObjectConstructParam *construct_properties)
+{
+	GtkTreeViewColumn *column;
+	GtkCellRenderer *renderer;
+
+	RBKeywordView *view;
+	RBKeywordViewClass *klass;
+
+	klass = RB_KEYWORD_VIEW_CLASS (g_type_class_peek (RB_TYPE_KEYWORD_VIEW));
+
+	view = RB_KEYWORD_VIEW (G_OBJECT_CLASS (rb_keyword_view_parent_class)->
+			constructor (type, n_construct_properties, construct_properties));
+
+	view->priv->prop_model = rhythmdb_keyword_model_new (view->priv->db);
+	view->priv->treeview = GTK_WIDGET (gtk_tree_view_new_with_model (GTK_TREE_MODEL (view->priv->prop_model)));
+
+	if (view->priv->draggable)
+		rhythmdb_keyword_model_enable_drag (view->priv->prop_model,
+						     GTK_TREE_VIEW (view->priv->treeview));
+
+	g_signal_connect_object (G_OBJECT (view->priv->treeview),
+			         "row_activated",
+			         G_CALLBACK (rb_keyword_view_row_activated_cb),
+			         view,
+				 0);
+
+	view->priv->selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (view->priv->treeview));
+	g_signal_connect_object (G_OBJECT (view->priv->selection),
+			         "changed",
+			         G_CALLBACK (rb_keyword_view_selection_changed_cb),
+			         view,
+				 0);
+	g_signal_connect_object (G_OBJECT (view->priv->treeview),
+				 "popup_menu",
+				 G_CALLBACK (rb_keyword_view_popup_menu_cb),
+				 view,
+				 0);
+
+	g_signal_connect_object (G_OBJECT (view->priv->treeview),
+			         "button_press_event",
+			         G_CALLBACK (rb_keyword_view_button_press_cb),
+			         view,
+				 0);
+
+	gtk_container_add (GTK_CONTAINER (view), view->priv->treeview);
+
+	gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (view->priv->treeview), TRUE);
+	gtk_tree_selection_set_mode (view->priv->selection, GTK_SELECTION_SINGLE);
+
+	column = gtk_tree_view_column_new ();
+	renderer = gtk_cell_renderer_text_new ();
+	gtk_tree_view_column_pack_start (column, renderer, TRUE);
+	gtk_tree_view_column_set_cell_data_func (column, renderer,
+						 (GtkTreeCellDataFunc) rb_keyword_view_cell_data_func,
+						 view, NULL);
+	gtk_tree_view_column_set_title (column, view->priv->title);
+	gtk_tree_view_column_set_sizing (column, GTK_TREE_VIEW_COLUMN_FIXED);
+	gtk_tree_view_append_column (GTK_TREE_VIEW (view->priv->treeview),
+				     column);
+
+	return G_OBJECT (view);
+}
+
+static void
+rb_keyword_view_row_activated_cb (GtkTreeView *treeview,
+				   GtkTreePath *path,
+				   GtkTreeViewColumn *column,
+				   RBKeywordView *view)
+{
+	GtkTreeIter iter;
+	char *val;
+	gboolean is_all;
+
+	rb_debug ("row activated");
+	g_return_if_fail (gtk_tree_model_get_iter (GTK_TREE_MODEL (view->priv->prop_model),
+			  &iter, path));
+
+	gtk_tree_model_get (GTK_TREE_MODEL (view->priv->prop_model), &iter,
+			    RHYTHMDB_KEYWORD_MODEL_COLUMN_TITLE, &val,
+			    RHYTHMDB_KEYWORD_MODEL_COLUMN_PRIORITY, &is_all, -1);
+
+	rb_debug ("emitting keyword activated");
+	g_signal_emit (G_OBJECT (view), rb_keyword_view_signals[KEYWORD_ACTIVATED], 0,
+		       is_all ? NULL : val);
+
+	g_free (val);
+}
+
+void
+rb_keyword_view_set_selection (RBKeywordView *view,
+				const GList *vals)
+{
+	g_return_if_fail (RB_IS_KEYWORD_VIEW (view));
+
+	view->priv->handling_row_deletion = TRUE;
+
+	gtk_tree_selection_unselect_all (view->priv->selection);
+
+	for (; vals ; vals = vals->next) {
+		GtkTreeIter iter;
+
+		if (rhythmdb_keyword_model_iter_from_string (view->priv->prop_model, vals->data, &iter)) {
+			GtkTreePath *path;
+
+			gtk_tree_selection_select_iter (view->priv->selection, &iter);
+			path = gtk_tree_model_get_path (GTK_TREE_MODEL (view->priv->prop_model), &iter);
+			if (path != NULL) {
+				gtk_tree_view_scroll_to_cell (GTK_TREE_VIEW (view->priv->treeview),
+							      path, NULL, TRUE,
+							      0.5, 0.0);
+				gtk_tree_path_free (path);
+			}
+
+		}
+	}
+
+	view->priv->handling_row_deletion = FALSE;
+	rb_keyword_view_selection_changed_cb (view->priv->selection, view);
+}
+
+static void
+rb_keyword_view_selection_changed_cb (GtkTreeSelection *selection,
+				       RBKeywordView *view)
+{
+	char *selected_prop = NULL;
+	gboolean is_all = TRUE;
+	GtkTreeModel *model;
+	GtkTreeIter iter;
+
+	if (view->priv->handling_row_deletion)
+		return;
+
+	rb_debug ("selection changed");
+	if (gtk_tree_selection_get_mode (selection) == GTK_SELECTION_MULTIPLE) {
+		GList *selected_rows, *tem;
+		GList *selected_properties = NULL;
+
+		selected_rows = gtk_tree_selection_get_selected_rows (view->priv->selection, &model);
+		for (tem = selected_rows; tem; tem = tem->next) {
+			g_assert (gtk_tree_model_get_iter (model, &iter, tem->data));
+			gtk_tree_model_get (model, &iter,
+					    RHYTHMDB_KEYWORD_MODEL_COLUMN_TITLE, &selected_prop,
+					    RHYTHMDB_KEYWORD_MODEL_COLUMN_PRIORITY, &is_all, -1);
+			if (is_all) {
+				g_list_free (selected_properties);
+				selected_properties = NULL;
+				break;
+			}
+			selected_properties = g_list_prepend (selected_properties,
+							     g_strdup (selected_prop));
+		}
+
+		g_list_foreach (selected_rows, (GFunc) gtk_tree_path_free, NULL);
+		g_list_free (selected_rows);
+
+		if (is_all) {
+			g_signal_handlers_block_by_func (G_OBJECT (view->priv->selection),
+							 G_CALLBACK (rb_keyword_view_selection_changed_cb),
+							 view);
+			gtk_tree_selection_unselect_all (selection);
+			if (gtk_tree_model_get_iter_first (model, &iter))
+				gtk_tree_selection_select_iter (selection, &iter);
+			g_signal_handlers_unblock_by_func (G_OBJECT (view->priv->selection),
+							   G_CALLBACK (rb_keyword_view_selection_changed_cb),
+							   view);
+		}
+		g_signal_emit (G_OBJECT (view), rb_keyword_view_signals[KEYWORDS_SELECTED], 0,
+			       selected_properties);
+		rb_list_deep_free (selected_properties);
+	} else {
+		if (gtk_tree_selection_get_selected (view->priv->selection, &model, &iter)) {
+			gtk_tree_model_get (model, &iter,
+					    RHYTHMDB_KEYWORD_MODEL_COLUMN_TITLE, &selected_prop,
+					    RHYTHMDB_KEYWORD_MODEL_COLUMN_PRIORITY, &is_all, -1);
+			g_signal_emit (G_OBJECT (view), rb_keyword_view_signals[KEYWORD_SELECTED], 0,
+				       is_all ? NULL : selected_prop);
+		}
+	}
+
+	g_free (selected_prop);
+}
+
+static gboolean
+rb_keyword_view_popup_menu_cb (GtkTreeView *treeview,
+				RBKeywordView *view)
+{
+	g_signal_emit (G_OBJECT (view), rb_keyword_view_signals[SHOW_POPUP], 0);
+	return TRUE;
+}
+
+void
+rb_keyword_view_append_column_custom (RBKeywordView *view,
+				       GtkTreeViewColumn *column)
+{
+	g_return_if_fail (RB_IS_KEYWORD_VIEW (view));
+
+	gtk_tree_view_append_column (GTK_TREE_VIEW (view->priv->treeview), column);
+}
+
+static gboolean
+rb_keyword_view_button_press_cb (GtkTreeView *tree,
+				  GdkEventButton *event,
+				  RBKeywordView *view)
+{
+
+	if (event->button == 3) {
+		GtkTreeSelection *selection;
+		GtkTreePath *path;
+
+		selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (view->priv->treeview));
+
+		gtk_tree_view_get_path_at_pos (GTK_TREE_VIEW (view->priv->treeview), event->x, event->y, &path, NULL, NULL, NULL);
+		if (path == NULL) {
+			gtk_tree_selection_unselect_all (selection);
+		} else {
+			GtkTreeModel *model;
+			GtkTreeIter iter;
+			char *val;
+			GList *lst = NULL;
+
+			model = gtk_tree_view_get_model (GTK_TREE_VIEW (view->priv->treeview));
+			if (gtk_tree_model_get_iter (model, &iter, path)) {
+				gtk_tree_model_get (model, &iter,
+						    RHYTHMDB_KEYWORD_MODEL_COLUMN_TITLE, &val, -1);
+				lst = g_list_prepend (lst, (gpointer) val);
+				rb_keyword_view_set_selection (view, lst);
+				g_free (val);
+			}
+		}
+		g_signal_emit (G_OBJECT (view), rb_keyword_view_signals[SHOW_POPUP], 0);
+		return TRUE;
+	}
+
+	return FALSE;
+}
+
+void
+rb_keyword_view_set_search_func (RBKeywordView *view,
+				  GtkTreeViewSearchEqualFunc func,
+				  gpointer func_data,
+				  GtkDestroyNotify notify)
+{
+	g_return_if_fail (RB_IS_KEYWORD_VIEW (view));
+
+	gtk_tree_view_set_search_equal_func (GTK_TREE_VIEW (view->priv->treeview),
+					     func, func_data,
+					     notify);
+}
