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