Add a basic contact merging feature to the address book, thanks to
[claws.git] / src / addrmerge.c
1 /* Claws Mail -- a GTK+ based, lightweight, and fast e-mail client
2  * Copyright (C) 2014 Charles Lehner and the Claws Mail team
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; either version 3 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program. If not, see <http://www.gnu.org/licenses/>. 
16  */
17
18
19 #ifdef HAVE_CONFIG_H
20 #  include "config.h"
21 #include "claws-features.h"
22 #endif
23
24 #include <gdk/gdk.h>
25 #include <gdk/gdkkeysyms.h>
26 #include <glib/gi18n.h>
27 #include <string.h>
28
29 #include "defs.h"
30
31 #ifdef USE_LDAP
32 #include "ldapserver.h"
33 #include "ldapupdate.h"
34 #endif
35 #include "addrbook.h"
36 #include "addressbook.h"
37 #include "addressitem.h"
38 #include "addrmerge.h"
39 #include "alertpanel.h"
40 #include "gtkutils.h"
41 #include "utils.h"
42 #include "prefs_common.h"
43
44 enum
45 {
46         COL_DISPLAYNAME,
47         COL_FIRSTNAME,
48         COL_LASTNAME,
49         COL_NICKNAME,
50         N_NAME_COLS
51 };
52
53 enum
54 {
55         SET_ICON,
56         SET_PERSON,
57         N_SET_COLUMNS
58 };
59
60 static void addrmerge_done(struct AddrMergePage *page)
61 {
62         g_list_free(page->emails);
63         g_list_free(page->persons);
64         gtk_widget_destroy(GTK_WIDGET(page->dialog));
65         g_free(page);
66 }
67
68 static void addrmerge_do_merge(struct AddrMergePage *page)
69 {
70         GList *node;
71         ItemEMail *email;
72         ItemPerson *person;
73         ItemPerson *target = page->target;
74         ItemPerson *nameTarget = page->nameTarget;
75
76         gtk_cmclist_freeze(GTK_CMCLIST(page->clist));
77
78         /* Update target name */
79         if (nameTarget && nameTarget != target) {
80                 target->status = UPDATE_ENTRY;
81                 addritem_person_set_first_name( target, nameTarget->firstName );
82                 addritem_person_set_last_name( target, nameTarget->lastName );
83                 addritem_person_set_nick_name( target, nameTarget->nickName );
84                 addritem_person_set_common_name( target, ADDRITEM_NAME(nameTarget ));
85         }
86
87         /* Merge emails into target */
88         for (node = page->emails; node; node = node->next) {
89                 email = node->data;
90                 person = ( ItemPerson * ) ADDRITEM_PARENT(email);
91                 /* Remove the email from the person */
92                 email = addrbook_person_remove_email( page->abf, person, email );
93                 if( email ) {
94                         addrcache_remove_email( page->abf->addressCache, email );
95                         /* Add the email to the target */
96                         addrcache_person_add_email( page->abf->addressCache, target, email );
97                 }
98                 person->status = UPDATE_ENTRY;
99                 addressbook_folder_refresh_one_person( page->clist, person );
100         }
101
102         /* Merge persons into target */
103         for (node = page->persons; node; node = node->next) {
104                 GList *nodeE, *nodeA;
105                 person = node->data;
106
107                 if (person == target) continue;
108                 person->status = DELETE_ENTRY;
109
110                 /* Move all emails to the target */
111                 for (nodeE = person->listEMail; nodeE; nodeE = nodeE->next) {
112                         email = nodeE->data;
113                         addritem_person_add_email( target, email );
114                 }
115                 g_list_free( person->listEMail );
116                 person->listEMail = NULL;
117
118                 /* Move all attributes to the target */
119                 for (nodeA = person->listAttrib; nodeA; nodeA = nodeA->next) {
120                         UserAttribute *attrib = nodeA->data;
121                         addritem_person_add_attribute( target, attrib );
122                 }
123                 g_list_free( person->listAttrib );
124                 person->listAttrib = NULL;
125
126                 /* Remove the person */
127                 addrselect_list_remove( page->addressSelect, (AddrItemObject *)person );
128                 addressbook_folder_remove_one_person( page->clist, person );
129                 if (page->pobj->type == ADDR_ITEM_FOLDER)
130                         addritem_folder_remove_person(ADAPTER_FOLDER(page->pobj)->itemFolder, person);
131                 person = addrbook_remove_person( page->abf, person );
132
133                 if( person ) {
134                         gchar *filename = addritem_person_get_picture(person);
135                         if ((strcmp2(person->picture, target->picture) &&
136                                         filename && is_file_exist(filename)))
137                                 claws_unlink(filename);
138                         if (filename)
139                                 g_free(filename);
140                         addritem_free_item_person( person );
141                 }
142         }
143
144         addressbook_folder_refresh_one_person( page->clist, target );
145
146         addrbook_set_dirty( page->abf, TRUE );
147         addressbook_export_to_file();
148
149 #ifdef USE_LDAP
150         if (page->ds && page->ds->type == ADDR_IF_LDAP) {
151                 LdapServer *server = page->ds->rawDataSource;
152                 ldapsvr_set_modified(server, TRUE);
153                 ldapsvr_update_book(server, NULL);
154         }
155 #endif
156         gtk_cmclist_thaw(GTK_CMCLIST(page->clist));
157
158         addrmerge_done(page);
159 }
160
161 static void addrmerge_dialog_cb(GtkWidget* widget, gint action, gpointer data) {
162         struct AddrMergePage* page = data;
163
164         if (action != GTK_RESPONSE_ACCEPT)
165                 return addrmerge_done(page);
166
167         addrmerge_do_merge(page);
168 }
169
170 static void addrmerge_update_dialog_sensitive( struct AddrMergePage *page )
171 {
172         gboolean canMerge = (page->target && page->nameTarget);
173         gtk_dialog_set_response_sensitive( GTK_DIALOG(page->dialog),
174                         GTK_RESPONSE_ACCEPT, canMerge );
175 }
176
177 static void addrmerge_name_selected( GtkCMCList *clist, gint row, gint column, GdkEvent *event, struct AddrMergePage *page )
178 {
179         ItemPerson *person = gtk_cmclist_get_row_data( clist, row );
180         page->nameTarget = person;
181         addrmerge_update_dialog_sensitive(page);
182 }
183
184 static void addrmerge_picture_selected(GtkTreeView *treeview,
185                 struct AddrMergePage *page)
186 {
187         GtkTreeModel *model;
188         GtkTreeIter iter;
189         GList *list;
190         ItemPerson *pictureTarget;
191
192         /* Get selected picture target */ 
193         model = gtk_icon_view_get_model(GTK_ICON_VIEW(page->iconView));
194         list = gtk_icon_view_get_selected_items(GTK_ICON_VIEW(page->iconView));
195         page->target = NULL;
196         if (list != NULL) {
197                 if (gtk_tree_model_get_iter(model, &iter, (GtkTreePath *)list->data)) {
198                         gtk_tree_model_get(model, &iter,
199                                         SET_PERSON, &pictureTarget,
200                                         -1);
201                         page->target = pictureTarget;
202                 }
203
204                 gtk_tree_path_free(list->data);
205                 g_list_free(list);
206         }
207         addrmerge_update_dialog_sensitive(page);
208 }
209
210 static void addrmerge_prompt( struct AddrMergePage *page )
211 {
212         GtkWidget *dialog;
213         GtkWidget *frame;
214         GtkWidget *mvbox, *vbox, *hbox;
215         GtkWidget *label;
216         GtkWidget *iconView = NULL;
217         GtkWidget *namesList = NULL;
218         MainWindow *mainwin = mainwindow_get_mainwindow();
219         GtkListStore *store = NULL;
220         GtkTreeIter iter;
221         GList *node;
222         ItemPerson *person;
223         GError *error = NULL;
224         gchar *msg, *label_msg;
225
226         dialog = page->dialog = gtk_dialog_new_with_buttons (
227                         _("Merge addresses"),
228                         GTK_WINDOW(mainwin->window),
229                         GTK_DIALOG_DESTROY_WITH_PARENT,
230                         GTK_STOCK_CANCEL,
231                         GTK_RESPONSE_CANCEL,
232                         "_Merge",
233                         GTK_RESPONSE_ACCEPT,
234                         NULL);
235
236         g_signal_connect ( dialog, "response",
237                         G_CALLBACK(addrmerge_dialog_cb), page);
238
239         mvbox = gtk_vbox_new(FALSE, 4);
240         gtk_container_add(GTK_CONTAINER(
241                         gtk_dialog_get_content_area(GTK_DIALOG(dialog))), mvbox);
242         gtk_container_set_border_width(GTK_CONTAINER(mvbox), 8);
243         hbox = gtk_hbox_new(FALSE, 4);
244         gtk_container_set_border_width(GTK_CONTAINER(hbox), 8);
245         gtk_box_pack_start(GTK_BOX(mvbox),
246                         hbox, FALSE, FALSE, 0);
247
248         msg = page->pickPicture || page->pickName ?
249                 _("Merging %u contacts." ) :
250                 _("Really merge these %u contacts?" );
251         label_msg = g_strdup_printf(msg,
252                         g_list_length(page->addressSelect->listSelect));
253         label = gtk_label_new( label_msg );
254         gtk_label_set_justify(GTK_LABEL(label), GTK_JUSTIFY_LEFT);
255         gtk_misc_set_alignment(GTK_MISC(label), 0, 0.5);
256         gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 0);
257         g_free(label_msg);
258
259         if (page->pickPicture) {
260                 GtkWidget *scrollwinPictures;
261
262                 store = gtk_list_store_new(N_SET_COLUMNS,
263                                         GDK_TYPE_PIXBUF,
264                                         G_TYPE_POINTER,
265                                         -1);
266                 gtk_list_store_clear(store);
267
268                 vbox = gtkut_get_options_frame(mvbox, &frame,
269                                 _("Keep which picture?"));
270                 gtk_container_set_border_width(GTK_CONTAINER(frame), 4);
271
272                 scrollwinPictures = gtk_scrolled_window_new(NULL, NULL);
273                 gtk_container_set_border_width(GTK_CONTAINER(scrollwinPictures), 1);
274                 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrollwinPictures),
275                                 GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
276                 gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrollwinPictures),
277                                 GTK_SHADOW_IN);
278                 gtk_box_pack_start (GTK_BOX (vbox), scrollwinPictures, FALSE, FALSE, 0);
279                 gtk_widget_set_size_request(scrollwinPictures, 464, 192);
280
281                 iconView = gtk_icon_view_new_with_model(GTK_TREE_MODEL(store));
282                 gtk_icon_view_set_selection_mode(GTK_ICON_VIEW(iconView), GTK_SELECTION_SINGLE);
283                 gtk_icon_view_set_pixbuf_column(GTK_ICON_VIEW(iconView), SET_ICON);
284                 gtk_container_add(GTK_CONTAINER(scrollwinPictures), GTK_WIDGET(iconView));
285                 g_signal_connect(G_OBJECT(iconView), "selection-changed",
286                                 G_CALLBACK(addrmerge_picture_selected), page);
287
288                 /* Add pictures from persons */
289                 for (node = page->persons; node; node = node->next) {
290                         gchar *filename;
291                         person = node->data;
292                         filename = addritem_person_get_picture(person);
293                         if (filename && is_file_exist(filename)) {
294                                 GdkPixbuf *pixbuf;
295                                 GtkWidget *image;
296
297                                 pixbuf = gdk_pixbuf_new_from_file(filename, &error);
298                                 if (error) {
299                                         debug_print("Failed to read image: \n%s",
300                                                         error->message);
301                                         g_error_free(error);
302                                         continue;
303                                 }
304
305                                 image = gtk_image_new();
306                                 gtk_image_set_from_pixbuf(GTK_IMAGE(image), pixbuf);
307
308                                 gtk_list_store_append(store, &iter);
309                                 gtk_list_store_set(store, &iter,
310                                                 SET_ICON, pixbuf,
311                                                 SET_PERSON, person,
312                                                 -1);
313                         }
314                         if (filename)
315                                 g_free(filename);
316                 }
317         }
318
319         if (page->pickName) {
320                 GtkWidget *scrollwinNames;
321                 gchar *name_titles[N_NAME_COLS];
322
323                 name_titles[COL_DISPLAYNAME] = _("Display Name");
324                 name_titles[COL_FIRSTNAME] = _("First Name");
325                 name_titles[COL_LASTNAME] = _("Last Name");
326                 name_titles[COL_NICKNAME] = _("Nickname");
327
328                 store = gtk_list_store_new(N_SET_COLUMNS,
329                                         GDK_TYPE_PIXBUF,
330                                         G_TYPE_POINTER,
331                                         -1);
332                 gtk_list_store_clear(store);
333
334                 vbox = gtkut_get_options_frame(mvbox, &frame,
335                                 _("Keep which name?"));
336                 gtk_container_set_border_width(GTK_CONTAINER(frame), 4);
337
338                 scrollwinNames = gtk_scrolled_window_new(NULL, NULL);
339                 gtk_container_set_border_width(GTK_CONTAINER(scrollwinNames), 1);
340                 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrollwinNames),
341                                 GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
342                 gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrollwinNames),
343                                 GTK_SHADOW_IN);
344                 gtk_box_pack_start(GTK_BOX(vbox), GTK_WIDGET(scrollwinNames), FALSE, FALSE, 0);
345
346                 namesList = gtk_cmclist_new_with_titles(N_NAME_COLS, name_titles);
347                 gtk_widget_set_can_focus(GTK_CMCLIST(namesList)->column[0].button, FALSE);
348                 gtk_cmclist_set_selection_mode(GTK_CMCLIST(namesList), GTK_SELECTION_BROWSE);
349                 gtk_cmclist_set_column_width(GTK_CMCLIST(namesList), COL_DISPLAYNAME, 164);
350
351                 gtk_container_add(GTK_CONTAINER(scrollwinNames), namesList);
352
353                 /* Add names from persons */
354                 for (node = page->persons; node; node = node->next) {
355                         int row;
356                         person = node->data;
357                         gchar *text[N_NAME_COLS];
358                         text[COL_DISPLAYNAME] = ADDRITEM_NAME(person);
359                         text[COL_FIRSTNAME] = person->firstName;
360                         text[COL_LASTNAME] = person->lastName;
361                         text[COL_NICKNAME] = person->nickName;
362                         row = gtk_cmclist_insert( GTK_CMCLIST(namesList), -1, text );
363                         gtk_cmclist_set_row_data( GTK_CMCLIST(namesList), row, person );
364                 }
365
366                 g_signal_connect(G_OBJECT(namesList), "select_row",
367                                 G_CALLBACK(addrmerge_name_selected), page);
368         }
369
370         page->iconView = iconView;
371         page->namesList = namesList;
372
373         addrmerge_update_dialog_sensitive(page);
374         gtk_widget_show_all(dialog);
375 }
376
377 void addrmerge_merge(
378                 GtkCMCTree *clist,
379                 AddressObject *pobj,
380                 AddressDataSource *ds,
381                 AddrSelectList *list)
382 {
383         struct AddrMergePage* page;
384         AdapterDSource *ads = NULL;
385         AddressBookFile *abf;
386         gboolean procFlag;
387         GList *node;
388         AddrSelectItem *item;
389         AddrItemObject *aio;
390         ItemPerson *person, *target = NULL, *nameTarget = NULL;
391         GList *persons = NULL, *emails = NULL;
392         gboolean pickPicture = FALSE, pickName = FALSE;
393
394         /* Test for read only */
395         if( ds->interface->readOnly ) {
396                 alertpanel( _("Merge addresses"),
397                         _("This address data is readonly and cannot be deleted."),
398                         GTK_STOCK_CLOSE, NULL, NULL, ALERTFOCUS_FIRST );
399                 return;
400         }
401
402         /* Test whether Ok to proceed */
403         procFlag = FALSE;
404         if( pobj->type == ADDR_DATASOURCE ) {
405                 ads = ADAPTER_DSOURCE(pobj);
406                 if( ads->subType == ADDR_BOOK ) procFlag = TRUE;
407         }
408         else if( pobj->type == ADDR_ITEM_FOLDER ) {
409                 procFlag = TRUE;
410         }
411         else if( pobj->type == ADDR_ITEM_GROUP ) {
412                 procFlag = TRUE;
413         }
414         if( ! procFlag ) return;
415         abf = ds->rawDataSource;
416         if( abf == NULL ) return;
417
418         /* Gather selected persons and emails */
419         for (node = list->listSelect; node; node = node->next) {
420                 item = node->data;
421                 aio = ( AddrItemObject * ) item->addressItem;
422                 if( aio->type == ITEMTYPE_EMAIL ) {
423                         emails = g_list_prepend(emails, aio);
424                 } else if( aio->type == ITEMTYPE_PERSON ) {
425                         persons = g_list_prepend(persons, aio);
426                 }
427         }
428
429         /* Check if more than one person has a picture */
430         for (node = persons; node; node = node->next) {
431                 gchar *filename;
432                 person = node->data;
433                 filename = addritem_person_get_picture(person);
434                 if (filename && is_file_exist(filename)) {
435                         if (target == NULL) {
436                                 target = person;
437                         } else {
438                                 pickPicture = TRUE;
439                                 target = NULL;
440                                 break;
441                         }
442                 }
443                 if (filename)
444                         g_free(filename);
445         }
446         if (pickPicture || target) {
447                 /* At least one person had a picture */
448         } else if (persons && persons->data) {
449                 /* No person had a picture. Use the first person as target */
450                 target = persons->data;
451         } else {
452                 /* No persons in list. Abort */
453                 goto abort;
454         }
455
456         /* Pick which name to keep */
457         for (node = persons; node; node = node->next) {
458                 person = node->data;
459                 if (nameTarget == NULL) {
460                         nameTarget = person;
461                 } else if (nameTarget == person) {
462                         continue;
463                 } else if (strcmp2(person->firstName, nameTarget->firstName) ||
464                                 strcmp2(person->lastName, nameTarget->lastName) ||
465                                 strcmp2(person->nickName, nameTarget->nickName) ||
466                                 strcmp2(ADDRITEM_NAME(person), ADDRITEM_NAME(nameTarget))) {
467                         pickName = TRUE;
468                         break;
469                 }
470         }
471         if (!nameTarget) {
472                 /* No persons in list */
473                 goto abort;
474         }
475
476         /* Create object */
477         page = g_new0(struct AddrMergePage, 1);
478         page->pickPicture = pickPicture;
479         page->pickName = pickName;
480         page->target = target;
481         page->nameTarget = nameTarget;
482         page->addressSelect = list;
483         page->persons = persons;
484         page->emails = emails;
485         page->clist = clist;
486         page->pobj = pobj;
487         page->abf = abf;
488         page->ds = ds;
489
490         addrmerge_prompt(page);
491         return;
492
493 abort:
494         g_list_free( emails );
495         g_list_free( persons );
496 }