Blob Blame History Raw
/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */

/* e-name-selector-dialog.c - Dialog that lets user pick EDestinations.
 *
 * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation.
 *
 * 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 Lesser General Public License
 * along with this program; if not, see <http://www.gnu.org/licenses/>.
 *
 * Author: Hans Petter Jansson <hpj@novell.com>
 */

#include "evolution-config.h"

#include <string.h>
#include <gdk/gdkkeysyms.h>
#include <glib/gi18n-lib.h>

#include <libebook/libebook.h>
#include <libebackend/libebackend.h>

#include "e-client-combo-box.h"
#include "e-destination-store.h"
#include "e-contact-store.h"
#include "e-name-selector-dialog.h"
#include "e-name-selector-entry.h"

#define E_NAME_SELECTOR_DIALOG_GET_PRIVATE(obj) \
	(G_TYPE_INSTANCE_GET_PRIVATE \
	((obj), E_TYPE_NAME_SELECTOR_DIALOG, ENameSelectorDialogPrivate))

typedef struct {
	gchar        *name;

	GtkGrid      *section_grid;
	GtkLabel     *label;
	GtkButton    *transfer_button;
	GtkButton    *remove_button;
	GtkTreeView  *destination_view;
}
Section;

typedef struct {
	GtkTreeView *view;
	GtkButton   *button;
	ENameSelectorDialog *dlg_ptr;
} SelData;

struct _ENameSelectorDialogPrivate {
	EClientCache *client_cache;
	ENameSelectorModel *name_selector_model;
	GtkTreeModelSort *contact_sort;
	GCancellable *cancellable;

	GtkWidget *client_combo;
	GtkTreeView *contact_view;
	GtkLabel *status_label;
	GtkGrid *destination_vgrid;
	GtkEntry *search_entry;
	GtkSizeGroup *button_size_group;
	GtkWidget *category_combobox;
	GtkWidget *contact_window;

	GArray *sections;

	guint destination_index;
	GSList *user_query_fields;
	GtkSizeGroup *dest_label_size_group;
};

enum {
	PROP_0,
	PROP_CLIENT_CACHE
};

static void     search_changed                (ENameSelectorDialog *name_selector_dialog);
static void     source_changed                (ENameSelectorDialog *name_selector_dialog,
					       EClientComboBox *combo_box);
static void     transfer_button_clicked       (ENameSelectorDialog *name_selector_dialog, GtkButton *transfer_button);
static void     contact_selection_changed     (ENameSelectorDialog *name_selector_dialog);
static void     setup_name_selector_model     (ENameSelectorDialog *name_selector_dialog);
static void     shutdown_name_selector_model  (ENameSelectorDialog *name_selector_dialog);
static void     contact_activated             (ENameSelectorDialog *name_selector_dialog, GtkTreePath *path);
static void     destination_activated         (ENameSelectorDialog *name_selector_dialog, GtkTreePath *path,
					       GtkTreeViewColumn *column, GtkTreeView *tree_view);
static gboolean destination_key_press         (ENameSelectorDialog *name_selector_dialog, GdkEventKey *event, GtkTreeView *tree_view);
static void remove_button_clicked (GtkButton *button, SelData *data);
static void     remove_books                  (ENameSelectorDialog *name_selector_dialog);
static void     contact_column_formatter      (GtkTreeViewColumn *column, GtkCellRenderer *cell,
					       GtkTreeModel *model, GtkTreeIter *iter,
					       ENameSelectorDialog *name_selector_dialog);
static void     destination_column_formatter  (GtkTreeViewColumn *column, GtkCellRenderer *cell,
					       GtkTreeModel *model, GtkTreeIter *iter,
					       ENameSelectorDialog *name_selector_dialog);

/* ------------------ *
 * Class/object setup *
 * ------------------ */

G_DEFINE_TYPE_WITH_CODE (
	ENameSelectorDialog,
	e_name_selector_dialog,
	GTK_TYPE_DIALOG,
	G_IMPLEMENT_INTERFACE (
		E_TYPE_EXTENSIBLE, NULL))

static void
name_selector_dialog_populate_categories (ENameSelectorDialog *name_selector_dialog)
{
	GtkWidget *combo_box;
	GList *category_list, *iter;

	/* "Any Category" is preloaded. */
	combo_box = name_selector_dialog->priv->category_combobox;
	if (gtk_combo_box_get_active (GTK_COMBO_BOX (combo_box)) == -1)
		gtk_combo_box_set_active (GTK_COMBO_BOX (combo_box), 0);

	/* Categories are already sorted. */
	category_list = e_categories_dup_list ();
	for (iter = category_list; iter != NULL; iter = iter->next) {
		/* Only add user-visible categories. */
		if (!e_categories_is_searchable (iter->data))
			continue;

		gtk_combo_box_text_append_text (
			GTK_COMBO_BOX_TEXT (combo_box), iter->data);
	}

	g_list_free_full (category_list, g_free);

	g_signal_connect_swapped (
		combo_box, "changed",
		G_CALLBACK (search_changed), name_selector_dialog);
}

static void
name_selector_dialog_set_client_cache (ENameSelectorDialog *name_selector_dialog,
                                       EClientCache *client_cache)
{
	g_return_if_fail (E_IS_CLIENT_CACHE (client_cache));
	g_return_if_fail (name_selector_dialog->priv->client_cache == NULL);

	name_selector_dialog->priv->client_cache = g_object_ref (client_cache);
}

static void
name_selector_dialog_set_property (GObject *object,
                                   guint property_id,
                                   const GValue *value,
                                   GParamSpec *pspec)
{
	switch (property_id) {
		case PROP_CLIENT_CACHE:
			name_selector_dialog_set_client_cache (
				E_NAME_SELECTOR_DIALOG (object),
				g_value_get_object (value));
			return;
	}

	G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
}

static void
name_selector_dialog_get_property (GObject *object,
                                   guint property_id,
                                   GValue *value,
                                   GParamSpec *pspec)
{
	switch (property_id) {
		case PROP_CLIENT_CACHE:
			g_value_take_object (
				value,
				e_name_selector_dialog_ref_client_cache (
				E_NAME_SELECTOR_DIALOG (object)));
			return;
	}

	G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
}

static void
name_selector_dialog_dispose (GObject *object)
{
	ENameSelectorDialogPrivate *priv;

	priv = E_NAME_SELECTOR_DIALOG_GET_PRIVATE (object);

	remove_books (E_NAME_SELECTOR_DIALOG (object));
	shutdown_name_selector_model (E_NAME_SELECTOR_DIALOG (object));

	g_clear_object (&priv->client_cache);

	/* Chain up to parent's dispose() method. */
	G_OBJECT_CLASS (e_name_selector_dialog_parent_class)->dispose (object);
}

static void
name_selector_dialog_finalize (GObject *object)
{
	ENameSelectorDialogPrivate *priv;

	priv = E_NAME_SELECTOR_DIALOG_GET_PRIVATE (object);

	g_slist_foreach (priv->user_query_fields, (GFunc) g_free, NULL);
	g_slist_free (priv->user_query_fields);

	g_array_free (priv->sections, TRUE);
	g_object_unref (priv->button_size_group);
	g_object_unref (priv->dest_label_size_group);

	/* Chain up to parent's finalize() method. */
	G_OBJECT_CLASS (e_name_selector_dialog_parent_class)->finalize (object);
}

static void
name_selector_dialog_constructed (GObject *object)
{
	ENameSelectorDialogPrivate *priv;
	GtkTreeSelection  *contact_selection;
	GtkTreeViewColumn *column;
	GtkCellRenderer   *cell_renderer;
	GtkTreeSelection  *selection;
	ESourceRegistry *registry;
	ESource *source;
	gchar *tmp_str;
	GtkWidget *name_selector_grid;
	GtkWidget *show_contacts_label;
	GtkWidget *hgrid;
	GtkWidget *label;
	GtkWidget *show_contacts_grid;
	GtkWidget *AddressBookLabel;
	GtkWidget *label_category;
	GtkWidget *search;
	AtkObject *atko;
	GtkWidget *label_search;
	GtkWidget *source_menu_hgrid;
	GtkWidget *combobox_category;
	GtkWidget *label_contacts;
	GtkWidget *scrolledwindow0;
	GtkWidget *scrolledwindow1;
	AtkRelationSet *tmp_relation_set;
	AtkRelationType tmp_relationship;
	AtkRelation *tmp_relation;
	AtkObject *scrolledwindow1_relation_targets[1];
	GtkWidget *source_tree_view;
	GtkWidget *destination_vgrid;
	GtkWidget *status_message;
	GtkWidget *client_combo;
	const gchar *extension_name;

	priv = E_NAME_SELECTOR_DIALOG_GET_PRIVATE (object);

	/* Chain up to parent's constructed() method. */
	G_OBJECT_CLASS (e_name_selector_dialog_parent_class)->constructed (object);

	name_selector_grid = g_object_new (
		GTK_TYPE_GRID,
		"orientation", GTK_ORIENTATION_VERTICAL,
		"column-homogeneous", FALSE,
		"row-spacing", 6,
		NULL);
	gtk_widget_show (name_selector_grid);
	gtk_container_set_border_width (GTK_CONTAINER (name_selector_grid), 0);

	tmp_str = g_strconcat ("<b>", _("Show Contacts"), "</b>", NULL);
	show_contacts_label = gtk_label_new (tmp_str);
	gtk_widget_show (show_contacts_label);
	gtk_container_add (GTK_CONTAINER (name_selector_grid), show_contacts_label);
	gtk_label_set_use_markup (GTK_LABEL (show_contacts_label), TRUE);
	gtk_misc_set_alignment (GTK_MISC (show_contacts_label), 0, 0.5);
	g_free (tmp_str);

	hgrid = g_object_new (
		GTK_TYPE_GRID,
		"orientation", GTK_ORIENTATION_HORIZONTAL,
		"row-homogeneous", FALSE,
		"column-spacing", 12,
		NULL);
	gtk_widget_show (hgrid);
	gtk_container_add (GTK_CONTAINER (name_selector_grid), hgrid);

	label = gtk_label_new ("");
	gtk_widget_show (label);
	gtk_container_add (GTK_CONTAINER (hgrid), label);

	show_contacts_grid = gtk_grid_new ();
	gtk_widget_show (show_contacts_grid);
	gtk_container_add (GTK_CONTAINER (hgrid), show_contacts_grid);
	g_object_set (
		G_OBJECT (show_contacts_grid),
		"column-spacing", 12,
		"row-spacing", 6,
		"hexpand", TRUE,
		"halign", GTK_ALIGN_FILL,
		NULL);

	AddressBookLabel = gtk_label_new_with_mnemonic (_("Address B_ook:"));
	gtk_widget_show (AddressBookLabel);
	gtk_grid_attach (GTK_GRID (show_contacts_grid), AddressBookLabel, 0, 0, 1, 1);
	gtk_widget_set_halign (AddressBookLabel, GTK_ALIGN_FILL);
	gtk_label_set_justify (GTK_LABEL (AddressBookLabel), GTK_JUSTIFY_CENTER);
	gtk_misc_set_alignment (GTK_MISC (AddressBookLabel), 0, 0.5);

	label_category = gtk_label_new_with_mnemonic (_("Cat_egory:"));
	gtk_widget_show (label_category);
	gtk_grid_attach (GTK_GRID (show_contacts_grid), label_category, 0, 1, 1, 1);
	gtk_widget_set_halign (label_category, GTK_ALIGN_FILL);
	gtk_label_set_justify (GTK_LABEL (label_category), GTK_JUSTIFY_CENTER);
	gtk_misc_set_alignment (GTK_MISC (label_category), 0, 0.5);

	hgrid = g_object_new (
		GTK_TYPE_GRID,
		"orientation", GTK_ORIENTATION_HORIZONTAL,
		"row-homogeneous", FALSE,
		"column-spacing", 12,
		"hexpand", TRUE,
		"halign", GTK_ALIGN_FILL,
		NULL);
	gtk_widget_show (hgrid);
	gtk_grid_attach (GTK_GRID (show_contacts_grid), hgrid, 1, 2, 1, 1);

	search = gtk_entry_new ();
	gtk_widget_show (search);
	gtk_widget_set_hexpand (search, TRUE);
	gtk_widget_set_halign (search, GTK_ALIGN_FILL);
	gtk_container_add (GTK_CONTAINER (hgrid), search);

	label_search = gtk_label_new_with_mnemonic (_("_Search:"));
	gtk_widget_show (label_search);
	gtk_grid_attach (GTK_GRID (show_contacts_grid), label_search, 0, 2, 1, 1);
	gtk_widget_set_halign (label_search, GTK_ALIGN_FILL);
	gtk_misc_set_alignment (GTK_MISC (label_search), 0, 0.5);

	source_menu_hgrid = g_object_new (
		GTK_TYPE_GRID,
		"orientation", GTK_ORIENTATION_HORIZONTAL,
		"row-homogeneous", FALSE,
		"column-spacing", 0,
		"halign", GTK_ALIGN_FILL,
		"valign", GTK_ALIGN_FILL,
		NULL);
	gtk_widget_show (source_menu_hgrid);
	gtk_grid_attach (GTK_GRID (show_contacts_grid), source_menu_hgrid, 1, 0, 1, 1);

	combobox_category = gtk_combo_box_text_new ();
	gtk_widget_show (combobox_category);
	g_object_set (
		G_OBJECT (combobox_category),
		"halign", GTK_ALIGN_FILL,
		"valign", GTK_ALIGN_FILL,
		NULL);
	gtk_grid_attach (GTK_GRID (show_contacts_grid), combobox_category, 1, 1, 1, 1);
	gtk_combo_box_text_append_text (
		GTK_COMBO_BOX_TEXT (combobox_category), _("Any Category"));

	tmp_str = g_strconcat ("<b>", _("Co_ntacts"), "</b>", NULL);
	label_contacts = gtk_label_new_with_mnemonic (tmp_str);
	gtk_widget_show (label_contacts);
	gtk_container_add (GTK_CONTAINER (name_selector_grid), label_contacts);
	gtk_label_set_use_markup (GTK_LABEL (label_contacts), TRUE);
	gtk_misc_set_alignment (GTK_MISC (label_contacts), 0, 0.5);
	g_free (tmp_str);

	scrolledwindow0 = gtk_scrolled_window_new (NULL, NULL);
	priv->contact_window = scrolledwindow0;
	gtk_widget_show (scrolledwindow0);
	gtk_widget_set_vexpand (scrolledwindow0, TRUE);
	gtk_widget_set_valign (scrolledwindow0, GTK_ALIGN_FILL);
	gtk_container_add (GTK_CONTAINER (name_selector_grid), scrolledwindow0);
	gtk_scrolled_window_set_policy (
		GTK_SCROLLED_WINDOW (scrolledwindow0),
		GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);

	hgrid = g_object_new (
		GTK_TYPE_GRID,
		"orientation", GTK_ORIENTATION_HORIZONTAL,
		"row-homogeneous", FALSE,
		"column-spacing", 12,
		NULL);
	gtk_widget_show (hgrid);
	gtk_scrolled_window_add_with_viewport (
		GTK_SCROLLED_WINDOW (scrolledwindow0), hgrid);

	label = gtk_label_new ("");
	gtk_widget_show (label);
	gtk_container_add (GTK_CONTAINER (hgrid), label);

	scrolledwindow1 = gtk_scrolled_window_new (NULL, NULL);
	gtk_widget_show (scrolledwindow1);
	gtk_container_add (GTK_CONTAINER (hgrid), scrolledwindow1);
	gtk_widget_set_hexpand (scrolledwindow1, TRUE);
	gtk_widget_set_halign (scrolledwindow1, GTK_ALIGN_FILL);
	gtk_scrolled_window_set_policy (
		GTK_SCROLLED_WINDOW (scrolledwindow1),
		GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
	gtk_scrolled_window_set_shadow_type (
		GTK_SCROLLED_WINDOW (scrolledwindow1), GTK_SHADOW_IN);

	source_tree_view = gtk_tree_view_new ();
	gtk_widget_show (source_tree_view);
	gtk_container_add (GTK_CONTAINER (scrolledwindow1), source_tree_view);
	gtk_tree_view_set_headers_visible (
		GTK_TREE_VIEW (source_tree_view), FALSE);
	gtk_tree_view_set_enable_search (
		GTK_TREE_VIEW (source_tree_view), FALSE);

	destination_vgrid = g_object_new (
		GTK_TYPE_GRID,
		"orientation", GTK_ORIENTATION_VERTICAL,
		"column-homogeneous", TRUE,
		"row-spacing", 6,
		"hexpand", TRUE,
		"halign", GTK_ALIGN_FILL,
		"vexpand", TRUE,
		"valign", GTK_ALIGN_FILL,
		NULL);
	gtk_widget_show (destination_vgrid);
	gtk_container_add (GTK_CONTAINER (hgrid), destination_vgrid);

	status_message = gtk_label_new ("");
	gtk_widget_show (status_message);
	gtk_container_add (GTK_CONTAINER (name_selector_grid), status_message);
	gtk_label_set_use_markup (GTK_LABEL (status_message), TRUE);
	gtk_misc_set_alignment (GTK_MISC (status_message), 0, 0.5);
	gtk_misc_set_padding (GTK_MISC (status_message), 0, 3);

	gtk_label_set_mnemonic_widget (GTK_LABEL (AddressBookLabel), source_menu_hgrid);
	gtk_label_set_mnemonic_widget (GTK_LABEL (label_category), combobox_category);
	gtk_label_set_mnemonic_widget (GTK_LABEL (label_search), search);
	gtk_label_set_mnemonic_widget (GTK_LABEL (label_contacts), source_tree_view);

	atko = gtk_widget_get_accessible (search);
	atk_object_set_name (atko, _("Search"));

	atko = gtk_widget_get_accessible (source_menu_hgrid);
	atk_object_set_name (atko, _("Address Book"));

	atko = gtk_widget_get_accessible (scrolledwindow1);
	atk_object_set_name (atko, _("Contacts"));
	tmp_relation_set = atk_object_ref_relation_set (atko);
	scrolledwindow1_relation_targets[0] = gtk_widget_get_accessible (label_contacts);
	tmp_relationship = atk_relation_type_for_name ("labelled-by");
	tmp_relation = atk_relation_new (scrolledwindow1_relation_targets, 1, tmp_relationship);
	atk_relation_set_add (tmp_relation_set, tmp_relation);
	g_object_unref (G_OBJECT (tmp_relation));
	g_object_unref (G_OBJECT (tmp_relation_set));

	gtk_box_pack_start (
		GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (object))),
		name_selector_grid, TRUE, TRUE, 0);

	/* Store pointers to relevant widgets */

	priv->contact_view = GTK_TREE_VIEW (source_tree_view);
	priv->status_label = GTK_LABEL (status_message);
	priv->destination_vgrid = GTK_GRID (destination_vgrid);
	priv->search_entry = GTK_ENTRY (search);
	priv->category_combobox = combobox_category;

	/* Create size group for transfer buttons */

	priv->button_size_group =
		gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL);

	/* Create size group for destination labels */

	priv->dest_label_size_group =
		gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL);

	/* Set up contacts view */

	column = gtk_tree_view_column_new ();
	cell_renderer = GTK_CELL_RENDERER (gtk_cell_renderer_text_new ());
	gtk_tree_view_column_pack_start (column, cell_renderer, TRUE);
	gtk_tree_view_column_set_cell_data_func (
		column, cell_renderer, (GtkTreeCellDataFunc)
		contact_column_formatter, object, NULL);

	selection = gtk_tree_view_get_selection (priv->contact_view);
	gtk_tree_selection_set_mode (selection, GTK_SELECTION_MULTIPLE);
	gtk_tree_view_append_column (priv->contact_view, column);
	g_signal_connect_swapped (
		priv->contact_view, "row-activated",
		G_CALLBACK (contact_activated), object);

	/* Listen for changes to the contact selection */

	contact_selection = gtk_tree_view_get_selection (priv->contact_view);
	g_signal_connect_swapped (
		contact_selection, "changed",
		G_CALLBACK (contact_selection_changed), object);

	/* Set up our data structures */

	priv->name_selector_model = e_name_selector_model_new ();
	priv->sections = g_array_new (FALSE, FALSE, sizeof (Section));

	setup_name_selector_model (E_NAME_SELECTOR_DIALOG (object));

	/* Create source menu */

	extension_name = E_SOURCE_EXTENSION_ADDRESS_BOOK;
	client_combo = e_client_combo_box_new (
		priv->client_cache, extension_name);
	priv->client_combo = client_combo;
	g_signal_connect_swapped (
		client_combo, "changed",
		G_CALLBACK (source_changed), object);

	gtk_label_set_mnemonic_widget (
		GTK_LABEL (AddressBookLabel), client_combo);
	gtk_widget_show (client_combo);
	gtk_widget_set_hexpand (client_combo, TRUE);
	gtk_widget_set_halign (client_combo, GTK_ALIGN_FILL);
	gtk_container_add (GTK_CONTAINER (source_menu_hgrid), client_combo);

	name_selector_dialog_populate_categories (
		E_NAME_SELECTOR_DIALOG (object));

	/* Set up search-as-you-type signal */

	g_signal_connect_swapped (
		search, "changed",
		G_CALLBACK (search_changed), object);

	/* Display initial source */

	registry = e_client_cache_ref_registry (priv->client_cache);
	source = e_source_registry_ref_default_address_book (registry);
	e_source_combo_box_set_active (
		E_SOURCE_COMBO_BOX (client_combo), source);
	g_object_unref (source);
	g_object_unref (registry);

	/* Set up dialog defaults */

	gtk_dialog_add_buttons (
		GTK_DIALOG (object),
		_("_Close"), GTK_RESPONSE_CLOSE,
		NULL);

	/* Try to figure out a sane default size for the dialog. We used to hard
	 * code this to 512 so keep using 512 if the screen is big enough,
	 * otherwise use -1 (use as little as possible, use the
	 * GtkScrolledWindow's scrollbars).
	 *
	 * This should allow scrolling on tiny netbook resolutions and let
	 * others see as much of the dialog as possible.
	 *
	 * 600 pixels seems to be a good lower bound resolution to allow room
	 * above or below for other UI (window manager's?)
	 */
	gtk_window_set_default_size (
		GTK_WINDOW (object), 700,
		gdk_screen_height () >= 600 ? 512 : -1);

	gtk_dialog_set_default_response (
		GTK_DIALOG (object), GTK_RESPONSE_CLOSE);
	gtk_window_set_modal (GTK_WINDOW (object), TRUE);
	gtk_window_set_resizable (GTK_WINDOW (object), TRUE);
	gtk_container_set_border_width (GTK_CONTAINER (object), 4);
	gtk_window_set_title (
		GTK_WINDOW (object),
		_("Select Contacts from Address Book"));
	gtk_widget_grab_focus (search);

	e_extensible_load_extensions (E_EXTENSIBLE (object));
}

static void
name_selector_dialog_realize (GtkWidget *widget)
{
	ENameSelectorDialog *name_selector_dialog;
	GtkWidget *client_combo;

	/* Chain up to parent's realize() method. */
	GTK_WIDGET_CLASS (e_name_selector_dialog_parent_class)->
		realize (widget);

	name_selector_dialog = E_NAME_SELECTOR_DIALOG (widget);
	client_combo = name_selector_dialog->priv->client_combo;

	source_changed (
		name_selector_dialog,
		E_CLIENT_COMBO_BOX (client_combo));
}

static void
e_name_selector_dialog_class_init (ENameSelectorDialogClass *class)
{
	GObjectClass *object_class;
	GtkWidgetClass *widget_class;

	g_type_class_add_private (class, sizeof (ENameSelectorDialogPrivate));

	object_class = G_OBJECT_CLASS (class);
	object_class->set_property = name_selector_dialog_set_property;
	object_class->get_property = name_selector_dialog_get_property;
	object_class->dispose = name_selector_dialog_dispose;
	object_class->finalize = name_selector_dialog_finalize;
	object_class->constructed = name_selector_dialog_constructed;

	widget_class = GTK_WIDGET_CLASS (class);
	widget_class->realize = name_selector_dialog_realize;

	/**
	 * ENameSelectorDialog:client-cache:
	 *
	 * Cache of shared #EClient instances.
	 **/
	g_object_class_install_property (
		object_class,
		PROP_CLIENT_CACHE,
		g_param_spec_object (
			"client-cache",
			"Client Cache",
			"Cache of shared EClient instances",
			E_TYPE_CLIENT_CACHE,
			G_PARAM_READWRITE |
			G_PARAM_CONSTRUCT_ONLY |
			G_PARAM_STATIC_STRINGS));
}

static void
e_name_selector_dialog_init (ENameSelectorDialog *name_selector_dialog)
{
	name_selector_dialog->priv =
		E_NAME_SELECTOR_DIALOG_GET_PRIVATE (name_selector_dialog);
}

/**
 * e_name_selector_dialog_new:
 * @client_cache: an #EClientCache
 *
 * Creates a new #ENameSelectorDialog.
 *
 * Returns: A new #ENameSelectorDialog.
 **/
ENameSelectorDialog *
e_name_selector_dialog_new (EClientCache *client_cache)
{
	g_return_val_if_fail (E_IS_CLIENT_CACHE (client_cache), NULL);

	return g_object_new (
		E_TYPE_NAME_SELECTOR_DIALOG,
		"client-cache", client_cache, NULL);
}

/**
 * e_name_selector_dialog_ref_client_cache:
 * @name_selector_dialog: an #ENameSelectorDialog
 *
 * Returns the #EClientCache passed to e_name_selector_dialog_new().
 *
 * The returned #EClientCache is referenced for thread-safety and must be
 * unreferenced with g_object_unref() when finished with it.
 *
 * Returns: an #EClientCache
 *
 * Since: 3.8
 **/
EClientCache *
e_name_selector_dialog_ref_client_cache (ENameSelectorDialog *name_selector_dialog)
{
	g_return_val_if_fail (
		E_IS_NAME_SELECTOR_DIALOG (name_selector_dialog), NULL);

	return g_object_ref (name_selector_dialog->priv->client_cache);
}

/* --------- *
 * Utilities *
 * --------- */

static gchar *
escape_sexp_string (const gchar *string)
{
	GString *gstring;
	gchar   *encoded_string;

	gstring = g_string_new ("");
	e_sexp_encode_string (gstring, string);

	encoded_string = gstring->str;
	g_string_free (gstring, FALSE);

	return encoded_string;
}

static void
sort_iter_to_contact_store_iter (ENameSelectorDialog *name_selector_dialog,
                                 GtkTreeIter *iter,
                                 gint *email_n)
{
	ETreeModelGenerator *contact_filter;
	GtkTreeIter          child_iter;
	gint                 email_n_local;

	contact_filter = e_name_selector_model_peek_contact_filter (
		name_selector_dialog->priv->name_selector_model);

	gtk_tree_model_sort_convert_iter_to_child_iter (
		name_selector_dialog->priv->contact_sort, &child_iter, iter);
	e_tree_model_generator_convert_iter_to_child_iter (
		contact_filter, iter, &email_n_local, &child_iter);

	if (email_n)
		*email_n = email_n_local;
}

static void
add_destination (ENameSelectorModel *name_selector_model,
                 EDestinationStore *destination_store,
                 EContact *contact,
                 gint email_n,
                 EBookClient *client)
{
	EDestination *destination;
	GList *email_list, *nth;

	/* get the correct index of an email in the contact */
	email_list = e_name_selector_model_get_contact_emails_without_used (name_selector_model, contact, FALSE);
	while (nth = g_list_nth (email_list, email_n), nth && nth->data == NULL) {
		email_n++;
	}
	e_name_selector_model_free_emails_list (email_list);

	/* Transfer (actually, copy into a destination and let the model filter out the
	 * source automatically) */

	destination = e_destination_new ();
	e_destination_set_contact (destination, contact, email_n);
	if (client)
		e_destination_set_client (destination, client);
	e_destination_store_append_destination (destination_store, destination);
	g_object_unref (destination);
}

static void
disable_sort (ENameSelectorDialog *dialog)
{
	if (dialog->priv->contact_sort) {
		g_object_unref (dialog->priv->contact_sort);
		dialog->priv->contact_sort = NULL;
	}

	gtk_tree_view_set_model (
		dialog->priv->contact_view,
		NULL);
}

static void
enable_sort (ENameSelectorDialog *dialog)
{
	ETreeModelGenerator *contact_filter;

	/* Get contact store and its filter wrapper */
	contact_filter = e_name_selector_model_peek_contact_filter (
		dialog->priv->name_selector_model);

	/* Create sorting model on top of filter, assign it to view */
	if (!dialog->priv->contact_sort) {
		dialog->priv->contact_sort = GTK_TREE_MODEL_SORT (
			gtk_tree_model_sort_new_with_model (GTK_TREE_MODEL (contact_filter)));

		/* sort on full name as we display full name in name selector dialog */
		gtk_tree_sortable_set_sort_column_id (
			GTK_TREE_SORTABLE (dialog->priv->contact_sort),
			E_CONTACT_FULL_NAME, GTK_SORT_ASCENDING);
	}

	gtk_tree_view_set_model (
		dialog->priv->contact_view,
		GTK_TREE_MODEL (dialog->priv->contact_sort));
}

static void
remove_books (ENameSelectorDialog *name_selector_dialog)
{
	EContactStore *contact_store;
	GSList        *clients, *l;

	if (!name_selector_dialog->priv->name_selector_model)
		return;

	contact_store = e_name_selector_model_peek_contact_store (
		name_selector_dialog->priv->name_selector_model);

	/* Remove books (should be just one) being viewed */
	clients = e_contact_store_get_clients (contact_store);
	for (l = clients; l; l = g_slist_next (l)) {
		EBookClient *client = l->data;
		e_contact_store_remove_client (contact_store, client);
	}
	g_slist_free (clients);

	/* See if we have a book pending; stop loading it if so */
	if (name_selector_dialog->priv->cancellable != NULL) {
		g_cancellable_cancel (name_selector_dialog->priv->cancellable);
		g_object_unref (name_selector_dialog->priv->cancellable);
		name_selector_dialog->priv->cancellable = NULL;
	}

	disable_sort (name_selector_dialog);
}


/* ------------------ *
 * Section management *
 * ------------------ */

static gint
find_section_by_transfer_button (ENameSelectorDialog *name_selector_dialog,
                                 GtkButton *transfer_button)
{
	gint i;

	for (i = 0; i < name_selector_dialog->priv->sections->len; i++) {
		Section *section = &g_array_index (
			name_selector_dialog->priv->sections, Section, i);

		if (section->transfer_button == transfer_button)
			return i;
	}

	return -1;
}

static gint
find_section_by_tree_view (ENameSelectorDialog *name_selector_dialog,
                           GtkTreeView *tree_view)
{
	gint i;

	for (i = 0; i < name_selector_dialog->priv->sections->len; i++) {
		Section *section = &g_array_index (
			name_selector_dialog->priv->sections, Section, i);

		if (section->destination_view == tree_view)
			return i;
	}

	return -1;
}

static gint
find_section_by_name (ENameSelectorDialog *name_selector_dialog,
                      const gchar *name)
{
	gint i;

	for (i = 0; i < name_selector_dialog->priv->sections->len; i++) {
		Section *section = &g_array_index (
			name_selector_dialog->priv->sections, Section, i);

		if (!strcmp (name, section->name))
			return i;
	}

	return -1;
}

static void
selection_changed (GtkTreeSelection *selection,
                   SelData *data)
{
	GtkTreeSelection *contact_selection;
	gboolean          have_selection = FALSE;

	contact_selection = gtk_tree_view_get_selection (data->view);
	if (gtk_tree_selection_count_selected_rows (contact_selection) > 0)
		have_selection = TRUE;
	gtk_widget_set_sensitive (GTK_WIDGET (data->button), have_selection);
}

static GtkTreeView *
make_tree_view_for_section (ENameSelectorDialog *name_selector_dialog,
                            EDestinationStore *destination_store)
{
	GtkTreeView *tree_view;
	GtkTreeViewColumn *column;
	GtkCellRenderer   *cell_renderer;

	tree_view = GTK_TREE_VIEW (gtk_tree_view_new ());

	column = gtk_tree_view_column_new ();
	cell_renderer = GTK_CELL_RENDERER (gtk_cell_renderer_text_new ());
	gtk_tree_view_column_pack_start (column, cell_renderer, TRUE);
	gtk_tree_view_column_set_cell_data_func (
		column, cell_renderer,
		(GtkTreeCellDataFunc) destination_column_formatter,
		name_selector_dialog, NULL);
	gtk_tree_view_append_column (tree_view, column);
	gtk_tree_view_set_headers_visible (tree_view, FALSE);
	gtk_tree_view_set_model (tree_view, GTK_TREE_MODEL (destination_store));

	return tree_view;
}

static void
setup_section_button (ENameSelectorDialog *name_selector_dialog,
                      GtkButton *button,
                      double halign,
                      const gchar *label_text,
                      const gchar *icon_name,
                      gboolean icon_before_label)
{
	GtkWidget *alignment;
	GtkWidget *hgrid;
	GtkWidget *label;
	GtkWidget *image;

	gtk_size_group_add_widget (
		name_selector_dialog->priv->button_size_group,
		GTK_WIDGET (button));

	alignment = gtk_alignment_new (halign, 0.5, 0.0, 0.0);
	gtk_container_add (GTK_CONTAINER (button), GTK_WIDGET (alignment));

	hgrid = g_object_new (
		GTK_TYPE_GRID,
		"orientation", GTK_ORIENTATION_HORIZONTAL,
		"row-homogeneous", FALSE,
		"column-spacing", 2,
		NULL);
	gtk_widget_show (hgrid);
	gtk_container_add (GTK_CONTAINER (alignment), hgrid);

	label = gtk_label_new_with_mnemonic (label_text);
	gtk_widget_show (label);

	image = gtk_image_new_from_icon_name (icon_name, GTK_ICON_SIZE_BUTTON);
	gtk_widget_show (image);

	if (icon_before_label) {
		gtk_container_add (GTK_CONTAINER (hgrid), image);
		gtk_container_add (GTK_CONTAINER (hgrid), label);
	} else {
		gtk_container_add (GTK_CONTAINER (hgrid), label);
		gtk_container_add (GTK_CONTAINER (hgrid), image);
	}
}

static gint
add_section (ENameSelectorDialog *name_selector_dialog,
             const gchar *name,
             const gchar *pretty_name,
             EDestinationStore *destination_store)
{
	ENameSelectorDialogPrivate *priv;
	Section            section;
	GtkWidget	  *vgrid;
	GtkWidget	  *alignment;
	GtkWidget	  *scrollwin;
	SelData		  *data;
	GtkTreeSelection  *selection;
	gchar		  *text;
	GtkWidget         *hgrid;

	g_return_val_if_fail (name != NULL, -1);
	g_return_val_if_fail (pretty_name != NULL, -1);
	g_return_val_if_fail (E_IS_DESTINATION_STORE (destination_store), -1);

	priv = E_NAME_SELECTOR_DIALOG_GET_PRIVATE (name_selector_dialog);

	memset (&section, 0, sizeof (Section));

	section.name = g_strdup (name);
	section.section_grid = g_object_new (
		GTK_TYPE_GRID,
		"orientation", GTK_ORIENTATION_HORIZONTAL,
		"row-homogeneous", FALSE,
		"column-spacing", 12,
		"vexpand", TRUE,
		"valign", GTK_ALIGN_FILL,
		NULL);
	section.label = GTK_LABEL (gtk_label_new_with_mnemonic (pretty_name));
	section.transfer_button = GTK_BUTTON (gtk_button_new ());
	section.remove_button = GTK_BUTTON (gtk_button_new ());
	section.destination_view = make_tree_view_for_section (name_selector_dialog, destination_store);

	gtk_label_set_mnemonic_widget (GTK_LABEL (section.label), GTK_WIDGET (section.destination_view));

	if (pango_parse_markup (pretty_name, -1, '_', NULL,
				&text, NULL, NULL))  {
		atk_object_set_name (gtk_widget_get_accessible (
					GTK_WIDGET (section.destination_view)), text);
		g_free (text);
	}

	/* Set up transfer button */
	g_signal_connect_swapped (
		section.transfer_button, "clicked",
		G_CALLBACK (transfer_button_clicked), name_selector_dialog);

	/*data for the remove callback*/
	data = g_malloc0 (sizeof (SelData));
	data->view = section.destination_view;
	data->dlg_ptr = name_selector_dialog;

	/*Associate to an object destroy so that it gets freed*/
	g_object_set_data_full ((GObject *) section.destination_view, "sel-remove-data", data, g_free);

	g_signal_connect (
		section.remove_button, "clicked",
		G_CALLBACK (remove_button_clicked), data);

	/* Alignment and vgrid for the add/remove buttons */

	alignment = gtk_alignment_new (0.5, 0.0, 0.0, 0.0);
	gtk_container_add (GTK_CONTAINER (section.section_grid), alignment);

	vgrid = g_object_new (
		GTK_TYPE_GRID,
		"orientation", GTK_ORIENTATION_VERTICAL,
		"column-homogeneous", TRUE,
		"row-spacing", 6,
		NULL);

	gtk_container_add (GTK_CONTAINER (alignment), vgrid);

	/* "Add" button */
	gtk_container_add (GTK_CONTAINER (vgrid), GTK_WIDGET (section.transfer_button));
	setup_section_button (name_selector_dialog, section.transfer_button, 0.7, _("_Add"), "go-next", FALSE);

	/* "Remove" button */
	gtk_container_add (GTK_CONTAINER (vgrid), GTK_WIDGET (section.remove_button));
	setup_section_button (name_selector_dialog, section.remove_button, 0.5, _("_Remove"), "go-previous", TRUE);
	gtk_widget_set_sensitive (GTK_WIDGET (section.remove_button), FALSE);

	/* hgrid for label and scrolled window. This is a separate hgrid, instead
	 * of just using the section.section_grid directly, as it has a different
	 * spacing.
	 */

	hgrid = g_object_new (
		GTK_TYPE_GRID,
		"orientation", GTK_ORIENTATION_HORIZONTAL,
		"row-homogeneous", FALSE,
		"column-spacing", 6,
		"vexpand", TRUE,
		"valign", GTK_ALIGN_FILL,
		NULL);
	gtk_container_add (GTK_CONTAINER (section.section_grid), hgrid);

	/* Title label */

	gtk_size_group_add_widget (priv->dest_label_size_group, GTK_WIDGET (section.label));

	gtk_misc_set_alignment (GTK_MISC (section.label), 0.0, 0.0);
	gtk_container_add (GTK_CONTAINER (hgrid), GTK_WIDGET (section.label));

	/* Treeview in a scrolled window */
	scrollwin = gtk_scrolled_window_new (NULL, NULL);
	gtk_container_add (GTK_CONTAINER (hgrid), scrollwin);
	gtk_widget_set_hexpand (scrollwin, TRUE);
	gtk_widget_set_halign (scrollwin, GTK_ALIGN_FILL);
	gtk_widget_set_vexpand (scrollwin, TRUE);
	gtk_widget_set_valign (scrollwin, GTK_ALIGN_FILL);
	gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrollwin), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
	gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (scrollwin), GTK_SHADOW_IN);
	gtk_container_add (GTK_CONTAINER (scrollwin), GTK_WIDGET (section.destination_view));

	/*data for 'changed' callback*/
	data = g_malloc0 (sizeof (SelData));
	data->view = section.destination_view;
	data->button = section.remove_button;
	g_object_set_data_full ((GObject *) section.destination_view, "sel-change-data", data, g_free);
	selection = gtk_tree_view_get_selection (section.destination_view);
	gtk_tree_selection_set_mode (selection, GTK_SELECTION_MULTIPLE);

	g_signal_connect (
		selection, "changed",
		G_CALLBACK (selection_changed), data);

	g_signal_connect_swapped (
		section.destination_view, "row-activated",
		G_CALLBACK (destination_activated), name_selector_dialog);
	g_signal_connect_swapped (
		section.destination_view, "key-press-event",
		G_CALLBACK (destination_key_press), name_selector_dialog);

	/* Done! */

	gtk_widget_show_all (GTK_WIDGET (section.section_grid));

	/* Pack this section's box into the dialog */
	gtk_container_add (GTK_CONTAINER (name_selector_dialog->priv->destination_vgrid), GTK_WIDGET (section.section_grid));
	g_object_set (
		G_OBJECT (section.section_grid),
		"vexpand", TRUE,
		"valign", GTK_ALIGN_FILL,
		NULL);

	g_array_append_val (name_selector_dialog->priv->sections, section);

	/* Make sure UI is consistent */
	contact_selection_changed (name_selector_dialog);

	return name_selector_dialog->priv->sections->len - 1;
}

static void
free_section (ENameSelectorDialog *name_selector_dialog,
              gint n)
{
	Section *section;

	g_return_if_fail (n >= 0);
	g_return_if_fail (n < name_selector_dialog->priv->sections->len);

	section = &g_array_index (
		name_selector_dialog->priv->sections, Section, n);

	g_free (section->name);
	gtk_widget_destroy (GTK_WIDGET (section->section_grid));
}

static void
model_section_added (ENameSelectorDialog *name_selector_dialog,
                     const gchar *name)
{
	gchar             *pretty_name;
	EDestinationStore *destination_store;

	e_name_selector_model_peek_section (
		name_selector_dialog->priv->name_selector_model,
		name, &pretty_name, &destination_store);
	add_section (name_selector_dialog, name, pretty_name, destination_store);
	g_free (pretty_name);
}

static void
model_section_removed (ENameSelectorDialog *name_selector_dialog,
                       const gchar *name)
{
	gint section_index;

	section_index = find_section_by_name (name_selector_dialog, name);
	g_return_if_fail (section_index >= 0);

	free_section (name_selector_dialog, section_index);
	g_array_remove_index (
		name_selector_dialog->priv->sections, section_index);
}

/* -------------------- *
 * Addressbook selector *
 * -------------------- */

static void
view_progress (EBookClientView *view,
               guint percent,
               const gchar *message,
               ENameSelectorDialog *dialog)
{
	if (message == NULL)
		gtk_label_set_text (dialog->priv->status_label, "");
	else
		gtk_label_set_text (dialog->priv->status_label, message);
}

static void
view_complete (EBookClientView *view,
               const GError *error,
               ENameSelectorDialog *dialog)
{
	view_progress (view, -1, NULL, dialog);

	enable_sort (dialog);
}

static void
start_client_view_cb (EContactStore *store,
                      EBookClientView *client_view,
                      ENameSelectorDialog *name_selector_dialog)
{
	g_signal_connect (
		client_view, "progress",
		G_CALLBACK (view_progress), name_selector_dialog);

	g_signal_connect (
		client_view, "complete",
		G_CALLBACK (view_complete), name_selector_dialog);
}

static void
stop_client_view_cb (EContactStore *store,
                     EBookClientView *client_view,
                     ENameSelectorDialog *name_selector_dialog)
{
	g_signal_handlers_disconnect_by_func (client_view, view_progress, name_selector_dialog);
	g_signal_handlers_disconnect_by_func (client_view, view_complete, name_selector_dialog);
}

static void
start_update_cb (EContactStore *store,
                 EBookClientView *client_view,
                 ENameSelectorDialog *name_selector_dialog)
{
	disable_sort (name_selector_dialog);
}

static void
stop_update_cb (EContactStore *store,
                EBookClientView *client_view,
                ENameSelectorDialog *name_selector_dialog)
{
	enable_sort (name_selector_dialog);
}

static void
name_selector_dialog_get_client_cb (GObject *source_object,
                                    GAsyncResult *result,
                                    gpointer user_data)
{
	ENameSelectorDialog *name_selector_dialog = user_data;
	EClient *client;
	EBookClient *book_client;
	EContactStore *store;
	ENameSelectorModel *model;
	GError *error = NULL;

	client = e_client_combo_box_get_client_finish (
		E_CLIENT_COMBO_BOX (source_object), result, &error);

	/* Sanity check. */
	g_return_if_fail (
		((client != NULL) && (error == NULL)) ||
		((client == NULL) && (error != NULL)));

	if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
		g_error_free (error);
		goto exit;
	}

	if (error != NULL) {
		gtk_label_set_text (
			name_selector_dialog->priv->status_label,
			error->message);
		g_error_free (error);
		goto exit;
	}

	book_client = E_BOOK_CLIENT (client);
	if (!book_client) {
		g_warn_if_fail (book_client != NULL);
		goto exit;
	}

	model = name_selector_dialog->priv->name_selector_model;
	store = e_name_selector_model_peek_contact_store (model);
	e_contact_store_add_client (store, book_client);
	g_object_unref (book_client);

 exit:
	g_object_unref (name_selector_dialog);
}

static void
source_changed (ENameSelectorDialog *name_selector_dialog,
                EClientComboBox *combo_box)
{
	GCancellable *cancellable;
	ESource *source;
	gpointer parent;

	source = e_source_combo_box_ref_active (E_SOURCE_COMBO_BOX (combo_box));

	parent = gtk_widget_get_toplevel (GTK_WIDGET (name_selector_dialog));
	parent = gtk_widget_is_toplevel (parent) ? parent : NULL;

	/* Remove any previous books being shown or loaded */
	remove_books (name_selector_dialog);

	if (source == NULL)
		return;

	cancellable = g_cancellable_new ();
	name_selector_dialog->priv->cancellable = cancellable;

	/* Connect to the selected source. */
	e_client_combo_box_get_client (
		combo_box, source, cancellable,
		name_selector_dialog_get_client_cb,
		g_object_ref (name_selector_dialog));

	g_object_unref (source);
}

/* --------------- *
 * Other UI events *
 * --------------- */

static void
search_changed (ENameSelectorDialog *name_selector_dialog)
{
	ENameSelectorDialogPrivate *priv = E_NAME_SELECTOR_DIALOG_GET_PRIVATE (name_selector_dialog);
	EContactStore *contact_store;
	EBookQuery    *book_query;
	GtkWidget     *combo_box;
	const gchar   *text;
	gchar         *text_escaped;
	gchar         *query_string;
	gchar         *category;
	gchar         *category_escaped;
	gchar         *user_fields_str;

	combo_box = priv->category_combobox;
	if (gtk_combo_box_get_active (GTK_COMBO_BOX (combo_box)) == -1)
		gtk_combo_box_set_active (GTK_COMBO_BOX (combo_box), 0);

	category = gtk_combo_box_text_get_active_text (GTK_COMBO_BOX_TEXT (combo_box));
	category_escaped = escape_sexp_string (category);

	text = gtk_entry_get_text (name_selector_dialog->priv->search_entry);
	text_escaped = escape_sexp_string (text);

	user_fields_str = ens_util_populate_user_query_fields (priv->user_query_fields, text, text_escaped);

	if (g_strcmp0 (category, _("Any Category")) == 0)
		query_string = g_strdup_printf (
			"(or (beginswith \"file_as\" %s) "
			"    (beginswith \"full_name\" %s) "
			"    (beginswith \"email\" %s) "
			"    (beginswith \"nickname\" %s)"
			"    (contains \"file_as\" %s) "
			"    (contains \"full_name\" %s) "
			"    (contains \"email\" %s) "
			"    (contains \"nickname\" %s)%s))",
			text_escaped, text_escaped,
			text_escaped, text_escaped,
			text_escaped, text_escaped,
			text_escaped, text_escaped,
			user_fields_str ? user_fields_str : "");
	else
		query_string = g_strdup_printf (
			"(and (is \"category_list\" %s) "
			"(or (beginswith \"file_as\" %s) "
			"    (beginswith \"full_name\" %s) "
			"    (beginswith \"email\" %s) "
			"    (beginswith \"nickname\" %s)"
			"    (contains \"file_as\" %s) "
			"    (contains \"full_name\" %s) "
			"    (contains \"email\" %s) "
			"    (contains \"nickname\" %s)%s))",
			category_escaped, text_escaped, text_escaped,
			text_escaped, text_escaped,
			text_escaped, text_escaped,
			text_escaped, text_escaped,
			user_fields_str ? user_fields_str : "");

	book_query = e_book_query_from_string (query_string);

	contact_store = e_name_selector_model_peek_contact_store (
		name_selector_dialog->priv->name_selector_model);
	e_contact_store_set_query (contact_store, book_query);
	e_book_query_unref (book_query);

	g_free (query_string);
	g_free (text_escaped);
	g_free (category_escaped);
	g_free (category);
	g_free (user_fields_str);
}

static void
contact_selection_changed (ENameSelectorDialog *name_selector_dialog)
{
	GtkTreeSelection *contact_selection;
	gboolean          have_selection = FALSE;
	gint              i;

	contact_selection = gtk_tree_view_get_selection (
		name_selector_dialog->priv->contact_view);
	if (gtk_tree_selection_count_selected_rows (contact_selection))
		have_selection = TRUE;

	for (i = 0; i < name_selector_dialog->priv->sections->len; i++) {
		Section *section = &g_array_index (
			name_selector_dialog->priv->sections, Section, i);
		gtk_widget_set_sensitive (GTK_WIDGET (section->transfer_button), have_selection);
	}
}

static void
contact_activated (ENameSelectorDialog *name_selector_dialog,
                   GtkTreePath *path)
{
	EContactStore     *contact_store;
	EDestinationStore *destination_store;
	EContact          *contact;
	GtkTreeIter       iter;
	Section           *section;
	gint               email_n;

	/* When a contact is activated, we transfer it to the first destination on our list */

	contact_store = e_name_selector_model_peek_contact_store (
		name_selector_dialog->priv->name_selector_model);

	/* If we have no sections, we can't transfer */
	if (name_selector_dialog->priv->sections->len == 0)
		return;

	/* Get the contact to be transferred */

	if (!gtk_tree_model_get_iter (
		GTK_TREE_MODEL (name_selector_dialog->priv->contact_sort),
		&iter, path))
		g_return_if_reached ();

	sort_iter_to_contact_store_iter (name_selector_dialog, &iter, &email_n);

	contact = e_contact_store_get_contact (contact_store, &iter);
	if (!contact) {
		g_warning ("ENameSelectorDialog could not get selected contact!");
		return;
	}

	section = &g_array_index (
		name_selector_dialog->priv->sections,
		Section, name_selector_dialog->priv->destination_index);
	if (!e_name_selector_model_peek_section (
		name_selector_dialog->priv->name_selector_model,
		section->name, NULL, &destination_store)) {
		g_warning ("ENameSelectorDialog has a section unknown to the model!");
		return;
	}

	add_destination (
		name_selector_dialog->priv->name_selector_model,
		destination_store, contact, email_n,
		e_contact_store_get_client (contact_store, &iter));
}

