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