From d09d8de870b6697c8a8b262e7e077b871a69b315 Mon Sep 17 00:00:00 2001 From: Matthew Barnes Date: Mon, 10 Dec 2012 08:09:59 -0500 Subject: 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. --- e-util/e-name-selector-entry.c | 3541 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 3541 insertions(+) create mode 100644 e-util/e-name-selector-entry.c (limited to 'e-util/e-name-selector-entry.c') 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 + */ + +#include +#include +#include + +#include +#include + +#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, ¤t_field, ¤t_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 + * 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; +} -- cgit v1.2.3