/* -*- 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) 2004 Novell, Inc. * * 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., 59 Temple Place - Suite 330, * Boston, MA 02111-1307, USA. * * Authors: Hans Petter Jansson */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "e-name-selector-entry.h" #define ENS_DEBUG(x) #define COMPLETION_CUE_MIN_LEN 3 G_DEFINE_TYPE (ENameSelectorEntry, e_name_selector_entry, GTK_TYPE_ENTRY); static void e_name_selector_entry_class_init (ENameSelectorEntryClass *name_selector_entry_class); static void e_name_selector_entry_init (ENameSelectorEntry *name_selector_entry); static void e_name_selector_entry_dispose (GObject *object); static void e_name_selector_entry_finalize (GObject *object); 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 e_name_selector_entry_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { } static void e_name_selector_entry_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { } static void e_name_selector_entry_realize (GtkWidget *widget) { ENameSelectorEntry *name_selector_entry = E_NAME_SELECTOR_ENTRY (widget); GTK_WIDGET_CLASS (e_name_selector_entry_parent_class)->realize (widget); if (!name_selector_entry->contact_store) { setup_default_contact_store (name_selector_entry); } } /* Partial, repeatable destruction. Release references. */ static void e_name_selector_entry_dispose (GObject *object) { if (G_OBJECT_CLASS (e_name_selector_entry_parent_class)->dispose) G_OBJECT_CLASS (e_name_selector_entry_parent_class)->dispose (object); } /* Final, one-time destruction. Free all. */ static void e_name_selector_entry_finalize (GObject *object) { if (G_OBJECT_CLASS (e_name_selector_entry_parent_class)->finalize) G_OBJECT_CLASS (e_name_selector_entry_parent_class)->finalize (object); } static void e_name_selector_entry_class_init (ENameSelectorEntryClass *name_selector_entry_class) { GObjectClass *object_class = G_OBJECT_CLASS (name_selector_entry_class); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (name_selector_entry_class); object_class->get_property = e_name_selector_entry_get_property; object_class->set_property = e_name_selector_entry_set_property; object_class->dispose = e_name_selector_entry_dispose; object_class->finalize = e_name_selector_entry_finalize; widget_class->realize = e_name_selector_entry_realize; /* Install properties */ /* Install signals */ } /* Remove unquoted commas 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; 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; 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->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->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; } static void set_completion_query (ENameSelectorEntry *name_selector_entry, const gchar *cue_str) { EBookQuery *book_query; gchar *query_str; gchar *encoded_cue_str; gchar *full_name_query_str; gchar *file_as_query_str; if (!name_selector_entry->contact_store) return; if (!cue_str) { /* Clear the store */ e_contact_store_set_query (name_selector_entry->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); query_str = g_strdup_printf ("(or " " (beginswith \"nickname\" %s) " " (beginswith \"email\" %s) " " %s " " %s " ")", encoded_cue_str, encoded_cue_str, full_name_query_str, file_as_query_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->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); int rv; rv = g_utf8_collate (s1, s2); g_free (s1); g_free (s2); return rv; } static gint utf8_casefold_collate (const gchar *str1, const gchar *str2) { return utf8_casefold_collate_len (str1, str2, -1); } 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", name); else textrep = g_strdup_printf ("%s", email); g_free (name); g_free (email); return textrep; } static gboolean contact_match_cue (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) < COMPLETION_CUE_MIN_LEN) 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) { GtkTreeIter iter; EContact *best_contact = NULL; gint best_field_rank = G_MAXINT; EContactField best_field = 0; gint cue_len; g_assert (cue_str); if (!name_selector_entry->contact_store) return FALSE; cue_len = strlen (cue_str); ENS_DEBUG (g_print ("Completing '%s'\n", cue_str)); if (!gtk_tree_model_get_iter_first (GTK_TREE_MODEL (name_selector_entry->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->contact_store, &iter); if (!current_contact) continue; matches = contact_match_cue (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; } } while (gtk_tree_model_iter_next (GTK_TREE_MODEL (name_selector_entry->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; 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->attr_list) pango_attr_list_unref (name_selector_entry->attr_list); name_selector_entry->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 expose_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->attr_list); return FALSE; } static void type_ahead_complete (ENameSelectorEntry *name_selector_entry) { EContact *contact; 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; 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 < COMPLETION_CUE_MIN_LEN) 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)) { g_free (cue_str); return; } g_free (cue_str); 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->destination_store, destination_row_changed, name_selector_entry); if (textrep_len > range_len) { 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); } 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); generate_attribute_list (name_selector_entry); } g_signal_handlers_unblock_by_func (name_selector_entry->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) { if (!name_selector_entry->contact_store) return; e_contact_store_set_query (name_selector_entry->contact_store, NULL); } 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 >= COMPLETION_CUE_MIN_LEN && 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_idle_cb (ENameSelectorEntry *name_selector_entry) { type_ahead_complete (name_selector_entry); name_selector_entry->type_ahead_complete_cb_id = 0; return FALSE; } static gboolean update_completions_on_idle_cb (ENameSelectorEntry *name_selector_entry) { update_completion_model (name_selector_entry); name_selector_entry->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->destination_store, destination_row_inserted, name_selector_entry); e_destination_store_insert_destination (name_selector_entry->destination_store, index, destination); g_signal_handlers_unblock_by_func (name_selector_entry->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); g_assert (destination); 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->destination_store, destination_row_changed, name_selector_entry); e_destination_set_raw (destination, raw_address); g_signal_handlers_unblock_by_func (name_selector_entry->destination_store, destination_row_changed, name_selector_entry); g_free (raw_address); if (rebuild_attributes) generate_attribute_list (name_selector_entry); } static void remove_destination_at_position (ENameSelectorEntry *name_selector_entry, gint pos) { EDestination *destination; destination = find_destination_at_position (name_selector_entry, pos); if (destination) { g_signal_handlers_block_by_func (name_selector_entry->destination_store, destination_row_deleted, name_selector_entry); e_destination_store_remove_destination (name_selector_entry->destination_store, destination); g_signal_handlers_unblock_by_func (name_selector_entry->destination_store, destination_row_deleted, name_selector_entry); } } 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 = sanitize_string (e_destination_get_textrep (destination, FALSE)); 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->destination_store, destination_row_deleted, name_selector_entry); e_destination_store_remove_destination (name_selector_entry->destination_store, destination); g_signal_handlers_unblock_by_func (name_selector_entry->destination_store, destination_row_deleted, 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); text = gtk_entry_get_text (GTK_ENTRY (name_selector_entry)); len = g_utf8_strlen (text, -1); text = g_utf8_next_char (text); if (!*text) { /* First and only character so far, create initial destination */ insert_destination_at_position (name_selector_entry, 0); } else { /* Modified existing destination */ modify_destination_at_position (name_selector_entry, *pos); } /* If editing within the string, we need to regenerate attributes */ if (*pos < len) generate_attribute_list (name_selector_entry); return 1; } static void user_insert_text (ENameSelectorEntry *name_selector_entry, gchar *new_text, gint new_text_length, gint *position, gpointer user_data) { gchar *p; gint chars_inserted = 0; 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); /* Apply some rules as to where spaces and commas can be inserted, and insert * a trailing space after comma. */ for (p = new_text; *p; p = g_utf8_next_char (p)) { gunichar c = g_utf8_get_char (p); insert_unichar (name_selector_entry, position, c); chars_inserted++; } if (chars_inserted == 1) { /* If the user inserted one character, kick off completion */ if (!name_selector_entry->update_completions_cb_id) { name_selector_entry->update_completions_cb_id = g_idle_add ((GSourceFunc) update_completions_on_idle_cb, name_selector_entry); } if (!name_selector_entry->type_ahead_complete_cb_id) { name_selector_entry->type_ahead_complete_cb_id = g_idle_add ((GSourceFunc) type_ahead_complete_on_idle_cb, name_selector_entry); } } else if (chars_inserted > 1 && name_selector_entry->type_ahead_complete_cb_id) { /* If the user inserted more than one character, prevent completion */ g_source_remove (name_selector_entry->type_ahead_complete_cb_id); name_selector_entry->type_ahead_complete_cb_id = 0; } 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 already_selected = FALSE; if (start_pos == end_pos) return; text = gtk_entry_get_text (GTK_ENTRY (name_selector_entry)); len = g_utf8_strlen (text, -1); if (gtk_editable_get_selection_bounds (GTK_EDITABLE (name_selector_entry), &selection_start, &selection_end)) if (g_utf8_get_char (g_utf8_offset_to_pointer (text, selection_end - 1)) == ',') already_selected = TRUE; get_utf8_string_context (text, start_pos, str_context, 2); get_utf8_string_context (text, end_pos-1, 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 */ if (!name_selector_entry->update_completions_cb_id) { name_selector_entry->update_completions_cb_id = g_idle_add ((GSourceFunc) update_completions_on_idle_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 user is trying to delete a ','-character, we assume the user * wants to remove the entire destination. */ if (((str_b_context [0] == ',' && str_b_context [1] == ' ') || str_b_context [1] == ',') && !already_selected) { EDestination *dest = find_destination_at_position (name_selector_entry, end_pos-1); const char *email = e_destination_get_email (dest); if (email && (strcmp (email, "")!=0)) { /* Therefore, in case it's a real destination, we select it. * Deleting this selection afterwards will leave the destination * empty. */ gint t = (str_b_context [1]==',')?end_pos:end_pos-1, b=t; do { t--; } while (t >= 1 && text[t-1] != ','); gtk_editable_select_region (GTK_EDITABLE(name_selector_entry), t, b); /* Since this is a special-case, we don't want the rest of this method * to happen. However, we do need to reenable the signal which we * disabled above! */ goto end_of_user_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--) remove_destination_by_index (name_selector_entry, i); /* Do the actual deletion */ 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 i; p = text + end_pos; for (p = text + (end_pos-1), i = end_pos - 1; *p && *p != '"' ; p = g_utf8_next_char (p), i++) { gunichar c = g_utf8_get_char (p); if(c == ',') { insert_destination_at_position (name_selector_entry, i+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 { 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->type_ahead_complete_cb_id) { g_source_remove (name_selector_entry->type_ahead_complete_cb_id); name_selector_entry->type_ahead_complete_cb_id = 0; } end_of_user_delete_text: g_signal_handlers_unblock_by_func (name_selector_entry, user_delete_text, name_selector_entry); } static gboolean completion_match_selected (ENameSelectorEntry *name_selector_entry, GtkTreeModel *model, GtkTreeIter *iter) { EContact *contact; EDestination *destination; gint cursor_pos; GtkTreeIter generator_iter; GtkTreeIter contact_iter; gint email_n; if (!name_selector_entry->contact_store) return FALSE; 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->email_generator, &contact_iter, &email_n, &generator_iter); contact = e_contact_store_get_contact (name_selector_entry->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); sync_destination_at_position (name_selector_entry, cursor_pos, &cursor_pos); gtk_editable_insert_text (GTK_EDITABLE (name_selector_entry), ",", -1, &cursor_pos); /* Place cursor at end of address */ gtk_editable_set_position (GTK_EDITABLE (name_selector_entry), cursor_pos); return TRUE; } static void entry_activate (ENameSelectorEntry *name_selector_entry) { gint cursor_pos; gint range_start, range_end; const gchar *text; /* Show us what's really there */ cursor_pos = gtk_editable_get_position (GTK_EDITABLE (name_selector_entry)); 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); gtk_editable_set_position (GTK_EDITABLE (name_selector_entry), range_end); } static gboolean user_focus_out (ENameSelectorEntry *name_selector_entry, GdkEventFocus *event_focus) { if (!event_focus->in) entry_activate (name_selector_entry); clear_completion_model (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); } 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->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->email_generator, &contact_store_iter, &email_n, &generator_iter); contact = e_contact_store_get_contact (name_selector_entry->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_idle (ENameSelectorEntry *name_selector_entry) { if (!name_selector_entry->type_ahead_complete_cb_id) { name_selector_entry->type_ahead_complete_cb_id = g_idle_add ((GSourceFunc) type_ahead_complete_on_idle_cb, name_selector_entry); } } static void setup_contact_store (ENameSelectorEntry *name_selector_entry) { if (name_selector_entry->email_generator) { g_object_unref (name_selector_entry->email_generator); name_selector_entry->email_generator = NULL; } if (name_selector_entry->contact_store) { name_selector_entry->email_generator = e_tree_model_generator_new (GTK_TREE_MODEL (name_selector_entry->contact_store)); e_tree_model_generator_set_generate_func (name_selector_entry->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->entry_completion, GTK_TREE_MODEL (name_selector_entry->email_generator)); /* Set up callback for incoming matches */ g_signal_connect_swapped (name_selector_entry->contact_store, "row-inserted", G_CALLBACK (ensure_type_ahead_complete_on_idle), name_selector_entry); } else { /* Remove the store from the entry completion */ gtk_entry_completion_set_model (name_selector_entry->entry_completion, NULL); } } static void setup_default_contact_store (ENameSelectorEntry *name_selector_entry) { GSList *groups; GSList *l; g_return_if_fail (name_selector_entry->contact_store == NULL); /* Create a book for each completion source, and assign them to the contact store */ name_selector_entry->contact_store = e_contact_store_new (); groups = e_source_list_peek_groups (name_selector_entry->source_list); for (l = groups; l; l = g_slist_next (l)) { ESourceGroup *group = l->data; GSList *sources = e_source_group_peek_sources (group); GSList *m; for (m = sources; m; m = g_slist_next (m)) { ESource *source = m->data; EBook *book; const gchar *completion; /* Skip non-completion sources */ completion = e_source_get_property (source, "completion"); if (!completion || g_ascii_strcasecmp (completion, "true")) continue; book = e_load_book_source (source, NULL, NULL); if (!book) continue; e_contact_store_add_book (name_selector_entry->contact_store, book); g_object_unref (book); } } 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->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)) { 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 = sanitize_string (e_destination_get_textrep (destination, FALSE)); 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->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 = sanitize_string (e_destination_get_textrep (destination, FALSE)); 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->destination_store, "row-changed", G_CALLBACK (destination_row_changed), name_selector_entry); g_signal_connect_swapped (name_selector_entry->destination_store, "row-deleted", G_CALLBACK (destination_row_deleted), name_selector_entry); g_signal_connect_swapped (name_selector_entry->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->destination_store), &iter)) return; do { GtkTreePath *path; path = gtk_tree_model_get_path (GTK_TREE_MODEL (name_selector_entry->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->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->popup_destination) { g_object_unref (name_selector_entry->popup_destination); name_selector_entry->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->popup_destination = g_object_ref (destination); return FALSE; } static EBook * find_book_by_contact (GList *books, const gchar *contact_uid) { GList *l; for (l = books; l; l = g_list_next (l)) { EBook *book = l->data; EContact *contact; gboolean result; result = e_book_get_contact (book, contact_uid, &contact, NULL); if (contact) g_object_unref (contact); if (result) return book; } return NULL; } static void editor_closed_cb (GtkObject *editor, gpointer data) { EContact *contact; gchar *contact_uid; EDestination *destination; GList *books; EBook *book; gboolean result; gint email_num; ENameSelectorEntry *name_selector_entry = E_NAME_SELECTOR_ENTRY (data); destination = name_selector_entry->popup_destination; 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->contact_store) { books = e_contact_store_get_books (name_selector_entry->contact_store); book = find_book_by_contact (books, contact_uid); g_list_free (books); } else { book = NULL; } if (!book) return; result = e_book_get_contact(book, contact_uid, &contact, NULL); email_num = e_destination_get_email_num(destination); e_destination_set_contact (destination, contact, email_num); g_free (contact_uid); g_object_unref (contact); g_object_unref (editor); } static void popup_activate_inline_expand (ENameSelectorEntry *name_selector_entry, GtkWidget *menu_item) { const char *email_list, *text; EDestination *destination = name_selector_entry->popup_destination; int position, start, end; position = GPOINTER_TO_INT(g_object_get_data ((GObject *)name_selector_entry, "index")); email_list = e_destination_get_address(destination); text = gtk_entry_get_text (GTK_ENTRY (name_selector_entry)); get_range_at_position (text, position, &start, &end); g_signal_handlers_block_by_func (name_selector_entry, user_delete_text, name_selector_entry); gtk_editable_delete_text (GTK_EDITABLE (name_selector_entry), start, end); text = sanitize_string (e_destination_get_textrep (destination, FALSE)); gtk_editable_insert_text (GTK_EDITABLE (name_selector_entry), email_list, -1, &start); 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 popup_activate_contact (ENameSelectorEntry *name_selector_entry, GtkWidget *menu_item) { EBook *book; GList *books; EDestination *destination; EContact *contact; gchar *contact_uid; destination = name_selector_entry->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->contact_store) { books = e_contact_store_get_books (name_selector_entry->contact_store); /*FIXME: read URI from contact and get the book ?*/ book = find_book_by_contact (books, contact_uid); g_list_free (books); g_free (contact_uid); } else { book = NULL; } if (!book) return; if (e_destination_is_evolution_list (destination)) { GtkWidget *contact_list_editor; if (!name_selector_entry->contact_list_editor_func) return; contact_list_editor = (*name_selector_entry->contact_list_editor_func) (book, contact, FALSE, TRUE); g_signal_connect (contact_list_editor, "editor_closed", G_CALLBACK (editor_closed_cb), name_selector_entry); } else { GtkWidget *contact_editor; if (!name_selector_entry->contact_editor_func) return; contact_editor = (*name_selector_entry->contact_editor_func) (book, contact, FALSE, TRUE); 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->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 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) { int 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; char *edit_label; int email_num, len; GSList *group = NULL; gboolean is_list; gboolean show_menu = FALSE; destination = name_selector_entry->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; int len = g_list_length ((GList *)dests); for (iter = (GList *)dests; iter; iter = iter->next) { EDestination *dest = (EDestination *) iter->data; const char *email = e_destination_get_email (dest); if (!email || *email == '\0') continue; if (len > 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 ( len > 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_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"), (char *)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); } /* Edit Contact item */ edit_label = g_strdup_printf (_("_Edit %s"), (char *)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 e_name_selector_entry_init (ENameSelectorEntry *name_selector_entry) { GtkCellRenderer *renderer; /* Source list */ if (!e_book_get_addressbooks (&name_selector_entry->source_list, NULL)) { g_warning ("ENameSelectorEntry can't find any addressbooks!"); return; } /* 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); /* Exposition */ g_signal_connect (name_selector_entry, "expose-event", G_CALLBACK (expose_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); /* Completion */ name_selector_entry->email_generator = NULL; name_selector_entry->entry_completion = gtk_entry_completion_new (); gtk_entry_completion_set_match_func (name_selector_entry->entry_completion, (GtkEntryCompletionMatchFunc) completion_match_cb, NULL, NULL); g_signal_connect_swapped (name_selector_entry->entry_completion, "match-selected", G_CALLBACK (completion_match_selected), name_selector_entry); gtk_entry_set_completion (GTK_ENTRY (name_selector_entry), name_selector_entry->entry_completion); /* Completion list name renderer */ renderer = gtk_cell_renderer_text_new (); gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (name_selector_entry->entry_completion), renderer, TRUE); gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (name_selector_entry->entry_completion), GTK_CELL_RENDERER (renderer), (GtkCellLayoutDataFunc) contact_layout_formatter, name_selector_entry, NULL); /* Destination store */ name_selector_entry->destination_store = e_destination_store_new (); setup_destination_store (name_selector_entry); } /** * e_name_selector_entry_new: * * Creates a new #ENameSelectorEntry. * * Return value: A new #ENameSelectorEntry. **/ ENameSelectorEntry * e_name_selector_entry_new (void) { return g_object_new (e_name_selector_entry_get_type (), NULL); } /** * e_name_selector_entry_peek_contact_store: * @name_selector_entry: an #ENameSelectorEntry * * Gets the #EContactStore being used by @name_selector_entry. * * Return value: 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->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->contact_store) return; if (name_selector_entry->contact_store) g_object_unref (name_selector_entry->contact_store); name_selector_entry->contact_store = contact_store; if (name_selector_entry->contact_store) g_object_ref (name_selector_entry->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. * * Return value: 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->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->destination_store) return; g_object_unref (name_selector_entry->destination_store); name_selector_entry->destination_store = g_object_ref (destination_store); setup_destination_store (name_selector_entry); } /** * 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->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->contact_list_editor_func = func; }