From a3b2e00c235aecaaaad14881d0444e840089cf7e Mon Sep 17 00:00:00 2001 From: Milan Crha Date: Tue, 24 Sep 2013 09:21:34 +0200 Subject: Bug #659890 - Merge addressbook 'widgets' and 'merging' libraries These two cross-referenced each other, which could cause runtime issues, thus rather merge them into one. --- addressbook/gui/widgets/Makefile.am | 21 +- .../eab-contact-commit-duplicate-detected.ui | 185 +++++ addressbook/gui/widgets/eab-contact-compare.c | 839 +++++++++++++++++++++ addressbook/gui/widgets/eab-contact-compare.h | 101 +++ .../gui/widgets/eab-contact-duplicate-detected.ui | 219 ++++++ addressbook/gui/widgets/eab-contact-merging.c | 797 +++++++++++++++++++ addressbook/gui/widgets/eab-contact-merging.h | 66 ++ 7 files changed, 2222 insertions(+), 6 deletions(-) create mode 100644 addressbook/gui/widgets/eab-contact-commit-duplicate-detected.ui create mode 100644 addressbook/gui/widgets/eab-contact-compare.c create mode 100644 addressbook/gui/widgets/eab-contact-compare.h create mode 100644 addressbook/gui/widgets/eab-contact-duplicate-detected.ui create mode 100644 addressbook/gui/widgets/eab-contact-merging.c create mode 100644 addressbook/gui/widgets/eab-contact-merging.h (limited to 'addressbook/gui/widgets') diff --git a/addressbook/gui/widgets/Makefile.am b/addressbook/gui/widgets/Makefile.am index f9fea781a8..1c7e8f090e 100644 --- a/addressbook/gui/widgets/Makefile.am +++ b/addressbook/gui/widgets/Makefile.am @@ -11,9 +11,9 @@ libeabwidgets_la_CPPFLAGS = \ -DEVOLUTION_RULEDIR=\"$(ruledir)\" \ -DEVOLUTION_IMAGESDIR=\"${imagesdir}\" \ -DEVOLUTION_PRIVDATADIR=\"${privdatadir}\" \ + -DEVOLUTION_UIDIR=\""$(uidir)"\" \ -I$(top_srcdir) \ -I$(top_srcdir)/addressbook \ - -I$(top_srcdir)/addressbook/gui/merging \ -I$(top_srcdir)/addressbook/util \ -I$(top_builddir)/shell \ $(EVOLUTION_DATA_SERVER_CFLAGS) \ @@ -29,10 +29,14 @@ eabinclude_HEADERS = \ libeabwidgets_la_SOURCES = \ eab-config.c \ + eab-contact-compare.c \ + eab-contact-compare.h \ eab-contact-display.c \ eab-contact-display.h \ eab-contact-formatter.c \ eab-contact-formatter.h \ + eab-contact-merging.c \ + eab-contact-merging.h \ eab-gui-util.c \ eab-gui-util.h \ e-contact-map.c \ @@ -61,12 +65,12 @@ libeabwidgets_la_SOURCES = \ e-addressbook-view.h \ gal-view-minicard.c \ gal-view-minicard.h \ - ea-minicard.c \ - ea-minicard.h \ + ea-minicard.c \ + ea-minicard.h \ ea-minicard-view.c \ ea-minicard-view.h \ - ea-addressbook-view.c \ - ea-addressbook-view.h \ + ea-addressbook-view.c \ + ea-addressbook-view.h \ ea-addressbook.c \ ea-addressbook.h @@ -85,8 +89,13 @@ dist-hook: etspec_DATA= e-addressbook-view.etspec +ui_DATA = \ + eab-contact-duplicate-detected.ui \ + eab-contact-commit-duplicate-detected.ui + EXTRA_DIST = \ $(etspec_DATA) \ - $(rule_DATA) + $(rule_DATA) \ + $(ui_DATA) -include $(top_srcdir)/git.mk diff --git a/addressbook/gui/widgets/eab-contact-commit-duplicate-detected.ui b/addressbook/gui/widgets/eab-contact-commit-duplicate-detected.ui new file mode 100644 index 0000000000..04ed728a56 --- /dev/null +++ b/addressbook/gui/widgets/eab-contact-commit-duplicate-detected.ui @@ -0,0 +1,185 @@ + + + + + 6 + True + Duplicate Contact Detected + GTK_WINDOW_TOPLEVEL + GTK_WIN_POS_CENTER_ON_PARENT + False + True + False + + + True + False + 6 + + + True + GTK_BUTTONBOX_END + + + True + True + True + gtk-cancel + True + GTK_RELIEF_NORMAL + + + + + True + True + True + gtk-save + True + GTK_RELIEF_NORMAL + + + + + 0 + False + True + GTK_PACK_END + + + + + 6 + True + 5 + 2 + False + 6 + 6 + + + True + + + 1 + 2 + 4 + 5 + + + + + True + The name or email of this contact already exists in this folder. Would you like to save the changes anyway? + False + False + GTK_JUSTIFY_LEFT + False + False + 0 + 0.5 + 0 + 0 + + + 1 + 2 + 0 + 1 + fill + + + + + True + Conflicting Contact: + False + False + GTK_JUSTIFY_CENTER + False + False + 0 + 0.5 + 0 + 0 + + + 1 + 2 + 3 + 4 + fill + + + + + True + Changed Contact: + False + False + GTK_JUSTIFY_CENTER + False + False + 0 + 0.5 + 0 + 0 + + + 1 + 2 + 1 + 2 + fill + + + + + + True + + + 1 + 2 + 2 + 3 + + + + + True + 0.5 + 0 + 1 + 0 + + + True + avatar-default + + + + + 0 + 1 + 0 + 5 + fill + fill + + + + + 0 + True + True + + + + + + button3 + button4 + + + diff --git a/addressbook/gui/widgets/eab-contact-compare.c b/addressbook/gui/widgets/eab-contact-compare.c new file mode 100644 index 0000000000..bfca37cbff --- /dev/null +++ b/addressbook/gui/widgets/eab-contact-compare.c @@ -0,0 +1,839 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see + * + * + * Authors: + * Jon Trowbridge + * Chris Toshok + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include + +#include "e-util/e-util.h" + +#include "addressbook/util/eab-book-util.h" +#include "eab-contact-compare.h" + +/* This is an "optimistic" combiner: the best of the two outcomes is + * selected. */ +static EABContactMatchType +combine_comparisons (EABContactMatchType prev, + EABContactMatchType new_info) +{ + if (new_info == EAB_CONTACT_MATCH_NOT_APPLICABLE) + return prev; + return (EABContactMatchType) MAX ((gint) prev, (gint) new_info); +} + +/*** Name comparisons ***/ + +/* This *so* doesn't belong here... at least not implemented in a + * sucky way like this. But it can be fixed later. */ + +/* This is very Anglocentric. */ +static const gchar *name_synonyms[][2] = { + { "jon", "john" }, /* Ah, the hacker's perogative */ + { "joseph", "joe" }, + { "robert", "bob" }, + { "gene", "jean" }, + { "jesse", "jessie" }, + { "ian", "iain" }, + { "richard", "dick" }, + { "william", "bill" }, + { "william", "will" }, + { "anthony", "tony" }, + { "michael", "mike" }, + { "eric", "erik" }, + { "elizabeth", "liz" }, + { "jeff", "geoff" }, + { "jeff", "geoffrey" }, + { "tom", "thomas" }, + { "dave", "david" }, + { "jim", "james" }, + { "abigal", "abby" }, + { "amanda", "amy" }, + { "amanda", "manda" }, + { "jennifer", "jenny" }, + { "christopher", "chris" }, + { "rebecca", "becca" }, + { "rebecca", "becky" }, + { "anderson", "andersen" }, + { "johnson", "johnsen" }, + /* We could go on and on... */ + /* We should add soundex here. */ + { NULL, NULL } +}; + +static gboolean +name_fragment_match_with_synonyms (const gchar *a, + const gchar *b, + gboolean strict) +{ + gint i; + + if (!(a && b && *a && *b)) + return FALSE; + + if (!e_utf8_casefold_collate (a, b)) + return TRUE; + + /* Check for nicknames. Yes, the linear search blows. */ + for (i = 0; name_synonyms[i][0]; ++i) { + + if (!e_utf8_casefold_collate (name_synonyms[i][0], a) + && !e_utf8_casefold_collate (name_synonyms[i][1], b)) + return TRUE; + + if (!e_utf8_casefold_collate (name_synonyms[i][0], b) + && !e_utf8_casefold_collate (name_synonyms[i][1], a)) + return TRUE; + } + + return FALSE; +} + +EABContactMatchType +eab_contact_compare_name_to_string (EContact *contact, + const gchar *str) +{ + return eab_contact_compare_name_to_string_full (contact, str, FALSE, NULL, NULL, NULL); +} + +EABContactMatchType +eab_contact_compare_name_to_string_full (EContact *contact, + const gchar *str, + gboolean allow_partial_matches, + gint *matched_parts_out, + EABContactMatchPart *first_matched_part_out, + gint *matched_character_count_out) +{ + gchar **namev, **givenv = NULL, **addv = NULL, **familyv = NULL; + + gint matched_parts = EAB_CONTACT_MATCH_PART_NONE; + EABContactMatchPart first_matched_part = EAB_CONTACT_MATCH_PART_NONE; + EABContactMatchPart this_part_match = EAB_CONTACT_MATCH_PART_NOT_APPLICABLE; + EABContactMatchType match_type; + EContactName *contact_name; + + gint match_count = 0, matched_character_count = 0, fragment_count; + gint i, j; + gchar *str_cpy, *s; + + g_return_val_if_fail (E_IS_CONTACT (contact), EAB_CONTACT_MATCH_NOT_APPLICABLE); + + if (!e_contact_get_const (contact, E_CONTACT_FULL_NAME)) + return EAB_CONTACT_MATCH_NOT_APPLICABLE; + if (str == NULL) + return EAB_CONTACT_MATCH_NOT_APPLICABLE; + + str_cpy = s = g_strdup (str); + while (*s) { + if (*s == ',' || *s == '"') + *s = ' '; + ++s; + } + namev = g_strsplit (str_cpy, " ", 0); + g_free (str_cpy); + + contact_name = e_contact_get (contact, E_CONTACT_NAME); + + if (contact_name->given) + givenv = g_strsplit (contact_name->given, " ", 0); + if (contact_name->additional) + addv = g_strsplit (contact_name->additional, " ", 0); + if (contact_name->family) + familyv = g_strsplit (contact_name->family, " ", 0); + + e_contact_name_free (contact_name); + + fragment_count = 0; + for (i = 0; givenv && givenv[i]; ++i) + ++fragment_count; + for (i = 0; addv && addv[i]; ++i) + ++fragment_count; + for (i = 0; familyv && familyv[i]; ++i) + ++fragment_count; + + for (i = 0; namev[i] && this_part_match != EAB_CONTACT_MATCH_PART_NONE; ++i) { + + if (*namev[i]) { + + this_part_match = EAB_CONTACT_MATCH_PART_NONE; + + /* When we are allowing partials, we are strict about the matches we allow. + * Does this make sense? Not really, but it does the right thing for the purposes + * of completion. */ + + if (givenv && this_part_match == EAB_CONTACT_MATCH_PART_NONE) { + for (j = 0; givenv[j]; ++j) { + if (name_fragment_match_with_synonyms (givenv[j], namev[i], allow_partial_matches)) { + + this_part_match = EAB_CONTACT_MATCH_PART_GIVEN_NAME; + + /* We remove a piece of a name once it has been matched against, so + * that "john john" won't match "john doe". */ + g_free (givenv[j]); + givenv[j] = g_strdup (""); + break; + } + } + } + + if (addv && this_part_match == EAB_CONTACT_MATCH_PART_NONE) { + for (j = 0; addv[j]; ++j) { + if (name_fragment_match_with_synonyms (addv[j], namev[i], allow_partial_matches)) { + + this_part_match = EAB_CONTACT_MATCH_PART_ADDITIONAL_NAME; + + g_free (addv[j]); + addv[j] = g_strdup (""); + break; + } + } + } + + if (familyv && this_part_match == EAB_CONTACT_MATCH_PART_NONE) { + for (j = 0; familyv[j]; ++j) { + if (allow_partial_matches ? name_fragment_match_with_synonyms (familyv[j], namev[i], allow_partial_matches) + : !e_utf8_casefold_collate (familyv[j], namev[i])) { + + this_part_match = EAB_CONTACT_MATCH_PART_FAMILY_NAME; + + g_free (familyv[j]); + familyv[j] = g_strdup (""); + break; + } + } + } + + if (this_part_match != EAB_CONTACT_MATCH_PART_NONE) { + ++match_count; + matched_character_count += g_utf8_strlen (namev[i], -1); + matched_parts |= this_part_match; + if (first_matched_part == EAB_CONTACT_MATCH_PART_NONE) + first_matched_part = this_part_match; + } + } + } + + match_type = EAB_CONTACT_MATCH_NONE; + + if (this_part_match != EAB_CONTACT_MATCH_PART_NONE) { + + if (match_count > 0) + match_type = EAB_CONTACT_MATCH_VAGUE; + + if (fragment_count == match_count) { + + match_type = EAB_CONTACT_MATCH_EXACT; + + } else if (fragment_count == match_count + 1) { + + match_type = EAB_CONTACT_MATCH_PARTIAL; + + } + } + + if (matched_parts_out) + *matched_parts_out = matched_parts; + if (first_matched_part_out) + *first_matched_part_out = first_matched_part; + if (matched_character_count_out) + *matched_character_count_out = matched_character_count; + + g_strfreev (namev); + g_strfreev (givenv); + g_strfreev (addv); + g_strfreev (familyv); + + return match_type; +} + +EABContactMatchType +eab_contact_compare_file_as (EContact *contact1, + EContact *contact2) +{ + EABContactMatchType match_type; + gchar *a, *b; + + g_return_val_if_fail (E_IS_CONTACT (contact1), EAB_CONTACT_MATCH_NOT_APPLICABLE); + g_return_val_if_fail (E_IS_CONTACT (contact2), EAB_CONTACT_MATCH_NOT_APPLICABLE); + + a = e_contact_get (contact1, E_CONTACT_FILE_AS); + b = e_contact_get (contact2, E_CONTACT_FILE_AS); + + if (a == NULL || b == NULL) { + g_free (a); + g_free (b); + return EAB_CONTACT_MATCH_NOT_APPLICABLE; + } + + if (!strcmp (a, b)) + match_type = EAB_CONTACT_MATCH_EXACT; + else if (g_utf8_validate (a, -1, NULL) && g_utf8_validate (b, -1, NULL) && + !g_utf8_collate (a, b)) + match_type = EAB_CONTACT_MATCH_PARTIAL; + else + match_type = EAB_CONTACT_MATCH_NONE; + + g_free (a); + g_free (b); + return match_type; +} + +EABContactMatchType +eab_contact_compare_name (EContact *contact1, + EContact *contact2) +{ + EContactName *a, *b; + gint matches = 0, possible = 0; + gboolean family_match = FALSE; + + g_return_val_if_fail (E_IS_CONTACT (contact1), EAB_CONTACT_MATCH_NOT_APPLICABLE); + g_return_val_if_fail (E_IS_CONTACT (contact2), EAB_CONTACT_MATCH_NOT_APPLICABLE); + + a = e_contact_get (contact1, E_CONTACT_NAME); + b = e_contact_get (contact2, E_CONTACT_NAME); + + if (a == NULL || b == NULL) { + g_free (a); + g_free (b); + return EAB_CONTACT_MATCH_NOT_APPLICABLE; + } + + if (a->given && b->given && *a->given && *b->given) { + ++possible; + if (name_fragment_match_with_synonyms (a->given, b->given, FALSE /* both inputs are complete */)) { + ++matches; + } + } + + if (a->additional && b->additional && *a->additional && *b->additional) { + ++possible; + if (name_fragment_match_with_synonyms (a->additional, b->additional, FALSE /* both inputs are complete */)) { + ++matches; + } + } + + if (a->family && b->family && *a->family && *b->family) { + ++possible; + /* We don't allow "loose matching" (i.e. John vs. Jon) on family names */ + if (!e_utf8_casefold_collate (a->family, b->family)) { + ++matches; + family_match = TRUE; + } + } + + e_contact_name_free (a); + e_contact_name_free (b); + + /* Now look at the # of matches and try to intelligently map + * an EAB_CONTACT_MATCH_* type to it. Special consideration is given + * to family-name matches. */ + + if (possible == 0) + return EAB_CONTACT_MATCH_NOT_APPLICABLE; + + if (possible == 1) + return family_match ? EAB_CONTACT_MATCH_VAGUE : EAB_CONTACT_MATCH_NONE; + + if (possible == matches) + return family_match ? EAB_CONTACT_MATCH_EXACT : EAB_CONTACT_MATCH_PARTIAL; + + if (possible == matches + 1) + return family_match ? EAB_CONTACT_MATCH_VAGUE : EAB_CONTACT_MATCH_NONE; + + return EAB_CONTACT_MATCH_NONE; +} + +/*** Nickname Comparisons ***/ + +EABContactMatchType +eab_contact_compare_nickname (EContact *contact1, + EContact *contact2) +{ + g_return_val_if_fail (contact1 && E_IS_CONTACT (contact1), EAB_CONTACT_MATCH_NOT_APPLICABLE); + g_return_val_if_fail (contact2 && E_IS_CONTACT (contact2), EAB_CONTACT_MATCH_NOT_APPLICABLE); + + return EAB_CONTACT_MATCH_NOT_APPLICABLE; +} + +/*** E-mail Comparisons ***/ + +static gboolean +match_email_username (const gchar *addr1, + const gchar *addr2) +{ + gint c1, c2; + if (addr1 == NULL || addr2 == NULL) + return FALSE; + + while (*addr1 && *addr2 && *addr1 != '@' && *addr2 != '@') { + c1 = isupper (*addr1) ? tolower (*addr1) : *addr1; + c2 = isupper (*addr2) ? tolower (*addr2) : *addr2; + if (c1 != c2) + return FALSE; + ++addr1; + ++addr2; + } + + return *addr1 == *addr2; +} + +static gboolean +match_email_hostname (const gchar *addr1, + const gchar *addr2) +{ + gint c1, c2; + gboolean seen_at1, seen_at2; + if (addr1 == NULL || addr2 == NULL) + return FALSE; + + /* Walk to the end of each string. */ + seen_at1 = FALSE; + if (*addr1) { + while (*addr1) { + if (*addr1 == '@') + seen_at1 = TRUE; + ++addr1; + } + --addr1; + } + + seen_at2 = FALSE; + if (*addr2) { + while (*addr2) { + if (*addr2 == '@') + seen_at2 = TRUE; + ++addr2; + } + --addr2; + } + + if (!seen_at1 && !seen_at2) + return TRUE; + if (!seen_at1 || !seen_at2) + return FALSE; + + while (*addr1 != '@' && *addr2 != '@') { + c1 = isupper (*addr1) ? tolower (*addr1) : *addr1; + c2 = isupper (*addr2) ? tolower (*addr2) : *addr2; + if (c1 != c2) + return FALSE; + --addr1; + --addr2; + } + if ((*addr1 == '@' && *addr2 != '@') || (*addr2 == '@' && *addr1 != '@')) + return FALSE; + + return TRUE; +} + +static EABContactMatchType +compare_email_addresses (const gchar *addr1, + const gchar *addr2) +{ + if (addr1 == NULL || *addr1 == 0 || + addr2 == NULL || *addr2 == 0) + return EAB_CONTACT_MATCH_NOT_APPLICABLE; + + if (match_email_username (addr1, addr2)) + return match_email_hostname (addr1, addr2) ? EAB_CONTACT_MATCH_EXACT : EAB_CONTACT_MATCH_VAGUE; + + return EAB_CONTACT_MATCH_NONE; +} + +EABContactMatchType +eab_contact_compare_email (EContact *contact1, + EContact *contact2) +{ + EABContactMatchType match = EAB_CONTACT_MATCH_NOT_APPLICABLE; + GList *contact1_email, *contact2_email; + GList *i1, *i2; + + g_return_val_if_fail (contact1 && E_IS_CONTACT (contact1), EAB_CONTACT_MATCH_NOT_APPLICABLE); + g_return_val_if_fail (contact2 && E_IS_CONTACT (contact2), EAB_CONTACT_MATCH_NOT_APPLICABLE); + + contact1_email = e_contact_get (contact1, E_CONTACT_EMAIL); + contact2_email = e_contact_get (contact2, E_CONTACT_EMAIL); + + if (contact1_email == NULL || contact2_email == NULL) { + g_list_foreach (contact1_email, (GFunc) g_free, NULL); + g_list_free (contact1_email); + + g_list_foreach (contact2_email, (GFunc) g_free, NULL); + g_list_free (contact2_email); + return EAB_CONTACT_MATCH_NOT_APPLICABLE; + } + + i1 = contact1_email; + + /* Do pairwise-comparisons on all of the e-mail addresses. If + * we find an exact match, there is no reason to keep + * checking. */ + while (i1 && match != EAB_CONTACT_MATCH_EXACT) { + gchar *addr1 = (gchar *) i1->data; + + i2 = contact2_email; + while (i2 && match != EAB_CONTACT_MATCH_EXACT) { + gchar *addr2 = (gchar *) i2->data; + + match = combine_comparisons (match, compare_email_addresses (addr1, addr2)); + + i2 = i2->next; + } + + i1 = i1->next; + } + + g_list_foreach (contact1_email, (GFunc) g_free, NULL); + g_list_free (contact1_email); + + g_list_foreach (contact2_email, (GFunc) g_free, NULL); + g_list_free (contact2_email); + + return match; +} + +EABContactMatchType +eab_contact_compare_address (EContact *contact1, + EContact *contact2) +{ + g_return_val_if_fail (contact1 && E_IS_CONTACT (contact1), EAB_CONTACT_MATCH_NOT_APPLICABLE); + g_return_val_if_fail (contact2 && E_IS_CONTACT (contact2), EAB_CONTACT_MATCH_NOT_APPLICABLE); + + /* Unimplemented */ + + return EAB_CONTACT_MATCH_NOT_APPLICABLE; +} + +EABContactMatchType +eab_contact_compare_telephone (EContact *contact1, + EContact *contact2) +{ + g_return_val_if_fail (contact1 && E_IS_CONTACT (contact1), EAB_CONTACT_MATCH_NOT_APPLICABLE); + g_return_val_if_fail (contact2 && E_IS_CONTACT (contact2), EAB_CONTACT_MATCH_NOT_APPLICABLE); + + /* Unimplemented */ + + return EAB_CONTACT_MATCH_NOT_APPLICABLE; +} + +EABContactMatchType +eab_contact_compare (EContact *contact1, + EContact *contact2) +{ + EABContactMatchType result; + + g_return_val_if_fail (contact1 && E_IS_CONTACT (contact1), EAB_CONTACT_MATCH_NOT_APPLICABLE); + g_return_val_if_fail (contact2 && E_IS_CONTACT (contact2), EAB_CONTACT_MATCH_NOT_APPLICABLE); + + result = EAB_CONTACT_MATCH_NONE; + if (!e_contact_get (contact1, E_CONTACT_IS_LIST)) { + result = combine_comparisons (result, eab_contact_compare_name (contact1, contact2)); + result = combine_comparisons (result, eab_contact_compare_nickname (contact1, contact2)); + if (!e_contact_get (contact2, E_CONTACT_IS_LIST)) + result = combine_comparisons (result, eab_contact_compare_email (contact1, contact2)); + result = combine_comparisons (result, eab_contact_compare_address (contact1, contact2)); + result = combine_comparisons (result, eab_contact_compare_telephone (contact1, contact2)); + } + result = combine_comparisons (result, eab_contact_compare_file_as (contact1, contact2)); + + return result; +} + +typedef struct _MatchSearchInfo MatchSearchInfo; +struct _MatchSearchInfo { + EContact *contact; + GList *avoid; + EABContactMatchQueryCallback cb; + gpointer closure; +}; + +static void +match_search_info_free (MatchSearchInfo *info) +{ + if (info) { + g_object_unref (info->contact); + + /* This should already have been deallocated, but just in case... */ + if (info->avoid) { + g_list_foreach (info->avoid, (GFunc) g_object_unref, NULL); + g_list_free (info->avoid); + info->avoid = NULL; + } + + g_free (info); + } +} + +static void +query_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + MatchSearchInfo *info = (MatchSearchInfo *) user_data; + EABContactMatchType best_match = EAB_CONTACT_MATCH_NONE; + EContact *best_contact = NULL; + EBookClient *book_client = E_BOOK_CLIENT (source_object); + GSList *remaining_contacts = NULL; + GSList *contacts = NULL; + GError *error = NULL; + const GSList *ii; + + if (result != NULL) + e_book_client_get_contacts_finish ( + book_client, result, &contacts, &error); + + if (error != NULL) { + g_warning ( + "%s: Failed to get contacts: %s\n", + G_STRFUNC, error->message); + g_error_free (error); + + info->cb ( + info->contact, NULL, + EAB_CONTACT_MATCH_NONE, + info->closure); + + match_search_info_free (info); + g_object_unref (book_client); + return; + } + + /* remove the contacts we're to avoid from the list, if they're present */ + for (ii = contacts; ii != NULL; ii = g_slist_next (ii)) { + EContact *this_contact = E_CONTACT (ii->data); + const gchar *this_uid; + GList *iterator; + gboolean avoid = FALSE; + + this_uid = e_contact_get_const (this_contact, E_CONTACT_UID); + if (!this_uid) + continue; + + for (iterator = info->avoid; iterator; iterator = iterator->next) { + const gchar *avoid_uid; + + avoid_uid = e_contact_get_const (iterator->data, E_CONTACT_UID); + if (!avoid_uid) + continue; + + if (!strcmp (avoid_uid, this_uid)) { + avoid = TRUE; + break; + } + } + if (!avoid) + remaining_contacts = g_slist_prepend (remaining_contacts, g_object_ref (this_contact)); + } + + remaining_contacts = g_slist_reverse (remaining_contacts); + + for (ii = remaining_contacts; ii != NULL; ii = g_slist_next (ii)) { + EContact *this_contact = E_CONTACT (ii->data); + EABContactMatchType this_match = eab_contact_compare (info->contact, this_contact); + if ((gint) this_match > (gint) best_match) { + best_match = this_match; + best_contact = this_contact; + } + } + + if (best_contact) + best_contact = g_object_ref (best_contact); + + g_slist_free_full (contacts, (GDestroyNotify) g_object_unref); + g_slist_free_full (remaining_contacts, (GDestroyNotify) g_object_unref); + + info->cb (info->contact, best_contact, best_match, info->closure); + match_search_info_free (info); + g_object_unref (book_client); + if (best_contact) + g_object_unref (best_contact); +} + +#define MAX_QUERY_PARTS 10 +static void +use_common_book_client (EBookClient *book_client, + MatchSearchInfo *info) +{ + EContact *contact = info->contact; + EContactName *contact_name; + GList *contact_email; + gchar *query_parts[MAX_QUERY_PARTS + 1]; + gint p = 0; + gchar *contact_file_as, *qj; + EBookQuery *query = NULL; + gint i; + + if (book_client == NULL) { + info->cb (info->contact, NULL, EAB_CONTACT_MATCH_NONE, info->closure); + match_search_info_free (info); + return; + } + + contact_file_as = e_contact_get (contact, E_CONTACT_FILE_AS); + if (contact_file_as) { + query_parts[p++] = g_strdup_printf ("(contains \"file_as\" \"%s\")", contact_file_as); + g_free (contact_file_as); + } + + if (!e_contact_get (contact, E_CONTACT_IS_LIST)) { + contact_name = e_contact_get (contact, E_CONTACT_NAME); + if (contact_name) { + if (contact_name->given && *contact_name->given) + query_parts[p++] = g_strdup_printf ("(contains \"full_name\" \"%s\")", contact_name->given); + + if (contact_name->additional && *contact_name->additional) + query_parts[p++] = g_strdup_printf ("(contains \"full_name\" \"%s\")", contact_name->additional); + + if (contact_name->family && *contact_name->family) + query_parts[p++] = g_strdup_printf ("(contains \"full_name\" \"%s\")", contact_name->family); + + e_contact_name_free (contact_name); + } + + contact_email = e_contact_get (contact, E_CONTACT_EMAIL); + if (contact_email) { + GList *iter; + for (iter = contact_email; iter && p < MAX_QUERY_PARTS; iter = iter->next) { + gchar *addr = g_strdup (iter->data); + if (addr && *addr) { + gchar *s = addr; + while (*s) { + if (*s == '@') { + *s = '\0'; + break; + } + ++s; + } + query_parts[p++] = g_strdup_printf ("(beginswith \"email\" \"%s\")", addr); + g_free (addr); + } + } + } + g_list_foreach (contact_email, (GFunc) g_free, NULL); + g_list_free (contact_email); + } + + /* Build up our full query from the parts. */ + query_parts[p] = NULL; + qj = g_strjoinv (" ", query_parts); + for (i = 0; query_parts[i] != NULL; i++) + g_free (query_parts[i]); + if (p > 1) { + gchar *s; + s = g_strdup_printf ("(or %s)", qj); + query = e_book_query_from_string (s); + g_free (s); + } + else if (p == 1) { + query = e_book_query_from_string (qj); + } + else { + query = NULL; + } + + if (query) { + gchar *query_str = e_book_query_to_string (query); + + e_book_client_get_contacts (book_client, query_str, NULL, query_cb, info); + + g_free (query_str); + } else + query_cb (G_OBJECT (book_client), NULL, info); + + g_free (qj); + if (query) + e_book_query_unref (query); +} + +static void +book_client_connect_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + MatchSearchInfo *info = user_data; + EClient *client; + + client = e_book_client_connect_finish (result, NULL); + + /* Client may be NULL; don't use a type cast macro. */ + use_common_book_client ((EBookClient *) client, info); +} + +void +eab_contact_locate_match (ESourceRegistry *registry, + EContact *contact, + EABContactMatchQueryCallback cb, + gpointer closure) +{ + eab_contact_locate_match_full ( + registry, NULL, contact, NULL, cb, closure); +} + +/** + * e_contact_locate_match_full: + * @registry: an #ESourceRegistry + * @book: The book to look in. If this is NULL, use the default + * addressbook. + * @contact: The contact to compare to. + * @avoid: A list of contacts to not match. These will not show up in the search. + * @cb: The function to call. + * @closure: The closure to add to the call. + * + * Look for the best match and return it using the EABContactMatchQueryCallback. + **/ +void +eab_contact_locate_match_full (ESourceRegistry *registry, + EBookClient *book_client, + EContact *contact, + GList *avoid, + EABContactMatchQueryCallback cb, + gpointer closure) +{ + MatchSearchInfo *info; + ESource *source; + + g_return_if_fail (E_IS_SOURCE_REGISTRY (registry)); + g_return_if_fail (E_IS_CONTACT (contact)); + g_return_if_fail (cb != NULL); + + info = g_new0 (MatchSearchInfo, 1); + info->contact = g_object_ref (contact); + info->cb = cb; + info->closure = closure; + info->avoid = g_list_copy (avoid); + g_list_foreach (info->avoid, (GFunc) g_object_ref, NULL); + + if (book_client) { + use_common_book_client (g_object_ref (book_client), info); + return; + } + + source = e_source_registry_ref_default_address_book (registry); + + e_book_client_connect (source, NULL, book_client_connect_cb, info); + + g_object_unref (source); +} + diff --git a/addressbook/gui/widgets/eab-contact-compare.h b/addressbook/gui/widgets/eab-contact-compare.h new file mode 100644 index 0000000000..f6b05705cf --- /dev/null +++ b/addressbook/gui/widgets/eab-contact-compare.h @@ -0,0 +1,101 @@ +/* + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see + * + * + * Authors: + * Jon Trowbridge + * Chris Toshok + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ + +#ifndef __EAB_CONTACT_COMPARE_H__ +#define __EAB_CONTACT_COMPARE_H__ + +#include + +typedef enum { + EAB_CONTACT_MATCH_NOT_APPLICABLE = 0, + EAB_CONTACT_MATCH_NONE = 1, + EAB_CONTACT_MATCH_VAGUE = 2, + EAB_CONTACT_MATCH_PARTIAL = 3, + EAB_CONTACT_MATCH_EXACT = 4 +} EABContactMatchType; + +typedef enum { + EAB_CONTACT_MATCH_PART_NOT_APPLICABLE = -1, + EAB_CONTACT_MATCH_PART_NONE = 0, + EAB_CONTACT_MATCH_PART_GIVEN_NAME = 1 << 0, + EAB_CONTACT_MATCH_PART_ADDITIONAL_NAME = 1 << 2, + EAB_CONTACT_MATCH_PART_FAMILY_NAME = 1 << 3 +} EABContactMatchPart; + +typedef void (*EABContactMatchQueryCallback) (EContact *contact, + EContact *match, + EABContactMatchType type, + gpointer closure); + +EABContactMatchType + eab_contact_compare_name_to_string + (EContact *contact, + const gchar *str); + +EABContactMatchType + eab_contact_compare_name_to_string_full + (EContact *contact, + const gchar *str, + gboolean allow_partial_matches, + gint *matched_parts, + EABContactMatchPart *first_matched_part, + gint *matched_character_count); + +EABContactMatchType + eab_contact_compare_file_as (EContact *contact1, + EContact *contact2); +EABContactMatchType + eab_contact_compare_name (EContact *contact1, + EContact *contact2); +EABContactMatchType + eab_contact_compare_nickname (EContact *contact1, + EContact *contact2); +EABContactMatchType + eab_contact_compare_email (EContact *contact1, + EContact *contact2); +EABContactMatchType + eab_contact_compare_address (EContact *contact1, + EContact *contact2); +EABContactMatchType + eab_contact_compare_telephone (EContact *contact1, + EContact *contact2); + +EABContactMatchType + eab_contact_compare (EContact *contact1, + EContact *contact2); + +void eab_contact_locate_match (ESourceRegistry *registry, + EContact *contact, + EABContactMatchQueryCallback cb, + gpointer closure); +void eab_contact_locate_match_full (ESourceRegistry *registry, + EBookClient *book_client, + EContact *contact, + GList *avoid, + EABContactMatchQueryCallback cb, + gpointer closure); + +#endif /* __E_CONTACT_COMPARE_H__ */ + diff --git a/addressbook/gui/widgets/eab-contact-duplicate-detected.ui b/addressbook/gui/widgets/eab-contact-duplicate-detected.ui new file mode 100644 index 0000000000..124b9c2787 --- /dev/null +++ b/addressbook/gui/widgets/eab-contact-duplicate-detected.ui @@ -0,0 +1,219 @@ + + + + + True + Duplicate Contact Detected + GTK_WINDOW_TOPLEVEL + GTK_WIN_POS_NONE + False + True + False + True + False + False + GDK_WINDOW_TYPE_HINT_DIALOG + GDK_GRAVITY_NORTH_WEST + 400 + 500 + + + True + False + 0 + + + True + GTK_BUTTONBOX_END + + + True + True + True + gtk-cancel + True + GTK_RELIEF_NORMAL + True + + + + + True + True + True + gtk-add + True + GTK_RELIEF_NORMAL + True + + + + + True + True + True + _Merge + True + GTK_RELIEF_NORMAL + True + + + + + 0 + False + True + GTK_PACK_END + + + + + True + True + GTK_POLICY_NEVER + GTK_POLICY_AUTOMATIC + GTK_SHADOW_IN + GTK_CORNER_TOP_LEFT + + + + + 12 + True + 5 + 2 + False + 6 + 12 + + + True + + + 1 + 2 + 4 + 5 + + + + + True + The name or email address of this contact already exists +in this folder. Would you like to add it anyway? + False + False + GTK_JUSTIFY_LEFT + False + False + 0 + 0.5 + 0 + 0 + + + 1 + 2 + 0 + 1 + fill + + + + + True + Original Contact: + False + False + GTK_JUSTIFY_CENTER + False + False + 0 + 0.5 + 0 + 0 + + + 1 + 2 + 3 + 4 + fill + + + + + True + New Contact: + False + False + GTK_JUSTIFY_CENTER + False + False + 0 + 0.5 + 0 + 0 + + + 1 + 2 + 1 + 2 + fill + + + + + + True + + + 1 + 2 + 2 + 3 + + + + + True + 0.5 + 0 + 1 + 0 + 0 + 0 + 0 + 0 + + + True + avatar-default + + + + + 0 + 1 + 0 + 5 + fill + fill + + + + + + + + + + + + button4 + button3 + button5 + + + diff --git a/addressbook/gui/widgets/eab-contact-merging.c b/addressbook/gui/widgets/eab-contact-merging.c new file mode 100644 index 0000000000..56fef2bc40 --- /dev/null +++ b/addressbook/gui/widgets/eab-contact-merging.c @@ -0,0 +1,797 @@ +/* + * Code for checking for duplicates when doing EContact work. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see + * + * + * Authors: + * Christopher James Lahey + * Chris Toshok + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "eab-contact-merging.h" +#include "eab-contact-compare.h" +#include +#include +#include "addressbook/gui/widgets/eab-contact-display.h" +#include "e-util/e-util.h" +#include "e-util/e-util-private.h" +#include + +#include + +typedef struct dropdown_data dropdown_data; +typedef enum { + E_CONTACT_MERGING_ADD, + E_CONTACT_MERGING_COMMIT, + E_CONTACT_MERGING_FIND +} EContactMergingOpType; + +typedef struct { + EContactMergingOpType op; + ESourceRegistry *registry; + EBookClient *book_client; + /*contact is the new contact which the user has tried to add to the addressbook*/ + EContact *contact; + /*match is the duplicate contact already existing in the addressbook*/ + EContact *match; + GList *avoid; + EABMergingAsyncCallback cb; + EABMergingIdAsyncCallback id_cb; + EABMergingContactAsyncCallback c_cb; + gpointer closure; +} EContactMergingLookup; + +struct dropdown_data { + EContact *match; + EContactField field; + + /* for E_CONTACT_EMAIL field */ + GList *email_attr_list_item; + EVCardAttribute *email_attr; +}; +static void match_query_callback (EContact *contact, EContact *match, EABContactMatchType type, gpointer closure); + +#define SIMULTANEOUS_MERGING_REQUESTS 20 + +static GList *merging_queue = NULL; +static gint running_merge_requests = 0; + +static void +add_lookup (EContactMergingLookup *lookup) +{ + if (running_merge_requests < SIMULTANEOUS_MERGING_REQUESTS) { + running_merge_requests++; + eab_contact_locate_match_full ( + lookup->registry, lookup->book_client, + lookup->contact, lookup->avoid, + match_query_callback, lookup); + } + else { + merging_queue = g_list_append (merging_queue, lookup); + } +} + +static void +finished_lookup (void) +{ + running_merge_requests--; + + while (running_merge_requests < SIMULTANEOUS_MERGING_REQUESTS) { + EContactMergingLookup *lookup; + + if (!merging_queue) + break; + + lookup = merging_queue->data; + + merging_queue = g_list_remove_link (merging_queue, merging_queue); + + running_merge_requests++; + eab_contact_locate_match_full ( + lookup->registry, lookup->book_client, + lookup->contact, lookup->avoid, + match_query_callback, lookup); + } +} + +static void +free_lookup (EContactMergingLookup *lookup) +{ + g_object_unref (lookup->registry); + g_object_unref (lookup->book_client); + g_object_unref (lookup->contact); + g_list_free (lookup->avoid); + if (lookup->match) + g_object_unref (lookup->match); + g_free (lookup); +} + +static void +final_id_cb (EBookClient *book_client, + const GError *error, + const gchar *id, + gpointer closure) +{ + EContactMergingLookup *lookup = closure; + + if (lookup->id_cb) + lookup->id_cb ( + lookup->book_client, + error, id, lookup->closure); + + free_lookup (lookup); + + finished_lookup (); +} + +static void +final_cb_as_id (EBookClient *book_client, + const GError *error, + gpointer closure) +{ + EContactMergingLookup *lookup = closure; + + if (lookup->id_cb) + lookup->id_cb ( + lookup->book_client, + error, lookup->contact ? + e_contact_get_const ( + lookup->contact, E_CONTACT_UID) : NULL, + lookup->closure); + + free_lookup (lookup); + + finished_lookup (); +} + +static void +final_cb (EBookClient *book_client, + const GError *error, + gpointer closure) +{ + EContactMergingLookup *lookup = closure; + + if (lookup->cb) + lookup->cb (lookup->book_client, error, lookup->closure); + + free_lookup (lookup); + + finished_lookup (); +} + +static void +modify_contact_ready_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + EBookClient *book_client = E_BOOK_CLIENT (source_object); + EContactMergingLookup *lookup = user_data; + GError *error = NULL; + + g_return_if_fail (book_client != NULL); + g_return_if_fail (lookup != NULL); + + e_book_client_modify_contact_finish (book_client, result, &error); + + if (lookup->op == E_CONTACT_MERGING_ADD) + final_cb_as_id (book_client, error, lookup); + else + final_cb (book_client, error, lookup); + + if (error != NULL) + g_error_free (error); +} + +static void +add_contact_ready_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + EBookClient *book_client = E_BOOK_CLIENT (source_object); + EContactMergingLookup *lookup = user_data; + gchar *uid = NULL; + GError *error = NULL; + + g_return_if_fail (book_client != NULL); + g_return_if_fail (lookup != NULL); + + e_book_client_add_contact_finish (book_client, result, &uid, &error); + + final_id_cb (book_client, error, uid, lookup); + + if (error != NULL) + g_error_free (error); +} + +static void +doit (EContactMergingLookup *lookup, + gboolean force_modify) +{ + if (lookup->op == E_CONTACT_MERGING_ADD) { + if (force_modify) + e_book_client_modify_contact (lookup->book_client, lookup->contact, NULL, modify_contact_ready_cb, lookup); + else + e_book_client_add_contact (lookup->book_client, lookup->contact, NULL, add_contact_ready_cb, lookup); + } else if (lookup->op == E_CONTACT_MERGING_COMMIT) + e_book_client_modify_contact (lookup->book_client, lookup->contact, NULL, modify_contact_ready_cb, lookup); +} + +static void +cancelit (EContactMergingLookup *lookup) +{ + GError *error; + + error = g_error_new_literal ( + G_IO_ERROR, G_IO_ERROR_CANCELLED, _("Cancelled")); + + if (lookup->op == E_CONTACT_MERGING_ADD) { + final_id_cb (lookup->book_client, error, NULL, lookup); + } else if (lookup->op == E_CONTACT_MERGING_COMMIT) { + final_cb (lookup->book_client, error, lookup); + } + + g_error_free (error); +} + +static void +dialog_map (GtkWidget *window, + GdkEvent *event, + GtkWidget *table) +{ + GtkAllocation allocation; + gint h, w; + + gtk_widget_get_allocation (table, &allocation); + + /* Spacing around the table */ + w = allocation.width + 30; + /* buttons and outer spacing */ + h = allocation.height + 60; + if (w > 400) + w = 400; + if (h > 450) + h = 450; + + gtk_widget_set_size_request (window, w, h); +} + +static void +dropdown_changed (GtkWidget *dropdown, + dropdown_data *data) +{ + gchar *str = gtk_combo_box_text_get_active_text (GTK_COMBO_BOX_TEXT (dropdown)); + + if (str && *str) + e_contact_set (data->match, data->field, str); + else + e_contact_set (data->match, data->field, NULL); + + g_free (str); +} + +static void +email_dropdown_changed (GtkWidget *dropdown, + dropdown_data *data) +{ + gchar *str = gtk_combo_box_text_get_active_text (GTK_COMBO_BOX_TEXT (dropdown)); + + if (str && *str) + data->email_attr_list_item->data = data->email_attr; + else + data->email_attr_list_item->data = NULL; + + g_free (str); +} + +static void +remove_contact_ready_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + EBookClient *book_client = E_BOOK_CLIENT (source_object); + EContactMergingLookup *lookup = user_data; + GError *error = NULL; + + g_return_if_fail (book_client != NULL); + g_return_if_fail (lookup != NULL); + + e_book_client_remove_contact_finish (book_client, result, &error); + + if (error != NULL) { + g_warning ( + "%s: Failed to remove contact: %s", + G_STRFUNC, error->message); + g_error_free (error); + } + + e_book_client_add_contact ( + book_client, lookup->contact, NULL, + add_contact_ready_cb, lookup); +} + +static gint +mergeit (EContactMergingLookup *lookup) +{ + GtkWidget *scrolled_window, *label, *hbox, *dropdown; + GtkWidget *content_area; + GtkWidget *dialog; + GtkTable *table; + EContactField field; + gchar *string = NULL, *string1 = NULL; + GList *match_email_attr_list, *contact_email_attr_list, *miter, *citer, *use_email_attr_list; + GHashTable *match_emails; /* emails in the 'match' contact */ + gint row = -1; + gint value = 0, result; + + dialog = gtk_dialog_new (); + gtk_window_set_title (GTK_WINDOW (dialog), _("Merge Contact")); + gtk_container_set_border_width (GTK_CONTAINER (dialog), 5); + + content_area = gtk_dialog_get_content_area (GTK_DIALOG (dialog)); + + scrolled_window = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_policy ( + GTK_SCROLLED_WINDOW (scrolled_window), + GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + + table = (GtkTable *) gtk_table_new (20, 2, FALSE); + gtk_container_set_border_width ((GtkContainer *) table, 12); + gtk_table_set_row_spacings (table, 6); + gtk_table_set_col_spacings (table, 2); + + gtk_dialog_add_buttons ( + GTK_DIALOG (dialog), + GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, + _("_Merge"), GTK_RESPONSE_OK, + NULL); + + match_emails = g_hash_table_new_full (camel_strcase_hash, camel_strcase_equal, g_free, NULL); + match_email_attr_list = e_contact_get_attributes (lookup->match, E_CONTACT_EMAIL); + contact_email_attr_list = e_contact_get_attributes (lookup->contact, E_CONTACT_EMAIL); + use_email_attr_list = NULL; + + for (miter = match_email_attr_list; miter; miter = g_list_next (miter)) { + EVCardAttribute *attr = miter->data; + gchar *email; + + email = e_vcard_attribute_get_value (attr); + if (email && *email) { + g_hash_table_insert (match_emails, email, attr); + use_email_attr_list = g_list_prepend (use_email_attr_list, attr); + } else { + g_free (email); + } + } + + use_email_attr_list = g_list_reverse (use_email_attr_list); + + /*we match all the string fields of the already existing contact and the new contact.*/ + for (field = E_CONTACT_FULL_NAME; field != (E_CONTACT_LAST_SIMPLE_STRING -1); field++) { + dropdown_data *data = NULL; + string = (gchar *) e_contact_get_const (lookup->contact, field); + string1 = (gchar *) e_contact_get_const (lookup->match, field); + + /*the field must exist in the new as well as the duplicate contact*/ + if (string && *string) { + if (field == E_CONTACT_EMAIL_1) { + for (citer = contact_email_attr_list; citer; citer = g_list_next (citer)) { + EVCardAttribute *attr = citer->data; + gchar *email; + + email = e_vcard_attribute_get_value (attr); + if (email && *email) { + if (!g_hash_table_lookup (match_emails, email)) { + dropdown_data *data; + + /* the email is not set in both contacts */ + use_email_attr_list = g_list_append (use_email_attr_list, attr); + + row++; + label = gtk_label_new (_("Email")); + hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); + gtk_box_pack_start (GTK_BOX (hbox), (GtkWidget *) label, FALSE, FALSE, 0); + gtk_table_attach_defaults (table, (GtkWidget *) hbox, 0, 1, row, row + 1); + + dropdown = gtk_combo_box_text_new (); + gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (dropdown), email); + + data = g_new0 (dropdown_data, 1); + + gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (dropdown), ""); + + gtk_combo_box_set_active (GTK_COMBO_BOX (dropdown), 0); + data->field = E_CONTACT_EMAIL; + data->match = lookup->match; + data->email_attr_list_item = g_list_last (use_email_attr_list); + data->email_attr = attr; + + g_signal_connect ( + dropdown, "changed", + G_CALLBACK (email_dropdown_changed), data); + g_object_set_data_full (G_OBJECT (dropdown), "eab-contact-merging::dropdown-data", data, g_free); + + hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); + gtk_box_pack_start (GTK_BOX (hbox), (GtkWidget *) dropdown, FALSE, FALSE, 0); + gtk_table_attach_defaults (table, (GtkWidget *) hbox, 1, 2, row, row + 1); + gtk_widget_show ((GtkWidget *) dropdown); + } + } + g_free (email); + } + continue; + } + + if (field == E_CONTACT_EMAIL_2 || field == E_CONTACT_EMAIL_3 || field == E_CONTACT_EMAIL_4) { + /* emails are compared above */ + continue; + } + + if (((field == E_CONTACT_FULL_NAME) && (!g_ascii_strcasecmp (string, string1)))) { + row++; + label = gtk_label_new (e_contact_pretty_name (field)); + hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); + gtk_box_pack_start (GTK_BOX (hbox), (GtkWidget *) label, FALSE, FALSE, 0); + gtk_table_attach_defaults (table, (GtkWidget *) hbox, 0, 1, row, row + 1); + + label = gtk_label_new (string); + hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); + gtk_box_pack_start (GTK_BOX (hbox), (GtkWidget *) label, FALSE, FALSE, 0); + gtk_table_attach_defaults (table, (GtkWidget *) hbox, 1, 2, row, row + 1); + continue; + } + + /*for all string fields except name and email*/ + if (!(string1 && *string1) || (g_ascii_strcasecmp (string, string1))) { + row++; + label = gtk_label_new (e_contact_pretty_name (field)); + hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); + gtk_box_pack_start (GTK_BOX (hbox), (GtkWidget *) label, FALSE, FALSE, 0); + gtk_table_attach_defaults (table, (GtkWidget *) hbox, 0, 1, row, row + 1); + data = g_new0 (dropdown_data, 1); + dropdown = gtk_combo_box_text_new (); + gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (dropdown), string); + e_contact_set (lookup->match, field, string); + + if (string1 && *string1) + gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (dropdown), string1); + else + gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (dropdown), ""); + + gtk_combo_box_set_active (GTK_COMBO_BOX (dropdown), 0); + data->field = field; + data->match = lookup->match; + + if (field == E_CONTACT_NICKNAME || field == E_CONTACT_GIVEN_NAME) + gtk_widget_set_sensitive ((GtkWidget *) dropdown, FALSE); + + g_signal_connect ( + dropdown, "changed", + G_CALLBACK (dropdown_changed), data); + g_object_set_data_full (G_OBJECT (dropdown), "eab-contact-merging::dropdown-data", data, g_free); + + hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); + gtk_box_pack_start (GTK_BOX (hbox), (GtkWidget *) dropdown, FALSE, FALSE, 0); + gtk_table_attach_defaults (table, (GtkWidget *) hbox, 1, 2, row, row + 1); + gtk_widget_show_all ((GtkWidget *) dropdown); + } + } + } + + gtk_window_set_default_size (GTK_WINDOW (dialog), 420, 300); + gtk_scrolled_window_add_with_viewport (GTK_SCROLLED_WINDOW (scrolled_window), GTK_WIDGET (table)); + gtk_box_pack_start (GTK_BOX (content_area), GTK_WIDGET (scrolled_window), TRUE, TRUE, 0); + gtk_widget_show (scrolled_window); + g_signal_connect ( + dialog, "map-event", + G_CALLBACK (dialog_map), table); + gtk_widget_show_all ((GtkWidget *) table); + result = gtk_dialog_run (GTK_DIALOG (dialog)); + + switch (result) { + case GTK_RESPONSE_OK: + citer = NULL; + for (miter = use_email_attr_list; miter; miter = g_list_next (miter)) { + if (miter->data) + citer = g_list_prepend (citer, miter->data); + } + citer = g_list_reverse (citer); + e_contact_set_attributes (lookup->match, E_CONTACT_EMAIL, citer); + g_list_free (citer); + + g_object_unref (lookup->contact); + lookup->contact = g_object_ref (lookup->match); + e_book_client_remove_contact ( + lookup->book_client, + lookup->match, NULL, + remove_contact_ready_cb, lookup); + value = 1; + break; + case GTK_RESPONSE_CANCEL: + default: + value = 0; + break; + } + gtk_widget_destroy (dialog); + g_list_free_full (match_email_attr_list, (GDestroyNotify) e_vcard_attribute_free); + g_list_free_full (contact_email_attr_list, (GDestroyNotify) e_vcard_attribute_free); + g_list_free (use_email_attr_list); + g_hash_table_destroy (match_emails); + + return value; +} + +static gboolean +check_if_same (EContact *contact, + EContact *match) +{ + EContactField field; + GList *email_attr_list; + gint num_of_email; + gchar *str = NULL, *string = NULL, *string1 = NULL; + gboolean res = TRUE; + + email_attr_list = e_contact_get_attributes (match, E_CONTACT_EMAIL); + num_of_email = g_list_length (email_attr_list); + + for (field = E_CONTACT_FULL_NAME; res && field != (E_CONTACT_LAST_SIMPLE_STRING -1); field++) { + + if ((field == E_CONTACT_EMAIL_1 || field == E_CONTACT_EMAIL_2 + || field == E_CONTACT_EMAIL_3 || field == E_CONTACT_EMAIL_4) && (num_of_email < 4)) { + str = (gchar *) e_contact_get_const (contact, field); + switch (num_of_email) + { + case 0: + res = FALSE; + break; + case 1: + if ((str && *str) && (g_ascii_strcasecmp (e_contact_get_const (match, E_CONTACT_EMAIL_1),str))) + res = FALSE; + break; + case 2: + if ((str && *str) && (g_ascii_strcasecmp (str,e_contact_get_const (match, E_CONTACT_EMAIL_1))) && + (g_ascii_strcasecmp (e_contact_get_const (match, E_CONTACT_EMAIL_2),str))) + res = FALSE; + break; + case 3: + if ((str && *str) && (g_ascii_strcasecmp (e_contact_get_const (match, E_CONTACT_EMAIL_1),str)) && + (g_ascii_strcasecmp (e_contact_get_const (match, E_CONTACT_EMAIL_2),str)) && + (g_ascii_strcasecmp (e_contact_get_const (match, E_CONTACT_EMAIL_3),str))) + res = FALSE; + break; + } + } + else { + string = (gchar *) e_contact_get_const (contact, field); + string1 = (gchar *) e_contact_get_const (match, field); + if ((string && *string) && (string1 && *string1) && (g_ascii_strcasecmp (string1, string))) { + res = FALSE; + break; + /*if the field entry exist in either of the contacts,we'll have to give the choice and thus merge button should be sensitive*/ + } else if ((string && *string) && !(string1 && *string1)) { + res = FALSE; + break; + } + } + } + + g_list_free_full (email_attr_list, (GDestroyNotify) e_vcard_attribute_free); + + return res; +} + +static void +response (GtkWidget *dialog, + gint response, + EContactMergingLookup *lookup) +{ + static gint merge_response; + + switch (response) { + case 0: + doit (lookup, FALSE); + break; + case 1: + cancelit (lookup); + break; + case 2: + merge_response = mergeit (lookup); + if (merge_response) + break; + return; + case GTK_RESPONSE_DELETE_EVENT: + cancelit (lookup); + break; + } + + gtk_widget_destroy (dialog); +} + +static void +match_query_callback (EContact *contact, + EContact *match, + EABContactMatchType type, + gpointer closure) +{ + EContactMergingLookup *lookup = closure; + gint flag; + gboolean same_uids; + + if (lookup->op == E_CONTACT_MERGING_FIND) { + if (lookup->c_cb) + lookup->c_cb ( + lookup->book_client, NULL, + (gint) type <= (gint) + EAB_CONTACT_MATCH_VAGUE ? NULL : match, + lookup->closure); + + free_lookup (lookup); + finished_lookup (); + return; + } + + /* if had same UID, then we are editing old contact, thus force commit change to it */ + same_uids = contact && match + && e_contact_get_const (contact, E_CONTACT_UID) + && e_contact_get_const (match, E_CONTACT_UID) + && g_str_equal (e_contact_get_const (contact, E_CONTACT_UID), e_contact_get_const (match, E_CONTACT_UID)); + + if ((gint) type <= (gint) EAB_CONTACT_MATCH_VAGUE || same_uids) { + doit (lookup, same_uids); + } else { + GtkBuilder *builder; + GtkWidget *container; + GtkWidget *merge_button; + GtkWidget *widget; + + builder = gtk_builder_new (); + + lookup->match = g_object_ref (match); + if (lookup->op == E_CONTACT_MERGING_ADD) { + /* Compares all the values of contacts and return true, if they match */ + flag = check_if_same (contact, match); + e_load_ui_builder_definition ( + builder, "eab-contact-duplicate-detected.ui"); + merge_button = e_builder_get_widget (builder, "button5"); + /* Merge Button not sensitive when all values are same */ + if (flag) + gtk_widget_set_sensitive (GTK_WIDGET (merge_button), FALSE); + } else if (lookup->op == E_CONTACT_MERGING_COMMIT) { + e_load_ui_builder_definition ( + builder, "eab-contact-commit-duplicate-detected.ui"); + } else { + doit (lookup, FALSE); + g_object_unref (builder); + return; + } + + widget = e_builder_get_widget (builder, "custom-old-contact"); + eab_contact_display_set_mode ( + EAB_CONTACT_DISPLAY (widget), + EAB_CONTACT_DISPLAY_RENDER_COMPACT); + eab_contact_display_set_contact ( + EAB_CONTACT_DISPLAY (widget), match); + + widget = e_builder_get_widget (builder, "custom-new-contact"); + eab_contact_display_set_mode ( + EAB_CONTACT_DISPLAY (widget), + EAB_CONTACT_DISPLAY_RENDER_COMPACT); + eab_contact_display_set_contact ( + EAB_CONTACT_DISPLAY (widget), contact); + + widget = e_builder_get_widget (builder, "dialog-duplicate-contact"); + + gtk_widget_ensure_style (widget); + + container = gtk_dialog_get_action_area (GTK_DIALOG (widget)); + gtk_container_set_border_width (GTK_CONTAINER (container), 12); + + container = gtk_dialog_get_content_area (GTK_DIALOG (widget)); + gtk_container_set_border_width (GTK_CONTAINER (container), 0); + + g_signal_connect ( + widget, "response", + G_CALLBACK (response), lookup); + + gtk_widget_show_all (widget); + + g_object_unref (builder); + } +} + +gboolean +eab_merging_book_add_contact (ESourceRegistry *registry, + EBookClient *book_client, + EContact *contact, + EABMergingIdAsyncCallback cb, + gpointer closure) +{ + EContactMergingLookup *lookup; + + g_return_val_if_fail (E_IS_SOURCE_REGISTRY (registry), FALSE); + + lookup = g_new (EContactMergingLookup, 1); + + lookup->op = E_CONTACT_MERGING_ADD; + lookup->registry = g_object_ref (registry); + lookup->book_client = g_object_ref (book_client); + lookup->contact = g_object_ref (contact); + lookup->id_cb = cb; + lookup->closure = closure; + lookup->avoid = NULL; + lookup->match = NULL; + + add_lookup (lookup); + + return TRUE; +} + +gboolean +eab_merging_book_modify_contact (ESourceRegistry *registry, + EBookClient *book_client, + EContact *contact, + EABMergingAsyncCallback cb, + gpointer closure) +{ + EContactMergingLookup *lookup; + + g_return_val_if_fail (E_IS_SOURCE_REGISTRY (registry), FALSE); + + lookup = g_new (EContactMergingLookup, 1); + + lookup->op = E_CONTACT_MERGING_COMMIT; + lookup->registry = g_object_ref (registry); + lookup->book_client = g_object_ref (book_client); + lookup->contact = g_object_ref (contact); + lookup->cb = cb; + lookup->closure = closure; + lookup->avoid = g_list_append (NULL, contact); + lookup->match = NULL; + + add_lookup (lookup); + + return TRUE; +} + +gboolean +eab_merging_book_find_contact (ESourceRegistry *registry, + EBookClient *book_client, + EContact *contact, + EABMergingContactAsyncCallback cb, + gpointer closure) +{ + EContactMergingLookup *lookup; + + lookup = g_new (EContactMergingLookup, 1); + + lookup->op = E_CONTACT_MERGING_FIND; + lookup->registry = g_object_ref (registry); + lookup->book_client = g_object_ref (book_client); + lookup->contact = g_object_ref (contact); + lookup->c_cb = cb; + lookup->closure = closure; + lookup->avoid = g_list_append (NULL, contact); + lookup->match = NULL; + + add_lookup (lookup); + + return TRUE; +} diff --git a/addressbook/gui/widgets/eab-contact-merging.h b/addressbook/gui/widgets/eab-contact-merging.h new file mode 100644 index 0000000000..fbdc9b0889 --- /dev/null +++ b/addressbook/gui/widgets/eab-contact-merging.h @@ -0,0 +1,66 @@ +/* + * The Evolution addressbook client object. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see + * + * + * Authors: + * Christopher James Lahey + * Chris Toshok + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifndef __E_CONTACT_MERGING_H__ +#define __E_CONTACT_MERGING_H__ + +#include + +G_BEGIN_DECLS + +typedef void (*EABMergingAsyncCallback) (EBookClient *book_client, + const GError *error, + gpointer closure); +typedef void (*EABMergingIdAsyncCallback) (EBookClient *book_client, + const GError *error, + const gchar *id, + gpointer closure); +typedef void (*EABMergingContactAsyncCallback) + (EBookClient *book_client, + const GError *error, + EContact *contact, + gpointer closure); + +gboolean eab_merging_book_add_contact (ESourceRegistry *registry, + EBookClient *book_client, + EContact *contact, + EABMergingIdAsyncCallback cb, + gpointer closure); + +gboolean eab_merging_book_modify_contact (ESourceRegistry *registry, + EBookClient *book_client, + EContact *contact, + EABMergingAsyncCallback cb, + gpointer closure); + +gboolean eab_merging_book_find_contact (ESourceRegistry *registry, + EBookClient *book_client, + EContact *contact, + EABMergingContactAsyncCallback cb, + gpointer closure); + +G_END_DECLS + +#endif /* __EAB_CONTACT_MERGING_H__ */ -- cgit v1.2.3