/* * Contacts - A small libebook-based address book. * * Authored By Chris Lord * * Copyright (c) 2005 OpenedHand Ltd - http://o-hand.com * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2, or (at your option) * any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * */ #include #include #include #include #include #include "contacts-utils.h" #include "contacts-defs.h" #include "contacts-main.h" /* The following functions taken from * http://svn.o-hand.com/repos/kozo/server/src/kozo-utf8.c */ /*****************************************************************************/ G_INLINE_FUNC char * e_util_unicode_get_utf8 (const char *text, gunichar * out) { *out = g_utf8_get_char (text); return (G_UNLIKELY (*out == (gunichar) - 1)) ? NULL : g_utf8_next_char (text); } static gunichar stripped_char (gunichar ch) { GUnicodeType utype; utype = g_unichar_type (ch); switch (utype) { case G_UNICODE_CONTROL: case G_UNICODE_FORMAT: case G_UNICODE_UNASSIGNED: case G_UNICODE_COMBINING_MARK: /* Ignore those */ return 0; default: /* Convert to lowercase, fall through */ ch = g_unichar_tolower (ch); case G_UNICODE_LOWERCASE_LETTER: return ch; } } const char * kozo_utf8_strstrcasestrip (const char *haystack, const gunichar * needle) { gunichar unival, sc; const char *o, *p, *q; int npos; /* FIXME this is unoptimal */ /* TODO use Bayer-Moore algorithm, if faster */ o = haystack; for (p = e_util_unicode_get_utf8 (o, &unival); p && unival; p = e_util_unicode_get_utf8 (p, &unival)) { sc = stripped_char (unival); if (sc) { /* We have valid stripped char */ if (sc == needle[0]) { q = p; npos = 1; while (needle[npos]) { q = e_util_unicode_get_utf8 (q, &unival); if (!q || !unival) return NULL; sc = stripped_char (unival); if ((!sc) || (sc != needle[npos])) break; npos++; } if (!needle[npos]) return o; } } o = p; } return NULL; } gunichar * kozo_utf8_strcasestrip (const char *str) { gunichar *needle; gunichar unival, sc; int nlen, normlen; const char *p; char *norm; /* FIXME unoptimal, too many iterations */ norm = g_utf8_normalize (str, -1, G_NORMALIZE_DEFAULT); normlen = g_utf8_strlen (norm, -1); if (G_UNLIKELY (normlen == 0)) { g_free (norm); return NULL; } /* Over-estimates length */ needle = g_new (gunichar, normlen + 1); nlen = 0; for (p = e_util_unicode_get_utf8 (norm, &unival); p && unival; p = e_util_unicode_get_utf8 (p, &unival)) { sc = stripped_char (unival); if (sc) needle[nlen++] = sc; } g_free (norm); /* NULL means there was illegal utf-8 sequence */ if (!p) return NULL; /* If everything is correct, we have decomposed, lowercase, stripped * needle */ needle[nlen] = 0; if (normlen == nlen) return needle; else return g_renew (gunichar, needle, nlen + 1); } /******************************************************************************/ /* List of always-available fields */ /* TODO: Revise 'supported' fields */ /* Note: PHOTO and CATEGORIES are special-cased (see contacts_edit_pane_show) */ static const ContactsField contacts_fields[] = { { "FN", E_CONTACT_FULL_NAME, NULL, FALSE, 10, TRUE }, { "ORG", E_CONTACT_ORG, NULL, FALSE, 15, TRUE }, { "TEL", 0, N_("Phone"), FALSE, 20, FALSE }, { "EMAIL", 0, N_("Email"), FALSE, 30, FALSE }, { "ADR", 0, N_("Address"), FALSE, 40, FALSE }, { "NICKNAME", E_CONTACT_NICKNAME, NULL, FALSE, 110, TRUE }, { "BDAY", E_CONTACT_BIRTH_DATE, NULL, FALSE, 120, TRUE }, { "URL", E_CONTACT_HOMEPAGE_URL, N_("Homepage"), FALSE, 130, FALSE }, { "NOTE", E_CONTACT_NOTE, NULL, TRUE, 140, TRUE }, { NULL } }; static const ContactsStructuredField contacts_sfields[] = { { "ADR", 0, N_("PO Box"), FALSE }, { "ADR", 1, N_("Ext."), TRUE }, { "ADR", 2, N_("Street"), TRUE }, { "ADR", 3, N_("Locality"), FALSE }, { "ADR", 4, N_("Region"), FALSE }, { "ADR", 5, N_("Post Code"), FALSE }, { "ADR", 6, N_("Country"), FALSE }, { NULL, 0, NULL, FALSE } }; struct contacts_field_types_def { char *field; gchar **types; }; static const struct contacts_field_types_def contacts_field_types[] = { /* TODO: can these be i18n-ized? */ { "TEL", (gchar*[]){"Home", "Msg", "Work", "Pref", "Voice", "Fax", "Cell", "Video", "Pager", "BBS", "Modem", "Car", "ISDN", "PCS", NULL }}, { "EMAIL", (gchar*[]){"Internet", "X400", "Pref", NULL}}, { "ADR", (gchar*[]){"Dom", "Intl", "Postal", "Parcel", "Home", "Work", "Pref", NULL}}, { NULL, NULL } }; const gchar ** contacts_get_field_types (const gchar *attr_name) { guint i; for (i = 0; contacts_field_types[i].field; i++) { if (strcmp (contacts_field_types[i].field, attr_name) == 0) return (const gchar **)contacts_field_types[i].types; } return NULL; } const ContactsStructuredField * contacts_get_structured_field (const gchar *attr_name, guint field) { guint i; for (i = 0; contacts_sfields[i].attr_name; i++) { if (strcmp (contacts_sfields[i].attr_name, attr_name) == 0) { if (contacts_sfields[i].field == field) return &contacts_sfields[i]; } } return NULL; } guint contacts_get_structured_field_size (const gchar *attr_name) { guint i, size = 1; for (i = 0; contacts_sfields[i].attr_name; i++) if (strcmp (contacts_sfields[i].attr_name, attr_name) == 0) if (contacts_sfields[i].field+1 > size) size = contacts_sfields[i].field+1; return size; } const ContactsField * contacts_get_contacts_field (const gchar *vcard_field) { guint i; for (i = 0; (contacts_fields[i].vcard_field) && (vcard_field); i++) { if (strcmp (contacts_fields[i].vcard_field, vcard_field) == 0) return &contacts_fields[i]; } return NULL; } const ContactsField * contacts_get_contacts_fields () { return contacts_fields; } const gchar * contacts_field_pretty_name (const ContactsField *field) { if (field->pretty_name) { return gettext(field->pretty_name); } else if (field->econtact_field > 0) { return e_contact_pretty_name (field->econtact_field); } else return NULL; } gint contacts_compare_attributes (EVCardAttribute *attrA, EVCardAttribute *attrB) { gint a = 0, b = 0, i; const gchar *nameA, *nameB; nameA = e_vcard_attribute_get_name (attrA); nameB = e_vcard_attribute_get_name (attrB); for (i = 0; contacts_fields[i].vcard_field; i++) { if (strcmp (nameA, contacts_fields[i].vcard_field)) a = contacts_fields[i].priority; if (strcmp (nameB, contacts_fields[i].vcard_field)) b = contacts_fields[i].priority; } return b - a; } EContact * contacts_contact_from_tree_path (GtkTreeModel *model, GtkTreePath *path, GHashTable *contacts_table) { GtkTreeIter iter; if (gtk_tree_model_get_iter (model, &iter, path)) { gchar *uid; EContactListHash *hash; gtk_tree_model_get (model, &iter, CONTACT_UID_COL, &uid, -1); if (uid) { hash = g_hash_table_lookup (contacts_table, uid); g_free (uid); if (hash) return hash->contact; } } return NULL; } EContact * contacts_contact_from_selection (GtkTreeSelection *selection, GHashTable *contacts_table) { GtkTreeModel *model; GList *selected_paths; GtkTreePath *path; EContact *result; if (!selection || !GTK_IS_TREE_SELECTION (selection)) return NULL; selected_paths = gtk_tree_selection_get_selected_rows (selection, &model); if (!selected_paths) return NULL; path = selected_paths->data; result = contacts_contact_from_tree_path (model, path, contacts_table); g_list_foreach (selected_paths, (GFunc) gtk_tree_path_free, NULL); g_list_free (selected_paths); return result; } EContact * contacts_get_selected_contact (ContactsData *data, GHashTable *contacts_table) { GtkWidget *widget; GtkTreeSelection *selection; EContact *contact; /* Get the currently selected contact */ widget = data->ui->contacts_treeview; selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (widget)); if (!selection || !GTK_IS_TREE_SELECTION (selection)) return NULL; contact = contacts_contact_from_selection (selection, contacts_table); return contact; } void contacts_set_selected_contact (ContactsData *data, const gchar *uid) { GtkTreeView *treeview; GtkTreeModel *model; GtkTreeIter iter; treeview = GTK_TREE_VIEW ( data->ui->contacts_treeview); model = /*gtk_tree_model_filter_get_model (GTK_TREE_MODEL_FILTER (*/ gtk_tree_view_get_model (treeview)/*))*/; if (!gtk_tree_model_get_iter_first (model, &iter)) return; do { const gchar *ruid; gtk_tree_model_get (model, &iter, CONTACT_UID_COL, &ruid, -1); if (strcmp (uid, ruid) == 0) { GtkTreeSelection *selection = gtk_tree_view_get_selection (treeview); if (selection) gtk_tree_selection_select_iter ( selection, &iter); return; } } while (gtk_tree_model_iter_next (model, &iter)); } static void contact_photo_size (GdkPixbufLoader * loader, gint width, gint height, gpointer user_data) { /* Max height of GTK_ICON_SIZE_DIALOG */ gint iconwidth, iconheight; gtk_icon_size_lookup (GTK_ICON_SIZE_DIALOG, &iconwidth, &iconheight); gdk_pixbuf_loader_set_size (loader, width / ((gdouble) height / iconheight), iconheight); } GtkImage * contacts_load_photo (EContact *contact) { GtkImage *image = NULL; EContactPhoto *photo; /* Retrieve contact picture and resize */ photo = e_contact_get (contact, E_CONTACT_PHOTO); if (photo) { GdkPixbufLoader *loader = gdk_pixbuf_loader_new (); if (loader) { g_signal_connect (G_OBJECT (loader), "size-prepared", G_CALLBACK (contact_photo_size), NULL); #if HAVE_PHOTO_TYPE switch (photo->type) { case E_CONTACT_PHOTO_TYPE_INLINED : gdk_pixbuf_loader_write (loader, photo->data.inlined.data, photo->data.inlined.length, NULL); break; case E_CONTACT_PHOTO_TYPE_URI : default : g_warning ("Cannot handle URI photos yet"); g_object_unref (loader); loader = NULL; break; } #else gdk_pixbuf_loader_write (loader, (const guchar *) photo->data, photo->length, NULL); #endif if (loader) { gdk_pixbuf_loader_close (loader, NULL); GdkPixbuf *pixbuf = gdk_pixbuf_loader_get_pixbuf (loader); if (pixbuf) { image = GTK_IMAGE ( gtk_image_new_from_pixbuf ( g_object_ref (pixbuf))); } g_object_unref (loader); } } e_contact_photo_free (photo); } return image ? image : GTK_IMAGE (gtk_image_new_from_icon_name ("stock_person", GTK_ICON_SIZE_DIALOG)); } /* This removes any vCard attributes that are just "", or have no associated * value. Evolution tends to add empty fields like this a lot - as does * contacts, to show required fields (but it removes them after editing, via * this function). * TODO: This really doesn't need to be recursive. */ void contacts_clean_contact (EContact *contact) { GList *attributes, *c; attributes = e_vcard_get_attributes (E_VCARD (contact)); for (c = attributes; c; c = c->next) { EVCardAttribute *a = (EVCardAttribute*)c->data; GList *values = e_vcard_attribute_get_values (a); gboolean remove = TRUE; for (; values; values = values->next) { if (g_utf8_strlen ((const gchar *)values->data, -1) > 0) remove = FALSE; } if (remove) { e_vcard_remove_attribute (E_VCARD (contact), a); contacts_clean_contact (contact); break; } } } gboolean contacts_contact_is_empty (EContact *contact) { GList *attributes, *c; attributes = e_vcard_get_attributes (E_VCARD (contact)); for (c = attributes; c; c = c->next) { EVCardAttribute *a = (EVCardAttribute*)c->data; GList *values = e_vcard_attribute_get_values (a); for (; values; values = values->next) { if (g_utf8_strlen ((const gchar *)values->data, -1) > 0) return FALSE; } } return TRUE; } gchar * contacts_string_list_as_string (GList *list, const gchar *separator, gboolean include_empty) { gchar *old_string, *new_string = NULL; GList *c; if (!include_empty) for (; ((list) && (strlen ((const gchar *)list->data) == 0)); list = list->next); if (!list) return NULL; new_string = g_strdup (list->data); for (c = list->next; c; c = c->next) { if ((strlen ((const gchar *)c->data) > 0) || (include_empty)) { old_string = new_string; new_string = g_strdup_printf ("%s%s%s", new_string, separator, (const gchar *)c->data); g_free (old_string); } } return new_string; } GList * contacts_get_types (GList *params) { GList *list = NULL; for (; params; params = params->next) { EVCardAttributeParam *p = (EVCardAttributeParam *)params->data; if (strcmp ("TYPE", e_vcard_attribute_param_get_name (p)) == 0) list = g_list_append (list, p); } return list; } GList * contacts_get_type_strings (GList *params) { GList *list = NULL; for (; params; params = params->next) { EVCardAttributeParam *p = (EVCardAttributeParam *)params->data; if (strcmp ("TYPE", e_vcard_attribute_param_get_name (p)) == 0){ GList *types = e_vcard_attribute_param_get_values (p); for (; types; types = types->next) { list = g_list_append (list, types->data); } } } return list; } void contacts_choose_photo (GtkWidget *button, EContact *contact) { GtkWidget *filechooser, *photo; GtkFileFilter *filter; gint result; GList *widgets; /* Get a filename */ /* Note: I don't use the GTK_WINDOW cast as gtk_widget_get_ancestor * can return NULL and this would probably throw a critical Gtk error. */ filechooser = gtk_file_chooser_dialog_new (_("Open image"), (GtkWindow *) gtk_widget_get_ancestor ( button, GTK_TYPE_WINDOW), GTK_FILE_CHOOSER_ACTION_OPEN, GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, GTK_STOCK_OPEN, GTK_RESPONSE_ACCEPT, _("No image"), NO_IMAGE, NULL); /* Set filter by supported EContactPhoto image types */ filter = gtk_file_filter_new (); /* mime types taken from e-contact.c */ gtk_file_filter_add_mime_type (filter, "image/gif"); gtk_file_filter_add_mime_type (filter, "image/jpeg"); gtk_file_filter_add_mime_type (filter, "image/png"); gtk_file_filter_add_mime_type (filter, "image/tiff"); gtk_file_filter_add_mime_type (filter, "image/ief"); gtk_file_filter_add_mime_type (filter, "image/cgm"); gtk_file_chooser_set_filter (GTK_FILE_CHOOSER (filechooser), filter); /* If a file was selected, get the image and set the contact to that * image. */ widgets = contacts_set_widgets_desensitive ( gtk_widget_get_ancestor (button, GTK_TYPE_WINDOW)); result = gtk_dialog_run (GTK_DIALOG (filechooser)); if (result == GTK_RESPONSE_ACCEPT) { gchar *filename = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (filechooser)); if (filename) { if (contact) { EContactPhoto new_photo; guchar **data; int *length; #if HAVE_PHOTO_TYPE new_photo.type = E_CONTACT_PHOTO_TYPE_INLINED; data = &new_photo.data.inlined.data; length = &new_photo.data.inlined.length; new_photo.data.inlined.mime_type = NULL; #else data = &new_photo.data; length = &new_photo.length; #endif if (g_file_get_contents (filename, (gchar **)data, (gsize *)length, NULL)) { e_contact_set (contact, E_CONTACT_PHOTO, &new_photo); g_free (*data); /* Re-display contact photo */ gtk_container_foreach ( GTK_CONTAINER (button), (GtkCallback)gtk_widget_destroy, NULL); photo = GTK_WIDGET (contacts_load_photo (contact)); gtk_container_add ( GTK_CONTAINER (button), photo); gtk_widget_show (photo); } } g_free (filename); } } else if (result == NO_IMAGE) { if (contact && E_IS_CONTACT (contact)) { e_contact_set (contact, E_CONTACT_PHOTO, NULL); /* Re-display contact photo */ gtk_container_foreach (GTK_CONTAINER (button), (GtkCallback)gtk_widget_destroy, NULL); photo = GTK_WIDGET (contacts_load_photo (contact)); gtk_container_add (GTK_CONTAINER (button), photo); gtk_widget_show (photo); } } contacts_set_widgets_sensitive (widgets); gtk_widget_destroy (filechooser); } void contacts_free_list_hash (gpointer data) { EContactListHash *hash = (EContactListHash *)data; if (hash) { GtkListStore *model = GTK_LIST_STORE (gtk_tree_model_filter_get_model (GTK_TREE_MODEL_FILTER (gtk_tree_view_get_model (GTK_TREE_VIEW (hash->contacts_data->ui->contacts_treeview))))); gtk_list_store_remove (model, &hash->iter); g_object_unref (hash->contact); g_free (hash); } } /* Helper method to harvest user-input from GtkContainer's */ GList * contacts_entries_get_values (GtkWidget *widget, GList *list) { if (GTK_IS_ENTRY (widget)) { return g_list_append (list, g_strdup ( gtk_entry_get_text (GTK_ENTRY (widget)))); } else if (GTK_IS_COMBO_BOX_ENTRY (widget)) { return g_list_append (list, g_strdup ( gtk_entry_get_text ( GTK_ENTRY (GTK_BIN (widget)->child)))); } else if (GTK_IS_TEXT_VIEW (widget)) { GtkTextIter start, end; GtkTextBuffer *buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (widget)); gtk_text_buffer_get_start_iter (buffer, &start); gtk_text_buffer_get_end_iter (buffer, &end); return g_list_append (list, gtk_text_buffer_get_text (buffer, &start, &end, FALSE)); } else if (GTK_IS_CONTAINER (widget)) { GList *c, *children = gtk_container_get_children (GTK_CONTAINER (widget)); for (c = children; c; c = c->next) { list = contacts_entries_get_values ( GTK_WIDGET (c->data), list); } g_list_free (children); } return list; } static void contacts_chooser_treeview_row_activated (GtkTreeView *tree, gpointer data) { GtkWidget *dialog = g_object_get_data (G_OBJECT (tree), "parent_dialog"); gtk_dialog_response (GTK_DIALOG (dialog), GTK_RESPONSE_OK); } gboolean contacts_chooser (ContactsData *data, const gchar *title, const gchar *label_markup, GList *choices, GList *chosen, gboolean allow_custom,gboolean multiple_choice, GList **results) { GList *c, *d, *widgets; GtkWidget *label_widget; GtkWidget *dialog = data->ui->chooser_dialog; GtkTreeView *tree = GTK_TREE_VIEW (data->ui->chooser_treeview); GtkTreeModel *model = gtk_tree_view_get_model (tree); GtkWidget *add_custom = data->ui->chooser_add_hbox; gint dialog_code; gboolean returnval = FALSE; if (allow_custom) gtk_widget_show (add_custom); else gtk_widget_hide (add_custom); label_widget = data->ui->chooser_label; if (label_markup) { gtk_label_set_markup (GTK_LABEL (label_widget), label_markup); gtk_widget_show (label_widget); } else { gtk_widget_hide (label_widget); } gtk_list_store_clear (GTK_LIST_STORE (model)); for (c = choices, d = chosen; c; c = c->next) { GtkTreeIter iter; gtk_list_store_append (GTK_LIST_STORE (model), &iter); gtk_list_store_set (GTK_LIST_STORE (model), &iter, CHOOSER_NAME_COL, (const gchar *)c->data, -1); if (multiple_choice) { if (d) { gboolean chosen = (gboolean)GPOINTER_TO_INT (d->data); gtk_list_store_set (GTK_LIST_STORE (model), &iter, CHOOSER_TICK_COL, chosen, -1); d = d->next; } else { gtk_list_store_set (GTK_LIST_STORE (model), &iter, CHOOSER_TICK_COL, FALSE, -1); } } } if (multiple_choice) gtk_tree_view_column_set_visible (gtk_tree_view_get_column ( tree, CHOOSER_TICK_COL), TRUE); else gtk_tree_view_column_set_visible (gtk_tree_view_get_column ( tree, CHOOSER_TICK_COL), FALSE); gtk_window_set_title (GTK_WINDOW (dialog), title); widgets = contacts_set_widgets_desensitive ( data->ui->main_window); /* * Set the parent dialog as data on the treeview. * Passing the dialog through the user data callback causes problems * because of the interaction between the application and the dialog * main loops. */ g_object_set_data (G_OBJECT (tree), "parent_dialog", dialog); if (!multiple_choice) g_signal_connect (G_OBJECT(tree), "row-activated", (GCallback) contacts_chooser_treeview_row_activated, NULL); dialog_code = gtk_dialog_run (GTK_DIALOG (dialog)); gtk_widget_hide (dialog); if (dialog_code == GTK_RESPONSE_OK) { if (multiple_choice) { GtkTreeIter iter; gboolean valid = gtk_tree_model_get_iter_first (model, &iter); while (valid) { gboolean selected; gtk_tree_model_get (model, &iter, CHOOSER_TICK_COL, &selected, -1); if (selected) { gchar *name; gtk_tree_model_get (model, &iter, CHOOSER_NAME_COL, &name, -1); *results = g_list_append (*results, name); } valid = gtk_tree_model_iter_next (model, &iter); } returnval = TRUE; } else { gchar *selection_name; GtkTreeSelection *selection = gtk_tree_view_get_selection (tree); GtkTreeIter iter; if (!gtk_tree_selection_get_selected ( selection, NULL, &iter)) { returnval = FALSE; } else { gtk_tree_model_get (model, &iter, CHOOSER_NAME_COL, &selection_name, -1); *results = g_list_append (NULL, selection_name); returnval = TRUE; } } } contacts_set_widgets_sensitive (widgets); g_list_free (widgets); return returnval; } static GList * contacts_set_widget_desensitive_recurse (GtkWidget *widget, GList **widgets) { if (GTK_IS_WIDGET (widget)) { gboolean sensitive; g_object_get (G_OBJECT (widget), "sensitive", &sensitive, NULL); if (sensitive) { gtk_widget_set_sensitive (widget, FALSE); *widgets = g_list_append (*widgets, widget); } if (GTK_IS_TABLE (widget) || GTK_IS_HBOX (widget) || GTK_IS_VBOX (widget) || GTK_IS_EVENT_BOX (widget)) { GList *c, *children = gtk_container_get_children ( GTK_CONTAINER (widget)); for (c = children; c; c = c->next) { contacts_set_widget_desensitive_recurse ( c->data, widgets); } g_list_free (children); } } return *widgets; } GList * contacts_set_widgets_desensitive (GtkWidget *widget) { GList *list = NULL; contacts_set_widget_desensitive_recurse (widget, &list); return list; } void contacts_set_widgets_sensitive (GList *widgets) { GList *w; for (w = widgets; w; w = w->next) { gtk_widget_set_sensitive (GTK_WIDGET (w->data), TRUE); } }