static void
destination_activated (ENameSelectorDialog *name_selector_dialog,
                       GtkTreePath *path,
                       GtkTreeViewColumn *column,
                       GtkTreeView *tree_view)
{
	gint               section_index;
	EDestinationStore *destination_store;
	EDestination      *destination;
	Section           *section;
	GtkTreeIter        iter;

	/* When a destination is activated, we remove it from the section */

	section_index = find_section_by_tree_view (
		name_selector_dialog, tree_view);
	if (section_index < 0) {
		g_warning ("ENameSelectorDialog got activation from unknown view!");
		return;
	}

	section = &g_array_index (
		name_selector_dialog->priv->sections, Section, section_index);
	if (!e_name_selector_model_peek_section (
		name_selector_dialog->priv->name_selector_model,
		section->name, NULL, &destination_store)) {
		g_warning ("ENameSelectorDialog has a section unknown to the model!");
		return;
	}

	if (!gtk_tree_model_get_iter (
		GTK_TREE_MODEL (destination_store), &iter, path))
		g_return_if_reached ();

	destination = e_destination_store_get_destination (
		destination_store, &iter);
	g_return_if_fail (destination);

	e_destination_store_remove_destination (
		destination_store, destination);
}

static gboolean
remove_selection (ENameSelectorDialog *name_selector_dialog,
                  GtkTreeView *tree_view)
{
	gint               section_index;
	EDestinationStore *destination_store;
	EDestination      *destination;
	Section           *section;
	GtkTreeSelection  *selection;
	GList		  *rows, *l;

	section_index = find_section_by_tree_view (
		name_selector_dialog, tree_view);
	if (section_index < 0) {
		g_warning ("ENameSelectorDialog got key press from unknown view!");
		return FALSE;
	}

	section = &g_array_index (
		name_selector_dialog->priv->sections, Section, section_index);
	if (!e_name_selector_model_peek_section (
		name_selector_dialog->priv->name_selector_model,
		section->name, NULL, &destination_store)) {
		g_warning ("ENameSelectorDialog has a section unknown to the model!");
		return FALSE;
	}

	selection = gtk_tree_view_get_selection (tree_view);
	if (!gtk_tree_selection_count_selected_rows (selection)) {
		g_warning ("ENameSelectorDialog remove button clicked, but no selection!");
		return FALSE;
	}

	rows = gtk_tree_selection_get_selected_rows (selection, NULL);
	rows = g_list_reverse (rows);

	for (l = rows; l; l = g_list_next (l)) {
		GtkTreeIter iter;
		GtkTreePath *path = l->data;

		if (!gtk_tree_model_get_iter (GTK_TREE_MODEL (destination_store), &iter, path))
			g_return_val_if_reached (FALSE);

		gtk_tree_path_free (path);

		destination = e_destination_store_get_destination (
			destination_store, &iter);
		g_return_val_if_fail (destination, FALSE);

		e_destination_store_remove_destination (
			destination_store, destination);
	}
	g_list_free (rows);

	return TRUE;
}

static void
remove_button_clicked (GtkButton *button,
                       SelData *data)
{
	GtkTreeView *view;
	ENameSelectorDialog *name_selector_dialog;

	view = data->view;
	name_selector_dialog = data->dlg_ptr;
	remove_selection (name_selector_dialog, view);
}

static gboolean
destination_key_press (ENameSelectorDialog *name_selector_dialog,
                       GdkEventKey *event,
                       GtkTreeView *tree_view)
{

	/* we only care about DEL key */
	if (event->keyval != GDK_KEY_Delete)
		return FALSE;
	return remove_selection (name_selector_dialog, tree_view);

}

static void
transfer_button_clicked (ENameSelectorDialog *name_selector_dialog,
                         GtkButton *transfer_button)
{
	EContactStore     *contact_store;
	EDestinationStore *destination_store;
	GtkTreeSelection  *selection;
	EContact          *contact;
	gint               section_index;
	Section           *section;
	gint               email_n;
	GList		  *rows, *l;

	/* Get the contact to be transferred */

	contact_store = e_name_selector_model_peek_contact_store (
		name_selector_dialog->priv->name_selector_model);
	selection = gtk_tree_view_get_selection (
		name_selector_dialog->priv->contact_view);

	if (!gtk_tree_selection_count_selected_rows (selection)) {
		g_warning ("ENameSelectorDialog transfer button clicked, but no selection!");
		return;
	}

	/* Get the target section */
	section_index = find_section_by_transfer_button (
		name_selector_dialog, transfer_button);
	if (section_index < 0) {
		g_warning ("ENameSelectorDialog got click from unknown button!");
		return;
	}

	section = &g_array_index (
		name_selector_dialog->priv->sections, Section, section_index);
	if (!e_name_selector_model_peek_section (
		name_selector_dialog->priv->name_selector_model,
		section->name, NULL, &destination_store)) {
		g_warning ("ENameSelectorDialog has a section unknown to the model!");
		return;
	}

	rows = gtk_tree_selection_get_selected_rows (selection, NULL);
	rows = g_list_reverse (rows);

	for (l = rows; l; l = g_list_next (l)) {
		GtkTreeIter iter;
		GtkTreePath *path = l->data;

		if (!gtk_tree_model_get_iter (
			GTK_TREE_MODEL (name_selector_dialog->priv->contact_sort),
			&iter, path)) {
			gtk_tree_path_free (path);
			return;
		}

		gtk_tree_path_free (path);
		sort_iter_to_contact_store_iter (name_selector_dialog, &iter, &email_n);

		contact = e_contact_store_get_contact (contact_store, &iter);
		if (!contact) {
			g_warning ("ENameSelectorDialog could not get selected contact!");
			g_list_free (rows);
			return;
		}

		add_destination (
			name_selector_dialog->priv->name_selector_model,
			destination_store, contact, email_n,
			e_contact_store_get_client (contact_store, &iter));
	}
	g_list_free (rows);
}

/* --------------------- *
 * Main model management *
 * --------------------- */

static void
setup_name_selector_model (ENameSelectorDialog *name_selector_dialog)
{
	EContactStore       *contact_store;
	GList               *new_sections;
	GList               *l;

	/* Create new destination sections in UI */

	new_sections = e_name_selector_model_list_sections (
		name_selector_dialog->priv->name_selector_model);

	for (l = new_sections; l; l = g_list_next (l)) {
		gchar             *name = l->data;
		gchar             *pretty_name;
		EDestinationStore *destination_store;

		e_name_selector_model_peek_section (
			name_selector_dialog->priv->name_selector_model,
			name, &pretty_name, &destination_store);

		add_section (name_selector_dialog, name, pretty_name, destination_store);

		g_free (pretty_name);
		g_free (name);
	}

	g_list_free (new_sections);

	/* Connect to section add/remove signals */

	g_signal_connect_swapped (
		name_selector_dialog->priv->name_selector_model, "section-added",
		G_CALLBACK (model_section_added), name_selector_dialog);
	g_signal_connect_swapped (
		name_selector_dialog->priv->name_selector_model, "section-removed",
		G_CALLBACK (model_section_removed), name_selector_dialog);

	contact_store = e_name_selector_model_peek_contact_store (name_selector_dialog->priv->name_selector_model);
	if (contact_store) {
		g_signal_connect (contact_store, "start-client-view", G_CALLBACK (start_client_view_cb), name_selector_dialog);
		g_signal_connect (contact_store, "stop-client-view", G_CALLBACK (stop_client_view_cb), name_selector_dialog);
		g_signal_connect (contact_store, "start-update", G_CALLBACK (start_update_cb), name_selector_dialog);
		g_signal_connect (contact_store, "stop-update", G_CALLBACK (stop_update_cb), name_selector_dialog);
	}

	/* Make sure UI is consistent */

	search_changed (name_selector_dialog);
	contact_selection_changed (name_selector_dialog);
}

