Fix #615: Find Duplicates use multicore multithread all cores threads
[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 #define DUPE_PROGRESS_PULSE_STEP 0.0001
62
63 /** column assignment order (simply change them here)
64  */
65 enum {
66         DUPE_COLUMN_POINTER = 0,
67         DUPE_COLUMN_RANK,
68         DUPE_COLUMN_THUMB,
69         DUPE_COLUMN_NAME,
70         DUPE_COLUMN_SIZE,
71         DUPE_COLUMN_DATE,
72         DUPE_COLUMN_DIMENSIONS,
73         DUPE_COLUMN_PATH,
74         DUPE_COLUMN_COLOR,
75         DUPE_COLUMN_SET,
76         DUPE_COLUMN_COUNT       /**< total columns */
77 };
78
79 typedef enum {
80         DUPE_MATCH = 0,
81         DUPE_NO_MATCH,
82         DUPE_NAME_MATCH
83 } DUPE_CHECK_RESULT;
84
85 typedef struct _DupeQueueItem DupeQueueItem;
86 /** Used for similarity checks. One for each item pushed
87  * onto the thread pool.
88  */
89 struct _DupeQueueItem
90 {
91         DupeItem *needle;
92         DupeWindow *dw;
93         GList *work; /**< pointer into \a dw->list or \a dw->second_list (#DupeItem) */
94         gint index; /**< The order items pushed onto thread pool. Used to sort returned matches */
95 };
96
97 typedef struct _DupeSearchMatch DupeSearchMatch;
98 /** Used for similarity checks thread. One for each pair match found.
99  */
100 struct _DupeSearchMatch
101 {
102         DupeItem *a; /**< \a a / \a b matched pair found */
103         DupeItem *b; /**< \a a / \a b matched pair found */
104         gdouble rank;
105         gint index; /**< The order items pushed onto thread pool. Used to sort returned matches */
106 };
107
108 static DupeMatchType param_match_mask;
109 static GList *dupe_window_list = NULL;  /**< list of open DupeWindow *s */
110
111 /*
112  * Well, after adding the 'compare two sets' option things got a little sloppy in here
113  * because we have to account for two 'modes' everywhere. (be careful).
114  */
115
116 static void dupe_match_unlink(DupeItem *a, DupeItem *b);
117 static DupeItem *dupe_match_find_parent(DupeWindow *dw, DupeItem *child);
118
119 static gint dupe_match(DupeItem *a, DupeItem *b, DupeMatchType mask, gdouble *rank, gint fast);
120
121 static void dupe_thumb_step(DupeWindow *dw);
122 static gint dupe_check_cb(gpointer data);
123
124 static void dupe_second_add(DupeWindow *dw, DupeItem *di);
125 static void dupe_second_remove(DupeWindow *dw, DupeItem *di);
126 static GtkWidget *dupe_menu_popup_second(DupeWindow *dw, DupeItem *di);
127
128 static void dupe_dnd_init(DupeWindow *dw);
129
130 static void dupe_notify_cb(FileData *fd, NotifyType type, gpointer data);
131
132 static GtkWidget *submenu_add_export(GtkWidget *menu, GtkWidget **menu_item, GCallback func, gpointer data);
133 static void dupe_pop_menu_export_cb(GtkWidget *widget, gpointer data);
134
135 static void dupe_init_list_cache(DupeWindow *dw);
136 static void dupe_destroy_list_cache(DupeWindow *dw);
137 static gboolean dupe_insert_in_list_cache(DupeWindow *dw, FileData *fd);
138
139 static void dupe_match_link(DupeItem *a, DupeItem *b, gdouble rank);
140 static gint dupe_match_link_exists(DupeItem *child, DupeItem *parent);
141
142 /**
143  * @brief The function run in threads for similarity checks
144  * @param d1 #DupeQueueItem
145  * @param d2 #DupeWindow
146  * 
147  * Used only for similarity checks.\n
148  * Search \a dqi->list for \a dqi->needle and if a match is
149  * found, create a #DupeSearchMatch and add to \a dw->search_matches list\n
150  * If \a dw->abort is set, just increment \a dw->thread_count
151  */
152 static void dupe_comparison_func(gpointer d1, gpointer d2)
153 {
154         DupeQueueItem *dqi = d1;
155         DupeWindow *dw = d2;
156         DupeSearchMatch *dsm;
157         DupeItem *di;
158         GList *matches = NULL;
159         gdouble rank = 0;
160
161         if (!dw->abort)
162                 {
163                 GList *work = dqi->work;
164                 while (work)
165                         {
166                         di = work->data;
167
168                         /* forward for second set, back for simple compare */
169                         if (dw->second_set)
170                                 {
171                                 work = work->next;
172                                 }
173                         else
174                                 {
175                                 work = work->prev;
176                                 }
177
178                         if (dupe_match(di, dqi->needle, dqi->dw->match_mask, &rank, TRUE))
179                                 {
180                                 dsm = g_new0(DupeSearchMatch, 1);
181                                 dsm->a = di;
182                                 dsm->b = dqi->needle;
183                                 dsm->rank = rank;
184                                 matches = g_list_prepend(matches, dsm);
185                                 dsm->index = dqi->index;
186                                 }
187
188                         if (dw->abort)
189                                 {
190                                 break;
191                                 }
192                         }
193
194                 matches = g_list_reverse(matches);
195                 g_mutex_lock(&dw->search_matches_mutex);
196                 dw->search_matches = g_list_concat(dw->search_matches, matches);
197                 g_mutex_unlock(&dw->search_matches_mutex);
198                 }
199
200         g_mutex_lock(&dw->thread_count_mutex);
201         dw->thread_count++;
202         g_mutex_unlock(&dw->thread_count_mutex);
203         g_free(dqi);
204 }
205
206 /*
207  * ------------------------------------------------------------------
208  * Window updates
209  * ------------------------------------------------------------------
210  */
211
212 /**
213  * @brief Update display of status label
214  * @param dw 
215  * @param count_only 
216  * 
217  * 
218  */
219 static void dupe_window_update_count(DupeWindow *dw, gboolean count_only)
220 {
221         gchar *text;
222
223         if (!dw->list)
224                 {
225                 text = g_strdup(_("Drop files to compare them."));
226                 }
227         else if (count_only)
228                 {
229                 text = g_strdup_printf(_("%d files"), g_list_length(dw->list));
230                 }
231         else
232                 {
233                 text = g_strdup_printf(_("%d matches found in %d files"), g_list_length(dw->dupes), g_list_length(dw->list));
234                 }
235
236         if (dw->second_set)
237                 {
238                 gchar *buf = g_strconcat(text, " ", _("[set 1]"), NULL);
239                 g_free(text);
240                 text = buf;
241                 }
242         gtk_label_set_text(GTK_LABEL(dw->status_label), text);
243
244         g_free(text);
245 }
246
247 /**
248  * @brief Returns time in µsec since Epoch
249  * @returns 
250  * 
251  * 
252  */
253 static guint64 msec_time(void)
254 {
255         struct timeval tv;
256
257         if (gettimeofday(&tv, NULL) == -1) return 0;
258
259         return (guint64)tv.tv_sec * 1000000 + (guint64)tv.tv_usec;
260 }
261
262 static gint dupe_iterations(gint n)
263 {
264         return (n * ((n + 1) / 2));
265 }
266
267 /**
268  * @brief 
269  * @param dw 
270  * @param status 
271  * @param value 
272  * @param force 
273  * 
274  * If \a status is blank, clear status bar text and set progress to zero. \n
275  * If \a force is not set, after 2 secs has elapsed, update time-to-go every 250 ms. 
276  */
277 static void dupe_window_update_progress(DupeWindow *dw, const gchar *status, gdouble value, gboolean force)
278 {
279         const gchar *status_text;
280
281         if (status)
282                 {
283                 guint64 new_time = 0;
284
285                 if (dw->setup_n % 10 == 0)
286                         {
287                         new_time = msec_time() - dw->setup_time;
288                         }
289
290                 if (!force &&
291                     value != 0.0 &&
292                     dw->setup_count > 0 &&
293                     new_time > 2000000)
294                         {
295                         gchar *buf;
296                         gint t;
297                         gint d;
298                         guint32 rem;
299
300                         if (new_time - dw->setup_time_count < 250000) return;
301                         dw->setup_time_count = new_time;
302
303                         if (dw->setup_done)
304                                 {
305                                 if (dw->second_set)
306                                         {
307                                         t = dw->setup_count;
308                                         d = dw->setup_count - dw->setup_n;
309                                         }
310                                 else
311                                         {
312                                         t = dupe_iterations(dw->setup_count);
313                                         d = dupe_iterations(dw->setup_count - dw->setup_n);
314                                         }
315                                 }
316                         else
317                                 {
318                                 t = dw->setup_count;
319                                 d = dw->setup_count - dw->setup_n;
320                                 }
321
322                         rem = (t - d) ? ((gdouble)(dw->setup_time_count / 1000000) / (t - d)) * d : 0;
323
324                         gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(dw->extra_label), value);
325
326                         buf = g_strdup_printf("%s %d:%02d ", status, rem / 60, rem % 60);
327                         gtk_progress_bar_set_text(GTK_PROGRESS_BAR(dw->extra_label), buf);
328                         g_free(buf);
329
330                         return;
331                         }
332                 else if (force ||
333                          value == 0.0 ||
334                          dw->setup_count == 0 ||
335                          dw->setup_time_count == 0 ||
336                          (new_time > 0 && new_time - dw->setup_time_count >= 250000))
337                         {
338                         if (dw->setup_time_count == 0) dw->setup_time_count = 1;
339                         if (new_time > 0) dw->setup_time_count = new_time;
340                         gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(dw->extra_label), value);
341                         status_text = status;
342                         }
343                 else
344                         {
345                         status_text = NULL;
346                         }
347                 }
348         else
349                 {
350                 gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(dw->extra_label), 0.0);
351                 status_text = " ";
352                 }
353
354         if (status_text) gtk_progress_bar_set_text(GTK_PROGRESS_BAR(dw->extra_label), status_text);
355 }
356
357 static void widget_set_cursor(GtkWidget *widget, gint icon)
358 {
359         GdkCursor *cursor;
360
361         if (!gtk_widget_get_window(widget)) return;
362
363         if (icon == -1)
364                 {
365                 cursor = NULL;
366                 }
367         else
368                 {
369                 cursor = gdk_cursor_new(icon);
370                 }
371
372         gdk_window_set_cursor(gtk_widget_get_window(widget), cursor);
373
374         if (cursor) gdk_cursor_unref(cursor);
375 }
376
377 /*
378  * ------------------------------------------------------------------
379  * row color utils
380  * ------------------------------------------------------------------
381  */
382
383 static void dupe_listview_realign_colors(DupeWindow *dw)
384 {
385         GtkTreeModel *store;
386         GtkTreeIter iter;
387         gboolean color_set = TRUE;
388         DupeItem *parent = NULL;
389         gboolean valid;
390
391         store = gtk_tree_view_get_model(GTK_TREE_VIEW(dw->listview));
392         valid = gtk_tree_model_get_iter_first(store, &iter);
393         while (valid)
394                 {
395                 DupeItem *child;
396                 DupeItem *child_parent;
397
398                 gtk_tree_model_get(store, &iter, DUPE_COLUMN_POINTER, &child, -1);
399                 child_parent = dupe_match_find_parent(dw, child);
400                 if (!parent || parent != child_parent)
401                         {
402                         if (!parent)
403                                 {
404                                 /* keep the first row as it is */
405                                 gtk_tree_model_get(store, &iter, DUPE_COLUMN_COLOR, &color_set, -1);
406                                 }
407                         else
408                                 {
409                                 color_set = !color_set;
410                                 }
411                         parent = dupe_match_find_parent(dw, child);
412                         }
413                 gtk_list_store_set(GTK_LIST_STORE(store), &iter, DUPE_COLUMN_COLOR, color_set, -1);
414
415                 valid = gtk_tree_model_iter_next(GTK_TREE_MODEL(store), &iter);
416                 }
417 }
418
419 /*
420  * ------------------------------------------------------------------
421  * Dupe item utils
422  * ------------------------------------------------------------------
423  */
424
425 static DupeItem *dupe_item_new(FileData *fd)
426 {
427         DupeItem *di;
428
429         di = g_new0(DupeItem, 1);
430
431         di->fd = file_data_ref(fd);
432         di->group_rank = 0.0;
433
434         return di;
435 }
436
437 static void dupe_item_free(DupeItem *di)
438 {
439         file_data_unref(di->fd);
440         image_sim_free(di->simd);
441         g_free(di->md5sum);
442         if (di->pixbuf) g_object_unref(di->pixbuf);
443
444         g_free(di);
445 }
446
447 static void dupe_list_free(GList *list)
448 {
449         GList *work = list;
450         while (work)
451                 {
452                 DupeItem *di = work->data;
453                 work = work->next;
454                 dupe_item_free(di);
455                 }
456         g_list_free(list);
457 }
458
459 /*
460 static DupeItem *dupe_item_find_fd_by_list(FileData *fd, GList *work)
461 {
462         while (work)
463                 {
464                 DupeItem *di = work->data;
465
466                 if (di->fd == fd) return di;
467
468                 work = work->next;
469                 }
470
471         return NULL;
472 }
473 */
474
475 /*
476 static DupeItem *dupe_item_find_fd(DupeWindow *dw, FileData *fd)
477 {
478         DupeItem *di;
479
480         di = dupe_item_find_fd_by_list(fd, dw->list);
481         if (!di && dw->second_set) di = dupe_item_find_fd_by_list(fd, dw->second_list);
482
483         return di;
484 }
485 */
486
487 static DupeItem *dupe_item_find_path_by_list(const gchar *path, GList *work)
488 {
489         while (work)
490                 {
491                 DupeItem *di = work->data;
492
493                 if (strcmp(di->fd->path, path) == 0) return di;
494
495                 work = work->next;
496                 }
497
498         return NULL;
499 }
500
501 static DupeItem *dupe_item_find_path(DupeWindow *dw, const gchar *path)
502 {
503         DupeItem *di;
504
505         di = dupe_item_find_path_by_list(path, dw->list);
506         if (!di && dw->second_set) di = dupe_item_find_path_by_list(path, dw->second_list);
507
508         return di;
509 }
510
511 /*
512  * ------------------------------------------------------------------
513  * Image property cache
514  * ------------------------------------------------------------------
515  */
516
517 static void dupe_item_read_cache(DupeItem *di)
518 {
519         gchar *path;
520         CacheData *cd;
521
522         if (!di) return;
523
524         path = cache_find_location(CACHE_TYPE_SIM, di->fd->path);
525         if (!path) return;
526
527         if (filetime(di->fd->path) != filetime(path))
528                 {
529                 g_free(path);
530                 return;
531                 }
532
533         cd = cache_sim_data_load(path);
534         g_free(path);
535
536         if (cd)
537                 {
538                 if (!di->simd && cd->sim)
539                         {
540                         di->simd = cd->sim;
541                         cd->sim = NULL;
542                         }
543                 if (di->width == 0 && di->height == 0 && cd->dimensions)
544                         {
545                         di->width = cd->width;
546                         di->height = cd->height;
547                         di->dimensions = (di->width << 16) + di->height;
548                         }
549                 if (!di->md5sum && cd->have_md5sum)
550                         {
551                         di->md5sum = md5_digest_to_text(cd->md5sum);
552                         }
553                 cache_sim_data_free(cd);
554                 }
555 }
556
557 static void dupe_item_write_cache(DupeItem *di)
558 {
559         gchar *base;
560         mode_t mode = 0755;
561
562         if (!di) return;
563
564         base = cache_get_location(CACHE_TYPE_SIM, di->fd->path, FALSE, &mode);
565         if (recursive_mkdir_if_not_exists(base, mode))
566                 {
567                 CacheData *cd;
568
569                 cd = cache_sim_data_new();
570                 cd->path = cache_get_location(CACHE_TYPE_SIM, di->fd->path, TRUE, NULL);
571
572                 if (di->width != 0) cache_sim_data_set_dimensions(cd, di->width, di->height);
573                 if (di->md5sum)
574                         {
575                         guchar digest[16];
576                         if (md5_digest_from_text(di->md5sum, digest)) cache_sim_data_set_md5sum(cd, digest);
577                         }
578                 if (di->simd) cache_sim_data_set_similarity(cd, di->simd);
579
580                 if (cache_sim_data_save(cd))
581                         {
582                         filetime_set(cd->path, filetime(di->fd->path));
583                         }
584                 cache_sim_data_free(cd);
585                 }
586         g_free(base);
587 }
588
589 /*
590  * ------------------------------------------------------------------
591  * Window list utils
592  * ------------------------------------------------------------------
593  */
594
595 static gint dupe_listview_find_item(GtkListStore *store, DupeItem *item, GtkTreeIter *iter)
596 {
597         gboolean valid;
598         gint row = 0;
599
600         valid = gtk_tree_model_get_iter_first(GTK_TREE_MODEL(store), iter);
601         while (valid)
602                 {
603                 DupeItem *item_n;
604                 gtk_tree_model_get(GTK_TREE_MODEL(store), iter, DUPE_COLUMN_POINTER, &item_n, -1);
605                 if (item_n == item) return row;
606
607                 valid = gtk_tree_model_iter_next(GTK_TREE_MODEL(store), iter);
608                 row++;
609                 }
610
611         return -1;
612 }
613
614 static void dupe_listview_add(DupeWindow *dw, DupeItem *parent, DupeItem *child)
615 {
616         DupeItem *di;
617         gint row;
618         gchar *text[DUPE_COLUMN_COUNT];
619         GtkListStore *store;
620         GtkTreeIter iter;
621         gboolean color_set = FALSE;
622         gint rank;
623
624         if (!parent) return;
625
626         store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->listview)));
627
628         if (child)
629                 {
630                 DupeMatch *dm;
631
632                 row = dupe_listview_find_item(store, parent, &iter);
633                 gtk_tree_model_get(GTK_TREE_MODEL(store), &iter, DUPE_COLUMN_COLOR, &color_set, -1);
634
635                 row++;
636
637                 if (child->group)
638                         {
639                         dm = child->group->data;
640                         rank = (gint)floor(dm->rank);
641                         }
642                 else
643                         {
644                         rank = 1;
645                         log_printf("NULL group in item!\n");
646                         }
647                 }
648         else
649                 {
650                 if (gtk_tree_model_get_iter_first(GTK_TREE_MODEL(store), &iter))
651                         {
652                         gtk_tree_model_get(GTK_TREE_MODEL(store), &iter, DUPE_COLUMN_COLOR, &color_set, -1);
653                         color_set = !color_set;
654                         dw->set_count++;
655                         }
656                 else
657                         {
658                         color_set = FALSE;
659                         }
660                 row = 0;
661                 rank = 0;
662                 }
663
664         di = (child) ? child : parent;
665
666         if (!child && dw->second_set)
667                 {
668                 text[DUPE_COLUMN_RANK] = g_strdup("[1]");
669                 }
670         else if (rank == 0)
671                 {
672                 text[DUPE_COLUMN_RANK] = g_strdup((di->second) ? "(2)" : "");
673                 }
674         else
675                 {
676                 text[DUPE_COLUMN_RANK] = g_strdup_printf("%d%s", rank, (di->second) ? " (2)" : "");
677                 }
678
679         text[DUPE_COLUMN_THUMB] = "";
680         text[DUPE_COLUMN_NAME] = (gchar *)di->fd->name;
681         text[DUPE_COLUMN_SIZE] = text_from_size(di->fd->size);
682         text[DUPE_COLUMN_DATE] = (gchar *)text_from_time(di->fd->date);
683         if (di->width > 0 && di->height > 0)
684                 {
685                 text[DUPE_COLUMN_DIMENSIONS] = g_strdup_printf("%d x %d", di->width, di->height);
686                 }
687         else
688                 {
689                 text[DUPE_COLUMN_DIMENSIONS] = g_strdup("");
690                 }
691         text[DUPE_COLUMN_PATH] = di->fd->path;
692         text[DUPE_COLUMN_COLOR] = NULL;
693
694         gtk_list_store_insert(store, &iter, row);
695         gtk_list_store_set(store, &iter,
696                                 DUPE_COLUMN_POINTER, di,
697                                 DUPE_COLUMN_RANK, text[DUPE_COLUMN_RANK],
698                                 DUPE_COLUMN_THUMB, NULL,
699                                 DUPE_COLUMN_NAME, text[DUPE_COLUMN_NAME],
700                                 DUPE_COLUMN_SIZE, text[DUPE_COLUMN_SIZE],
701                                 DUPE_COLUMN_DATE, text[DUPE_COLUMN_DATE],
702                                 DUPE_COLUMN_DIMENSIONS, text[DUPE_COLUMN_DIMENSIONS],
703                                 DUPE_COLUMN_PATH, text[DUPE_COLUMN_PATH],
704                                 DUPE_COLUMN_COLOR, color_set,
705                                 DUPE_COLUMN_SET, dw->set_count,
706                                 -1);
707
708         g_free(text[DUPE_COLUMN_RANK]);
709         g_free(text[DUPE_COLUMN_SIZE]);
710         g_free(text[DUPE_COLUMN_DIMENSIONS]);
711 }
712
713 static void dupe_listview_select_dupes(DupeWindow *dw, DupeSelectType parents);
714
715 static void dupe_listview_populate(DupeWindow *dw)
716 {
717         GtkListStore *store;
718         GList *work;
719
720         store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->listview)));
721         gtk_list_store_clear(store);
722
723         work = g_list_last(dw->dupes);
724         while (work)
725                 {
726                 DupeItem *parent = work->data;
727                 GList *temp;
728
729                 dupe_listview_add(dw, parent, NULL);
730
731                 temp = g_list_last(parent->group);
732                 while (temp)
733                         {
734                         DupeMatch *dm = temp->data;
735                         DupeItem *child;
736
737                         child = dm->di;
738
739                         dupe_listview_add(dw, parent, child);
740
741                         temp = temp->prev;
742                         }
743
744                 work = work->prev;
745                 }
746
747         gtk_tree_view_columns_autosize(GTK_TREE_VIEW(dw->listview));
748
749         if (options->duplicates_select_type == DUPE_SELECT_GROUP1)
750                 {
751                 dupe_listview_select_dupes(dw, DUPE_SELECT_GROUP1);
752                 }
753         else if (options->duplicates_select_type == DUPE_SELECT_GROUP2)
754                 {
755                 dupe_listview_select_dupes(dw, DUPE_SELECT_GROUP2);
756                 }
757
758 }
759
760 static void dupe_listview_remove(DupeWindow *dw, DupeItem *di)
761 {
762         GtkListStore *store;
763         GtkTreeIter iter;
764         gint row;
765
766         if (!di) return;
767
768         store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->listview)));
769         row = dupe_listview_find_item(store, di, &iter);
770         if (row < 0) return;
771
772         tree_view_move_cursor_away(GTK_TREE_VIEW(dw->listview), &iter, TRUE);
773         gtk_list_store_remove(store, &iter);
774
775         if (g_list_find(dw->dupes, di) != NULL)
776                 {
777                 if (!dw->color_frozen) dupe_listview_realign_colors(dw);
778                 }
779 }
780
781
782 static GList *dupe_listview_get_filelist(DupeWindow *dw, GtkWidget *listview)
783 {
784         GtkTreeModel *store;
785         GtkTreeIter iter;
786         gboolean valid;
787         GList *list = NULL;
788
789         store = gtk_tree_view_get_model(GTK_TREE_VIEW(listview));
790         valid = gtk_tree_model_get_iter_first(store, &iter);
791         while (valid)
792                 {
793                 DupeItem *di;
794                 gtk_tree_model_get(store, &iter, DUPE_COLUMN_POINTER, &di, -1);
795                 list = g_list_prepend(list, file_data_ref(di->fd));
796
797                 valid = gtk_tree_model_iter_next(store, &iter);
798                 }
799
800         return g_list_reverse(list);
801 }
802
803
804 static GList *dupe_listview_get_selection(DupeWindow *dw, GtkWidget *listview)
805 {
806         GtkTreeModel *store;
807         GtkTreeSelection *selection;
808         GList *slist;
809         GList *list = NULL;
810         GList *work;
811
812         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(listview));
813         slist = gtk_tree_selection_get_selected_rows(selection, &store);
814         work = slist;
815         while (work)
816                 {
817                 GtkTreePath *tpath = work->data;
818                 DupeItem *di = NULL;
819                 GtkTreeIter iter;
820
821                 gtk_tree_model_get_iter(store, &iter, tpath);
822                 gtk_tree_model_get(store, &iter, DUPE_COLUMN_POINTER, &di, -1);
823                 if (di)
824                         {
825                         list = g_list_prepend(list, file_data_ref(di->fd));
826                         }
827                 work = work->next;
828                 }
829         g_list_foreach(slist, (GFunc)tree_path_free_wrapper, NULL);
830         g_list_free(slist);
831
832         return g_list_reverse(list);
833 }
834
835 static gboolean dupe_listview_item_is_selected(DupeWindow *dw, DupeItem *di, GtkWidget *listview)
836 {
837         GtkTreeModel *store;
838         GtkTreeSelection *selection;
839         GList *slist;
840         GList *work;
841         gboolean found = FALSE;
842
843         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(listview));
844         slist = gtk_tree_selection_get_selected_rows(selection, &store);
845         work = slist;
846         while (!found && work)
847                 {
848                 GtkTreePath *tpath = work->data;
849                 DupeItem *di_n;
850                 GtkTreeIter iter;
851
852                 gtk_tree_model_get_iter(store, &iter, tpath);
853                 gtk_tree_model_get(store, &iter, DUPE_COLUMN_POINTER, &di_n, -1);
854                 if (di_n == di) found = TRUE;
855                 work = work->next;
856                 }
857         g_list_foreach(slist, (GFunc)tree_path_free_wrapper, NULL);
858         g_list_free(slist);
859
860         return found;
861 }
862
863 static void dupe_listview_select_dupes(DupeWindow *dw, DupeSelectType parents)
864 {
865         GtkTreeModel *store;
866         GtkTreeSelection *selection;
867         GtkTreeIter iter;
868         gboolean valid;
869         gint set_count = 0;
870         gint set_count_last = -1;
871
872         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(dw->listview));
873         gtk_tree_selection_unselect_all(selection);
874
875         store = gtk_tree_view_get_model(GTK_TREE_VIEW(dw->listview));
876         valid = gtk_tree_model_get_iter_first(store, &iter);
877         while (valid)
878                 {
879                 DupeItem *di;
880
881                 gtk_tree_model_get(store, &iter, DUPE_COLUMN_POINTER, &di, DUPE_COLUMN_SET, &set_count, -1);
882                 if (set_count != set_count_last)
883                         {
884                         set_count_last = set_count;
885                         if (parents == DUPE_SELECT_GROUP1)
886                                 {
887                                 gtk_tree_selection_select_iter(selection, &iter);
888                                 }
889                         }
890                 else
891                         {
892                         if (parents == DUPE_SELECT_GROUP2)
893                                 {
894                                 gtk_tree_selection_select_iter(selection, &iter);
895                                 }
896                         }
897                 valid = gtk_tree_model_iter_next(store, &iter);
898                 }
899 }
900
901 /*
902  * ------------------------------------------------------------------
903  * Match group manipulation
904  * ------------------------------------------------------------------
905  */
906
907 /**
908  * @brief Search \a parent->group for \a child (#DupeItem)
909  * @param child 
910  * @param parent 
911  * @returns 
912  * 
913  */
914 static DupeMatch *dupe_match_find_match(DupeItem *child, DupeItem *parent)
915 {
916         GList *work;
917
918         work = parent->group;
919         while (work)
920                 {
921                 DupeMatch *dm = work->data;
922                 if (dm->di == child) return dm;
923                 work = work->next;
924                 }
925         return NULL;
926 }
927
928 /**
929  * @brief Create #DupeMatch structure for \a child, and insert into \a parent->group list.
930  * @param child 
931  * @param parent 
932  * @param rank 
933  * 
934  */
935 static void dupe_match_link_child(DupeItem *child, DupeItem *parent, gdouble rank)
936 {
937         DupeMatch *dm;
938
939         dm = g_new0(DupeMatch, 1);
940         dm->di = child;
941         dm->rank = rank;
942         parent->group = g_list_append(parent->group, dm);
943 }
944
945 /**
946  * @brief Link \a a & \a b as both parent and child
947  * @param a 
948  * @param b 
949  * @param rank 
950  * 
951  * Link \a a as child of \a b, and \a b as child of \a a
952  */
953 static void dupe_match_link(DupeItem *a, DupeItem *b, gdouble rank)
954 {
955         dupe_match_link_child(a, b, rank);
956         dupe_match_link_child(b, a, rank);
957 }
958
959 /**
960  * @brief Remove \a child #DupeMatch from \a parent->group list.
961  * @param child 
962  * @param parent 
963  * 
964  */
965 static void dupe_match_unlink_child(DupeItem *child, DupeItem *parent)
966 {
967         DupeMatch *dm;
968
969         dm = dupe_match_find_match(child, parent);
970         if (dm)
971                 {
972                 parent->group = g_list_remove(parent->group, dm);
973                 g_free(dm);
974                 }
975 }
976
977 /**
978  * @brief  Unlink \a a from \a b, and \a b from \a a
979  * @param a 
980  * @param b 
981  *
982  * Free the relevant #DupeMatch items from the #DupeItem group lists
983  */
984 static void dupe_match_unlink(DupeItem *a, DupeItem *b)
985 {
986         dupe_match_unlink_child(a, b);
987         dupe_match_unlink_child(b, a);
988 }
989
990 /**
991  * @brief 
992  * @param parent 
993  * @param unlink_children 
994  * 
995  * If \a unlink_children is set, unlink all entries in \a parent->group list. \n
996  * Free the \a parent->group list and set group_rank to zero;
997  */
998 static void dupe_match_link_clear(DupeItem *parent, gboolean unlink_children)
999 {
1000         GList *work;
1001
1002         work = parent->group;
1003         while (work)
1004                 {
1005                 DupeMatch *dm = work->data;
1006                 work = work->next;
1007
1008                 if (unlink_children) dupe_match_unlink_child(parent, dm->di);
1009
1010                 g_free(dm);
1011                 }
1012
1013         g_list_free(parent->group);
1014         parent->group = NULL;
1015         parent->group_rank = 0.0;
1016 }
1017
1018 /**
1019  * @brief Search \a parent->group list for \a child
1020  * @param child 
1021  * @param parent 
1022  * @returns boolean TRUE/FALSE found/not found
1023  * 
1024  */
1025 static gint dupe_match_link_exists(DupeItem *child, DupeItem *parent)
1026 {
1027         return (dupe_match_find_match(child, parent) != NULL);
1028 }
1029
1030 /**
1031  * @brief  Search \a parent->group for \a child, and return \a child->rank
1032  * @param child 
1033  * @param parent 
1034  * @returns \a dm->di->rank
1035  *
1036  */
1037 static gdouble dupe_match_link_rank(DupeItem *child, DupeItem *parent)
1038 {
1039         DupeMatch *dm;
1040
1041         dm = dupe_match_find_match(child, parent);
1042         if (dm) return dm->rank;
1043
1044         return 0.0;
1045 }
1046
1047 /**
1048  * @brief Find highest rank in \a child->group
1049  * @param child 
1050  * @returns 
1051  * 
1052  * Search the #DupeMatch entries in the \a child->group list.
1053  * Return the #DupeItem with the highest rank. If more than one have
1054  * the same rank, the first encountered is used.
1055  */
1056 static DupeItem *dupe_match_highest_rank(DupeItem *child)
1057 {
1058         DupeMatch *dr;
1059         GList *work;
1060
1061         dr = NULL;
1062         work = child->group;
1063         while (work)
1064                 {
1065                 DupeMatch *dm = work->data;
1066                 if (!dr || dm->rank > dr->rank)
1067                         {
1068                         dr = dm;
1069                         }
1070                 work = work->next;
1071                 }
1072
1073         return (dr) ? dr->di : NULL;
1074 }
1075
1076 /** 
1077  * @brief Compute and store \a parent->group_rank
1078  * @param parent 
1079  * 
1080  * Group_rank = (sum of all child ranks) / n
1081  */
1082 static void dupe_match_rank_update(DupeItem *parent)
1083 {
1084         GList *work;
1085         gdouble rank = 0.0;
1086         gint c = 0;
1087
1088         work = parent->group;
1089         while (work)
1090                 {
1091                 DupeMatch *dm = work->data;
1092                 work = work->next;
1093                 rank += dm->rank;
1094                 c++;
1095                 }
1096
1097         if (c > 0)
1098                 {
1099                 parent->group_rank = rank / c;
1100                 }
1101         else
1102                 {
1103                 parent->group_rank = 0.0;
1104                 }
1105 }
1106
1107 static DupeItem *dupe_match_find_parent(DupeWindow *dw, DupeItem *child)
1108 {
1109         GList *work;
1110
1111         if (g_list_find(dw->dupes, child)) return child;
1112
1113         work = child->group;
1114         while (work)
1115                 {
1116                 DupeMatch *dm = work->data;
1117                 if (g_list_find(dw->dupes, dm->di)) return dm->di;
1118                 work = work->next;
1119                 }
1120
1121         return NULL;
1122 }
1123
1124 /**
1125  * @brief 
1126  * @param work (#DupeItem) dw->list or dw->second_list
1127  * 
1128  * Unlink all #DupeItem-s in \a work.
1129  * Do not unlink children.
1130  */
1131 static void dupe_match_reset_list(GList *work)
1132 {
1133         while (work)
1134                 {
1135                 DupeItem *di = work->data;
1136                 work = work->next;
1137
1138                 dupe_match_link_clear(di, FALSE);
1139                 }
1140 }
1141
1142 static void dupe_match_reparent(DupeWindow *dw, DupeItem *old, DupeItem *new)
1143 {
1144         GList *work;
1145
1146         if (!old || !new || !dupe_match_link_exists(old, new)) return;
1147
1148         dupe_match_link_clear(new, TRUE);
1149         work = old->group;
1150         while (work)
1151                 {
1152                 DupeMatch *dm = work->data;
1153                 dupe_match_unlink_child(old, dm->di);
1154                 dupe_match_link_child(new, dm->di, dm->rank);
1155                 work = work->next;
1156                 }
1157
1158         new->group = old->group;
1159         old->group = NULL;
1160
1161         work = g_list_find(dw->dupes, old);
1162         if (work) work->data = new;
1163 }
1164
1165 static void dupe_match_print_group(DupeItem *di)
1166 {
1167         GList *work;
1168
1169         log_printf("+ %f %s\n", di->group_rank, di->fd->name);
1170
1171         work = di->group;
1172         while (work)
1173                 {
1174                 DupeMatch *dm = work->data;
1175                 work = work->next;
1176
1177                 log_printf("  %f %s\n", dm->rank, dm->di->fd->name);
1178                 }
1179
1180         log_printf("\n");
1181 }
1182
1183 static void dupe_match_print_list(GList *list)
1184 {
1185         GList *work;
1186
1187         work = list;
1188         while (work)
1189                 {
1190                 DupeItem *di = work->data;
1191                 dupe_match_print_group(di);
1192                 work = work->next;
1193                 }
1194 }
1195
1196 /* level 3, unlinking and orphan handling */
1197 /**
1198  * @brief 
1199  * @param child 
1200  * @param parent \a di from \a child->group
1201  * @param[inout] list \a dw->list sorted by rank (#DupeItem)
1202  * @param dw 
1203  * @returns modified \a list
1204  *
1205  * Called for each entry in \a child->group (#DupeMatch) with \a parent set to \a dm->di. \n
1206  * Find the highest rank #DupeItem of the \a parent's children. \n
1207  * If that is == \a child OR
1208  * highest rank #DupeItem of \a child == \a parent then FIXME:
1209  * 
1210  */
1211 static GList *dupe_match_unlink_by_rank(DupeItem *child, DupeItem *parent, GList *list, DupeWindow *dw)
1212 {
1213         DupeItem *best = NULL;
1214
1215         best = dupe_match_highest_rank(parent); // highest rank in parent->group
1216         if (best == child || dupe_match_highest_rank(child) == parent)
1217                 {
1218                 GList *work;
1219                 gdouble rank;
1220
1221                 DEBUG_2("link found %s to %s [%d]", child->fd->name, parent->fd->name, g_list_length(parent->group));
1222
1223                 work = parent->group;
1224                 while (work)
1225                         {
1226                         DupeMatch *dm = work->data;
1227                         DupeItem *orphan;
1228
1229                         work = work->next;
1230                         orphan = dm->di;
1231                         if (orphan != child && g_list_length(orphan->group) < 2)
1232                                 {
1233                                 dupe_match_link_clear(orphan, TRUE);
1234                                 if (!dw->second_set || orphan->second)
1235                                         {
1236                                         dupe_match(orphan, child, dw->match_mask, &rank, FALSE);
1237                                         dupe_match_link(orphan, child, rank);
1238                                         }
1239                                 list = g_list_remove(list, orphan);
1240                                 }
1241                         }
1242
1243                 rank = dupe_match_link_rank(child, parent); // child->rank
1244                 dupe_match_link_clear(parent, TRUE);
1245                 dupe_match_link(child, parent, rank);
1246                 list = g_list_remove(list, parent);
1247                 }
1248         else
1249                 {
1250                 DEBUG_2("unlinking %s and %s", child->fd->name, parent->fd->name);
1251
1252                 dupe_match_unlink(child, parent);
1253                 }
1254
1255         return list;
1256 }
1257
1258 /* level 2 */
1259 /**
1260  * @brief 
1261  * @param[inout] list \a dw->list sorted by rank (#DupeItem)
1262  * @param di 
1263  * @param dw 
1264  * @returns modified \a list
1265  * 
1266  * Called for each entry in \a list.
1267  * Call unlink for each child in \a di->group
1268  */
1269 static GList *dupe_match_group_filter(GList *list, DupeItem *di, DupeWindow *dw)
1270 {
1271         GList *work;
1272
1273         work = g_list_last(di->group);
1274         while (work)
1275                 {
1276                 DupeMatch *dm = work->data;
1277                 work = work->prev;
1278                 list = dupe_match_unlink_by_rank(di, dm->di, list, dw);
1279                 }
1280
1281         return list;
1282 }
1283
1284 /* level 1 (top) */
1285 /**
1286  * @brief 
1287  * @param[inout] list \a dw->list sorted by rank (#DupeItem)
1288  * @param dw 
1289  * @returns Filtered \a list
1290  * 
1291  * Called once.
1292  * Call group filter for each \a di in \a list
1293  */
1294 static GList *dupe_match_group_trim(GList *list, DupeWindow *dw)
1295 {
1296         GList *work;
1297
1298         work = list;
1299         while (work)
1300                 {
1301                 DupeItem *di = work->data;
1302                 if (!di->second) list = dupe_match_group_filter(list, di, dw);
1303                 work = work->next;
1304                 if (di->second) list = g_list_remove(list, di);
1305                 }
1306
1307         return list;
1308 }
1309
1310 static gint dupe_match_sort_groups_cb(gconstpointer a, gconstpointer b)
1311 {
1312         DupeMatch *da = (DupeMatch *)a;
1313         DupeMatch *db = (DupeMatch *)b;
1314
1315         if (da->rank > db->rank) return -1;
1316         if (da->rank < db->rank) return 1;
1317         return 0;
1318 }
1319
1320 /**
1321  * @brief Sorts the children of each #DupeItem in \a list
1322  * @param list #DupeItem
1323  * 
1324  * Sorts the #DupeItem->group children on rank
1325  */
1326 static void dupe_match_sort_groups(GList *list)
1327 {
1328         GList *work;
1329
1330         work = list;
1331         while (work)
1332                 {
1333                 DupeItem *di = work->data;
1334                 di->group = g_list_sort(di->group, dupe_match_sort_groups_cb);
1335                 work = work->next;
1336                 }
1337 }
1338
1339 static gint dupe_match_totals_sort_cb(gconstpointer a, gconstpointer b)
1340 {
1341         DupeItem *da = (DupeItem *)a;
1342         DupeItem *db = (DupeItem *)b;
1343
1344         if (g_list_length(da->group) > g_list_length(db->group)) return -1;
1345         if (g_list_length(da->group) < g_list_length(db->group)) return 1;
1346
1347         if (da->group_rank < db->group_rank) return -1;
1348         if (da->group_rank > db->group_rank) return 1;
1349
1350         return 0;
1351 }
1352
1353 /**
1354  * @brief Callback for group_rank sort
1355  * @param a 
1356  * @param b 
1357  * @returns 
1358  * 
1359  * 
1360  */
1361 static gint dupe_match_rank_sort_cb(gconstpointer a, gconstpointer b)
1362 {
1363         DupeItem *da = (DupeItem *)a;
1364         DupeItem *db = (DupeItem *)b;
1365
1366         if (da->group_rank > db->group_rank) return -1;
1367         if (da->group_rank < db->group_rank) return 1;
1368         return 0;
1369 }
1370
1371 /**
1372  * @brief Sorts \a source_list by group-rank
1373  * @param source_list #DupeItem
1374  * @returns 
1375  *
1376  * Computes group_rank for each #DupeItem. \n
1377  * Items with no group list are ignored.
1378  * Returns allocated GList of #DupeItem-s sorted by group_rank
1379  */
1380 static GList *dupe_match_rank_sort(GList *source_list)
1381 {
1382         GList *list = NULL;
1383         GList *work;
1384
1385         work = source_list;
1386         while (work)
1387                 {
1388                 DupeItem *di = work->data;
1389
1390                 if (di->group)
1391                         {
1392                         dupe_match_rank_update(di); // Compute and store group_rank for di
1393                         list = g_list_prepend(list, di);
1394                         }
1395
1396                 work = work->next;
1397                 }
1398
1399         return g_list_sort(list, dupe_match_rank_sort_cb);
1400 }
1401
1402 /**
1403  * @brief Returns allocated GList of dupes sorted by totals
1404  * @param source_list 
1405  * @returns 
1406  * 
1407  * 
1408  */
1409 static GList *dupe_match_totals_sort(GList *source_list)
1410 {
1411         source_list = g_list_sort(source_list, dupe_match_totals_sort_cb);
1412
1413         source_list = g_list_first(source_list);
1414         return g_list_reverse(source_list);
1415 }
1416
1417 /**
1418  * @brief 
1419  * @param dw 
1420  * 
1421  * Called once.
1422  */
1423 static void dupe_match_rank(DupeWindow *dw)
1424 {
1425         GList *list;
1426
1427         list = dupe_match_rank_sort(dw->list); // sorted by group_rank, no-matches filtered out
1428
1429         if (required_debug_level(2)) dupe_match_print_list(list);
1430
1431         DEBUG_1("Similar items: %d", g_list_length(list));
1432         list = dupe_match_group_trim(list, dw);
1433         DEBUG_1("Unique groups: %d", g_list_length(list));
1434
1435         dupe_match_sort_groups(list);
1436
1437         if (required_debug_level(2)) dupe_match_print_list(list);
1438
1439         list = dupe_match_rank_sort(list);
1440         if (options->sort_totals)
1441                 {
1442                 list = dupe_match_totals_sort(list);
1443                 }
1444         if (required_debug_level(2)) dupe_match_print_list(list);
1445
1446         g_list_free(dw->dupes);
1447         dw->dupes = list;
1448 }
1449
1450 /*
1451  * ------------------------------------------------------------------
1452  * Match group tests
1453  * ------------------------------------------------------------------
1454  */
1455
1456 /**
1457  * @brief 
1458  * @param[in] a 
1459  * @param[in] b 
1460  * @param[in] mask 
1461  * @param[out] rank 
1462  * @param[in] fast 
1463  * @returns 
1464  * 
1465  * For similarity checks, compute rank - (similarity factor between a and b). \n
1466  * If rank < user-set sim value, returns FALSE.
1467  */
1468 static gboolean dupe_match(DupeItem *a, DupeItem *b, DupeMatchType mask, gdouble *rank, gint fast)
1469 {
1470         *rank = 0.0;
1471
1472         if (a->fd->path == b->fd->path) return FALSE;
1473
1474         if (mask & DUPE_MATCH_ALL)
1475                 {
1476                 return TRUE;
1477                 }
1478         if (mask & DUPE_MATCH_PATH)
1479                 {
1480                 if (utf8_compare(a->fd->path, b->fd->path, TRUE) != 0) return FALSE;
1481                 }
1482         if (mask & DUPE_MATCH_NAME)
1483                 {
1484                 if (strcmp(a->fd->collate_key_name, b->fd->collate_key_name) != 0) return FALSE;
1485                 }
1486         if (mask & DUPE_MATCH_NAME_CI)
1487                 {
1488                 if (strcmp(a->fd->collate_key_name_nocase, b->fd->collate_key_name_nocase) != 0) return FALSE;
1489                 }
1490         if (mask & DUPE_MATCH_NAME_CONTENT)
1491                 {
1492                 if (strcmp(a->fd->collate_key_name, b->fd->collate_key_name) == 0)
1493                         {
1494                         if (!a->md5sum) a->md5sum = md5_text_from_file_utf8(a->fd->path, "");
1495                         if (!b->md5sum) b->md5sum = md5_text_from_file_utf8(b->fd->path, "");
1496                         if (a->md5sum[0] == '\0' ||
1497                             b->md5sum[0] == '\0' ||
1498                             strcmp(a->md5sum, b->md5sum) != 0)
1499                                 {
1500                                 return TRUE;
1501                                 }
1502                         else
1503                                 {
1504                                 return FALSE;
1505                                 }
1506                         }
1507                 else
1508                         {
1509                         return FALSE;
1510                         }
1511                 }
1512         if (mask & DUPE_MATCH_NAME_CI_CONTENT)
1513                 {
1514                 if (strcmp(a->fd->collate_key_name_nocase, b->fd->collate_key_name_nocase) == 0)
1515                         {
1516                         if (!a->md5sum) a->md5sum = md5_text_from_file_utf8(a->fd->path, "");
1517                         if (!b->md5sum) b->md5sum = md5_text_from_file_utf8(b->fd->path, "");
1518                         if (a->md5sum[0] == '\0' ||
1519                             b->md5sum[0] == '\0' ||
1520                             strcmp(a->md5sum, b->md5sum) != 0)
1521                                 {
1522                                 return TRUE;
1523                                 }
1524                         else
1525                                 {
1526                                 return FALSE;
1527                                 }
1528                         }
1529                 else
1530                         {
1531                         return FALSE;
1532                         }
1533                 }
1534         if (mask & DUPE_MATCH_SIZE)
1535                 {
1536                 if (a->fd->size != b->fd->size) return FALSE;
1537                 }
1538         if (mask & DUPE_MATCH_DATE)
1539                 {
1540                 if (a->fd->date != b->fd->date) return FALSE;
1541                 }
1542         if (mask & DUPE_MATCH_SUM)
1543                 {
1544                 if (!a->md5sum) a->md5sum = md5_text_from_file_utf8(a->fd->path, "");
1545                 if (!b->md5sum) b->md5sum = md5_text_from_file_utf8(b->fd->path, "");
1546                 if (a->md5sum[0] == '\0' ||
1547                     b->md5sum[0] == '\0' ||
1548                     strcmp(a->md5sum, b->md5sum) != 0) return FALSE;
1549                 }
1550         if (mask & DUPE_MATCH_DIM)
1551                 {
1552                 if (a->width == 0) image_load_dimensions(a->fd, &a->width, &a->height);
1553                 if (b->width == 0) image_load_dimensions(b->fd, &b->width, &b->height);
1554                 if (a->width != b->width || a->height != b->height) return FALSE;
1555                 }
1556         if (mask & DUPE_MATCH_SIM_HIGH ||
1557             mask & DUPE_MATCH_SIM_MED ||
1558             mask & DUPE_MATCH_SIM_LOW ||
1559             mask & DUPE_MATCH_SIM_CUSTOM)
1560                 {
1561                 gdouble f;
1562                 gdouble m;
1563
1564                 if (mask & DUPE_MATCH_SIM_HIGH) m = 0.95;
1565                 else if (mask & DUPE_MATCH_SIM_MED) m = 0.90;
1566                 else if (mask & DUPE_MATCH_SIM_CUSTOM) m = (gdouble)options->duplicates_similarity_threshold / 100.0;
1567                 else m = 0.85;
1568
1569                 if (fast)
1570                         {
1571                         f = image_sim_compare_fast(a->simd, b->simd, m);
1572                         }
1573                 else
1574                         {
1575                         f = image_sim_compare(a->simd, b->simd);
1576                         }
1577
1578                 *rank = f * 100.0;
1579
1580                 if (f < m) return FALSE;
1581
1582                 DEBUG_3("similar: %32s %32s = %f", a->fd->name, b->fd->name, f);
1583                 }
1584
1585         return TRUE;
1586 }
1587
1588 /**
1589  * @brief  Determine if there is a match
1590  * @param di1 
1591  * @param di2 
1592  * @param data 
1593  * @returns DUPE_MATCH/DUPE_NO_MATCH/DUPE_NAME_MATCH
1594  *                      DUPE_NAME_MATCH is used for name != contents searches:
1595  *                                                      the name and content match i.e.
1596  *                                                      no match, but keep searching
1597  * 
1598  * Called when stepping down the array looking for adjacent matches,
1599  * and from the 2nd set search.
1600  * 
1601  * Is not used for similarity checks.
1602  */
1603 static DUPE_CHECK_RESULT dupe_match_check(DupeItem *di1, DupeItem *di2, gpointer data)
1604 {
1605         DupeWindow *dw = data;
1606         DupeMatchType mask = dw->match_mask;
1607
1608         if (mask & DUPE_MATCH_ALL)
1609                 {
1610                 return DUPE_MATCH;
1611                 }
1612         if (mask & DUPE_MATCH_PATH)
1613                 {
1614                 if (utf8_compare(di1->fd->path, di2->fd->path, TRUE) != 0)
1615                         {
1616                         return DUPE_NO_MATCH;
1617                         }
1618                 }
1619         if (mask & DUPE_MATCH_NAME)
1620                 {
1621                 if (g_strcmp0(di1->fd->collate_key_name, di2->fd->collate_key_name) != 0)
1622                         {
1623                         return DUPE_NO_MATCH;
1624                         }
1625                 }
1626         if (mask & DUPE_MATCH_NAME_CI)
1627                 {
1628                 if (g_strcmp0(di1->fd->collate_key_name_nocase, di2->fd->collate_key_name_nocase) != 0 )
1629                         {
1630                         return DUPE_NO_MATCH;
1631                         }
1632                 }
1633         if (mask & DUPE_MATCH_NAME_CONTENT)
1634                 {
1635                 if (g_strcmp0(di1->fd->collate_key_name, di2->fd->collate_key_name) == 0)
1636                         {
1637                         if (g_strcmp0(di1->md5sum, di2->md5sum) == 0)
1638                                 {
1639                                 return DUPE_NAME_MATCH;
1640                                 }
1641                         }
1642                 else
1643                         {
1644                         return DUPE_NO_MATCH;
1645                         }
1646                 }
1647         if (mask & DUPE_MATCH_NAME_CI_CONTENT)
1648                 {
1649                 if (strcmp(di1->fd->collate_key_name_nocase, di2->fd->collate_key_name_nocase) == 0)
1650                         {
1651                         if (g_strcmp0(di1->md5sum, di2->md5sum) == 0)
1652                                 {
1653                                 return DUPE_NAME_MATCH;
1654                                 }
1655                         }
1656                 else
1657                         {
1658                         return DUPE_NO_MATCH;
1659                         }
1660                 }
1661         if (mask & DUPE_MATCH_SIZE)
1662                 {
1663                 if (di1->fd->size != di2->fd->size)
1664                         {
1665                         return DUPE_NO_MATCH;
1666                         }
1667                 }
1668         if (mask & DUPE_MATCH_DATE)
1669                 {
1670                 if (di1->fd->date != di2->fd->date)
1671                         {
1672                         return DUPE_NO_MATCH;
1673                         }
1674                 }
1675         if (mask & DUPE_MATCH_SUM)
1676                 {
1677                 if (g_strcmp0(di1->md5sum, di2->md5sum) != 0)
1678                         {
1679                         return DUPE_NO_MATCH;
1680                         }
1681                 }
1682         if (mask & DUPE_MATCH_DIM)
1683                 {
1684                 if (di1->dimensions != di2->dimensions)
1685                         {
1686                         return DUPE_NO_MATCH;
1687                         }
1688                 }
1689
1690         return DUPE_MATCH;
1691 }
1692
1693 /**
1694  * @brief The callback for the binary search
1695  * @param a 
1696  * @param b 
1697  * @param param_match_mask
1698  * @returns negative/0/positive
1699  * 
1700  * Is not used for similarity checks.
1701  *
1702  * Used only when two file sets are used.
1703  * Requires use of a global for param_match_mask because there is no
1704  * g_array_binary_search_with_data() function in glib.
1705  */
1706 static gint dupe_match_binary_search_cb(gconstpointer a, gconstpointer b)
1707 {
1708         const DupeItem *di1 = *((DupeItem **) a);
1709         const DupeItem *di2 = b;
1710         DupeMatchType mask = param_match_mask;
1711
1712         if (mask & DUPE_MATCH_ALL)
1713                 {
1714                 return 0;
1715                 }
1716         if (mask & DUPE_MATCH_PATH)
1717                 {
1718                 return utf8_compare(di1->fd->path, di2->fd->path, TRUE);
1719                 }
1720         if (mask & DUPE_MATCH_NAME)
1721                 {
1722                 return g_strcmp0(di1->fd->collate_key_name, di2->fd->collate_key_name);
1723                 }
1724         if (mask & DUPE_MATCH_NAME_CI)
1725                 {
1726                 return strcmp(di1->fd->collate_key_name_nocase, di2->fd->collate_key_name_nocase);
1727                 }
1728         if (mask & DUPE_MATCH_NAME_CONTENT)
1729                 {
1730                 return g_strcmp0(di1->fd->collate_key_name, di2->fd->collate_key_name);
1731                 }
1732         if (mask & DUPE_MATCH_NAME_CI_CONTENT)
1733                 {
1734                 return strcmp(di1->fd->collate_key_name_nocase, di2->fd->collate_key_name_nocase);
1735                 }
1736         if (mask & DUPE_MATCH_SIZE)
1737                 {
1738                 return (di1->fd->size - di2->fd->size);
1739                 }
1740         if (mask & DUPE_MATCH_DATE)
1741                 {
1742                 return (di1->fd->date - di2->fd->date);
1743                 }
1744         if (mask & DUPE_MATCH_SUM)
1745                 {
1746                 return g_strcmp0(di1->md5sum, di2->md5sum);
1747                 }
1748         if (mask & DUPE_MATCH_DIM)
1749                 {
1750                 return (di1->dimensions - di2->dimensions);
1751                 }
1752
1753         return 0;
1754 }
1755
1756 /**
1757  * @brief The callback for the array sort
1758  * @param a 
1759  * @param b 
1760  * @param data 
1761  * @returns negative/0/positive
1762  * 
1763  * Is not used for similarity checks.
1764 */
1765 static gint dupe_match_sort_cb(gconstpointer a, gconstpointer b, gpointer data)
1766 {
1767         const DupeItem *di1 = *((DupeItem **) a);
1768         const DupeItem *di2 = *((DupeItem **) b);
1769         DupeWindow *dw = data;
1770         DupeMatchType mask = dw->match_mask;
1771
1772         if (mask & DUPE_MATCH_ALL)
1773                 {
1774                 return 0;
1775                 }
1776         if (mask & DUPE_MATCH_PATH)
1777                 {
1778                 return utf8_compare(di1->fd->path, di2->fd->path, TRUE);
1779                 }
1780         if (mask & DUPE_MATCH_NAME)
1781                 {
1782                 return g_strcmp0(di1->fd->collate_key_name, di2->fd->collate_key_name);
1783                 }
1784         if (mask & DUPE_MATCH_NAME_CI)
1785                 {
1786                 return strcmp(di1->fd->collate_key_name_nocase, di2->fd->collate_key_name_nocase);
1787                 }
1788         if (mask & DUPE_MATCH_NAME_CONTENT)
1789                 {
1790                 return g_strcmp0(di1->fd->collate_key_name, di2->fd->collate_key_name);
1791                 }
1792         if (mask & DUPE_MATCH_NAME_CI_CONTENT)
1793                 {
1794                 return strcmp(di1->fd->collate_key_name_nocase, di2->fd->collate_key_name_nocase);
1795                 }
1796         if (mask & DUPE_MATCH_SIZE)
1797                 {
1798                 return (di1->fd->size - di2->fd->size);
1799                 }
1800         if (mask & DUPE_MATCH_DATE)
1801                 {
1802                 return (di1->fd->date - di2->fd->date);
1803                 }
1804         if (mask & DUPE_MATCH_SUM)
1805                 {
1806                 if (di1->md5sum[0] == '\0' || di2->md5sum[0] == '\0')
1807                     {
1808                         return -1;
1809                         }
1810                 else
1811                         {
1812                         return strcmp(di1->md5sum, di2->md5sum);
1813                         }
1814                 }
1815         if (mask & DUPE_MATCH_DIM)
1816                 {
1817                 if (!di1 || !di2 || !di1->width || !di1->height || !di2->width || !di2->height)
1818                         {
1819                         return -1;
1820                         }
1821                 return (di1->dimensions - di2->dimensions);
1822                 }
1823
1824         return 0; // should not execute
1825 }
1826
1827 /**
1828  * @brief Check for duplicate matches
1829  * @param dw 
1830  *
1831  * Is not used for similarity checks.
1832  *
1833  * Loads the file sets into an array and sorts on the searched
1834  * for parameter.
1835  * 
1836  * If one file set, steps down the array looking for adjacent equal values.
1837  * 
1838  * If two file sets, steps down the first set and for each value
1839  * does a binary search for matches in the second set.
1840  */ 
1841 static void dupe_array_check(DupeWindow *dw )
1842 {
1843         GArray *array_set1;
1844         GArray *array_set2;
1845         GList *work;
1846         gint i_set1;
1847         gint i_set2;
1848         DUPE_CHECK_RESULT check_result;
1849         DupeMatchType mask = dw->match_mask;
1850         param_match_mask = dw->match_mask;
1851         guint out_match_index;
1852
1853         if (!dw->list) return;
1854
1855         array_set1 = g_array_new(TRUE, TRUE, sizeof(gpointer));
1856         array_set2 = g_array_new(TRUE, TRUE, sizeof(gpointer));
1857         dupe_match_reset_list(dw->list);
1858
1859         work = dw->list;
1860         while (work)
1861                 {
1862                 DupeItem *di = work->data;
1863                 g_array_append_val(array_set1, di);
1864                 work = work->next;
1865                 }
1866
1867         g_array_sort_with_data(array_set1, dupe_match_sort_cb, dw);
1868
1869         if (dw->second_set)
1870                 {
1871                 /* Two sets - nothing can be done until a second set is loaded */
1872                 if (dw->second_list)
1873                         {
1874                         work = dw->second_list;
1875                         while (work)
1876                                 {
1877                                 DupeItem *di = work->data;
1878                                 g_array_append_val(array_set2, (work->data));
1879                                 work = work->next;
1880                                 }
1881                         g_array_sort_with_data(array_set2, dupe_match_sort_cb, dw);
1882
1883                         for (i_set1 = 0; i_set1 <= (gint)(array_set1->len) - 1; i_set1++)
1884                                 {
1885                                 DupeItem *di1 = g_array_index(array_set1, gpointer, i_set1);
1886                                 DupeItem *di2 = NULL;
1887                                 /* If multiple identical entries in set 1, use the last one */
1888                                 if (i_set1 < (gint)(array_set1->len) - 2)
1889                                         {
1890                                         di2 = g_array_index(array_set1, gpointer, i_set1 + 1);
1891                                         check_result = dupe_match_check(di1, di2, dw);
1892                                         if (check_result == DUPE_MATCH || check_result == DUPE_NAME_MATCH)
1893                                                 {
1894                                                 continue;
1895                                                 }
1896                                         }
1897                                 if (g_array_binary_search(array_set2, di1, dupe_match_binary_search_cb, &out_match_index))
1898                                         {
1899                                         di2 = g_array_index(array_set2, gpointer, out_match_index);
1900
1901                                         check_result = dupe_match_check(di1, di2, dw);
1902                                         if (check_result == DUPE_MATCH || check_result == DUPE_NAME_MATCH)
1903                                                 {
1904                                                 if (check_result == DUPE_MATCH)
1905                                                         {
1906                                                         dupe_match_link(di2, di1, 0.0);
1907                                                         }
1908                                                 i_set2 = out_match_index + 1;
1909
1910                                                 if (i_set2 > (gint)(array_set2->len) - 1)
1911                                                         {
1912                                                         break;
1913                                                         }
1914                                                 /* Look for multiple matches in set 2 for item di1 */
1915                                                 di2 = g_array_index(array_set2, gpointer, i_set2);
1916                                                 check_result = dupe_match_check(di1, di2, dw);
1917                                                 while (check_result == DUPE_MATCH || check_result == DUPE_NAME_MATCH)
1918                                                         {
1919                                                         if (check_result == DUPE_MATCH)
1920                                                                 {
1921                                                                 dupe_match_link(di2, di1, 0.0);
1922                                                                 }
1923                                                         i_set2++;
1924                                                         if (i_set2 > (gint)(array_set2->len) - 1)
1925                                                                 {
1926                                                                 break;
1927                                                                 }
1928                                                         di2 = g_array_index(array_set2, gpointer, i_set2);
1929                                                         check_result = dupe_match_check(di1, di2, dw);
1930                                                         }
1931                                                 }
1932                                         }
1933                                 }
1934                         }
1935                 }
1936         else
1937                 {
1938                 /* File set 1 only */
1939                 g_list_free(dw->dupes);
1940                 dw->dupes = NULL;
1941
1942                 if ((gint)(array_set1->len) > 1)
1943                         {
1944                         for (i_set1 = 0; i_set1 <= (gint)(array_set1->len) - 2; i_set1++)
1945                                 {
1946                                 DupeItem *di1 = g_array_index(array_set1, gpointer, i_set1);
1947                                 DupeItem *di2 = g_array_index(array_set1, gpointer, i_set1 + 1);
1948
1949                                 check_result = dupe_match_check(di1, di2, dw);
1950                                 if (check_result == DUPE_MATCH || check_result == DUPE_NAME_MATCH)
1951                                         {
1952                                         if (check_result == DUPE_MATCH)
1953                                                 {
1954                                                 dupe_match_link(di2, di1, 0.0);
1955                                                 }
1956                                         i_set1++;
1957
1958                                         if ( i_set1 + 1 > (gint)(array_set1->len) - 1)
1959                                                 {
1960                                                 break;
1961                                                 }
1962                                         /* Look for multiple matches for item di1 */
1963                                         di2 = g_array_index(array_set1, gpointer, i_set1 + 1);
1964                                         check_result = dupe_match_check(di1, di2, dw);
1965                                         while (check_result == DUPE_MATCH || check_result == DUPE_NAME_MATCH)
1966                                                 {
1967                                                 if (check_result == DUPE_MATCH)
1968                                                         {
1969                                                         dupe_match_link(di2, di1, 0.0);
1970                                                         }
1971                                                 i_set1++;
1972
1973                                                 if (i_set1 + 1 > (gint)(array_set1->len) - 1)
1974                                                         {
1975                                                         break;
1976                                                         }
1977                                                 di2 = g_array_index(array_set1, gpointer, i_set1 + 1);
1978                                                 check_result = dupe_match_check(di1, di2, dw);
1979                                                 }
1980                                         }
1981                                 }
1982                         }
1983                 }
1984         g_array_free(array_set1, TRUE);
1985         g_array_free(array_set2, TRUE);
1986 }
1987
1988 /**
1989  * @brief Look for similarity match
1990  * @param dw 
1991  * @param needle 
1992  * @param start 
1993  * 
1994  * Only used for similarity checks.\n
1995  * Called from dupe_check_cb.
1996  * Called for each entry in the list.
1997  * Steps through the list looking for matches against needle.
1998  * Pushes a #DupeQueueItem onto thread pool queue.
1999  */
2000 static void dupe_list_check_match(DupeWindow *dw, DupeItem *needle, GList *start)
2001 {
2002         GList *work;
2003         DupeQueueItem *dqi;
2004
2005         if (dw->second_set)
2006                 {
2007                 work = dw->second_list;
2008                 }
2009         else if (start)
2010                 {
2011                 work = start;
2012                 }
2013         else
2014                 {
2015                 work = g_list_last(dw->list);
2016                 }
2017
2018         dqi = g_new0(DupeQueueItem, 1);
2019         dqi->needle = needle;
2020         dqi->dw = dw;
2021         dqi->work = work;
2022         dqi->index = dw->queue_count;
2023         g_thread_pool_push(dw->dupe_comparison_thread_pool, dqi, NULL);
2024 }
2025
2026 /*
2027  * ------------------------------------------------------------------
2028  * Thumbnail handling
2029  * ------------------------------------------------------------------
2030  */
2031
2032 static void dupe_listview_set_thumb(DupeWindow *dw, DupeItem *di, GtkTreeIter *iter)
2033 {
2034         GtkListStore *store;
2035         GtkTreeIter iter_n;
2036
2037         store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->listview)));
2038         if (!iter)
2039                 {
2040                 if (dupe_listview_find_item(store, di, &iter_n) >= 0)
2041                         {
2042                         iter = &iter_n;
2043                         }
2044                 }
2045
2046         if (iter) gtk_list_store_set(store, iter, DUPE_COLUMN_THUMB, di->pixbuf, -1);
2047 }
2048
2049 static void dupe_thumb_do(DupeWindow *dw)
2050 {
2051         DupeItem *di;
2052
2053         if (!dw->thumb_loader || !dw->thumb_item) return;
2054         di = dw->thumb_item;
2055
2056         if (di->pixbuf) g_object_unref(di->pixbuf);
2057         di->pixbuf = thumb_loader_get_pixbuf(dw->thumb_loader);
2058
2059         dupe_listview_set_thumb(dw, di, NULL);
2060 }
2061
2062 static void dupe_thumb_error_cb(ThumbLoader *tl, gpointer data)
2063 {
2064         DupeWindow *dw = data;
2065
2066         dupe_thumb_do(dw);
2067         dupe_thumb_step(dw);
2068 }
2069
2070 static void dupe_thumb_done_cb(ThumbLoader *tl, gpointer data)
2071 {
2072         DupeWindow *dw = data;
2073
2074         dupe_thumb_do(dw);
2075         dupe_thumb_step(dw);
2076 }
2077
2078 static void dupe_thumb_step(DupeWindow *dw)
2079 {
2080         GtkTreeModel *store;
2081         GtkTreeIter iter;
2082         DupeItem *di = NULL;
2083         gboolean valid;
2084         gint row = 0;
2085         gint length = 0;
2086
2087         store = gtk_tree_view_get_model(GTK_TREE_VIEW(dw->listview));
2088         valid = gtk_tree_model_get_iter_first(store, &iter);
2089
2090         while (!di && valid)
2091                 {
2092                 GdkPixbuf *pixbuf;
2093
2094                 length++;
2095                 gtk_tree_model_get(store, &iter, DUPE_COLUMN_POINTER, &di, DUPE_COLUMN_THUMB, &pixbuf, -1);
2096                 if (pixbuf || di->pixbuf)
2097                         {
2098                         if (!pixbuf) gtk_list_store_set(GTK_LIST_STORE(store), &iter, DUPE_COLUMN_THUMB, di->pixbuf, -1);
2099                         row++;
2100                         di = NULL;
2101                         }
2102                 valid = gtk_tree_model_iter_next(store, &iter);
2103                 }
2104         if (valid)
2105                 {
2106                 while (gtk_tree_model_iter_next(store, &iter)) length++;
2107                 }
2108
2109         if (!di)
2110                 {
2111                 dw->thumb_item = NULL;
2112                 thumb_loader_free(dw->thumb_loader);
2113                 dw->thumb_loader = NULL;
2114
2115                 dupe_window_update_progress(dw, NULL, 0.0, FALSE);
2116                 return;
2117                 }
2118
2119         dupe_window_update_progress(dw, _("Loading thumbs..."),
2120                                     length == 0 ? 0.0 : (gdouble)(row) / length, FALSE);
2121
2122         dw->thumb_item = di;
2123         thumb_loader_free(dw->thumb_loader);
2124         dw->thumb_loader = thumb_loader_new(options->thumbnails.max_width, options->thumbnails.max_height);
2125
2126         thumb_loader_set_callbacks(dw->thumb_loader,
2127                                    dupe_thumb_done_cb,
2128                                    dupe_thumb_error_cb,
2129                                    NULL,
2130                                    dw);
2131
2132         /* start it */
2133         if (!thumb_loader_start(dw->thumb_loader, di->fd))
2134                 {
2135                 /* error, handle it, do next */
2136                 DEBUG_1("error loading thumb for %s", di->fd->path);
2137                 dupe_thumb_do(dw);
2138                 dupe_thumb_step(dw);
2139                 }
2140 }
2141
2142 /*
2143  * ------------------------------------------------------------------
2144  * Dupe checking loop
2145  * ------------------------------------------------------------------
2146  */
2147
2148 static void dupe_check_stop(DupeWindow *dw)
2149 {
2150         if (dw->idle_id > 0)
2151                 {
2152                 g_source_remove(dw->idle_id);
2153                 dw->idle_id = 0;
2154                 }
2155
2156         dw->abort = TRUE;
2157
2158         while (dw->thread_count < dw->queue_count) // Wait for the queue to empty
2159                 {
2160                 dupe_window_update_progress(dw, NULL, 0.0, FALSE);
2161                 widget_set_cursor(dw->listview, -1);
2162                 }
2163
2164         g_list_free(dw->search_matches);
2165         dw->search_matches = NULL;
2166
2167         if (dw->idle_id || dw->img_loader || dw->thumb_loader)
2168                 {
2169                 if (dw->idle_id > 0)
2170                         {
2171                         g_source_remove(dw->idle_id);
2172                         dw->idle_id = 0;
2173                         }
2174                 dupe_window_update_progress(dw, NULL, 0.0, FALSE);
2175                 widget_set_cursor(dw->listview, -1);
2176                 }
2177
2178         if (dw->add_files_queue_id)
2179                 {
2180                 g_source_remove(dw->add_files_queue_id);
2181                 dw->add_files_queue_id = 0;
2182                 dupe_destroy_list_cache(dw);
2183                 gtk_widget_set_sensitive(dw->controls_box, TRUE);
2184                 if (g_list_length(dw->add_files_queue) > 0)
2185                         {
2186                         filelist_free(dw->add_files_queue);
2187                         }
2188                 dw->add_files_queue = NULL;
2189                 dupe_window_update_progress(dw, NULL, 0.0, FALSE);
2190                 widget_set_cursor(dw->listview, -1);
2191                 }
2192
2193         thumb_loader_free(dw->thumb_loader);
2194         dw->thumb_loader = NULL;
2195
2196         image_loader_free(dw->img_loader);
2197         dw->img_loader = NULL;
2198 }
2199
2200 static void dupe_check_stop_cb(GtkWidget *widget, gpointer data)
2201 {
2202         DupeWindow *dw = data;
2203
2204         dupe_check_stop(dw);
2205 }
2206
2207 static void dupe_loader_done_cb(ImageLoader *il, gpointer data)
2208 {
2209         DupeWindow *dw = data;
2210         GdkPixbuf *pixbuf;
2211
2212         pixbuf = image_loader_get_pixbuf(il);
2213
2214         if (dw->setup_point)
2215                 {
2216                 DupeItem *di = dw->setup_point->data;
2217
2218                 if (!di->simd)
2219                         {
2220                         di->simd = image_sim_new_from_pixbuf(pixbuf);
2221                         }
2222                 else
2223                         {
2224                         image_sim_fill_data(di->simd, pixbuf);
2225                         }
2226
2227                 if (di->width == 0 && di->height == 0)
2228                         {
2229                         di->width = gdk_pixbuf_get_width(pixbuf);
2230                         di->height = gdk_pixbuf_get_height(pixbuf);
2231                         }
2232                 if (options->thumbnails.enable_caching)
2233                         {
2234                         dupe_item_write_cache(di);
2235                         }
2236
2237                 image_sim_alternate_processing(di->simd);
2238                 }
2239
2240         image_loader_free(dw->img_loader);
2241         dw->img_loader = NULL;
2242
2243         dw->idle_id = g_idle_add(dupe_check_cb, dw);
2244 }
2245
2246 static void dupe_setup_reset(DupeWindow *dw)
2247 {
2248         dw->setup_point = NULL;
2249         dw->setup_n = 0;
2250         dw->setup_time = msec_time();
2251         dw->setup_time_count = 0;
2252 }
2253
2254 static GList *dupe_setup_point_step(DupeWindow *dw, GList *p)
2255 {
2256         if (!p) return NULL;
2257
2258         if (p->next) return p->next;
2259
2260         if (dw->second_set && g_list_first(p) == dw->list) return dw->second_list;
2261
2262         return NULL;
2263 }
2264
2265 /**
2266  * @brief Generates the sumcheck or dimensions
2267  * @param list Set1 or set2
2268  * @returns TRUE/FALSE = not completed/completed
2269  * 
2270  * Ensures that the DIs contain the MD5SUM or dimensions for all items in
2271  * the list. One item at a time. Re-enters if not completed.
2272  */
2273 static gboolean create_checksums_dimensions(DupeWindow *dw, GList *list)
2274 {
2275                 if ((dw->match_mask & DUPE_MATCH_SUM) ||
2276                         (dw->match_mask & DUPE_MATCH_NAME_CONTENT) ||
2277                         (dw->match_mask & DUPE_MATCH_NAME_CI_CONTENT))
2278                         {
2279                         /* MD5SUM only */
2280                         if (!dw->setup_point) dw->setup_point = list; // setup_point clear on 1st entry
2281
2282                         while (dw->setup_point)
2283                                 {
2284                                 DupeItem *di = dw->setup_point->data;
2285
2286                                 dw->setup_point = dupe_setup_point_step(dw, dw->setup_point);
2287                                 dw->setup_n++;
2288
2289                                 if (!di->md5sum)
2290                                         {
2291                                         dupe_window_update_progress(dw, _("Reading checksums..."),
2292                                                 dw->setup_count == 0 ? 0.0 : (gdouble)(dw->setup_n - 1) / dw->setup_count, FALSE);
2293
2294                                         if (options->thumbnails.enable_caching)
2295                                                 {
2296                                                 dupe_item_read_cache(di);
2297                                                 if (di->md5sum)
2298                                                         {
2299                                                         return TRUE;
2300                                                         }
2301                                                 }
2302
2303                                         di->md5sum = md5_text_from_file_utf8(di->fd->path, "");
2304                                         if (options->thumbnails.enable_caching)
2305                                                 {
2306                                                 dupe_item_write_cache(di);
2307                                                 }
2308                                         return TRUE;
2309                                         }
2310                                 }
2311                         dupe_setup_reset(dw);
2312                         }
2313
2314                 if ((dw->match_mask & DUPE_MATCH_DIM)  )
2315                         {
2316                         /* Dimensions only */
2317                         if (!dw->setup_point) dw->setup_point = list;
2318
2319                         while (dw->setup_point)
2320                                 {
2321                                 DupeItem *di = dw->setup_point->data;
2322
2323                                 dw->setup_point = dupe_setup_point_step(dw, dw->setup_point);
2324                                 dw->setup_n++;
2325                                 if (di->width == 0 && di->height == 0)
2326                                         {
2327                                         dupe_window_update_progress(dw, _("Reading dimensions..."),
2328                                                 dw->setup_count == 0 ? 0.0 : (gdouble)(dw->setup_n - 1) / dw->setup_count, FALSE);
2329
2330                                         if (options->thumbnails.enable_caching)
2331                                                 {
2332                                                 dupe_item_read_cache(di);
2333                                                 if (di->width != 0 || di->height != 0)
2334                                                         {
2335                                                         return TRUE;
2336                                                         }
2337                                                 }
2338
2339                                         image_load_dimensions(di->fd, &di->width, &di->height);
2340                                         di->dimensions = (di->width << 16) + di->height;
2341                                         if (options->thumbnails.enable_caching)
2342                                                 {
2343                                                 dupe_item_write_cache(di);
2344                                                 }
2345                                         return TRUE;
2346                                         }
2347                                 }
2348                         dupe_setup_reset(dw);
2349                         }
2350
2351         return FALSE;
2352 }
2353
2354 /**
2355  * @brief Compare func. for sorting search matches
2356  * @param a #DupeSearchMatch
2357  * @param b #DupeSearchMatch
2358  * @returns 
2359  * 
2360  * Used only for similarity checks\n
2361  * Sorts search matches on order they were inserted into the pool queue
2362  */
2363 static gint sort_func(gconstpointer a, gconstpointer b)
2364 {
2365         return (((DupeSearchMatch *)a)->index - ((DupeSearchMatch *)b)->index);
2366 }
2367
2368 /**
2369  * @brief Check set 1 (and set 2) for matches
2370  * @param data DupeWindow
2371  * @returns TRUE/FALSE = not completed/completed
2372  * 
2373  * Initiated from start, loader done and item remove
2374  *
2375  * On first entry generates di->MD5SUM, di->dimensions and sim data,
2376  * and updates the cache.
2377  */
2378 static gboolean dupe_check_cb(gpointer data)
2379 {
2380         DupeWindow *dw = data;
2381         DupeSearchMatch *search_match_list_item;
2382
2383         if (!dw->idle_id)
2384                 {
2385                 return FALSE;
2386                 }
2387
2388         if (!dw->setup_done) /* Clear on 1st entry */
2389                 {
2390                 if (dw->list)
2391                         {
2392                         if (create_checksums_dimensions(dw, dw->list))
2393                                 {
2394                                 return TRUE;
2395                                 }
2396                         }
2397                 if (dw->second_list)
2398                         {
2399                         if (create_checksums_dimensions(dw, dw->second_list))
2400                                 {
2401                                 return TRUE;
2402                                 }
2403                         }
2404                 if ((dw->match_mask & DUPE_MATCH_SIM_HIGH ||
2405                      dw->match_mask & DUPE_MATCH_SIM_MED ||
2406                      dw->match_mask & DUPE_MATCH_SIM_LOW ||
2407                      dw->match_mask & DUPE_MATCH_SIM_CUSTOM) &&
2408                     !(dw->setup_mask & DUPE_MATCH_SIM_MED) )
2409                         {
2410                         /* Similarity only */
2411                         if (!dw->setup_point) dw->setup_point = dw->list;
2412
2413                         while (dw->setup_point)
2414                                 {
2415                                 DupeItem *di = dw->setup_point->data;
2416
2417                                 if (!di->simd)
2418                                         {
2419                                         dupe_window_update_progress(dw, _("Reading similarity data..."),
2420                                                 dw->setup_count == 0 ? 0.0 : (gdouble)dw->setup_n / dw->setup_count, FALSE);
2421
2422                                         if (options->thumbnails.enable_caching)
2423                                                 {
2424                                                 dupe_item_read_cache(di);
2425                                                 if (cache_sim_data_filled(di->simd))
2426                                                         {
2427                                                         image_sim_alternate_processing(di->simd);
2428                                                         return TRUE;
2429                                                         }
2430                                                 }
2431
2432                                         dw->img_loader = image_loader_new(di->fd);
2433                                         image_loader_set_buffer_size(dw->img_loader, 8);
2434                                         g_signal_connect(G_OBJECT(dw->img_loader), "error", (GCallback)dupe_loader_done_cb, dw);
2435                                         g_signal_connect(G_OBJECT(dw->img_loader), "done", (GCallback)dupe_loader_done_cb, dw);
2436
2437                                         if (!image_loader_start(dw->img_loader))
2438                                                 {
2439                                                 image_sim_free(di->simd);
2440                                                 di->simd = image_sim_new();
2441                                                 image_loader_free(dw->img_loader);
2442                                                 dw->img_loader = NULL;
2443                                                 return TRUE;
2444                                                 }
2445                                         dw->idle_id = 0;
2446                                         return FALSE;
2447                                         }
2448
2449                                 dw->setup_point = dupe_setup_point_step(dw, dw->setup_point);
2450                                 dw->setup_n++;
2451                                 }
2452                         dw->setup_mask |= DUPE_MATCH_SIM_MED;
2453                         dupe_setup_reset(dw);
2454                         }
2455
2456                 /* End of setup not done */
2457                 dupe_window_update_progress(dw, _("Comparing..."), 0.0, FALSE);
2458                 dw->setup_done = TRUE;
2459                 dupe_setup_reset(dw);
2460                 dw->setup_count = g_list_length(dw->list);
2461                 }
2462
2463         /* Setup done - dw->working set to NULL below
2464          * Set before 1st entry: dw->working = g_list_last(dw->list)
2465          * Set before 1st entry: dw->setup_count = g_list_length(dw->list)
2466          */
2467         if (!dw->working)
2468                 {
2469                 /* Similarity check threads may still be running */
2470                 if (dw->setup_count > 0 && (dw->match_mask == DUPE_MATCH_SIM_HIGH ||
2471                         dw->match_mask == DUPE_MATCH_SIM_MED ||
2472                         dw->match_mask == DUPE_MATCH_SIM_LOW ||
2473                         dw->match_mask == DUPE_MATCH_SIM_CUSTOM))
2474                         {
2475                         if( dw->thread_count < dw->queue_count)
2476                                 {
2477                                 dupe_window_update_progress(dw, _("Comparing..."), 0.0, FALSE);
2478
2479                                 return TRUE;
2480                                 }
2481
2482                         if (dw->search_matches_sorted == NULL)
2483                                 {
2484                                 dw->search_matches_sorted = g_list_sort(dw->search_matches, sort_func);
2485                                 dupe_setup_reset(dw);
2486                                 }
2487
2488                         while (dw->search_matches_sorted)
2489                                 {
2490                                 dw->setup_n++;
2491                                 dupe_window_update_progress(dw, _("Sorting..."), 0.0, FALSE);
2492                                 search_match_list_item = dw->search_matches_sorted->data;
2493
2494                                 if (!dupe_match_link_exists(search_match_list_item->a, search_match_list_item->b))
2495                                         {
2496                                         dupe_match_link(search_match_list_item->a, search_match_list_item->b, search_match_list_item->rank);
2497                                         }
2498
2499                                 dw->search_matches_sorted = dw->search_matches_sorted->next;
2500
2501                                 if (dw->search_matches_sorted != NULL)
2502                                         {
2503                                         return TRUE;
2504                                         }
2505                                 }
2506                         g_list_free(dw->search_matches);
2507                         dw->search_matches = NULL;
2508                         g_list_free(dw->search_matches_sorted);
2509                         dw->search_matches_sorted = NULL;
2510                         dw->setup_count = 0;
2511                         }
2512                 else
2513                         {
2514                         if (dw->setup_count > 0)
2515                                 {
2516                                 dw->setup_count = 0;
2517                                 dupe_window_update_progress(dw, _("Sorting..."), 1.0, TRUE);
2518                                 return TRUE;
2519                                 }
2520                         }
2521
2522                 dw->idle_id = 0;
2523                 dupe_window_update_progress(dw, NULL, 0.0, FALSE);
2524
2525                 dupe_match_rank(dw);
2526                 dupe_window_update_count(dw, FALSE);
2527
2528                 dupe_listview_populate(dw);
2529
2530                 /* check thumbs */
2531                 if (dw->show_thumbs) dupe_thumb_step(dw);
2532
2533                 widget_set_cursor(dw->listview, -1);
2534
2535                 return FALSE;
2536                 /* The end */
2537                 }
2538
2539         /* Setup done - working */
2540         if (dw->match_mask == DUPE_MATCH_SIM_HIGH ||
2541                 dw->match_mask == DUPE_MATCH_SIM_MED ||
2542                 dw->match_mask == DUPE_MATCH_SIM_LOW ||
2543                 dw->match_mask == DUPE_MATCH_SIM_CUSTOM)
2544                 {
2545                 /* This is the similarity comparison */
2546                 dupe_list_check_match(dw, (DupeItem *)dw->working->data, dw->working);
2547                 dupe_window_update_progress(dw, _("Queuing..."), dw->setup_count == 0 ? 0.0 : (gdouble) dw->setup_n / dw->setup_count, FALSE);
2548                 dw->setup_n++;
2549                 dw->queue_count++;
2550
2551                 dw->working = dw->working->prev; /* Is NULL when complete */
2552                 }
2553         else
2554                 {
2555                 /* This is the comparison for all other parameters.
2556                  * dupe_array_check() processes the entire list in one go
2557                 */
2558                 dw->working = NULL;
2559                 dupe_window_update_progress(dw, _("Comparing..."), 0.0, FALSE);
2560                 dupe_array_check(dw);
2561                 }
2562
2563         return TRUE;
2564 }
2565
2566 static void dupe_check_start(DupeWindow *dw)
2567 {
2568         dw->setup_done = FALSE;
2569
2570         dw->setup_count = g_list_length(dw->list);
2571         if (dw->second_set) dw->setup_count += g_list_length(dw->second_list);
2572
2573         dw->setup_mask = 0;
2574         dupe_setup_reset(dw);
2575
2576         dw->working = g_list_last(dw->list);
2577
2578         dupe_window_update_count(dw, TRUE);
2579         widget_set_cursor(dw->listview, GDK_WATCH);
2580         dw->queue_count = 0;
2581         dw->thread_count = 0;
2582         dw->search_matches_sorted = NULL;
2583         dw->abort = FALSE;
2584
2585         if (dw->idle_id) return;
2586
2587         dw->idle_id = g_idle_add(dupe_check_cb, dw);
2588 }
2589
2590 static gboolean dupe_check_start_cb(gpointer data)
2591 {
2592         DupeWindow *dw = data;
2593
2594         dupe_check_start(dw);
2595
2596         return FALSE;
2597 }
2598
2599 /*
2600  * ------------------------------------------------------------------
2601  * Item addition, removal
2602  * ------------------------------------------------------------------
2603  */
2604
2605 static void dupe_item_remove(DupeWindow *dw, DupeItem *di)
2606 {
2607         if (!di) return;
2608
2609         /* handle things that may be in progress... */
2610         if (dw->working && dw->working->data == di)
2611                 {
2612                 dw->working = dw->working->prev;
2613                 }
2614         if (dw->thumb_loader && dw->thumb_item == di)
2615                 {
2616                 dupe_thumb_step(dw);
2617                 }
2618         if (dw->setup_point && dw->setup_point->data == di)
2619                 {
2620                 dw->setup_point = dupe_setup_point_step(dw, dw->setup_point);
2621                 if (dw->img_loader)
2622                         {
2623                         image_loader_free(dw->img_loader);
2624                         dw->img_loader = NULL;
2625                         dw->idle_id = g_idle_add(dupe_check_cb, dw);
2626                         }
2627                 }
2628
2629         if (di->group && dw->dupes)
2630                 {
2631                 /* is a dupe, must remove from group/reset children if a parent */
2632                 DupeItem *parent;
2633
2634                 parent = dupe_match_find_parent(dw, di);
2635                 if (di == parent)
2636                         {
2637                         if (g_list_length(parent->group) < 2)
2638                                 {
2639                                 DupeItem *child;
2640
2641                                 child = dupe_match_highest_rank(parent);
2642                                 dupe_match_link_clear(child, TRUE);
2643                                 dupe_listview_remove(dw, child);
2644
2645                                 dupe_match_link_clear(parent, TRUE);
2646                                 dupe_listview_remove(dw, parent);
2647                                 dw->dupes = g_list_remove(dw->dupes, parent);
2648                                 }
2649                         else
2650                                 {
2651                                 DupeItem *new_parent;
2652                                 DupeMatch *dm;
2653
2654                                 dm = parent->group->data;
2655                                 new_parent = dm->di;
2656                                 dupe_match_reparent(dw, parent, new_parent);
2657                                 dupe_listview_remove(dw, parent);
2658                                 }
2659                         }
2660                 else
2661                         {
2662                         if (g_list_length(parent->group) < 2)
2663                                 {
2664                                 dupe_match_link_clear(parent, TRUE);
2665                                 dupe_listview_remove(dw, parent);
2666                                 dw->dupes = g_list_remove(dw->dupes, parent);
2667                                 }
2668                         dupe_match_link_clear(di, TRUE);
2669                         dupe_listview_remove(dw, di);
2670                         }
2671                 }
2672         else
2673                 {
2674                 /* not a dupe, or not sorted yet, simply reset */
2675                 dupe_match_link_clear(di, TRUE);
2676                 }
2677
2678         if (dw->second_list && g_list_find(dw->second_list, di))
2679                 {
2680                 dupe_second_remove(dw, di);
2681                 }
2682         else
2683                 {
2684                 dw->list = g_list_remove(dw->list, di);
2685                 }
2686         dupe_item_free(di);
2687
2688         dupe_window_update_count(dw, FALSE);
2689 }
2690
2691 static gboolean dupe_item_remove_by_path(DupeWindow *dw, const gchar *path)
2692 {
2693         DupeItem *di;
2694
2695         di = dupe_item_find_path(dw, path);
2696         if (!di) return FALSE;
2697
2698         dupe_item_remove(dw, di);
2699
2700         return TRUE;
2701 }
2702
2703 static gboolean dupe_files_add_queue_cb(gpointer data)
2704 {
2705         DupeItem *di = NULL;
2706         DupeWindow *dw = data;
2707         FileData *fd;
2708         GList *queue = dw->add_files_queue;
2709
2710         gtk_progress_bar_pulse(GTK_PROGRESS_BAR(dw->extra_label));
2711
2712         if (queue == NULL)
2713                 {
2714                 dw->add_files_queue_id = 0;
2715                 dupe_destroy_list_cache(dw);
2716                 g_idle_add(dupe_check_start_cb, dw);
2717                 gtk_widget_set_sensitive(dw->controls_box, TRUE);
2718                 return FALSE;
2719                 }
2720
2721         fd = queue->data;
2722         if (fd)
2723                 {
2724                 if (isfile(fd->path))
2725                         {
2726                         di = dupe_item_new(fd);
2727                         }
2728                 else if (isdir(fd->path))
2729                         {
2730                         GList *f, *d;
2731                         dw->add_files_queue = g_list_remove(dw->add_files_queue, g_list_first(dw->add_files_queue)->data);
2732
2733                         if (filelist_read(fd, &f, &d))
2734                                 {
2735                                 f = filelist_filter(f, FALSE);
2736                                 d = filelist_filter(d, TRUE);
2737
2738                                 dw->add_files_queue = g_list_concat(f, dw->add_files_queue);
2739                                 dw->add_files_queue = g_list_concat(d, dw->add_files_queue);
2740                                 }
2741                         }
2742                 else
2743                         {
2744                         /* Not a file and not a dir */
2745                         dw->add_files_queue = g_list_remove(dw->add_files_queue, g_list_first(dw->add_files_queue)->data);
2746                         }
2747                 }
2748
2749         if (!di)
2750                 {
2751                 /* A dir was found. Process the contents on next entry */
2752                 return TRUE;
2753                 }
2754
2755         dw->add_files_queue = g_list_remove(dw->add_files_queue, g_list_first(dw->add_files_queue)->data);
2756
2757         dupe_item_read_cache(di);
2758
2759         /* Ensure images in the lists have unique FileDatas */
2760         if (!dupe_insert_in_list_cache(dw, di->fd))
2761                 {
2762                 dupe_item_free(di);
2763                 return TRUE;
2764                 }
2765
2766         if (dw->second_drop)
2767                 {
2768                 dupe_second_add(dw, di);
2769                 }
2770         else
2771                 {
2772                 dw->list = g_list_prepend(dw->list, di);
2773                 }
2774
2775         if (dw->add_files_queue != NULL)
2776                 {
2777                 return TRUE;
2778                 }
2779         else
2780                 {
2781                 dw->add_files_queue_id = 0;
2782                 dupe_destroy_list_cache(dw);
2783                 g_idle_add(dupe_check_start_cb, dw);
2784                 gtk_widget_set_sensitive(dw->controls_box, TRUE);
2785                 return FALSE;
2786                 }
2787 }
2788
2789 static void dupe_files_add(DupeWindow *dw, CollectionData *collection, CollectInfo *info,
2790                            FileData *fd, gboolean recurse)
2791 {
2792         DupeItem *di = NULL;
2793
2794         if (info)
2795                 {
2796                 di = dupe_item_new(info->fd);
2797                 }
2798         else if (fd)
2799                 {
2800                 if (isfile(fd->path) && !g_file_test(fd->path, G_FILE_TEST_IS_SYMLINK))
2801                         {
2802                         di = dupe_item_new(fd);
2803                         }
2804                 else if (isdir(fd->path) && recurse)
2805                         {
2806                         GList *f, *d;
2807                         if (filelist_read(fd, &f, &d))
2808                                 {
2809                                 GList *work;
2810
2811                                 f = filelist_filter(f, FALSE);
2812                                 d = filelist_filter(d, TRUE);
2813
2814                                 work = f;
2815                                 while (work)
2816                                         {
2817                                         dupe_files_add(dw, NULL, NULL, (FileData *)work->data, TRUE);
2818                                         work = work->next;
2819                                         }
2820                                 filelist_free(f);
2821                                 work = d;
2822                                 while (work)
2823                                         {
2824                                         dupe_files_add(dw, NULL, NULL, (FileData *)work->data, TRUE);
2825                                         work = work->next;
2826                                         }
2827                                 filelist_free(d);
2828                                 }
2829                         }
2830                 }
2831
2832         if (!di) return;
2833
2834         dupe_item_read_cache(di);
2835
2836         /* Ensure images in the lists have unique FileDatas */
2837         GList *work;
2838         DupeItem *di_list;
2839         work = g_list_first(dw->list);
2840         while (work)
2841                 {
2842                 di_list = work->data;
2843                 if (di_list->fd == di->fd)
2844                         {
2845                         return;
2846                         }
2847                 else
2848                         {
2849                         work = work->next;
2850                         }
2851                 }
2852
2853         if (dw->second_list)
2854                 {
2855                 work = g_list_first(dw->second_list);
2856                 while (work)
2857                         {
2858                         di_list = work->data;
2859                         if (di_list->fd == di->fd)
2860                                 {
2861                                 return;
2862                                 }
2863                         else
2864                                 {
2865                                 work = work->next;
2866                                 }
2867                         }
2868                 }
2869
2870         if (dw->second_drop)
2871                 {
2872                 dupe_second_add(dw, di);
2873                 }
2874         else
2875                 {
2876                 dw->list = g_list_prepend(dw->list, di);
2877                 }
2878 }
2879
2880 static void dupe_init_list_cache(DupeWindow *dw)
2881 {
2882         dw->list_cache = g_hash_table_new(g_direct_hash, g_direct_equal);
2883         dw->second_list_cache = g_hash_table_new(g_direct_hash, g_direct_equal);
2884
2885         for (GList *i = dw->list; i != NULL; i = i->next)
2886                 {
2887                         DupeItem *di = i->data;
2888
2889                         g_hash_table_add(dw->list_cache, di->fd);
2890                 }
2891
2892         for (GList *i = dw->second_list; i != NULL; i = i->next)
2893                 {
2894                         DupeItem *di = i->data;
2895
2896                         g_hash_table_add(dw->second_list_cache, di->fd);
2897                 }
2898 }
2899
2900 static void dupe_destroy_list_cache(DupeWindow *dw)
2901 {
2902         g_hash_table_destroy(dw->list_cache);
2903         g_hash_table_destroy(dw->second_list_cache);
2904 }
2905
2906 /**
2907  * @brief Return true if the fd was not in the cache
2908  * @param dw 
2909  * @param fd 
2910  * @returns 
2911  * 
2912  * 
2913  */
2914 static gboolean dupe_insert_in_list_cache(DupeWindow *dw, FileData *fd)
2915 {
2916         GHashTable *table =
2917                 dw->second_drop ? dw->second_list_cache : dw->list_cache;
2918         /* We do this as a lookup + add as we don't want to overwrite
2919            items as that would leak the old value. */
2920         if (g_hash_table_lookup(table, fd) != NULL)
2921                 return FALSE;
2922         return g_hash_table_add(table, fd);
2923 }
2924
2925 void dupe_window_add_collection(DupeWindow *dw, CollectionData *collection)
2926 {
2927         CollectInfo *info;
2928
2929         info = collection_get_first(collection);
2930         while (info)
2931                 {
2932                 dupe_files_add(dw, collection, info, NULL, FALSE);
2933                 info = collection_next_by_info(collection, info);
2934                 }
2935
2936         dupe_check_start(dw);
2937 }
2938
2939 void dupe_window_add_files(DupeWindow *dw, GList *list, gboolean recurse)
2940 {
2941         GList *work;
2942
2943         work = list;
2944         while (work)
2945                 {
2946                 FileData *fd = work->data;
2947                 work = work->next;
2948                 if (isdir(fd->path) && !recurse)
2949                         {
2950                         GList *f, *d;
2951
2952                         if (filelist_read(fd, &f, &d))
2953                                 {
2954                                 GList *work_file;
2955                                 work_file = f;
2956
2957                                 while (work_file)
2958                                         {
2959                                         /* Add only the files, ignore the dirs when no recurse */
2960                                         dw->add_files_queue = g_list_prepend(dw->add_files_queue, work_file->data);
2961                                         work_file = work_file->next;
2962                                         }
2963                                 g_list_free(f);
2964                                 g_list_free(d);
2965                                 }
2966                         }
2967                 else
2968                         {
2969                         dw->add_files_queue = g_list_prepend(dw->add_files_queue, fd);
2970                         }
2971                 }
2972         if (dw->add_files_queue_id == 0)
2973                 {
2974                 gtk_progress_bar_pulse(GTK_PROGRESS_BAR(dw->extra_label));
2975                 gtk_progress_bar_set_pulse_step(GTK_PROGRESS_BAR(dw->extra_label), DUPE_PROGRESS_PULSE_STEP);
2976                 gtk_progress_bar_set_text(GTK_PROGRESS_BAR(dw->extra_label), _("Loading file list"));
2977
2978                 dupe_init_list_cache(dw);
2979                 dw->add_files_queue_id = g_idle_add(dupe_files_add_queue_cb, dw);
2980                 gtk_widget_set_sensitive(dw->controls_box, FALSE);
2981                 }
2982 }
2983
2984 static void dupe_item_update(DupeWindow *dw, DupeItem *di)
2985 {
2986         if ( (dw->match_mask & DUPE_MATCH_NAME) || (dw->match_mask & DUPE_MATCH_PATH || (dw->match_mask & DUPE_MATCH_NAME_CI)) )
2987                 {
2988                 /* only effects matches on name or path */
2989 /*
2990                 FileData *fd = file_data_ref(di->fd);
2991                 gint second;
2992
2993                 second = di->second;
2994                 dupe_item_remove(dw, di);
2995
2996                 dw->second_drop = second;
2997                 dupe_files_add(dw, NULL, NULL, fd, FALSE);
2998                 dw->second_drop = FALSE;
2999
3000                 file_data_unref(fd);
3001 */
3002                 dupe_check_start(dw);
3003                 }
3004         else
3005                 {
3006                 GtkListStore *store;
3007                 GtkTreeIter iter;
3008                 gint row;
3009                 /* update the listview(s) */
3010
3011                 store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->listview)));
3012                 row = dupe_listview_find_item(store, di, &iter);
3013                 if (row >= 0)
3014                         {
3015                         gtk_list_store_set(store, &iter,
3016                                            DUPE_COLUMN_NAME, di->fd->name,
3017                                            DUPE_COLUMN_PATH, di->fd->path, -1);
3018                         }
3019
3020                 if (dw->second_listview)
3021                         {
3022                         store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->second_listview)));
3023                         row = dupe_listview_find_item(store, di, &iter);
3024                         if (row >= 0)
3025                                 {
3026                                 gtk_list_store_set(store, &iter, 1, di->fd->path, -1);
3027                                 }
3028                         }
3029                 }
3030
3031 }
3032
3033 static void dupe_item_update_fd_in_list(DupeWindow *dw, FileData *fd, GList *work)
3034 {
3035         while (work)
3036                 {
3037                 DupeItem *di = work->data;
3038
3039                 if (di->fd == fd)
3040                         dupe_item_update(dw, di);
3041
3042                 work = work->next;
3043                 }
3044 }
3045
3046 static void dupe_item_update_fd(DupeWindow *dw, FileData *fd)
3047 {
3048         dupe_item_update_fd_in_list(dw, fd, dw->list);
3049         if (dw->second_set) dupe_item_update_fd_in_list(dw, fd, dw->second_list);
3050 }
3051
3052
3053 /*
3054  * ------------------------------------------------------------------
3055  * Misc.
3056  * ------------------------------------------------------------------
3057  */
3058
3059 static GtkWidget *dupe_display_label(GtkWidget *vbox, const gchar *description, const gchar *text)
3060 {
3061         GtkWidget *hbox;
3062         GtkWidget *label;
3063
3064         hbox = gtk_hbox_new(FALSE, 10);
3065
3066         label = gtk_label_new(description);
3067         gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 0);
3068         gtk_widget_show(label);
3069
3070         label = gtk_label_new(text);
3071         gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 0);
3072         gtk_widget_show(label);
3073
3074         gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0);
3075         gtk_widget_show(hbox);
3076
3077         return label;
3078 }
3079
3080 static void dupe_display_stats(DupeWindow *dw, DupeItem *di)
3081 {
3082         GenericDialog *gd;
3083         gchar *buf;
3084
3085         if (!di) return;
3086
3087         gd = file_util_gen_dlg("Image thumbprint debug info", "thumbprint",
3088                                dw->window, TRUE,
3089                                NULL, NULL);
3090         generic_dialog_add_button(gd, GTK_STOCK_CLOSE, NULL, NULL, TRUE);
3091
3092         dupe_display_label(gd->vbox, "name:", di->fd->name);
3093         buf = text_from_size(di->fd->size);
3094         dupe_display_label(gd->vbox, "size:", buf);
3095         g_free(buf);
3096         dupe_display_label(gd->vbox, "date:", text_from_time(di->fd->date));
3097         buf = g_strdup_printf("%d x %d", di->width, di->height);
3098         dupe_display_label(gd->vbox, "dimensions:", buf);
3099         g_free(buf);
3100         dupe_display_label(gd->vbox, "md5sum:", (di->md5sum) ? di->md5sum : "not generated");
3101
3102         dupe_display_label(gd->vbox, "thumbprint:", (di->simd) ? "" : "not generated");
3103         if (di->simd)
3104                 {
3105                 GtkWidget *image;
3106                 GdkPixbuf *pixbuf;
3107                 gint x, y;
3108                 guchar *d_pix;
3109                 guchar *dp;
3110                 gint rs;
3111                 gint sp;
3112
3113                 pixbuf = gdk_pixbuf_new(GDK_COLORSPACE_RGB, FALSE, 8, 32, 32);
3114                 rs = gdk_pixbuf_get_rowstride(pixbuf);
3115                 d_pix = gdk_pixbuf_get_pixels(pixbuf);
3116
3117                 for (y = 0; y < 32; y++)
3118                         {
3119                         dp = d_pix + (y * rs);
3120                         sp = y * 32;
3121                         for (x = 0; x < 32; x++)
3122                                 {
3123                                 *(dp++) = di->simd->avg_r[sp + x];
3124                                 *(dp++) = di->simd->avg_g[sp + x];
3125                                 *(dp++) = di->simd->avg_b[sp + x];
3126                                 }
3127                         }
3128
3129                 image = gtk_image_new_from_pixbuf(pixbuf);
3130                 gtk_box_pack_start(GTK_BOX(gd->vbox), image, FALSE, FALSE, 0);
3131                 gtk_widget_show(image);
3132
3133                 g_object_unref(pixbuf);
3134                 }
3135
3136         gtk_widget_show(gd->dialog);
3137 }
3138
3139 static void dupe_window_recompare(DupeWindow *dw)
3140 {
3141         GtkListStore *store;
3142
3143         dupe_check_stop(dw);
3144
3145         store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->listview)));
3146         gtk_list_store_clear(store);
3147
3148         g_list_free(dw->dupes);
3149         dw->dupes = NULL;
3150
3151         dupe_match_reset_list(dw->list);
3152         dupe_match_reset_list(dw->second_list);
3153         dw->set_count = 0;
3154
3155         dupe_check_start(dw);
3156 }
3157
3158 static void dupe_menu_view(DupeWindow *dw, DupeItem *di, GtkWidget *listview, gint new_window)
3159 {
3160         if (!di) return;
3161
3162         if (di->collection && collection_info_valid(di->collection, di->info))
3163                 {
3164                 if (new_window)
3165                         {
3166                         view_window_new_from_collection(di->collection, di->info);
3167                         }
3168                 else
3169                         {
3170                         layout_image_set_collection(NULL, di->collection, di->info);
3171                         }
3172                 }
3173         else
3174                 {
3175                 if (new_window)
3176                         {
3177                         GList *list;
3178
3179                         list = dupe_listview_get_selection(dw, listview);
3180                         view_window_new_from_list(list);
3181                         filelist_free(list);
3182                         }
3183                 else
3184                         {
3185                         layout_set_fd(NULL, di->fd);
3186                         }
3187                 }
3188 }
3189
3190 static void dupe_window_remove_selection(DupeWindow *dw, GtkWidget *listview)
3191 {
3192         GtkTreeSelection *selection;
3193         GtkTreeModel *store;
3194         GtkTreeIter iter;
3195         GList *slist;
3196         GList *list = NULL;
3197         GList *work;
3198
3199         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(listview));
3200         slist = gtk_tree_selection_get_selected_rows(selection, &store);
3201         work = slist;
3202         while (work)
3203                 {
3204                 GtkTreePath *tpath = work->data;
3205                 DupeItem *di = NULL;
3206
3207                 gtk_tree_model_get_iter(store, &iter, tpath);
3208                 gtk_tree_model_get(store, &iter, DUPE_COLUMN_POINTER, &di, -1);
3209                 if (di) list = g_list_prepend(list, di);
3210                 work = work->next;
3211                 }
3212         g_list_foreach(slist, (GFunc)tree_path_free_wrapper, NULL);
3213         g_list_free(slist);
3214
3215         dw->color_frozen = TRUE;
3216         work = list;
3217         while (work)
3218                 {
3219                 DupeItem *di;
3220
3221                 di = work->data;
3222                 work = work->next;
3223                 dupe_item_remove(dw, di);
3224                 }
3225         dw->color_frozen = FALSE;
3226
3227         g_list_free(list);
3228
3229         dupe_listview_realign_colors(dw);
3230 }
3231
3232 static void dupe_window_edit_selected(DupeWindow *dw, const gchar *key)
3233 {
3234         file_util_start_editor_from_filelist(key, dupe_listview_get_selection(dw, dw->listview), NULL, dw->window);
3235 }
3236
3237 static void dupe_window_collection_from_selection(DupeWindow *dw)
3238 {
3239         CollectWindow *w;
3240         GList *list;
3241
3242         list = dupe_listview_get_selection(dw, dw->listview);
3243         w = collection_window_new(NULL);
3244         collection_table_add_filelist(w->table, list);
3245         filelist_free(list);
3246 }
3247
3248 static void dupe_window_append_file_list(DupeWindow *dw, gint on_second)
3249 {
3250         GList *list;
3251
3252         dw->second_drop = (dw->second_set && on_second);
3253
3254         list = layout_list(NULL);
3255         dupe_window_add_files(dw, list, FALSE);
3256         filelist_free(list);
3257 }
3258
3259 /*
3260  *-------------------------------------------------------------------
3261  * main pop-up menu callbacks
3262  *-------------------------------------------------------------------
3263  */
3264
3265 static void dupe_menu_view_cb(GtkWidget *widget, gpointer data)
3266 {
3267         DupeWindow *dw = data;
3268
3269         if (dw->click_item) dupe_menu_view(dw, dw->click_item, dw->listview, FALSE);
3270 }
3271
3272 static void dupe_menu_viewnew_cb(GtkWidget *widget, gpointer data)
3273 {
3274         DupeWindow *dw = data;
3275
3276         if (dw->click_item) dupe_menu_view(dw, dw->click_item, dw->listview, TRUE);
3277 }
3278
3279 static void dupe_menu_select_all_cb(GtkWidget *widget, gpointer data)
3280 {
3281         DupeWindow *dw = data;
3282         GtkTreeSelection *selection;
3283
3284         options->duplicates_select_type = DUPE_SELECT_NONE;
3285         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(dw->listview));
3286         gtk_tree_selection_select_all(selection);
3287 }
3288
3289 static void dupe_menu_select_none_cb(GtkWidget *widget, gpointer data)
3290 {
3291         DupeWindow *dw = data;
3292         GtkTreeSelection *selection;
3293
3294         options->duplicates_select_type = DUPE_SELECT_NONE;
3295         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(dw->listview));
3296         gtk_tree_selection_unselect_all(selection);
3297 }
3298
3299 static void dupe_menu_select_dupes_set1_cb(GtkWidget *widget, gpointer data)
3300 {
3301         DupeWindow *dw = data;
3302
3303         options->duplicates_select_type = DUPE_SELECT_GROUP1;
3304         dupe_listview_select_dupes(dw, DUPE_SELECT_GROUP1);
3305 }
3306
3307 static void dupe_menu_select_dupes_set2_cb(GtkWidget *widget, gpointer data)
3308 {
3309         DupeWindow *dw = data;
3310
3311         options->duplicates_select_type = DUPE_SELECT_GROUP2;
3312         dupe_listview_select_dupes(dw, DUPE_SELECT_GROUP2);
3313 }
3314
3315 static void dupe_menu_edit_cb(GtkWidget *widget, gpointer data)
3316 {
3317         DupeWindow *dw;
3318         const gchar *key = data;
3319
3320         dw = submenu_item_get_data(widget);
3321         if (!dw) return;
3322
3323         dupe_window_edit_selected(dw, key);
3324 }
3325
3326 static void dupe_menu_print_cb(GtkWidget *widget, gpointer data)
3327 {
3328         DupeWindow *dw = data;
3329         FileData *fd;
3330
3331         fd = (dw->click_item) ? dw->click_item->fd : NULL;
3332
3333         print_window_new(fd,
3334                          dupe_listview_get_selection(dw, dw->listview),
3335                          dupe_listview_get_filelist(dw, dw->listview), dw->window);
3336 }
3337
3338 static void dupe_menu_copy_cb(GtkWidget *widget, gpointer data)
3339 {
3340         DupeWindow *dw = data;
3341
3342         file_util_copy(NULL, dupe_listview_get_selection(dw, dw->listview), NULL, dw->window);
3343 }
3344
3345 static void dupe_menu_move_cb(GtkWidget *widget, gpointer data)
3346 {
3347         DupeWindow *dw = data;
3348
3349         file_util_move(NULL, dupe_listview_get_selection(dw, dw->listview), NULL, dw->window);
3350 }
3351
3352 static void dupe_menu_rename_cb(GtkWidget *widget, gpointer data)
3353 {
3354         DupeWindow *dw = data;
3355
3356         file_util_rename(NULL, dupe_listview_get_selection(dw, dw->listview), dw->window);
3357 }
3358
3359 static void dupe_menu_delete_cb(GtkWidget *widget, gpointer data)
3360 {
3361         DupeWindow *dw = data;
3362
3363         options->file_ops.safe_delete_enable = FALSE;
3364         file_util_delete(NULL, dupe_listview_get_selection(dw, dw->listview), dw->window);
3365 }
3366
3367 static void dupe_menu_move_to_trash_cb(GtkWidget *widget, gpointer data)
3368 {
3369         DupeWindow *dw = data;
3370
3371         options->file_ops.safe_delete_enable = TRUE;
3372         file_util_delete(NULL, dupe_listview_get_selection(dw, dw->listview), dw->window);
3373 }
3374
3375 static void dupe_menu_copy_path_cb(GtkWidget *widget, gpointer data)
3376 {
3377         DupeWindow *dw = data;
3378
3379         file_util_copy_path_list_to_clipboard(dupe_listview_get_selection(dw, dw->listview), TRUE);
3380 }
3381
3382 static void dupe_menu_copy_path_unquoted_cb(GtkWidget *widget, gpointer data)
3383 {
3384         DupeWindow *dw = data;
3385
3386         file_util_copy_path_list_to_clipboard(dupe_listview_get_selection(dw, dw->listview), FALSE);
3387 }
3388
3389 static void dupe_menu_remove_cb(GtkWidget *widget, gpointer data)
3390 {
3391         DupeWindow *dw = data;
3392
3393         dupe_window_remove_selection(dw, dw->listview);
3394 }
3395
3396 static void dupe_menu_clear_cb(GtkWidget *widget, gpointer data)
3397 {
3398         DupeWindow *dw = data;
3399
3400         dupe_window_clear(dw);
3401 }
3402
3403 static void dupe_menu_close_cb(GtkWidget *widget, gpointer data)
3404 {
3405         DupeWindow *dw = data;
3406
3407         dupe_window_close(dw);
3408 }
3409
3410 static void dupe_menu_popup_destroy_cb(GtkWidget *widget, gpointer data)
3411 {
3412         GList *editmenu_fd_list = data;
3413
3414         filelist_free(editmenu_fd_list);
3415 }
3416
3417 static GList *dupe_window_get_fd_list(DupeWindow *dw)
3418 {
3419         GList *list;
3420
3421         if (gtk_widget_has_focus(dw->second_listview))
3422                 {
3423                 list = dupe_listview_get_selection(dw, dw->second_listview);
3424                 }
3425         else
3426                 {
3427                 list = dupe_listview_get_selection(dw, dw->listview);
3428                 }
3429
3430         return list;
3431 }
3432
3433 /**
3434  * @brief Add file selection list to a collection
3435  * @param[in] widget 
3436  * @param[in] data Index to the collection list menu item selected, or -1 for new collection
3437  * 
3438  * 
3439  */
3440 static void dupe_pop_menu_collections_cb(GtkWidget *widget, gpointer data)
3441 {
3442         DupeWindow *dw;
3443         GList *selection_list;
3444
3445         dw = submenu_item_get_data(widget);
3446         selection_list = dupe_listview_get_selection(dw, dw->listview);
3447         pop_menu_collections(selection_list, data);
3448
3449         filelist_free(selection_list);
3450 }
3451
3452 static GtkWidget *dupe_menu_popup_main(DupeWindow *dw, DupeItem *di)
3453 {
3454         GtkWidget *menu;
3455         GtkWidget *item;
3456         gint on_row;
3457         GList *editmenu_fd_list;
3458
3459         on_row = (di != NULL);
3460
3461         menu = popup_menu_short_lived();
3462
3463         menu_item_add_sensitive(menu, _("_View"), on_row,
3464                                 G_CALLBACK(dupe_menu_view_cb), dw);
3465         menu_item_add_stock_sensitive(menu, _("View in _new window"), GTK_STOCK_NEW, on_row,
3466                                 G_CALLBACK(dupe_menu_viewnew_cb), dw);
3467         menu_item_add_divider(menu);
3468         menu_item_add_sensitive(menu, _("Select all"), (dw->dupes != NULL),
3469                                 G_CALLBACK(dupe_menu_select_all_cb), dw);
3470         menu_item_add_sensitive(menu, _("Select none"), (dw->dupes != NULL),
3471                                 G_CALLBACK(dupe_menu_select_none_cb), dw);
3472         menu_item_add_sensitive(menu, _("Select group _1 duplicates"), (dw->dupes != NULL),
3473                                 G_CALLBACK(dupe_menu_select_dupes_set1_cb), dw);
3474         menu_item_add_sensitive(menu, _("Select group _2 duplicates"), (dw->dupes != NULL),
3475                                 G_CALLBACK(dupe_menu_select_dupes_set2_cb), dw);
3476         menu_item_add_divider(menu);
3477
3478         submenu_add_export(menu, &item, G_CALLBACK(dupe_pop_menu_export_cb), dw);
3479         gtk_widget_set_sensitive(item, on_row);
3480         menu_item_add_divider(menu);
3481
3482         editmenu_fd_list = dupe_window_get_fd_list(dw);
3483         g_signal_connect(G_OBJECT(menu), "destroy",
3484                          G_CALLBACK(dupe_menu_popup_destroy_cb), editmenu_fd_list);
3485         submenu_add_edit(menu, &item, G_CALLBACK(dupe_menu_edit_cb), dw, editmenu_fd_list);
3486         if (!on_row) gtk_widget_set_sensitive(item, FALSE);
3487
3488         submenu_add_collections(menu, &item,
3489                                                                 G_CALLBACK(dupe_pop_menu_collections_cb), dw);
3490         gtk_widget_set_sensitive(item, on_row);
3491
3492         menu_item_add_stock_sensitive(menu, _("Print..."), GTK_STOCK_PRINT, on_row,
3493                                 G_CALLBACK(dupe_menu_print_cb), dw);
3494         menu_item_add_divider(menu);
3495         menu_item_add_stock_sensitive(menu, _("_Copy..."), GTK_STOCK_COPY, on_row,
3496                                 G_CALLBACK(dupe_menu_copy_cb), dw);
3497         menu_item_add_sensitive(menu, _("_Move..."), on_row,
3498                                 G_CALLBACK(dupe_menu_move_cb), dw);
3499         menu_item_add_sensitive(menu, _("_Rename..."), on_row,
3500                                 G_CALLBACK(dupe_menu_rename_cb), dw);
3501         menu_item_add_sensitive(menu, _("_Copy path"), on_row,
3502                                 G_CALLBACK(dupe_menu_copy_path_cb), dw);
3503         menu_item_add_sensitive(menu, _("_Copy path unquoted"), on_row,
3504                                 G_CALLBACK(dupe_menu_copy_path_unquoted_cb), dw);
3505
3506         menu_item_add_divider(menu);
3507         menu_item_add_stock_sensitive(menu,
3508                                 options->file_ops.confirm_move_to_trash ? _("Move to Trash...") :
3509                                         _("Move to Trash"), PIXBUF_INLINE_ICON_TRASH, on_row,
3510                                 G_CALLBACK(dupe_menu_move_to_trash_cb), dw);
3511         menu_item_add_stock_sensitive(menu,
3512                                 options->file_ops.confirm_delete ? _("_Delete...") :
3513                                         _("_Delete"), GTK_STOCK_DELETE, on_row,
3514                                 G_CALLBACK(dupe_menu_delete_cb), dw);
3515
3516         menu_item_add_divider(menu);
3517         menu_item_add_stock_sensitive(menu, _("Rem_ove"), GTK_STOCK_REMOVE, on_row,
3518                                 G_CALLBACK(dupe_menu_remove_cb), dw);
3519         menu_item_add_stock_sensitive(menu, _("C_lear"), GTK_STOCK_CLEAR, (dw->list != NULL),
3520                                 G_CALLBACK(dupe_menu_clear_cb), dw);
3521         menu_item_add_divider(menu);
3522         menu_item_add_stock(menu, _("Close _window"), GTK_STOCK_CLOSE,
3523                             G_CALLBACK(dupe_menu_close_cb), dw);
3524
3525         return menu;
3526 }
3527
3528 static gboolean dupe_listview_press_cb(GtkWidget *widget, GdkEventButton *bevent, gpointer data)
3529 {
3530         DupeWindow *dw = data;
3531         GtkTreeModel *store;
3532         GtkTreePath *tpath;
3533         GtkTreeIter iter;
3534         DupeItem *di = NULL;
3535
3536         store = gtk_tree_view_get_model(GTK_TREE_VIEW(widget));
3537
3538         if (gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(widget), bevent->x, bevent->y,
3539                                           &tpath, NULL, NULL, NULL))
3540                 {
3541                 gtk_tree_model_get_iter(store, &iter, tpath);
3542                 gtk_tree_model_get(store, &iter, DUPE_COLUMN_POINTER, &di, -1);
3543                 gtk_tree_path_free(tpath);
3544                 }
3545
3546         dw->click_item = di;
3547
3548         if (bevent->button == MOUSE_BUTTON_RIGHT)
3549                 {
3550                 /* right click menu */
3551                 GtkWidget *menu;
3552
3553                 if (bevent->state & GDK_CONTROL_MASK && bevent->state & GDK_SHIFT_MASK)
3554                         {
3555                         dupe_display_stats(dw, di);
3556                         return TRUE;
3557                         }
3558                 if (widget == dw->listview)
3559                         {
3560                         menu = dupe_menu_popup_main(dw, di);
3561                         }
3562                 else
3563                         {
3564                         menu = dupe_menu_popup_second(dw, di);
3565                         }
3566                 gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL, bevent->button, bevent->time);
3567                 }
3568
3569         if (!di) return FALSE;
3570
3571         if (bevent->button == MOUSE_BUTTON_LEFT &&
3572             bevent->type == GDK_2BUTTON_PRESS)
3573                 {
3574                 dupe_menu_view(dw, di, widget, FALSE);
3575                 }
3576
3577         if (bevent->button == MOUSE_BUTTON_MIDDLE) return TRUE;
3578
3579         if (bevent->button == MOUSE_BUTTON_RIGHT)
3580                 {
3581                 if (!dupe_listview_item_is_selected(dw, di, widget))
3582                         {
3583                         GtkTreeSelection *selection;
3584
3585                         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(widget));
3586                         gtk_tree_selection_unselect_all(selection);
3587                         gtk_tree_selection_select_iter(selection, &iter);
3588
3589                         tpath = gtk_tree_model_get_path(GTK_TREE_MODEL(store), &iter);
3590                         gtk_tree_view_set_cursor(GTK_TREE_VIEW(widget), tpath, NULL, FALSE);
3591                         gtk_tree_path_free(tpath);
3592                         }
3593
3594                 return TRUE;
3595                 }
3596
3597         if (bevent->button == MOUSE_BUTTON_LEFT &&
3598             bevent->type == GDK_BUTTON_PRESS &&
3599             !(bevent->state & GDK_SHIFT_MASK ) &&
3600             !(bevent->state & GDK_CONTROL_MASK ) &&
3601             dupe_listview_item_is_selected(dw, di, widget))
3602                 {
3603                 /* this selection handled on release_cb */
3604                 gtk_widget_grab_focus(widget);
3605                 return TRUE;
3606                 }
3607
3608         return FALSE;
3609 }
3610
3611 static gboolean dupe_listview_release_cb(GtkWidget *widget, GdkEventButton *bevent, gpointer data)
3612 {
3613         DupeWindow *dw = data;
3614         GtkTreeModel *store;
3615         GtkTreePath *tpath;
3616         GtkTreeIter iter;
3617         DupeItem *di = NULL;
3618
3619         if (bevent->button != MOUSE_BUTTON_LEFT && bevent->button != MOUSE_BUTTON_MIDDLE) return TRUE;
3620
3621         store = gtk_tree_view_get_model(GTK_TREE_VIEW(widget));
3622
3623         if ((bevent->x != 0 || bevent->y != 0) &&
3624             gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(widget), bevent->x, bevent->y,
3625                                           &tpath, NULL, NULL, NULL))
3626                 {
3627                 gtk_tree_model_get_iter(store, &iter, tpath);
3628                 gtk_tree_model_get(store, &iter, DUPE_COLUMN_POINTER, &di, -1);
3629                 gtk_tree_path_free(tpath);
3630                 }
3631
3632         if (bevent->button == MOUSE_BUTTON_MIDDLE)
3633                 {
3634                 if (di && dw->click_item == di)
3635                         {
3636                         GtkTreeSelection *selection;
3637
3638                         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(widget));
3639                         if (dupe_listview_item_is_selected(dw, di, widget))
3640                                 {
3641                                 gtk_tree_selection_unselect_iter(selection, &iter);
3642                                 }
3643                         else
3644                                 {
3645                                 gtk_tree_selection_select_iter(selection, &iter);
3646                                 }
3647                         }
3648                 return TRUE;
3649                 }
3650
3651         if (di && dw->click_item == di &&
3652             !(bevent->state & GDK_SHIFT_MASK ) &&
3653             !(bevent->state & GDK_CONTROL_MASK ) &&
3654             dupe_listview_item_is_selected(dw, di, widget))
3655                 {
3656                 GtkTreeSelection *selection;
3657
3658                 selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(widget));
3659                 gtk_tree_selection_unselect_all(selection);
3660                 gtk_tree_selection_select_iter(selection, &iter);
3661
3662                 tpath = gtk_tree_model_get_path(store, &iter);
3663                 gtk_tree_view_set_cursor(GTK_TREE_VIEW(widget), tpath, NULL, FALSE);
3664                 gtk_tree_path_free(tpath);
3665
3666                 return TRUE;
3667                 }
3668
3669         return FALSE;
3670 }
3671
3672 /*
3673  *-------------------------------------------------------------------
3674  * second set stuff
3675  *-------------------------------------------------------------------
3676  */
3677
3678 static void dupe_second_update_status(DupeWindow *dw)
3679 {
3680         gchar *buf;
3681
3682         buf = g_strdup_printf(_("%d files (set 2)"), g_list_length(dw->second_list));
3683         gtk_label_set_text(GTK_LABEL(dw->second_status_label), buf);
3684         g_free(buf);
3685 }
3686
3687 static void dupe_second_add(DupeWindow *dw, DupeItem *di)
3688 {
3689         GtkListStore *store;
3690         GtkTreeIter iter;
3691
3692         if (!di) return;
3693
3694         di->second = TRUE;
3695         dw->second_list = g_list_prepend(dw->second_list, di);
3696
3697         store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->second_listview)));
3698         gtk_list_store_append(store, &iter);
3699         gtk_list_store_set(store, &iter, DUPE_COLUMN_POINTER, di, 1, di->fd->path, -1);
3700
3701         dupe_second_update_status(dw);
3702 }
3703
3704 static void dupe_second_remove(DupeWindow *dw, DupeItem *di)
3705 {
3706         GtkListStore *store;
3707         GtkTreeIter iter;
3708
3709         store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->second_listview)));
3710         if (dupe_listview_find_item(store, di, &iter) >= 0)
3711                 {
3712                 tree_view_move_cursor_away(GTK_TREE_VIEW(dw->second_listview), &iter, TRUE);
3713                 gtk_list_store_remove(store, &iter);
3714                 }
3715
3716         dw->second_list = g_list_remove(dw->second_list, di);
3717
3718         dupe_second_update_status(dw);
3719 }
3720
3721 static void dupe_second_clear(DupeWindow *dw)
3722 {
3723         GtkListStore *store;
3724
3725         store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->second_listview)));
3726         gtk_list_store_clear(store);
3727         gtk_tree_view_columns_autosize(GTK_TREE_VIEW(dw->second_listview));
3728
3729         g_list_free(dw->dupes);
3730         dw->dupes = NULL;
3731
3732         dupe_list_free(dw->second_list);
3733         dw->second_list = NULL;
3734
3735         dupe_match_reset_list(dw->list);
3736
3737         dupe_second_update_status(dw);
3738 }
3739
3740 static void dupe_second_menu_view_cb(GtkWidget *widget, gpointer data)
3741 {
3742         DupeWindow *dw = data;
3743
3744         if (dw->click_item) dupe_menu_view(dw, dw->click_item, dw->second_listview, FALSE);
3745 }
3746
3747 static void dupe_second_menu_viewnew_cb(GtkWidget *widget, gpointer data)
3748 {
3749         DupeWindow *dw = data;
3750
3751         if (dw->click_item) dupe_menu_view(dw, dw->click_item, dw->second_listview, TRUE);
3752 }
3753
3754 static void dupe_second_menu_select_all_cb(GtkWidget *widget, gpointer data)
3755 {
3756         GtkTreeSelection *selection;
3757         DupeWindow *dw = data;
3758
3759         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(dw->second_listview));
3760         gtk_tree_selection_select_all(selection);
3761 }
3762
3763 static void dupe_second_menu_select_none_cb(GtkWidget *widget, gpointer data)
3764 {
3765         GtkTreeSelection *selection;
3766         DupeWindow *dw = data;
3767
3768         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(dw->second_listview));
3769         gtk_tree_selection_unselect_all(selection);
3770 }
3771
3772 static void dupe_second_menu_remove_cb(GtkWidget *widget, gpointer data)
3773 {
3774         DupeWindow *dw = data;
3775
3776         dupe_window_remove_selection(dw, dw->second_listview);
3777 }
3778
3779 static void dupe_second_menu_clear_cb(GtkWidget *widget, gpointer data)
3780 {
3781         DupeWindow *dw = data;
3782
3783         dupe_second_clear(dw);
3784         dupe_window_recompare(dw);
3785 }
3786
3787 static GtkWidget *dupe_menu_popup_second(DupeWindow *dw, DupeItem *di)
3788 {
3789         GtkWidget *menu;
3790         gboolean notempty = (dw->second_list != NULL);
3791         gboolean on_row = (di != NULL);
3792
3793         menu = popup_menu_short_lived();
3794         menu_item_add_sensitive(menu, _("_View"), on_row,
3795                                 G_CALLBACK(dupe_second_menu_view_cb), dw);
3796         menu_item_add_stock_sensitive(menu, _("View in _new window"), GTK_STOCK_NEW, on_row,
3797                                 G_CALLBACK(dupe_second_menu_viewnew_cb), dw);
3798         menu_item_add_divider(menu);
3799         menu_item_add_sensitive(menu, _("Select all"), notempty,
3800                                 G_CALLBACK(dupe_second_menu_select_all_cb), dw);
3801         menu_item_add_sensitive(menu, _("Select none"), notempty,
3802                                 G_CALLBACK(dupe_second_menu_select_none_cb), dw);
3803         menu_item_add_divider(menu);
3804         menu_item_add_stock_sensitive(menu, _("Rem_ove"), GTK_STOCK_REMOVE, on_row,
3805                                       G_CALLBACK(dupe_second_menu_remove_cb), dw);
3806         menu_item_add_stock_sensitive(menu, _("C_lear"), GTK_STOCK_CLEAR, notempty,
3807                                    G_CALLBACK(dupe_second_menu_clear_cb), dw);
3808         menu_item_add_divider(menu);
3809         menu_item_add_stock(menu, _("Close _window"), GTK_STOCK_CLOSE,
3810                             G_CALLBACK(dupe_menu_close_cb), dw);
3811
3812         return menu;
3813 }
3814
3815 static void dupe_second_set_toggle_cb(GtkWidget *widget, gpointer data)
3816 {
3817         DupeWindow *dw = data;
3818
3819         dw->second_set = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget));
3820
3821         if (dw->second_set)
3822                 {
3823                 dupe_second_update_status(dw);
3824                 gtk_table_set_col_spacings(GTK_TABLE(dw->table), PREF_PAD_GAP);
3825                 gtk_widget_show(dw->second_vbox);
3826                 }
3827         else
3828                 {
3829                 gtk_table_set_col_spacings(GTK_TABLE(dw->table), 0);
3830                 gtk_widget_hide(dw->second_vbox);
3831                 dupe_second_clear(dw);
3832                 }
3833
3834         dupe_window_recompare(dw);
3835 }
3836
3837 static void dupe_sort_totals_toggle_cb(GtkWidget *widget, gpointer data)
3838 {
3839         DupeWindow *dw = data;
3840
3841         options->sort_totals = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget));
3842         dupe_window_recompare(dw);
3843
3844 }
3845
3846 /*
3847  *-------------------------------------------------------------------
3848  * match type menu
3849  *-------------------------------------------------------------------
3850  */
3851
3852 enum {
3853         DUPE_MENU_COLUMN_NAME = 0,
3854         DUPE_MENU_COLUMN_MASK
3855 };
3856
3857 static void dupe_listview_show_rank(GtkWidget *listview, gboolean rank);
3858
3859 static void dupe_menu_type_cb(GtkWidget *combo, gpointer data)
3860 {
3861         DupeWindow *dw = data;
3862         GtkTreeModel *store;
3863         GtkTreeIter iter;
3864
3865         store = gtk_combo_box_get_model(GTK_COMBO_BOX(combo));
3866         if (!gtk_combo_box_get_active_iter(GTK_COMBO_BOX(combo), &iter)) return;
3867         gtk_tree_model_get(store, &iter, DUPE_MENU_COLUMN_MASK, &dw->match_mask, -1);
3868
3869         options->duplicates_match = dw->match_mask;
3870
3871         if (dw->match_mask & (DUPE_MATCH_SIM_HIGH | DUPE_MATCH_SIM_MED | DUPE_MATCH_SIM_LOW | DUPE_MATCH_SIM_CUSTOM))
3872                 {
3873                 dupe_listview_show_rank(dw->listview, TRUE);
3874                 }
3875         else
3876                 {
3877                 dupe_listview_show_rank(dw->listview, FALSE);
3878                 }
3879         dupe_window_recompare(dw);
3880 }
3881
3882 static void dupe_menu_add_item(GtkListStore *store, const gchar *text, DupeMatchType type, DupeWindow *dw)
3883 {
3884         GtkTreeIter iter;
3885
3886         gtk_list_store_append(store, &iter);
3887         gtk_list_store_set(store, &iter, DUPE_MENU_COLUMN_NAME, text,
3888                                          DUPE_MENU_COLUMN_MASK, type, -1);
3889
3890         if (dw->match_mask == type) gtk_combo_box_set_active_iter(GTK_COMBO_BOX(dw->combo), &iter);
3891 }
3892
3893 static void dupe_menu_setup(DupeWindow *dw)
3894 {
3895         GtkListStore *store;
3896         GtkCellRenderer *renderer;
3897
3898         store = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_INT);
3899         dw->combo = gtk_combo_box_new_with_model(GTK_TREE_MODEL(store));
3900         g_object_unref(store);
3901
3902         renderer = gtk_cell_renderer_text_new();
3903         gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(dw->combo), renderer, TRUE);
3904         gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(dw->combo), renderer,
3905                                        "text", DUPE_MENU_COLUMN_NAME, NULL);
3906
3907         dupe_menu_add_item(store, _("Name"), DUPE_MATCH_NAME, dw);
3908         dupe_menu_add_item(store, _("Name case-insensitive"), DUPE_MATCH_NAME_CI, dw);
3909         dupe_menu_add_item(store, _("Size"), DUPE_MATCH_SIZE, dw);
3910         dupe_menu_add_item(store, _("Date"), DUPE_MATCH_DATE, dw);
3911         dupe_menu_add_item(store, _("Dimensions"), DUPE_MATCH_DIM, dw);
3912         dupe_menu_add_item(store, _("Checksum"), DUPE_MATCH_SUM, dw);
3913         dupe_menu_add_item(store, _("Path"), DUPE_MATCH_PATH, dw);
3914         dupe_menu_add_item(store, _("Similarity (high - 95)"), DUPE_MATCH_SIM_HIGH, dw);
3915         dupe_menu_add_item(store, _("Similarity (med. - 90)"), DUPE_MATCH_SIM_MED, dw);
3916         dupe_menu_add_item(store, _("Similarity (low - 85)"), DUPE_MATCH_SIM_LOW, dw);
3917         dupe_menu_add_item(store, _("Similarity (custom)"), DUPE_MATCH_SIM_CUSTOM, dw);
3918         dupe_menu_add_item(store, _("Name ≠ content"), DUPE_MATCH_NAME_CONTENT, dw);
3919         dupe_menu_add_item(store, _("Name case-insensitive ≠ content"), DUPE_MATCH_NAME_CI_CONTENT, dw);
3920         dupe_menu_add_item(store, _("Show all"), DUPE_MATCH_ALL, dw);
3921
3922         g_signal_connect(G_OBJECT(dw->combo), "changed",
3923                          G_CALLBACK(dupe_menu_type_cb), dw);
3924 }
3925
3926 /*
3927  *-------------------------------------------------------------------
3928  * list view columns
3929  *-------------------------------------------------------------------
3930  */
3931
3932 /* this overrides the low default of a GtkCellRenderer from 100 to CELL_HEIGHT_OVERRIDE, something sane for our purposes */
3933
3934 #define CELL_HEIGHT_OVERRIDE 512
3935
3936 void cell_renderer_height_override(GtkCellRenderer *renderer)
3937 {
3938         GParamSpec *spec;
3939
3940         spec = g_object_class_find_property(G_OBJECT_GET_CLASS(G_OBJECT(renderer)), "height");
3941         if (spec && G_IS_PARAM_SPEC_INT(spec))
3942                 {
3943                 GParamSpecInt *spec_int;
3944
3945                 spec_int = G_PARAM_SPEC_INT(spec);
3946                 if (spec_int->maximum < CELL_HEIGHT_OVERRIDE) spec_int->maximum = CELL_HEIGHT_OVERRIDE;
3947                 }
3948 }
3949
3950 static GdkColor *dupe_listview_color_shifted(GtkWidget *widget)
3951 {
3952         static GdkColor color;
3953         static GtkWidget *done = NULL;
3954
3955         if (done != widget)
3956                 {
3957                 GtkStyle *style;
3958
3959                 style = gtk_widget_get_style(widget);
3960                 memcpy(&color, &style->base[GTK_STATE_NORMAL], sizeof(color));
3961                 shift_color(&color, -1, 0);
3962                 done = widget;
3963                 }
3964
3965         return &color;
3966 }
3967
3968 static void dupe_listview_color_cb(GtkTreeViewColumn *tree_column, GtkCellRenderer *cell,
3969                                    GtkTreeModel *tree_model, GtkTreeIter *iter, gpointer data)
3970 {
3971         DupeWindow *dw = data;
3972         gboolean set;
3973
3974         gtk_tree_model_get(tree_model, iter, DUPE_COLUMN_COLOR, &set, -1);
3975         g_object_set(G_OBJECT(cell),
3976                      "cell-background-gdk", dupe_listview_color_shifted(dw->listview),
3977                      "cell-background-set", set, NULL);
3978 }
3979
3980 static void dupe_listview_add_column(DupeWindow *dw, GtkWidget *listview, gint n, const gchar *title, gboolean image, gboolean right_justify)
3981 {
3982         GtkTreeViewColumn *column;
3983         GtkCellRenderer *renderer;
3984
3985         column = gtk_tree_view_column_new();
3986         gtk_tree_view_column_set_title(column, title);
3987         gtk_tree_view_column_set_min_width(column, 4);
3988         gtk_tree_view_column_set_sort_column_id(column, n);
3989
3990         if (n != DUPE_COLUMN_RANK &&
3991             n != DUPE_COLUMN_THUMB)
3992                 {
3993                 gtk_tree_view_column_set_resizable(column, TRUE);
3994                 }
3995
3996         if (!image)
3997                 {
3998                 gtk_tree_view_column_set_sizing(column, GTK_TREE_VIEW_COLUMN_GROW_ONLY);
3999                 renderer = gtk_cell_renderer_text_new();
4000                 if (right_justify)
4001                         {
4002                         g_object_set(G_OBJECT(renderer), "xalign", 1.0, NULL);
4003                         }
4004                 gtk_tree_view_column_pack_start(column, renderer, TRUE);
4005                 gtk_tree_view_column_add_attribute(column, renderer, "text", n);
4006                 }
4007         else
4008                 {
4009                 gtk_tree_view_column_set_sizing(column, GTK_TREE_VIEW_COLUMN_FIXED);
4010                 renderer = gtk_cell_renderer_pixbuf_new();
4011                 cell_renderer_height_override(renderer);
4012                 gtk_tree_view_column_pack_start(column, renderer, TRUE);
4013                 gtk_tree_view_column_add_attribute(column, renderer, "pixbuf", n);
4014                 }
4015
4016         if (listview == dw->listview)
4017                 {
4018                 /* sets background before rendering */
4019                 gtk_tree_view_column_set_cell_data_func(column, renderer, dupe_listview_color_cb, dw, NULL);
4020                 }
4021
4022         gtk_tree_view_append_column(GTK_TREE_VIEW(listview), column);
4023 }
4024
4025 static void dupe_listview_set_height(GtkWidget *listview, gboolean thumb)
4026 {
4027         GtkTreeViewColumn *column;
4028         GtkCellRenderer *cell;
4029         GList *list;
4030
4031         column = gtk_tree_view_get_column(GTK_TREE_VIEW(listview), DUPE_COLUMN_THUMB - 1);
4032         if (!column) return;
4033
4034         gtk_tree_view_column_set_fixed_width(column, (thumb) ? options->thumbnails.max_width : 4);
4035         gtk_tree_view_column_set_visible(column, thumb);
4036
4037         list = gtk_cell_layout_get_cells(GTK_CELL_LAYOUT(column));
4038         if (!list) return;
4039         cell = list->data;
4040         g_list_free(list);
4041
4042         g_object_set(G_OBJECT(cell), "height", (thumb) ? options->thumbnails.max_height : -1, NULL);
4043         gtk_tree_view_columns_autosize(GTK_TREE_VIEW(listview));
4044 }
4045
4046 static void dupe_listview_show_rank(GtkWidget *listview, gboolean rank)
4047 {
4048         GtkTreeViewColumn *column;
4049
4050         column = gtk_tree_view_get_column(GTK_TREE_VIEW(listview), DUPE_COLUMN_RANK - 1);
4051         if (!column) return;
4052
4053         gtk_tree_view_column_set_visible(column, rank);
4054 }
4055
4056 /*
4057  *-------------------------------------------------------------------
4058  * misc cb
4059  *-------------------------------------------------------------------
4060  */
4061
4062 static void dupe_window_show_thumb_cb(GtkWidget *widget, gpointer data)
4063 {
4064         DupeWindow *dw = data;
4065
4066         dw->show_thumbs = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget));
4067         options->duplicates_thumbnails = dw->show_thumbs;
4068
4069         if (dw->show_thumbs)
4070                 {
4071                 if (!dw->working) dupe_thumb_step(dw);
4072                 }
4073         else
4074                 {
4075                 GtkTreeModel *store;
4076                 GtkTreeIter iter;
4077                 gboolean valid;
4078
4079                 thumb_loader_free(dw->thumb_loader);
4080                 dw->thumb_loader = NULL;
4081
4082                 store = gtk_tree_view_get_model(GTK_TREE_VIEW(dw->listview));
4083                 valid = gtk_tree_model_get_iter_first(store, &iter);
4084
4085                 while (valid)
4086                         {
4087                         gtk_list_store_set(GTK_LIST_STORE(store), &iter, DUPE_COLUMN_THUMB, NULL, -1);
4088                         valid = gtk_tree_model_iter_next(store, &iter);
4089                         }
4090                 dupe_window_update_progress(dw, NULL, 0.0, FALSE);
4091                 }
4092
4093         dupe_listview_set_height(dw->listview, dw->show_thumbs);
4094 }
4095
4096 static void dupe_window_rotation_invariant_cb(GtkWidget *widget, gpointer data)
4097 {
4098         DupeWindow *dw = data;
4099
4100         options->rot_invariant_sim = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget));
4101         dupe_window_recompare(dw);
4102 }
4103
4104 static void dupe_window_custom_threshold_cb(GtkWidget *widget, gpointer data)
4105 {
4106         DupeWindow *dw = data;
4107         DupeMatchType match_type;
4108         GtkTreeModel *store;
4109         gboolean valid;
4110         GtkTreeIter iter;
4111
4112         options->duplicates_similarity_threshold = gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(widget));
4113         dw->match_mask = DUPE_MATCH_SIM_CUSTOM;
4114
4115         store = gtk_combo_box_get_model(GTK_COMBO_BOX(dw->combo));
4116         valid = gtk_tree_model_get_iter_first(store, &iter);
4117         while (valid)
4118                 {
4119                 gtk_tree_model_get(store, &iter, DUPE_MENU_COLUMN_MASK, &match_type, -1);
4120                 if (match_type == DUPE_MATCH_SIM_CUSTOM)
4121                         {
4122                         break;
4123                         }
4124                 valid = gtk_tree_model_iter_next(store, &iter);
4125                 }
4126
4127         gtk_combo_box_set_active_iter(GTK_COMBO_BOX(dw->combo), &iter);
4128         dupe_window_recompare(dw);
4129 }
4130
4131 static void dupe_popup_menu_pos_cb(GtkMenu *menu, gint *x, gint *y, gboolean *push_in, gpointer data)
4132 {
4133         GtkWidget *view = data;
4134         GtkTreePath *tpath;
4135         gint cx, cy, cw, ch;
4136         gint column;
4137
4138         gtk_tree_view_get_cursor(GTK_TREE_VIEW(view), &tpath, NULL);
4139         if (!tpath) return;
4140
4141         if (gtk_tree_view_get_column(GTK_TREE_VIEW(view), DUPE_COLUMN_NAME - 1) != NULL)
4142                 {
4143                 column = DUPE_COLUMN_NAME - 1;
4144                 }
4145         else
4146                 {
4147                 /* dw->second_listview */
4148                 column = 0;
4149                 }
4150         tree_view_get_cell_clamped(GTK_TREE_VIEW(view), tpath, column, TRUE, &cx, &cy, &cw, &ch);
4151         gtk_tree_path_free(tpath);
4152         cy += ch;
4153         popup_menu_position_clamp(menu, &cx, &cy, 0);
4154         *x = cx;
4155         *y = cy;
4156 }
4157
4158 static gboolean dupe_window_keypress_cb(GtkWidget *widget, GdkEventKey *event, gpointer data)
4159 {
4160         DupeWindow *dw = data;
4161         gboolean stop_signal = FALSE;
4162         gboolean on_second;
4163         GtkWidget *listview;
4164         GtkTreeModel *store;
4165         GtkTreeSelection *selection;
4166         GList *slist;
4167         DupeItem *di = NULL;
4168
4169         on_second = gtk_widget_has_focus(dw->second_listview);
4170
4171         if (on_second)
4172                 {
4173                 listview = dw->second_listview;
4174                 }
4175         else
4176                 {
4177                 listview = dw->listview;
4178                 }
4179
4180         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(listview));
4181         slist = gtk_tree_selection_get_selected_rows(selection, &store);
4182         if (slist)
4183                 {
4184                 GtkTreePath *tpath;
4185                 GtkTreeIter iter;
4186                 GList *last;
4187
4188                 last = g_list_last(slist);
4189                 tpath = last->data;
4190
4191                 /* last is newest selected file */
4192                 gtk_tree_model_get_iter(store, &iter, tpath);
4193                 gtk_tree_model_get(store, &iter, DUPE_COLUMN_POINTER, &di, -1);
4194                 }
4195         g_list_foreach(slist, (GFunc)tree_path_free_wrapper, NULL);
4196         g_list_free(slist);
4197
4198         if (event->state & GDK_CONTROL_MASK)
4199                 {
4200                 if (!on_second)
4201                         {
4202                         stop_signal = TRUE;
4203                         switch (event->keyval)
4204                                 {
4205                                 case '1':
4206                                 case '2':
4207                                 case '3':
4208                                 case '4':
4209                                 case '5':
4210                                 case '6':
4211                                 case '7':
4212                                 case '8':
4213                                 case '9':
4214                                 case '0':
4215                                         break;
4216                                 case 'C': case 'c':
4217                                         file_util_copy(NULL, dupe_listview_get_selection(dw, listview),
4218                                                        NULL, dw->window);
4219                                         break;
4220                                 case 'M': case 'm':
4221                                         file_util_move(NULL, dupe_listview_get_selection(dw, listview),
4222                                                        NULL, dw->window);
4223                                         break;
4224                                 case 'R': case 'r':
4225                                         file_util_rename(NULL, dupe_listview_get_selection(dw, listview), dw->window);
4226                                         break;
4227                                 case 'D': case 'd':
4228                                         options->file_ops.safe_delete_enable = TRUE;
4229                                         file_util_delete(NULL, dupe_listview_get_selection(dw, listview), dw->window);
4230                                         break;
4231                                 default:
4232                                         stop_signal = FALSE;
4233                                         break;
4234                                 }
4235                         }
4236
4237                 if (!stop_signal)
4238                         {
4239                         stop_signal = TRUE;
4240                         switch (event->keyval)
4241                                 {
4242                                 case 'A': case 'a':
4243                                         if (event->state & GDK_SHIFT_MASK)
4244                                                 {
4245                                                 gtk_tree_selection_unselect_all(selection);
4246                                                 }
4247                                         else
4248                                                 {
4249                                                 gtk_tree_selection_select_all(selection);
4250                                                 }
4251                                         break;
4252                                 case GDK_KEY_Delete: case GDK_KEY_KP_Delete:
4253                                         if (on_second)
4254                                                 {
4255                                                 dupe_second_clear(dw);
4256                                                 dupe_window_recompare(dw);
4257                                                 }
4258                                         else
4259                                                 {
4260                                                 dupe_window_clear(dw);
4261                                                 }
4262                                         break;
4263                                 case 'L': case 'l':
4264                                         dupe_window_append_file_list(dw, FALSE);
4265                                         break;
4266                                 case 'T': case 't':
4267                                         gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(dw->button_thumbs),
4268                                                 !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(dw->button_thumbs)));
4269                                         break;
4270                                 case 'W': case 'w':
4271                                         dupe_window_close(dw);
4272                                         break;
4273                                 default:
4274                                         stop_signal = FALSE;
4275                                         break;
4276                                 }
4277                         }
4278                 }
4279         else
4280                 {
4281                 stop_signal = TRUE;
4282                 switch (event->keyval)
4283                         {
4284                         case GDK_KEY_Return: case GDK_KEY_KP_Enter:
4285                                 dupe_menu_view(dw, di, listview, FALSE);
4286                                 break;
4287                         case 'V': case 'v':
4288                                 dupe_menu_view(dw, di, listview, TRUE);
4289                                 break;
4290                         case GDK_KEY_Delete: case GDK_KEY_KP_Delete:
4291                                 dupe_window_remove_selection(dw, listview);
4292                                 break;
4293                         case 'C': case 'c':
4294                                 if (!on_second)
4295                                         {
4296                                         dupe_window_collection_from_selection(dw);
4297                                         }
4298                                 break;
4299                         case '0':
4300                                 options->duplicates_select_type = DUPE_SELECT_NONE;
4301                                 dupe_listview_select_dupes(dw, DUPE_SELECT_NONE);
4302                                 break;
4303                         case '1':
4304                                 options->duplicates_select_type = DUPE_SELECT_GROUP1;
4305                                 dupe_listview_select_dupes(dw, DUPE_SELECT_GROUP1);
4306                                 break;
4307                         case '2':
4308                                 options->duplicates_select_type = DUPE_SELECT_GROUP2;
4309                                 dupe_listview_select_dupes(dw, DUPE_SELECT_GROUP2);
4310                                 break;
4311                         case GDK_KEY_Menu:
4312                         case GDK_KEY_F10:
4313                                 if (!on_second)
4314                                         {
4315                                         GtkWidget *menu;
4316
4317                                         menu = dupe_menu_popup_main(dw, di);
4318                                         gtk_menu_popup(GTK_MENU(menu), NULL, NULL,
4319                                                        dupe_popup_menu_pos_cb, listview, 0, GDK_CURRENT_TIME);
4320                                         }
4321                                 else
4322                                         {
4323                                         GtkWidget *menu;
4324
4325                                         menu = dupe_menu_popup_second(dw, di);
4326                                         gtk_menu_popup(GTK_MENU(menu), NULL, NULL,
4327                                                        dupe_popup_menu_pos_cb, listview, 0, GDK_CURRENT_TIME);
4328                                         }
4329                                 break;
4330                         default:
4331                                 stop_signal = FALSE;
4332                                 break;
4333                         }
4334                 }
4335         if (!stop_signal && is_help_key(event))
4336                 {
4337                 help_window_show("GuideImageSearchFindingDuplicates.html");
4338                 stop_signal = TRUE;
4339                 }
4340
4341         return stop_signal;
4342 }
4343
4344
4345 void dupe_window_clear(DupeWindow *dw)
4346 {
4347         GtkListStore *store;
4348
4349         dupe_check_stop(dw);
4350
4351         store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->listview)));
4352         gtk_list_store_clear(store);
4353         gtk_tree_view_columns_autosize(GTK_TREE_VIEW(dw->listview));
4354
4355         g_list_free(dw->dupes);
4356         dw->dupes = NULL;
4357
4358         dupe_list_free(dw->list);
4359         dw->list = NULL;
4360         dw->set_count = 0;
4361
4362         dupe_match_reset_list(dw->second_list);
4363
4364         dupe_window_update_count(dw, FALSE);
4365         dupe_window_update_progress(dw, NULL, 0.0, FALSE);
4366 }
4367
4368 static void dupe_window_get_geometry(DupeWindow *dw)
4369 {
4370         GdkWindow *window;
4371         LayoutWindow *lw = NULL;
4372
4373         layout_valid(&lw);
4374
4375         if (!dw || !lw) return;
4376
4377         window = gtk_widget_get_window(dw->window);
4378         gdk_window_get_position(window, &lw->options.dupe_window.x, &lw->options.dupe_window.y);
4379         lw->options.dupe_window.w = gdk_window_get_width(window);
4380         lw->options.dupe_window.h = gdk_window_get_height(window);
4381 }
4382
4383 void dupe_window_close(DupeWindow *dw)
4384 {
4385         dupe_check_stop(dw);
4386
4387         dupe_window_get_geometry(dw);
4388
4389         dupe_window_list = g_list_remove(dupe_window_list, dw);
4390         gtk_widget_destroy(dw->window);
4391
4392         g_list_free(dw->dupes);
4393         dupe_list_free(dw->list);
4394
4395         dupe_list_free(dw->second_list);
4396
4397         file_data_unregister_notify_func(dupe_notify_cb, dw);
4398
4399         g_thread_pool_free(dw->dupe_comparison_thread_pool, TRUE, TRUE);
4400
4401         g_free(dw);
4402 }
4403
4404 static gint dupe_window_close_cb(GtkWidget *widget, gpointer data)
4405 {
4406         DupeWindow *dw = data;
4407
4408         dupe_window_close(dw);
4409
4410         return TRUE;
4411 }
4412
4413 static gint dupe_window_delete(GtkWidget *widget, GdkEvent *event, gpointer data)
4414 {
4415         DupeWindow *dw = data;
4416         dupe_window_close(dw);
4417
4418         return TRUE;
4419 }
4420
4421 static void dupe_help_cb(GtkAction *action, gpointer data)
4422 {
4423         help_window_show("GuideImageSearchFindingDuplicates.html");
4424 }
4425
4426 static gint default_sort_cb(GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, gpointer data)
4427 {
4428         return 0;
4429 }
4430
4431 static gint column_sort_cb(GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, gpointer data)
4432 {
4433         GtkTreeSortable *sortable = data;
4434         gint ret = 0;
4435         gchar *rank_str_a, *rank_str_b;
4436         gint rank_int_a;
4437         gint rank_int_b;
4438         gint group_a;
4439         gint group_b;
4440         gint sort_column_id;
4441         GtkSortType sort_order;
4442         DupeItem *di_a;
4443         DupeItem *di_b;
4444
4445         gtk_tree_sortable_get_sort_column_id(sortable, &sort_column_id, &sort_order);
4446
4447         gtk_tree_model_get(model, a, DUPE_COLUMN_RANK, &rank_str_a, DUPE_COLUMN_SET, &group_a, DUPE_COLUMN_POINTER, &di_a, -1);
4448
4449         gtk_tree_model_get(model, b, DUPE_COLUMN_RANK, &rank_str_b, DUPE_COLUMN_SET, &group_b, DUPE_COLUMN_POINTER, &di_b, -1);
4450
4451         if (group_a == group_b)
4452                 {
4453                 switch (sort_column_id)
4454                         {
4455                         case DUPE_COLUMN_NAME:
4456                                 ret = utf8_compare(di_a->fd->name, di_b->fd->name, TRUE);
4457                                 break;
4458                         case DUPE_COLUMN_SIZE:
4459                                 if (di_a->fd->size == di_b->fd->size)
4460                                         {
4461                                         ret = 0;
4462                                         }
4463                                 else
4464                                         {
4465                                         ret = (di_a->fd->size > di_b->fd->size) ? 1 : -1;
4466                                         }
4467                                 break;
4468                         case DUPE_COLUMN_DATE:
4469                                 if (di_a->fd->date == di_b->fd->date)
4470                                         {
4471                                         ret = 0;
4472                                         }
4473                                 else
4474                                         {
4475                                         ret = (di_a->fd->date > di_b->fd->date) ? 1 : -1;
4476                                         }
4477                                 break;
4478                         case DUPE_COLUMN_DIMENSIONS:
4479                                 if ((di_a->width == di_b->width) && (di_a->height == di_b->height))
4480                                         {
4481                                         ret = 0;
4482                                         }
4483                                 else
4484                                         {
4485                                         ret = ((di_a->width * di_a->height) > (di_b->width * di_b->height)) ? 1 : -1;
4486                                         }
4487                                 break;
4488                         case DUPE_COLUMN_RANK:
4489                                 rank_int_a = atoi(rank_str_a);
4490                                 rank_int_b = atoi(rank_str_b);
4491                                 if (rank_int_a == 0) rank_int_a = 101;
4492                                 if (rank_int_b == 0) rank_int_b = 101;
4493
4494                                 if (rank_int_a == rank_int_b)
4495                                         {
4496                                         ret = 0;
4497                                         }
4498                                 else
4499                                         {
4500                                         ret = (rank_int_a > rank_int_b) ? 1 : -1;
4501                                         }
4502                                 break;
4503                         case DUPE_COLUMN_PATH:
4504                                 ret = utf8_compare(di_a->fd->path, di_b->fd->path, TRUE);
4505                                 break;
4506                         }
4507                 }
4508         else if (group_a < group_b)
4509                 {
4510                 ret = (sort_order == GTK_SORT_ASCENDING) ? 1 : -1;
4511                 }
4512         else
4513                 {
4514                 ret = (sort_order == GTK_SORT_ASCENDING) ? -1 : 1;
4515                 }
4516
4517         return ret;
4518 }
4519
4520 static void column_clicked_cb(GtkWidget *widget,  gpointer data)
4521 {
4522         DupeWindow *dw = data;
4523
4524         options->duplicates_match = DUPE_SELECT_NONE;
4525         dupe_listview_select_dupes(dw, DUPE_SELECT_NONE);
4526 }
4527
4528 /* collection and files can be NULL */
4529 DupeWindow *dupe_window_new()
4530 {
4531         DupeWindow *dw;
4532         GtkWidget *vbox;
4533         GtkWidget *hbox;
4534         GtkWidget *scrolled;
4535         GtkWidget *frame;
4536         GtkWidget *status_box;
4537         GtkWidget *controls_box;
4538         GtkWidget *button_box;
4539         GtkWidget *label;
4540         GtkWidget *button;
4541         GtkListStore *store;
4542         GtkTreeSelection *selection;
4543         GdkGeometry geometry;
4544         LayoutWindow *lw = NULL;
4545
4546         layout_valid(&lw);
4547
4548         dw = g_new0(DupeWindow, 1);
4549         dw->add_files_queue = NULL;
4550         dw->add_files_queue_id = 0;
4551
4552         dw->match_mask = DUPE_MATCH_NAME;
4553         if (options->duplicates_match == DUPE_MATCH_NAME) dw->match_mask = DUPE_MATCH_NAME;
4554         if (options->duplicates_match == DUPE_MATCH_SIZE) dw->match_mask = DUPE_MATCH_SIZE;
4555         if (options->duplicates_match == DUPE_MATCH_DATE) dw->match_mask = DUPE_MATCH_DATE;
4556         if (options->duplicates_match == DUPE_MATCH_DIM) dw->match_mask = DUPE_MATCH_DIM;
4557         if (options->duplicates_match == DUPE_MATCH_SUM) dw->match_mask = DUPE_MATCH_SUM;
4558         if (options->duplicates_match == DUPE_MATCH_PATH) dw->match_mask = DUPE_MATCH_PATH;
4559         if (options->duplicates_match == DUPE_MATCH_SIM_HIGH) dw->match_mask = DUPE_MATCH_SIM_HIGH;
4560         if (options->duplicates_match == DUPE_MATCH_SIM_MED) dw->match_mask = DUPE_MATCH_SIM_MED;
4561         if (options->duplicates_match == DUPE_MATCH_SIM_LOW) dw->match_mask = DUPE_MATCH_SIM_LOW;
4562         if (options->duplicates_match == DUPE_MATCH_SIM_CUSTOM) dw->match_mask = DUPE_MATCH_SIM_CUSTOM;
4563         if (options->duplicates_match == DUPE_MATCH_NAME_CI) dw->match_mask = DUPE_MATCH_NAME_CI;
4564         if (options->duplicates_match == DUPE_MATCH_NAME_CONTENT) dw->match_mask = DUPE_MATCH_NAME_CONTENT;
4565         if (options->duplicates_match == DUPE_MATCH_NAME_CI_CONTENT) dw->match_mask = DUPE_MATCH_NAME_CI_CONTENT;
4566         if (options->duplicates_match == DUPE_MATCH_ALL) dw->match_mask = DUPE_MATCH_ALL;
4567
4568         dw->window = window_new(GTK_WINDOW_TOPLEVEL, "dupe", NULL, NULL, _("Find duplicates"));
4569         DEBUG_NAME(dw->window);
4570
4571         geometry.min_width = DEFAULT_MINIMAL_WINDOW_SIZE;
4572         geometry.min_height = DEFAULT_MINIMAL_WINDOW_SIZE;
4573         geometry.base_width = DUPE_DEF_WIDTH;
4574         geometry.base_height = DUPE_DEF_HEIGHT;
4575         gtk_window_set_geometry_hints(GTK_WINDOW(dw->window), NULL, &geometry,
4576                                       GDK_HINT_MIN_SIZE | GDK_HINT_BASE_SIZE);
4577
4578         if (lw && options->save_window_positions)
4579                 {
4580                 gtk_window_set_default_size(GTK_WINDOW(dw->window), lw->options.dupe_window.w, lw->options.dupe_window.h);
4581                 gtk_window_move(GTK_WINDOW(dw->window), lw->options.dupe_window.x, lw->options.dupe_window.y);
4582                 }
4583         else
4584                 {
4585                 gtk_window_set_default_size(GTK_WINDOW(dw->window), DUPE_DEF_WIDTH, DUPE_DEF_HEIGHT);
4586                 }
4587
4588         gtk_window_set_resizable(GTK_WINDOW(dw->window), TRUE);
4589         gtk_container_set_border_width(GTK_CONTAINER(dw->window), 0);
4590
4591         g_signal_connect(G_OBJECT(dw->window), "delete_event",
4592                          G_CALLBACK(dupe_window_delete), dw);
4593         g_signal_connect(G_OBJECT(dw->window), "key_press_event",
4594                          G_CALLBACK(dupe_window_keypress_cb), dw);
4595
4596         vbox = gtk_vbox_new(FALSE, 0);
4597         gtk_container_add(GTK_CONTAINER(dw->window), vbox);
4598         gtk_widget_show(vbox);
4599
4600         dw->table = gtk_table_new(1, 3, FALSE);
4601         gtk_box_pack_start(GTK_BOX(vbox), dw->table, TRUE, TRUE, 0);
4602         gtk_widget_show(dw->table);
4603
4604         scrolled = gtk_scrolled_window_new(NULL, NULL);
4605         gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled), GTK_SHADOW_IN);
4606         gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
4607         gtk_table_attach_defaults(GTK_TABLE(dw->table), scrolled, 0, 2, 0, 1);
4608         gtk_widget_show(scrolled);
4609
4610         store = gtk_list_store_new(DUPE_COLUMN_COUNT, G_TYPE_POINTER, G_TYPE_STRING, GDK_TYPE_PIXBUF, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_INT, G_TYPE_INT);
4611         dw->listview = gtk_tree_view_new_with_model(GTK_TREE_MODEL(store));
4612         g_object_unref(store);
4613
4614         dw->sortable = GTK_TREE_SORTABLE(store);
4615
4616         gtk_tree_sortable_set_sort_func(dw->sortable, DUPE_COLUMN_RANK, column_sort_cb, dw->sortable, NULL);
4617         gtk_tree_sortable_set_sort_func(dw->sortable, DUPE_COLUMN_SET, default_sort_cb, dw->sortable, NULL);
4618         gtk_tree_sortable_set_sort_func(dw->sortable, DUPE_COLUMN_THUMB, default_sort_cb, dw->sortable, NULL);
4619         gtk_tree_sortable_set_sort_func(dw->sortable, DUPE_COLUMN_NAME, column_sort_cb, dw->sortable, NULL);
4620         gtk_tree_sortable_set_sort_func(dw->sortable, DUPE_COLUMN_SIZE, column_sort_cb, dw->sortable, NULL);
4621         gtk_tree_sortable_set_sort_func(dw->sortable, DUPE_COLUMN_DATE, column_sort_cb, dw->sortable, NULL);
4622         gtk_tree_sortable_set_sort_func(dw->sortable, DUPE_COLUMN_DIMENSIONS, column_sort_cb, dw->sortable, NULL);
4623         gtk_tree_sortable_set_sort_func(dw->sortable, DUPE_COLUMN_PATH, column_sort_cb, dw->sortable, NULL);
4624
4625         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(dw->listview));
4626         gtk_tree_selection_set_mode(GTK_TREE_SELECTION(selection), GTK_SELECTION_MULTIPLE);
4627         gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(dw->listview), TRUE);
4628         gtk_tree_view_set_enable_search(GTK_TREE_VIEW(dw->listview), FALSE);
4629
4630         dupe_listview_add_column(dw, dw->listview, DUPE_COLUMN_RANK, _("Rank"), FALSE, TRUE);
4631         dupe_listview_add_column(dw, dw->listview, DUPE_COLUMN_THUMB, _("Thumb"), TRUE, FALSE);
4632         dupe_listview_add_column(dw, dw->listview, DUPE_COLUMN_NAME, _("Name"), FALSE, FALSE);
4633         dupe_listview_add_column(dw, dw->listview, DUPE_COLUMN_SIZE, _("Size"), FALSE, TRUE);
4634         dupe_listview_add_column(dw, dw->listview, DUPE_COLUMN_DATE, _("Date"), FALSE, TRUE);
4635         dupe_listview_add_column(dw, dw->listview, DUPE_COLUMN_DIMENSIONS, _("Dimensions"), FALSE, FALSE);
4636         dupe_listview_add_column(dw, dw->listview, DUPE_COLUMN_PATH, _("Path"), FALSE, FALSE);
4637         dupe_listview_add_column(dw, dw->listview, DUPE_COLUMN_SET, _("Set"), FALSE, FALSE);
4638
4639         g_signal_connect(gtk_tree_view_get_column(GTK_TREE_VIEW(dw->listview), DUPE_COLUMN_RANK - 1), "clicked", (GCallback)column_clicked_cb, dw);
4640         g_signal_connect(gtk_tree_view_get_column(GTK_TREE_VIEW(dw->listview), DUPE_COLUMN_NAME - 1), "clicked", (GCallback)column_clicked_cb, dw);
4641         g_signal_connect(gtk_tree_view_get_column(GTK_TREE_VIEW(dw->listview), DUPE_COLUMN_SIZE - 1), "clicked", (GCallback)column_clicked_cb, dw);
4642         g_signal_connect(gtk_tree_view_get_column(GTK_TREE_VIEW(dw->listview), DUPE_COLUMN_DATE - 1), "clicked", (GCallback)column_clicked_cb, dw);
4643         g_signal_connect(gtk_tree_view_get_column(GTK_TREE_VIEW(dw->listview), DUPE_COLUMN_DIMENSIONS - 1), "clicked", (GCallback)column_clicked_cb, dw);
4644         g_signal_connect(gtk_tree_view_get_column(GTK_TREE_VIEW(dw->listview), DUPE_COLUMN_PATH - 1), "clicked", (GCallback)column_clicked_cb, dw);
4645
4646         gtk_container_add(GTK_CONTAINER(scrolled), dw->listview);
4647         gtk_widget_show(dw->listview);
4648
4649         dw->second_vbox = gtk_vbox_new(FALSE, 0);
4650         gtk_table_attach_defaults(GTK_TABLE(dw->table), dw->second_vbox, 2, 3, 0, 1);
4651         if (dw->second_set)
4652                 {
4653                 gtk_table_set_col_spacings(GTK_TABLE(dw->table), PREF_PAD_GAP);
4654                 gtk_widget_show(dw->second_vbox);
4655                 }
4656         else
4657                 {
4658                 gtk_table_set_col_spacings(GTK_TABLE(dw->table), 0);
4659                 }
4660
4661         scrolled = gtk_scrolled_window_new(NULL, NULL);
4662         gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled), GTK_SHADOW_IN);
4663         gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
4664         gtk_box_pack_start(GTK_BOX(dw->second_vbox), scrolled, TRUE, TRUE, 0);
4665         gtk_widget_show(scrolled);
4666
4667         store = gtk_list_store_new(2, G_TYPE_POINTER, G_TYPE_STRING);
4668         dw->second_listview = gtk_tree_view_new_with_model(GTK_TREE_MODEL(store));
4669
4670         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(dw->second_listview));
4671         gtk_tree_selection_set_mode(GTK_TREE_SELECTION(selection), GTK_SELECTION_MULTIPLE);
4672
4673         gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(dw->second_listview), TRUE);
4674         gtk_tree_view_set_enable_search(GTK_TREE_VIEW(dw->second_listview), FALSE);
4675
4676         dupe_listview_add_column(dw, dw->second_listview, 1, _("Compare to:"), FALSE, FALSE);
4677
4678         gtk_container_add(GTK_CONTAINER(scrolled), dw->second_listview);
4679         gtk_widget_show(dw->second_listview);
4680
4681         dw->second_status_label = gtk_label_new("");
4682         gtk_box_pack_start(GTK_BOX(dw->second_vbox), dw->second_status_label, FALSE, FALSE, 0);
4683         gtk_widget_show(dw->second_status_label);
4684
4685         pref_line(dw->second_vbox, GTK_ORIENTATION_HORIZONTAL);
4686
4687         status_box = gtk_hbox_new(FALSE, 0);
4688         gtk_box_pack_start(GTK_BOX(vbox), status_box, FALSE, FALSE, 0);
4689         gtk_widget_show(status_box);
4690
4691         frame = gtk_frame_new(NULL);
4692         DEBUG_NAME(frame);
4693         gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_IN);
4694         gtk_box_pack_start(GTK_BOX(status_box), frame, TRUE, TRUE, 0);
4695         gtk_widget_show(frame);
4696
4697         dw->status_label = gtk_label_new("");
4698         gtk_container_add(GTK_CONTAINER(frame), dw->status_label);
4699         gtk_widget_show(dw->status_label);
4700
4701         dw->extra_label = gtk_progress_bar_new();
4702         gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(dw->extra_label), 0.0);
4703 #if GTK_CHECK_VERSION(3,0,0)
4704         gtk_progress_bar_set_text(GTK_PROGRESS_BAR(dw->extra_label), "");
4705         gtk_progress_bar_set_show_text(GTK_PROGRESS_BAR(dw->extra_label), TRUE);
4706 #endif
4707         gtk_box_pack_start(GTK_BOX(status_box), dw->extra_label, FALSE, FALSE, PREF_PAD_SPACE);
4708         gtk_widget_show(dw->extra_label);
4709
4710         controls_box = pref_box_new(vbox, FALSE, GTK_ORIENTATION_HORIZONTAL, 0);
4711         dw->controls_box = controls_box;
4712
4713         dw->button_thumbs = gtk_check_button_new_with_label(_("Thumbnails"));
4714         dw->show_thumbs = options->duplicates_thumbnails;
4715         gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(dw->button_thumbs), dw->show_thumbs);
4716         g_signal_connect(G_OBJECT(dw->button_thumbs), "toggled",
4717                          G_CALLBACK(dupe_window_show_thumb_cb), dw);
4718         gtk_box_pack_start(GTK_BOX(controls_box), dw->button_thumbs, FALSE, FALSE, PREF_PAD_SPACE);
4719         gtk_widget_show(dw->button_thumbs);
4720
4721         label = gtk_label_new(_("Compare by:"));
4722         gtk_box_pack_start(GTK_BOX(controls_box), label, FALSE, FALSE, PREF_PAD_SPACE);
4723         gtk_widget_show(label);
4724
4725         dupe_menu_setup(dw);
4726         gtk_box_pack_start(GTK_BOX(controls_box), dw->combo, FALSE, FALSE, 0);
4727         gtk_widget_show(dw->combo);
4728
4729         label = gtk_label_new(_("Custom Threshold"));
4730         gtk_box_pack_start(GTK_BOX(controls_box), label, FALSE, FALSE, PREF_PAD_SPACE);
4731         gtk_widget_show(label);
4732         dw->custom_threshold = gtk_spin_button_new_with_range(1, 100, 1);
4733         gtk_widget_set_tooltip_text(GTK_WIDGET(dw->custom_threshold), "Custom similarity threshold\n(Use tab key to set value)");
4734         gtk_spin_button_set_value(GTK_SPIN_BUTTON(dw->custom_threshold), options->duplicates_similarity_threshold);
4735         g_signal_connect(G_OBJECT(dw->custom_threshold), "value_changed", G_CALLBACK(dupe_window_custom_threshold_cb), dw);
4736         gtk_box_pack_start(GTK_BOX(controls_box), dw->custom_threshold, FALSE, FALSE, PREF_PAD_SPACE);
4737         gtk_widget_show(dw->custom_threshold);
4738
4739         button = gtk_check_button_new_with_label(_("Sort"));
4740         gtk_widget_set_tooltip_text(GTK_WIDGET(button), "Sort by group totals");
4741         gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(button), options->sort_totals);
4742         g_signal_connect(G_OBJECT(button), "toggled", G_CALLBACK(dupe_sort_totals_toggle_cb), dw);
4743         gtk_box_pack_start(GTK_BOX(controls_box), button, FALSE, FALSE, PREF_PAD_SPACE);
4744         gtk_widget_show(button);
4745
4746         dw->button_rotation_invariant = gtk_check_button_new_with_label(_("Ignore Orientation"));
4747         gtk_widget_set_tooltip_text(GTK_WIDGET(dw->button_rotation_invariant), "Ignore image orientation");
4748         gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(dw->button_rotation_invariant), options->rot_invariant_sim);
4749         g_signal_connect(G_OBJECT(dw->button_rotation_invariant), "toggled",
4750                          G_CALLBACK(dupe_window_rotation_invariant_cb), dw);
4751         gtk_box_pack_start(GTK_BOX(controls_box), dw->button_rotation_invariant, FALSE, FALSE, PREF_PAD_SPACE);
4752         gtk_widget_show(dw->button_rotation_invariant);
4753
4754         button = gtk_check_button_new_with_label(_("Compare two file sets"));
4755         gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(button), dw->second_set);
4756         g_signal_connect(G_OBJECT(button), "toggled",
4757                          G_CALLBACK(dupe_second_set_toggle_cb), dw);
4758         gtk_box_pack_start(GTK_BOX(controls_box), button, FALSE, FALSE, PREF_PAD_SPACE);
4759         gtk_widget_show(button);
4760
4761         button_box = gtk_hbox_new(FALSE, 0);
4762         gtk_box_pack_start(GTK_BOX(vbox), button_box, FALSE, FALSE, 0);
4763         gtk_widget_show(button_box);
4764
4765         hbox = gtk_hbutton_box_new();
4766         gtk_button_box_set_layout(GTK_BUTTON_BOX(hbox), GTK_BUTTONBOX_END);
4767         gtk_box_set_spacing(GTK_BOX(hbox), PREF_PAD_SPACE);
4768         gtk_box_pack_end(GTK_BOX(button_box), hbox, FALSE, FALSE, 0);
4769         gtk_widget_show(hbox);
4770
4771         button = pref_button_new(NULL, GTK_STOCK_HELP, NULL, FALSE, G_CALLBACK(dupe_help_cb), NULL);
4772         gtk_container_add(GTK_CONTAINER(hbox), button);
4773         gtk_widget_set_can_default(button, TRUE);
4774         gtk_widget_show(button);
4775
4776         button = pref_button_new(NULL, GTK_STOCK_STOP, NULL, FALSE, G_CALLBACK(dupe_check_stop_cb), dw);
4777         gtk_container_add(GTK_CONTAINER(hbox), button);
4778         gtk_widget_set_can_default(button, TRUE);
4779         gtk_widget_show(button);
4780
4781         button = pref_button_new(NULL, GTK_STOCK_CLOSE, NULL, FALSE, G_CALLBACK(dupe_window_close_cb), dw);
4782         gtk_container_add(GTK_CONTAINER(hbox), button);
4783         gtk_widget_set_can_default(button, TRUE);
4784         gtk_widget_grab_default(button);
4785         gtk_widget_show(button);
4786         dupe_dnd_init(dw);
4787
4788         /* order is important here, dnd_init should be seeing mouse
4789          * presses before we possibly handle (and stop) the signal
4790          */
4791         g_signal_connect(G_OBJECT(dw->listview), "button_press_event",
4792                          G_CALLBACK(dupe_listview_press_cb), dw);
4793         g_signal_connect(G_OBJECT(dw->listview), "button_release_event",
4794                          G_CALLBACK(dupe_listview_release_cb), dw);
4795         g_signal_connect(G_OBJECT(dw->second_listview), "button_press_event",
4796                          G_CALLBACK(dupe_listview_press_cb), dw);
4797         g_signal_connect(G_OBJECT(dw->second_listview), "button_release_event",
4798                          G_CALLBACK(dupe_listview_release_cb), dw);
4799
4800         gtk_widget_show(dw->window);
4801
4802         dupe_listview_set_height(dw->listview, dw->show_thumbs);
4803         g_signal_emit_by_name(G_OBJECT(dw->combo), "changed");
4804
4805         dupe_window_update_count(dw, TRUE);
4806         dupe_window_update_progress(dw, NULL, 0.0, FALSE);
4807
4808         dupe_window_list = g_list_append(dupe_window_list, dw);
4809
4810         file_data_register_notify_func(dupe_notify_cb, dw, NOTIFY_PRIORITY_MEDIUM);
4811
4812         g_mutex_init(&dw->thread_count_mutex);
4813         g_mutex_init(&dw->search_matches_mutex);
4814         dw->dupe_comparison_thread_pool = g_thread_pool_new(dupe_comparison_func, dw, -1, FALSE, NULL);
4815
4816         return dw;
4817 }
4818
4819 /*
4820  *-------------------------------------------------------------------
4821  * dnd confirm dir
4822  *-------------------------------------------------------------------
4823  */
4824
4825 typedef struct {
4826         DupeWindow *dw;
4827         GList *list;
4828 } CDupeConfirmD;
4829
4830 static void confirm_dir_list_cancel(GtkWidget *widget, gpointer data)
4831 {
4832         /* do nothing */
4833 }
4834
4835 static void confirm_dir_list_add(GtkWidget *widget, gpointer data)
4836 {
4837         CDupeConfirmD *d = data;
4838         GList *work;
4839
4840         dupe_window_add_files(d->dw, d->list, FALSE);
4841
4842         work = d->list;
4843         while (work)
4844                 {
4845                 FileData *fd = work->data;
4846                 work = work->next;
4847                 if (isdir(fd->path))
4848                         {
4849                         GList *list;
4850
4851                         filelist_read(fd, &list, NULL);
4852                         list = filelist_filter(list, FALSE);
4853                         if (list)
4854                                 {
4855                                 dupe_window_add_files(d->dw, list, FALSE);
4856                                 filelist_free(list);
4857                                 }
4858                         }
4859                 }
4860 }
4861
4862 static void confirm_dir_list_recurse(GtkWidget *widget, gpointer data)
4863 {
4864         CDupeConfirmD *d = data;
4865         dupe_window_add_files(d->dw, d->list, TRUE);
4866 }
4867
4868 static void confirm_dir_list_skip(GtkWidget *widget, gpointer data)
4869 {
4870         CDupeConfirmD *d = data;
4871         dupe_window_add_files(d->dw, d->list, FALSE);
4872 }
4873
4874 static void confirm_dir_list_destroy(GtkWidget *widget, gpointer data)
4875 {
4876         CDupeConfirmD *d = data;
4877         filelist_free(d->list);
4878         g_free(d);
4879 }
4880
4881 static GtkWidget *dupe_confirm_dir_list(DupeWindow *dw, GList *list)
4882 {
4883         GtkWidget *menu;
4884         CDupeConfirmD *d;
4885
4886         d = g_new0(CDupeConfirmD, 1);
4887         d->dw = dw;
4888         d->list = list;
4889
4890         menu = popup_menu_short_lived();
4891         g_signal_connect(G_OBJECT(menu), "destroy",
4892                          G_CALLBACK(confirm_dir_list_destroy), d);
4893
4894         menu_item_add_stock(menu, _("Dropped list includes folders."), GTK_STOCK_DND_MULTIPLE, NULL, NULL);
4895         menu_item_add_divider(menu);
4896         menu_item_add_stock(menu, _("_Add contents"), GTK_STOCK_OK, G_CALLBACK(confirm_dir_list_add), d);
4897         menu_item_add_stock(menu, _("Add contents _recursive"), GTK_STOCK_ADD, G_CALLBACK(confirm_dir_list_recurse), d);
4898         menu_item_add_stock(menu, _("_Skip folders"), GTK_STOCK_REMOVE, G_CALLBACK(confirm_dir_list_skip), d);
4899         menu_item_add_divider(menu);
4900         menu_item_add_stock(menu, _("Cancel"), GTK_STOCK_CANCEL, G_CALLBACK(confirm_dir_list_cancel), d);
4901
4902         return menu;
4903 }
4904
4905 /*
4906  *-------------------------------------------------------------------
4907  * dnd
4908  *-------------------------------------------------------------------
4909  */
4910
4911 static GtkTargetEntry dupe_drag_types[] = {
4912         { "text/uri-list", 0, TARGET_URI_LIST },
4913         { "text/plain", 0, TARGET_TEXT_PLAIN }
4914 };
4915 static gint n_dupe_drag_types = 2;
4916
4917 static GtkTargetEntry dupe_drop_types[] = {
4918         { TARGET_APP_COLLECTION_MEMBER_STRING, 0, TARGET_APP_COLLECTION_MEMBER },
4919         { "text/uri-list", 0, TARGET_URI_LIST }
4920 };
4921 static gint n_dupe_drop_types = 2;
4922
4923 static void dupe_dnd_data_set(GtkWidget *widget, GdkDragContext *context,
4924                               GtkSelectionData *selection_data, guint info,
4925                               guint time, gpointer data)
4926 {
4927         DupeWindow *dw = data;
4928         GList *list;
4929
4930         switch (info)
4931                 {
4932                 case TARGET_URI_LIST:
4933                 case TARGET_TEXT_PLAIN:
4934                         list = dupe_listview_get_selection(dw, widget);
4935                         if (!list) return;
4936                         uri_selection_data_set_uris_from_filelist(selection_data, list);
4937                         filelist_free(list);
4938                         break;
4939                 default:
4940                         break;
4941                 }
4942 }
4943
4944 static void dupe_dnd_data_get(GtkWidget *widget, GdkDragContext *context,
4945                               gint x, gint y,
4946                               GtkSelectionData *selection_data, guint info,
4947                               guint time, gpointer data)
4948 {
4949         DupeWindow *dw = data;
4950         GtkWidget *source;
4951         GList *list = NULL;
4952         GList *work;
4953
4954         if (dw->add_files_queue_id > 0)
4955                 {
4956                 warning_dialog(_("Find duplicates"), _("Please wait for the current file selection to be loaded."), GTK_STOCK_DIALOG_INFO, dw->window);
4957
4958                 return;
4959                 }
4960
4961         source = gtk_drag_get_source_widget(context);
4962         if (source == dw->listview || source == dw->second_listview) return;
4963
4964         dw->second_drop = (dw->second_set && widget == dw->second_listview);
4965
4966         switch (info)
4967                 {
4968                 case TARGET_APP_COLLECTION_MEMBER:
4969                         collection_from_dnd_data((gchar *)gtk_selection_data_get_data(selection_data), &list, NULL);
4970                         break;
4971                 case TARGET_URI_LIST:
4972                         list = uri_filelist_from_gtk_selection_data(selection_data);
4973                         work = list;
4974                         while (work)
4975                                 {
4976                                 FileData *fd = work->data;
4977                                 if (isdir(fd->path))
4978                                         {
4979                                         GtkWidget *menu;
4980                                         menu = dupe_confirm_dir_list(dw, list);
4981                                         gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL, 0, time);
4982                                         return;
4983                                         }
4984                                 work = work->next;
4985                                 }
4986                         break;
4987                 default:
4988                         list = NULL;
4989                         break;
4990                 }
4991
4992         if (list)
4993                 {
4994                 dupe_window_add_files(dw, list, FALSE);
4995                 filelist_free(list);
4996                 }
4997 }
4998
4999 static void dupe_dest_set(GtkWidget *widget, gboolean enable)
5000 {
5001         if (enable)
5002                 {
5003                 gtk_drag_dest_set(widget,
5004                         GTK_DEST_DEFAULT_MOTION | GTK_DEST_DEFAULT_HIGHLIGHT | GTK_DEST_DEFAULT_DROP,
5005                         dupe_drop_types, n_dupe_drop_types,
5006                         GDK_ACTION_COPY | GDK_ACTION_MOVE | GDK_ACTION_ASK);
5007
5008                 }
5009         else
5010                 {
5011                 gtk_drag_dest_unset(widget);
5012                 }
5013 }
5014
5015 static void dupe_dnd_begin(GtkWidget *widget, GdkDragContext *context, gpointer data)
5016 {
5017         DupeWindow *dw = data;
5018         dupe_dest_set(dw->listview, FALSE);
5019         dupe_dest_set(dw->second_listview, FALSE);
5020
5021         if (dw->click_item && !dupe_listview_item_is_selected(dw, dw->click_item, widget))
5022                 {
5023                 GtkListStore *store;
5024                 GtkTreeIter iter;
5025
5026                 store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(widget)));
5027                 if (dupe_listview_find_item(store, dw->click_item, &iter) >= 0)
5028                         {
5029                         GtkTreeSelection *selection;
5030                         GtkTreePath *tpath;
5031
5032                         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(widget));
5033                         gtk_tree_selection_unselect_all(selection);
5034                         gtk_tree_selection_select_iter(selection, &iter);
5035
5036                         tpath = gtk_tree_model_get_path(GTK_TREE_MODEL(store), &iter);
5037                         gtk_tree_view_set_cursor(GTK_TREE_VIEW(widget), tpath, NULL, FALSE);
5038                         gtk_tree_path_free(tpath);
5039                         }
5040                 }
5041
5042         if (dw->show_thumbs &&
5043             widget == dw->listview &&
5044             dw->click_item && dw->click_item->pixbuf)
5045                 {
5046                 GtkTreeSelection *selection;
5047                 gint items;
5048
5049                 selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(widget));
5050                 items = gtk_tree_selection_count_selected_rows(selection);
5051                 dnd_set_drag_icon(widget, context, dw->click_item->pixbuf, items);
5052                 }
5053 }
5054
5055 static void dupe_dnd_end(GtkWidget *widget, GdkDragContext *context, gpointer data)
5056 {
5057         DupeWindow *dw = data;
5058         dupe_dest_set(dw->listview, TRUE);
5059         dupe_dest_set(dw->second_listview, TRUE);
5060 }
5061
5062 static void dupe_dnd_init(DupeWindow *dw)
5063 {
5064         gtk_drag_source_set(dw->listview, GDK_BUTTON1_MASK | GDK_BUTTON2_MASK,
5065                             dupe_drag_types, n_dupe_drag_types,
5066                             GDK_ACTION_COPY | GDK_ACTION_MOVE | GDK_ACTION_LINK);
5067         g_signal_connect(G_OBJECT(dw->listview), "drag_data_get",
5068                          G_CALLBACK(dupe_dnd_data_set), dw);
5069         g_signal_connect(G_OBJECT(dw->listview), "drag_begin",
5070                          G_CALLBACK(dupe_dnd_begin), dw);
5071         g_signal_connect(G_OBJECT(dw->listview), "drag_end",
5072                          G_CALLBACK(dupe_dnd_end), dw);
5073
5074         dupe_dest_set(dw->listview, TRUE);
5075         g_signal_connect(G_OBJECT(dw->listview), "drag_data_received",
5076                          G_CALLBACK(dupe_dnd_data_get), dw);
5077
5078         gtk_drag_source_set(dw->second_listview, GDK_BUTTON1_MASK | GDK_BUTTON2_MASK,
5079                             dupe_drag_types, n_dupe_drag_types,
5080                             GDK_ACTION_COPY | GDK_ACTION_MOVE | GDK_ACTION_LINK);
5081         g_signal_connect(G_OBJECT(dw->second_listview), "drag_data_get",
5082                          G_CALLBACK(dupe_dnd_data_set), dw);
5083         g_signal_connect(G_OBJECT(dw->second_listview), "drag_begin",
5084                          G_CALLBACK(dupe_dnd_begin), dw);
5085         g_signal_connect(G_OBJECT(dw->second_listview), "drag_end",
5086                          G_CALLBACK(dupe_dnd_end), dw);
5087
5088         dupe_dest_set(dw->second_listview, TRUE);
5089         g_signal_connect(G_OBJECT(dw->second_listview), "drag_data_received",
5090                          G_CALLBACK(dupe_dnd_data_get), dw);
5091 }
5092
5093 /*
5094  *-------------------------------------------------------------------
5095  * maintenance (move, delete, etc.)
5096  *-------------------------------------------------------------------
5097  */
5098
5099 static void dupe_notify_cb(FileData *fd, NotifyType type, gpointer data)
5100 {
5101         DupeWindow *dw = data;
5102
5103         if (!(type & NOTIFY_CHANGE) || !fd->change) return;
5104
5105         DEBUG_1("Notify dupe: %s %04x", fd->path, type);
5106
5107         switch (fd->change->type)
5108                 {
5109                 case FILEDATA_CHANGE_MOVE:
5110                 case FILEDATA_CHANGE_RENAME:
5111                         dupe_item_update_fd(dw, fd);
5112                         break;
5113                 case FILEDATA_CHANGE_COPY:
5114                         break;
5115                 case FILEDATA_CHANGE_DELETE:
5116                         while (dupe_item_remove_by_path(dw, fd->path));
5117                         break;
5118                 case FILEDATA_CHANGE_UNSPECIFIED:
5119                 case FILEDATA_CHANGE_WRITE_METADATA:
5120                         break;
5121                 }
5122
5123 }
5124
5125 /*
5126  *-------------------------------------------------------------------
5127  * Export duplicates data
5128  *-------------------------------------------------------------------
5129  */
5130
5131  typedef enum {
5132         EXPORT_CSV = 0,
5133         EXPORT_TSV
5134 } SeparatorType;
5135
5136 typedef struct _ExportDupesData ExportDupesData;
5137 struct _ExportDupesData
5138 {
5139         FileDialog *dialog;
5140         SeparatorType separator;
5141         DupeWindow *dupewindow;
5142 };
5143
5144 static void export_duplicates_close(ExportDupesData *edd)
5145 {
5146         if (edd->dialog) file_dialog_close(edd->dialog);
5147         edd->dialog = NULL;
5148 }
5149
5150 static void export_duplicates_data_cancel_cb(FileDialog *fdlg, gpointer data)
5151 {
5152         ExportDupesData *edd = data;
5153
5154         export_duplicates_close(edd);
5155 }
5156
5157 static void export_duplicates_data_save_cb(FileDialog *fdlg, gpointer data)
5158 {
5159         ExportDupesData *edd = data;
5160         GError *error = NULL;
5161         GtkTreeModel *store;
5162         GtkTreeIter iter;
5163         DupeItem *di;
5164         GFileOutputStream *gfstream;
5165         GFile *out_file;
5166         GString *output_string;
5167         gchar *sep;
5168         gchar* rank;
5169         GList *work;
5170         GtkTreeSelection *selection;
5171         GList *slist;
5172         gchar *thumb_cache;
5173         gchar **rank_split;
5174         GtkTreePath *tpath;
5175         gboolean color_old = FALSE;
5176         gboolean color_new = FALSE;
5177         gint match_count;
5178         gchar *name;
5179
5180         history_list_add_to_key("export_duplicates", fdlg->dest_path, -1);
5181
5182         out_file = g_file_new_for_path(fdlg->dest_path);
5183
5184         gfstream = g_file_replace(out_file, NULL, TRUE, G_FILE_CREATE_NONE, NULL, &error);
5185         if (error)
5186                 {
5187                 log_printf(_("Error creating Export duplicates data file: Error: %s\n"), error->message);
5188                 g_error_free(error);
5189                 return;
5190                 }
5191
5192         sep = g_strdup((edd->separator == EXPORT_CSV) ?  "," : "\t");
5193         output_string = g_string_new(g_strjoin(sep, _("Match"), _("Group"), _("Similarity"), _("Set"), _("Thumbnail"), _("Name"), _("Size"), _("Date"), _("Width"), _("Height"), _("Path\n"), NULL));
5194
5195         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(edd->dupewindow->listview));
5196         slist = gtk_tree_selection_get_selected_rows(selection, &store);
5197         work = slist;
5198
5199         tpath = work->data;
5200         gtk_tree_model_get_iter(store, &iter, tpath);
5201         gtk_tree_model_get(GTK_TREE_MODEL(store), &iter, DUPE_COLUMN_COLOR, &color_new, -1);
5202         color_old = !color_new;
5203         match_count = 0;
5204
5205         while (work)
5206                 {
5207                 tpath = work->data;
5208                 gtk_tree_model_get_iter(store, &iter, tpath);
5209
5210                 gtk_tree_model_get(GTK_TREE_MODEL(store), &iter, DUPE_COLUMN_POINTER, &di, -1);
5211
5212                 gtk_tree_model_get(GTK_TREE_MODEL(store), &iter, DUPE_COLUMN_COLOR, &color_new, -1);
5213                 if (color_new != color_old)
5214                         {
5215                         match_count++;
5216                         }
5217                 color_old = color_new;
5218                 output_string = g_string_append(output_string, g_strdup_printf("%d", match_count));
5219                 output_string = g_string_append(output_string, sep);
5220
5221                 if ((dupe_match_find_parent(edd->dupewindow, di) == di))
5222                         {
5223                         output_string = g_string_append(output_string, "1");
5224                         }
5225                 else
5226                         {
5227                         output_string = g_string_append(output_string, "2");
5228                         }
5229                 output_string = g_string_append(output_string, sep);
5230
5231                 gtk_tree_model_get(GTK_TREE_MODEL(store), &iter, DUPE_COLUMN_RANK, &rank, -1);
5232                 rank_split = g_strsplit_set(rank, " [(", -1);
5233                 if (rank_split[0] == NULL)
5234                         {
5235                         output_string = g_string_append(output_string, "");
5236                         }
5237                 else
5238                         {
5239                         output_string = g_string_append(output_string, g_strdup_printf("%s", rank_split[0]));
5240                         }
5241                 output_string = g_string_append(output_string, sep);
5242                 g_free(rank);
5243                 g_strfreev(rank_split);
5244
5245                 output_string = g_string_append(output_string, g_strdup_printf("%d", (di->second + 1)));
5246                 output_string = g_string_append(output_string, sep);
5247
5248                 thumb_cache = cache_find_location(CACHE_TYPE_THUMB, di->fd->path);
5249                 if (thumb_cache)
5250                         {
5251                         output_string = g_string_append(output_string, thumb_cache);
5252                         g_free(thumb_cache);
5253                         }
5254                 else
5255                         {
5256                         output_string = g_string_append(output_string, "");
5257                         }
5258                 output_string = g_string_append(output_string, sep);
5259
5260                 gtk_tree_model_get(GTK_TREE_MODEL(store), &iter, DUPE_COLUMN_NAME, &name, -1);
5261                 output_string = g_string_append(output_string, name);
5262                 output_string = g_string_append(output_string, sep);
5263                 g_free(name);
5264
5265                 output_string = g_string_append(output_string, g_strdup_printf("%"PRIu64, di->fd->size));
5266                 output_string = g_string_append(output_string, sep);
5267                 output_string = g_string_append(output_string, text_from_time(di->fd->date));
5268                 output_string = g_string_append(output_string, sep);
5269                 output_string = g_string_append(output_string, g_strdup_printf("%d", (di->width ? di->width : 0)));
5270                 output_string = g_string_append(output_string, sep);
5271                 output_string = g_string_append(output_string, g_strdup_printf("%d", (di->height ? di->height : 0)));
5272                 output_string = g_string_append(output_string, sep);
5273                 output_string = g_string_append(output_string, di->fd->path);
5274                 output_string = g_string_append_c(output_string, '\n');
5275
5276                 work = work->next;
5277                 }
5278
5279         g_output_stream_write(G_OUTPUT_STREAM(gfstream), output_string->str, strlen(output_string->str), NULL, &error);
5280
5281         g_free(sep);
5282         g_string_free(output_string, TRUE);
5283         g_object_unref(gfstream);
5284         g_object_unref(out_file);
5285
5286         export_duplicates_close(edd);
5287 }
5288
5289 static void pop_menu_export(GList *selection_list, gpointer dupe_window, gpointer data)
5290 {
5291         const gint index = GPOINTER_TO_INT(data);
5292         DupeWindow *dw = dupe_window;
5293         gchar *title = "Export duplicates data";
5294         gchar *default_path = "/tmp/";
5295         gchar *file_extension;
5296         const gchar *stock_id;
5297         ExportDupesData *edd;
5298         const gchar *previous_path;
5299
5300         edd = g_new0(ExportDupesData, 1);
5301         edd->dialog = file_util_file_dlg(title, "export_duplicates", NULL, export_duplicates_data_cancel_cb, edd);
5302
5303         switch (index)
5304                 {
5305                 case EXPORT_CSV:
5306                         edd->separator = EXPORT_CSV;
5307                         file_extension = g_strdup(".csv");
5308                         break;
5309                 case EXPORT_TSV:
5310                         edd->separator = EXPORT_TSV;
5311                         file_extension = g_strdup(".tsv");
5312                         break;
5313                 default:
5314                         return;
5315                 }
5316
5317         stock_id = GTK_STOCK_SAVE;
5318
5319         generic_dialog_add_message(GENERIC_DIALOG(edd->dialog), NULL, title, NULL, FALSE);
5320         file_dialog_add_button(edd->dialog, stock_id, NULL, export_duplicates_data_save_cb, TRUE);
5321
5322         previous_path = history_list_find_last_path_by_key("export_duplicates");
5323
5324         file_dialog_add_path_widgets(edd->dialog, default_path, previous_path, "export_duplicates", file_extension, _("Export Files"));
5325
5326         edd->dupewindow = dw;
5327
5328         gtk_widget_show(GENERIC_DIALOG(edd->dialog)->dialog);
5329
5330         g_free(file_extension);
5331 }
5332
5333 static void dupe_pop_menu_export_cb(GtkWidget *widget, gpointer data)
5334 {
5335         DupeWindow *dw;
5336         GList *selection_list;
5337
5338         dw = submenu_item_get_data(widget);
5339         selection_list = dupe_listview_get_selection(dw, dw->listview);
5340         pop_menu_export(selection_list, dw, data);
5341
5342         filelist_free(selection_list);
5343 }
5344
5345 static GtkWidget *submenu_add_export(GtkWidget *menu, GtkWidget **menu_item, GCallback func, gpointer data)
5346 {
5347         GtkWidget *item;
5348         GtkWidget *submenu;
5349
5350         item = menu_item_add(menu, _("Export"), NULL, NULL);
5351
5352         submenu = gtk_menu_new();
5353         g_object_set_data(G_OBJECT(submenu), "submenu_data", data);
5354
5355         menu_item_add_stock_sensitive(submenu, _("Export to csv"),
5356                                         GTK_STOCK_INDEX, TRUE, G_CALLBACK(func), GINT_TO_POINTER(0));
5357         menu_item_add_stock_sensitive(submenu, _("Export to tab-delimited"),
5358                                         GTK_STOCK_INDEX, TRUE, G_CALLBACK(func), GINT_TO_POINTER(1));
5359
5360         gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), submenu);
5361         if (menu_item) *menu_item = item;
5362
5363         return submenu;
5364 }
5365
5366 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */