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