static void
shutdown_name_selector_model (ENameSelectorDialog *name_selector_dialog)
{
	gint i;

	/* Rid UI of previous destination sections */

	for (i = 0; i < name_selector_dialog->priv->sections->len; i++)
		free_section (name_selector_dialog, i);

	g_array_set_size (name_selector_dialog->priv->sections, 0);

	/* Free sorting model */

	if (name_selector_dialog->priv->contact_sort) {
		g_object_unref (name_selector_dialog->priv->contact_sort);
		name_selector_dialog->priv->contact_sort = NULL;
	}

	/* Free backend model */

	if (name_selector_dialog->priv->name_selector_model) {
		EContactStore *contact_store;

		contact_store = e_name_selector_model_peek_contact_store (name_selector_dialog->priv->name_selector_model);
		if (contact_store) {
			g_signal_handlers_disconnect_by_func (contact_store, start_client_view_cb, name_selector_dialog);
			g_signal_handlers_disconnect_by_func (contact_store, stop_client_view_cb, name_selector_dialog);
			g_signal_handlers_disconnect_by_func (contact_store, start_update_cb, name_selector_dialog);
			g_signal_handlers_disconnect_by_func (contact_store, stop_update_cb, name_selector_dialog);
		}

		g_signal_handlers_disconnect_matched (
			name_selector_dialog->priv->name_selector_model,
			G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, name_selector_dialog);

		g_object_unref (name_selector_dialog->priv->name_selector_model);
		name_selector_dialog->priv->name_selector_model = NULL;
	}
}

static void
contact_column_formatter (GtkTreeViewColumn *column,
                          GtkCellRenderer *cell,
                          GtkTreeModel *model,
                          GtkTreeIter *iter,
                          ENameSelectorDialog *name_selector_dialog)
{
	EContactStore *contact_store;
	EContact      *contact;
	GtkTreeIter    contact_store_iter;
	GList         *email_list;
	gchar         *string;
	gchar         *full_name_str;
	gchar         *email_str;
	gint           email_n;

	contact_store_iter = *iter;
	sort_iter_to_contact_store_iter (
		name_selector_dialog, &contact_store_iter, &email_n);

	contact_store = e_name_selector_model_peek_contact_store (
		name_selector_dialog->priv->name_selector_model);
	contact = e_contact_store_get_contact (
		contact_store, &contact_store_iter);
	email_list = e_name_selector_model_get_contact_emails_without_used (
		name_selector_dialog->priv->name_selector_model, contact, TRUE);
	email_str = g_list_nth_data (email_list, email_n);
	full_name_str = e_contact_get (contact, E_CONTACT_FULL_NAME);

	if (e_contact_get (contact, E_CONTACT_IS_LIST)) {
		if (!full_name_str)
			full_name_str = e_contact_get (contact, E_CONTACT_FILE_AS);
		string = g_strdup_printf ("%s", full_name_str ? full_name_str : "?");
	} else {
		string = g_strdup_printf (
			"%s%s<%s>", full_name_str ? full_name_str : "",
			full_name_str ? " " : "",
			email_str ? email_str : "");
	}

	g_free (full_name_str);
	e_name_selector_model_free_emails_list (email_list);

	g_object_set (cell, "text", string, NULL);
	g_free (string);
}

static void
destination_column_formatter (GtkTreeViewColumn *column,
                              GtkCellRenderer *cell,
                              GtkTreeModel *model,
                              GtkTreeIter *iter,
                              ENameSelectorDialog *name_selector_dialog)
{
	EDestinationStore *destination_store = E_DESTINATION_STORE (model);
	EDestination      *destination;
	GString           *buffer;

	destination = e_destination_store_get_destination (destination_store, iter);
	g_return_if_fail (destination);

	buffer = g_string_new (e_destination_get_name (destination));

	if (!e_destination_is_evolution_list (destination)) {
		const gchar *email;

		email = e_destination_get_email (destination);
		if (email == NULL || *email == '\0')
			email = "?";
		g_string_append_printf (buffer, " <%s>", email);
	}

	g_object_set (cell, "text", buffer->str, NULL);
	g_string_free (buffer, TRUE);
}

/* ----------------------- *
 * ENameSelectorDialog API *
 * ----------------------- */

/**
 * e_name_selector_dialog_peek_model:
 * @name_selector_dialog: an #ENameSelectorDialog
 *
 * Gets the #ENameSelectorModel used by @name_selector_model.
 *
 * Returns: The #ENameSelectorModel being used.
 **/
ENameSelectorModel *
e_name_selector_dialog_peek_model (ENameSelectorDialog *name_selector_dialog)
{
	g_return_val_if_fail (E_IS_NAME_SELECTOR_DIALOG (name_selector_dialog), NULL);

	return name_selector_dialog->priv->name_selector_model;
}

/**
 * e_name_selector_dialog_set_model:
 * @name_selector_dialog: an #ENameSelectorDialog
 * @model: an #ENameSelectorModel
 *
 * Sets the model being used by @name_selector_dialog to @model.
 **/
void
e_name_selector_dialog_set_model (ENameSelectorDialog *name_selector_dialog,
                                  ENameSelectorModel *model)
{
	g_return_if_fail (E_IS_NAME_SELECTOR_DIALOG (name_selector_dialog));
	g_return_if_fail (E_IS_NAME_SELECTOR_MODEL (model));

	if (model == name_selector_dialog->priv->name_selector_model)
		return;

	shutdown_name_selector_model (name_selector_dialog);
	name_selector_dialog->priv->name_selector_model = g_object_ref (model);

	setup_name_selector_model (name_selector_dialog);
}

/**
 * e_name_selector_dialog_set_destination_index:
 * @name_selector_dialog: an #ENameSelectorDialog
 * @index: index of the destination section, starting from 0.
 *
 * Sets the index number of the destination section.
 **/
void
e_name_selector_dialog_set_destination_index (ENameSelectorDialog *name_selector_dialog,
                                              guint index)
{
	g_return_if_fail (E_IS_NAME_SELECTOR_DIALOG (name_selector_dialog));

	if (index >= name_selector_dialog->priv->sections->len)
		return;

	name_selector_dialog->priv->destination_index = index;
}

/**
 * e_name_selector_dialog_set_scrolling_policy:
 * @name_selector_dialog: an #ENameSelectorDialog
 * @hscrollbar_policy: scrolling policy for horizontal bar of the contacts window.
 * @vscrollbar_policy: scrolling policy for vertical bar of the contacts window.
 *
 * Sets the scrolling policy for the contacts section.
 *
 * Since: 3.2
 **/
void
e_name_selector_dialog_set_scrolling_policy (ENameSelectorDialog *name_selector_dialog,
                                             GtkPolicyType hscrollbar_policy,
                                             GtkPolicyType vscrollbar_policy)
{
	GtkScrolledWindow *win = GTK_SCROLLED_WINDOW (name_selector_dialog->priv->contact_window);

	gtk_scrolled_window_set_policy (win, hscrollbar_policy, vscrollbar_policy);
}

/**
 * e_name_selector_dialog_get_section_visible:
 * @name_selector_dialog: an #ENameSelectorDialog
 * @name: name of the section
 *
 * Returns: whether section named @name is visible in the dialog.
 *
 * Since: 3.8
 **/
gboolean
e_name_selector_dialog_get_section_visible (ENameSelectorDialog *name_selector_dialog,
                                            const gchar *name)
{
	Section *section;
	gint index;

	g_return_val_if_fail (E_IS_NAME_SELECTOR_DIALOG (name_selector_dialog), FALSE);
	g_return_val_if_fail (name != NULL, FALSE);

	index = find_section_by_name (name_selector_dialog, name);
	g_return_val_if_fail (index != -1, FALSE);

	section = &g_array_index (name_selector_dialog->priv->sections, Section, index);
	return gtk_widget_get_visible (GTK_WIDGET (section->section_grid));
}

/**
 * e_name_selector_dialog_set_section_visible:
 * @name_selector_dialog: an #ENameSelectorDialog
 * @name: name of the section
 * @visible: whether to show or hide the section
 *
 * Shows or hides section named @name in the dialog.
 *
 * Since: 3.8
 **/
void
e_name_selector_dialog_set_section_visible (ENameSelectorDialog *name_selector_dialog,
                                            const gchar *name,
                                            gboolean visible)
{
	Section *section;
	gint index;

	g_return_if_fail (E_IS_NAME_SELECTOR_DIALOG (name_selector_dialog));
	g_return_if_fail (name != NULL);

	index = find_section_by_name (name_selector_dialog, name);
	g_return_if_fail (index != -1);

	section = &g_array_index (name_selector_dialog->priv->sections, Section, index);

	if (visible)
		gtk_widget_show (GTK_WIDGET (section->section_grid));
	else
		gtk_widget_hide (GTK_WIDGET (section->section_grid));
}