edca01f72b8d491592b88ab3c21dadfe7bc82c1b
[geeqie.git] / src / dupe.c
1 /*
2  * Copyright (C) 2005 John Ellis
3  * Copyright (C) 2008 - 2016 The Geeqie Team
4  *
5  * Author: John Ellis
6  *
7  * This program is free software; you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License as published by
9  * the Free Software Foundation; either version 2 of the License, or
10  * (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License along
18  * with this program; if not, write to the Free Software Foundation, Inc.,
19  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20  */
21
22 #include <inttypes.h>
23
24 #include "main.h"
25 #include "dupe.h"
26
27 #include "cache.h"
28 #include "collect.h"
29 #include "collect-table.h"
30 #include "dnd.h"
31 #include "editors.h"
32 #include "filedata.h"
33 #include "history_list.h"
34 #include "image-load.h"
35 #include "img-view.h"
36 #include "layout.h"
37 #include "layout_image.h"
38 #include "layout_util.h"
39 #include "md5-util.h"
40 #include "menu.h"
41 #include "misc.h"
42 #include "pixbuf_util.h"
43 #include "print.h"
44 #include "thumb.h"
45 #include "ui_fileops.h"
46 #include "ui_menu.h"
47 #include "ui_misc.h"
48 #include "ui_tree_edit.h"
49 #include "uri_utils.h"
50 #include "utilops.h"
51 #include "window.h"
52
53 #include <gdk/gdkkeysyms.h> /* for keyboard values */
54
55
56 #include <math.h>
57
58
59 #define DUPE_DEF_WIDTH 800
60 #define DUPE_DEF_HEIGHT 400
61
62 /* column assignment order (simply change them here) */
63 enum {
64         DUPE_COLUMN_POINTER = 0,
65         DUPE_COLUMN_RANK,
66         DUPE_COLUMN_THUMB,
67         DUPE_COLUMN_NAME,
68         DUPE_COLUMN_SIZE,
69         DUPE_COLUMN_DATE,
70         DUPE_COLUMN_DIMENSIONS,
71         DUPE_COLUMN_PATH,
72         DUPE_COLUMN_COLOR,
73         DUPE_COLUMN_COUNT       /* total columns */
74 };
75
76
77 static GList *dupe_window_list = NULL;  /* list of open DupeWindow *s */
78
79 /*
80  * Well, after adding the 'compare two sets' option things got a little sloppy in here
81  * because we have to account for two 'modes' everywhere. (be careful).
82  */
83
84 static void dupe_match_unlink(DupeItem *a, DupeItem *b);
85 static DupeItem *dupe_match_find_parent(DupeWindow *dw, DupeItem *child);
86
87 static gint dupe_match(DupeItem *a, DupeItem *b, DupeMatchType mask, gdouble *rank, gint fast);
88
89 static void dupe_thumb_step(DupeWindow *dw);
90 static gint dupe_check_cb(gpointer data);
91
92 static void dupe_second_add(DupeWindow *dw, DupeItem *di);
93 static void dupe_second_remove(DupeWindow *dw, DupeItem *di);
94 static GtkWidget *dupe_menu_popup_second(DupeWindow *dw, DupeItem *di);
95
96 static void dupe_dnd_init(DupeWindow *dw);
97
98 static void dupe_notify_cb(FileData *fd, NotifyType type, gpointer data);
99
100 static GtkWidget *submenu_add_export(GtkWidget *menu, GtkWidget **menu_item, GCallback func, gpointer data);
101 static void dupe_pop_menu_export_cb(GtkWidget *widget, gpointer data);
102 /*
103  * ------------------------------------------------------------------
104  * Window updates
105  * ------------------------------------------------------------------
106  */
107
108
109 static void dupe_window_update_count(DupeWindow *dw, gboolean count_only)
110 {
111         gchar *text;
112
113         if (!dw->list)
114                 {
115                 text = g_strdup(_("Drop files to compare them."));
116                 }
117         else if (count_only)
118                 {
119                 text = g_strdup_printf(_("%d files"), g_list_length(dw->list));
120                 }
121         else
122                 {
123                 text = g_strdup_printf(_("%d matches found in %d files"), g_list_length(dw->dupes), g_list_length(dw->list));
124                 }
125
126         if (dw->second_set)
127                 {
128                 gchar *buf = g_strconcat(text, " ", _("[set 1]"), NULL);
129                 g_free(text);
130                 text = buf;
131                 }
132         gtk_label_set_text(GTK_LABEL(dw->status_label), text);
133
134         g_free(text);
135 }
136
137 static guint64 msec_time(void)
138 {
139         struct timeval tv;
140
141         if (gettimeofday(&tv, NULL) == -1) return 0;
142
143         return (guint64)tv.tv_sec * 1000000 + (guint64)tv.tv_usec;
144 }
145
146 static gint dupe_iterations(gint n)
147 {
148         return (n * ((n + 1) / 2));
149 }
150
151 static void dupe_window_update_progress(DupeWindow *dw, const gchar *status, gdouble value, gboolean force)
152 {
153         const gchar *status_text;
154
155         if (status)
156                 {
157                 guint64 new_time = 0;
158
159                 if (dw->setup_n % 10 == 0)
160                         {
161                         new_time = msec_time() - dw->setup_time;
162                         }
163
164                 if (!force &&
165                     value != 0.0 &&
166                     dw->setup_count > 0 &&
167                     new_time > 2000000)
168                         {
169                         gchar *buf;
170                         gint t;
171                         gint d;
172                         guint32 rem;
173
174                         if (new_time - dw->setup_time_count < 250000) return;
175                         dw->setup_time_count = new_time;
176
177                         if (dw->setup_done)
178                                 {
179                                 if (dw->second_set)
180                                         {
181                                         t = dw->setup_count;
182                                         d = dw->setup_count - dw->setup_n;
183                                         }
184                                 else
185                                         {
186                                         t = dupe_iterations(dw->setup_count);
187                                         d = dupe_iterations(dw->setup_count - dw->setup_n);
188                                         }
189                                 }
190                         else
191                                 {
192                                 t = dw->setup_count;
193                                 d = dw->setup_count - dw->setup_n;
194                                 }
195
196                         rem = (t - d) ? ((gdouble)(dw->setup_time_count / 1000000) / (t - d)) * d : 0;
197
198                         gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(dw->extra_label), value);
199
200                         buf = g_strdup_printf("%s %d:%02d ", status, rem / 60, rem % 60);
201                         gtk_progress_bar_set_text(GTK_PROGRESS_BAR(dw->extra_label), buf);
202                         g_free(buf);
203
204                         return;
205                         }
206                 else if (force ||
207                          value == 0.0 ||
208                          dw->setup_count == 0 ||
209                          dw->setup_time_count == 0 ||
210                          (new_time > 0 && new_time - dw->setup_time_count >= 250000))
211                         {
212                         if (dw->setup_time_count == 0) dw->setup_time_count = 1;
213                         if (new_time > 0) dw->setup_time_count = new_time;
214                         gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(dw->extra_label), value);
215                         status_text = status;
216                         }
217                 else
218                         {
219                         status_text = NULL;
220                         }
221                 }
222         else
223                 {
224                 gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(dw->extra_label), 0.0);
225                 status_text = " ";
226                 }
227
228         if (status_text) gtk_progress_bar_set_text(GTK_PROGRESS_BAR(dw->extra_label), status_text);
229 }
230
231 static void widget_set_cursor(GtkWidget *widget, gint icon)
232 {
233         GdkCursor *cursor;
234
235         if (!gtk_widget_get_window(widget)) return;
236
237         if (icon == -1)
238                 {
239                 cursor = NULL;
240                 }
241         else
242                 {
243                 cursor = gdk_cursor_new(icon);
244                 }
245
246         gdk_window_set_cursor(gtk_widget_get_window(widget), cursor);
247
248         if (cursor) gdk_cursor_unref(cursor);
249 }
250
251 /*
252  * ------------------------------------------------------------------
253  * row color utils
254  * ------------------------------------------------------------------
255  */
256
257 static void dupe_listview_realign_colors(DupeWindow *dw)
258 {
259         GtkTreeModel *store;
260         GtkTreeIter iter;
261         gboolean color_set = TRUE;
262         DupeItem *parent = NULL;
263         gboolean valid;
264
265         store = gtk_tree_view_get_model(GTK_TREE_VIEW(dw->listview));
266         valid = gtk_tree_model_get_iter_first(store, &iter);
267         while (valid)
268                 {
269                 DupeItem *child;
270                 DupeItem *child_parent;
271
272                 gtk_tree_model_get(store, &iter, DUPE_COLUMN_POINTER, &child, -1);
273                 child_parent = dupe_match_find_parent(dw, child);
274                 if (!parent || parent != child_parent)
275                         {
276                         if (!parent)
277                                 {
278                                 /* keep the first row as it is */
279                                 gtk_tree_model_get(store, &iter, DUPE_COLUMN_COLOR, &color_set, -1);
280                                 }
281                         else
282                                 {
283                                 color_set = !color_set;
284                                 }
285                         parent = dupe_match_find_parent(dw, child);
286                         }
287                 gtk_list_store_set(GTK_LIST_STORE(store), &iter, DUPE_COLUMN_COLOR, color_set, -1);
288
289                 valid = gtk_tree_model_iter_next(GTK_TREE_MODEL(store), &iter);
290                 }
291 }
292
293 /*
294  * ------------------------------------------------------------------
295  * Dupe item utils
296  * ------------------------------------------------------------------
297  */
298
299 static DupeItem *dupe_item_new(FileData *fd)
300 {
301         DupeItem *di;
302
303         di = g_new0(DupeItem, 1);
304
305         di->fd = file_data_ref(fd);
306         di->group_rank = 0.0;
307
308         return di;
309 }
310
311 static void dupe_item_free(DupeItem *di)
312 {
313         file_data_unref(di->fd);
314         image_sim_free(di->simd);
315         g_free(di->md5sum);
316         if (di->pixbuf) g_object_unref(di->pixbuf);
317
318         g_free(di);
319 }
320
321 static void dupe_list_free(GList *list)
322 {
323         GList *work = list;
324         while (work)
325                 {
326                 DupeItem *di = work->data;
327                 work = work->next;
328                 dupe_item_free(di);
329                 }
330         g_list_free(list);
331 }
332
333 /*
334 static DupeItem *dupe_item_find_fd_by_list(FileData *fd, GList *work)
335 {
336         while (work)
337                 {
338                 DupeItem *di = work->data;
339
340                 if (di->fd == fd) return di;
341
342                 work = work->next;
343                 }
344
345         return NULL;
346 }
347 */
348
349 /*
350 static DupeItem *dupe_item_find_fd(DupeWindow *dw, FileData *fd)
351 {
352         DupeItem *di;
353
354         di = dupe_item_find_fd_by_list(fd, dw->list);
355         if (!di && dw->second_set) di = dupe_item_find_fd_by_list(fd, dw->second_list);
356
357         return di;
358 }
359 */
360
361 static DupeItem *dupe_item_find_path_by_list(const gchar *path, GList *work)
362 {
363         while (work)
364                 {
365                 DupeItem *di = work->data;
366
367                 if (strcmp(di->fd->path, path) == 0) return di;
368
369                 work = work->next;
370                 }
371
372         return NULL;
373 }
374
375 static DupeItem *dupe_item_find_path(DupeWindow *dw, const gchar *path)
376 {
377         DupeItem *di;
378
379         di = dupe_item_find_path_by_list(path, dw->list);
380         if (!di && dw->second_set) di = dupe_item_find_path_by_list(path, dw->second_list);
381
382         return di;
383 }
384
385 /*
386  * ------------------------------------------------------------------
387  * Image property cache
388  * ------------------------------------------------------------------
389  */
390
391 static void dupe_item_read_cache(DupeItem *di)
392 {
393         gchar *path;
394         CacheData *cd;
395
396         if (!di) return;
397
398         path = cache_find_location(CACHE_TYPE_SIM, di->fd->path);
399         if (!path) return;
400
401         if (filetime(di->fd->path) != filetime(path))
402                 {
403                 g_free(path);
404                 return;
405                 }
406
407         cd = cache_sim_data_load(path);
408         g_free(path);
409
410         if (cd)
411                 {
412                 if (!di->simd && cd->sim)
413                         {
414                         di->simd = cd->sim;
415                         cd->sim = NULL;
416                         }
417                 if (di->width == 0 && di->height == 0 && cd->dimensions)
418                         {
419                         di->width = cd->width;
420                         di->height = cd->height;
421                         }
422                 if (!di->md5sum && cd->have_md5sum)
423                         {
424                         di->md5sum = md5_digest_to_text(cd->md5sum);
425                         }
426                 cache_sim_data_free(cd);
427                 }
428 }
429
430 static void dupe_item_write_cache(DupeItem *di)
431 {
432         gchar *base;
433         mode_t mode = 0755;
434
435         if (!di) return;
436
437         base = cache_get_location(CACHE_TYPE_SIM, di->fd->path, FALSE, &mode);
438         if (recursive_mkdir_if_not_exists(base, mode))
439                 {
440                 CacheData *cd;
441
442                 cd = cache_sim_data_new();
443                 cd->path = cache_get_location(CACHE_TYPE_SIM, di->fd->path, TRUE, NULL);
444
445                 if (di->width != 0) cache_sim_data_set_dimensions(cd, di->width, di->height);
446                 if (di->md5sum)
447                         {
448                         guchar digest[16];
449                         if (md5_digest_from_text(di->md5sum, digest)) cache_sim_data_set_md5sum(cd, digest);
450                         }
451                 if (di->simd) cache_sim_data_set_similarity(cd, di->simd);
452
453                 if (cache_sim_data_save(cd))
454                         {
455                         filetime_set(cd->path, filetime(di->fd->path));
456                         }
457                 cache_sim_data_free(cd);
458                 }
459         g_free(base);
460 }
461
462 /*
463  * ------------------------------------------------------------------
464  * Window list utils
465  * ------------------------------------------------------------------
466  */
467
468 static gint dupe_listview_find_item(GtkListStore *store, DupeItem *item, GtkTreeIter *iter)
469 {
470         gboolean valid;
471         gint row = 0;
472
473         valid = gtk_tree_model_get_iter_first(GTK_TREE_MODEL(store), iter);
474         while (valid)
475                 {
476                 DupeItem *item_n;
477                 gtk_tree_model_get(GTK_TREE_MODEL(store), iter, DUPE_COLUMN_POINTER, &item_n, -1);
478                 if (item_n == item) return row;
479
480                 valid = gtk_tree_model_iter_next(GTK_TREE_MODEL(store), iter);
481                 row++;
482                 }
483
484         return -1;
485 }
486
487 static void dupe_listview_add(DupeWindow *dw, DupeItem *parent, DupeItem *child)
488 {
489         DupeItem *di;
490         gint row;
491         gchar *text[DUPE_COLUMN_COUNT];
492         GtkListStore *store;
493         GtkTreeIter iter;
494         gboolean color_set = FALSE;
495         gint rank;
496
497         if (!parent) return;
498
499         store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->listview)));
500
501         if (child)
502                 {
503                 DupeMatch *dm;
504
505                 row = dupe_listview_find_item(store, parent, &iter);
506                 gtk_tree_model_get(GTK_TREE_MODEL(store), &iter, DUPE_COLUMN_COLOR, &color_set, -1);
507
508                 row++;
509
510                 if (child->group)
511                         {
512                         dm = child->group->data;
513                         rank = (gint)floor(dm->rank);
514                         }
515                 else
516                         {
517                         rank = 1;
518                         log_printf("NULL group in item!\n");
519                         }
520                 }
521         else
522                 {
523                 if (gtk_tree_model_get_iter_first(GTK_TREE_MODEL(store), &iter))
524                         {
525                         gtk_tree_model_get(GTK_TREE_MODEL(store), &iter, DUPE_COLUMN_COLOR, &color_set, -1);
526                         color_set = !color_set;
527                         }
528                 else
529                         {
530                         color_set = FALSE;
531                         }
532                 row = 0;
533                 rank = 0;
534                 }
535
536         di = (child) ? child : parent;
537
538         if (!child && dw->second_set)
539                 {
540                 text[DUPE_COLUMN_RANK] = g_strdup("[1]");
541                 }
542         else if (rank == 0)
543                 {
544                 text[DUPE_COLUMN_RANK] = g_strdup((di->second) ? "(2)" : "");
545                 }
546         else
547                 {
548                 text[DUPE_COLUMN_RANK] = g_strdup_printf("%d%s", rank, (di->second) ? " (2)" : "");
549                 }
550
551         text[DUPE_COLUMN_THUMB] = "";
552         text[DUPE_COLUMN_NAME] = (gchar *)di->fd->name;
553         text[DUPE_COLUMN_SIZE] = text_from_size(di->fd->size);
554         text[DUPE_COLUMN_DATE] = (gchar *)text_from_time(di->fd->date);
555         if (di->width > 0 && di->height > 0)
556                 {
557                 text[DUPE_COLUMN_DIMENSIONS] = g_strdup_printf("%d x %d", di->width, di->height);
558                 }
559         else
560                 {
561                 text[DUPE_COLUMN_DIMENSIONS] = g_strdup("");
562                 }
563         text[DUPE_COLUMN_PATH] = di->fd->path;
564         text[DUPE_COLUMN_COLOR] = NULL;
565
566         gtk_list_store_insert(store, &iter, row);
567         gtk_list_store_set(store, &iter,
568                                 DUPE_COLUMN_POINTER, di,
569                                 DUPE_COLUMN_RANK, text[DUPE_COLUMN_RANK],
570                                 DUPE_COLUMN_THUMB, NULL,
571                                 DUPE_COLUMN_NAME, text[DUPE_COLUMN_NAME],
572                                 DUPE_COLUMN_SIZE, text[DUPE_COLUMN_SIZE],
573                                 DUPE_COLUMN_DATE, text[DUPE_COLUMN_DATE],
574                                 DUPE_COLUMN_DIMENSIONS, text[DUPE_COLUMN_DIMENSIONS],
575                                 DUPE_COLUMN_PATH, text[DUPE_COLUMN_PATH],
576                                 DUPE_COLUMN_COLOR, color_set,
577                                 -1);
578
579         g_free(text[DUPE_COLUMN_RANK]);
580         g_free(text[DUPE_COLUMN_SIZE]);
581         g_free(text[DUPE_COLUMN_DIMENSIONS]);
582 }
583
584 static void dupe_listview_select_dupes(DupeWindow *dw, DupeSelectType parents);
585
586 static void dupe_listview_populate(DupeWindow *dw)
587 {
588         GtkListStore *store;
589         GList *work;
590
591         store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->listview)));
592         gtk_list_store_clear(store);
593
594         work = g_list_last(dw->dupes);
595         while (work)
596                 {
597                 DupeItem *parent = work->data;
598                 GList *temp;
599
600                 dupe_listview_add(dw, parent, NULL);
601
602                 temp = g_list_last(parent->group);
603                 while (temp)
604                         {
605                         DupeMatch *dm = temp->data;
606                         DupeItem *child;
607
608                         child = dm->di;
609
610                         dupe_listview_add(dw, parent, child);
611
612                         temp = temp->prev;
613                         }
614
615                 work = work->prev;
616                 }
617
618         gtk_tree_view_columns_autosize(GTK_TREE_VIEW(dw->listview));
619
620         if (options->duplicates_select_type == DUPE_SELECT_GROUP1)
621                 {
622                 dupe_listview_select_dupes(dw, DUPE_SELECT_GROUP1);
623                 }
624         else if (options->duplicates_select_type == DUPE_SELECT_GROUP2)
625                 {
626                 dupe_listview_select_dupes(dw, DUPE_SELECT_GROUP2);
627                 }
628
629 }
630
631 static void dupe_listview_remove(DupeWindow *dw, DupeItem *di)
632 {
633         GtkListStore *store;
634         GtkTreeIter iter;
635         gint row;
636
637         if (!di) return;
638
639         store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->listview)));
640         row = dupe_listview_find_item(store, di, &iter);
641         if (row < 0) return;
642
643         tree_view_move_cursor_away(GTK_TREE_VIEW(dw->listview), &iter, TRUE);
644         gtk_list_store_remove(store, &iter);
645
646         if (g_list_find(dw->dupes, di) != NULL)
647                 {
648                 if (!dw->color_frozen) dupe_listview_realign_colors(dw);
649                 }
650 }
651
652
653 static GList *dupe_listview_get_filelist(DupeWindow *dw, GtkWidget *listview)
654 {
655         GtkTreeModel *store;
656         GtkTreeIter iter;
657         gboolean valid;
658         GList *list = NULL;
659
660         store = gtk_tree_view_get_model(GTK_TREE_VIEW(listview));
661         valid = gtk_tree_model_get_iter_first(store, &iter);
662         while (valid)
663                 {
664                 DupeItem *di;
665                 gtk_tree_model_get(store, &iter, DUPE_COLUMN_POINTER, &di, -1);
666                 list = g_list_prepend(list, file_data_ref(di->fd));
667
668                 valid = gtk_tree_model_iter_next(store, &iter);
669                 }
670
671         return g_list_reverse(list);
672 }
673
674
675 static GList *dupe_listview_get_selection(DupeWindow *dw, GtkWidget *listview)
676 {
677         GtkTreeModel *store;
678         GtkTreeSelection *selection;
679         GList *slist;
680         GList *list = NULL;
681         GList *work;
682
683         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(listview));
684         slist = gtk_tree_selection_get_selected_rows(selection, &store);
685         work = slist;
686         while (work)
687                 {
688                 GtkTreePath *tpath = work->data;
689                 DupeItem *di = NULL;
690                 GtkTreeIter iter;
691
692                 gtk_tree_model_get_iter(store, &iter, tpath);
693                 gtk_tree_model_get(store, &iter, DUPE_COLUMN_POINTER, &di, -1);
694                 if (di)
695                         {
696                         list = g_list_prepend(list, file_data_ref(di->fd));
697                         }
698                 work = work->next;
699                 }
700         g_list_foreach(slist, (GFunc)gtk_tree_path_free, NULL);
701         g_list_free(slist);
702
703         return g_list_reverse(list);
704 }
705
706 static gboolean dupe_listview_item_is_selected(DupeWindow *dw, DupeItem *di, GtkWidget *listview)
707 {
708         GtkTreeModel *store;
709         GtkTreeSelection *selection;
710         GList *slist;
711         GList *work;
712         gboolean found = FALSE;
713
714         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(listview));
715         slist = gtk_tree_selection_get_selected_rows(selection, &store);
716         work = slist;
717         while (!found && work)
718                 {
719                 GtkTreePath *tpath = work->data;
720                 DupeItem *di_n;
721                 GtkTreeIter iter;
722
723                 gtk_tree_model_get_iter(store, &iter, tpath);
724                 gtk_tree_model_get(store, &iter, DUPE_COLUMN_POINTER, &di_n, -1);
725                 if (di_n == di) found = TRUE;
726                 work = work->next;
727                 }
728         g_list_foreach(slist, (GFunc)gtk_tree_path_free, NULL);
729         g_list_free(slist);
730
731         return found;
732 }
733
734 static void dupe_listview_select_dupes(DupeWindow *dw, DupeSelectType parents)
735 {
736         GtkTreeModel *store;
737         GtkTreeSelection *selection;
738         GtkTreeIter iter;
739         gboolean valid;
740
741         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(dw->listview));
742         gtk_tree_selection_unselect_all(selection);
743
744         store = gtk_tree_view_get_model(GTK_TREE_VIEW(dw->listview));
745         valid = gtk_tree_model_get_iter_first(store, &iter);
746         while (valid)
747                 {
748                 DupeItem *di;
749
750                 gtk_tree_model_get(store, &iter, DUPE_COLUMN_POINTER, &di, -1);
751                 if ((dupe_match_find_parent(dw, di) == di) == (parents == DUPE_SELECT_GROUP1))
752                         {
753                         gtk_tree_selection_select_iter(selection, &iter);
754                         }
755                 valid = gtk_tree_model_iter_next(store, &iter);
756                 }
757 }
758
759 /*
760  * ------------------------------------------------------------------
761  * Match group manipulation
762  * ------------------------------------------------------------------
763  */
764
765 static DupeMatch *dupe_match_find_match(DupeItem *child, DupeItem *parent)
766 {
767         GList *work;
768
769         work = parent->group;
770         while (work)
771                 {
772                 DupeMatch *dm = work->data;
773                 if (dm->di == child) return dm;
774                 work = work->next;
775                 }
776         return NULL;
777 }
778
779 static void dupe_match_link_child(DupeItem *child, DupeItem *parent, gdouble rank)
780 {
781         DupeMatch *dm;
782
783         dm = g_new0(DupeMatch, 1);
784         dm->di = child;
785         dm->rank = rank;
786         parent->group = g_list_append(parent->group, dm);
787 }
788
789 static void dupe_match_link(DupeItem *a, DupeItem *b, gdouble rank)
790 {
791         dupe_match_link_child(a, b, rank);
792         dupe_match_link_child(b, a, rank);
793 }
794
795 static void dupe_match_unlink_child(DupeItem *child, DupeItem *parent)
796 {
797         DupeMatch *dm;
798
799         dm = dupe_match_find_match(child, parent);
800         if (dm)
801                 {
802                 parent->group = g_list_remove(parent->group, dm);
803                 g_free(dm);
804                 }
805 }
806
807 static void dupe_match_unlink(DupeItem *a, DupeItem *b)
808 {
809         dupe_match_unlink_child(a, b);
810         dupe_match_unlink_child(b, a);
811 }
812
813 static void dupe_match_link_clear(DupeItem *parent, gboolean unlink_children)
814 {
815         GList *work;
816
817         work = parent->group;
818         while (work)
819                 {
820                 DupeMatch *dm = work->data;
821                 work = work->next;
822
823                 if (unlink_children) dupe_match_unlink_child(parent, dm->di);
824
825                 g_free(dm);
826                 }
827
828         g_list_free(parent->group);
829         parent->group = NULL;
830         parent->group_rank = 0.0;
831 }
832
833 static gint dupe_match_link_exists(DupeItem *child, DupeItem *parent)
834 {
835         return (dupe_match_find_match(child, parent) != NULL);
836 }
837
838 static gdouble dupe_match_link_rank(DupeItem *child, DupeItem *parent)
839 {
840         DupeMatch *dm;
841
842         dm = dupe_match_find_match(child, parent);
843         if (dm) return dm->rank;
844
845         return 0.0;
846 }
847
848 static DupeItem *dupe_match_highest_rank(DupeItem *child)
849 {
850         DupeMatch *dr;
851         GList *work;
852
853         dr = NULL;
854         work = child->group;
855         while (work)
856                 {
857                 DupeMatch *dm = work->data;
858                 if (!dr || dm->rank > dr->rank) dr = dm;
859                 work = work->next;
860                 }
861
862         return (dr) ? dr->di : NULL;
863 }
864
865 static void dupe_match_rank_update(DupeItem *parent)
866 {
867         GList *work;
868         gdouble rank = 0.0;
869         gint c = 0;
870
871         work = parent->group;
872         while (work)
873                 {
874                 DupeMatch *dm = work->data;
875                 work = work->next;
876                 rank += dm->rank;
877                 c++;
878                 }
879
880         if (c > 0)
881                 {
882                 parent->group_rank = rank / c;
883                 }
884         else
885                 {
886                 parent->group_rank = 0.0;
887                 }
888 }
889
890 static DupeItem *dupe_match_find_parent(DupeWindow *dw, DupeItem *child)
891 {
892         GList *work;
893
894         if (g_list_find(dw->dupes, child)) return child;
895
896         work = child->group;
897         while (work)
898                 {
899                 DupeMatch *dm = work->data;
900                 if (g_list_find(dw->dupes, dm->di)) return dm->di;
901                 work = work->next;
902                 }
903
904         return NULL;
905 }
906
907 static void dupe_match_reset_list(GList *work)
908 {
909         while (work)
910                 {
911                 DupeItem *di = work->data;
912                 work = work->next;
913
914                 dupe_match_link_clear(di, FALSE);
915                 }
916 }
917
918 static void dupe_match_reparent(DupeWindow *dw, DupeItem *old, DupeItem *new)
919 {
920         GList *work;
921
922         if (!old || !new || !dupe_match_link_exists(old, new)) return;
923
924         dupe_match_link_clear(new, TRUE);
925         work = old->group;
926         while (work)
927                 {
928                 DupeMatch *dm = work->data;
929                 dupe_match_unlink_child(old, dm->di);
930                 dupe_match_link_child(new, dm->di, dm->rank);
931                 work = work->next;
932                 }
933
934         new->group = old->group;
935         old->group = NULL;
936
937         work = g_list_find(dw->dupes, old);
938         if (work) work->data = new;
939 }
940
941 static void dupe_match_print_group(DupeItem *di)
942 {
943         GList *work;
944
945         log_printf("+ %f %s\n", di->group_rank, di->fd->name);
946
947         work = di->group;
948         while (work)
949                 {
950                 DupeMatch *dm = work->data;
951                 work = work->next;
952
953                 log_printf("  %f %s\n", dm->rank, dm->di->fd->name);
954                 }
955
956         log_printf("\n");
957 }
958
959 static void dupe_match_print_list(GList *list)
960 {
961         GList *work;
962
963         work = list;
964         while (work)
965                 {
966                 DupeItem *di = work->data;
967                 dupe_match_print_group(di);
968                 work = work->next;
969                 }
970 }
971
972 /* level 3, unlinking and orphan handling */
973 static GList *dupe_match_unlink_by_rank(DupeItem *child, DupeItem *parent, GList *list, DupeWindow *dw)
974 {
975         DupeItem *best;
976
977         best = dupe_match_highest_rank(parent);
978         if (best == child || dupe_match_highest_rank(child) == parent)
979                 {
980                 GList *work;
981                 gdouble rank;
982
983                 DEBUG_2("link found %s to %s [%d]", child->fd->name, parent->fd->name, g_list_length(parent->group));
984
985                 work = parent->group;
986                 while (work)
987                         {
988                         DupeMatch *dm = work->data;
989                         DupeItem *orphan;
990
991                         work = work->next;
992                         orphan = dm->di;
993                         if (orphan != child && g_list_length(orphan->group) < 2)
994                                 {
995                                 dupe_match_link_clear(orphan, TRUE);
996                                 if (!dw->second_set || orphan->second)
997                                         {
998                                         dupe_match(orphan, child, dw->match_mask, &rank, FALSE);
999                                         dupe_match_link(orphan, child, rank);
1000                                         }
1001                                 list = g_list_remove(list, orphan);
1002                                 }
1003                         }
1004
1005                 rank = dupe_match_link_rank(child, parent);
1006                 dupe_match_link_clear(parent, TRUE);
1007                 dupe_match_link(child, parent, rank);
1008                 list = g_list_remove(list, parent);
1009                 }
1010         else
1011                 {
1012                 DEBUG_2("unlinking %s and %s", child->fd->name, parent->fd->name);
1013
1014                 dupe_match_unlink(child, parent);
1015                 }
1016
1017         return list;
1018 }
1019
1020 /* level 2 */
1021 static GList *dupe_match_group_filter(GList *list, DupeItem *di, DupeWindow *dw)
1022 {
1023         GList *work;
1024
1025         work = g_list_last(di->group);
1026         while (work)
1027                 {
1028                 DupeMatch *dm = work->data;
1029                 work = work->prev;
1030                 list = dupe_match_unlink_by_rank(di, dm->di, list, dw);
1031                 }
1032
1033         return list;
1034 }
1035
1036 /* level 1 (top) */
1037 static GList *dupe_match_group_trim(GList *list, DupeWindow *dw)
1038 {
1039         GList *work;
1040
1041         work = list;
1042         while (work)
1043                 {
1044                 DupeItem *di = work->data;
1045                 if (!di->second) list = dupe_match_group_filter(list, di, dw);
1046                 work = work->next;
1047                 if (di->second) list = g_list_remove(list, di);
1048                 }
1049
1050         return list;
1051 }
1052
1053 static gint dupe_match_sort_groups_cb(gconstpointer a, gconstpointer b)
1054 {
1055         DupeMatch *da = (DupeMatch *)a;
1056         DupeMatch *db = (DupeMatch *)b;
1057
1058         if (da->rank > db->rank) return -1;
1059         if (da->rank < db->rank) return 1;
1060         return 0;
1061 }
1062
1063 static void dupe_match_sort_groups(GList *list)
1064 {
1065         GList *work;
1066
1067         work = list;
1068         while (work)
1069                 {
1070                 DupeItem *di = work->data;
1071                 di->group = g_list_sort(di->group, dupe_match_sort_groups_cb);
1072                 work = work->next;
1073                 }
1074 }
1075
1076 static gint dupe_match_totals_sort_cb(gconstpointer a, gconstpointer b)
1077 {
1078         DupeItem *da = (DupeItem *)a;
1079         DupeItem *db = (DupeItem *)b;
1080
1081         if (g_list_length(da->group) > g_list_length(db->group)) return -1;
1082         if (g_list_length(da->group) < g_list_length(db->group)) return 1;
1083
1084         if (da->group_rank < db->group_rank) return -1;
1085         if (da->group_rank > db->group_rank) return 1;
1086
1087         return 0;
1088 }
1089
1090 static gint dupe_match_rank_sort_cb(gconstpointer a, gconstpointer b)
1091 {
1092         DupeItem *da = (DupeItem *)a;
1093         DupeItem *db = (DupeItem *)b;
1094
1095         if (da->group_rank > db->group_rank) return -1;
1096         if (da->group_rank < db->group_rank) return 1;
1097         return 0;
1098 }
1099
1100 /* returns allocated GList of dupes sorted by rank */
1101 static GList *dupe_match_rank_sort(GList *source_list)
1102 {
1103         GList *list = NULL;
1104         GList *work;
1105
1106         work = source_list;
1107         while (work)
1108                 {
1109                 DupeItem *di = work->data;
1110
1111                 if (di->group)
1112                         {
1113                         dupe_match_rank_update(di);
1114                         list = g_list_prepend(list, di);
1115                         }
1116
1117                 work = work->next;
1118                 }
1119
1120         return g_list_sort(list, dupe_match_rank_sort_cb);
1121 }
1122
1123 /* returns allocated GList of dupes sorted by totals */
1124 static GList *dupe_match_totals_sort(GList *source_list)
1125 {
1126         source_list = g_list_sort(source_list, dupe_match_totals_sort_cb);
1127
1128         source_list = g_list_first(source_list);
1129         return g_list_reverse(source_list);
1130 }
1131
1132 static void dupe_match_rank(DupeWindow *dw)
1133 {
1134         GList *list;
1135
1136         list = dupe_match_rank_sort(dw->list);
1137
1138         if (required_debug_level(2)) dupe_match_print_list(list);
1139
1140         DEBUG_1("Similar items: %d", g_list_length(list));
1141         list = dupe_match_group_trim(list, dw);
1142         DEBUG_1("Unique groups: %d", g_list_length(list));
1143
1144         dupe_match_sort_groups(list);
1145
1146         if (required_debug_level(2)) dupe_match_print_list(list);
1147
1148         list = dupe_match_rank_sort(list);
1149         if (options->sort_totals)
1150                 {
1151                 list = dupe_match_totals_sort(list);
1152                 }
1153         if (required_debug_level(2)) dupe_match_print_list(list);
1154
1155         g_list_free(dw->dupes);
1156         dw->dupes = list;
1157 }
1158
1159 /*
1160  * ------------------------------------------------------------------
1161  * Match group tests
1162  * ------------------------------------------------------------------
1163  */
1164
1165 static gboolean dupe_match(DupeItem *a, DupeItem *b, DupeMatchType mask, gdouble *rank, gint fast)
1166 {
1167         *rank = 0.0;
1168
1169         if (a->fd->path == b->fd->path) return FALSE;
1170
1171         if (mask & DUPE_MATCH_PATH)
1172                 {
1173                 if (utf8_compare(a->fd->path, b->fd->path, TRUE) != 0) return FALSE;
1174                 }
1175         if (mask & DUPE_MATCH_NAME)
1176                 {
1177                 if (strcmp(a->fd->collate_key_name, b->fd->collate_key_name) != 0) return FALSE;
1178                 }
1179         if (mask & DUPE_MATCH_NAME_CI)
1180                 {
1181                 if (strcmp(a->fd->collate_key_name_nocase, b->fd->collate_key_name_nocase) != 0) return FALSE;
1182                 }
1183         if (mask & DUPE_MATCH_SIZE)
1184                 {
1185                 if (a->fd->size != b->fd->size) return FALSE;
1186                 }
1187         if (mask & DUPE_MATCH_DATE)
1188                 {
1189                 if (a->fd->date != b->fd->date) return FALSE;
1190                 }
1191         if (mask & DUPE_MATCH_SUM)
1192                 {
1193                 if (!a->md5sum) a->md5sum = md5_text_from_file_utf8(a->fd->path, "");
1194                 if (!b->md5sum) b->md5sum = md5_text_from_file_utf8(b->fd->path, "");
1195                 if (a->md5sum[0] == '\0' ||
1196                     b->md5sum[0] == '\0' ||
1197                     strcmp(a->md5sum, b->md5sum) != 0) return FALSE;
1198                 }
1199         if (mask & DUPE_MATCH_DIM)
1200                 {
1201                 if (a->width == 0) image_load_dimensions(a->fd, &a->width, &a->height);
1202                 if (b->width == 0) image_load_dimensions(b->fd, &b->width, &b->height);
1203                 if (a->width != b->width || a->height != b->height) return FALSE;
1204                 }
1205         if (mask & DUPE_MATCH_SIM_HIGH ||
1206             mask & DUPE_MATCH_SIM_MED ||
1207             mask & DUPE_MATCH_SIM_LOW ||
1208             mask & DUPE_MATCH_SIM_CUSTOM)
1209                 {
1210                 gdouble f;
1211                 gdouble m;
1212
1213                 if (mask & DUPE_MATCH_SIM_HIGH) m = 0.95;
1214                 else if (mask & DUPE_MATCH_SIM_MED) m = 0.90;
1215                 else if (mask & DUPE_MATCH_SIM_CUSTOM) m = (gdouble)options->duplicates_similarity_threshold / 100.0;
1216                 else m = 0.85;
1217
1218                 if (fast)
1219                         {
1220                         f = image_sim_compare_fast(a->simd, b->simd, m);
1221                         }
1222                 else
1223                         {
1224                         f = image_sim_compare(a->simd, b->simd);
1225                         }
1226
1227                 *rank = f * 100.0;
1228
1229                 if (f < m) return FALSE;
1230
1231                 DEBUG_3("similar: %32s %32s = %f", a->fd->name, b->fd->name, f);
1232                 }
1233
1234         return TRUE;
1235 }
1236
1237 static void dupe_list_check_match(DupeWindow *dw, DupeItem *needle, GList *start)
1238 {
1239         GList *work;
1240
1241         if (dw->second_set)
1242                 {
1243                 work = dw->second_list;
1244                 }
1245         else if (start)
1246                 {
1247                 work = start;
1248                 }
1249         else
1250                 {
1251                 work = g_list_last(dw->list);
1252                 }
1253
1254         while (work)
1255                 {
1256                 DupeItem *di = work->data;
1257
1258                 /* speed opt: forward for second set, back for simple compare */
1259                 if (dw->second_set)
1260                         work = work->next;
1261                 else
1262                         work = work->prev;
1263
1264                 if (!dupe_match_link_exists(needle, di))
1265                         {
1266                         gdouble rank;
1267
1268                         if (dupe_match(di, needle, dw->match_mask, &rank, TRUE))
1269                                 {
1270                                 dupe_match_link(di, needle, rank);
1271                                 }
1272                         }
1273                 }
1274 }
1275
1276 /*
1277  * ------------------------------------------------------------------
1278  * Thumbnail handling
1279  * ------------------------------------------------------------------
1280  */
1281
1282 static void dupe_listview_set_thumb(DupeWindow *dw, DupeItem *di, GtkTreeIter *iter)
1283 {
1284         GtkListStore *store;
1285         GtkTreeIter iter_n;
1286
1287         store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->listview)));
1288         if (!iter)
1289                 {
1290                 if (dupe_listview_find_item(store, di, &iter_n) >= 0)
1291                         {
1292                         iter = &iter_n;
1293                         }
1294                 }
1295
1296         if (iter) gtk_list_store_set(store, iter, DUPE_COLUMN_THUMB, di->pixbuf, -1);
1297 }
1298
1299 static void dupe_thumb_do(DupeWindow *dw)
1300 {
1301         DupeItem *di;
1302
1303         if (!dw->thumb_loader || !dw->thumb_item) return;
1304         di = dw->thumb_item;
1305
1306         if (di->pixbuf) g_object_unref(di->pixbuf);
1307         di->pixbuf = thumb_loader_get_pixbuf(dw->thumb_loader);
1308
1309         dupe_listview_set_thumb(dw, di, NULL);
1310 }
1311
1312 static void dupe_thumb_error_cb(ThumbLoader *tl, gpointer data)
1313 {
1314         DupeWindow *dw = data;
1315
1316         dupe_thumb_do(dw);
1317         dupe_thumb_step(dw);
1318 }
1319
1320 static void dupe_thumb_done_cb(ThumbLoader *tl, gpointer data)
1321 {
1322         DupeWindow *dw = data;
1323
1324         dupe_thumb_do(dw);
1325         dupe_thumb_step(dw);
1326 }
1327
1328 static void dupe_thumb_step(DupeWindow *dw)
1329 {
1330         GtkTreeModel *store;
1331         GtkTreeIter iter;
1332         DupeItem *di = NULL;
1333         gboolean valid;
1334         gint row = 0;
1335         gint length = 0;
1336
1337         store = gtk_tree_view_get_model(GTK_TREE_VIEW(dw->listview));
1338         valid = gtk_tree_model_get_iter_first(store, &iter);
1339
1340         while (!di && valid)
1341                 {
1342                 GdkPixbuf *pixbuf;
1343
1344                 length++;
1345                 gtk_tree_model_get(store, &iter, DUPE_COLUMN_POINTER, &di, DUPE_COLUMN_THUMB, &pixbuf, -1);
1346                 if (pixbuf || di->pixbuf)
1347                         {
1348                         if (!pixbuf) gtk_list_store_set(GTK_LIST_STORE(store), &iter, DUPE_COLUMN_THUMB, di->pixbuf, -1);
1349                         row++;
1350                         di = NULL;
1351                         }
1352                 valid = gtk_tree_model_iter_next(store, &iter);
1353                 }
1354         if (valid)
1355                 {
1356                 while (gtk_tree_model_iter_next(store, &iter)) length++;
1357                 }
1358
1359         if (!di)
1360                 {
1361                 dw->thumb_item = NULL;
1362                 thumb_loader_free(dw->thumb_loader);
1363                 dw->thumb_loader = NULL;
1364
1365                 dupe_window_update_progress(dw, NULL, 0.0, FALSE);
1366                 return;
1367                 }
1368
1369         dupe_window_update_progress(dw, _("Loading thumbs..."),
1370                                     length == 0 ? 0.0 : (gdouble)(row) / length, FALSE);
1371
1372         dw->thumb_item = di;
1373         thumb_loader_free(dw->thumb_loader);
1374         dw->thumb_loader = thumb_loader_new(options->thumbnails.max_width, options->thumbnails.max_height);
1375
1376         thumb_loader_set_callbacks(dw->thumb_loader,
1377                                    dupe_thumb_done_cb,
1378                                    dupe_thumb_error_cb,
1379                                    NULL,
1380                                    dw);
1381
1382         /* start it */
1383         if (!thumb_loader_start(dw->thumb_loader, di->fd))
1384                 {
1385                 /* error, handle it, do next */
1386                 DEBUG_1("error loading thumb for %s", di->fd->path);
1387                 dupe_thumb_do(dw);
1388                 dupe_thumb_step(dw);
1389                 }
1390 }
1391
1392 /*
1393  * ------------------------------------------------------------------
1394  * Dupe checking loop
1395  * ------------------------------------------------------------------
1396  */
1397
1398 static void dupe_check_stop(DupeWindow *dw)
1399 {
1400         if (dw->idle_id || dw->img_loader || dw->thumb_loader)
1401                 {
1402                 g_source_remove(dw->idle_id);
1403                 dw->idle_id = 0;
1404                 dupe_window_update_progress(dw, NULL, 0.0, FALSE);
1405                 widget_set_cursor(dw->listview, -1);
1406                 }
1407
1408         thumb_loader_free(dw->thumb_loader);
1409         dw->thumb_loader = NULL;
1410
1411         image_loader_free(dw->img_loader);
1412         dw->img_loader = NULL;
1413 }
1414
1415 static void dupe_loader_done_cb(ImageLoader *il, gpointer data)
1416 {
1417         DupeWindow *dw = data;
1418         GdkPixbuf *pixbuf;
1419
1420         pixbuf = image_loader_get_pixbuf(il);
1421
1422         if (dw->setup_point)
1423                 {
1424                 DupeItem *di = dw->setup_point->data;
1425
1426                 if (!di->simd)
1427                         {
1428                         di->simd = image_sim_new_from_pixbuf(pixbuf);
1429                         }
1430                 else
1431                         {
1432                         image_sim_fill_data(di->simd, pixbuf);
1433                         }
1434
1435                 if (di->width == 0 && di->height == 0)
1436                         {
1437                         di->width = gdk_pixbuf_get_width(pixbuf);
1438                         di->height = gdk_pixbuf_get_height(pixbuf);
1439                         }
1440                 if (options->thumbnails.enable_caching)
1441                         {
1442                         dupe_item_write_cache(di);
1443                         }
1444
1445                 image_sim_alternate_processing(di->simd);
1446                 }
1447
1448         image_loader_free(dw->img_loader);
1449         dw->img_loader = NULL;
1450
1451         dw->idle_id = g_idle_add(dupe_check_cb, dw);
1452 }
1453
1454 static void dupe_setup_reset(DupeWindow *dw)
1455 {
1456         dw->setup_point = NULL;
1457         dw->setup_n = 0;
1458         dw->setup_time = msec_time();
1459         dw->setup_time_count = 0;
1460 }
1461
1462 static GList *dupe_setup_point_step(DupeWindow *dw, GList *p)
1463 {
1464         if (!p) return NULL;
1465
1466         if (p->next) return p->next;
1467
1468         if (dw->second_set && g_list_first(p) == dw->list) return dw->second_list;
1469
1470         return NULL;
1471 }
1472
1473 static gboolean dupe_check_cb(gpointer data)
1474 {
1475         DupeWindow *dw = data;
1476
1477         if (!dw->idle_id) return FALSE;
1478
1479         if (!dw->setup_done)
1480                 {
1481                 if ((dw->match_mask & DUPE_MATCH_SUM) &&
1482                     !(dw->setup_mask & DUPE_MATCH_SUM) )
1483                         {
1484                         if (!dw->setup_point) dw->setup_point = dw->list;
1485
1486                         while (dw->setup_point)
1487                                 {
1488                                 DupeItem *di = dw->setup_point->data;
1489
1490                                 dw->setup_point = dupe_setup_point_step(dw, dw->setup_point);
1491                                 dw->setup_n++;
1492
1493                                 if (!di->md5sum)
1494                                         {
1495                                         dupe_window_update_progress(dw, _("Reading checksums..."),
1496                                                 dw->setup_count == 0 ? 0.0 : (gdouble)(dw->setup_n - 1) / dw->setup_count, FALSE);
1497
1498                                         if (options->thumbnails.enable_caching)
1499                                                 {
1500                                                 dupe_item_read_cache(di);
1501                                                 if (di->md5sum) return TRUE;
1502                                                 }
1503
1504                                         di->md5sum = md5_text_from_file_utf8(di->fd->path, "");
1505                                         if (options->thumbnails.enable_caching)
1506                                                 {
1507                                                 dupe_item_write_cache(di);
1508                                                 }
1509                                         return TRUE;
1510                                         }
1511                                 }
1512                         dw->setup_mask |= DUPE_MATCH_SUM;
1513                         dupe_setup_reset(dw);
1514                         }
1515                 if ((dw->match_mask & DUPE_MATCH_DIM) &&
1516                     !(dw->setup_mask & DUPE_MATCH_DIM) )
1517                         {
1518                         if (!dw->setup_point) dw->setup_point = dw->list;
1519
1520                         while (dw->setup_point)
1521                                 {
1522                                 DupeItem *di = dw->setup_point->data;
1523
1524                                 dw->setup_point = dupe_setup_point_step(dw, dw->setup_point);
1525                                 dw->setup_n++;
1526                                 if (di->width == 0 && di->height == 0)
1527                                         {
1528                                         dupe_window_update_progress(dw, _("Reading dimensions..."),
1529                                                 dw->setup_count == 0 ? 0.0 : (gdouble)(dw->setup_n - 1) / dw->setup_count, FALSE);
1530
1531                                         if (options->thumbnails.enable_caching)
1532                                                 {
1533                                                 dupe_item_read_cache(di);
1534                                                 if (di->width != 0 || di->height != 0) return TRUE;
1535                                                 }
1536
1537                                         image_load_dimensions(di->fd, &di->width, &di->height);
1538                                         if (options->thumbnails.enable_caching)
1539                                                 {
1540                                                 dupe_item_write_cache(di);
1541                                                 }
1542                                         return TRUE;
1543                                         }
1544                                 }
1545                         dw->setup_mask |= DUPE_MATCH_DIM;
1546                         dupe_setup_reset(dw);
1547                         }
1548                 if ((dw->match_mask & DUPE_MATCH_SIM_HIGH ||
1549                      dw->match_mask & DUPE_MATCH_SIM_MED ||
1550                      dw->match_mask & DUPE_MATCH_SIM_LOW ||
1551                      dw->match_mask & DUPE_MATCH_SIM_CUSTOM) &&
1552                     !(dw->setup_mask & DUPE_MATCH_SIM_MED) )
1553                         {
1554                         if (!dw->setup_point) dw->setup_point = dw->list;
1555
1556                         while (dw->setup_point)
1557                                 {
1558                                 DupeItem *di = dw->setup_point->data;
1559
1560                                 if (!di->simd)
1561                                         {
1562                                         dupe_window_update_progress(dw, _("Reading similarity data..."),
1563                                                 dw->setup_count == 0 ? 0.0 : (gdouble)dw->setup_n / dw->setup_count, FALSE);
1564
1565                                         if (options->thumbnails.enable_caching)
1566                                                 {
1567                                                 dupe_item_read_cache(di);
1568                                                 if (cache_sim_data_filled(di->simd))
1569                                                         {
1570                                                         image_sim_alternate_processing(di->simd);
1571                                                         return TRUE;
1572                                                         }
1573                                                 }
1574
1575                                         dw->img_loader = image_loader_new(di->fd);
1576                                         image_loader_set_buffer_size(dw->img_loader, 8);
1577                                         g_signal_connect(G_OBJECT(dw->img_loader), "error", (GCallback)dupe_loader_done_cb, dw);
1578                                         g_signal_connect(G_OBJECT(dw->img_loader), "done", (GCallback)dupe_loader_done_cb, dw);
1579
1580                                         if (!image_loader_start(dw->img_loader))
1581                                                 {
1582                                                 image_sim_free(di->simd);
1583                                                 di->simd = image_sim_new();
1584                                                 image_loader_free(dw->img_loader);
1585                                                 dw->img_loader = NULL;
1586                                                 return TRUE;
1587                                                 }
1588                                         dw->idle_id = 0;
1589                                         return FALSE;
1590                                         }
1591
1592                                 dw->setup_point = dupe_setup_point_step(dw, dw->setup_point);
1593                                 dw->setup_n++;
1594                                 }
1595                         dw->setup_mask |= DUPE_MATCH_SIM_MED;
1596                         dupe_setup_reset(dw);
1597                         }
1598                 dupe_window_update_progress(dw, _("Comparing..."), 0.0, FALSE);
1599                 dw->setup_done = TRUE;
1600                 dupe_setup_reset(dw);
1601                 dw->setup_count = g_list_length(dw->list);
1602                 }
1603
1604         if (!dw->working)
1605                 {
1606                 if (dw->setup_count > 0)
1607                         {
1608                         dw->setup_count = 0;
1609                         dupe_window_update_progress(dw, _("Sorting..."), 1.0, TRUE);
1610                         return TRUE;
1611                         }
1612                 dw->idle_id = 0;
1613                 dupe_window_update_progress(dw, NULL, 0.0, FALSE);
1614
1615                 dupe_match_rank(dw);
1616                 dupe_window_update_count(dw, FALSE);
1617
1618                 dupe_listview_populate(dw);
1619
1620                 /* check thumbs */
1621                 if (dw->show_thumbs) dupe_thumb_step(dw);
1622
1623                 widget_set_cursor(dw->listview, -1);
1624
1625                 return FALSE;
1626                 }
1627
1628         dupe_list_check_match(dw, (DupeItem *)dw->working->data, dw->working);
1629         dupe_window_update_progress(dw, _("Comparing..."), dw->setup_count == 0 ? 0.0 : (gdouble) dw->setup_n / dw->setup_count, FALSE);
1630         dw->setup_n++;
1631
1632         dw->working = dw->working->prev;
1633
1634         return TRUE;
1635 }
1636
1637 static void dupe_check_start(DupeWindow *dw)
1638 {
1639         dw->setup_done = FALSE;
1640
1641         dw->setup_count = g_list_length(dw->list);
1642         if (dw->second_set) dw->setup_count += g_list_length(dw->second_list);
1643
1644         dw->setup_mask = 0;
1645         dupe_setup_reset(dw);
1646
1647         dw->working = g_list_last(dw->list);
1648
1649         dupe_window_update_count(dw, TRUE);
1650         widget_set_cursor(dw->listview, GDK_WATCH);
1651
1652         if (dw->idle_id) return;
1653
1654         dw->idle_id = g_idle_add(dupe_check_cb, dw);
1655 }
1656
1657 /*
1658  * ------------------------------------------------------------------
1659  * Item addition, removal
1660  * ------------------------------------------------------------------
1661  */
1662
1663 static void dupe_item_remove(DupeWindow *dw, DupeItem *di)
1664 {
1665         if (!di) return;
1666
1667         /* handle things that may be in progress... */
1668         if (dw->working && dw->working->data == di)
1669                 {
1670                 dw->working = dw->working->prev;
1671                 }
1672         if (dw->thumb_loader && dw->thumb_item == di)
1673                 {
1674                 dupe_thumb_step(dw);
1675                 }
1676         if (dw->setup_point && dw->setup_point->data == di)
1677                 {
1678                 dw->setup_point = dupe_setup_point_step(dw, dw->setup_point);
1679                 if (dw->img_loader)
1680                         {
1681                         image_loader_free(dw->img_loader);
1682                         dw->img_loader = NULL;
1683                         dw->idle_id = g_idle_add(dupe_check_cb, dw);
1684                         }
1685                 }
1686
1687         if (di->group && dw->dupes)
1688                 {
1689                 /* is a dupe, must remove from group/reset children if a parent */
1690                 DupeItem *parent;
1691
1692                 parent = dupe_match_find_parent(dw, di);
1693                 if (di == parent)
1694                         {
1695                         if (g_list_length(parent->group) < 2)
1696                                 {
1697                                 DupeItem *child;
1698
1699                                 child = dupe_match_highest_rank(parent);
1700                                 dupe_match_link_clear(child, TRUE);
1701                                 dupe_listview_remove(dw, child);
1702
1703                                 dupe_match_link_clear(parent, TRUE);
1704                                 dupe_listview_remove(dw, parent);
1705                                 dw->dupes = g_list_remove(dw->dupes, parent);
1706                                 }
1707                         else
1708                                 {
1709                                 DupeItem *new_parent;
1710                                 DupeMatch *dm;
1711
1712                                 dm = parent->group->data;
1713                                 new_parent = dm->di;
1714                                 dupe_match_reparent(dw, parent, new_parent);
1715                                 dupe_listview_remove(dw, parent);
1716                                 }
1717                         }
1718                 else
1719                         {
1720                         if (g_list_length(parent->group) < 2)
1721                                 {
1722                                 dupe_match_link_clear(parent, TRUE);
1723                                 dupe_listview_remove(dw, parent);
1724                                 dw->dupes = g_list_remove(dw->dupes, parent);
1725                                 }
1726                         dupe_match_link_clear(di, TRUE);
1727                         dupe_listview_remove(dw, di);
1728                         }
1729                 }
1730         else
1731                 {
1732                 /* not a dupe, or not sorted yet, simply reset */
1733                 dupe_match_link_clear(di, TRUE);
1734                 }
1735
1736         if (dw->second_list && g_list_find(dw->second_list, di))
1737                 {
1738                 dupe_second_remove(dw, di);
1739                 }
1740         else
1741                 {
1742                 dw->list = g_list_remove(dw->list, di);
1743                 }
1744         dupe_item_free(di);
1745
1746         dupe_window_update_count(dw, FALSE);
1747 }
1748
1749 static gboolean dupe_item_remove_by_path(DupeWindow *dw, const gchar *path)
1750 {
1751         DupeItem *di;
1752
1753         di = dupe_item_find_path(dw, path);
1754         if (!di) return FALSE;
1755
1756         dupe_item_remove(dw, di);
1757
1758         return TRUE;
1759 }
1760
1761 static void dupe_files_add(DupeWindow *dw, CollectionData *collection, CollectInfo *info,
1762                            FileData *fd, gboolean recurse)
1763 {
1764         DupeItem *di = NULL;
1765
1766         if (info)
1767                 {
1768                 di = dupe_item_new(info->fd);
1769                 }
1770         else if (fd)
1771                 {
1772                 if (isfile(fd->path))
1773                         {
1774                         di = dupe_item_new(fd);
1775                         }
1776                 else if (isdir(fd->path) && recurse)
1777                         {
1778                         GList *f, *d;
1779                         if (filelist_read(fd, &f, &d))
1780                                 {
1781                                 GList *work;
1782
1783                                 f = filelist_filter(f, FALSE);
1784                                 d = filelist_filter(d, TRUE);
1785
1786                                 work = f;
1787                                 while (work)
1788                                         {
1789                                         dupe_files_add(dw, NULL, NULL, (FileData *)work->data, TRUE);
1790                                         work = work->next;
1791                                         }
1792                                 filelist_free(f);
1793                                 work = d;
1794                                 while (work)
1795                                         {
1796                                         dupe_files_add(dw, NULL, NULL, (FileData *)work->data, TRUE);
1797                                         work = work->next;
1798                                         }
1799                                 filelist_free(d);
1800                                 }
1801                         }
1802                 }
1803
1804         if (!di) return;
1805
1806         /* Ensure images in the lists have unique FileDatas */
1807         GList *work;
1808         DupeItem *di_list;
1809         work = g_list_first(dw->list);
1810         while (work)
1811                 {
1812                 di_list = work->data;
1813                 if (di_list->fd == di->fd)
1814                         {
1815                         return;
1816                         }
1817                 else
1818                         {
1819                         work = work->next;
1820                         }
1821                 }
1822
1823         if (dw->second_list)
1824                 {
1825                 work = g_list_first(dw->second_list);
1826                 while (work)
1827                         {
1828                         di_list = work->data;
1829                         if (di_list->fd == di->fd)
1830                                 {
1831                                 return;
1832                                 }
1833                         else
1834                                 {
1835                                 work = work->next;
1836                                 }
1837                         }
1838                 }
1839
1840         if (dw->second_drop)
1841                 {
1842                 dupe_second_add(dw, di);
1843                 }
1844         else
1845                 {
1846                 dw->list = g_list_prepend(dw->list, di);
1847                 }
1848 }
1849
1850 void dupe_window_add_collection(DupeWindow *dw, CollectionData *collection)
1851 {
1852         CollectInfo *info;
1853
1854         info = collection_get_first(collection);
1855         while (info)
1856                 {
1857                 dupe_files_add(dw, collection, info, NULL, FALSE);
1858                 info = collection_next_by_info(collection, info);
1859                 }
1860
1861         dupe_check_start(dw);
1862 }
1863
1864 void dupe_window_add_files(DupeWindow *dw, GList *list, gboolean recurse)
1865 {
1866         GList *work;
1867
1868         work = list;
1869         while (work)
1870                 {
1871                 FileData *fd = work->data;
1872                 work = work->next;
1873
1874                 dupe_files_add(dw, NULL, NULL, fd, recurse);
1875                 }
1876
1877         dupe_check_start(dw);
1878 }
1879
1880 static void dupe_item_update(DupeWindow *dw, DupeItem *di)
1881 {
1882         if ( (dw->match_mask & DUPE_MATCH_NAME) || (dw->match_mask & DUPE_MATCH_PATH || (dw->match_mask & DUPE_MATCH_NAME_CI)) )
1883                 {
1884                 /* only effects matches on name or path */
1885 /*
1886                 FileData *fd = file_data_ref(di->fd);
1887                 gint second;
1888
1889                 second = di->second;
1890                 dupe_item_remove(dw, di);
1891
1892                 dw->second_drop = second;
1893                 dupe_files_add(dw, NULL, NULL, fd, FALSE);
1894                 dw->second_drop = FALSE;
1895
1896                 file_data_unref(fd);
1897 */
1898                 dupe_check_start(dw);
1899                 }
1900         else
1901                 {
1902                 GtkListStore *store;
1903                 GtkTreeIter iter;
1904                 gint row;
1905                 /* update the listview(s) */
1906
1907                 store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->listview)));
1908                 row = dupe_listview_find_item(store, di, &iter);
1909                 if (row >= 0)
1910                         {
1911                         gtk_list_store_set(store, &iter,
1912                                            DUPE_COLUMN_NAME, di->fd->name,
1913                                            DUPE_COLUMN_PATH, di->fd->path, -1);
1914                         }
1915
1916                 if (dw->second_listview)
1917                         {
1918                         store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->second_listview)));
1919                         row = dupe_listview_find_item(store, di, &iter);
1920                         if (row >= 0)
1921                                 {
1922                                 gtk_list_store_set(store, &iter, 1, di->fd->path, -1);
1923                                 }
1924                         }
1925                 }
1926
1927 }
1928
1929 static void dupe_item_update_fd_in_list(DupeWindow *dw, FileData *fd, GList *work)
1930 {
1931         while (work)
1932                 {
1933                 DupeItem *di = work->data;
1934
1935                 if (di->fd == fd)
1936                         dupe_item_update(dw, di);
1937
1938                 work = work->next;
1939                 }
1940 }
1941
1942 static void dupe_item_update_fd(DupeWindow *dw, FileData *fd)
1943 {
1944         dupe_item_update_fd_in_list(dw, fd, dw->list);
1945         if (dw->second_set) dupe_item_update_fd_in_list(dw, fd, dw->second_list);
1946 }
1947
1948
1949 /*
1950  * ------------------------------------------------------------------
1951  * Misc.
1952  * ------------------------------------------------------------------
1953  */
1954
1955 static GtkWidget *dupe_display_label(GtkWidget *vbox, const gchar *description, const gchar *text)
1956 {
1957         GtkWidget *hbox;
1958         GtkWidget *label;
1959
1960         hbox = gtk_hbox_new(FALSE, 10);
1961
1962         label = gtk_label_new(description);
1963         gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 0);
1964         gtk_widget_show(label);
1965
1966         label = gtk_label_new(text);
1967         gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 0);
1968         gtk_widget_show(label);
1969
1970         gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0);
1971         gtk_widget_show(hbox);
1972
1973         return label;
1974 }
1975
1976 static void dupe_display_stats(DupeWindow *dw, DupeItem *di)
1977 {
1978         GenericDialog *gd;
1979         gchar *buf;
1980
1981         if (!di) return;
1982
1983         gd = file_util_gen_dlg("Image thumbprint debug info", "thumbprint",
1984                                dw->window, TRUE,
1985                                NULL, NULL);
1986         generic_dialog_add_button(gd, GTK_STOCK_CLOSE, NULL, NULL, TRUE);
1987
1988         dupe_display_label(gd->vbox, "name:", di->fd->name);
1989         buf = text_from_size(di->fd->size);
1990         dupe_display_label(gd->vbox, "size:", buf);
1991         g_free(buf);
1992         dupe_display_label(gd->vbox, "date:", text_from_time(di->fd->date));
1993         buf = g_strdup_printf("%d x %d", di->width, di->height);
1994         dupe_display_label(gd->vbox, "dimensions:", buf);
1995         g_free(buf);
1996         dupe_display_label(gd->vbox, "md5sum:", (di->md5sum) ? di->md5sum : "not generated");
1997
1998         dupe_display_label(gd->vbox, "thumbprint:", (di->simd) ? "" : "not generated");
1999         if (di->simd)
2000                 {
2001                 GtkWidget *image;
2002                 GdkPixbuf *pixbuf;
2003                 gint x, y;
2004                 guchar *d_pix;
2005                 guchar *dp;
2006                 gint rs;
2007                 gint sp;
2008
2009                 pixbuf = gdk_pixbuf_new(GDK_COLORSPACE_RGB, FALSE, 8, 32, 32);
2010                 rs = gdk_pixbuf_get_rowstride(pixbuf);
2011                 d_pix = gdk_pixbuf_get_pixels(pixbuf);
2012
2013                 for (y = 0; y < 32; y++)
2014                         {
2015                         dp = d_pix + (y * rs);
2016                         sp = y * 32;
2017                         for (x = 0; x < 32; x++)
2018                                 {
2019                                 *(dp++) = di->simd->avg_r[sp + x];
2020                                 *(dp++) = di->simd->avg_g[sp + x];
2021                                 *(dp++) = di->simd->avg_b[sp + x];
2022                                 }
2023                         }
2024
2025                 image = gtk_image_new_from_pixbuf(pixbuf);
2026                 gtk_box_pack_start(GTK_BOX(gd->vbox), image, FALSE, FALSE, 0);
2027                 gtk_widget_show(image);
2028
2029                 g_object_unref(pixbuf);
2030                 }
2031
2032         gtk_widget_show(gd->dialog);
2033 }
2034
2035 static void dupe_window_recompare(DupeWindow *dw)
2036 {
2037         GtkListStore *store;
2038
2039         dupe_check_stop(dw);
2040
2041         store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->listview)));
2042         gtk_list_store_clear(store);
2043
2044         g_list_free(dw->dupes);
2045         dw->dupes = NULL;
2046
2047         dupe_match_reset_list(dw->list);
2048         dupe_match_reset_list(dw->second_list);
2049
2050         dupe_check_start(dw);
2051 }
2052
2053 static void dupe_menu_view(DupeWindow *dw, DupeItem *di, GtkWidget *listview, gint new_window)
2054 {
2055         if (!di) return;
2056
2057         if (di->collection && collection_info_valid(di->collection, di->info))
2058                 {
2059                 if (new_window)
2060                         {
2061                         view_window_new_from_collection(di->collection, di->info);
2062                         }
2063                 else
2064                         {
2065                         layout_image_set_collection(NULL, di->collection, di->info);
2066                         }
2067                 }
2068         else
2069                 {
2070                 if (new_window)
2071                         {
2072                         GList *list;
2073
2074                         list = dupe_listview_get_selection(dw, listview);
2075                         view_window_new_from_list(list);
2076                         filelist_free(list);
2077                         }
2078                 else
2079                         {
2080                         layout_set_fd(NULL, di->fd);
2081                         }
2082                 }
2083 }
2084
2085 static void dupe_window_remove_selection(DupeWindow *dw, GtkWidget *listview)
2086 {
2087         GtkTreeSelection *selection;
2088         GtkTreeModel *store;
2089         GtkTreeIter iter;
2090         GList *slist;
2091         GList *list = NULL;
2092         GList *work;
2093
2094         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(listview));
2095         slist = gtk_tree_selection_get_selected_rows(selection, &store);
2096         work = slist;
2097         while (work)
2098                 {
2099                 GtkTreePath *tpath = work->data;
2100                 DupeItem *di = NULL;
2101
2102                 gtk_tree_model_get_iter(store, &iter, tpath);
2103                 gtk_tree_model_get(store, &iter, DUPE_COLUMN_POINTER, &di, -1);
2104                 if (di) list = g_list_prepend(list, di);
2105                 work = work->next;
2106                 }
2107         g_list_foreach(slist, (GFunc)gtk_tree_path_free, NULL);
2108         g_list_free(slist);
2109
2110         dw->color_frozen = TRUE;
2111         work = list;
2112         while (work)
2113                 {
2114                 DupeItem *di;
2115
2116                 di = work->data;
2117                 work = work->next;
2118                 dupe_item_remove(dw, di);
2119                 }
2120         dw->color_frozen = FALSE;
2121
2122         g_list_free(list);
2123
2124         dupe_listview_realign_colors(dw);
2125 }
2126
2127 static void dupe_window_edit_selected(DupeWindow *dw, const gchar *key)
2128 {
2129         file_util_start_editor_from_filelist(key, dupe_listview_get_selection(dw, dw->listview), NULL, dw->window);
2130 }
2131
2132 static void dupe_window_collection_from_selection(DupeWindow *dw)
2133 {
2134         CollectWindow *w;
2135         GList *list;
2136
2137         list = dupe_listview_get_selection(dw, dw->listview);
2138         w = collection_window_new(NULL);
2139         collection_table_add_filelist(w->table, list);
2140         filelist_free(list);
2141 }
2142
2143 static void dupe_window_append_file_list(DupeWindow *dw, gint on_second)
2144 {
2145         GList *list;
2146
2147         dw->second_drop = (dw->second_set && on_second);
2148
2149         list = layout_list(NULL);
2150         dupe_window_add_files(dw, list, FALSE);
2151         filelist_free(list);
2152 }
2153
2154 /*
2155  *-------------------------------------------------------------------
2156  * main pop-up menu callbacks
2157  *-------------------------------------------------------------------
2158  */
2159
2160 static void dupe_menu_view_cb(GtkWidget *widget, gpointer data)
2161 {
2162         DupeWindow *dw = data;
2163
2164         if (dw->click_item) dupe_menu_view(dw, dw->click_item, dw->listview, FALSE);
2165 }
2166
2167 static void dupe_menu_viewnew_cb(GtkWidget *widget, gpointer data)
2168 {
2169         DupeWindow *dw = data;
2170
2171         if (dw->click_item) dupe_menu_view(dw, dw->click_item, dw->listview, TRUE);
2172 }
2173
2174 static void dupe_menu_select_all_cb(GtkWidget *widget, gpointer data)
2175 {
2176         DupeWindow *dw = data;
2177         GtkTreeSelection *selection;
2178
2179         options->duplicates_select_type = DUPE_SELECT_NONE;
2180         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(dw->listview));
2181         gtk_tree_selection_select_all(selection);
2182 }
2183
2184 static void dupe_menu_select_none_cb(GtkWidget *widget, gpointer data)
2185 {
2186         DupeWindow *dw = data;
2187         GtkTreeSelection *selection;
2188
2189         options->duplicates_select_type = DUPE_SELECT_NONE;
2190         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(dw->listview));
2191         gtk_tree_selection_unselect_all(selection);
2192 }
2193
2194 static void dupe_menu_select_dupes_set1_cb(GtkWidget *widget, gpointer data)
2195 {
2196         DupeWindow *dw = data;
2197
2198         options->duplicates_select_type = DUPE_SELECT_GROUP1;
2199         dupe_listview_select_dupes(dw, DUPE_SELECT_GROUP1);
2200 }
2201
2202 static void dupe_menu_select_dupes_set2_cb(GtkWidget *widget, gpointer data)
2203 {
2204         DupeWindow *dw = data;
2205
2206         options->duplicates_select_type = DUPE_SELECT_GROUP2;
2207         dupe_listview_select_dupes(dw, DUPE_SELECT_GROUP2);
2208 }
2209
2210 static void dupe_menu_edit_cb(GtkWidget *widget, gpointer data)
2211 {
2212         DupeWindow *dw;
2213         const gchar *key = data;
2214
2215         dw = submenu_item_get_data(widget);
2216         if (!dw) return;
2217
2218         dupe_window_edit_selected(dw, key);
2219 }
2220
2221 static void dupe_menu_print_cb(GtkWidget *widget, gpointer data)
2222 {
2223         DupeWindow *dw = data;
2224         FileData *fd;
2225
2226         fd = (dw->click_item) ? dw->click_item->fd : NULL;
2227
2228         print_window_new(fd,
2229                          dupe_listview_get_selection(dw, dw->listview),
2230                          dupe_listview_get_filelist(dw, dw->listview), dw->window);
2231 }
2232
2233 static void dupe_menu_copy_cb(GtkWidget *widget, gpointer data)
2234 {
2235         DupeWindow *dw = data;
2236
2237         file_util_copy(NULL, dupe_listview_get_selection(dw, dw->listview), NULL, dw->window);
2238 }
2239
2240 static void dupe_menu_move_cb(GtkWidget *widget, gpointer data)
2241 {
2242         DupeWindow *dw = data;
2243
2244         file_util_move(NULL, dupe_listview_get_selection(dw, dw->listview), NULL, dw->window);
2245 }
2246
2247 static void dupe_menu_rename_cb(GtkWidget *widget, gpointer data)
2248 {
2249         DupeWindow *dw = data;
2250
2251         file_util_rename(NULL, dupe_listview_get_selection(dw, dw->listview), dw->window);
2252 }
2253
2254 static void dupe_menu_delete_cb(GtkWidget *widget, gpointer data)
2255 {
2256         DupeWindow *dw = data;
2257
2258         options->file_ops.safe_delete_enable = FALSE;
2259         file_util_delete(NULL, dupe_listview_get_selection(dw, dw->listview), dw->window);
2260 }
2261
2262 static void dupe_menu_move_to_trash_cb(GtkWidget *widget, gpointer data)
2263 {
2264         DupeWindow *dw = data;
2265
2266         options->file_ops.safe_delete_enable = TRUE;
2267         file_util_delete(NULL, dupe_listview_get_selection(dw, dw->listview), dw->window);
2268 }
2269
2270 static void dupe_menu_copy_path_cb(GtkWidget *widget, gpointer data)
2271 {
2272         DupeWindow *dw = data;
2273
2274         file_util_copy_path_list_to_clipboard(dupe_listview_get_selection(dw, dw->listview), TRUE);
2275 }
2276
2277 static void dupe_menu_copy_path_unquoted_cb(GtkWidget *widget, gpointer data)
2278 {
2279         DupeWindow *dw = data;
2280
2281         file_util_copy_path_list_to_clipboard(dupe_listview_get_selection(dw, dw->listview), FALSE);
2282 }
2283
2284 static void dupe_menu_remove_cb(GtkWidget *widget, gpointer data)
2285 {
2286         DupeWindow *dw = data;
2287
2288         dupe_window_remove_selection(dw, dw->listview);
2289 }
2290
2291 static void dupe_menu_clear_cb(GtkWidget *widget, gpointer data)
2292 {
2293         DupeWindow *dw = data;
2294
2295         dupe_window_clear(dw);
2296 }
2297
2298 static void dupe_menu_close_cb(GtkWidget *widget, gpointer data)
2299 {
2300         DupeWindow *dw = data;
2301
2302         dupe_window_close(dw);
2303 }
2304
2305 static void dupe_menu_popup_destroy_cb(GtkWidget *widget, gpointer data)
2306 {
2307         GList *editmenu_fd_list = data;
2308
2309         filelist_free(editmenu_fd_list);
2310 }
2311
2312 static GList *dupe_window_get_fd_list(DupeWindow *dw)
2313 {
2314         GList *list;
2315
2316         if (gtk_widget_has_focus(dw->second_listview))
2317                 {
2318                 list = dupe_listview_get_selection(dw, dw->second_listview);
2319                 }
2320         else
2321                 {
2322                 list = dupe_listview_get_selection(dw, dw->listview);
2323                 }
2324
2325         return list;
2326 }
2327
2328 /**
2329  * @brief Add file selection list to a collection
2330  * @param[in] widget 
2331  * @param[in] data Index to the collection list menu item selected, or -1 for new collection
2332  * 
2333  * 
2334  */
2335 static void dupe_pop_menu_collections_cb(GtkWidget *widget, gpointer data)
2336 {
2337         DupeWindow *dw;
2338         GList *selection_list;
2339
2340         dw = submenu_item_get_data(widget);
2341         selection_list = dupe_listview_get_selection(dw, dw->listview);
2342         pop_menu_collections(selection_list, data);
2343
2344         filelist_free(selection_list);
2345 }
2346
2347 static GtkWidget *dupe_menu_popup_main(DupeWindow *dw, DupeItem *di)
2348 {
2349         GtkWidget *menu;
2350         GtkWidget *item;
2351         gint on_row;
2352         GList *editmenu_fd_list;
2353
2354         on_row = (di != NULL);
2355
2356         menu = popup_menu_short_lived();
2357
2358         menu_item_add_sensitive(menu, _("_View"), on_row,
2359                                 G_CALLBACK(dupe_menu_view_cb), dw);
2360         menu_item_add_stock_sensitive(menu, _("View in _new window"), GTK_STOCK_NEW, on_row,
2361                                 G_CALLBACK(dupe_menu_viewnew_cb), dw);
2362         menu_item_add_divider(menu);
2363         menu_item_add_sensitive(menu, _("Select all"), (dw->dupes != NULL),
2364                                 G_CALLBACK(dupe_menu_select_all_cb), dw);
2365         menu_item_add_sensitive(menu, _("Select none"), (dw->dupes != NULL),
2366                                 G_CALLBACK(dupe_menu_select_none_cb), dw);
2367         menu_item_add_sensitive(menu, _("Select group _1 duplicates"), (dw->dupes != NULL),
2368                                 G_CALLBACK(dupe_menu_select_dupes_set1_cb), dw);
2369         menu_item_add_sensitive(menu, _("Select group _2 duplicates"), (dw->dupes != NULL),
2370                                 G_CALLBACK(dupe_menu_select_dupes_set2_cb), dw);
2371         menu_item_add_divider(menu);
2372
2373         submenu_add_export(menu, &item, G_CALLBACK(dupe_pop_menu_export_cb), dw);
2374         gtk_widget_set_sensitive(item, on_row);
2375         menu_item_add_divider(menu);
2376
2377         editmenu_fd_list = dupe_window_get_fd_list(dw);
2378         g_signal_connect(G_OBJECT(menu), "destroy",
2379                          G_CALLBACK(dupe_menu_popup_destroy_cb), editmenu_fd_list);
2380         submenu_add_edit(menu, &item, G_CALLBACK(dupe_menu_edit_cb), dw, editmenu_fd_list);
2381         if (!on_row) gtk_widget_set_sensitive(item, FALSE);
2382
2383         submenu_add_collections(menu, &item,
2384                                                                 G_CALLBACK(dupe_pop_menu_collections_cb), dw);
2385         gtk_widget_set_sensitive(item, on_row);
2386
2387         menu_item_add_stock_sensitive(menu, _("Print..."), GTK_STOCK_PRINT, on_row,
2388                                 G_CALLBACK(dupe_menu_print_cb), dw);
2389         menu_item_add_divider(menu);
2390         menu_item_add_stock_sensitive(menu, _("_Copy..."), GTK_STOCK_COPY, on_row,
2391                                 G_CALLBACK(dupe_menu_copy_cb), dw);
2392         menu_item_add_sensitive(menu, _("_Move..."), on_row,
2393                                 G_CALLBACK(dupe_menu_move_cb), dw);
2394         menu_item_add_sensitive(menu, _("_Rename..."), on_row,
2395                                 G_CALLBACK(dupe_menu_rename_cb), dw);
2396         menu_item_add_sensitive(menu, _("_Copy path"), on_row,
2397                                 G_CALLBACK(dupe_menu_copy_path_cb), dw);
2398         menu_item_add_sensitive(menu, _("_Copy path unquoted"), on_row,
2399                                 G_CALLBACK(dupe_menu_copy_path_unquoted_cb), dw);
2400
2401         menu_item_add_divider(menu);
2402         menu_item_add_stock_sensitive(menu,
2403                                 options->file_ops.confirm_move_to_trash ? _("Move to Trash...") :
2404                                         _("Move to Trash"), PIXBUF_INLINE_ICON_TRASH, on_row,
2405                                 G_CALLBACK(dupe_menu_move_to_trash_cb), dw);
2406         menu_item_add_stock_sensitive(menu,
2407                                 options->file_ops.confirm_delete ? _("_Delete...") :
2408                                         _("_Delete"), GTK_STOCK_DELETE, on_row,
2409                                 G_CALLBACK(dupe_menu_delete_cb), dw);
2410
2411         menu_item_add_divider(menu);
2412         menu_item_add_stock_sensitive(menu, _("Rem_ove"), GTK_STOCK_REMOVE, on_row,
2413                                 G_CALLBACK(dupe_menu_remove_cb), dw);
2414         menu_item_add_stock_sensitive(menu, _("C_lear"), GTK_STOCK_CLEAR, (dw->list != NULL),
2415                                 G_CALLBACK(dupe_menu_clear_cb), dw);
2416         menu_item_add_divider(menu);
2417         menu_item_add_stock(menu, _("Close _window"), GTK_STOCK_CLOSE,
2418                             G_CALLBACK(dupe_menu_close_cb), dw);
2419
2420         return menu;
2421 }
2422
2423 static gboolean dupe_listview_press_cb(GtkWidget *widget, GdkEventButton *bevent, gpointer data)
2424 {
2425         DupeWindow *dw = data;
2426         GtkTreeModel *store;
2427         GtkTreePath *tpath;
2428         GtkTreeIter iter;
2429         DupeItem *di = NULL;
2430
2431         store = gtk_tree_view_get_model(GTK_TREE_VIEW(widget));
2432
2433         if (gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(widget), bevent->x, bevent->y,
2434                                           &tpath, NULL, NULL, NULL))
2435                 {
2436                 gtk_tree_model_get_iter(store, &iter, tpath);
2437                 gtk_tree_model_get(store, &iter, DUPE_COLUMN_POINTER, &di, -1);
2438                 gtk_tree_path_free(tpath);
2439                 }
2440
2441         dw->click_item = di;
2442
2443         if (bevent->button == MOUSE_BUTTON_RIGHT)
2444                 {
2445                 /* right click menu */
2446                 GtkWidget *menu;
2447
2448                 if (bevent->state & GDK_CONTROL_MASK && bevent->state & GDK_SHIFT_MASK)
2449                         {
2450                         dupe_display_stats(dw, di);
2451                         return TRUE;
2452                         }
2453                 if (widget == dw->listview)
2454                         {
2455                         menu = dupe_menu_popup_main(dw, di);
2456                         }
2457                 else
2458                         {
2459                         menu = dupe_menu_popup_second(dw, di);
2460                         }
2461                 gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL, bevent->button, bevent->time);
2462                 }
2463
2464         if (!di) return FALSE;
2465
2466         if (bevent->button == MOUSE_BUTTON_LEFT &&
2467             bevent->type == GDK_2BUTTON_PRESS)
2468                 {
2469                 dupe_menu_view(dw, di, widget, FALSE);
2470                 }
2471
2472         if (bevent->button == MOUSE_BUTTON_MIDDLE) return TRUE;
2473
2474         if (bevent->button == MOUSE_BUTTON_RIGHT)
2475                 {
2476                 if (!dupe_listview_item_is_selected(dw, di, widget))
2477                         {
2478                         GtkTreeSelection *selection;
2479
2480                         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(widget));
2481                         gtk_tree_selection_unselect_all(selection);
2482                         gtk_tree_selection_select_iter(selection, &iter);
2483
2484                         tpath = gtk_tree_model_get_path(GTK_TREE_MODEL(store), &iter);
2485                         gtk_tree_view_set_cursor(GTK_TREE_VIEW(widget), tpath, NULL, FALSE);
2486                         gtk_tree_path_free(tpath);
2487                         }
2488
2489                 return TRUE;
2490                 }
2491
2492         if (bevent->button == MOUSE_BUTTON_LEFT &&
2493             bevent->type == GDK_BUTTON_PRESS &&
2494             !(bevent->state & GDK_SHIFT_MASK ) &&
2495             !(bevent->state & GDK_CONTROL_MASK ) &&
2496             dupe_listview_item_is_selected(dw, di, widget))
2497                 {
2498                 /* this selection handled on release_cb */
2499                 gtk_widget_grab_focus(widget);
2500                 return TRUE;
2501                 }
2502
2503         return FALSE;
2504 }
2505
2506 static gboolean dupe_listview_release_cb(GtkWidget *widget, GdkEventButton *bevent, gpointer data)
2507 {
2508         DupeWindow *dw = data;
2509         GtkTreeModel *store;
2510         GtkTreePath *tpath;
2511         GtkTreeIter iter;
2512         DupeItem *di = NULL;
2513
2514         if (bevent->button != MOUSE_BUTTON_LEFT && bevent->button != MOUSE_BUTTON_MIDDLE) return TRUE;
2515
2516         store = gtk_tree_view_get_model(GTK_TREE_VIEW(widget));
2517
2518         if ((bevent->x != 0 || bevent->y != 0) &&
2519             gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(widget), bevent->x, bevent->y,
2520                                           &tpath, NULL, NULL, NULL))
2521                 {
2522                 gtk_tree_model_get_iter(store, &iter, tpath);
2523                 gtk_tree_model_get(store, &iter, DUPE_COLUMN_POINTER, &di, -1);
2524                 gtk_tree_path_free(tpath);
2525                 }
2526
2527         if (bevent->button == MOUSE_BUTTON_MIDDLE)
2528                 {
2529                 if (di && dw->click_item == di)
2530                         {
2531                         GtkTreeSelection *selection;
2532
2533                         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(widget));
2534                         if (dupe_listview_item_is_selected(dw, di, widget))
2535                                 {
2536                                 gtk_tree_selection_unselect_iter(selection, &iter);
2537                                 }
2538                         else
2539                                 {
2540                                 gtk_tree_selection_select_iter(selection, &iter);
2541                                 }
2542                         }
2543                 return TRUE;
2544                 }
2545
2546         if (di && dw->click_item == di &&
2547             !(bevent->state & GDK_SHIFT_MASK ) &&
2548             !(bevent->state & GDK_CONTROL_MASK ) &&
2549             dupe_listview_item_is_selected(dw, di, widget))
2550                 {
2551                 GtkTreeSelection *selection;
2552
2553                 selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(widget));
2554                 gtk_tree_selection_unselect_all(selection);
2555                 gtk_tree_selection_select_iter(selection, &iter);
2556
2557                 tpath = gtk_tree_model_get_path(store, &iter);
2558                 gtk_tree_view_set_cursor(GTK_TREE_VIEW(widget), tpath, NULL, FALSE);
2559                 gtk_tree_path_free(tpath);
2560
2561                 return TRUE;
2562                 }
2563
2564         return FALSE;
2565 }
2566
2567 /*
2568  *-------------------------------------------------------------------
2569  * second set stuff
2570  *-------------------------------------------------------------------
2571  */
2572
2573 static void dupe_second_update_status(DupeWindow *dw)
2574 {
2575         gchar *buf;
2576
2577         buf = g_strdup_printf(_("%d files (set 2)"), g_list_length(dw->second_list));
2578         gtk_label_set_text(GTK_LABEL(dw->second_status_label), buf);
2579         g_free(buf);
2580 }
2581
2582 static void dupe_second_add(DupeWindow *dw, DupeItem *di)
2583 {
2584         GtkListStore *store;
2585         GtkTreeIter iter;
2586
2587         if (!di) return;
2588
2589         di->second = TRUE;
2590         dw->second_list = g_list_prepend(dw->second_list, di);
2591
2592         store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->second_listview)));
2593         gtk_list_store_append(store, &iter);
2594         gtk_list_store_set(store, &iter, DUPE_COLUMN_POINTER, di, 1, di->fd->path, -1);
2595
2596         dupe_second_update_status(dw);
2597 }
2598
2599 static void dupe_second_remove(DupeWindow *dw, DupeItem *di)
2600 {
2601         GtkListStore *store;
2602         GtkTreeIter iter;
2603
2604         store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->second_listview)));
2605         if (dupe_listview_find_item(store, di, &iter) >= 0)
2606                 {
2607                 tree_view_move_cursor_away(GTK_TREE_VIEW(dw->second_listview), &iter, TRUE);
2608                 gtk_list_store_remove(store, &iter);
2609                 }
2610
2611         dw->second_list = g_list_remove(dw->second_list, di);
2612
2613         dupe_second_update_status(dw);
2614 }
2615
2616 static void dupe_second_clear(DupeWindow *dw)
2617 {
2618         GtkListStore *store;
2619
2620         store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->second_listview)));
2621         gtk_list_store_clear(store);
2622         gtk_tree_view_columns_autosize(GTK_TREE_VIEW(dw->second_listview));
2623
2624         g_list_free(dw->dupes);
2625         dw->dupes = NULL;
2626
2627         dupe_list_free(dw->second_list);
2628         dw->second_list = NULL;
2629
2630         dupe_match_reset_list(dw->list);
2631
2632         dupe_second_update_status(dw);
2633 }
2634
2635 static void dupe_second_menu_view_cb(GtkWidget *widget, gpointer data)
2636 {
2637         DupeWindow *dw = data;
2638
2639         if (dw->click_item) dupe_menu_view(dw, dw->click_item, dw->second_listview, FALSE);
2640 }
2641
2642 static void dupe_second_menu_viewnew_cb(GtkWidget *widget, gpointer data)
2643 {
2644         DupeWindow *dw = data;
2645
2646         if (dw->click_item) dupe_menu_view(dw, dw->click_item, dw->second_listview, TRUE);
2647 }
2648
2649 static void dupe_second_menu_select_all_cb(GtkWidget *widget, gpointer data)
2650 {
2651         GtkTreeSelection *selection;
2652         DupeWindow *dw = data;
2653
2654         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(dw->second_listview));
2655         gtk_tree_selection_select_all(selection);
2656 }
2657
2658 static void dupe_second_menu_select_none_cb(GtkWidget *widget, gpointer data)
2659 {
2660         GtkTreeSelection *selection;
2661         DupeWindow *dw = data;
2662
2663         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(dw->second_listview));
2664         gtk_tree_selection_unselect_all(selection);
2665 }
2666
2667 static void dupe_second_menu_remove_cb(GtkWidget *widget, gpointer data)
2668 {
2669         DupeWindow *dw = data;
2670
2671         dupe_window_remove_selection(dw, dw->second_listview);
2672 }
2673
2674 static void dupe_second_menu_clear_cb(GtkWidget *widget, gpointer data)
2675 {
2676         DupeWindow *dw = data;
2677
2678         dupe_second_clear(dw);
2679         dupe_window_recompare(dw);
2680 }
2681
2682 static GtkWidget *dupe_menu_popup_second(DupeWindow *dw, DupeItem *di)
2683 {
2684         GtkWidget *menu;
2685         gboolean notempty = (dw->second_list != NULL);
2686         gboolean on_row = (di != NULL);
2687
2688         menu = popup_menu_short_lived();
2689         menu_item_add_sensitive(menu, _("_View"), on_row,
2690                                 G_CALLBACK(dupe_second_menu_view_cb), dw);
2691         menu_item_add_stock_sensitive(menu, _("View in _new window"), GTK_STOCK_NEW, on_row,
2692                                 G_CALLBACK(dupe_second_menu_viewnew_cb), dw);
2693         menu_item_add_divider(menu);
2694         menu_item_add_sensitive(menu, _("Select all"), notempty,
2695                                 G_CALLBACK(dupe_second_menu_select_all_cb), dw);
2696         menu_item_add_sensitive(menu, _("Select none"), notempty,
2697                                 G_CALLBACK(dupe_second_menu_select_none_cb), dw);
2698         menu_item_add_divider(menu);
2699         menu_item_add_stock_sensitive(menu, _("Rem_ove"), GTK_STOCK_REMOVE, on_row,
2700                                       G_CALLBACK(dupe_second_menu_remove_cb), dw);
2701         menu_item_add_stock_sensitive(menu, _("C_lear"), GTK_STOCK_CLEAR, notempty,
2702                                    G_CALLBACK(dupe_second_menu_clear_cb), dw);
2703         menu_item_add_divider(menu);
2704         menu_item_add_stock(menu, _("Close _window"), GTK_STOCK_CLOSE,
2705                             G_CALLBACK(dupe_menu_close_cb), dw);
2706
2707         return menu;
2708 }
2709
2710 static void dupe_second_set_toggle_cb(GtkWidget *widget, gpointer data)
2711 {
2712         DupeWindow *dw = data;
2713
2714         dw->second_set = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget));
2715
2716         if (dw->second_set)
2717                 {
2718                 dupe_second_update_status(dw);
2719                 gtk_table_set_col_spacings(GTK_TABLE(dw->table), PREF_PAD_GAP);
2720                 gtk_widget_show(dw->second_vbox);
2721                 }
2722         else
2723                 {
2724                 gtk_table_set_col_spacings(GTK_TABLE(dw->table), 0);
2725                 gtk_widget_hide(dw->second_vbox);
2726                 dupe_second_clear(dw);
2727                 }
2728
2729         dupe_window_recompare(dw);
2730 }
2731
2732 static void dupe_sort_totals_toggle_cb(GtkWidget *widget, gpointer data)
2733 {
2734         DupeWindow *dw = data;
2735
2736         options->sort_totals = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget));
2737         dupe_window_recompare(dw);
2738
2739 }
2740
2741 /*
2742  *-------------------------------------------------------------------
2743  * match type menu
2744  *-------------------------------------------------------------------
2745  */
2746
2747 enum {
2748         DUPE_MENU_COLUMN_NAME = 0,
2749         DUPE_MENU_COLUMN_MASK
2750 };
2751
2752 static void dupe_menu_type_cb(GtkWidget *combo, gpointer data)
2753 {
2754         DupeWindow *dw = data;
2755         GtkTreeModel *store;
2756         GtkTreeIter iter;
2757
2758         store = gtk_combo_box_get_model(GTK_COMBO_BOX(combo));
2759         if (!gtk_combo_box_get_active_iter(GTK_COMBO_BOX(combo), &iter)) return;
2760         gtk_tree_model_get(store, &iter, DUPE_MENU_COLUMN_MASK, &dw->match_mask, -1);
2761
2762         options->duplicates_match = dw->match_mask;
2763
2764         dupe_window_recompare(dw);
2765 }
2766
2767 static void dupe_menu_add_item(GtkListStore *store, const gchar *text, DupeMatchType type, DupeWindow *dw)
2768 {
2769         GtkTreeIter iter;
2770
2771         gtk_list_store_append(store, &iter);
2772         gtk_list_store_set(store, &iter, DUPE_MENU_COLUMN_NAME, text,
2773                                          DUPE_MENU_COLUMN_MASK, type, -1);
2774
2775         if (dw->match_mask == type) gtk_combo_box_set_active_iter(GTK_COMBO_BOX(dw->combo), &iter);
2776 }
2777
2778 static void dupe_menu_setup(DupeWindow *dw)
2779 {
2780         GtkListStore *store;
2781         GtkCellRenderer *renderer;
2782
2783         store = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_INT);
2784         dw->combo = gtk_combo_box_new_with_model(GTK_TREE_MODEL(store));
2785         g_object_unref(store);
2786
2787         renderer = gtk_cell_renderer_text_new();
2788         gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(dw->combo), renderer, TRUE);
2789         gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(dw->combo), renderer,
2790                                        "text", DUPE_MENU_COLUMN_NAME, NULL);
2791
2792         dupe_menu_add_item(store, _("Name"), DUPE_MATCH_NAME, dw);
2793         dupe_menu_add_item(store, _("Name case-insensitive"), DUPE_MATCH_NAME_CI, dw);
2794         dupe_menu_add_item(store, _("Size"), DUPE_MATCH_SIZE, dw);
2795         dupe_menu_add_item(store, _("Date"), DUPE_MATCH_DATE, dw);
2796         dupe_menu_add_item(store, _("Dimensions"), DUPE_MATCH_DIM, dw);
2797         dupe_menu_add_item(store, _("Checksum"), DUPE_MATCH_SUM, dw);
2798         dupe_menu_add_item(store, _("Path"), DUPE_MATCH_PATH, dw);
2799         dupe_menu_add_item(store, _("Similarity (high)"), DUPE_MATCH_SIM_HIGH, dw);
2800         dupe_menu_add_item(store, _("Similarity"), DUPE_MATCH_SIM_MED, dw);
2801         dupe_menu_add_item(store, _("Similarity (low)"), DUPE_MATCH_SIM_LOW, dw);
2802         dupe_menu_add_item(store, _("Similarity (custom)"), DUPE_MATCH_SIM_CUSTOM, dw);
2803
2804         g_signal_connect(G_OBJECT(dw->combo), "changed",
2805                          G_CALLBACK(dupe_menu_type_cb), dw);
2806 }
2807
2808 /*
2809  *-------------------------------------------------------------------
2810  * list view columns
2811  *-------------------------------------------------------------------
2812  */
2813
2814 /* this overrides the low default of a GtkCellRenderer from 100 to CELL_HEIGHT_OVERRIDE, something sane for our purposes */
2815
2816 #define CELL_HEIGHT_OVERRIDE 512
2817
2818 void cell_renderer_height_override(GtkCellRenderer *renderer)
2819 {
2820         GParamSpec *spec;
2821
2822         spec = g_object_class_find_property(G_OBJECT_GET_CLASS(G_OBJECT(renderer)), "height");
2823         if (spec && G_IS_PARAM_SPEC_INT(spec))
2824                 {
2825                 GParamSpecInt *spec_int;
2826
2827                 spec_int = G_PARAM_SPEC_INT(spec);
2828                 if (spec_int->maximum < CELL_HEIGHT_OVERRIDE) spec_int->maximum = CELL_HEIGHT_OVERRIDE;
2829                 }
2830 }
2831
2832 static GdkColor *dupe_listview_color_shifted(GtkWidget *widget)
2833 {
2834         static GdkColor color;
2835         static GtkWidget *done = NULL;
2836
2837         if (done != widget)
2838                 {
2839                 GtkStyle *style;
2840
2841                 style = gtk_widget_get_style(widget);
2842                 memcpy(&color, &style->base[GTK_STATE_NORMAL], sizeof(color));
2843                 shift_color(&color, -1, 0);
2844                 done = widget;
2845                 }
2846
2847         return &color;
2848 }
2849
2850 static void dupe_listview_color_cb(GtkTreeViewColumn *tree_column, GtkCellRenderer *cell,
2851                                    GtkTreeModel *tree_model, GtkTreeIter *iter, gpointer data)
2852 {
2853         DupeWindow *dw = data;
2854         gboolean set;
2855
2856         gtk_tree_model_get(tree_model, iter, DUPE_COLUMN_COLOR, &set, -1);
2857         g_object_set(G_OBJECT(cell),
2858                      "cell-background-gdk", dupe_listview_color_shifted(dw->listview),
2859                      "cell-background-set", set, NULL);
2860 }
2861
2862 static void dupe_listview_add_column(DupeWindow *dw, GtkWidget *listview, gint n, const gchar *title, gboolean image, gboolean right_justify)
2863 {
2864         GtkTreeViewColumn *column;
2865         GtkCellRenderer *renderer;
2866
2867         column = gtk_tree_view_column_new();
2868         gtk_tree_view_column_set_title(column, title);
2869         gtk_tree_view_column_set_min_width(column, 4);
2870
2871         if (n != DUPE_COLUMN_RANK &&
2872             n != DUPE_COLUMN_THUMB)
2873                 {
2874                 gtk_tree_view_column_set_resizable(column, TRUE);
2875                 }
2876
2877         if (!image)
2878                 {
2879                 gtk_tree_view_column_set_sizing(column, GTK_TREE_VIEW_COLUMN_GROW_ONLY);
2880                 renderer = gtk_cell_renderer_text_new();
2881                 if (right_justify)
2882                         {
2883                         g_object_set(G_OBJECT(renderer), "xalign", 1.0, NULL);
2884                         }
2885                 gtk_tree_view_column_pack_start(column, renderer, TRUE);
2886                 gtk_tree_view_column_add_attribute(column, renderer, "text", n);
2887                 }
2888         else
2889                 {
2890                 gtk_tree_view_column_set_sizing(column, GTK_TREE_VIEW_COLUMN_FIXED);
2891                 renderer = gtk_cell_renderer_pixbuf_new();
2892                 cell_renderer_height_override(renderer);
2893                 gtk_tree_view_column_pack_start(column, renderer, TRUE);
2894                 gtk_tree_view_column_add_attribute(column, renderer, "pixbuf", n);
2895                 }
2896
2897         if (listview == dw->listview)
2898                 {
2899                 /* sets background before rendering */
2900                 gtk_tree_view_column_set_cell_data_func(column, renderer, dupe_listview_color_cb, dw, NULL);
2901                 }
2902
2903         gtk_tree_view_append_column(GTK_TREE_VIEW(listview), column);
2904 }
2905
2906 static void dupe_listview_set_height(GtkWidget *listview, gboolean thumb)
2907 {
2908         GtkTreeViewColumn *column;
2909         GtkCellRenderer *cell;
2910         GList *list;
2911
2912         column = gtk_tree_view_get_column(GTK_TREE_VIEW(listview), DUPE_COLUMN_THUMB - 1);
2913         if (!column) return;
2914
2915         gtk_tree_view_column_set_fixed_width(column, (thumb) ? options->thumbnails.max_width : 4);
2916
2917         list = gtk_cell_layout_get_cells(GTK_CELL_LAYOUT(column));
2918         if (!list) return;
2919         cell = list->data;
2920         g_list_free(list);
2921
2922         g_object_set(G_OBJECT(cell), "height", (thumb) ? options->thumbnails.max_height : -1, NULL);
2923         gtk_tree_view_columns_autosize(GTK_TREE_VIEW(listview));
2924 }
2925
2926
2927 /*
2928  *-------------------------------------------------------------------
2929  * misc cb
2930  *-------------------------------------------------------------------
2931  */
2932
2933 static void dupe_window_show_thumb_cb(GtkWidget *widget, gpointer data)
2934 {
2935         DupeWindow *dw = data;
2936
2937         dw->show_thumbs = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget));
2938         options->duplicates_thumbnails = dw->show_thumbs;
2939
2940         if (dw->show_thumbs)
2941                 {
2942                 if (!dw->working) dupe_thumb_step(dw);
2943                 }
2944         else
2945                 {
2946                 GtkTreeModel *store;
2947                 GtkTreeIter iter;
2948                 gboolean valid;
2949
2950                 thumb_loader_free(dw->thumb_loader);
2951                 dw->thumb_loader = NULL;
2952
2953                 store = gtk_tree_view_get_model(GTK_TREE_VIEW(dw->listview));
2954                 valid = gtk_tree_model_get_iter_first(store, &iter);
2955
2956                 while (valid)
2957                         {
2958                         gtk_list_store_set(GTK_LIST_STORE(store), &iter, DUPE_COLUMN_THUMB, NULL, -1);
2959                         valid = gtk_tree_model_iter_next(store, &iter);
2960                         }
2961                 dupe_window_update_progress(dw, NULL, 0.0, FALSE);
2962                 }
2963
2964         dupe_listview_set_height(dw->listview, dw->show_thumbs);
2965 }
2966
2967 static void dupe_window_rotation_invariant_cb(GtkWidget *widget, gpointer data)
2968 {
2969         DupeWindow *dw = data;
2970
2971         options->rot_invariant_sim = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget));
2972         dupe_window_recompare(dw);
2973 }
2974
2975 static void dupe_window_custom_threshold_cb(GtkWidget *widget, gpointer data)
2976 {
2977         DupeWindow *dw = data;
2978         DupeMatchType match_type;
2979         GtkTreeModel *store;
2980         gboolean valid;
2981         GtkTreeIter iter;
2982
2983         options->duplicates_similarity_threshold = gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(widget));
2984         dw->match_mask = DUPE_MATCH_SIM_CUSTOM;
2985
2986         store = gtk_combo_box_get_model(GTK_COMBO_BOX(dw->combo));
2987         valid = gtk_tree_model_get_iter_first(store, &iter);
2988         while (valid)
2989                 {
2990                 gtk_tree_model_get(store, &iter, DUPE_MENU_COLUMN_MASK, &match_type, -1);
2991                 if (match_type == DUPE_MATCH_SIM_CUSTOM)
2992                         {
2993                         break;
2994                         }
2995                 valid = gtk_tree_model_iter_next(store, &iter);
2996                 }
2997
2998         gtk_combo_box_set_active_iter(GTK_COMBO_BOX(dw->combo), &iter);
2999         dupe_window_recompare(dw);
3000 }
3001
3002 static void dupe_popup_menu_pos_cb(GtkMenu *menu, gint *x, gint *y, gboolean *push_in, gpointer data)
3003 {
3004         GtkWidget *view = data;
3005         GtkTreePath *tpath;
3006         gint cx, cy, cw, ch;
3007         gint column;
3008
3009         gtk_tree_view_get_cursor(GTK_TREE_VIEW(view), &tpath, NULL);
3010         if (!tpath) return;
3011
3012         if (gtk_tree_view_get_column(GTK_TREE_VIEW(view), DUPE_COLUMN_NAME - 1) != NULL)
3013                 {
3014                 column = DUPE_COLUMN_NAME - 1;
3015                 }
3016         else
3017                 {
3018                 /* dw->second_listview */
3019                 column = 0;
3020                 }
3021         tree_view_get_cell_clamped(GTK_TREE_VIEW(view), tpath, column, TRUE, &cx, &cy, &cw, &ch);
3022         gtk_tree_path_free(tpath);
3023         cy += ch;
3024         popup_menu_position_clamp(menu, &cx, &cy, 0);
3025         *x = cx;
3026         *y = cy;
3027 }
3028
3029 static gboolean dupe_window_keypress_cb(GtkWidget *widget, GdkEventKey *event, gpointer data)
3030 {
3031         DupeWindow *dw = data;
3032         gboolean stop_signal = FALSE;
3033         gboolean on_second;
3034         GtkWidget *listview;
3035         GtkTreeModel *store;
3036         GtkTreeSelection *selection;
3037         GList *slist;
3038         DupeItem *di = NULL;
3039
3040         on_second = gtk_widget_has_focus(dw->second_listview);
3041
3042         if (on_second)
3043                 {
3044                 listview = dw->second_listview;
3045                 }
3046         else
3047                 {
3048                 listview = dw->listview;
3049                 }
3050
3051         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(listview));
3052         slist = gtk_tree_selection_get_selected_rows(selection, &store);
3053         if (slist)
3054                 {
3055                 GtkTreePath *tpath;
3056                 GtkTreeIter iter;
3057                 GList *last;
3058
3059                 last = g_list_last(slist);
3060                 tpath = last->data;
3061
3062                 /* last is newest selected file */
3063                 gtk_tree_model_get_iter(store, &iter, tpath);
3064                 gtk_tree_model_get(store, &iter, DUPE_COLUMN_POINTER, &di, -1);
3065                 }
3066         g_list_foreach(slist, (GFunc)gtk_tree_path_free, NULL);
3067         g_list_free(slist);
3068
3069         if (event->state & GDK_CONTROL_MASK)
3070                 {
3071                 if (!on_second)
3072                         {
3073                         stop_signal = TRUE;
3074                         switch (event->keyval)
3075                                 {
3076                                 case '1':
3077                                 case '2':
3078                                 case '3':
3079                                 case '4':
3080                                 case '5':
3081                                 case '6':
3082                                 case '7':
3083                                 case '8':
3084                                 case '9':
3085                                 case '0':
3086                                         break;
3087                                 case 'C': case 'c':
3088                                         file_util_copy(NULL, dupe_listview_get_selection(dw, listview),
3089                                                        NULL, dw->window);
3090                                         break;
3091                                 case 'M': case 'm':
3092                                         file_util_move(NULL, dupe_listview_get_selection(dw, listview),
3093                                                        NULL, dw->window);
3094                                         break;
3095                                 case 'R': case 'r':
3096                                         file_util_rename(NULL, dupe_listview_get_selection(dw, listview), dw->window);
3097                                         break;
3098                                 case 'D': case 'd':
3099                                         options->file_ops.safe_delete_enable = TRUE;
3100                                         file_util_delete(NULL, dupe_listview_get_selection(dw, listview), dw->window);
3101                                         break;
3102                                 default:
3103                                         stop_signal = FALSE;
3104                                         break;
3105                                 }
3106                         }
3107
3108                 if (!stop_signal)
3109                         {
3110                         stop_signal = TRUE;
3111                         switch (event->keyval)
3112                                 {
3113                                 case 'A': case 'a':
3114                                         if (event->state & GDK_SHIFT_MASK)
3115                                                 {
3116                                                 gtk_tree_selection_unselect_all(selection);
3117                                                 }
3118                                         else
3119                                                 {
3120                                                 gtk_tree_selection_select_all(selection);
3121                                                 }
3122                                         break;
3123                                 case GDK_KEY_Delete: case GDK_KEY_KP_Delete:
3124                                         if (on_second)
3125                                                 {
3126                                                 dupe_second_clear(dw);
3127                                                 dupe_window_recompare(dw);
3128                                                 }
3129                                         else
3130                                                 {
3131                                                 dupe_window_clear(dw);
3132                                                 }
3133                                         break;
3134                                 case 'L': case 'l':
3135                                         dupe_window_append_file_list(dw, FALSE);
3136                                         break;
3137                                 case 'T': case 't':
3138                                         gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(dw->button_thumbs),
3139                                                 !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(dw->button_thumbs)));
3140                                         break;
3141                                 case 'W': case 'w':
3142                                         dupe_window_close(dw);
3143                                         break;
3144                                 default:
3145                                         stop_signal = FALSE;
3146                                         break;
3147                                 }
3148                         }
3149                 }
3150         else
3151                 {
3152                 stop_signal = TRUE;
3153                 switch (event->keyval)
3154                         {
3155                         case GDK_KEY_Return: case GDK_KEY_KP_Enter:
3156                                 dupe_menu_view(dw, di, listview, FALSE);
3157                                 break;
3158                         case 'V': case 'v':
3159                                 dupe_menu_view(dw, di, listview, TRUE);
3160                                 break;
3161                         case GDK_KEY_Delete: case GDK_KEY_KP_Delete:
3162                                 dupe_window_remove_selection(dw, listview);
3163                                 break;
3164                         case 'C': case 'c':
3165                                 if (!on_second)
3166                                         {
3167                                         dupe_window_collection_from_selection(dw);
3168                                         }
3169                                 break;
3170                         case '1':
3171                                 options->duplicates_select_type = DUPE_SELECT_GROUP1;
3172                                 dupe_listview_select_dupes(dw, DUPE_SELECT_GROUP1);
3173                                 break;
3174                         case '2':
3175                                 options->duplicates_select_type = DUPE_SELECT_GROUP2;
3176                                 dupe_listview_select_dupes(dw, DUPE_SELECT_GROUP2);
3177                                 break;
3178                         case GDK_KEY_Menu:
3179                         case GDK_KEY_F10:
3180                                 if (!on_second)
3181                                         {
3182                                         GtkWidget *menu;
3183
3184                                         menu = dupe_menu_popup_main(dw, di);
3185                                         gtk_menu_popup(GTK_MENU(menu), NULL, NULL,
3186                                                        dupe_popup_menu_pos_cb, listview, 0, GDK_CURRENT_TIME);
3187                                         }
3188                                 else
3189                                         {
3190                                         GtkWidget *menu;
3191
3192                                         menu = dupe_menu_popup_second(dw, di);
3193                                         gtk_menu_popup(GTK_MENU(menu), NULL, NULL,
3194                                                        dupe_popup_menu_pos_cb, listview, 0, GDK_CURRENT_TIME);
3195                                         }
3196                                 break;
3197                         default:
3198                                 stop_signal = FALSE;
3199                                 break;
3200                         }
3201                 }
3202         if (!stop_signal && is_help_key(event))
3203                 {
3204                 help_window_show("GuideImageSearchFindingDuplicates.html");
3205                 stop_signal = TRUE;
3206                 }
3207
3208         return stop_signal;
3209 }
3210
3211
3212 void dupe_window_clear(DupeWindow *dw)
3213 {
3214         GtkListStore *store;
3215
3216         dupe_check_stop(dw);
3217
3218         store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->listview)));
3219         gtk_list_store_clear(store);
3220         gtk_tree_view_columns_autosize(GTK_TREE_VIEW(dw->listview));
3221
3222         g_list_free(dw->dupes);
3223         dw->dupes = NULL;
3224
3225         dupe_list_free(dw->list);
3226         dw->list = NULL;
3227
3228         dupe_match_reset_list(dw->second_list);
3229
3230         dupe_window_update_count(dw, FALSE);
3231         dupe_window_update_progress(dw, NULL, 0.0, FALSE);
3232 }
3233
3234 void dupe_window_close(DupeWindow *dw)
3235 {
3236         dupe_check_stop(dw);
3237
3238         dupe_window_list = g_list_remove(dupe_window_list, dw);
3239         gtk_widget_destroy(dw->window);
3240
3241         g_list_free(dw->dupes);
3242         dupe_list_free(dw->list);
3243
3244         dupe_list_free(dw->second_list);
3245
3246         file_data_unregister_notify_func(dupe_notify_cb, dw);
3247
3248         g_free(dw);
3249 }
3250
3251 static gint dupe_window_delete(GtkWidget *widget, GdkEvent *event, gpointer data)
3252 {
3253         DupeWindow *dw = data;
3254         dupe_window_close(dw);
3255
3256         return TRUE;
3257 }
3258
3259 /* collection and files can be NULL */
3260 DupeWindow *dupe_window_new()
3261 {
3262         DupeWindow *dw;
3263         GtkWidget *vbox;
3264         GtkWidget *scrolled;
3265         GtkWidget *frame;
3266         GtkWidget *status_box;
3267         GtkWidget *label;
3268         GtkWidget *button;
3269         GtkListStore *store;
3270         GtkTreeSelection *selection;
3271         GdkGeometry geometry;
3272
3273         dw = g_new0(DupeWindow, 1);
3274
3275         dw->match_mask = DUPE_MATCH_NAME;
3276         if (options->duplicates_match == DUPE_MATCH_NAME) dw->match_mask = DUPE_MATCH_NAME;
3277         if (options->duplicates_match == DUPE_MATCH_SIZE) dw->match_mask = DUPE_MATCH_SIZE;
3278         if (options->duplicates_match == DUPE_MATCH_DATE) dw->match_mask = DUPE_MATCH_DATE;
3279         if (options->duplicates_match == DUPE_MATCH_DIM) dw->match_mask = DUPE_MATCH_DIM;
3280         if (options->duplicates_match == DUPE_MATCH_SUM) dw->match_mask = DUPE_MATCH_SUM;
3281         if (options->duplicates_match == DUPE_MATCH_PATH) dw->match_mask = DUPE_MATCH_PATH;
3282         if (options->duplicates_match == DUPE_MATCH_SIM_HIGH) dw->match_mask = DUPE_MATCH_SIM_HIGH;
3283         if (options->duplicates_match == DUPE_MATCH_SIM_MED) dw->match_mask = DUPE_MATCH_SIM_MED;
3284         if (options->duplicates_match == DUPE_MATCH_SIM_LOW) dw->match_mask = DUPE_MATCH_SIM_LOW;
3285         if (options->duplicates_match == DUPE_MATCH_SIM_CUSTOM) dw->match_mask = DUPE_MATCH_SIM_CUSTOM;
3286         if (options->duplicates_match == DUPE_MATCH_NAME_CI) dw->match_mask = DUPE_MATCH_NAME_CI;
3287
3288         dw->window = window_new(GTK_WINDOW_TOPLEVEL, "dupe", NULL, NULL, _("Find duplicates"));
3289         DEBUG_NAME(dw->window);
3290
3291         geometry.min_width = DEFAULT_MINIMAL_WINDOW_SIZE;
3292         geometry.min_height = DEFAULT_MINIMAL_WINDOW_SIZE;
3293         geometry.base_width = DUPE_DEF_WIDTH;
3294         geometry.base_height = DUPE_DEF_HEIGHT;
3295         gtk_window_set_geometry_hints(GTK_WINDOW(dw->window), NULL, &geometry,
3296                                       GDK_HINT_MIN_SIZE | GDK_HINT_BASE_SIZE);
3297
3298         gtk_window_set_default_size(GTK_WINDOW(dw->window), DUPE_DEF_WIDTH, DUPE_DEF_HEIGHT);
3299
3300         gtk_window_set_resizable(GTK_WINDOW(dw->window), TRUE);
3301         gtk_container_set_border_width(GTK_CONTAINER(dw->window), 0);
3302
3303         g_signal_connect(G_OBJECT(dw->window), "delete_event",
3304                          G_CALLBACK(dupe_window_delete), dw);
3305         g_signal_connect(G_OBJECT(dw->window), "key_press_event",
3306                          G_CALLBACK(dupe_window_keypress_cb), dw);
3307
3308         vbox = gtk_vbox_new(FALSE, 0);
3309         gtk_container_add(GTK_CONTAINER(dw->window), vbox);
3310         gtk_widget_show(vbox);
3311
3312         dw->table = gtk_table_new(1, 3, FALSE);
3313         gtk_box_pack_start(GTK_BOX(vbox), dw->table, TRUE, TRUE, 0);
3314         gtk_widget_show(dw->table);
3315
3316         scrolled = gtk_scrolled_window_new(NULL, NULL);
3317         gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled), GTK_SHADOW_IN);
3318         gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
3319         gtk_table_attach_defaults(GTK_TABLE(dw->table), scrolled, 0, 2, 0, 1);
3320         gtk_widget_show(scrolled);
3321
3322         store = gtk_list_store_new(9, G_TYPE_POINTER, G_TYPE_STRING, GDK_TYPE_PIXBUF,
3323                                    G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING,
3324                                    G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN);
3325         dw->listview = gtk_tree_view_new_with_model(GTK_TREE_MODEL(store));
3326         g_object_unref(store);
3327
3328         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(dw->listview));
3329         gtk_tree_selection_set_mode(GTK_TREE_SELECTION(selection), GTK_SELECTION_MULTIPLE);
3330         gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(dw->listview), TRUE);
3331         gtk_tree_view_set_enable_search(GTK_TREE_VIEW(dw->listview), FALSE);
3332
3333         dupe_listview_add_column(dw, dw->listview, DUPE_COLUMN_RANK, "", FALSE, TRUE);
3334         dupe_listview_add_column(dw, dw->listview, DUPE_COLUMN_THUMB, "", TRUE, FALSE);
3335         dupe_listview_add_column(dw, dw->listview, DUPE_COLUMN_NAME, _("Name"), FALSE, FALSE);
3336         dupe_listview_add_column(dw, dw->listview, DUPE_COLUMN_SIZE, _("Size"), FALSE, TRUE);
3337         dupe_listview_add_column(dw, dw->listview, DUPE_COLUMN_DATE, _("Date"), FALSE, TRUE);
3338         dupe_listview_add_column(dw, dw->listview, DUPE_COLUMN_DIMENSIONS, _("Dimensions"), FALSE, FALSE);
3339         dupe_listview_add_column(dw, dw->listview, DUPE_COLUMN_PATH, _("Path"), FALSE, FALSE);
3340
3341         gtk_container_add(GTK_CONTAINER(scrolled), dw->listview);
3342         gtk_widget_show(dw->listview);
3343
3344         dw->second_vbox = gtk_vbox_new(FALSE, 0);
3345         gtk_table_attach_defaults(GTK_TABLE(dw->table), dw->second_vbox, 2, 3, 0, 1);
3346         if (dw->second_set)
3347                 {
3348                 gtk_table_set_col_spacings(GTK_TABLE(dw->table), PREF_PAD_GAP);
3349                 gtk_widget_show(dw->second_vbox);
3350                 }
3351         else
3352                 {
3353                 gtk_table_set_col_spacings(GTK_TABLE(dw->table), 0);
3354                 }
3355
3356         scrolled = gtk_scrolled_window_new(NULL, NULL);
3357         gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled), GTK_SHADOW_IN);
3358         gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
3359         gtk_box_pack_start(GTK_BOX(dw->second_vbox), scrolled, TRUE, TRUE, 0);
3360         gtk_widget_show(scrolled);
3361
3362         store = gtk_list_store_new(2, G_TYPE_POINTER, G_TYPE_STRING);
3363         dw->second_listview = gtk_tree_view_new_with_model(GTK_TREE_MODEL(store));
3364
3365         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(dw->second_listview));
3366         gtk_tree_selection_set_mode(GTK_TREE_SELECTION(selection), GTK_SELECTION_MULTIPLE);
3367
3368         gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(dw->second_listview), TRUE);
3369         gtk_tree_view_set_enable_search(GTK_TREE_VIEW(dw->second_listview), FALSE);
3370
3371         dupe_listview_add_column(dw, dw->second_listview, 1, _("Compare to:"), FALSE, FALSE);
3372
3373         gtk_container_add(GTK_CONTAINER(scrolled), dw->second_listview);
3374         gtk_widget_show(dw->second_listview);
3375
3376         dw->second_status_label = gtk_label_new("");
3377         gtk_box_pack_start(GTK_BOX(dw->second_vbox), dw->second_status_label, FALSE, FALSE, 0);
3378         gtk_widget_show(dw->second_status_label);
3379
3380         pref_line(dw->second_vbox, GTK_ORIENTATION_HORIZONTAL);
3381
3382         status_box = pref_box_new(vbox, FALSE, GTK_ORIENTATION_HORIZONTAL, 0);
3383
3384         label = gtk_label_new(_("Compare by:"));
3385         gtk_box_pack_start(GTK_BOX(status_box), label, FALSE, FALSE, PREF_PAD_SPACE);
3386         gtk_widget_show(label);
3387
3388         dupe_menu_setup(dw);
3389         gtk_box_pack_start(GTK_BOX(status_box), dw->combo, FALSE, FALSE, 0);
3390         gtk_widget_show(dw->combo);
3391
3392         dw->button_thumbs = gtk_check_button_new_with_label(_("Thumbnails"));
3393         dw->show_thumbs = options->duplicates_thumbnails;
3394         gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(dw->button_thumbs), dw->show_thumbs);
3395         g_signal_connect(G_OBJECT(dw->button_thumbs), "toggled",
3396                          G_CALLBACK(dupe_window_show_thumb_cb), dw);
3397         gtk_box_pack_start(GTK_BOX(status_box), dw->button_thumbs, FALSE, FALSE, PREF_PAD_SPACE);
3398         gtk_widget_show(dw->button_thumbs);
3399
3400         dw->button_rotation_invariant = gtk_check_button_new_with_label(_("Ignore Rotation"));
3401         gtk_widget_set_tooltip_text(GTK_WIDGET(dw->button_rotation_invariant), "Ignore image orientation");
3402         gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(dw->button_rotation_invariant), options->rot_invariant_sim);
3403         g_signal_connect(G_OBJECT(dw->button_rotation_invariant), "toggled",
3404                          G_CALLBACK(dupe_window_rotation_invariant_cb), dw);
3405         gtk_box_pack_start(GTK_BOX(status_box), dw->button_rotation_invariant, FALSE, FALSE, PREF_PAD_SPACE);
3406         gtk_widget_show(dw->button_rotation_invariant);
3407
3408         button = gtk_check_button_new_with_label(_("Compare two file sets"));
3409         gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(button), dw->second_set);
3410         g_signal_connect(G_OBJECT(button), "toggled",
3411                          G_CALLBACK(dupe_second_set_toggle_cb), dw);
3412         gtk_box_pack_end(GTK_BOX(status_box), button, FALSE, FALSE, PREF_PAD_SPACE);
3413         gtk_widget_show(button);
3414
3415         status_box = gtk_hbox_new(FALSE, 0);
3416         gtk_box_pack_start(GTK_BOX(vbox), status_box, FALSE, FALSE, 0);
3417         gtk_widget_show(status_box);
3418
3419         frame = gtk_frame_new(NULL);
3420         DEBUG_NAME(frame);
3421         gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_IN);
3422         gtk_box_pack_start(GTK_BOX(status_box), frame, TRUE, TRUE, 0);
3423         gtk_widget_show(frame);
3424
3425         dw->status_label = gtk_label_new("");
3426         gtk_container_add(GTK_CONTAINER(frame), dw->status_label);
3427         gtk_widget_show(dw->status_label);
3428
3429         button = gtk_check_button_new_with_label(_("Sort"));
3430         gtk_widget_set_tooltip_text(GTK_WIDGET(button), "Sort by group totals");
3431         gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(button), options->sort_totals);
3432         g_signal_connect(G_OBJECT(button), "toggled",
3433                          G_CALLBACK(dupe_sort_totals_toggle_cb), dw);
3434         gtk_box_pack_start(GTK_BOX(status_box), button, FALSE, FALSE, PREF_PAD_SPACE);
3435         gtk_widget_show(button);
3436
3437         label = gtk_label_new(_("Custom Threshold"));
3438         gtk_box_pack_start(GTK_BOX(status_box), label, FALSE, FALSE, PREF_PAD_SPACE);
3439         gtk_widget_show(label);
3440         dw->custom_threshold = gtk_spin_button_new_with_range(1, 100, 1);
3441         gtk_widget_set_tooltip_text(GTK_WIDGET(dw->custom_threshold), "Custom similarity threshold");
3442         gtk_spin_button_set_value(GTK_SPIN_BUTTON(dw->custom_threshold), options->duplicates_similarity_threshold);
3443         g_signal_connect(G_OBJECT(dw->custom_threshold), "value_changed",
3444                                                                                                         G_CALLBACK(dupe_window_custom_threshold_cb), dw);
3445         gtk_box_pack_start(GTK_BOX(status_box), dw->custom_threshold, FALSE, FALSE, PREF_PAD_SPACE);
3446         gtk_widget_show(dw->custom_threshold);
3447
3448         dw->extra_label = gtk_progress_bar_new();
3449         gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(dw->extra_label), 0.0);
3450 #if GTK_CHECK_VERSION(3,0,0)
3451         gtk_progress_bar_set_text(GTK_PROGRESS_BAR(dw->extra_label), "");
3452         gtk_progress_bar_set_show_text(GTK_PROGRESS_BAR(dw->extra_label), TRUE);
3453 #endif
3454         gtk_box_pack_end(GTK_BOX(status_box), dw->extra_label, FALSE, FALSE, 0);
3455         gtk_widget_show(dw->extra_label);
3456
3457         dupe_dnd_init(dw);
3458
3459         /* order is important here, dnd_init should be seeing mouse
3460          * presses before we possibly handle (and stop) the signal
3461          */
3462         g_signal_connect(G_OBJECT(dw->listview), "button_press_event",
3463                          G_CALLBACK(dupe_listview_press_cb), dw);
3464         g_signal_connect(G_OBJECT(dw->listview), "button_release_event",
3465                          G_CALLBACK(dupe_listview_release_cb), dw);
3466         g_signal_connect(G_OBJECT(dw->second_listview), "button_press_event",
3467                          G_CALLBACK(dupe_listview_press_cb), dw);
3468         g_signal_connect(G_OBJECT(dw->second_listview), "button_release_event",
3469                          G_CALLBACK(dupe_listview_release_cb), dw);
3470
3471         gtk_widget_show(dw->window);
3472
3473         dupe_window_update_count(dw, TRUE);
3474         dupe_window_update_progress(dw, NULL, 0.0, FALSE);
3475
3476         dupe_window_list = g_list_append(dupe_window_list, dw);
3477
3478         file_data_register_notify_func(dupe_notify_cb, dw, NOTIFY_PRIORITY_MEDIUM);
3479
3480         return dw;
3481 }
3482
3483 /*
3484  *-------------------------------------------------------------------
3485  * dnd confirm dir
3486  *-------------------------------------------------------------------
3487  */
3488
3489 typedef struct {
3490         DupeWindow *dw;
3491         GList *list;
3492 } CDupeConfirmD;
3493
3494 static void confirm_dir_list_cancel(GtkWidget *widget, gpointer data)
3495 {
3496         /* do nothing */
3497 }
3498
3499 static void confirm_dir_list_add(GtkWidget *widget, gpointer data)
3500 {
3501         CDupeConfirmD *d = data;
3502         GList *work;
3503
3504         dupe_window_add_files(d->dw, d->list, FALSE);
3505
3506         work = d->list;
3507         while (work)
3508                 {
3509                 FileData *fd = work->data;
3510                 work = work->next;
3511                 if (isdir(fd->path))
3512                         {
3513                         GList *list;
3514
3515                         filelist_read(fd, &list, NULL);
3516                         list = filelist_filter(list, FALSE);
3517                         if (list)
3518                                 {
3519                                 dupe_window_add_files(d->dw, list, FALSE);
3520                                 filelist_free(list);
3521                                 }
3522                         }
3523                 }
3524 }
3525
3526 static void confirm_dir_list_recurse(GtkWidget *widget, gpointer data)
3527 {
3528         CDupeConfirmD *d = data;
3529         dupe_window_add_files(d->dw, d->list, TRUE);
3530 }
3531
3532 static void confirm_dir_list_skip(GtkWidget *widget, gpointer data)
3533 {
3534         CDupeConfirmD *d = data;
3535         dupe_window_add_files(d->dw, d->list, FALSE);
3536 }
3537
3538 static void confirm_dir_list_destroy(GtkWidget *widget, gpointer data)
3539 {
3540         CDupeConfirmD *d = data;
3541         filelist_free(d->list);
3542         g_free(d);
3543 }
3544
3545 static GtkWidget *dupe_confirm_dir_list(DupeWindow *dw, GList *list)
3546 {
3547         GtkWidget *menu;
3548         CDupeConfirmD *d;
3549
3550         d = g_new0(CDupeConfirmD, 1);
3551         d->dw = dw;
3552         d->list = list;
3553
3554         menu = popup_menu_short_lived();
3555         g_signal_connect(G_OBJECT(menu), "destroy",
3556                          G_CALLBACK(confirm_dir_list_destroy), d);
3557
3558         menu_item_add_stock(menu, _("Dropped list includes folders."), GTK_STOCK_DND_MULTIPLE, NULL, NULL);
3559         menu_item_add_divider(menu);
3560         menu_item_add_stock(menu, _("_Add contents"), GTK_STOCK_OK, G_CALLBACK(confirm_dir_list_add), d);
3561         menu_item_add_stock(menu, _("Add contents _recursive"), GTK_STOCK_ADD, G_CALLBACK(confirm_dir_list_recurse), d);
3562         menu_item_add_stock(menu, _("_Skip folders"), GTK_STOCK_REMOVE, G_CALLBACK(confirm_dir_list_skip), d);
3563         menu_item_add_divider(menu);
3564         menu_item_add_stock(menu, _("Cancel"), GTK_STOCK_CANCEL, G_CALLBACK(confirm_dir_list_cancel), d);
3565
3566         return menu;
3567 }
3568
3569 /*
3570  *-------------------------------------------------------------------
3571  * dnd
3572  *-------------------------------------------------------------------
3573  */
3574
3575 static GtkTargetEntry dupe_drag_types[] = {
3576         { "text/uri-list", 0, TARGET_URI_LIST },
3577         { "text/plain", 0, TARGET_TEXT_PLAIN }
3578 };
3579 static gint n_dupe_drag_types = 2;
3580
3581 static GtkTargetEntry dupe_drop_types[] = {
3582         { TARGET_APP_COLLECTION_MEMBER_STRING, 0, TARGET_APP_COLLECTION_MEMBER },
3583         { "text/uri-list", 0, TARGET_URI_LIST }
3584 };
3585 static gint n_dupe_drop_types = 2;
3586
3587 static void dupe_dnd_data_set(GtkWidget *widget, GdkDragContext *context,
3588                               GtkSelectionData *selection_data, guint info,
3589                               guint time, gpointer data)
3590 {
3591         DupeWindow *dw = data;
3592         GList *list;
3593
3594         switch (info)
3595                 {
3596                 case TARGET_URI_LIST:
3597                 case TARGET_TEXT_PLAIN:
3598                         list = dupe_listview_get_selection(dw, widget);
3599                         if (!list) return;
3600                         uri_selection_data_set_uris_from_filelist(selection_data, list);
3601                         filelist_free(list);
3602                         break;
3603                 default:
3604                         break;
3605                 }
3606 }
3607
3608 static void dupe_dnd_data_get(GtkWidget *widget, GdkDragContext *context,
3609                               gint x, gint y,
3610                               GtkSelectionData *selection_data, guint info,
3611                               guint time, gpointer data)
3612 {
3613         DupeWindow *dw = data;
3614         GtkWidget *source;
3615         GList *list = NULL;
3616         GList *work;
3617
3618         source = gtk_drag_get_source_widget(context);
3619         if (source == dw->listview || source == dw->second_listview) return;
3620
3621         dw->second_drop = (dw->second_set && widget == dw->second_listview);
3622
3623         switch (info)
3624                 {
3625                 case TARGET_APP_COLLECTION_MEMBER:
3626                         collection_from_dnd_data((gchar *)gtk_selection_data_get_data(selection_data), &list, NULL);
3627                         break;
3628                 case TARGET_URI_LIST:
3629                         list = uri_filelist_from_gtk_selection_data(selection_data);
3630                         work = list;
3631                         while (work)
3632                                 {
3633                                 FileData *fd = work->data;
3634                                 if (isdir(fd->path))
3635                                         {
3636                                         GtkWidget *menu;
3637                                         menu = dupe_confirm_dir_list(dw, list);
3638                                         gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL, 0, time);
3639                                         return;
3640                                         }
3641                                 work = work->next;
3642                                 }
3643                         break;
3644                 default:
3645                         list = NULL;
3646                         break;
3647                 }
3648
3649         if (list)
3650                 {
3651                 dupe_window_add_files(dw, list, FALSE);
3652                 filelist_free(list);
3653                 }
3654 }
3655
3656 static void dupe_dest_set(GtkWidget *widget, gboolean enable)
3657 {
3658         if (enable)
3659                 {
3660                 gtk_drag_dest_set(widget,
3661                         GTK_DEST_DEFAULT_MOTION | GTK_DEST_DEFAULT_HIGHLIGHT | GTK_DEST_DEFAULT_DROP,
3662                         dupe_drop_types, n_dupe_drop_types,
3663                         GDK_ACTION_COPY | GDK_ACTION_MOVE | GDK_ACTION_ASK);
3664
3665                 }
3666         else
3667                 {
3668                 gtk_drag_dest_unset(widget);
3669                 }
3670 }
3671
3672 static void dupe_dnd_begin(GtkWidget *widget, GdkDragContext *context, gpointer data)
3673 {
3674         DupeWindow *dw = data;
3675         dupe_dest_set(dw->listview, FALSE);
3676         dupe_dest_set(dw->second_listview, FALSE);
3677
3678         if (dw->click_item && !dupe_listview_item_is_selected(dw, dw->click_item, widget))
3679                 {
3680                 GtkListStore *store;
3681                 GtkTreeIter iter;
3682
3683                 store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(widget)));
3684                 if (dupe_listview_find_item(store, dw->click_item, &iter) >= 0)
3685                         {
3686                         GtkTreeSelection *selection;
3687                         GtkTreePath *tpath;
3688
3689                         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(widget));
3690                         gtk_tree_selection_unselect_all(selection);
3691                         gtk_tree_selection_select_iter(selection, &iter);
3692
3693                         tpath = gtk_tree_model_get_path(GTK_TREE_MODEL(store), &iter);
3694                         gtk_tree_view_set_cursor(GTK_TREE_VIEW(widget), tpath, NULL, FALSE);
3695                         gtk_tree_path_free(tpath);
3696                         }
3697                 }
3698
3699         if (dw->show_thumbs &&
3700             widget == dw->listview &&
3701             dw->click_item && dw->click_item->pixbuf)
3702                 {
3703                 GtkTreeSelection *selection;
3704                 gint items;
3705
3706                 selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(widget));
3707                 items = gtk_tree_selection_count_selected_rows(selection);
3708                 dnd_set_drag_icon(widget, context, dw->click_item->pixbuf, items);
3709                 }
3710 }
3711
3712 static void dupe_dnd_end(GtkWidget *widget, GdkDragContext *context, gpointer data)
3713 {
3714         DupeWindow *dw = data;
3715         dupe_dest_set(dw->listview, TRUE);
3716         dupe_dest_set(dw->second_listview, TRUE);
3717 }
3718
3719 static void dupe_dnd_init(DupeWindow *dw)
3720 {
3721         gtk_drag_source_set(dw->listview, GDK_BUTTON1_MASK | GDK_BUTTON2_MASK,
3722                             dupe_drag_types, n_dupe_drag_types,
3723                             GDK_ACTION_COPY | GDK_ACTION_MOVE | GDK_ACTION_LINK);
3724         g_signal_connect(G_OBJECT(dw->listview), "drag_data_get",
3725                          G_CALLBACK(dupe_dnd_data_set), dw);
3726         g_signal_connect(G_OBJECT(dw->listview), "drag_begin",
3727                          G_CALLBACK(dupe_dnd_begin), dw);
3728         g_signal_connect(G_OBJECT(dw->listview), "drag_end",
3729                          G_CALLBACK(dupe_dnd_end), dw);
3730
3731         dupe_dest_set(dw->listview, TRUE);
3732         g_signal_connect(G_OBJECT(dw->listview), "drag_data_received",
3733                          G_CALLBACK(dupe_dnd_data_get), dw);
3734
3735         gtk_drag_source_set(dw->second_listview, GDK_BUTTON1_MASK | GDK_BUTTON2_MASK,
3736                             dupe_drag_types, n_dupe_drag_types,
3737                             GDK_ACTION_COPY | GDK_ACTION_MOVE | GDK_ACTION_LINK);
3738         g_signal_connect(G_OBJECT(dw->second_listview), "drag_data_get",
3739                          G_CALLBACK(dupe_dnd_data_set), dw);
3740         g_signal_connect(G_OBJECT(dw->second_listview), "drag_begin",
3741                          G_CALLBACK(dupe_dnd_begin), dw);
3742         g_signal_connect(G_OBJECT(dw->second_listview), "drag_end",
3743                          G_CALLBACK(dupe_dnd_end), dw);
3744
3745         dupe_dest_set(dw->second_listview, TRUE);
3746         g_signal_connect(G_OBJECT(dw->second_listview), "drag_data_received",
3747                          G_CALLBACK(dupe_dnd_data_get), dw);
3748 }
3749
3750 /*
3751  *-------------------------------------------------------------------
3752  * maintenance (move, delete, etc.)
3753  *-------------------------------------------------------------------
3754  */
3755
3756 static void dupe_notify_cb(FileData *fd, NotifyType type, gpointer data)
3757 {
3758         DupeWindow *dw = data;
3759
3760         if (!(type & NOTIFY_CHANGE) || !fd->change) return;
3761
3762         DEBUG_1("Notify dupe: %s %04x", fd->path, type);
3763
3764         switch (fd->change->type)
3765                 {
3766                 case FILEDATA_CHANGE_MOVE:
3767                 case FILEDATA_CHANGE_RENAME:
3768                         dupe_item_update_fd(dw, fd);
3769                         break;
3770                 case FILEDATA_CHANGE_COPY:
3771                         break;
3772                 case FILEDATA_CHANGE_DELETE:
3773                         while (dupe_item_remove_by_path(dw, fd->path));
3774                         break;
3775                 case FILEDATA_CHANGE_UNSPECIFIED:
3776                 case FILEDATA_CHANGE_WRITE_METADATA:
3777                         break;
3778                 }
3779
3780 }
3781
3782 /*
3783  *-------------------------------------------------------------------
3784  * Export duplicates data
3785  *-------------------------------------------------------------------
3786  */
3787
3788  typedef enum {
3789         EXPORT_CSV = 0,
3790         EXPORT_TSV
3791 } SeparatorType;
3792
3793 typedef struct _ExportDupesData ExportDupesData;
3794 struct _ExportDupesData
3795 {
3796         FileDialog *dialog;
3797         SeparatorType separator;
3798         DupeWindow *dupewindow;
3799 };
3800
3801 static void export_duplicates_close(ExportDupesData *edd)
3802 {
3803         if (edd->dialog) file_dialog_close(edd->dialog);
3804         edd->dialog = NULL;
3805 }
3806
3807 static void export_duplicates_data_cancel_cb(FileDialog *fdlg, gpointer data)
3808 {
3809         ExportDupesData *edd = data;
3810
3811         export_duplicates_close(edd);
3812 }
3813
3814 static void export_duplicates_data_save_cb(FileDialog *fdlg, gpointer data)
3815 {
3816         ExportDupesData *edd = data;
3817         GError *error = NULL;
3818         GtkTreeModel *store;
3819         GtkTreeIter iter;
3820         DupeItem *di;
3821         GFileOutputStream *gfstream;
3822         GFile *out_file;
3823         GString *output_string;
3824         gchar *sep;
3825         gchar* rank;
3826         GList *work;
3827         GtkTreeSelection *selection;
3828         GList *slist;
3829         gchar *thumb_cache;
3830         gchar **rank_split;
3831         GtkTreePath *tpath;
3832         gboolean color_old = FALSE;
3833         gboolean color_new = FALSE;
3834         gint match_count;
3835         gchar *name;
3836
3837         history_list_add_to_key("export_duplicates", fdlg->dest_path, -1);
3838
3839         out_file = g_file_new_for_path(fdlg->dest_path);
3840
3841         gfstream = g_file_replace(out_file, NULL, TRUE, G_FILE_CREATE_NONE, NULL, &error);
3842         if (error)
3843                 {
3844                 log_printf(_("Error creating Export duplicates data file: Error: %s\n"), error->message);
3845                 g_error_free(error);
3846                 return;
3847                 }
3848
3849         sep = g_strdup((edd->separator == EXPORT_CSV) ?  "," : "\t");
3850         output_string = g_string_new(g_strjoin(sep, _("Match"), _("Group"), _("Similarity"), _("Set"), _("Thumbnail"), _("Name"), _("Size"), _("Date"), _("Width"), _("Height"), _("Path\n"), NULL));
3851
3852         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(edd->dupewindow->listview));
3853         slist = gtk_tree_selection_get_selected_rows(selection, &store);
3854         work = slist;
3855
3856         tpath = work->data;
3857         gtk_tree_model_get_iter(store, &iter, tpath);
3858         gtk_tree_model_get(GTK_TREE_MODEL(store), &iter, DUPE_COLUMN_COLOR, &color_new, -1);
3859         color_old = !color_new;
3860         match_count = 0;
3861
3862         while (work)
3863                 {
3864                 tpath = work->data;
3865                 gtk_tree_model_get_iter(store, &iter, tpath);
3866
3867                 gtk_tree_model_get(GTK_TREE_MODEL(store), &iter, DUPE_COLUMN_POINTER, &di, -1);
3868
3869                 gtk_tree_model_get(GTK_TREE_MODEL(store), &iter, DUPE_COLUMN_COLOR, &color_new, -1);
3870                 if (color_new != color_old)
3871                         {
3872                         match_count++;
3873                         }
3874                 color_old = color_new;
3875                 output_string = g_string_append(output_string, g_strdup_printf("%d", match_count));
3876                 output_string = g_string_append(output_string, sep);
3877
3878                 if ((dupe_match_find_parent(edd->dupewindow, di) == di))
3879                         {
3880                         output_string = g_string_append(output_string, "1");
3881                         }
3882                 else
3883                         {
3884                         output_string = g_string_append(output_string, "2");
3885                         }
3886                 output_string = g_string_append(output_string, sep);
3887
3888                 gtk_tree_model_get(GTK_TREE_MODEL(store), &iter, DUPE_COLUMN_RANK, &rank, -1);
3889                 rank_split = g_strsplit_set(rank, " [(", -1);
3890                 if (rank_split[0] == NULL)
3891                         {
3892                         output_string = g_string_append(output_string, "");
3893                         }
3894                 else
3895                         {
3896                         output_string = g_string_append(output_string, g_strdup_printf("%s", rank_split[0]));
3897                         }
3898                 output_string = g_string_append(output_string, sep);
3899                 g_free(rank);
3900                 g_strfreev(rank_split);
3901
3902                 output_string = g_string_append(output_string, g_strdup_printf("%d", (di->second + 1)));
3903                 output_string = g_string_append(output_string, sep);
3904
3905                 thumb_cache = cache_find_location(CACHE_TYPE_THUMB, di->fd->path);
3906                 if (thumb_cache)
3907                         {
3908                         output_string = g_string_append(output_string, thumb_cache);
3909                         g_free(thumb_cache);
3910                         }
3911                 else
3912                         {
3913                         output_string = g_string_append(output_string, "");
3914                         }
3915                 output_string = g_string_append(output_string, sep);
3916
3917                 gtk_tree_model_get(GTK_TREE_MODEL(store), &iter, DUPE_COLUMN_NAME, &name, -1);
3918                 output_string = g_string_append(output_string, name);
3919                 output_string = g_string_append(output_string, sep);
3920                 g_free(name);
3921
3922                 output_string = g_string_append(output_string, g_strdup_printf("%"PRIu64, di->fd->size));
3923                 output_string = g_string_append(output_string, sep);
3924                 output_string = g_string_append(output_string, text_from_time(di->fd->date));
3925                 output_string = g_string_append(output_string, sep);
3926                 output_string = g_string_append(output_string, g_strdup_printf("%d", (di->width ? di->width : 0)));
3927                 output_string = g_string_append(output_string, sep);
3928                 output_string = g_string_append(output_string, g_strdup_printf("%d", (di->height ? di->height : 0)));
3929                 output_string = g_string_append(output_string, sep);
3930                 output_string = g_string_append(output_string, di->fd->path);
3931                 output_string = g_string_append_c(output_string, '\n');
3932
3933                 work = work->next;
3934                 }
3935
3936         g_output_stream_write(G_OUTPUT_STREAM(gfstream), output_string->str, strlen(output_string->str), NULL, &error);
3937
3938         g_free(sep);
3939         g_string_free(output_string, TRUE);
3940         g_object_unref(gfstream);
3941         g_object_unref(out_file);
3942
3943         export_duplicates_close(edd);
3944 }
3945
3946 static void pop_menu_export(GList *selection_list, gpointer dupe_window, gpointer data)
3947 {
3948         const gint index = GPOINTER_TO_INT(data);
3949         DupeWindow *dw = dupe_window;
3950         gchar *title = "Export duplicates data";
3951         gchar *default_path = "/tmp/";
3952         gchar *file_extension;
3953         const gchar *stock_id;
3954         ExportDupesData *edd;
3955         const gchar *previous_path;
3956
3957         edd = g_new0(ExportDupesData, 1);
3958         edd->dialog = file_util_file_dlg(title, "export_duplicates", NULL, export_duplicates_data_cancel_cb, edd);
3959
3960         switch (index)
3961                 {
3962                 case EXPORT_CSV:
3963                         edd->separator = EXPORT_CSV;
3964                         file_extension = g_strdup(".csv");
3965                         break;
3966                 case EXPORT_TSV:
3967                         edd->separator = EXPORT_TSV;
3968                         file_extension = g_strdup(".tsv");
3969                         break;
3970                 default:
3971                         return;
3972                 }
3973
3974         stock_id = GTK_STOCK_SAVE;
3975
3976         generic_dialog_add_message(GENERIC_DIALOG(edd->dialog), NULL, title, NULL, FALSE);
3977         file_dialog_add_button(edd->dialog, stock_id, NULL, export_duplicates_data_save_cb, TRUE);
3978
3979         previous_path = history_list_find_last_path_by_key("export_duplicates");
3980
3981         file_dialog_add_path_widgets(edd->dialog, default_path, previous_path, "export_duplicates", file_extension, _("Export Files"));
3982
3983         edd->dupewindow = dw;
3984
3985         gtk_widget_show(GENERIC_DIALOG(edd->dialog)->dialog);
3986
3987         g_free(file_extension);
3988 }
3989
3990 static void dupe_pop_menu_export_cb(GtkWidget *widget, gpointer data)
3991 {
3992         DupeWindow *dw;
3993         GList *selection_list;
3994
3995         dw = submenu_item_get_data(widget);
3996         selection_list = dupe_listview_get_selection(dw, dw->listview);
3997         pop_menu_export(selection_list, dw, data);
3998
3999         filelist_free(selection_list);
4000 }
4001
4002 static GtkWidget *submenu_add_export(GtkWidget *menu, GtkWidget **menu_item, GCallback func, gpointer data)
4003 {
4004         GtkWidget *item;
4005         GtkWidget *submenu;
4006
4007         item = menu_item_add(menu, _("Export"), NULL, NULL);
4008
4009         submenu = gtk_menu_new();
4010         g_object_set_data(G_OBJECT(submenu), "submenu_data", data);
4011
4012         menu_item_add_stock_sensitive(submenu, _("Export to csv"),
4013                                         GTK_STOCK_INDEX, TRUE, G_CALLBACK(func), GINT_TO_POINTER(0));
4014         menu_item_add_stock_sensitive(submenu, _("Export to tab-delimited"),
4015                                         GTK_STOCK_INDEX, TRUE, G_CALLBACK(func), GINT_TO_POINTER(1));
4016
4017         gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), submenu);
4018         if (menu_item) *menu_item = item;
4019
4020         return submenu;
4021 }
4022
4023 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */