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