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