aboutsummaryrefslogtreecommitdiffstats
path: root/e-util/e-name-selector-entry.c
diff options
context:
space:
mode:
authorMatthew Barnes <mbarnes@redhat.com>2012-12-10 21:09:59 +0800
committerMatthew Barnes <mbarnes@redhat.com>2012-12-13 03:33:43 +0800
commitd09d8de870b6697c8a8b262e7e077b871a69b315 (patch)
tree3b718882e7a0bb0a996daf2967a033d91714c9b5 /e-util/e-name-selector-entry.c
parentb61331ed03ac1c7a9b8614e25510040b9c60ae02 (diff)
downloadgsoc2013-evolution-d09d8de870b6697c8a8b262e7e077b871a69b315.tar
gsoc2013-evolution-d09d8de870b6697c8a8b262e7e077b871a69b315.tar.gz
gsoc2013-evolution-d09d8de870b6697c8a8b262e7e077b871a69b315.tar.bz2
gsoc2013-evolution-d09d8de870b6697c8a8b262e7e077b871a69b315.tar.lz
gsoc2013-evolution-d09d8de870b6697c8a8b262e7e077b871a69b315.tar.xz
gsoc2013-evolution-d09d8de870b6697c8a8b262e7e077b871a69b315.tar.zst
gsoc2013-evolution-d09d8de870b6697c8a8b262e7e077b871a69b315.zip
Consolidate base utility libraries into libeutil.
Evolution consists of entirely too many small utility libraries, which increases linking and loading time, places a burden on higher layers of the application (e.g. modules) which has to remember to link to all the small in-tree utility libraries, and makes it difficult to generate API documentation for these utility libraries in one Gtk-Doc module. Merge the following utility libraries under the umbrella of libeutil, and enforce a single-include policy on libeutil so we can reorganize the files as desired without disrupting its pseudo-public API. libemail-utils/libemail-utils.la libevolution-utils/libevolution-utils.la filter/libfilter.la widgets/e-timezone-dialog/libetimezonedialog.la widgets/menus/libmenus.la widgets/misc/libemiscwidgets.la widgets/table/libetable.la widgets/text/libetext.la This also merges libedataserverui from the Evolution-Data-Server module, since Evolution is its only consumer nowadays, and I'd like to make some improvements to those APIs without concern for backward-compatibility. And finally, start a Gtk-Doc module for libeutil. It's going to be a project just getting all the symbols _listed_ much less _documented_. But the skeletal structure is in place and I'm off to a good start.
Diffstat (limited to 'e-util/e-name-selector-entry.c')
-rw-r--r--e-util/e-name-selector-entry.c3541
1 files changed, 3541 insertions, 0 deletions
diff --git a/e-util/e-name-selector-entry.c b/e-util/e-name-selector-entry.c
new file mode 100644
index 0000000000..ea7e2ef383
--- /dev/null
+++ b/e-util/e-name-selector-entry.c
@@ -0,0 +1,3541 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+
+/* e-name-selector-entry.c - Single-line text entry widget for EDestinations.
+ *
+ * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of version 2 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 library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * Authors: Hans Petter Jansson <hpj@novell.com>
+ */
+
+#include <config.h>
+#include <string.h>
+#include <glib/gi18n-lib.h>
+
+#include <camel/camel.h>
+#include <libebackend/libebackend.h>
+
+#include "e-client-utils.h"
+#include "e-name-selector-entry.h"
+
+#define E_NAME_SELECTOR_ENTRY_GET_PRIVATE(obj) \
+ (G_TYPE_INSTANCE_GET_PRIVATE \
+ ((obj), E_TYPE_NAME_SELECTOR_ENTRY, ENameSelectorEntryPrivate))
+
+struct _ENameSelectorEntryPrivate {
+
+ ESourceRegistry *registry;
+ gint minimum_query_length;
+ gboolean show_address;
+
+ PangoAttrList *attr_list;
+ EContactStore *contact_store;
+ ETreeModelGenerator *email_generator;
+ EDestinationStore *destination_store;
+ GtkEntryCompletion *entry_completion;
+
+ guint type_ahead_complete_cb_id;
+ guint update_completions_cb_id;
+
+ EDestination *popup_destination;
+
+ gpointer (*contact_editor_func) (EBookClient *,
+ EContact *,
+ gboolean,
+ gboolean);
+ gpointer (*contact_list_editor_func)
+ (EBookClient *,
+ EContact *,
+ gboolean,
+ gboolean);
+
+ gboolean is_completing;
+ GSList *user_query_fields;
+
+ /* For asynchronous operations. */
+ GQueue cancellables;
+};
+
+enum {
+ PROP_0,
+ PROP_REGISTRY,
+ PROP_MINIMUM_QUERY_LENGTH,
+ PROP_SHOW_ADDRESS
+};
+
+enum {
+ UPDATED,
+ LAST_SIGNAL
+};
+
+static guint signals[LAST_SIGNAL] = { 0 };
+#define ENS_DEBUG(x)
+
+G_DEFINE_TYPE_WITH_CODE (
+ ENameSelectorEntry,
+ e_name_selector_entry,
+ GTK_TYPE_ENTRY,
+ G_IMPLEMENT_INTERFACE (
+ E_TYPE_EXTENSIBLE, NULL))
+
+/* 1/3 of the second to wait until invoking autocomplete lookup */
+#define AUTOCOMPLETE_TIMEOUT 333
+
+#define re_set_timeout(id,func,ptr) \
+ if (id) \
+ g_source_remove (id); \
+ id = g_timeout_add (AUTOCOMPLETE_TIMEOUT, \
+ (GSourceFunc) func, ptr);
+
+static void destination_row_inserted (ENameSelectorEntry *name_selector_entry, GtkTreePath *path, GtkTreeIter *iter);
+static void destination_row_changed (ENameSelectorEntry *name_selector_entry, GtkTreePath *path, GtkTreeIter *iter);
+static void destination_row_deleted (ENameSelectorEntry *name_selector_entry, GtkTreePath *path);
+
+static void user_insert_text (ENameSelectorEntry *name_selector_entry, gchar *new_text, gint new_text_length, gint *position, gpointer user_data);
+static void user_delete_text (ENameSelectorEntry *name_selector_entry, gint start_pos, gint end_pos, gpointer user_data);
+
+static void setup_default_contact_store (ENameSelectorEntry *name_selector_entry);
+static void deep_free_list (GList *list);
+
+static void
+name_selector_entry_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ switch (property_id) {
+ case PROP_REGISTRY:
+ e_name_selector_entry_set_registry (
+ E_NAME_SELECTOR_ENTRY (object),
+ g_value_get_object (value));
+ return;
+
+ case PROP_MINIMUM_QUERY_LENGTH:
+ e_name_selector_entry_set_minimum_query_length (
+ E_NAME_SELECTOR_ENTRY (object),
+ g_value_get_int (value));
+ return;
+
+ case PROP_SHOW_ADDRESS:
+ e_name_selector_entry_set_show_address (
+ E_NAME_SELECTOR_ENTRY (object),
+ g_value_get_boolean (value));
+ return;
+ }
+
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+name_selector_entry_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ switch (property_id) {
+ case PROP_REGISTRY:
+ g_value_set_object (
+ value,
+ e_name_selector_entry_get_registry (
+ E_NAME_SELECTOR_ENTRY (object)));
+ return;
+
+ case PROP_MINIMUM_QUERY_LENGTH:
+ g_value_set_int (
+ value,
+ e_name_selector_entry_get_minimum_query_length (
+ E_NAME_SELECTOR_ENTRY (object)));
+ return;
+
+ case PROP_SHOW_ADDRESS:
+ g_value_set_boolean (
+ value,
+ e_name_selector_entry_get_show_address (
+ E_NAME_SELECTOR_ENTRY (object)));
+ return;
+ }
+
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+name_selector_entry_dispose (GObject *object)
+{
+ ENameSelectorEntryPrivate *priv;
+
+ priv = E_NAME_SELECTOR_ENTRY_GET_PRIVATE (object);
+
+ if (priv->registry != NULL) {
+ g_object_unref (priv->registry);
+ priv->registry = NULL;
+ }
+
+ if (priv->attr_list != NULL) {
+ pango_attr_list_unref (priv->attr_list);
+ priv->attr_list = NULL;
+ }
+
+ if (priv->entry_completion) {
+ g_object_unref (priv->entry_completion);
+ priv->entry_completion = NULL;
+ }
+
+ if (priv->destination_store) {
+ g_object_unref (priv->destination_store);
+ priv->destination_store = NULL;
+ }
+
+ if (priv->email_generator) {
+ g_object_unref (priv->email_generator);
+ priv->email_generator = NULL;
+ }
+
+ if (priv->contact_store) {
+ g_object_unref (priv->contact_store);
+ priv->contact_store = NULL;
+ }
+
+ g_slist_foreach (priv->user_query_fields, (GFunc) g_free, NULL);
+ g_slist_free (priv->user_query_fields);
+ priv->user_query_fields = NULL;
+
+ /* Cancel any stuck book loading operations. */
+ while (!g_queue_is_empty (&priv->cancellables)) {
+ GCancellable *cancellable;
+
+ cancellable = g_queue_pop_head (&priv->cancellables);
+ g_cancellable_cancel (cancellable);
+ g_object_unref (cancellable);
+ }
+
+ /* Chain up to parent's dispose() method. */
+ G_OBJECT_CLASS (e_name_selector_entry_parent_class)->dispose (object);
+}
+
+static void
+name_selector_entry_constructed (GObject *object)
+{
+ /* Chain up to parent's constructed() method. */
+ G_OBJECT_CLASS (e_name_selector_entry_parent_class)->
+ constructed (object);
+
+ e_extensible_load_extensions (E_EXTENSIBLE (object));
+}
+
+static void
+name_selector_entry_realize (GtkWidget *widget)
+{
+ ENameSelectorEntryPrivate *priv;
+
+ priv = E_NAME_SELECTOR_ENTRY_GET_PRIVATE (widget);
+
+ /* Chain up to parent's realize() method. */
+ GTK_WIDGET_CLASS (e_name_selector_entry_parent_class)->realize (widget);
+
+ if (priv->contact_store == NULL)
+ setup_default_contact_store (E_NAME_SELECTOR_ENTRY (widget));
+}
+
+static void
+name_selector_entry_drag_data_received (GtkWidget *widget,
+ GdkDragContext *context,
+ gint x,
+ gint y,
+ GtkSelectionData *selection_data,
+ guint info,
+ guint time)
+{
+ CamelInternetAddress *address;
+ gint n_addresses = 0;
+ gchar *text;
+
+ address = camel_internet_address_new ();
+ text = (gchar *) gtk_selection_data_get_text (selection_data);
+
+ /* See if Camel can parse a valid email address from the text. */
+ if (text != NULL && *text != '\0') {
+ camel_url_decode (text);
+ if (g_ascii_strncasecmp (text, "mailto:", 7) == 0)
+ n_addresses = camel_address_decode (
+ CAMEL_ADDRESS (address), text + 7);
+ else
+ n_addresses = camel_address_decode (
+ CAMEL_ADDRESS (address), text);
+ }
+
+ if (n_addresses > 0) {
+ GtkEditable *editable;
+ GdkDragAction action;
+ gboolean delete;
+ gint position;
+
+ editable = GTK_EDITABLE (widget);
+ gtk_editable_set_position (editable, -1);
+ position = gtk_editable_get_position (editable);
+
+ g_free (text);
+
+ text = camel_address_format (CAMEL_ADDRESS (address));
+ gtk_editable_insert_text (editable, text, -1, &position);
+
+ action = gdk_drag_context_get_selected_action (context);
+ delete = (action == GDK_ACTION_MOVE);
+ gtk_drag_finish (context, TRUE, delete, time);
+ }
+
+ g_object_unref (address);
+ g_free (text);
+
+ if (n_addresses <= 0)
+ /* Chain up to parent's drag_data_received() method. */
+ GTK_WIDGET_CLASS (e_name_selector_entry_parent_class)->
+ drag_data_received (
+ widget, context, x, y,
+ selection_data, info, time);
+}
+
+static void
+e_name_selector_entry_class_init (ENameSelectorEntryClass *class)
+{
+ GObjectClass *object_class;
+ GtkWidgetClass *widget_class;
+
+ g_type_class_add_private (class, sizeof (ENameSelectorEntryPrivate));
+
+ object_class = G_OBJECT_CLASS (class);
+ object_class->set_property = name_selector_entry_set_property;
+ object_class->get_property = name_selector_entry_get_property;
+ object_class->dispose = name_selector_entry_dispose;
+ object_class->constructed = name_selector_entry_constructed;
+
+ widget_class = GTK_WIDGET_CLASS (class);
+ widget_class->realize = name_selector_entry_realize;
+ widget_class->drag_data_received = name_selector_entry_drag_data_received;
+
+ g_object_class_install_property (
+ object_class,
+ PROP_REGISTRY,
+ g_param_spec_object (
+ "registry",
+ "Registry",
+ "Data source registry",
+ E_TYPE_SOURCE_REGISTRY,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT |
+ G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property (
+ object_class,
+ PROP_MINIMUM_QUERY_LENGTH,
+ g_param_spec_int (
+ "minimum-query-length",
+ "Minimum Query Length",
+ NULL,
+ 1, G_MAXINT,
+ 3,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property (
+ object_class,
+ PROP_SHOW_ADDRESS,
+ g_param_spec_boolean (
+ "show-address",
+ "Show Address",
+ NULL,
+ FALSE,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ signals[UPDATED] = g_signal_new (
+ "updated",
+ E_TYPE_NAME_SELECTOR_ENTRY,
+ G_SIGNAL_RUN_FIRST,
+ G_STRUCT_OFFSET (ENameSelectorEntryClass, updated),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__POINTER,
+ G_TYPE_NONE, 1, G_TYPE_POINTER);
+}
+
+/* Remove unquoted commas and control characters from string */
+static gchar *
+sanitize_string (const gchar *string)
+{
+ GString *gstring;
+ gboolean quoted = FALSE;
+ const gchar *p;
+
+ gstring = g_string_new ("");
+
+ if (!string)
+ return g_string_free (gstring, FALSE);
+
+ for (p = string; *p; p = g_utf8_next_char (p)) {
+ gunichar c = g_utf8_get_char (p);
+
+ if (c == '"')
+ quoted = ~quoted;
+ else if (c == ',' && !quoted)
+ continue;
+ else if (c == '\t' || c == '\n')
+ continue;
+
+ g_string_append_unichar (gstring, c);
+ }
+
+ return g_string_free (gstring, FALSE);
+}
+
+/* Called for each list store entry whenever the user types (but not on cut/paste) */
+static gboolean
+completion_match_cb (GtkEntryCompletion *completion,
+ const gchar *key,
+ GtkTreeIter *iter,
+ gpointer user_data)
+{
+ ENS_DEBUG (g_print ("completion_match_cb, key=%s\n", key));
+
+ return TRUE;
+}
+
+/* Gets context of n_unichars total (n_unicars / 2, before and after position)
+ * and places them in array. If any positions would be outside the string, the
+ * corresponding unichars are set to zero. */
+static void
+get_utf8_string_context (const gchar *string,
+ gint position,
+ gunichar *unichars,
+ gint n_unichars)
+{
+ gchar *p = NULL;
+ gint len;
+ gint gap;
+ gint i;
+
+ /* n_unichars must be even */
+ g_assert (n_unichars % 2 == 0);
+
+ len = g_utf8_strlen (string, -1);
+ gap = n_unichars / 2;
+
+ for (i = 0; i < n_unichars; i++) {
+ gint char_pos = position - gap + i;
+
+ if (char_pos < 0 || char_pos >= len) {
+ unichars[i] = '\0';
+ continue;
+ }
+
+ if (p)
+ p = g_utf8_next_char (p);
+ else
+ p = g_utf8_offset_to_pointer (string, char_pos);
+
+ unichars[i] = g_utf8_get_char (p);
+ }
+}
+
+static gboolean
+get_range_at_position (const gchar *string,
+ gint pos,
+ gint *start_pos,
+ gint *end_pos)
+{
+ const gchar *p;
+ gboolean quoted = FALSE;
+ gint local_start_pos = 0;
+ gint local_end_pos = 0;
+ gint i;
+
+ if (!string || !*string)
+ return FALSE;
+
+ for (p = string, i = 0; *p; p = g_utf8_next_char (p), i++) {
+ gunichar c = g_utf8_get_char (p);
+
+ if (c == '"') {
+ quoted = ~quoted;
+ } else if (c == ',' && !quoted) {
+ if (i < pos) {
+ /* Start right after comma */
+ local_start_pos = i + 1;
+ } else {
+ /* Stop right before comma */
+ local_end_pos = i;
+ break;
+ }
+ } else if (c == ' ' && local_start_pos == i) {
+ /* Adjust start to skip space after first comma */
+ local_start_pos++;
+ }
+ }
+
+ /* If we didn't hit a comma, we must've hit NULL, and ours was the last element. */
+ if (!local_end_pos)
+ local_end_pos = i;
+
+ if (start_pos)
+ *start_pos = local_start_pos;
+ if (end_pos)
+ *end_pos = local_end_pos;
+
+ return TRUE;
+}
+
+static gboolean
+is_quoted_at (const gchar *string,
+ gint pos)
+{
+ const gchar *p;
+ gboolean quoted = FALSE;
+ gint i;
+
+ for (p = string, i = 0; *p && i < pos; p = g_utf8_next_char (p), i++) {
+ gunichar c = g_utf8_get_char (p);
+
+ if (c == '"')
+ quoted = ~quoted;
+ }
+
+ return quoted ? TRUE : FALSE;
+}
+
+static gint
+get_index_at_position (const gchar *string,
+ gint pos)
+{
+ const gchar *p;
+ gboolean quoted = FALSE;
+ gint n = 0;
+ gint i;
+
+ for (p = string, i = 0; *p && i < pos; p = g_utf8_next_char (p), i++) {
+ gunichar c = g_utf8_get_char (p);
+
+ if (c == '"')
+ quoted = ~quoted;
+ else if (c == ',' && !quoted)
+ n++;
+ }
+
+ return n;
+}
+
+static gboolean
+get_range_by_index (const gchar *string,
+ gint index,
+ gint *start_pos,
+ gint *end_pos)
+{
+ const gchar *p;
+ gboolean quoted = FALSE;
+ gint i;
+ gint n = 0;
+
+ for (p = string, i = 0; *p && n < index; p = g_utf8_next_char (p), i++) {
+ gunichar c = g_utf8_get_char (p);
+
+ if (c == '"')
+ quoted = ~quoted;
+ if (c == ',' && !quoted)
+ n++;
+ }
+
+ if (n < index)
+ return FALSE;
+
+ return get_range_at_position (string, i, start_pos, end_pos);
+}
+
+static gchar *
+get_address_at_position (const gchar *string,
+ gint pos)
+{
+ gint start_pos;
+ gint end_pos;
+ const gchar *start_p;
+ const gchar *end_p;
+
+ if (!get_range_at_position (string, pos, &start_pos, &end_pos))
+ return NULL;
+
+ start_p = g_utf8_offset_to_pointer (string, start_pos);
+ end_p = g_utf8_offset_to_pointer (string, end_pos);
+
+ return g_strndup (start_p, end_p - start_p);
+}
+
+/* Finds the destination in model */
+static EDestination *
+find_destination_by_index (ENameSelectorEntry *name_selector_entry,
+ gint index)
+{
+ GtkTreePath *path;
+ GtkTreeIter iter;
+
+ path = gtk_tree_path_new_from_indices (index, -1);
+ if (!gtk_tree_model_get_iter (GTK_TREE_MODEL (name_selector_entry->priv->destination_store),
+ &iter, path)) {
+ /* If we have zero destinations, getting a NULL destination at index 0
+ * is valid. */
+ if (index > 0)
+ g_warning ("ENameSelectorEntry is out of sync with model!");
+ gtk_tree_path_free (path);
+ return NULL;
+ }
+ gtk_tree_path_free (path);
+
+ return e_destination_store_get_destination (name_selector_entry->priv->destination_store, &iter);
+}
+
+/* Finds the destination in model */
+static EDestination *
+find_destination_at_position (ENameSelectorEntry *name_selector_entry,
+ gint pos)
+{
+ const gchar *text;
+ gint index;
+
+ text = gtk_entry_get_text (GTK_ENTRY (name_selector_entry));
+ index = get_index_at_position (text, pos);
+
+ return find_destination_by_index (name_selector_entry, index);
+}
+
+/* Builds destination from our text */
+static EDestination *
+build_destination_at_position (const gchar *string,
+ gint pos)
+{
+ EDestination *destination;
+ gchar *address;
+
+ address = get_address_at_position (string, pos);
+ if (!address)
+ return NULL;
+
+ destination = e_destination_new ();
+ e_destination_set_raw (destination, address);
+
+ g_free (address);
+ return destination;
+}
+
+static gchar *
+name_style_query (const gchar *field,
+ const gchar *value)
+{
+ gchar *spaced_str;
+ gchar *comma_str;
+ GString *out = g_string_new ("");
+ gchar **strv;
+ gchar *query;
+
+ spaced_str = sanitize_string (value);
+ g_strstrip (spaced_str);
+
+ strv = g_strsplit (spaced_str, " ", 0);
+
+ if (strv[0] && strv[1]) {
+ g_string_append (out, "(or ");
+ comma_str = g_strjoinv (", ", strv);
+ } else {
+ comma_str = NULL;
+ }
+
+ g_string_append (out, " (beginswith ");
+ e_sexp_encode_string (out, field);
+ e_sexp_encode_string (out, spaced_str);
+ g_string_append (out, ")");
+
+ if (comma_str) {
+ g_string_append (out, " (beginswith ");
+
+ e_sexp_encode_string (out, field);
+ g_strstrip (comma_str);
+ e_sexp_encode_string (out, comma_str);
+ g_string_append (out, "))");
+ }
+
+ query = g_string_free (out, FALSE);
+
+ g_free (spaced_str);
+ g_free (comma_str);
+ g_strfreev (strv);
+
+ return query;
+}
+
+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;
+}
+
+/**
+ * ens_util_populate_user_query_fields:
+ *
+ * Populates list of user query fields to string usable in query string.
+ * Returned pointer is either newly allocated string, supposed to be freed with g_free,
+ * or NULL if no fields defined.
+ *
+ * Since: 2.24
+ **/
+gchar *
+ens_util_populate_user_query_fields (GSList *user_query_fields,
+ const gchar *cue_str,
+ const gchar *encoded_cue_str)
+{
+ GString *user_fields;
+ GSList *s;
+
+ g_return_val_if_fail (cue_str != NULL, NULL);
+ g_return_val_if_fail (encoded_cue_str != NULL, NULL);
+
+ user_fields = g_string_new ("");
+
+ for (s = user_query_fields; s; s = s->next) {
+ const gchar *field = s->data;
+
+ if (!field || !*field)
+ continue;
+
+ if (*field == '$') {
+ g_string_append_printf (user_fields, " (beginswith \"%s\" %s) ", field + 1, encoded_cue_str);
+ } else if (*field == '@') {
+ g_string_append_printf (user_fields, " (is \"%s\" %s) ", field + 1, encoded_cue_str);
+ } else {
+ gchar *tmp = name_style_query (field, cue_str);
+
+ g_string_append (user_fields, " ");
+ g_string_append (user_fields, tmp);
+ g_string_append (user_fields, " ");
+ g_free (tmp);
+ }
+ }
+
+ return g_string_free (user_fields, !user_fields->str || !*user_fields->str);
+}
+
+static void
+set_completion_query (ENameSelectorEntry *name_selector_entry,
+ const gchar *cue_str)
+{
+ ENameSelectorEntryPrivate *priv;
+ EBookQuery *book_query;
+ gchar *query_str;
+ gchar *encoded_cue_str;
+ gchar *full_name_query_str;
+ gchar *file_as_query_str;
+ gchar *user_fields_str;
+
+ priv = E_NAME_SELECTOR_ENTRY_GET_PRIVATE (name_selector_entry);
+
+ if (!name_selector_entry->priv->contact_store)
+ return;
+
+ if (!cue_str) {
+ /* Clear the store */
+ e_contact_store_set_query (name_selector_entry->priv->contact_store, NULL);
+ return;
+ }
+
+ encoded_cue_str = escape_sexp_string (cue_str);
+ full_name_query_str = name_style_query ("full_name", cue_str);
+ file_as_query_str = name_style_query ("file_as", cue_str);
+ user_fields_str = ens_util_populate_user_query_fields (priv->user_query_fields, cue_str, encoded_cue_str);
+
+ query_str = g_strdup_printf (
+ "(or "
+ " (beginswith \"nickname\" %s) "
+ " (beginswith \"email\" %s) "
+ " %s "
+ " %s "
+ " %s "
+ ")",
+ encoded_cue_str, encoded_cue_str,
+ full_name_query_str, file_as_query_str,
+ user_fields_str ? user_fields_str : "");
+
+ g_free (user_fields_str);
+ g_free (file_as_query_str);
+ g_free (full_name_query_str);
+ g_free (encoded_cue_str);
+
+ ENS_DEBUG (g_print ("%s\n", query_str));
+
+ book_query = e_book_query_from_string (query_str);
+ e_contact_store_set_query (name_selector_entry->priv->contact_store, book_query);
+ e_book_query_unref (book_query);
+
+ g_free (query_str);
+}
+
+static gchar *
+get_entry_substring (ENameSelectorEntry *name_selector_entry,
+ gint range_start,
+ gint range_end)
+{
+ const gchar *entry_text;
+ gchar *p0, *p1;
+
+ entry_text = gtk_entry_get_text (GTK_ENTRY (name_selector_entry));
+
+ p0 = g_utf8_offset_to_pointer (entry_text, range_start);
+ p1 = g_utf8_offset_to_pointer (entry_text, range_end);
+
+ return g_strndup (p0, p1 - p0);
+}
+
+static gint
+utf8_casefold_collate_len (const gchar *str1,
+ const gchar *str2,
+ gint len)
+{
+ gchar *s1 = g_utf8_casefold (str1, len);
+ gchar *s2 = g_utf8_casefold (str2, len);
+ gint rv;
+
+ rv = g_utf8_collate (s1, s2);
+
+ g_free (s1);
+ g_free (s2);
+
+ return rv;
+}
+
+static gchar *
+build_textrep_for_contact (EContact *contact,
+ EContactField cue_field)
+{
+ gchar *name = NULL;
+ gchar *email = NULL;
+ gchar *textrep;
+
+ switch (cue_field) {
+ case E_CONTACT_FULL_NAME:
+ case E_CONTACT_NICKNAME:
+ case E_CONTACT_FILE_AS:
+ name = e_contact_get (contact, cue_field);
+ email = e_contact_get (contact, E_CONTACT_EMAIL_1);
+ break;
+
+ case E_CONTACT_EMAIL_1:
+ case E_CONTACT_EMAIL_2:
+ case E_CONTACT_EMAIL_3:
+ case E_CONTACT_EMAIL_4:
+ name = NULL;
+ email = e_contact_get (contact, cue_field);
+ break;
+
+ default:
+ g_assert_not_reached ();
+ break;
+ }
+
+ g_assert (email);
+ g_assert (strlen (email) > 0);
+
+ if (name)
+ textrep = g_strdup_printf ("%s <%s>", name, email);
+ else
+ textrep = g_strdup_printf ("%s", email);
+
+ g_free (name);
+ g_free (email);
+ return textrep;
+}
+
+static gboolean
+contact_match_cue (ENameSelectorEntry *name_selector_entry,
+ EContact *contact,
+ const gchar *cue_str,
+ EContactField *matched_field,
+ gint *matched_field_rank)
+{
+ EContactField fields[] = { E_CONTACT_FULL_NAME, E_CONTACT_NICKNAME, E_CONTACT_FILE_AS,
+ E_CONTACT_EMAIL_1, E_CONTACT_EMAIL_2, E_CONTACT_EMAIL_3,
+ E_CONTACT_EMAIL_4 };
+ gchar *email;
+ gboolean result = FALSE;
+ gint cue_len;
+ gint i;
+
+ g_assert (contact);
+ g_assert (cue_str);
+
+ if (g_utf8_strlen (cue_str, -1) < name_selector_entry->priv->minimum_query_length)
+ return FALSE;
+
+ cue_len = strlen (cue_str);
+
+ /* Make sure contact has an email address */
+ email = e_contact_get (contact, E_CONTACT_EMAIL_1);
+ if (!email || !*email) {
+ g_free (email);
+ return FALSE;
+ }
+ g_free (email);
+
+ for (i = 0; i < G_N_ELEMENTS (fields); i++) {
+ gchar *value;
+ gchar *value_sane;
+
+ /* Don't match e-mail addresses in contact lists */
+ if (e_contact_get (contact, E_CONTACT_IS_LIST) &&
+ fields[i] >= E_CONTACT_FIRST_EMAIL_ID &&
+ fields[i] <= E_CONTACT_LAST_EMAIL_ID)
+ continue;
+
+ value = e_contact_get (contact, fields[i]);
+ if (!value)
+ continue;
+
+ value_sane = sanitize_string (value);
+ g_free (value);
+
+ ENS_DEBUG (g_print ("Comparing '%s' to '%s'\n", value, cue_str));
+
+ if (!utf8_casefold_collate_len (value_sane, cue_str, cue_len)) {
+ if (matched_field)
+ *matched_field = fields [i];
+ if (matched_field_rank)
+ *matched_field_rank = i;
+
+ result = TRUE;
+ g_free (value_sane);
+ break;
+ }
+ g_free (value_sane);
+ }
+
+ return result;
+}
+
+static gboolean
+find_existing_completion (ENameSelectorEntry *name_selector_entry,
+ const gchar *cue_str,
+ EContact **contact,
+ gchar **text,
+ EContactField *matched_field,
+ EBookClient **book_client)
+{
+ GtkTreeIter iter;
+ EContact *best_contact = NULL;
+ gint best_field_rank = G_MAXINT;
+ EContactField best_field = 0;
+ EBookClient *best_book_client = NULL;
+
+ g_assert (cue_str);
+
+ if (!name_selector_entry->priv->contact_store)
+ return FALSE;
+
+ ENS_DEBUG (g_print ("Completing '%s'\n", cue_str));
+
+ if (!gtk_tree_model_get_iter_first (GTK_TREE_MODEL (name_selector_entry->priv->contact_store), &iter))
+ return FALSE;
+
+ do {
+ EContact *current_contact;
+ gint current_field_rank;
+ EContactField current_field;
+ gboolean matches;
+
+ current_contact = e_contact_store_get_contact (name_selector_entry->priv->contact_store, &iter);
+ if (!current_contact)
+ continue;
+
+ matches = contact_match_cue (name_selector_entry, current_contact, cue_str, &current_field, &current_field_rank);
+ if (matches && current_field_rank < best_field_rank) {
+ best_contact = current_contact;
+ best_field_rank = current_field_rank;
+ best_field = current_field;
+ best_book_client = e_contact_store_get_client (name_selector_entry->priv->contact_store, &iter);
+ }
+ } while (gtk_tree_model_iter_next (GTK_TREE_MODEL (name_selector_entry->priv->contact_store), &iter));
+
+ if (!best_contact)
+ return FALSE;
+
+ if (contact)
+ *contact = best_contact;
+ if (text)
+ *text = build_textrep_for_contact (best_contact, best_field);
+ if (matched_field)
+ *matched_field = best_field;
+ if (book_client)
+ *book_client = best_book_client;
+
+ return TRUE;
+}
+
+static void
+generate_attribute_list (ENameSelectorEntry *name_selector_entry)
+{
+ PangoLayout *layout;
+ PangoAttrList *attr_list;
+ const gchar *text;
+ gint i;
+
+ text = gtk_entry_get_text (GTK_ENTRY (name_selector_entry));
+ layout = gtk_entry_get_layout (GTK_ENTRY (name_selector_entry));
+
+ /* Set up the attribute list */
+
+ attr_list = pango_attr_list_new ();
+
+ if (name_selector_entry->priv->attr_list)
+ pango_attr_list_unref (name_selector_entry->priv->attr_list);
+
+ name_selector_entry->priv->attr_list = attr_list;
+
+ /* Parse the entry's text and apply attributes to real contacts */
+
+ for (i = 0; ; i++) {
+ EDestination *destination;
+ PangoAttribute *attr;
+ gint start_pos;
+ gint end_pos;
+
+ if (!get_range_by_index (text, i, &start_pos, &end_pos))
+ break;
+
+ destination = find_destination_at_position (name_selector_entry, start_pos);
+
+ /* Destination will be NULL if we have no entries */
+ if (!destination || !e_destination_get_contact (destination))
+ continue;
+
+ attr = pango_attr_underline_new (PANGO_UNDERLINE_SINGLE);
+ attr->start_index = g_utf8_offset_to_pointer (text, start_pos) - text;
+ attr->end_index = g_utf8_offset_to_pointer (text, end_pos) - text;
+ pango_attr_list_insert (attr_list, attr);
+ }
+
+ pango_layout_set_attributes (layout, attr_list);
+}
+
+static gboolean
+draw_event (ENameSelectorEntry *name_selector_entry)
+{
+ PangoLayout *layout;
+
+ layout = gtk_entry_get_layout (GTK_ENTRY (name_selector_entry));
+ pango_layout_set_attributes (layout, name_selector_entry->priv->attr_list);
+
+ return FALSE;
+}
+
+static void
+type_ahead_complete (ENameSelectorEntry *name_selector_entry)
+{
+ EContact *contact;
+ EBookClient *book_client = NULL;
+ EContactField matched_field;
+ EDestination *destination;
+ gint cursor_pos;
+ gint range_start = 0;
+ gint range_end = 0;
+ gint pos = 0;
+ gchar *textrep;
+ gint textrep_len;
+ gint range_len;
+ const gchar *text;
+ gchar *cue_str;
+ gchar *temp_str;
+ ENameSelectorEntryPrivate *priv;
+
+ priv = E_NAME_SELECTOR_ENTRY_GET_PRIVATE (name_selector_entry);
+
+ cursor_pos = gtk_editable_get_position (GTK_EDITABLE (name_selector_entry));
+ if (cursor_pos < 0)
+ return;
+
+ text = gtk_entry_get_text (GTK_ENTRY (name_selector_entry));
+ get_range_at_position (text, cursor_pos, &range_start, &range_end);
+ range_len = range_end - range_start;
+ if (range_len < priv->minimum_query_length)
+ return;
+
+ destination = find_destination_at_position (name_selector_entry, cursor_pos);
+
+ cue_str = get_entry_substring (name_selector_entry, range_start, range_end);
+ if (!find_existing_completion (name_selector_entry, cue_str, &contact,
+ &textrep, &matched_field, &book_client)) {
+ g_free (cue_str);
+ return;
+ }
+
+ temp_str = sanitize_string (textrep);
+ g_free (textrep);
+ textrep = temp_str;
+
+ textrep_len = g_utf8_strlen (textrep, -1);
+ pos = range_start;
+
+ g_signal_handlers_block_by_func (
+ name_selector_entry,
+ user_insert_text, name_selector_entry);
+ g_signal_handlers_block_by_func (
+ name_selector_entry,
+ user_delete_text, name_selector_entry);
+ g_signal_handlers_block_by_func (
+ name_selector_entry->priv->destination_store,
+ destination_row_changed, name_selector_entry);
+
+ if (textrep_len > range_len) {
+ gint i;
+
+ /* keep character's case as user types */
+ for (i = 0; textrep[i] && cue_str[i]; i++)
+ textrep[i] = cue_str[i];
+
+ gtk_editable_delete_text (
+ GTK_EDITABLE (name_selector_entry),
+ range_start, range_end);
+ gtk_editable_insert_text (
+ GTK_EDITABLE (name_selector_entry),
+ textrep, -1, &pos);
+ gtk_editable_select_region (
+ GTK_EDITABLE (name_selector_entry),
+ range_end, range_start + textrep_len);
+ priv->is_completing = TRUE;
+ }
+ g_free (cue_str);
+
+ if (contact && destination) {
+ gint email_n = 0;
+
+ if (matched_field >= E_CONTACT_FIRST_EMAIL_ID && matched_field <= E_CONTACT_LAST_EMAIL_ID)
+ email_n = matched_field - E_CONTACT_FIRST_EMAIL_ID;
+
+ e_destination_set_contact (destination, contact, email_n);
+ if (book_client)
+ e_destination_set_client (destination, book_client);
+ generate_attribute_list (name_selector_entry);
+ }
+
+ g_signal_handlers_unblock_by_func (
+ name_selector_entry->priv->destination_store,
+ destination_row_changed, name_selector_entry);
+ g_signal_handlers_unblock_by_func (name_selector_entry, user_delete_text, name_selector_entry);
+ g_signal_handlers_unblock_by_func (name_selector_entry, user_insert_text, name_selector_entry);
+
+ g_free (textrep);
+}
+
+static void
+clear_completion_model (ENameSelectorEntry *name_selector_entry)
+{
+ ENameSelectorEntryPrivate *priv;
+
+ priv = E_NAME_SELECTOR_ENTRY_GET_PRIVATE (name_selector_entry);
+
+ if (!name_selector_entry->priv->contact_store)
+ return;
+
+ e_contact_store_set_query (name_selector_entry->priv->contact_store, NULL);
+ priv->is_completing = FALSE;
+}
+
+static void
+update_completion_model (ENameSelectorEntry *name_selector_entry)
+{
+ const gchar *text;
+ gint cursor_pos;
+ gint range_start = 0;
+ gint range_end = 0;
+
+ text = gtk_entry_get_text (GTK_ENTRY (name_selector_entry));
+ cursor_pos = gtk_editable_get_position (GTK_EDITABLE (name_selector_entry));
+
+ if (cursor_pos >= 0)
+ get_range_at_position (text, cursor_pos, &range_start, &range_end);
+
+ if (range_end - range_start >= name_selector_entry->priv->minimum_query_length && cursor_pos == range_end) {
+ gchar *cue_str;
+
+ cue_str = get_entry_substring (name_selector_entry, range_start, range_end);
+ set_completion_query (name_selector_entry, cue_str);
+ g_free (cue_str);
+ } else {
+ /* N/A; Clear completion model */
+ clear_completion_model (name_selector_entry);
+ }
+}
+
+static gboolean
+type_ahead_complete_on_timeout_cb (ENameSelectorEntry *name_selector_entry)
+{
+ type_ahead_complete (name_selector_entry);
+ name_selector_entry->priv->type_ahead_complete_cb_id = 0;
+ return FALSE;
+}
+
+static gboolean
+update_completions_on_timeout_cb (ENameSelectorEntry *name_selector_entry)
+{
+ update_completion_model (name_selector_entry);
+ name_selector_entry->priv->update_completions_cb_id = 0;
+ return FALSE;
+}
+
+static void
+insert_destination_at_position (ENameSelectorEntry *name_selector_entry,
+ gint pos)
+{
+ EDestination *destination;
+ const gchar *text;
+ gint index;
+
+ text = gtk_entry_get_text (GTK_ENTRY (name_selector_entry));
+ index = get_index_at_position (text, pos);
+
+ destination = build_destination_at_position (text, pos);
+ g_assert (destination);
+
+ g_signal_handlers_block_by_func (
+ name_selector_entry->priv->destination_store,
+ destination_row_inserted, name_selector_entry);
+ e_destination_store_insert_destination (
+ name_selector_entry->priv->destination_store,
+ index, destination);
+ g_signal_handlers_unblock_by_func (
+ name_selector_entry->priv->destination_store,
+ destination_row_inserted, name_selector_entry);
+ g_object_unref (destination);
+}
+
+static void
+modify_destination_at_position (ENameSelectorEntry *name_selector_entry,
+ gint pos)
+{
+ EDestination *destination;
+ const gchar *text;
+ gchar *raw_address;
+ gboolean rebuild_attributes = FALSE;
+
+ destination = find_destination_at_position (name_selector_entry, pos);
+ if (!destination)
+ return;
+
+ text = gtk_entry_get_text (GTK_ENTRY (name_selector_entry));
+ raw_address = get_address_at_position (text, pos);
+ g_assert (raw_address);
+
+ if (e_destination_get_contact (destination))
+ rebuild_attributes = TRUE;
+
+ g_signal_handlers_block_by_func (
+ name_selector_entry->priv->destination_store,
+ destination_row_changed, name_selector_entry);
+ e_destination_set_raw (destination, raw_address);
+ g_signal_handlers_unblock_by_func (
+ name_selector_entry->priv->destination_store,
+ destination_row_changed, name_selector_entry);
+
+ g_free (raw_address);
+
+ if (rebuild_attributes)
+ generate_attribute_list (name_selector_entry);
+}
+
+static gchar *
+get_destination_textrep (ENameSelectorEntry *name_selector_entry,
+ EDestination *destination)
+{
+ gboolean show_email = e_name_selector_entry_get_show_address (name_selector_entry);
+ EContact *contact;
+
+ g_return_val_if_fail (destination != NULL, NULL);
+
+ contact = e_destination_get_contact (destination);
+
+ if (!show_email) {
+ if (contact && !e_contact_get (contact, E_CONTACT_IS_LIST)) {
+ GList *email_list;
+
+ email_list = e_contact_get (contact, E_CONTACT_EMAIL);
+ show_email = g_list_length (email_list) > 1;
+ deep_free_list (email_list);
+ }
+ }
+
+ /* do not show emails for contact lists even user forces it */
+ if (show_email && contact && e_contact_get (contact, E_CONTACT_IS_LIST))
+ show_email = FALSE;
+
+ return sanitize_string (e_destination_get_textrep (destination, show_email));
+}
+
+static void
+sync_destination_at_position (ENameSelectorEntry *name_selector_entry,
+ gint range_pos,
+ gint *cursor_pos)
+{
+ EDestination *destination;
+ const gchar *text;
+ gchar *address;
+ gint address_len;
+ gint range_start, range_end;
+
+ /* Get the destination we're looking at. Note that the entry may be empty, and so
+ * there may not be one. */
+ destination = find_destination_at_position (name_selector_entry, range_pos);
+ if (!destination)
+ return;
+
+ text = gtk_entry_get_text (GTK_ENTRY (name_selector_entry));
+ if (!get_range_at_position (text, range_pos, &range_start, &range_end)) {
+ g_warning ("ENameSelectorEntry is out of sync with model!");
+ return;
+ }
+
+ address = get_destination_textrep (name_selector_entry, destination);
+ address_len = g_utf8_strlen (address, -1);
+
+ if (cursor_pos) {
+ /* Update cursor placement */
+ if (*cursor_pos >= range_end)
+ *cursor_pos += address_len - (range_end - range_start);
+ else if (*cursor_pos > range_start)
+ *cursor_pos = range_start + address_len;
+ }
+
+ g_signal_handlers_block_by_func (name_selector_entry, user_insert_text, name_selector_entry);
+ g_signal_handlers_block_by_func (name_selector_entry, user_delete_text, name_selector_entry);
+
+ gtk_editable_delete_text (GTK_EDITABLE (name_selector_entry), range_start, range_end);
+ gtk_editable_insert_text (GTK_EDITABLE (name_selector_entry), address, -1, &range_start);
+
+ g_signal_handlers_unblock_by_func (name_selector_entry, user_delete_text, name_selector_entry);
+ g_signal_handlers_unblock_by_func (name_selector_entry, user_insert_text, name_selector_entry);
+
+ generate_attribute_list (name_selector_entry);
+ g_free (address);
+}
+
+static void
+remove_destination_by_index (ENameSelectorEntry *name_selector_entry,
+ gint index)
+{
+ EDestination *destination;
+
+ destination = find_destination_by_index (name_selector_entry, index);
+ if (destination) {
+ g_signal_handlers_block_by_func (
+ name_selector_entry->priv->destination_store,
+ destination_row_deleted, name_selector_entry);
+ e_destination_store_remove_destination (
+ name_selector_entry->priv->destination_store,
+ destination);
+ g_signal_handlers_unblock_by_func (
+ name_selector_entry->priv->destination_store,
+ destination_row_deleted, name_selector_entry);
+ }
+}
+
+static void
+post_insert_update (ENameSelectorEntry *name_selector_entry,
+ gint position)
+{
+ const gchar *text;
+ glong length;
+
+ text = gtk_entry_get_text (GTK_ENTRY (name_selector_entry));
+ length = g_utf8_strlen (text, -1);
+ text = g_utf8_next_char (text);
+
+ if (*text == '\0') {
+ /* First and only character, create initial destination. */
+ insert_destination_at_position (name_selector_entry, 0);
+ } else {
+ /* Modified an existing destination. */
+ modify_destination_at_position (name_selector_entry, position);
+ }
+
+ /* If editing within the string, regenerate attributes. */
+ if (position < length)
+ generate_attribute_list (name_selector_entry);
+}
+
+/* Returns the number of characters inserted */
+static gint
+insert_unichar (ENameSelectorEntry *name_selector_entry,
+ gint *pos,
+ gunichar c)
+{
+ const gchar *text;
+ gunichar str_context[4];
+ gchar buf[7];
+ gint len;
+
+ text = gtk_entry_get_text (GTK_ENTRY (name_selector_entry));
+ get_utf8_string_context (text, *pos, str_context, 4);
+
+ /* Space is not allowed:
+ * - Before or after another space.
+ * - At start of string. */
+
+ if (c == ' ' && (str_context[1] == ' ' || str_context[1] == '\0' || str_context[2] == ' '))
+ return 0;
+
+ /* Comma is not allowed:
+ * - After another comma.
+ * - At start of string. */
+
+ if (c == ',' && !is_quoted_at (text, *pos)) {
+ gint start_pos;
+ gint end_pos;
+ gboolean at_start = FALSE;
+ gboolean at_end = FALSE;
+
+ if (str_context[1] == ',' || str_context[1] == '\0')
+ return 0;
+
+ /* We do this so we can avoid disturbing destinations with completed contacts
+ * either before or after the destination being inserted. */
+ get_range_at_position (text, *pos, &start_pos, &end_pos);
+ if (*pos <= start_pos)
+ at_start = TRUE;
+ if (*pos >= end_pos)
+ at_end = TRUE;
+
+ /* Must insert comma first, so modify_destination_at_position can do its job
+ * correctly, splitting up the contact if necessary. */
+ gtk_editable_insert_text (GTK_EDITABLE (name_selector_entry), ", ", -1, pos);
+
+ /* Update model */
+ g_assert (*pos >= 2);
+
+ /* If we inserted the comma at the end of, or in the middle of, an existing
+ * address, add a new destination for what appears after comma. Else, we
+ * have to add a destination for what appears before comma (a blank one). */
+ if (at_end) {
+ /* End: Add last, sync first */
+ insert_destination_at_position (name_selector_entry, *pos);
+ sync_destination_at_position (name_selector_entry, *pos - 2, pos);
+ /* Sync generates the attributes list */
+ } else if (at_start) {
+ /* Start: Add first */
+ insert_destination_at_position (name_selector_entry, *pos - 2);
+ generate_attribute_list (name_selector_entry);
+ } else {
+ /* Middle: */
+ insert_destination_at_position (name_selector_entry, *pos);
+ modify_destination_at_position (name_selector_entry, *pos - 2);
+ generate_attribute_list (name_selector_entry);
+ }
+
+ return 2;
+ }
+
+ /* Generic case. Allowed spaces also end up here. */
+
+ len = g_unichar_to_utf8 (c, buf);
+ buf[len] = '\0';
+
+ gtk_editable_insert_text (GTK_EDITABLE (name_selector_entry), buf, -1, pos);
+
+ post_insert_update (name_selector_entry, *pos);
+
+ return 1;
+}
+
+static void
+user_insert_text (ENameSelectorEntry *name_selector_entry,
+ gchar *new_text,
+ gint new_text_length,
+ gint *position,
+ gpointer user_data)
+{
+ gint chars_inserted = 0;
+ gboolean fast_insert;
+
+ g_signal_handlers_block_by_func (name_selector_entry, user_insert_text, name_selector_entry);
+ g_signal_handlers_block_by_func (name_selector_entry, user_delete_text, name_selector_entry);
+
+ fast_insert =
+ (g_utf8_strchr (new_text, new_text_length, ' ') == NULL) &&
+ (g_utf8_strchr (new_text, new_text_length, ',') == NULL);
+
+ /* If the text to insert does not contain spaces or commas,
+ * insert all of it at once. This avoids confusing on-going
+ * input method behavior. */
+ if (fast_insert) {
+ gint old_position = *position;
+
+ gtk_editable_insert_text (
+ GTK_EDITABLE (name_selector_entry),
+ new_text, new_text_length, position);
+
+ chars_inserted = *position - old_position;
+ if (chars_inserted > 0)
+ post_insert_update (name_selector_entry, *position);
+
+ /* Otherwise, apply some rules as to where spaces and commas
+ * can be inserted, and insert a trailing space after comma. */
+ } else {
+ const gchar *cp;
+
+ for (cp = new_text; *cp; cp = g_utf8_next_char (cp)) {
+ gunichar uc = g_utf8_get_char (cp);
+ insert_unichar (name_selector_entry, position, uc);
+ chars_inserted++;
+ }
+ }
+
+ if (chars_inserted >= 1) {
+ /* If the user inserted one character, kick off completion */
+ re_set_timeout (name_selector_entry->priv->update_completions_cb_id, update_completions_on_timeout_cb, name_selector_entry);
+ re_set_timeout (name_selector_entry->priv->type_ahead_complete_cb_id, type_ahead_complete_on_timeout_cb, name_selector_entry);
+ }
+
+ g_signal_handlers_unblock_by_func (name_selector_entry, user_delete_text, name_selector_entry);
+ g_signal_handlers_unblock_by_func (name_selector_entry, user_insert_text, name_selector_entry);
+
+ g_signal_stop_emission_by_name (name_selector_entry, "insert_text");
+}
+
+static void
+user_delete_text (ENameSelectorEntry *name_selector_entry,
+ gint start_pos,
+ gint end_pos,
+ gpointer user_data)
+{
+ const gchar *text;
+ gint index_start, index_end;
+ gint selection_start, selection_end;
+ gunichar str_context[2], str_b_context[2];
+ gint len;
+ gint i;
+ gboolean del_space = FALSE, del_comma = FALSE;
+
+ if (start_pos == end_pos)
+ return;
+
+ text = gtk_entry_get_text (GTK_ENTRY (name_selector_entry));
+ len = g_utf8_strlen (text, -1);
+
+ if (end_pos == -1)
+ end_pos = len;
+
+ gtk_editable_get_selection_bounds (
+ GTK_EDITABLE (name_selector_entry),
+ &selection_start, &selection_end);
+
+ get_utf8_string_context (text, start_pos, str_context, 2);
+ get_utf8_string_context (text, end_pos, str_b_context, 2);
+
+ g_signal_handlers_block_by_func (name_selector_entry, user_delete_text, name_selector_entry);
+
+ if (end_pos - start_pos == 1) {
+ /* Might be backspace; update completion model so dropdown is accurate */
+ re_set_timeout (name_selector_entry->priv->update_completions_cb_id, update_completions_on_timeout_cb, name_selector_entry);
+ }
+
+ index_start = get_index_at_position (text, start_pos);
+ index_end = get_index_at_position (text, end_pos);
+
+ g_signal_stop_emission_by_name (name_selector_entry, "delete_text");
+
+ /* If the deletion touches more than one destination, the first one is changed
+ * and the rest are removed. If the last destination wasn't completely deleted,
+ * it becomes part of the first one, since the separator between them was
+ * removed.
+ *
+ * Here, we let the model know about removals. */
+ for (i = index_end; i > index_start; i--) {
+ EDestination *destination = find_destination_by_index (name_selector_entry, i);
+ gint range_start, range_end;
+ gchar *ttext;
+ const gchar *email = NULL;
+ gboolean sel = FALSE;
+
+ if (destination)
+ email = e_destination_get_textrep (destination, TRUE);
+
+ if (!email || !*email)
+ continue;
+
+ if (!get_range_by_index (text, i, &range_start, &range_end)) {
+ g_warning ("ENameSelectorEntry is out of sync with model!");
+ return;
+ }
+
+ if ((selection_start < range_start && selection_end > range_start) ||
+ (selection_end > range_start && selection_end < range_end))
+ sel = TRUE;
+
+ if (!sel) {
+ g_signal_handlers_block_by_func (name_selector_entry, user_insert_text, name_selector_entry);
+ g_signal_handlers_block_by_func (name_selector_entry, user_delete_text, name_selector_entry);
+
+ gtk_editable_delete_text (GTK_EDITABLE (name_selector_entry), range_start, range_end);
+
+ ttext = sanitize_string (email);
+ gtk_editable_insert_text (GTK_EDITABLE (name_selector_entry), ttext, -1, &range_start);
+ g_free (ttext);
+
+ g_signal_handlers_unblock_by_func (name_selector_entry, user_delete_text, name_selector_entry);
+ g_signal_handlers_unblock_by_func (name_selector_entry, user_insert_text, name_selector_entry);
+
+ }
+
+ remove_destination_by_index (name_selector_entry, i);
+ }
+
+ /* Do the actual deletion */
+
+ if (end_pos == start_pos +1 && index_end == index_start) {
+ /* We could be just deleting the empty text */
+ gchar *c;
+
+ /* Get the actual deleted text */
+ c = gtk_editable_get_chars (GTK_EDITABLE (name_selector_entry), start_pos, start_pos + 1);
+
+ if ( c[0] == ' ') {
+ /* If we are at the beginning or removing junk space, let us ignore it */
+ del_space = TRUE;
+ }
+ g_free (c);
+ } else if (end_pos == start_pos +1 && index_end == index_start + 1) {
+ /* We could be just deleting the empty text */
+ gchar *c;
+
+ /* Get the actual deleted text */
+ c = gtk_editable_get_chars (GTK_EDITABLE (name_selector_entry), start_pos, start_pos + 1);
+
+ if ( c[0] == ',' && !is_quoted_at (text, start_pos)) {
+ /* If we are at the beginning or removing junk space, let us ignore it */
+ del_comma = TRUE;
+ }
+ g_free (c);
+ }
+
+ if (del_comma) {
+ gint range_start=-1, range_end;
+ EDestination *dest = find_destination_by_index (name_selector_entry, index_end);
+ /* If we have deleted the last comma, let us autocomplete normally
+ */
+
+ if (dest && len - end_pos != 0) {
+
+ EDestination *destination1 = find_destination_by_index (name_selector_entry, index_start);
+ gchar *ttext;
+ const gchar *email = NULL;
+
+ if (destination1)
+ email = e_destination_get_textrep (destination1, TRUE);
+
+ if (email && *email) {
+
+ if (!get_range_by_index (text, i, &range_start, &range_end)) {
+ g_warning ("ENameSelectorEntry is out of sync with model!");
+ return;
+ }
+
+ g_signal_handlers_block_by_func (name_selector_entry, user_insert_text, name_selector_entry);
+ g_signal_handlers_block_by_func (name_selector_entry, user_delete_text, name_selector_entry);
+
+ gtk_editable_delete_text (GTK_EDITABLE (name_selector_entry), range_start, range_end);
+
+ ttext = sanitize_string (email);
+ gtk_editable_insert_text (GTK_EDITABLE (name_selector_entry), ttext, -1, &range_start);
+ g_free (ttext);
+
+ g_signal_handlers_unblock_by_func (name_selector_entry, user_delete_text, name_selector_entry);
+ g_signal_handlers_unblock_by_func (name_selector_entry, user_insert_text, name_selector_entry);
+ }
+
+ if (range_start != -1) {
+ start_pos = range_start;
+ end_pos = start_pos + 1;
+ gtk_editable_set_position (GTK_EDITABLE (name_selector_entry),start_pos);
+ }
+ }
+ }
+ gtk_editable_delete_text (
+ GTK_EDITABLE (name_selector_entry),
+ start_pos, end_pos);
+
+ /*If the user is deleting a '"' new destinations have to be created for ',' between the quoted text
+ Like "fd,ty,uy" is a one entity, but if you remove the quotes it has to be broken doan into 3 seperate
+ addresses.
+ */
+
+ if (str_b_context[1] == '"') {
+ const gchar *p;
+ gint j;
+ p = text + end_pos;
+ for (p = text + (end_pos - 1), j = end_pos - 1; *p && *p != '"' ; p = g_utf8_next_char (p), j++) {
+ gunichar c = g_utf8_get_char (p);
+ if (c == ',') {
+ insert_destination_at_position (name_selector_entry, j + 1);
+ }
+ }
+
+ }
+
+ /* Let model know about changes */
+ text = gtk_entry_get_text (GTK_ENTRY (name_selector_entry));
+ if (!*text || strlen (text) <= 0) {
+ /* If the entry was completely cleared, remove the initial destination too */
+ remove_destination_by_index (name_selector_entry, 0);
+ generate_attribute_list (name_selector_entry);
+ } else if (!del_space) {
+ modify_destination_at_position (name_selector_entry, start_pos);
+ }
+
+ /* If editing within the string, we need to regenerate attributes */
+ if (end_pos < len)
+ generate_attribute_list (name_selector_entry);
+
+ /* Prevent type-ahead completion */
+ if (name_selector_entry->priv->type_ahead_complete_cb_id) {
+ g_source_remove (name_selector_entry->priv->type_ahead_complete_cb_id);
+ name_selector_entry->priv->type_ahead_complete_cb_id = 0;
+ }
+
+ g_signal_handlers_unblock_by_func (name_selector_entry, user_delete_text, name_selector_entry);
+}
+
+static gboolean
+completion_match_selected (ENameSelectorEntry *name_selector_entry,
+ ETreeModelGenerator *email_generator_model,
+ GtkTreeIter *generator_iter)
+{
+ EContact *contact;
+ EBookClient *book_client;
+ EDestination *destination;
+ gint cursor_pos;
+ GtkTreeIter contact_iter;
+ gint email_n;
+
+ if (!name_selector_entry->priv->contact_store)
+ return FALSE;
+
+ g_return_val_if_fail (name_selector_entry->priv->email_generator == email_generator_model, FALSE);
+
+ e_tree_model_generator_convert_iter_to_child_iter (
+ email_generator_model,
+ &contact_iter, &email_n,
+ generator_iter);
+
+ contact = e_contact_store_get_contact (name_selector_entry->priv->contact_store, &contact_iter);
+ book_client = e_contact_store_get_client (name_selector_entry->priv->contact_store, &contact_iter);
+ cursor_pos = gtk_editable_get_position (GTK_EDITABLE (name_selector_entry));
+
+ /* Set the contact in the model's destination */
+
+ destination = find_destination_at_position (name_selector_entry, cursor_pos);
+ e_destination_set_contact (destination, contact, email_n);
+ if (book_client)
+ e_destination_set_client (destination, book_client);
+ sync_destination_at_position (name_selector_entry, cursor_pos, &cursor_pos);
+
+ g_signal_handlers_block_by_func (name_selector_entry, user_insert_text, name_selector_entry);
+ gtk_editable_insert_text (GTK_EDITABLE (name_selector_entry), ", ", -1, &cursor_pos);
+ g_signal_handlers_unblock_by_func (name_selector_entry, user_insert_text, name_selector_entry);
+
+ /*Add destination at end for next entry*/
+ insert_destination_at_position (name_selector_entry, cursor_pos);
+ /* Place cursor at end of address */
+
+ gtk_editable_set_position (GTK_EDITABLE (name_selector_entry), cursor_pos);
+ g_signal_emit (name_selector_entry, signals[UPDATED], 0, destination, NULL);
+ return TRUE;
+}
+
+static void
+entry_activate (ENameSelectorEntry *name_selector_entry)
+{
+ gint cursor_pos;
+ gint range_start, range_end;
+ ENameSelectorEntryPrivate *priv;
+ EDestination *destination;
+ gint range_len;
+ const gchar *text;
+ gchar *cue_str;
+
+ cursor_pos = gtk_editable_get_position (GTK_EDITABLE (name_selector_entry));
+ if (cursor_pos < 0)
+ return;
+
+ priv = E_NAME_SELECTOR_ENTRY_GET_PRIVATE (name_selector_entry);
+
+ text = gtk_entry_get_text (GTK_ENTRY (name_selector_entry));
+ if (!get_range_at_position (text, cursor_pos, &range_start, &range_end))
+ return;
+
+ range_len = range_end - range_start;
+ if (range_len < priv->minimum_query_length)
+ return;
+
+ destination = find_destination_at_position (name_selector_entry, cursor_pos);
+ if (!destination)
+ return;
+
+ cue_str = get_entry_substring (name_selector_entry, range_start, range_end);
+#if 0
+ if (!find_existing_completion (name_selector_entry, cue_str, &contact,
+ &textrep, &matched_field)) {
+ g_free (cue_str);
+ return;
+ }
+#endif
+ g_free (cue_str);
+ sync_destination_at_position (name_selector_entry, cursor_pos, &cursor_pos);
+
+ /* Place cursor at end of address */
+ text = gtk_entry_get_text (GTK_ENTRY (name_selector_entry));
+ get_range_at_position (text, cursor_pos, &range_start, &range_end);
+
+ if (priv->is_completing) {
+ gchar *str_context = NULL;
+
+ str_context = gtk_editable_get_chars (GTK_EDITABLE (name_selector_entry), range_end, range_end + 1);
+
+ if (str_context[0] != ',') {
+ /* At the end*/
+ gtk_editable_insert_text (GTK_EDITABLE (name_selector_entry), ", ", -1, &range_end);
+ } else {
+ /* In the middle */
+ gint newpos = strlen (text);
+
+ /* Doing this we can make sure that It wont ask for completion again. */
+ gtk_editable_insert_text (GTK_EDITABLE (name_selector_entry), ", ", -1, &newpos);
+ g_signal_handlers_block_by_func (name_selector_entry, user_delete_text, name_selector_entry);
+ gtk_editable_delete_text (GTK_EDITABLE (name_selector_entry), newpos - 2, newpos);
+ g_signal_handlers_unblock_by_func (name_selector_entry, user_delete_text, name_selector_entry);
+
+ /* Move it close to next destination*/
+ range_end = range_end + 2;
+
+ }
+ g_free (str_context);
+ }
+
+ gtk_editable_set_position (GTK_EDITABLE (name_selector_entry), range_end);
+ g_signal_emit (name_selector_entry, signals[UPDATED], 0, destination, NULL);
+
+ if (priv->is_completing)
+ clear_completion_model (name_selector_entry);
+}
+
+static void
+update_text (ENameSelectorEntry *name_selector_entry,
+ const gchar *text)
+{
+ gint start = 0, end = 0;
+ gboolean has_selection;
+
+ has_selection = gtk_editable_get_selection_bounds (GTK_EDITABLE (name_selector_entry), &start, &end);
+
+ gtk_entry_set_text (GTK_ENTRY (name_selector_entry), text);
+
+ if (has_selection)
+ gtk_editable_select_region (GTK_EDITABLE (name_selector_entry), start, end);
+}
+
+static void
+sanitize_entry (ENameSelectorEntry *name_selector_entry)
+{
+ gint n;
+ GList *l, *known, *del = NULL;
+ GString *str = g_string_new ("");
+
+ g_signal_handlers_block_matched (name_selector_entry, G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, name_selector_entry);
+ g_signal_handlers_block_matched (name_selector_entry->priv->destination_store, G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, name_selector_entry);
+
+ known = e_destination_store_list_destinations (name_selector_entry->priv->destination_store);
+ for (l = known, n = 0; l != NULL; l = l->next, n++) {
+ EDestination *dest = l->data;
+
+ if (!dest || !e_destination_get_address (dest))
+ del = g_list_prepend (del, GINT_TO_POINTER (n));
+ else {
+ gchar *text;
+
+ text = get_destination_textrep (name_selector_entry, dest);
+ if (text) {
+ if (str->str && str->str[0])
+ g_string_append (str, ", ");
+
+ g_string_append (str, text);
+ }
+ g_free (text);
+ }
+ }
+ g_list_free (known);
+
+ for (l = del; l != NULL; l = l->next) {
+ e_destination_store_remove_destination_nth (name_selector_entry->priv->destination_store, GPOINTER_TO_INT (l->data));
+ }
+ g_list_free (del);
+
+ update_text (name_selector_entry, str->str);
+
+ g_string_free (str, TRUE);
+
+ g_signal_handlers_unblock_matched (name_selector_entry->priv->destination_store, G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, name_selector_entry);
+ g_signal_handlers_unblock_matched (name_selector_entry, G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, name_selector_entry);
+
+ generate_attribute_list (name_selector_entry);
+}
+
+static gboolean
+user_focus_in (ENameSelectorEntry *name_selector_entry,
+ GdkEventFocus *event_focus)
+{
+ gint n;
+ GList *l, *known;
+ GString *str = g_string_new ("");
+ EDestination *dest_dummy = e_destination_new ();
+
+ g_signal_handlers_block_matched (name_selector_entry, G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, name_selector_entry);
+ g_signal_handlers_block_matched (name_selector_entry->priv->destination_store, G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, name_selector_entry);
+
+ known = e_destination_store_list_destinations (name_selector_entry->priv->destination_store);
+ for (l = known, n = 0; l != NULL; l = l->next, n++) {
+ EDestination *dest = l->data;
+
+ if (dest) {
+ gchar *text;
+
+ text = get_destination_textrep (name_selector_entry, dest);
+ if (text) {
+ if (str->str && str->str[0])
+ g_string_append (str, ", ");
+
+ g_string_append (str, text);
+ }
+ g_free (text);
+ }
+ }
+ g_list_free (known);
+
+ /* Add a blank destination */
+ e_destination_store_append_destination (name_selector_entry->priv->destination_store, dest_dummy);
+ if (str->str && str->str[0])
+ g_string_append (str, ", ");
+
+ gtk_entry_set_text (GTK_ENTRY (name_selector_entry), str->str);
+
+ g_string_free (str, TRUE);
+
+ g_signal_handlers_unblock_matched (name_selector_entry->priv->destination_store, G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, name_selector_entry);
+ g_signal_handlers_unblock_matched (name_selector_entry, G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, name_selector_entry);
+
+ generate_attribute_list (name_selector_entry);
+
+ return FALSE;
+}
+
+static gboolean
+user_focus_out (ENameSelectorEntry *name_selector_entry,
+ GdkEventFocus *event_focus)
+{
+ if (!event_focus->in) {
+ entry_activate (name_selector_entry);
+ }
+
+ if (name_selector_entry->priv->type_ahead_complete_cb_id) {
+ g_source_remove (name_selector_entry->priv->type_ahead_complete_cb_id);
+ name_selector_entry->priv->type_ahead_complete_cb_id = 0;
+ }
+
+ if (name_selector_entry->priv->update_completions_cb_id) {
+ g_source_remove (name_selector_entry->priv->update_completions_cb_id);
+ name_selector_entry->priv->update_completions_cb_id = 0;
+ }
+
+ clear_completion_model (name_selector_entry);
+
+ if (!event_focus->in) {
+ sanitize_entry (name_selector_entry);
+ }
+
+ return FALSE;
+}
+
+static void
+deep_free_list (GList *list)
+{
+ GList *l;
+
+ for (l = list; l; l = g_list_next (l))
+ g_free (l->data);
+
+ g_list_free (list);
+}
+
+/* Given a widget, determines the height that text will normally be drawn. */
+static guint
+entry_height (GtkWidget *widget)
+{
+ PangoLayout *layout;
+ gint bound;
+
+ g_return_val_if_fail (widget != NULL, 0);
+
+ layout = gtk_widget_create_pango_layout (widget, NULL);
+
+ pango_layout_get_pixel_size (layout, NULL, &bound);
+
+ return bound;
+}
+
+static void
+contact_layout_pixbuffer (GtkCellLayout *cell_layout,
+ GtkCellRenderer *cell,
+ GtkTreeModel *model,
+ GtkTreeIter *iter,
+ ENameSelectorEntry *name_selector_entry)
+{
+ EContact *contact;
+ GtkTreeIter generator_iter;
+ GtkTreeIter contact_store_iter;
+ gint email_n;
+ EContactPhoto *photo;
+ GdkPixbuf *pixbuf = NULL;
+
+ if (!name_selector_entry->priv->contact_store)
+ return;
+
+ gtk_tree_model_filter_convert_iter_to_child_iter (
+ GTK_TREE_MODEL_FILTER (model),
+ &generator_iter, iter);
+ e_tree_model_generator_convert_iter_to_child_iter (
+ name_selector_entry->priv->email_generator,
+ &contact_store_iter, &email_n,
+ &generator_iter);
+
+ contact = e_contact_store_get_contact (name_selector_entry->priv->contact_store, &contact_store_iter);
+ if (!contact) {
+ g_object_set (cell, "pixbuf", pixbuf, NULL);
+ return;
+ }
+
+ photo = e_contact_get (contact, E_CONTACT_PHOTO);
+ if (photo && photo->type == E_CONTACT_PHOTO_TYPE_INLINED) {
+ guint max_height = entry_height (GTK_WIDGET (name_selector_entry));
+ GdkPixbufLoader *loader;
+
+ loader = gdk_pixbuf_loader_new ();
+ if (gdk_pixbuf_loader_write (loader, (guchar *) photo->data.inlined.data, photo->data.inlined.length, NULL) &&
+ gdk_pixbuf_loader_close (loader, NULL)) {
+ pixbuf = gdk_pixbuf_loader_get_pixbuf (loader);
+ if (pixbuf)
+ g_object_ref (pixbuf);
+ }
+ g_object_unref (loader);
+
+ if (pixbuf) {
+ gint w, h;
+ gdouble scale = 1.0;
+
+ w = gdk_pixbuf_get_width (pixbuf);
+ h = gdk_pixbuf_get_height (pixbuf);
+
+ if (h > w)
+ scale = max_height / (double) h;
+ else
+ scale = max_height / (double) w;
+
+ if (scale < 1.0) {
+ GdkPixbuf *tmp;
+
+ tmp = gdk_pixbuf_scale_simple (pixbuf, w * scale, h * scale, GDK_INTERP_BILINEAR);
+ g_object_unref (pixbuf);
+ pixbuf = tmp;
+ }
+
+ }
+ }
+
+ e_contact_photo_free (photo);
+
+ g_object_set (cell, "pixbuf", pixbuf, NULL);
+
+ if (pixbuf)
+ g_object_unref (pixbuf);
+}
+
+static void
+contact_layout_formatter (GtkCellLayout *cell_layout,
+ GtkCellRenderer *cell,
+ GtkTreeModel *model,
+ GtkTreeIter *iter,
+ ENameSelectorEntry *name_selector_entry)
+{
+ EContact *contact;
+ GtkTreeIter generator_iter;
+ GtkTreeIter contact_store_iter;
+ GList *email_list;
+ gchar *string;
+ gchar *file_as_str;
+ gchar *email_str;
+ gint email_n;
+
+ if (!name_selector_entry->priv->contact_store)
+ return;
+
+ gtk_tree_model_filter_convert_iter_to_child_iter (
+ GTK_TREE_MODEL_FILTER (model),
+ &generator_iter, iter);
+ e_tree_model_generator_convert_iter_to_child_iter (
+ name_selector_entry->priv->email_generator,
+ &contact_store_iter, &email_n,
+ &generator_iter);
+
+ contact = e_contact_store_get_contact (name_selector_entry->priv->contact_store, &contact_store_iter);
+ email_list = e_contact_get (contact, E_CONTACT_EMAIL);
+ email_str = g_list_nth_data (email_list, email_n);
+ file_as_str = e_contact_get (contact, E_CONTACT_FILE_AS);
+
+ if (e_contact_get (contact, E_CONTACT_IS_LIST)) {
+ string = g_strdup_printf ("%s", file_as_str ? file_as_str : "?");
+ } else {
+ string = g_strdup_printf (
+ "%s%s<%s>", file_as_str ? file_as_str : "",
+ file_as_str ? " " : "",
+ email_str ? email_str : "");
+ }
+
+ g_free (file_as_str);
+ deep_free_list (email_list);
+
+ g_object_set (cell, "text", string, NULL);
+ g_free (string);
+}
+
+static gint
+generate_contact_rows (EContactStore *contact_store,
+ GtkTreeIter *iter,
+ ENameSelectorEntry *name_selector_entry)
+{
+ EContact *contact;
+ const gchar *contact_uid;
+ GList *email_list;
+ gint n_rows;
+
+ contact = e_contact_store_get_contact (contact_store, iter);
+ g_assert (contact != NULL);
+
+ contact_uid = e_contact_get_const (contact, E_CONTACT_UID);
+ if (!contact_uid)
+ return 0; /* Can happen with broken databases */
+
+ if (e_contact_get (contact, E_CONTACT_IS_LIST))
+ return 1;
+
+ email_list = e_contact_get (contact, E_CONTACT_EMAIL);
+ n_rows = g_list_length (email_list);
+ deep_free_list (email_list);
+
+ return n_rows;
+}
+
+static void
+ensure_type_ahead_complete_on_timeout (ENameSelectorEntry *name_selector_entry)
+{
+ re_set_timeout (
+ name_selector_entry->priv->type_ahead_complete_cb_id,
+ type_ahead_complete_on_timeout_cb, name_selector_entry);
+}
+
+static void
+setup_contact_store (ENameSelectorEntry *name_selector_entry)
+{
+ if (name_selector_entry->priv->email_generator) {
+ g_object_unref (name_selector_entry->priv->email_generator);
+ name_selector_entry->priv->email_generator = NULL;
+ }
+
+ if (name_selector_entry->priv->contact_store) {
+ name_selector_entry->priv->email_generator =
+ e_tree_model_generator_new (
+ GTK_TREE_MODEL (
+ name_selector_entry->priv->contact_store));
+
+ e_tree_model_generator_set_generate_func (
+ name_selector_entry->priv->email_generator,
+ (ETreeModelGeneratorGenerateFunc) generate_contact_rows,
+ name_selector_entry, NULL);
+
+ /* Assign the store to the entry completion */
+
+ gtk_entry_completion_set_model (
+ name_selector_entry->priv->entry_completion,
+ GTK_TREE_MODEL (
+ name_selector_entry->priv->email_generator));
+
+ /* Set up callback for incoming matches */
+ g_signal_connect_swapped (
+ name_selector_entry->priv->contact_store, "row-inserted",
+ G_CALLBACK (ensure_type_ahead_complete_on_timeout), name_selector_entry);
+ } else {
+ /* Remove the store from the entry completion */
+
+ gtk_entry_completion_set_model (name_selector_entry->priv->entry_completion, NULL);
+ }
+}
+
+static void
+book_loaded_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ EContactStore *contact_store = user_data;
+ ESource *source = E_SOURCE (source_object);
+ EBookClient *book_client;
+ EClient *client = NULL;
+ GError *error = NULL;
+
+ e_client_utils_open_new_finish (source, result, &client, &error);
+
+ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+ g_warn_if_fail (client == NULL);
+ g_error_free (error);
+ goto exit;
+ }
+
+ if (error != NULL) {
+ g_warning ("%s", error->message);
+ g_warn_if_fail (client == NULL);
+ g_error_free (error);
+ goto exit;
+ }
+
+ book_client = E_BOOK_CLIENT (client);
+
+ g_return_if_fail (E_IS_BOOK_CLIENT (book_client));
+ e_contact_store_add_client (contact_store, book_client);
+ g_object_unref (book_client);
+
+ exit:
+ g_object_unref (contact_store);
+}
+
+static void
+setup_default_contact_store (ENameSelectorEntry *name_selector_entry)
+{
+ ESourceRegistry *registry;
+ EContactStore *contact_store;
+ GList *list, *iter;
+ const gchar *extension_name;
+
+ g_return_if_fail (name_selector_entry->priv->contact_store == NULL);
+
+ /* Create a book for each completion source, and assign them to the contact store */
+
+ contact_store = e_contact_store_new ();
+ name_selector_entry->priv->contact_store = contact_store;
+
+ extension_name = E_SOURCE_EXTENSION_ADDRESS_BOOK;
+ registry = e_name_selector_entry_get_registry (name_selector_entry);
+
+ /* An ESourceRegistry should have been set by now. */
+ g_return_if_fail (registry != NULL);
+
+ list = e_source_registry_list_sources (registry, extension_name);
+
+ for (iter = list; iter != NULL; iter = g_list_next (iter)) {
+ ESource *source = E_SOURCE (iter->data);
+ ESourceAutocomplete *extension;
+ GCancellable *cancellable;
+ const gchar *extension_name;
+
+ extension_name = E_SOURCE_EXTENSION_AUTOCOMPLETE;
+ extension = e_source_get_extension (source, extension_name);
+
+ /* Skip disabled address books. */
+ if (!e_source_registry_check_enabled (registry, source))
+ continue;
+
+ /* Skip non-completion address books. */
+ if (!e_source_autocomplete_get_include_me (extension))
+ continue;
+
+ cancellable = g_cancellable_new ();
+
+ g_queue_push_tail (
+ &name_selector_entry->priv->cancellables,
+ cancellable);
+
+ e_client_utils_open_new (
+ source, E_CLIENT_SOURCE_TYPE_CONTACTS, TRUE, cancellable,
+ book_loaded_cb, g_object_ref (contact_store));
+ }
+
+ g_list_free_full (list, (GDestroyNotify) g_object_unref);
+
+ setup_contact_store (name_selector_entry);
+}
+
+static void
+destination_row_changed (ENameSelectorEntry *name_selector_entry,
+ GtkTreePath *path,
+ GtkTreeIter *iter)
+{
+ EDestination *destination;
+ const gchar *entry_text;
+ gchar *text;
+ gint range_start, range_end;
+ gint n;
+
+ n = gtk_tree_path_get_indices (path)[0];
+ destination = e_destination_store_get_destination (name_selector_entry->priv->destination_store, iter);
+
+ if (!destination)
+ return;
+
+ g_assert (n >= 0);
+
+ entry_text = gtk_entry_get_text (GTK_ENTRY (name_selector_entry));
+ if (!get_range_by_index (entry_text, n, &range_start, &range_end)) {
+ g_warning ("ENameSelectorEntry is out of sync with model!");
+ return;
+ }
+
+ g_signal_handlers_block_by_func (name_selector_entry, user_insert_text, name_selector_entry);
+ g_signal_handlers_block_by_func (name_selector_entry, user_delete_text, name_selector_entry);
+
+ gtk_editable_delete_text (GTK_EDITABLE (name_selector_entry), range_start, range_end);
+
+ text = get_destination_textrep (name_selector_entry, destination);
+ gtk_editable_insert_text (GTK_EDITABLE (name_selector_entry), text, -1, &range_start);
+ g_free (text);
+
+ g_signal_handlers_unblock_by_func (name_selector_entry, user_delete_text, name_selector_entry);
+ g_signal_handlers_unblock_by_func (name_selector_entry, user_insert_text, name_selector_entry);
+
+ clear_completion_model (name_selector_entry);
+ generate_attribute_list (name_selector_entry);
+}
+
+static void
+destination_row_inserted (ENameSelectorEntry *name_selector_entry,
+ GtkTreePath *path,
+ GtkTreeIter *iter)
+{
+ EDestination *destination;
+ const gchar *entry_text;
+ gchar *text;
+ gboolean comma_before = FALSE;
+ gboolean comma_after = FALSE;
+ gint range_start, range_end;
+ gint insert_pos;
+ gint n;
+
+ n = gtk_tree_path_get_indices (path)[0];
+ destination = e_destination_store_get_destination (name_selector_entry->priv->destination_store, iter);
+
+ g_assert (n >= 0);
+ g_assert (destination != NULL);
+
+ entry_text = gtk_entry_get_text (GTK_ENTRY (name_selector_entry));
+
+ if (get_range_by_index (entry_text, n, &range_start, &range_end) && range_start != range_end) {
+ /* Another destination comes after us */
+ insert_pos = range_start;
+ comma_after = TRUE;
+ } else if (n > 0 && get_range_by_index (entry_text, n - 1, &range_start, &range_end)) {
+ /* Another destination comes before us */
+ insert_pos = range_end;
+ comma_before = TRUE;
+ } else if (n == 0) {
+ /* We're the sole destination */
+ insert_pos = 0;
+ } else {
+ g_warning ("ENameSelectorEntry is out of sync with model!");
+ return;
+ }
+
+ g_signal_handlers_block_by_func (name_selector_entry, user_insert_text, name_selector_entry);
+
+ if (comma_before)
+ gtk_editable_insert_text (GTK_EDITABLE (name_selector_entry), ", ", -1, &insert_pos);
+
+ text = get_destination_textrep (name_selector_entry, destination);
+ gtk_editable_insert_text (GTK_EDITABLE (name_selector_entry), text, -1, &insert_pos);
+ g_free (text);
+
+ if (comma_after)
+ gtk_editable_insert_text (GTK_EDITABLE (name_selector_entry), ", ", -1, &insert_pos);
+
+ g_signal_handlers_unblock_by_func (name_selector_entry, user_insert_text, name_selector_entry);
+
+ clear_completion_model (name_selector_entry);
+ generate_attribute_list (name_selector_entry);
+}
+
+static void
+destination_row_deleted (ENameSelectorEntry *name_selector_entry,
+ GtkTreePath *path)
+{
+ const gchar *text;
+ gboolean deleted_comma = FALSE;
+ gint range_start, range_end;
+ gchar *p0;
+ gint n;
+
+ n = gtk_tree_path_get_indices (path)[0];
+ g_assert (n >= 0);
+
+ text = gtk_entry_get_text (GTK_ENTRY (name_selector_entry));
+
+ if (!get_range_by_index (text, n, &range_start, &range_end)) {
+ g_warning ("ENameSelectorEntry is out of sync with model!");
+ return;
+ }
+
+ /* Expand range for deletion forwards */
+ for (p0 = g_utf8_offset_to_pointer (text, range_end); *p0;
+ p0 = g_utf8_next_char (p0), range_end++) {
+ gunichar c = g_utf8_get_char (p0);
+
+ /* Gobble spaces directly after comma */
+ if (c != ' ' && deleted_comma) {
+ range_end--;
+ break;
+ }
+
+ if (c == ',') {
+ deleted_comma = TRUE;
+ range_end++;
+ }
+ }
+
+ /* Expand range for deletion backwards */
+ for (p0 = g_utf8_offset_to_pointer (text, range_start); range_start > 0;
+ p0 = g_utf8_prev_char (p0), range_start--) {
+ gunichar c = g_utf8_get_char (p0);
+
+ if (c == ',') {
+ if (!deleted_comma) {
+ deleted_comma = TRUE;
+ break;
+ }
+
+ range_start++;
+
+ /* Leave a space in front; we deleted the comma and spaces before the
+ * following destination */
+ p0 = g_utf8_next_char (p0);
+ c = g_utf8_get_char (p0);
+ if (c == ' ')
+ range_start++;
+
+ break;
+ }
+ }
+
+ g_signal_handlers_block_by_func (name_selector_entry, user_delete_text, name_selector_entry);
+ gtk_editable_delete_text (GTK_EDITABLE (name_selector_entry), range_start, range_end);
+ g_signal_handlers_unblock_by_func (name_selector_entry, user_delete_text, name_selector_entry);
+
+ clear_completion_model (name_selector_entry);
+ generate_attribute_list (name_selector_entry);
+}
+
+static void
+setup_destination_store (ENameSelectorEntry *name_selector_entry)
+{
+ GtkTreeIter iter;
+
+ g_signal_connect_swapped (
+ name_selector_entry->priv->destination_store, "row-changed",
+ G_CALLBACK (destination_row_changed), name_selector_entry);
+ g_signal_connect_swapped (
+ name_selector_entry->priv->destination_store, "row-deleted",
+ G_CALLBACK (destination_row_deleted), name_selector_entry);
+ g_signal_connect_swapped (
+ name_selector_entry->priv->destination_store, "row-inserted",
+ G_CALLBACK (destination_row_inserted), name_selector_entry);
+
+ if (!gtk_tree_model_get_iter_first (GTK_TREE_MODEL (name_selector_entry->priv->destination_store), &iter))
+ return;
+
+ do {
+ GtkTreePath *path;
+
+ path = gtk_tree_model_get_path (GTK_TREE_MODEL (name_selector_entry->priv->destination_store), &iter);
+ g_assert (path);
+
+ destination_row_inserted (name_selector_entry, path, &iter);
+ } while (gtk_tree_model_iter_next (GTK_TREE_MODEL (name_selector_entry->priv->destination_store), &iter));
+}
+
+static gboolean
+prepare_popup_destination (ENameSelectorEntry *name_selector_entry,
+ GdkEventButton *event_button)
+{
+ EDestination *destination;
+ PangoLayout *layout;
+ gint layout_offset_x;
+ gint layout_offset_y;
+ gint x, y;
+ gint index;
+
+ if (event_button->type != GDK_BUTTON_PRESS)
+ return FALSE;
+
+ if (event_button->button != 3)
+ return FALSE;
+
+ if (name_selector_entry->priv->popup_destination) {
+ g_object_unref (name_selector_entry->priv->popup_destination);
+ name_selector_entry->priv->popup_destination = NULL;
+ }
+
+ gtk_entry_get_layout_offsets (
+ GTK_ENTRY (name_selector_entry),
+ &layout_offset_x, &layout_offset_y);
+ x = (event_button->x + 0.5) - layout_offset_x;
+ y = (event_button->y + 0.5) - layout_offset_y;
+
+ if (x < 0 || y < 0)
+ return FALSE;
+
+ layout = gtk_entry_get_layout (GTK_ENTRY (name_selector_entry));
+ if (!pango_layout_xy_to_index (layout, x * PANGO_SCALE, y * PANGO_SCALE, &index, NULL))
+ return FALSE;
+
+ index = gtk_entry_layout_index_to_text_index (GTK_ENTRY (name_selector_entry), index);
+ destination = find_destination_at_position (name_selector_entry, index);
+ /* FIXME: Add this to a private variable, in ENameSelectorEntry Class*/
+ g_object_set_data ((GObject *) name_selector_entry, "index", GINT_TO_POINTER (index));
+
+ if (!destination || !e_destination_get_contact (destination))
+ return FALSE;
+
+ /* TODO: Unref destination when we finalize */
+ name_selector_entry->priv->popup_destination = g_object_ref (destination);
+ return FALSE;
+}
+
+static EBookClient *
+find_client_by_contact (GSList *clients,
+ const gchar *contact_uid,
+ const gchar *source_uid)
+{
+ GSList *l;
+
+ if (source_uid && *source_uid) {
+ /* this is much quicket than asking each client for an existence */
+ for (l = clients; l; l = g_slist_next (l)) {
+ EBookClient *client = l->data;
+ ESource *source = e_client_get_source (E_CLIENT (client));
+
+ if (!source)
+ continue;
+
+ if (g_strcmp0 (source_uid, e_source_get_uid (source)) == 0)
+ return client;
+ }
+ }
+
+ for (l = clients; l; l = g_slist_next (l)) {
+ EBookClient *client = l->data;
+ EContact *contact = NULL;
+ gboolean result;
+
+ result = e_book_client_get_contact_sync (client, contact_uid, &contact, NULL, NULL);
+ if (contact)
+ g_object_unref (contact);
+
+ if (result)
+ return client;
+ }
+
+ return NULL;
+}
+
+static void
+editor_closed_cb (GtkWidget *editor,
+ gpointer data)
+{
+ EContact *contact;
+ gchar *contact_uid;
+ EDestination *destination;
+ GSList *clients;
+ EBookClient *book_client;
+ gint email_num;
+ ENameSelectorEntry *name_selector_entry = E_NAME_SELECTOR_ENTRY (data);
+
+ destination = name_selector_entry->priv->popup_destination;
+ contact = e_destination_get_contact (destination);
+ if (!contact) {
+ g_object_unref (name_selector_entry);
+ return;
+ }
+
+ contact_uid = e_contact_get (contact, E_CONTACT_UID);
+ if (!contact_uid) {
+ g_object_unref (contact);
+ g_object_unref (name_selector_entry);
+ return;
+ }
+
+ if (name_selector_entry->priv->contact_store) {
+ clients = e_contact_store_get_clients (name_selector_entry->priv->contact_store);
+ book_client = find_client_by_contact (clients, contact_uid, e_destination_get_source_uid (destination));
+ g_slist_free (clients);
+ } else {
+ book_client = NULL;
+ }
+
+ if (book_client) {
+ contact = NULL;
+
+ g_warn_if_fail (e_book_client_get_contact_sync (book_client, contact_uid, &contact, NULL, NULL));
+ email_num = e_destination_get_email_num (destination);
+ e_destination_set_contact (destination, contact, email_num);
+ e_destination_set_client (destination, book_client);
+ } else {
+ contact = NULL;
+ }
+
+ g_free (contact_uid);
+ if (contact)
+ g_object_unref (contact);
+ g_object_unref (name_selector_entry);
+}
+
+/* To parse something like...
+ * =?UTF-8?Q?=E0=A4=95=E0=A4=95=E0=A4=AC=E0=A5=82=E0=A5=8B=E0=A5=87?=\t\n=?UTF-8?Q?=E0=A4=B0?=\t\n<aa@aa.ccom>
+ * and return the decoded representation of name & email parts.
+ * */
+static gboolean
+eab_parse_qp_email (const gchar *string,
+ gchar **name,
+ gchar **email)
+{
+ struct _camel_header_address *address;
+ gboolean res = FALSE;
+
+ address = camel_header_address_decode (string, "UTF-8");
+
+ if (!address)
+ return FALSE;
+
+ /* report success only when we have filled both name and email address */
+ if (address->type == CAMEL_HEADER_ADDRESS_NAME && address->name && *address->name && address->v.addr && *address->v.addr) {
+ *name = g_strdup (address->name);
+ *email = g_strdup (address->v.addr);
+ res = TRUE;
+ }
+
+ camel_header_address_unref (address);
+
+ return res;
+}
+
+static void
+popup_activate_inline_expand (ENameSelectorEntry *name_selector_entry,
+ GtkWidget *menu_item)
+{
+ const gchar *text;
+ GString *sanitized_text = g_string_new ("");
+ EDestination *destination = name_selector_entry->priv->popup_destination;
+ gint position, start, end;
+ const GList *dests;
+
+ position = GPOINTER_TO_INT (g_object_get_data ((GObject *) name_selector_entry, "index"));
+
+ for (dests = e_destination_list_get_dests (destination); dests; dests = dests->next) {
+ const EDestination *dest = dests->data;
+ gchar *sanitized;
+ gchar *name = NULL, *email = NULL, *tofree = NULL;
+
+ if (!dest)
+ continue;
+
+ text = e_destination_get_textrep (dest, TRUE);
+
+ if (!text || !*text)
+ continue;
+
+ if (eab_parse_qp_email (text, &name, &email)) {
+ tofree = g_strdup_printf ("%s <%s>", name, email);
+ text = tofree;
+ g_free (name);
+ g_free (email);
+ }
+
+ sanitized = sanitize_string (text);
+ g_free (tofree);
+ if (!sanitized)
+ continue;
+
+ if (*sanitized) {
+ if (*sanitized_text->str)
+ g_string_append (sanitized_text, ", ");
+
+ g_string_append (sanitized_text, sanitized);
+ }
+
+ g_free (sanitized);
+ }
+
+ text = gtk_entry_get_text (GTK_ENTRY (name_selector_entry));
+ get_range_at_position (text, position, &start, &end);
+ gtk_editable_delete_text (GTK_EDITABLE (name_selector_entry), start, end);
+ gtk_editable_insert_text (GTK_EDITABLE (name_selector_entry), sanitized_text->str, -1, &start);
+ g_string_free (sanitized_text, TRUE);
+
+ clear_completion_model (name_selector_entry);
+ generate_attribute_list (name_selector_entry);
+}
+
+static void
+popup_activate_contact (ENameSelectorEntry *name_selector_entry,
+ GtkWidget *menu_item)
+{
+ EBookClient *book_client;
+ GSList *clients;
+ EDestination *destination;
+ EContact *contact;
+ gchar *contact_uid;
+
+ destination = name_selector_entry->priv->popup_destination;
+ if (!destination)
+ return;
+
+ contact = e_destination_get_contact (destination);
+ if (!contact)
+ return;
+
+ contact_uid = e_contact_get (contact, E_CONTACT_UID);
+ if (!contact_uid)
+ return;
+
+ if (name_selector_entry->priv->contact_store) {
+ clients = e_contact_store_get_clients (name_selector_entry->priv->contact_store);
+ book_client = find_client_by_contact (clients, contact_uid, e_destination_get_source_uid (destination));
+ g_slist_free (clients);
+ g_free (contact_uid);
+ } else {
+ book_client = NULL;
+ }
+
+ if (!book_client)
+ return;
+
+ if (e_destination_is_evolution_list (destination)) {
+ GtkWidget *contact_list_editor;
+
+ if (!name_selector_entry->priv->contact_list_editor_func)
+ return;
+
+ contact_list_editor = (*name_selector_entry->priv->contact_list_editor_func) (book_client, contact, FALSE, TRUE);
+ g_object_ref (name_selector_entry);
+ g_signal_connect (
+ contact_list_editor, "editor_closed",
+ G_CALLBACK (editor_closed_cb), name_selector_entry);
+ } else {
+ GtkWidget *contact_editor;
+
+ if (!name_selector_entry->priv->contact_editor_func)
+ return;
+
+ contact_editor = (*name_selector_entry->priv->contact_editor_func) (book_client, contact, FALSE, TRUE);
+ g_object_ref (name_selector_entry);
+ g_signal_connect (
+ contact_editor, "editor_closed",
+ G_CALLBACK (editor_closed_cb), name_selector_entry);
+ }
+}
+
+static void
+popup_activate_email (ENameSelectorEntry *name_selector_entry,
+ GtkWidget *menu_item)
+{
+ EDestination *destination;
+ EContact *contact;
+ gint email_num;
+
+ destination = name_selector_entry->priv->popup_destination;
+ if (!destination)
+ return;
+
+ contact = e_destination_get_contact (destination);
+ if (!contact)
+ return;
+
+ email_num = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (menu_item), "order"));
+ e_destination_set_contact (destination, contact, email_num);
+}
+
+static void
+popup_activate_list (EDestination *destination,
+ GtkWidget *item)
+{
+ gboolean status = gtk_check_menu_item_get_active (GTK_CHECK_MENU_ITEM (item));
+
+ e_destination_set_ignored (destination, !status);
+}
+
+static void
+popup_activate_cut (ENameSelectorEntry *name_selector_entry,
+ GtkWidget *menu_item)
+{
+ EDestination *destination;
+ const gchar *contact_email;
+ gchar *pemail = NULL;
+ GtkClipboard *clipboard;
+
+ destination = name_selector_entry->priv->popup_destination;
+ contact_email =e_destination_get_textrep (destination, TRUE);
+
+ g_signal_handlers_block_by_func (name_selector_entry, user_insert_text, name_selector_entry);
+ g_signal_handlers_block_by_func (name_selector_entry, user_delete_text, name_selector_entry);
+
+ clipboard = gtk_clipboard_get (GDK_SELECTION_PRIMARY);
+ pemail = g_strconcat (contact_email, ",", NULL);
+ gtk_clipboard_set_text (clipboard, pemail, strlen (pemail));
+
+ clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD);
+ gtk_clipboard_set_text (clipboard, pemail, strlen (pemail));
+
+ gtk_editable_delete_text (GTK_EDITABLE (name_selector_entry), 0, 0);
+ e_destination_store_remove_destination (name_selector_entry->priv->destination_store, destination);
+
+ g_free (pemail);
+ g_signal_handlers_unblock_by_func (name_selector_entry, user_delete_text, name_selector_entry);
+ g_signal_handlers_unblock_by_func (name_selector_entry, user_insert_text, name_selector_entry);
+}
+
+static void
+popup_activate_copy (ENameSelectorEntry *name_selector_entry,
+ GtkWidget *menu_item)
+{
+ EDestination *destination;
+ const gchar *contact_email;
+ gchar *pemail;
+ GtkClipboard *clipboard;
+
+ destination = name_selector_entry->priv->popup_destination;
+ contact_email = e_destination_get_textrep (destination, TRUE);
+
+ g_signal_handlers_block_by_func (name_selector_entry, user_insert_text, name_selector_entry);
+ g_signal_handlers_block_by_func (name_selector_entry, user_delete_text, name_selector_entry);
+
+ clipboard = gtk_clipboard_get (GDK_SELECTION_PRIMARY);
+ pemail = g_strconcat (contact_email, ",", NULL);
+ gtk_clipboard_set_text (clipboard, pemail, strlen (pemail));
+
+ clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD);
+ gtk_clipboard_set_text (clipboard, pemail, strlen (pemail));
+ g_free (pemail);
+ g_signal_handlers_unblock_by_func (name_selector_entry, user_delete_text, name_selector_entry);
+ g_signal_handlers_unblock_by_func (name_selector_entry, user_insert_text, name_selector_entry);
+}
+
+static void
+destination_set_list (GtkWidget *item,
+ EDestination *destination)
+{
+ EContact *contact;
+ gboolean status = gtk_check_menu_item_get_active (GTK_CHECK_MENU_ITEM (item));
+
+ contact = e_destination_get_contact (destination);
+ if (!contact)
+ return;
+
+ e_destination_set_ignored (destination, !status);
+}
+
+static void
+destination_set_email (GtkWidget *item,
+ EDestination *destination)
+{
+ gint email_num;
+ EContact *contact;
+
+ if (!gtk_check_menu_item_get_active (GTK_CHECK_MENU_ITEM (item)))
+ return;
+ contact = e_destination_get_contact (destination);
+ if (!contact)
+ return;
+
+ email_num = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (item), "order"));
+ e_destination_set_contact (destination, contact, email_num);
+}
+
+static void
+populate_popup (ENameSelectorEntry *name_selector_entry,
+ GtkMenu *menu)
+{
+ EDestination *destination;
+ EContact *contact;
+ GtkWidget *menu_item;
+ GList *email_list = NULL;
+ GList *l;
+ gint i;
+ gchar *edit_label;
+ gchar *cut_label;
+ gchar *copy_label;
+ gint email_num, len;
+ GSList *group = NULL;
+ gboolean is_list;
+ gboolean show_menu = FALSE;
+
+ destination = name_selector_entry->priv->popup_destination;
+ if (!destination)
+ return;
+
+ contact = e_destination_get_contact (destination);
+ if (!contact)
+ return;
+
+ /* Prepend the menu items, backwards */
+
+ /* Separator */
+
+ menu_item = gtk_separator_menu_item_new ();
+ gtk_widget_show (menu_item);
+ gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), menu_item);
+ email_num = e_destination_get_email_num (destination);
+
+ /* Addresses */
+ is_list = e_contact_get (contact, E_CONTACT_IS_LIST) ? TRUE : FALSE;
+ if (is_list) {
+ const GList *dests = e_destination_list_get_dests (destination);
+ GList *iter;
+ gint length = g_list_length ((GList *) dests);
+
+ for (iter = (GList *) dests; iter; iter = iter->next) {
+ EDestination *dest = (EDestination *) iter->data;
+ const gchar *email = e_destination_get_email (dest);
+
+ if (!email || *email == '\0')
+ continue;
+
+ if (length > 1) {
+ menu_item = gtk_check_menu_item_new_with_label (email);
+ g_signal_connect (
+ menu_item, "toggled",
+ G_CALLBACK (destination_set_list), dest);
+ } else {
+ menu_item = gtk_menu_item_new_with_label (email);
+ }
+
+ gtk_widget_show (menu_item);
+ gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), menu_item);
+ show_menu = TRUE;
+
+ if (length > 1) {
+ gtk_check_menu_item_set_active (
+ GTK_CHECK_MENU_ITEM (menu_item),
+ !e_destination_is_ignored (dest));
+ g_signal_connect_swapped (
+ menu_item, "activate",
+ G_CALLBACK (popup_activate_list), dest);
+ }
+ }
+
+ } else {
+ email_list = e_contact_get (contact, E_CONTACT_EMAIL);
+ len = g_list_length (email_list);
+
+ for (l = email_list, i = 0; l; l = g_list_next (l), i++) {
+ gchar *email = l->data;
+
+ if (!email || *email == '\0')
+ continue;
+
+ if (len > 1) {
+ menu_item = gtk_radio_menu_item_new_with_label (group, email);
+ group = gtk_radio_menu_item_get_group (GTK_RADIO_MENU_ITEM (menu_item));
+ g_signal_connect (menu_item, "toggled", G_CALLBACK (destination_set_email), destination);
+ } else {
+ menu_item = gtk_menu_item_new_with_label (email);
+ }
+
+ gtk_widget_show (menu_item);
+ gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), menu_item);
+ show_menu = TRUE;
+ g_object_set_data (G_OBJECT (menu_item), "order", GINT_TO_POINTER (i));
+
+ if (i == email_num && len > 1) {
+ gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (menu_item), TRUE);
+ g_signal_connect_swapped (
+ menu_item, "activate",
+ G_CALLBACK (popup_activate_email),
+ name_selector_entry);
+ }
+ }
+ }
+
+ /* Separator */
+
+ if (show_menu) {
+ menu_item = gtk_separator_menu_item_new ();
+ gtk_widget_show (menu_item);
+ gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), menu_item);
+ }
+
+ /* Expand a list inline */
+ if (is_list) {
+ /* To Translators: This would be similiar to "Expand MyList Inline" where MyList is a Contact List*/
+ edit_label = g_strdup_printf (_("E_xpand %s Inline"), (gchar *) e_contact_get_const (contact, E_CONTACT_FILE_AS));
+ menu_item = gtk_menu_item_new_with_mnemonic (edit_label);
+ g_free (edit_label);
+ gtk_widget_show (menu_item);
+ gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), menu_item);
+ g_signal_connect_swapped (
+ menu_item, "activate", G_CALLBACK (popup_activate_inline_expand),
+ name_selector_entry);
+
+ /* Separator */
+ menu_item = gtk_separator_menu_item_new ();
+ gtk_widget_show (menu_item);
+ gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), menu_item);
+ }
+
+ /* Copy Contact Item */
+ copy_label = g_strdup_printf (_("Cop_y %s"), (gchar *) e_contact_get_const (contact, E_CONTACT_FILE_AS));
+ menu_item = gtk_menu_item_new_with_mnemonic (copy_label);
+ g_free (copy_label);
+ gtk_widget_show (menu_item);
+ gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), menu_item);
+
+ g_signal_connect_swapped (
+ menu_item, "activate", G_CALLBACK (popup_activate_copy),
+ name_selector_entry);
+
+ /* Cut Contact Item */
+ cut_label = g_strdup_printf (_("C_ut %s"), (gchar *) e_contact_get_const (contact, E_CONTACT_FILE_AS));
+ menu_item = gtk_menu_item_new_with_mnemonic (cut_label);
+ g_free (cut_label);
+ gtk_widget_show (menu_item);
+ gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), menu_item);
+
+ g_signal_connect_swapped (
+ menu_item, "activate", G_CALLBACK (popup_activate_cut),
+ name_selector_entry);
+
+ if (show_menu) {
+ menu_item = gtk_separator_menu_item_new ();
+ gtk_widget_show (menu_item);
+ gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), menu_item);
+ }
+
+ /* Edit Contact item */
+
+ edit_label = g_strdup_printf (_("_Edit %s"), (gchar *) e_contact_get_const (contact, E_CONTACT_FILE_AS));
+ menu_item = gtk_menu_item_new_with_mnemonic (edit_label);
+ g_free (edit_label);
+ gtk_widget_show (menu_item);
+ gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), menu_item);
+
+ g_signal_connect_swapped (
+ menu_item, "activate", G_CALLBACK (popup_activate_contact),
+ name_selector_entry);
+
+ deep_free_list (email_list);
+}
+
+static void
+copy_or_cut_clipboard (ENameSelectorEntry *name_selector_entry,
+ gboolean is_cut)
+{
+ GtkClipboard *clipboard;
+ GtkEditable *editable;
+ const gchar *text, *cp;
+ GHashTable *hash;
+ GHashTableIter iter;
+ gpointer key, value;
+ GString *addresses;
+ gint ii, start, end;
+ gunichar uc;
+
+ editable = GTK_EDITABLE (name_selector_entry);
+ text = gtk_entry_get_text (GTK_ENTRY (editable));
+
+ if (!gtk_editable_get_selection_bounds (editable, &start, &end))
+ return;
+
+ g_return_if_fail (end > start);
+
+ hash = g_hash_table_new (g_direct_hash, g_direct_equal);
+
+ ii = end;
+ cp = g_utf8_offset_to_pointer (text, end);
+ uc = g_utf8_get_char (cp);
+
+ /* Exclude trailing whitespace and commas. */
+ while (ii >= start && (uc == ',' || g_unichar_isspace (uc))) {
+ cp = g_utf8_prev_char (cp);
+ uc = g_utf8_get_char (cp);
+ ii--;
+ }
+
+ /* Determine the index of each remaining character. */
+ while (ii >= start) {
+ gint index = get_index_at_position (text, ii--);
+ g_hash_table_insert (hash, GINT_TO_POINTER (index), NULL);
+ }
+
+ addresses = g_string_new ("");
+
+ g_hash_table_iter_init (&iter, hash);
+ while (g_hash_table_iter_next (&iter, &key, &value)) {
+ gint index = GPOINTER_TO_INT (key);
+ EDestination *dest;
+ gint rstart, rend;
+
+ if (!get_range_by_index (text, index, &rstart, &rend))
+ continue;
+
+ if (rstart < start) {
+ if (addresses->str && *addresses->str)
+ g_string_append (addresses, ", ");
+
+ g_string_append_len (addresses, text + start, rend - start);
+ } else if (rend > end) {
+ if (addresses->str && *addresses->str)
+ g_string_append (addresses, ", ");
+
+ g_string_append_len (addresses, text + rstart, end - rstart);
+ } else {
+ /* the contact is whole selected */
+ dest = find_destination_by_index (name_selector_entry, index);
+ if (dest && e_destination_get_textrep (dest, TRUE)) {
+ if (addresses->str && *addresses->str)
+ g_string_append (addresses, ", ");
+
+ g_string_append (addresses, e_destination_get_textrep (dest, TRUE));
+
+ /* store the 'dest' as a value for the index */
+ g_hash_table_insert (hash, GINT_TO_POINTER (index), dest);
+ } else
+ g_string_append_len (addresses, text + rstart, rend - rstart);
+ }
+ }
+
+ if (is_cut)
+ gtk_editable_delete_text (editable, start, end);
+
+ g_hash_table_unref (hash);
+
+ clipboard = gtk_widget_get_clipboard (
+ GTK_WIDGET (name_selector_entry), GDK_SELECTION_CLIPBOARD);
+ gtk_clipboard_set_text (clipboard, addresses->str, -1);
+
+ g_string_free (addresses, TRUE);
+}
+
+static void
+copy_clipboard (GtkEntry *entry,
+ ENameSelectorEntry *name_selector_entry)
+{
+ copy_or_cut_clipboard (name_selector_entry, FALSE);
+ g_signal_stop_emission_by_name (entry, "copy-clipboard");
+}
+
+static void
+cut_clipboard (GtkEntry *entry,
+ ENameSelectorEntry *name_selector_entry)
+{
+ copy_or_cut_clipboard (name_selector_entry, TRUE);
+ g_signal_stop_emission_by_name (entry, "cut-clipboard");
+}
+
+static void
+e_name_selector_entry_init (ENameSelectorEntry *name_selector_entry)
+{
+ GtkCellRenderer *renderer;
+
+ name_selector_entry->priv =
+ E_NAME_SELECTOR_ENTRY_GET_PRIVATE (name_selector_entry);
+
+ g_queue_init (&name_selector_entry->priv->cancellables);
+
+ name_selector_entry->priv->minimum_query_length = 3;
+ name_selector_entry->priv->show_address = FALSE;
+
+ /* Edit signals */
+
+ g_signal_connect (
+ name_selector_entry, "insert-text",
+ G_CALLBACK (user_insert_text), name_selector_entry);
+ g_signal_connect (
+ name_selector_entry, "delete-text",
+ G_CALLBACK (user_delete_text), name_selector_entry);
+ g_signal_connect (
+ name_selector_entry, "focus-out-event",
+ G_CALLBACK (user_focus_out), name_selector_entry);
+ g_signal_connect_after (
+ name_selector_entry, "focus-in-event",
+ G_CALLBACK (user_focus_in), name_selector_entry);
+
+ /* Drawing */
+
+ g_signal_connect (
+ name_selector_entry, "draw",
+ G_CALLBACK (draw_event), name_selector_entry);
+
+ /* Activation: Complete current entry if possible */
+
+ g_signal_connect (
+ name_selector_entry, "activate",
+ G_CALLBACK (entry_activate), name_selector_entry);
+
+ /* Pop-up menu */
+
+ g_signal_connect (
+ name_selector_entry, "button-press-event",
+ G_CALLBACK (prepare_popup_destination), name_selector_entry);
+ g_signal_connect (
+ name_selector_entry, "populate-popup",
+ G_CALLBACK (populate_popup), name_selector_entry);
+
+ /* Clipboard signals */
+ g_signal_connect (
+ name_selector_entry, "copy-clipboard",
+ G_CALLBACK (copy_clipboard), name_selector_entry);
+ g_signal_connect (
+ name_selector_entry, "cut-clipboard",
+ G_CALLBACK (cut_clipboard), name_selector_entry);
+
+ /* Completion */
+
+ name_selector_entry->priv->email_generator = NULL;
+
+ name_selector_entry->priv->entry_completion = gtk_entry_completion_new ();
+ gtk_entry_completion_set_match_func (
+ name_selector_entry->priv->entry_completion,
+ (GtkEntryCompletionMatchFunc) completion_match_cb, NULL, NULL);
+ g_signal_connect_swapped (
+ name_selector_entry->priv->entry_completion, "match-selected",
+ G_CALLBACK (completion_match_selected), name_selector_entry);
+
+ gtk_entry_set_completion (
+ GTK_ENTRY (name_selector_entry),
+ name_selector_entry->priv->entry_completion);
+
+ renderer = gtk_cell_renderer_pixbuf_new ();
+ gtk_cell_layout_pack_start (
+ GTK_CELL_LAYOUT (name_selector_entry->priv->entry_completion),
+ renderer, FALSE);
+ gtk_cell_layout_set_cell_data_func (
+ GTK_CELL_LAYOUT (name_selector_entry->priv->entry_completion),
+ GTK_CELL_RENDERER (renderer),
+ (GtkCellLayoutDataFunc) contact_layout_pixbuffer,
+ name_selector_entry, NULL);
+
+ /* Completion list name renderer */
+ renderer = gtk_cell_renderer_text_new ();
+ gtk_cell_layout_pack_start (
+ GTK_CELL_LAYOUT (name_selector_entry->priv->entry_completion),
+ renderer, TRUE);
+ gtk_cell_layout_set_cell_data_func (
+ GTK_CELL_LAYOUT (name_selector_entry->priv->entry_completion),
+ GTK_CELL_RENDERER (renderer),
+ (GtkCellLayoutDataFunc) contact_layout_formatter,
+ name_selector_entry, NULL);
+
+ /* Destination store */
+
+ name_selector_entry->priv->destination_store = e_destination_store_new ();
+ setup_destination_store (name_selector_entry);
+ name_selector_entry->priv->is_completing = FALSE;
+}
+
+/**
+ * e_name_selector_entry_new:
+ *
+ * Creates a new #ENameSelectorEntry.
+ *
+ * Returns: A new #ENameSelectorEntry.
+ **/
+ENameSelectorEntry *
+e_name_selector_entry_new (ESourceRegistry *registry)
+{
+ g_return_val_if_fail (E_IS_SOURCE_REGISTRY (registry), NULL);
+
+ return g_object_new (
+ E_TYPE_NAME_SELECTOR_ENTRY,
+ "registry", registry, NULL);
+}
+
+/**
+ * e_name_selector_entry_get_registry:
+ * @name_selector_entry: an #ENameSelectorEntry
+ *
+ * Returns the #ESourceRegistry used to query address books.
+ *
+ * Returns: the #ESourceRegistry, or %NULL
+ *
+ * Since: 3.6
+ **/
+ESourceRegistry *
+e_name_selector_entry_get_registry (ENameSelectorEntry *name_selector_entry)
+{
+ g_return_val_if_fail (
+ E_IS_NAME_SELECTOR_ENTRY (name_selector_entry), NULL);
+
+ return name_selector_entry->priv->registry;
+}
+
+/**
+ * e_name_selector_entry_set_registry:
+ * @name_selector_entry: an #ENameSelectorEntry
+ * @registry: an #ESourceRegistry
+ *
+ * Sets the #ESourceRegistry used to query address books.
+ *
+ * This function is intended for cases where @name_selector_entry is
+ * instantiated by a #GtkBuilder and has to be given an #EsourceRegistry
+ * after it is fully constructed.
+ *
+ * Since: 3.6
+ **/
+void
+e_name_selector_entry_set_registry (ENameSelectorEntry *name_selector_entry,
+ ESourceRegistry *registry)
+{
+ g_return_if_fail (E_IS_NAME_SELECTOR_ENTRY (name_selector_entry));
+
+ if (name_selector_entry->priv->registry == registry)
+ return;
+
+ if (registry != NULL) {
+ g_return_if_fail (E_IS_SOURCE_REGISTRY (registry));
+ g_object_ref (registry);
+ }
+
+ if (name_selector_entry->priv->registry != NULL)
+ g_object_unref (name_selector_entry->priv->registry);
+
+ name_selector_entry->priv->registry = registry;
+
+ g_object_notify (G_OBJECT (name_selector_entry), "registry");
+}
+
+/**
+ * e_name_selector_entry_get_minimum_query_length:
+ * @name_selector_entry: an #ENameSelectorEntry
+ *
+ * Returns: Minimum length of query before completion starts
+ *
+ * Since: 3.6
+ **/
+gint
+e_name_selector_entry_get_minimum_query_length (ENameSelectorEntry *name_selector_entry)
+{
+ g_return_val_if_fail (E_IS_NAME_SELECTOR_ENTRY (name_selector_entry), -1);
+
+ return name_selector_entry->priv->minimum_query_length;
+}
+
+/**
+ * e_name_selector_entry_set_minimum_query_length:
+ * @name_selector_entry: an #ENameSelectorEntry
+ * @length: minimum query length
+ *
+ * Sets minimum length of query before completion starts.
+ *
+ * Since: 3.6
+ **/
+void
+e_name_selector_entry_set_minimum_query_length (ENameSelectorEntry *name_selector_entry,
+ gint length)
+{
+ g_return_if_fail (E_IS_NAME_SELECTOR_ENTRY (name_selector_entry));
+ g_return_if_fail (length > 0);
+
+ if (name_selector_entry->priv->minimum_query_length == length)
+ return;
+
+ name_selector_entry->priv->minimum_query_length = length;
+
+ g_object_notify (G_OBJECT (name_selector_entry), "minimum-query-length");
+}
+
+/**
+ * e_name_selector_entry_get_show_address:
+ * @name_selector_entry: an #ENameSelectorEntry
+ *
+ * Returns: Whether always show email address for an auto-completed contact.
+ *
+ * Since: 3.6
+ **/
+gboolean
+e_name_selector_entry_get_show_address (ENameSelectorEntry *name_selector_entry)
+{
+ g_return_val_if_fail (E_IS_NAME_SELECTOR_ENTRY (name_selector_entry), FALSE);
+
+ return name_selector_entry->priv->show_address;
+}
+
+/**
+ * e_name_selector_entry_set_show_address:
+ * @name_selector_entry: an #ENameSelectorEntry
+ * @show: new value to set
+ *
+ * Sets whether always show email address for an auto-completed contact.
+ *
+ * Since: 3.6
+ **/
+void
+e_name_selector_entry_set_show_address (ENameSelectorEntry *name_selector_entry,
+ gboolean show)
+{
+ g_return_if_fail (E_IS_NAME_SELECTOR_ENTRY (name_selector_entry));
+
+ if ((name_selector_entry->priv->show_address ? 1 : 0) == (show ? 1 : 0))
+ return;
+
+ name_selector_entry->priv->show_address = show;
+
+ sanitize_entry (name_selector_entry);
+
+ g_object_notify (G_OBJECT (name_selector_entry), "show-address");
+}
+
+/**
+ * e_name_selector_entry_peek_contact_store:
+ * @name_selector_entry: an #ENameSelectorEntry
+ *
+ * Gets the #EContactStore being used by @name_selector_entry.
+ *
+ * Returns: An #EContactStore.
+ **/
+EContactStore *
+e_name_selector_entry_peek_contact_store (ENameSelectorEntry *name_selector_entry)
+{
+ g_return_val_if_fail (E_IS_NAME_SELECTOR_ENTRY (name_selector_entry), NULL);
+
+ return name_selector_entry->priv->contact_store;
+}
+
+/**
+ * e_name_selector_entry_set_contact_store:
+ * @name_selector_entry: an #ENameSelectorEntry
+ * @contact_store: an #EContactStore to use
+ *
+ * Sets the #EContactStore being used by @name_selector_entry to @contact_store.
+ **/
+void
+e_name_selector_entry_set_contact_store (ENameSelectorEntry *name_selector_entry,
+ EContactStore *contact_store)
+{
+ g_return_if_fail (E_IS_NAME_SELECTOR_ENTRY (name_selector_entry));
+ g_return_if_fail (contact_store == NULL || E_IS_CONTACT_STORE (contact_store));
+
+ if (contact_store == name_selector_entry->priv->contact_store)
+ return;
+
+ if (name_selector_entry->priv->contact_store)
+ g_object_unref (name_selector_entry->priv->contact_store);
+ name_selector_entry->priv->contact_store = contact_store;
+ if (name_selector_entry->priv->contact_store)
+ g_object_ref (name_selector_entry->priv->contact_store);
+
+ setup_contact_store (name_selector_entry);
+}
+
+/**
+ * e_name_selector_entry_peek_destination_store:
+ * @name_selector_entry: an #ENameSelectorEntry
+ *
+ * Gets the #EDestinationStore being used to store @name_selector_entry's destinations.
+ *
+ * Returns: An #EDestinationStore.
+ **/
+EDestinationStore *
+e_name_selector_entry_peek_destination_store (ENameSelectorEntry *name_selector_entry)
+{
+ g_return_val_if_fail (E_IS_NAME_SELECTOR_ENTRY (name_selector_entry), NULL);
+
+ return name_selector_entry->priv->destination_store;
+}
+
+/**
+ * e_name_selector_entry_set_destination_store:
+ * @name_selector_entry: an #ENameSelectorEntry
+ * @destination_store: an #EDestinationStore to use
+ *
+ * Sets @destination_store as the #EDestinationStore to be used to store
+ * destinations for @name_selector_entry.
+ **/
+void
+e_name_selector_entry_set_destination_store (ENameSelectorEntry *name_selector_entry,
+ EDestinationStore *destination_store)
+{
+ g_return_if_fail (E_IS_NAME_SELECTOR_ENTRY (name_selector_entry));
+ g_return_if_fail (E_IS_DESTINATION_STORE (destination_store));
+
+ if (destination_store == name_selector_entry->priv->destination_store)
+ return;
+
+ g_object_unref (name_selector_entry->priv->destination_store);
+ name_selector_entry->priv->destination_store = g_object_ref (destination_store);
+
+ setup_destination_store (name_selector_entry);
+}
+
+/**
+ * e_name_selector_entry_get_popup_destination:
+ *
+ * Since: 2.32
+ **/
+EDestination *
+e_name_selector_entry_get_popup_destination (ENameSelectorEntry *name_selector_entry)
+{
+ g_return_val_if_fail (E_IS_NAME_SELECTOR_ENTRY (name_selector_entry), NULL);
+
+ return name_selector_entry->priv->popup_destination;
+}
+
+/**
+ * e_name_selector_entry_set_contact_editor_func:
+ *
+ * DO NOT USE.
+ **/
+void
+e_name_selector_entry_set_contact_editor_func (ENameSelectorEntry *name_selector_entry,
+ gpointer func)
+{
+ name_selector_entry->priv->contact_editor_func = func;
+}
+
+/**
+ * e_name_selector_entry_set_contact_list_editor_func:
+ *
+ * DO NOT USE.
+ **/
+void
+e_name_selector_entry_set_contact_list_editor_func (ENameSelectorEntry *name_selector_entry,
+ gpointer func)
+{
+ name_selector_entry->priv->contact_list_editor_func = func;
+}