GTK2 no longer supported
[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) g_object_unref(G_OBJECT(cursor));
405 }
406
407 /*
408  * ------------------------------------------------------------------
409  * row color utils
410  * ------------------------------------------------------------------
411  */
412
413 static void dupe_listview_realign_colors(DupeWindow *dw)
414 {
415         GtkTreeModel *store;
416         GtkTreeIter iter;
417         gboolean color_set = TRUE;
418         DupeItem *parent = 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 *UNUSED(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 *UNUSED(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 *UNUSED(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 *UNUSED(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 *UNUSED(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 *UNUSED(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 *UNUSED(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                                         file_data_ref((FileData *)work_file->data);
3017                                         work_file = work_file->next;
3018                                         }
3019                                 g_list_free(f);
3020                                 g_list_free(d);
3021                                 }
3022                         }
3023                 else
3024                         {
3025                         dw->add_files_queue = g_list_prepend(dw->add_files_queue, fd);
3026                         file_data_ref(fd);
3027                         }
3028                 }
3029         if (dw->add_files_queue_id == 0)
3030                 {
3031                 gtk_progress_bar_pulse(GTK_PROGRESS_BAR(dw->extra_label));
3032                 gtk_progress_bar_set_pulse_step(GTK_PROGRESS_BAR(dw->extra_label), DUPE_PROGRESS_PULSE_STEP);
3033                 gtk_progress_bar_set_text(GTK_PROGRESS_BAR(dw->extra_label), _("Loading file list"));
3034
3035                 dupe_init_list_cache(dw);
3036                 dw->add_files_queue_id = g_idle_add(dupe_files_add_queue_cb, dw);
3037                 gtk_widget_set_sensitive(dw->controls_box, FALSE);
3038                 }
3039 }
3040
3041 static void dupe_item_update(DupeWindow *dw, DupeItem *di)
3042 {
3043         if ( (dw->match_mask & DUPE_MATCH_NAME) || (dw->match_mask & DUPE_MATCH_PATH || (dw->match_mask & DUPE_MATCH_NAME_CI)) )
3044                 {
3045                 /* only effects matches on name or path */
3046 /*
3047                 FileData *fd = file_data_ref(di->fd);
3048                 gint second;
3049
3050                 second = di->second;
3051                 dupe_item_remove(dw, di);
3052
3053                 dw->second_drop = second;
3054                 dupe_files_add(dw, NULL, NULL, fd, FALSE);
3055                 dw->second_drop = FALSE;
3056
3057                 file_data_unref(fd);
3058 */
3059                 dupe_check_start(dw);
3060                 }
3061         else
3062                 {
3063                 GtkListStore *store;
3064                 GtkTreeIter iter;
3065                 gint row;
3066                 /* update the listview(s) */
3067
3068                 store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->listview)));
3069                 row = dupe_listview_find_item(store, di, &iter);
3070                 if (row >= 0)
3071                         {
3072                         gtk_list_store_set(store, &iter,
3073                                            DUPE_COLUMN_NAME, di->fd->name,
3074                                            DUPE_COLUMN_PATH, di->fd->path, -1);
3075                         }
3076
3077                 if (dw->second_listview)
3078                         {
3079                         store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->second_listview)));
3080                         row = dupe_listview_find_item(store, di, &iter);
3081                         if (row >= 0)
3082                                 {
3083                                 gtk_list_store_set(store, &iter, 1, di->fd->path, -1);
3084                                 }
3085                         }
3086                 }
3087
3088 }
3089
3090 static void dupe_item_update_fd_in_list(DupeWindow *dw, FileData *fd, GList *work)
3091 {
3092         while (work)
3093                 {
3094                 DupeItem *di = work->data;
3095
3096                 if (di->fd == fd)
3097                         dupe_item_update(dw, di);
3098
3099                 work = work->next;
3100                 }
3101 }
3102
3103 static void dupe_item_update_fd(DupeWindow *dw, FileData *fd)
3104 {
3105         dupe_item_update_fd_in_list(dw, fd, dw->list);
3106         if (dw->second_set) dupe_item_update_fd_in_list(dw, fd, dw->second_list);
3107 }
3108
3109
3110 /*
3111  * ------------------------------------------------------------------
3112  * Misc.
3113  * ------------------------------------------------------------------
3114  */
3115
3116 static GtkWidget *dupe_display_label(GtkWidget *vbox, const gchar *description, const gchar *text)
3117 {
3118         GtkWidget *hbox;
3119         GtkWidget *label;
3120
3121         hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10);
3122
3123         label = gtk_label_new(description);
3124         gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 0);
3125         gtk_widget_show(label);
3126
3127         label = gtk_label_new(text);
3128         gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 0);
3129         gtk_widget_show(label);
3130
3131         gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0);
3132         gtk_widget_show(hbox);
3133
3134         return label;
3135 }
3136
3137 static void dupe_display_stats(DupeWindow *dw, DupeItem *di)
3138 {
3139         GenericDialog *gd;
3140         gchar *buf;
3141
3142         if (!di) return;
3143
3144         gd = file_util_gen_dlg("Image thumbprint debug info", "thumbprint",
3145                                dw->window, TRUE,
3146                                NULL, NULL);
3147         generic_dialog_add_button(gd, GTK_STOCK_CLOSE, NULL, NULL, TRUE);
3148
3149         dupe_display_label(gd->vbox, "name:", di->fd->name);
3150         buf = text_from_size(di->fd->size);
3151         dupe_display_label(gd->vbox, "size:", buf);
3152         g_free(buf);
3153         dupe_display_label(gd->vbox, "date:", text_from_time(di->fd->date));
3154         buf = g_strdup_printf("%d x %d", di->width, di->height);
3155         dupe_display_label(gd->vbox, "dimensions:", buf);
3156         g_free(buf);
3157         dupe_display_label(gd->vbox, "md5sum:", (di->md5sum) ? di->md5sum : "not generated");
3158
3159         dupe_display_label(gd->vbox, "thumbprint:", (di->simd) ? "" : "not generated");
3160         if (di->simd)
3161                 {
3162                 GtkWidget *image;
3163                 GdkPixbuf *pixbuf;
3164                 gint x, y;
3165                 guchar *d_pix;
3166                 guchar *dp;
3167                 gint rs;
3168                 gint sp;
3169
3170                 pixbuf = gdk_pixbuf_new(GDK_COLORSPACE_RGB, FALSE, 8, 32, 32);
3171                 rs = gdk_pixbuf_get_rowstride(pixbuf);
3172                 d_pix = gdk_pixbuf_get_pixels(pixbuf);
3173
3174                 for (y = 0; y < 32; y++)
3175                         {
3176                         dp = d_pix + (y * rs);
3177                         sp = y * 32;
3178                         for (x = 0; x < 32; x++)
3179                                 {
3180                                 *(dp++) = di->simd->avg_r[sp + x];
3181                                 *(dp++) = di->simd->avg_g[sp + x];
3182                                 *(dp++) = di->simd->avg_b[sp + x];
3183                                 }
3184                         }
3185
3186                 image = gtk_image_new_from_pixbuf(pixbuf);
3187                 gtk_box_pack_start(GTK_BOX(gd->vbox), image, FALSE, FALSE, 0);
3188                 gtk_widget_show(image);
3189
3190                 g_object_unref(pixbuf);
3191                 }
3192
3193         gtk_widget_show(gd->dialog);
3194 }
3195
3196 static void dupe_window_recompare(DupeWindow *dw)
3197 {
3198         GtkListStore *store;
3199
3200         dupe_check_stop(dw);
3201
3202         store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->listview)));
3203         gtk_list_store_clear(store);
3204
3205         g_list_free(dw->dupes);
3206         dw->dupes = NULL;
3207
3208         dupe_match_reset_list(dw->list);
3209         dupe_match_reset_list(dw->second_list);
3210         dw->set_count = 0;
3211
3212         dupe_check_start(dw);
3213 }
3214
3215 static void dupe_menu_view(DupeWindow *dw, DupeItem *di, GtkWidget *listview, gint new_window)
3216 {
3217         if (!di) return;
3218
3219         if (di->collection && collection_info_valid(di->collection, di->info))
3220                 {
3221                 if (new_window)
3222                         {
3223                         view_window_new_from_collection(di->collection, di->info);
3224                         }
3225                 else
3226                         {
3227                         layout_image_set_collection(NULL, di->collection, di->info);
3228                         }
3229                 }
3230         else
3231                 {
3232                 if (new_window)
3233                         {
3234                         GList *list;
3235
3236                         list = dupe_listview_get_selection(dw, listview);
3237                         view_window_new_from_list(list);
3238                         filelist_free(list);
3239                         }
3240                 else
3241                         {
3242                         layout_set_fd(NULL, di->fd);
3243                         }
3244                 }
3245 }
3246
3247 static void dupe_window_remove_selection(DupeWindow *dw, GtkWidget *listview)
3248 {
3249         GtkTreeSelection *selection;
3250         GtkTreeModel *store;
3251         GtkTreeIter iter;
3252         GList *slist;
3253         GList *list = NULL;
3254         GList *work;
3255
3256         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(listview));
3257         slist = gtk_tree_selection_get_selected_rows(selection, &store);
3258         work = slist;
3259         while (work)
3260                 {
3261                 GtkTreePath *tpath = work->data;
3262                 DupeItem *di = NULL;
3263
3264                 gtk_tree_model_get_iter(store, &iter, tpath);
3265                 gtk_tree_model_get(store, &iter, DUPE_COLUMN_POINTER, &di, -1);
3266                 if (di) list = g_list_prepend(list, di);
3267                 work = work->next;
3268                 }
3269         g_list_foreach(slist, (GFunc)tree_path_free_wrapper, NULL);
3270         g_list_free(slist);
3271
3272         dw->color_frozen = TRUE;
3273         work = list;
3274         while (work)
3275                 {
3276                 DupeItem *di;
3277
3278                 di = work->data;
3279                 work = work->next;
3280                 dupe_item_remove(dw, di);
3281                 }
3282         dw->color_frozen = FALSE;
3283
3284         g_list_free(list);
3285
3286         dupe_listview_realign_colors(dw);
3287 }
3288
3289 static void dupe_window_edit_selected(DupeWindow *dw, const gchar *key)
3290 {
3291         file_util_start_editor_from_filelist(key, dupe_listview_get_selection(dw, dw->listview), NULL, dw->window);
3292 }
3293
3294 static void dupe_window_collection_from_selection(DupeWindow *dw)
3295 {
3296         CollectWindow *w;
3297         GList *list;
3298
3299         list = dupe_listview_get_selection(dw, dw->listview);
3300         w = collection_window_new(NULL);
3301         collection_table_add_filelist(w->table, list);
3302         filelist_free(list);
3303 }
3304
3305 static void dupe_window_append_file_list(DupeWindow *dw, gint on_second)
3306 {
3307         GList *list;
3308
3309         dw->second_drop = (dw->second_set && on_second);
3310
3311         list = layout_list(NULL);
3312         dupe_window_add_files(dw, list, FALSE);
3313         filelist_free(list);
3314 }
3315
3316 /*
3317  *-------------------------------------------------------------------
3318  * main pop-up menu callbacks
3319  *-------------------------------------------------------------------
3320  */
3321
3322 static void dupe_menu_view_cb(GtkWidget *UNUSED(widget), gpointer data)
3323 {
3324         DupeWindow *dw = data;
3325
3326         if (dw->click_item) dupe_menu_view(dw, dw->click_item, dw->listview, FALSE);
3327 }
3328
3329 static void dupe_menu_viewnew_cb(GtkWidget *UNUSED(widget), gpointer data)
3330 {
3331         DupeWindow *dw = data;
3332
3333         if (dw->click_item) dupe_menu_view(dw, dw->click_item, dw->listview, TRUE);
3334 }
3335
3336 static void dupe_menu_select_all_cb(GtkWidget *UNUSED(widget), gpointer data)
3337 {
3338         DupeWindow *dw = data;
3339         GtkTreeSelection *selection;
3340
3341         options->duplicates_select_type = DUPE_SELECT_NONE;
3342         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(dw->listview));
3343         gtk_tree_selection_select_all(selection);
3344 }
3345
3346 static void dupe_menu_select_none_cb(GtkWidget *UNUSED(widget), gpointer data)
3347 {
3348         DupeWindow *dw = data;
3349         GtkTreeSelection *selection;
3350
3351         options->duplicates_select_type = DUPE_SELECT_NONE;
3352         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(dw->listview));
3353         gtk_tree_selection_unselect_all(selection);
3354 }
3355
3356 static void dupe_menu_select_dupes_set1_cb(GtkWidget *UNUSED(widget), gpointer data)
3357 {
3358         DupeWindow *dw = data;
3359
3360         options->duplicates_select_type = DUPE_SELECT_GROUP1;
3361         dupe_listview_select_dupes(dw, DUPE_SELECT_GROUP1);
3362 }
3363
3364 static void dupe_menu_select_dupes_set2_cb(GtkWidget *UNUSED(widget), gpointer data)
3365 {
3366         DupeWindow *dw = data;
3367
3368         options->duplicates_select_type = DUPE_SELECT_GROUP2;
3369         dupe_listview_select_dupes(dw, DUPE_SELECT_GROUP2);
3370 }
3371
3372 static void dupe_menu_edit_cb(GtkWidget *widget, gpointer data)
3373 {
3374         DupeWindow *dw;
3375         const gchar *key = data;
3376
3377         dw = submenu_item_get_data(widget);
3378         if (!dw) return;
3379
3380         dupe_window_edit_selected(dw, key);
3381 }
3382
3383 static void dupe_menu_print_cb(GtkWidget *UNUSED(widget), gpointer data)
3384 {
3385         DupeWindow *dw = data;
3386         FileData *fd;
3387
3388         fd = (dw->click_item) ? dw->click_item->fd : NULL;
3389
3390         print_window_new(fd,
3391                          dupe_listview_get_selection(dw, dw->listview),
3392                          dupe_listview_get_filelist(dw, dw->listview), dw->window);
3393 }
3394
3395 static void dupe_menu_copy_cb(GtkWidget *UNUSED(widget), gpointer data)
3396 {
3397         DupeWindow *dw = data;
3398
3399         file_util_copy(NULL, dupe_listview_get_selection(dw, dw->listview), NULL, dw->window);
3400 }
3401
3402 static void dupe_menu_move_cb(GtkWidget *UNUSED(widget), gpointer data)
3403 {
3404         DupeWindow *dw = data;
3405
3406         file_util_move(NULL, dupe_listview_get_selection(dw, dw->listview), NULL, dw->window);
3407 }
3408
3409 static void dupe_menu_rename_cb(GtkWidget *UNUSED(widget), gpointer data)
3410 {
3411         DupeWindow *dw = data;
3412
3413         file_util_rename(NULL, dupe_listview_get_selection(dw, dw->listview), dw->window);
3414 }
3415
3416 static void dupe_menu_delete_cb(GtkWidget *UNUSED(widget), gpointer data)
3417 {
3418         DupeWindow *dw = data;
3419
3420         options->file_ops.safe_delete_enable = FALSE;
3421         file_util_delete_notify_done(NULL, dupe_listview_get_selection(dw, dw->listview), dw->window, delete_finished_cb, dw);
3422 }
3423
3424 static void dupe_menu_move_to_trash_cb(GtkWidget *UNUSED(widget), gpointer data)
3425 {
3426         DupeWindow *dw = data;
3427
3428         options->file_ops.safe_delete_enable = TRUE;
3429         file_util_delete_notify_done(NULL, dupe_listview_get_selection(dw, dw->listview), dw->window, delete_finished_cb, dw);
3430 }
3431
3432 static void dupe_menu_copy_path_cb(GtkWidget *UNUSED(widget), gpointer data)
3433 {
3434         DupeWindow *dw = data;
3435
3436         file_util_copy_path_list_to_clipboard(dupe_listview_get_selection(dw, dw->listview), TRUE);
3437 }
3438
3439 static void dupe_menu_copy_path_unquoted_cb(GtkWidget *UNUSED(widget), gpointer data)
3440 {
3441         DupeWindow *dw = data;
3442
3443         file_util_copy_path_list_to_clipboard(dupe_listview_get_selection(dw, dw->listview), FALSE);
3444 }
3445
3446 static void dupe_menu_remove_cb(GtkWidget *UNUSED(widget), gpointer data)
3447 {
3448         DupeWindow *dw = data;
3449
3450         dupe_window_remove_selection(dw, dw->listview);
3451 }
3452
3453 static void dupe_menu_clear_cb(GtkWidget *UNUSED(widget), gpointer data)
3454 {
3455         DupeWindow *dw = data;
3456
3457         dupe_window_clear(dw);
3458 }
3459
3460 static void dupe_menu_close_cb(GtkWidget *UNUSED(widget), gpointer data)
3461 {
3462         DupeWindow *dw = data;
3463
3464         dupe_window_close(dw);
3465 }
3466
3467 static void dupe_menu_popup_destroy_cb(GtkWidget *UNUSED(widget), gpointer data)
3468 {
3469         GList *editmenu_fd_list = data;
3470
3471         filelist_free(editmenu_fd_list);
3472 }
3473
3474 static GList *dupe_window_get_fd_list(DupeWindow *dw)
3475 {
3476         GList *list;
3477
3478         if (gtk_widget_has_focus(dw->second_listview))
3479                 {
3480                 list = dupe_listview_get_selection(dw, dw->second_listview);
3481                 }
3482         else
3483                 {
3484                 list = dupe_listview_get_selection(dw, dw->listview);
3485                 }
3486
3487         return list;
3488 }
3489
3490 /**
3491  * @brief Add file selection list to a collection
3492  * @param[in] widget 
3493  * @param[in] data Index to the collection list menu item selected, or -1 for new collection
3494  * 
3495  * 
3496  */
3497 static void dupe_pop_menu_collections_cb(GtkWidget *widget, gpointer data)
3498 {
3499         DupeWindow *dw;
3500         GList *selection_list;
3501
3502         dw = submenu_item_get_data(widget);
3503         selection_list = dupe_listview_get_selection(dw, dw->listview);
3504         pop_menu_collections(selection_list, data);
3505
3506         filelist_free(selection_list);
3507 }
3508
3509 static GtkWidget *dupe_menu_popup_main(DupeWindow *dw, DupeItem *di)
3510 {
3511         GtkWidget *menu;
3512         GtkWidget *item;
3513         gint on_row;
3514         GList *editmenu_fd_list;
3515         GtkAccelGroup *accel_group;
3516
3517         on_row = (di != NULL);
3518
3519         menu = popup_menu_short_lived();
3520
3521         accel_group = gtk_accel_group_new();
3522         gtk_menu_set_accel_group(GTK_MENU(menu), accel_group);
3523
3524         g_object_set_data(G_OBJECT(menu), "window_keys", dupe_window_keys);
3525         g_object_set_data(G_OBJECT(menu), "accel_group", accel_group);
3526
3527         menu_item_add_sensitive(menu, _("_View"), on_row,
3528                                 G_CALLBACK(dupe_menu_view_cb), dw);
3529         menu_item_add_stock_sensitive(menu, _("View in _new window"), GTK_STOCK_NEW, on_row,
3530                                 G_CALLBACK(dupe_menu_viewnew_cb), dw);
3531         menu_item_add_divider(menu);
3532         menu_item_add_sensitive(menu, _("Select all"), (dw->dupes != NULL),
3533                                 G_CALLBACK(dupe_menu_select_all_cb), dw);
3534         menu_item_add_sensitive(menu, _("Select none"), (dw->dupes != NULL),
3535                                 G_CALLBACK(dupe_menu_select_none_cb), dw);
3536         menu_item_add_sensitive(menu, _("Select group _1 duplicates"), (dw->dupes != NULL),
3537                                 G_CALLBACK(dupe_menu_select_dupes_set1_cb), dw);
3538         menu_item_add_sensitive(menu, _("Select group _2 duplicates"), (dw->dupes != NULL),
3539                                 G_CALLBACK(dupe_menu_select_dupes_set2_cb), dw);
3540         menu_item_add_divider(menu);
3541
3542         submenu_add_export(menu, &item, G_CALLBACK(dupe_pop_menu_export_cb), dw);
3543         gtk_widget_set_sensitive(item, on_row);
3544         menu_item_add_divider(menu);
3545
3546         editmenu_fd_list = dupe_window_get_fd_list(dw);
3547         g_signal_connect(G_OBJECT(menu), "destroy",
3548                          G_CALLBACK(dupe_menu_popup_destroy_cb), editmenu_fd_list);
3549         submenu_add_edit(menu, &item, G_CALLBACK(dupe_menu_edit_cb), dw, editmenu_fd_list);
3550         if (!on_row) gtk_widget_set_sensitive(item, FALSE);
3551
3552         submenu_add_collections(menu, &item,
3553                                                                 G_CALLBACK(dupe_pop_menu_collections_cb), dw);
3554         gtk_widget_set_sensitive(item, on_row);
3555
3556         menu_item_add_stock_sensitive(menu, _("Print..."), GTK_STOCK_PRINT, on_row,
3557                                 G_CALLBACK(dupe_menu_print_cb), dw);
3558         menu_item_add_divider(menu);
3559         menu_item_add_stock_sensitive(menu, _("_Copy..."), GTK_STOCK_COPY, on_row,
3560                                 G_CALLBACK(dupe_menu_copy_cb), dw);
3561         menu_item_add_sensitive(menu, _("_Move..."), on_row,
3562                                 G_CALLBACK(dupe_menu_move_cb), dw);
3563         menu_item_add_sensitive(menu, _("_Rename..."), on_row,
3564                                 G_CALLBACK(dupe_menu_rename_cb), dw);
3565         menu_item_add_sensitive(menu, _("_Copy path"), on_row,
3566                                 G_CALLBACK(dupe_menu_copy_path_cb), dw);
3567         menu_item_add_sensitive(menu, _("_Copy path unquoted"), on_row,
3568                                 G_CALLBACK(dupe_menu_copy_path_unquoted_cb), dw);
3569
3570         menu_item_add_divider(menu);
3571         menu_item_add_stock_sensitive(menu,
3572                                 options->file_ops.confirm_move_to_trash ? _("Move to Trash...") :
3573                                         _("Move to Trash"), PIXBUF_INLINE_ICON_TRASH, on_row,
3574                                 G_CALLBACK(dupe_menu_move_to_trash_cb), dw);
3575         menu_item_add_stock_sensitive(menu,
3576                                 options->file_ops.confirm_delete ? _("_Delete...") :
3577                                         _("_Delete"), GTK_STOCK_DELETE, on_row,
3578                                 G_CALLBACK(dupe_menu_delete_cb), dw);
3579
3580         menu_item_add_divider(menu);
3581         menu_item_add_stock_sensitive(menu, _("Rem_ove"), GTK_STOCK_REMOVE, on_row,
3582                                 G_CALLBACK(dupe_menu_remove_cb), dw);
3583         menu_item_add_stock_sensitive(menu, _("C_lear"), GTK_STOCK_CLEAR, (dw->list != NULL),
3584                                 G_CALLBACK(dupe_menu_clear_cb), dw);
3585         menu_item_add_divider(menu);
3586         menu_item_add_stock(menu, _("Close _window"), GTK_STOCK_CLOSE,
3587                             G_CALLBACK(dupe_menu_close_cb), dw);
3588
3589         return menu;
3590 }
3591
3592 static gboolean dupe_listview_press_cb(GtkWidget *widget, GdkEventButton *bevent, gpointer data)
3593 {
3594         DupeWindow *dw = data;
3595         GtkTreeModel *store;
3596         GtkTreePath *tpath;
3597         GtkTreeIter iter;
3598         DupeItem *di = NULL;
3599
3600         store = gtk_tree_view_get_model(GTK_TREE_VIEW(widget));
3601
3602         if (gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(widget), bevent->x, bevent->y,
3603                                           &tpath, NULL, NULL, NULL))
3604                 {
3605                 gtk_tree_model_get_iter(store, &iter, tpath);
3606                 gtk_tree_model_get(store, &iter, DUPE_COLUMN_POINTER, &di, -1);
3607                 gtk_tree_path_free(tpath);
3608                 }
3609
3610         dw->click_item = di;
3611
3612         if (bevent->button == MOUSE_BUTTON_RIGHT)
3613                 {
3614                 /* right click menu */
3615                 GtkWidget *menu;
3616
3617                 if (bevent->state & GDK_CONTROL_MASK && bevent->state & GDK_SHIFT_MASK)
3618                         {
3619                         dupe_display_stats(dw, di);
3620                         return TRUE;
3621                         }
3622                 if (widget == dw->listview)
3623                         {
3624                         menu = dupe_menu_popup_main(dw, di);
3625                         }
3626                 else
3627                         {
3628                         menu = dupe_menu_popup_second(dw, di);
3629                         }
3630                 gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL, bevent->button, bevent->time);
3631                 }
3632
3633         if (!di) return FALSE;
3634
3635         if (bevent->button == MOUSE_BUTTON_LEFT &&
3636             bevent->type == GDK_2BUTTON_PRESS)
3637                 {
3638                 dupe_menu_view(dw, di, widget, FALSE);
3639                 }
3640
3641         if (bevent->button == MOUSE_BUTTON_MIDDLE) return TRUE;
3642
3643         if (bevent->button == MOUSE_BUTTON_RIGHT)
3644                 {
3645                 if (!dupe_listview_item_is_selected(dw, di, widget))
3646                         {
3647                         GtkTreeSelection *selection;
3648
3649                         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(widget));
3650                         gtk_tree_selection_unselect_all(selection);
3651                         gtk_tree_selection_select_iter(selection, &iter);
3652
3653                         tpath = gtk_tree_model_get_path(GTK_TREE_MODEL(store), &iter);
3654                         gtk_tree_view_set_cursor(GTK_TREE_VIEW(widget), tpath, NULL, FALSE);
3655                         gtk_tree_path_free(tpath);
3656                         }
3657
3658                 return TRUE;
3659                 }
3660
3661         if (bevent->button == MOUSE_BUTTON_LEFT &&
3662             bevent->type == GDK_BUTTON_PRESS &&
3663             !(bevent->state & GDK_SHIFT_MASK ) &&
3664             !(bevent->state & GDK_CONTROL_MASK ) &&
3665             dupe_listview_item_is_selected(dw, di, widget))
3666                 {
3667                 /* this selection handled on release_cb */
3668                 gtk_widget_grab_focus(widget);
3669                 return TRUE;
3670                 }
3671
3672         return FALSE;
3673 }
3674
3675 static gboolean dupe_listview_release_cb(GtkWidget *widget, GdkEventButton *bevent, gpointer data)
3676 {
3677         DupeWindow *dw = data;
3678         GtkTreeModel *store;
3679         GtkTreePath *tpath;
3680         GtkTreeIter iter;
3681         DupeItem *di = NULL;
3682
3683         if (bevent->button != MOUSE_BUTTON_LEFT && bevent->button != MOUSE_BUTTON_MIDDLE) return TRUE;
3684
3685         store = gtk_tree_view_get_model(GTK_TREE_VIEW(widget));
3686
3687         if ((bevent->x != 0 || bevent->y != 0) &&
3688             gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(widget), bevent->x, bevent->y,
3689                                           &tpath, NULL, NULL, NULL))
3690                 {
3691                 gtk_tree_model_get_iter(store, &iter, tpath);
3692                 gtk_tree_model_get(store, &iter, DUPE_COLUMN_POINTER, &di, -1);
3693                 gtk_tree_path_free(tpath);
3694                 }
3695
3696         if (bevent->button == MOUSE_BUTTON_MIDDLE)
3697                 {
3698                 if (di && dw->click_item == di)
3699                         {
3700                         GtkTreeSelection *selection;
3701
3702                         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(widget));
3703                         if (dupe_listview_item_is_selected(dw, di, widget))
3704                                 {
3705                                 gtk_tree_selection_unselect_iter(selection, &iter);
3706                                 }
3707                         else
3708                                 {
3709                                 gtk_tree_selection_select_iter(selection, &iter);
3710                                 }
3711                         }
3712                 return TRUE;
3713                 }
3714
3715         if (di && dw->click_item == di &&
3716             !(bevent->state & GDK_SHIFT_MASK ) &&
3717             !(bevent->state & GDK_CONTROL_MASK ) &&
3718             dupe_listview_item_is_selected(dw, di, widget))
3719                 {
3720                 GtkTreeSelection *selection;
3721
3722                 selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(widget));
3723                 gtk_tree_selection_unselect_all(selection);
3724                 gtk_tree_selection_select_iter(selection, &iter);
3725
3726                 tpath = gtk_tree_model_get_path(store, &iter);
3727                 gtk_tree_view_set_cursor(GTK_TREE_VIEW(widget), tpath, NULL, FALSE);
3728                 gtk_tree_path_free(tpath);
3729
3730                 return TRUE;
3731                 }
3732
3733         return FALSE;
3734 }
3735
3736 /*
3737  *-------------------------------------------------------------------
3738  * second set stuff
3739  *-------------------------------------------------------------------
3740  */
3741
3742 static void dupe_second_update_status(DupeWindow *dw)
3743 {
3744         gchar *buf;
3745
3746         buf = g_strdup_printf(_("%d files (set 2)"), g_list_length(dw->second_list));
3747         gtk_label_set_text(GTK_LABEL(dw->second_status_label), buf);
3748         g_free(buf);
3749 }
3750
3751 static void dupe_second_add(DupeWindow *dw, DupeItem *di)
3752 {
3753         GtkListStore *store;
3754         GtkTreeIter iter;
3755
3756         if (!di) return;
3757
3758         di->second = TRUE;
3759         dw->second_list = g_list_prepend(dw->second_list, di);
3760
3761         store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->second_listview)));
3762         gtk_list_store_append(store, &iter);
3763         gtk_list_store_set(store, &iter, DUPE_COLUMN_POINTER, di, 1, di->fd->path, -1);
3764
3765         dupe_second_update_status(dw);
3766 }
3767
3768 static void dupe_second_remove(DupeWindow *dw, DupeItem *di)
3769 {
3770         GtkListStore *store;
3771         GtkTreeIter iter;
3772
3773         store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->second_listview)));
3774         if (dupe_listview_find_item(store, di, &iter) >= 0)
3775                 {
3776                 tree_view_move_cursor_away(GTK_TREE_VIEW(dw->second_listview), &iter, TRUE);
3777                 gtk_list_store_remove(store, &iter);
3778                 }
3779
3780         dw->second_list = g_list_remove(dw->second_list, di);
3781
3782         dupe_second_update_status(dw);
3783 }
3784
3785 static void dupe_second_clear(DupeWindow *dw)
3786 {
3787         GtkListStore *store;
3788
3789         store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->second_listview)));
3790         gtk_list_store_clear(store);
3791         gtk_tree_view_columns_autosize(GTK_TREE_VIEW(dw->second_listview));
3792
3793         g_list_free(dw->dupes);
3794         dw->dupes = NULL;
3795
3796         dupe_list_free(dw->second_list);
3797         dw->second_list = NULL;
3798
3799         dupe_match_reset_list(dw->list);
3800
3801         dupe_second_update_status(dw);
3802 }
3803
3804 static void dupe_second_menu_view_cb(GtkWidget *UNUSED(widget), gpointer data)
3805 {
3806         DupeWindow *dw = data;
3807
3808         if (dw->click_item) dupe_menu_view(dw, dw->click_item, dw->second_listview, FALSE);
3809 }
3810
3811 static void dupe_second_menu_viewnew_cb(GtkWidget *UNUSED(widget), gpointer data)
3812 {
3813         DupeWindow *dw = data;
3814
3815         if (dw->click_item) dupe_menu_view(dw, dw->click_item, dw->second_listview, TRUE);
3816 }
3817
3818 static void dupe_second_menu_select_all_cb(GtkWidget *UNUSED(widget), gpointer data)
3819 {
3820         GtkTreeSelection *selection;
3821         DupeWindow *dw = data;
3822
3823         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(dw->second_listview));
3824         gtk_tree_selection_select_all(selection);
3825 }
3826
3827 static void dupe_second_menu_select_none_cb(GtkWidget *UNUSED(widget), gpointer data)
3828 {
3829         GtkTreeSelection *selection;
3830         DupeWindow *dw = data;
3831
3832         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(dw->second_listview));
3833         gtk_tree_selection_unselect_all(selection);
3834 }
3835
3836 static void dupe_second_menu_remove_cb(GtkWidget *UNUSED(widget), gpointer data)
3837 {
3838         DupeWindow *dw = data;
3839
3840         dupe_window_remove_selection(dw, dw->second_listview);
3841 }
3842
3843 static void dupe_second_menu_clear_cb(GtkWidget *UNUSED(widget), gpointer data)
3844 {
3845         DupeWindow *dw = data;
3846
3847         dupe_second_clear(dw);
3848         dupe_window_recompare(dw);
3849 }
3850
3851 static GtkWidget *dupe_menu_popup_second(DupeWindow *dw, DupeItem *di)
3852 {
3853         GtkWidget *menu;
3854         gboolean notempty = (dw->second_list != NULL);
3855         gboolean on_row = (di != NULL);
3856         GtkAccelGroup *accel_group;
3857
3858         menu = popup_menu_short_lived();
3859         accel_group = gtk_accel_group_new();
3860         gtk_menu_set_accel_group(GTK_MENU(menu), accel_group);
3861
3862         g_object_set_data(G_OBJECT(menu), "window_keys", dupe_window_keys);
3863         g_object_set_data(G_OBJECT(menu), "accel_group", accel_group);
3864
3865         menu_item_add_sensitive(menu, _("_View"), on_row,
3866                                 G_CALLBACK(dupe_second_menu_view_cb), dw);
3867         menu_item_add_stock_sensitive(menu, _("View in _new window"), GTK_STOCK_NEW, on_row,
3868                                 G_CALLBACK(dupe_second_menu_viewnew_cb), dw);
3869         menu_item_add_divider(menu);
3870         menu_item_add_sensitive(menu, _("Select all"), notempty,
3871                                 G_CALLBACK(dupe_second_menu_select_all_cb), dw);
3872         menu_item_add_sensitive(menu, _("Select none"), notempty,
3873                                 G_CALLBACK(dupe_second_menu_select_none_cb), dw);
3874         menu_item_add_divider(menu);
3875         menu_item_add_stock_sensitive(menu, _("Rem_ove"), GTK_STOCK_REMOVE, on_row,
3876                                       G_CALLBACK(dupe_second_menu_remove_cb), dw);
3877         menu_item_add_stock_sensitive(menu, _("C_lear"), GTK_STOCK_CLEAR, notempty,
3878                                    G_CALLBACK(dupe_second_menu_clear_cb), dw);
3879         menu_item_add_divider(menu);
3880         menu_item_add_stock(menu, _("Close _window"), GTK_STOCK_CLOSE,
3881                             G_CALLBACK(dupe_menu_close_cb), dw);
3882
3883         return menu;
3884 }
3885
3886 static void dupe_second_set_toggle_cb(GtkWidget *widget, gpointer data)
3887 {
3888         DupeWindow *dw = data;
3889
3890         dw->second_set = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget));
3891
3892         if (dw->second_set)
3893                 {
3894                 dupe_second_update_status(dw);
3895                 gtk_table_set_col_spacings(GTK_TABLE(dw->table), PREF_PAD_GAP);
3896                 gtk_widget_show(dw->second_vbox);
3897                 }
3898         else
3899                 {
3900                 gtk_table_set_col_spacings(GTK_TABLE(dw->table), 0);
3901                 gtk_widget_hide(dw->second_vbox);
3902                 dupe_second_clear(dw);
3903                 }
3904
3905         dupe_window_recompare(dw);
3906 }
3907
3908 static void dupe_sort_totals_toggle_cb(GtkWidget *widget, gpointer data)
3909 {
3910         DupeWindow *dw = data;
3911
3912         options->sort_totals = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget));
3913         dupe_window_recompare(dw);
3914
3915 }
3916
3917 /*
3918  *-------------------------------------------------------------------
3919  * match type menu
3920  *-------------------------------------------------------------------
3921  */
3922
3923 enum {
3924         DUPE_MENU_COLUMN_NAME = 0,
3925         DUPE_MENU_COLUMN_MASK
3926 };
3927
3928 static void dupe_listview_show_rank(GtkWidget *listview, gboolean rank);
3929
3930 static void dupe_menu_type_cb(GtkWidget *combo, gpointer data)
3931 {
3932         DupeWindow *dw = data;
3933         GtkTreeModel *store;
3934         GtkTreeIter iter;
3935
3936         store = gtk_combo_box_get_model(GTK_COMBO_BOX(combo));
3937         if (!gtk_combo_box_get_active_iter(GTK_COMBO_BOX(combo), &iter)) return;
3938         gtk_tree_model_get(store, &iter, DUPE_MENU_COLUMN_MASK, &dw->match_mask, -1);
3939
3940         options->duplicates_match = dw->match_mask;
3941
3942         if (dw->match_mask & (DUPE_MATCH_SIM_HIGH | DUPE_MATCH_SIM_MED | DUPE_MATCH_SIM_LOW | DUPE_MATCH_SIM_CUSTOM))
3943                 {
3944                 dupe_listview_show_rank(dw->listview, TRUE);
3945                 }
3946         else
3947                 {
3948                 dupe_listview_show_rank(dw->listview, FALSE);
3949                 }
3950         dupe_window_recompare(dw);
3951 }
3952
3953 static void dupe_menu_add_item(GtkListStore *store, const gchar *text, DupeMatchType type, DupeWindow *dw)
3954 {
3955         GtkTreeIter iter;
3956
3957         gtk_list_store_append(store, &iter);
3958         gtk_list_store_set(store, &iter, DUPE_MENU_COLUMN_NAME, text,
3959                                          DUPE_MENU_COLUMN_MASK, type, -1);
3960
3961         if (dw->match_mask == type) gtk_combo_box_set_active_iter(GTK_COMBO_BOX(dw->combo), &iter);
3962 }
3963
3964 static void dupe_menu_setup(DupeWindow *dw)
3965 {
3966         GtkListStore *store;
3967         GtkCellRenderer *renderer;
3968
3969         store = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_INT);
3970         dw->combo = gtk_combo_box_new_with_model(GTK_TREE_MODEL(store));
3971         g_object_unref(store);
3972
3973         renderer = gtk_cell_renderer_text_new();
3974         gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(dw->combo), renderer, TRUE);
3975         gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(dw->combo), renderer,
3976                                        "text", DUPE_MENU_COLUMN_NAME, NULL);
3977
3978         dupe_menu_add_item(store, _("Name"), DUPE_MATCH_NAME, dw);
3979         dupe_menu_add_item(store, _("Name case-insensitive"), DUPE_MATCH_NAME_CI, dw);
3980         dupe_menu_add_item(store, _("Size"), DUPE_MATCH_SIZE, dw);
3981         dupe_menu_add_item(store, _("Date"), DUPE_MATCH_DATE, dw);
3982         dupe_menu_add_item(store, _("Dimensions"), DUPE_MATCH_DIM, dw);
3983         dupe_menu_add_item(store, _("Checksum"), DUPE_MATCH_SUM, dw);
3984         dupe_menu_add_item(store, _("Path"), DUPE_MATCH_PATH, dw);
3985         dupe_menu_add_item(store, _("Similarity (high - 95)"), DUPE_MATCH_SIM_HIGH, dw);
3986         dupe_menu_add_item(store, _("Similarity (med. - 90)"), DUPE_MATCH_SIM_MED, dw);
3987         dupe_menu_add_item(store, _("Similarity (low - 85)"), DUPE_MATCH_SIM_LOW, dw);
3988         dupe_menu_add_item(store, _("Similarity (custom)"), DUPE_MATCH_SIM_CUSTOM, dw);
3989         dupe_menu_add_item(store, _("Name â‰  content"), DUPE_MATCH_NAME_CONTENT, dw);
3990         dupe_menu_add_item(store, _("Name case-insensitive â‰  content"), DUPE_MATCH_NAME_CI_CONTENT, dw);
3991         dupe_menu_add_item(store, _("Show all"), DUPE_MATCH_ALL, dw);
3992
3993         g_signal_connect(G_OBJECT(dw->combo), "changed",
3994                          G_CALLBACK(dupe_menu_type_cb), dw);
3995 }
3996
3997 /*
3998  *-------------------------------------------------------------------
3999  * list view columns
4000  *-------------------------------------------------------------------
4001  */
4002
4003 /* this overrides the low default of a GtkCellRenderer from 100 to CELL_HEIGHT_OVERRIDE, something sane for our purposes */
4004
4005 #define CELL_HEIGHT_OVERRIDE 512
4006
4007 void cell_renderer_height_override(GtkCellRenderer *renderer)
4008 {
4009         GParamSpec *spec;
4010
4011         spec = g_object_class_find_property(G_OBJECT_GET_CLASS(G_OBJECT(renderer)), "height");
4012         if (spec && G_IS_PARAM_SPEC_INT(spec))
4013                 {
4014                 GParamSpecInt *spec_int;
4015
4016                 spec_int = G_PARAM_SPEC_INT(spec);
4017                 if (spec_int->maximum < CELL_HEIGHT_OVERRIDE) spec_int->maximum = CELL_HEIGHT_OVERRIDE;
4018                 }
4019 }
4020
4021 static GdkColor *dupe_listview_color_shifted(GtkWidget *widget)
4022 {
4023         static GdkColor color;
4024         static GtkWidget *done = NULL;
4025
4026         if (done != widget)
4027                 {
4028                 GtkStyle *style;
4029
4030                 style = gtk_widget_get_style(widget);
4031                 memcpy(&color, &style->base[GTK_STATE_NORMAL], sizeof(color));
4032                 shift_color(&color, -1, 0);
4033                 done = widget;
4034                 }
4035
4036         return &color;
4037 }
4038
4039 static void dupe_listview_color_cb(GtkTreeViewColumn *UNUSED(tree_column), GtkCellRenderer *cell,
4040                                    GtkTreeModel *tree_model, GtkTreeIter *iter, gpointer data)
4041 {
4042         DupeWindow *dw = data;
4043         gboolean set;
4044
4045         gtk_tree_model_get(tree_model, iter, DUPE_COLUMN_COLOR, &set, -1);
4046         g_object_set(G_OBJECT(cell),
4047                      "cell-background-gdk", dupe_listview_color_shifted(dw->listview),
4048                      "cell-background-set", set, NULL);
4049 }
4050
4051 static void dupe_listview_add_column(DupeWindow *dw, GtkWidget *listview, gint n, const gchar *title, gboolean image, gboolean right_justify)
4052 {
4053         GtkTreeViewColumn *column;
4054         GtkCellRenderer *renderer;
4055
4056         column = gtk_tree_view_column_new();
4057         gtk_tree_view_column_set_title(column, title);
4058         gtk_tree_view_column_set_min_width(column, 4);
4059         gtk_tree_view_column_set_sort_column_id(column, n);
4060
4061         if (n != DUPE_COLUMN_RANK &&
4062             n != DUPE_COLUMN_THUMB)
4063                 {
4064                 gtk_tree_view_column_set_resizable(column, TRUE);
4065                 }
4066
4067         if (!image)
4068                 {
4069                 gtk_tree_view_column_set_sizing(column, GTK_TREE_VIEW_COLUMN_GROW_ONLY);
4070                 renderer = gtk_cell_renderer_text_new();
4071                 if (right_justify)
4072                         {
4073                         g_object_set(G_OBJECT(renderer), "xalign", 1.0, NULL);
4074                         }
4075                 gtk_tree_view_column_pack_start(column, renderer, TRUE);
4076                 gtk_tree_view_column_add_attribute(column, renderer, "text", n);
4077                 }
4078         else
4079                 {
4080                 gtk_tree_view_column_set_sizing(column, GTK_TREE_VIEW_COLUMN_FIXED);
4081                 renderer = gtk_cell_renderer_pixbuf_new();
4082                 cell_renderer_height_override(renderer);
4083                 gtk_tree_view_column_pack_start(column, renderer, TRUE);
4084                 gtk_tree_view_column_add_attribute(column, renderer, "pixbuf", n);
4085                 }
4086
4087         if (listview == dw->listview)
4088                 {
4089                 /* sets background before rendering */
4090                 gtk_tree_view_column_set_cell_data_func(column, renderer, dupe_listview_color_cb, dw, NULL);
4091                 }
4092
4093         gtk_tree_view_append_column(GTK_TREE_VIEW(listview), column);
4094 }
4095
4096 static void dupe_listview_set_height(GtkWidget *listview, gboolean thumb)
4097 {
4098         GtkTreeViewColumn *column;
4099         GtkCellRenderer *cell;
4100         GList *list;
4101
4102         column = gtk_tree_view_get_column(GTK_TREE_VIEW(listview), DUPE_COLUMN_THUMB - 1);
4103         if (!column) return;
4104
4105         gtk_tree_view_column_set_fixed_width(column, (thumb) ? options->thumbnails.max_width : 4);
4106         gtk_tree_view_column_set_visible(column, thumb);
4107
4108         list = gtk_cell_layout_get_cells(GTK_CELL_LAYOUT(column));
4109         if (!list) return;
4110         cell = list->data;
4111         g_list_free(list);
4112
4113         g_object_set(G_OBJECT(cell), "height", (thumb) ? options->thumbnails.max_height : -1, NULL);
4114         gtk_tree_view_columns_autosize(GTK_TREE_VIEW(listview));
4115 }
4116
4117 static void dupe_listview_show_rank(GtkWidget *listview, gboolean rank)
4118 {
4119         GtkTreeViewColumn *column;
4120
4121         column = gtk_tree_view_get_column(GTK_TREE_VIEW(listview), DUPE_COLUMN_RANK - 1);
4122         if (!column) return;
4123
4124         gtk_tree_view_column_set_visible(column, rank);
4125 }
4126
4127 /*
4128  *-------------------------------------------------------------------
4129  * misc cb
4130  *-------------------------------------------------------------------
4131  */
4132
4133 static void dupe_window_show_thumb_cb(GtkWidget *widget, gpointer data)
4134 {
4135         DupeWindow *dw = data;
4136
4137         dw->show_thumbs = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget));
4138         options->duplicates_thumbnails = dw->show_thumbs;
4139
4140         if (dw->show_thumbs)
4141                 {
4142                 if (!dw->working) dupe_thumb_step(dw);
4143                 }
4144         else
4145                 {
4146                 GtkTreeModel *store;
4147                 GtkTreeIter iter;
4148                 gboolean valid;
4149
4150                 thumb_loader_free(dw->thumb_loader);
4151                 dw->thumb_loader = NULL;
4152
4153                 store = gtk_tree_view_get_model(GTK_TREE_VIEW(dw->listview));
4154                 valid = gtk_tree_model_get_iter_first(store, &iter);
4155
4156                 while (valid)
4157                         {
4158                         gtk_list_store_set(GTK_LIST_STORE(store), &iter, DUPE_COLUMN_THUMB, NULL, -1);
4159                         valid = gtk_tree_model_iter_next(store, &iter);
4160                         }
4161                 dupe_window_update_progress(dw, NULL, 0.0, FALSE);
4162                 }
4163
4164         dupe_listview_set_height(dw->listview, dw->show_thumbs);
4165 }
4166
4167 static void dupe_window_rotation_invariant_cb(GtkWidget *widget, gpointer data)
4168 {
4169         DupeWindow *dw = data;
4170
4171         options->rot_invariant_sim = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget));
4172         dupe_window_recompare(dw);
4173 }
4174
4175 static void dupe_window_custom_threshold_cb(GtkWidget *widget, gpointer data)
4176 {
4177         DupeWindow *dw = data;
4178         DupeMatchType match_type;
4179         GtkTreeModel *store;
4180         gboolean valid;
4181         GtkTreeIter iter;
4182
4183         options->duplicates_similarity_threshold = gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(widget));
4184         dw->match_mask = DUPE_MATCH_SIM_CUSTOM;
4185
4186         store = gtk_combo_box_get_model(GTK_COMBO_BOX(dw->combo));
4187         valid = gtk_tree_model_get_iter_first(store, &iter);
4188         while (valid)
4189                 {
4190                 gtk_tree_model_get(store, &iter, DUPE_MENU_COLUMN_MASK, &match_type, -1);
4191                 if (match_type == DUPE_MATCH_SIM_CUSTOM)
4192                         {
4193                         break;
4194                         }
4195                 valid = gtk_tree_model_iter_next(store, &iter);
4196                 }
4197
4198         gtk_combo_box_set_active_iter(GTK_COMBO_BOX(dw->combo), &iter);
4199         dupe_window_recompare(dw);
4200 }
4201
4202 static void dupe_popup_menu_pos_cb(GtkMenu *menu, gint *x, gint *y, gboolean *UNUSED(push_in), gpointer data)
4203 {
4204         GtkWidget *view = data;
4205         GtkTreePath *tpath;
4206         gint cx, cy, cw, ch;
4207         gint column;
4208
4209         gtk_tree_view_get_cursor(GTK_TREE_VIEW(view), &tpath, NULL);
4210         if (!tpath) return;
4211
4212         if (gtk_tree_view_get_column(GTK_TREE_VIEW(view), DUPE_COLUMN_NAME - 1) != NULL)
4213                 {
4214                 column = DUPE_COLUMN_NAME - 1;
4215                 }
4216         else
4217                 {
4218                 /* dw->second_listview */
4219                 column = 0;
4220                 }
4221         tree_view_get_cell_clamped(GTK_TREE_VIEW(view), tpath, column, TRUE, &cx, &cy, &cw, &ch);
4222         gtk_tree_path_free(tpath);
4223         cy += ch;
4224         popup_menu_position_clamp(menu, &cx, &cy, 0);
4225         *x = cx;
4226         *y = cy;
4227 }
4228
4229 static gboolean dupe_window_keypress_cb(GtkWidget *UNUSED(widget), GdkEventKey *event, gpointer data)
4230 {
4231         DupeWindow *dw = data;
4232         gboolean stop_signal = FALSE;
4233         gboolean on_second;
4234         GtkWidget *listview;
4235         GtkTreeModel *store;
4236         GtkTreeSelection *selection;
4237         GList *slist;
4238         DupeItem *di = NULL;
4239
4240         on_second = gtk_widget_has_focus(dw->second_listview);
4241
4242         if (on_second)
4243                 {
4244                 listview = dw->second_listview;
4245                 }
4246         else
4247                 {
4248                 listview = dw->listview;
4249                 }
4250
4251         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(listview));
4252         slist = gtk_tree_selection_get_selected_rows(selection, &store);
4253         if (slist)
4254                 {
4255                 GtkTreePath *tpath;
4256                 GtkTreeIter iter;
4257                 GList *last;
4258
4259                 last = g_list_last(slist);
4260                 tpath = last->data;
4261
4262                 /* last is newest selected file */
4263                 gtk_tree_model_get_iter(store, &iter, tpath);
4264                 gtk_tree_model_get(store, &iter, DUPE_COLUMN_POINTER, &di, -1);
4265                 }
4266         g_list_foreach(slist, (GFunc)tree_path_free_wrapper, NULL);
4267         g_list_free(slist);
4268
4269         if (event->state & GDK_CONTROL_MASK)
4270                 {
4271                 if (!on_second)
4272                         {
4273                         stop_signal = TRUE;
4274                         switch (event->keyval)
4275                                 {
4276                                 case '1':
4277                                 case '2':
4278                                 case '3':
4279                                 case '4':
4280                                 case '5':
4281                                 case '6':
4282                                 case '7':
4283                                 case '8':
4284                                 case '9':
4285                                 case '0':
4286                                         break;
4287                                 case 'C': case 'c':
4288                                         file_util_copy(NULL, dupe_listview_get_selection(dw, listview),
4289                                                        NULL, dw->window);
4290                                         break;
4291                                 case 'M': case 'm':
4292                                         file_util_move(NULL, dupe_listview_get_selection(dw, listview),
4293                                                        NULL, dw->window);
4294                                         break;
4295                                 case 'R': case 'r':
4296                                         file_util_rename(NULL, dupe_listview_get_selection(dw, listview), dw->window);
4297                                         break;
4298                                 case 'D': case 'd':
4299                                         options->file_ops.safe_delete_enable = TRUE;
4300                                         file_util_delete(NULL, dupe_listview_get_selection(dw, listview), dw->window);
4301                                         break;
4302                                 default:
4303                                         stop_signal = FALSE;
4304                                         break;
4305                                 }
4306                         }
4307
4308                 if (!stop_signal)
4309                         {
4310                         stop_signal = TRUE;
4311                         switch (event->keyval)
4312                                 {
4313                                 case 'A': case 'a':
4314                                         if (event->state & GDK_SHIFT_MASK)
4315                                                 {
4316                                                 gtk_tree_selection_unselect_all(selection);
4317                                                 }
4318                                         else
4319                                                 {
4320                                                 gtk_tree_selection_select_all(selection);
4321                                                 }
4322                                         break;
4323                                 case GDK_KEY_Delete: case GDK_KEY_KP_Delete:
4324                                         if (on_second)
4325                                                 {
4326                                                 dupe_second_clear(dw);
4327                                                 dupe_window_recompare(dw);
4328                                                 }
4329                                         else
4330                                                 {
4331                                                 dupe_window_clear(dw);
4332                                                 }
4333                                         break;
4334                                 case 'L': case 'l':
4335                                         dupe_window_append_file_list(dw, FALSE);
4336                                         break;
4337                                 case 'T': case 't':
4338                                         gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(dw->button_thumbs),
4339                                                 !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(dw->button_thumbs)));
4340                                         break;
4341                                 case 'W': case 'w':
4342                                         dupe_window_close(dw);
4343                                         break;
4344                                 default:
4345                                         stop_signal = FALSE;
4346                                         break;
4347                                 }
4348                         }
4349                 }
4350         else
4351                 {
4352                 stop_signal = TRUE;
4353                 switch (event->keyval)
4354                         {
4355                         case GDK_KEY_Return: case GDK_KEY_KP_Enter:
4356                                 dupe_menu_view(dw, di, listview, FALSE);
4357                                 break;
4358                         case 'V': case 'v':
4359                                 dupe_menu_view(dw, di, listview, TRUE);
4360                                 break;
4361                         case GDK_KEY_Delete: case GDK_KEY_KP_Delete:
4362                                 dupe_window_remove_selection(dw, listview);
4363                                 break;
4364                         case 'C': case 'c':
4365                                 if (!on_second)
4366                                         {
4367                                         dupe_window_collection_from_selection(dw);
4368                                         }
4369                                 break;
4370                         case '0':
4371                                 options->duplicates_select_type = DUPE_SELECT_NONE;
4372                                 dupe_listview_select_dupes(dw, DUPE_SELECT_NONE);
4373                                 break;
4374                         case '1':
4375                                 options->duplicates_select_type = DUPE_SELECT_GROUP1;
4376                                 dupe_listview_select_dupes(dw, DUPE_SELECT_GROUP1);
4377                                 break;
4378                         case '2':
4379                                 options->duplicates_select_type = DUPE_SELECT_GROUP2;
4380                                 dupe_listview_select_dupes(dw, DUPE_SELECT_GROUP2);
4381                                 break;
4382                         case GDK_KEY_Menu:
4383                         case GDK_KEY_F10:
4384                                 if (!on_second)
4385                                         {
4386                                         GtkWidget *menu;
4387
4388                                         menu = dupe_menu_popup_main(dw, di);
4389                                         gtk_menu_popup(GTK_MENU(menu), NULL, NULL,
4390                                                        dupe_popup_menu_pos_cb, listview, 0, GDK_CURRENT_TIME);
4391                                         }
4392                                 else
4393                                         {
4394                                         GtkWidget *menu;
4395
4396                                         menu = dupe_menu_popup_second(dw, di);
4397                                         gtk_menu_popup(GTK_MENU(menu), NULL, NULL,
4398                                                        dupe_popup_menu_pos_cb, listview, 0, GDK_CURRENT_TIME);
4399                                         }
4400                                 break;
4401                         default:
4402                                 stop_signal = FALSE;
4403                                 break;
4404                         }
4405                 }
4406         if (!stop_signal && is_help_key(event))
4407                 {
4408                 help_window_show("GuideImageSearchFindingDuplicates.html");
4409                 stop_signal = TRUE;
4410                 }
4411
4412         return stop_signal;
4413 }
4414
4415
4416 void dupe_window_clear(DupeWindow *dw)
4417 {
4418         GtkListStore *store;
4419
4420         dupe_check_stop(dw);
4421
4422         store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->listview)));
4423         gtk_list_store_clear(store);
4424         gtk_tree_view_columns_autosize(GTK_TREE_VIEW(dw->listview));
4425
4426         g_list_free(dw->dupes);
4427         dw->dupes = NULL;
4428
4429         dupe_list_free(dw->list);
4430         dw->list = NULL;
4431         dw->set_count = 0;
4432
4433         dupe_match_reset_list(dw->second_list);
4434
4435         dupe_window_update_count(dw, FALSE);
4436         dupe_window_update_progress(dw, NULL, 0.0, FALSE);
4437 }
4438
4439 static void dupe_window_get_geometry(DupeWindow *dw)
4440 {
4441         GdkWindow *window;
4442         LayoutWindow *lw = NULL;
4443
4444         layout_valid(&lw);
4445
4446         if (!dw || !lw) return;
4447
4448         window = gtk_widget_get_window(dw->window);
4449         gdk_window_get_position(window, &lw->options.dupe_window.x, &lw->options.dupe_window.y);
4450         lw->options.dupe_window.w = gdk_window_get_width(window);
4451         lw->options.dupe_window.h = gdk_window_get_height(window);
4452 }
4453
4454 void dupe_window_close(DupeWindow *dw)
4455 {
4456         dupe_check_stop(dw);
4457
4458         dupe_window_get_geometry(dw);
4459
4460         dupe_window_list = g_list_remove(dupe_window_list, dw);
4461         gtk_widget_destroy(dw->window);
4462
4463         g_list_free(dw->dupes);
4464         dupe_list_free(dw->list);
4465
4466         dupe_list_free(dw->second_list);
4467
4468         file_data_unregister_notify_func(dupe_notify_cb, dw);
4469
4470         g_thread_pool_free(dw->dupe_comparison_thread_pool, TRUE, TRUE);
4471
4472         g_free(dw);
4473 }
4474
4475 static gint dupe_window_close_cb(GtkWidget *UNUSED(widget), gpointer data)
4476 {
4477         DupeWindow *dw = data;
4478
4479         dupe_window_close(dw);
4480
4481         return TRUE;
4482 }
4483
4484 static gint dupe_window_delete(GtkWidget *UNUSED(widget), GdkEvent *UNUSED(event), gpointer data)
4485 {
4486         DupeWindow *dw = data;
4487         dupe_window_close(dw);
4488
4489         return TRUE;
4490 }
4491
4492 static void dupe_help_cb(GtkAction *UNUSED(action), gpointer UNUSED(data))
4493 {
4494         help_window_show("GuideImageSearchFindingDuplicates.html");
4495 }
4496
4497 static gint default_sort_cb(GtkTreeModel *UNUSED(model), GtkTreeIter *UNUSED(a), GtkTreeIter *UNUSED(b), gpointer UNUSED(data))
4498 {
4499         return 0;
4500 }
4501
4502 static gint column_sort_cb(GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, gpointer data)
4503 {
4504         GtkTreeSortable *sortable = data;
4505         gint ret = 0;
4506         gchar *rank_str_a, *rank_str_b;
4507         gint rank_int_a;
4508         gint rank_int_b;
4509         gint group_a;
4510         gint group_b;
4511         gint sort_column_id;
4512         GtkSortType sort_order;
4513         DupeItem *di_a;
4514         DupeItem *di_b;
4515
4516         gtk_tree_sortable_get_sort_column_id(sortable, &sort_column_id, &sort_order);
4517
4518         gtk_tree_model_get(model, a, DUPE_COLUMN_RANK, &rank_str_a, DUPE_COLUMN_SET, &group_a, DUPE_COLUMN_POINTER, &di_a, -1);
4519
4520         gtk_tree_model_get(model, b, DUPE_COLUMN_RANK, &rank_str_b, DUPE_COLUMN_SET, &group_b, DUPE_COLUMN_POINTER, &di_b, -1);
4521
4522         if (group_a == group_b)
4523                 {
4524                 switch (sort_column_id)
4525                         {
4526                         case DUPE_COLUMN_NAME:
4527                                 ret = utf8_compare(di_a->fd->name, di_b->fd->name, TRUE);
4528                                 break;
4529                         case DUPE_COLUMN_SIZE:
4530                                 if (di_a->fd->size == di_b->fd->size)
4531                                         {
4532                                         ret = 0;
4533                                         }
4534                                 else
4535                                         {
4536                                         ret = (di_a->fd->size > di_b->fd->size) ? 1 : -1;
4537                                         }
4538                                 break;
4539                         case DUPE_COLUMN_DATE:
4540                                 if (di_a->fd->date == di_b->fd->date)
4541                                         {
4542                                         ret = 0;
4543                                         }
4544                                 else
4545                                         {
4546                                         ret = (di_a->fd->date > di_b->fd->date) ? 1 : -1;
4547                                         }
4548                                 break;
4549                         case DUPE_COLUMN_DIMENSIONS:
4550                                 if ((di_a->width == di_b->width) && (di_a->height == di_b->height))
4551                                         {
4552                                         ret = 0;
4553                                         }
4554                                 else
4555                                         {
4556                                         ret = ((di_a->width * di_a->height) > (di_b->width * di_b->height)) ? 1 : -1;
4557                                         }
4558                                 break;
4559                         case DUPE_COLUMN_RANK:
4560                                 rank_int_a = atoi(rank_str_a);
4561                                 rank_int_b = atoi(rank_str_b);
4562                                 if (rank_int_a == 0) rank_int_a = 101;
4563                                 if (rank_int_b == 0) rank_int_b = 101;
4564
4565                                 if (rank_int_a == rank_int_b)
4566                                         {
4567                                         ret = 0;
4568                                         }
4569                                 else
4570                                         {
4571                                         ret = (rank_int_a > rank_int_b) ? 1 : -1;
4572                                         }
4573                                 break;
4574                         case DUPE_COLUMN_PATH:
4575                                 ret = utf8_compare(di_a->fd->path, di_b->fd->path, TRUE);
4576                                 break;
4577                         }
4578                 }
4579         else if (group_a < group_b)
4580                 {
4581                 ret = (sort_order == GTK_SORT_ASCENDING) ? 1 : -1;
4582                 }
4583         else
4584                 {
4585                 ret = (sort_order == GTK_SORT_ASCENDING) ? -1 : 1;
4586                 }
4587
4588         return ret;
4589 }
4590
4591 static void column_clicked_cb(GtkWidget *UNUSED(widget),  gpointer data)
4592 {
4593         DupeWindow *dw = data;
4594
4595         options->duplicates_match = DUPE_SELECT_NONE;
4596         dupe_listview_select_dupes(dw, DUPE_SELECT_NONE);
4597 }
4598
4599 /* collection and files can be NULL */
4600 DupeWindow *dupe_window_new()
4601 {
4602         DupeWindow *dw;
4603         GtkWidget *vbox;
4604         GtkWidget *hbox;
4605         GtkWidget *scrolled;
4606         GtkWidget *frame;
4607         GtkWidget *status_box;
4608         GtkWidget *controls_box;
4609         GtkWidget *button_box;
4610         GtkWidget *label;
4611         GtkWidget *button;
4612         GtkListStore *store;
4613         GtkTreeSelection *selection;
4614         GdkGeometry geometry;
4615         LayoutWindow *lw = NULL;
4616
4617         layout_valid(&lw);
4618
4619         dw = g_new0(DupeWindow, 1);
4620         dw->add_files_queue = NULL;
4621         dw->add_files_queue_id = 0;
4622
4623         dw->match_mask = DUPE_MATCH_NAME;
4624         if (options->duplicates_match == DUPE_MATCH_NAME) dw->match_mask = DUPE_MATCH_NAME;
4625         if (options->duplicates_match == DUPE_MATCH_SIZE) dw->match_mask = DUPE_MATCH_SIZE;
4626         if (options->duplicates_match == DUPE_MATCH_DATE) dw->match_mask = DUPE_MATCH_DATE;
4627         if (options->duplicates_match == DUPE_MATCH_DIM) dw->match_mask = DUPE_MATCH_DIM;
4628         if (options->duplicates_match == DUPE_MATCH_SUM) dw->match_mask = DUPE_MATCH_SUM;
4629         if (options->duplicates_match == DUPE_MATCH_PATH) dw->match_mask = DUPE_MATCH_PATH;
4630         if (options->duplicates_match == DUPE_MATCH_SIM_HIGH) dw->match_mask = DUPE_MATCH_SIM_HIGH;
4631         if (options->duplicates_match == DUPE_MATCH_SIM_MED) dw->match_mask = DUPE_MATCH_SIM_MED;
4632         if (options->duplicates_match == DUPE_MATCH_SIM_LOW) dw->match_mask = DUPE_MATCH_SIM_LOW;
4633         if (options->duplicates_match == DUPE_MATCH_SIM_CUSTOM) dw->match_mask = DUPE_MATCH_SIM_CUSTOM;
4634         if (options->duplicates_match == DUPE_MATCH_NAME_CI) dw->match_mask = DUPE_MATCH_NAME_CI;
4635         if (options->duplicates_match == DUPE_MATCH_NAME_CONTENT) dw->match_mask = DUPE_MATCH_NAME_CONTENT;
4636         if (options->duplicates_match == DUPE_MATCH_NAME_CI_CONTENT) dw->match_mask = DUPE_MATCH_NAME_CI_CONTENT;
4637         if (options->duplicates_match == DUPE_MATCH_ALL) dw->match_mask = DUPE_MATCH_ALL;
4638
4639         dw->window = window_new(GTK_WINDOW_TOPLEVEL, "dupe", NULL, NULL, _("Find duplicates"));
4640         DEBUG_NAME(dw->window);
4641
4642         geometry.min_width = DEFAULT_MINIMAL_WINDOW_SIZE;
4643         geometry.min_height = DEFAULT_MINIMAL_WINDOW_SIZE;
4644         geometry.base_width = DUPE_DEF_WIDTH;
4645         geometry.base_height = DUPE_DEF_HEIGHT;
4646         gtk_window_set_geometry_hints(GTK_WINDOW(dw->window), NULL, &geometry,
4647                                       GDK_HINT_MIN_SIZE | GDK_HINT_BASE_SIZE);
4648
4649         if (lw && options->save_window_positions)
4650                 {
4651                 gtk_window_set_default_size(GTK_WINDOW(dw->window), lw->options.dupe_window.w, lw->options.dupe_window.h);
4652                 gtk_window_move(GTK_WINDOW(dw->window), lw->options.dupe_window.x, lw->options.dupe_window.y);
4653                 }
4654         else
4655                 {
4656                 gtk_window_set_default_size(GTK_WINDOW(dw->window), DUPE_DEF_WIDTH, DUPE_DEF_HEIGHT);
4657                 }
4658
4659         gtk_window_set_resizable(GTK_WINDOW(dw->window), TRUE);
4660         gtk_container_set_border_width(GTK_CONTAINER(dw->window), 0);
4661
4662         g_signal_connect(G_OBJECT(dw->window), "delete_event",
4663                          G_CALLBACK(dupe_window_delete), dw);
4664         g_signal_connect(G_OBJECT(dw->window), "key_press_event",
4665                          G_CALLBACK(dupe_window_keypress_cb), dw);
4666
4667         vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
4668         gtk_container_add(GTK_CONTAINER(dw->window), vbox);
4669         gtk_widget_show(vbox);
4670
4671         dw->table = gtk_table_new(1, 3, FALSE);
4672         gtk_box_pack_start(GTK_BOX(vbox), dw->table, TRUE, TRUE, 0);
4673         gtk_widget_show(dw->table);
4674
4675         scrolled = gtk_scrolled_window_new(NULL, NULL);
4676         gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled), GTK_SHADOW_IN);
4677         gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
4678         gtk_table_attach_defaults(GTK_TABLE(dw->table), scrolled, 0, 2, 0, 1);
4679         gtk_widget_show(scrolled);
4680
4681         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);
4682         dw->listview = gtk_tree_view_new_with_model(GTK_TREE_MODEL(store));
4683         g_object_unref(store);
4684
4685         dw->sortable = GTK_TREE_SORTABLE(store);
4686
4687         gtk_tree_sortable_set_sort_func(dw->sortable, DUPE_COLUMN_RANK, column_sort_cb, dw->sortable, NULL);
4688         gtk_tree_sortable_set_sort_func(dw->sortable, DUPE_COLUMN_SET, default_sort_cb, dw->sortable, NULL);
4689         gtk_tree_sortable_set_sort_func(dw->sortable, DUPE_COLUMN_THUMB, default_sort_cb, dw->sortable, NULL);
4690         gtk_tree_sortable_set_sort_func(dw->sortable, DUPE_COLUMN_NAME, column_sort_cb, dw->sortable, NULL);
4691         gtk_tree_sortable_set_sort_func(dw->sortable, DUPE_COLUMN_SIZE, column_sort_cb, dw->sortable, NULL);
4692         gtk_tree_sortable_set_sort_func(dw->sortable, DUPE_COLUMN_DATE, column_sort_cb, dw->sortable, NULL);
4693         gtk_tree_sortable_set_sort_func(dw->sortable, DUPE_COLUMN_DIMENSIONS, column_sort_cb, dw->sortable, NULL);
4694         gtk_tree_sortable_set_sort_func(dw->sortable, DUPE_COLUMN_PATH, column_sort_cb, dw->sortable, NULL);
4695
4696         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(dw->listview));
4697         gtk_tree_selection_set_mode(GTK_TREE_SELECTION(selection), GTK_SELECTION_MULTIPLE);
4698         gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(dw->listview), TRUE);
4699         gtk_tree_view_set_enable_search(GTK_TREE_VIEW(dw->listview), FALSE);
4700
4701         dupe_listview_add_column(dw, dw->listview, DUPE_COLUMN_RANK, _("Rank"), FALSE, TRUE);
4702         dupe_listview_add_column(dw, dw->listview, DUPE_COLUMN_THUMB, _("Thumb"), TRUE, FALSE);
4703         dupe_listview_add_column(dw, dw->listview, DUPE_COLUMN_NAME, _("Name"), FALSE, FALSE);
4704         dupe_listview_add_column(dw, dw->listview, DUPE_COLUMN_SIZE, _("Size"), FALSE, TRUE);
4705         dupe_listview_add_column(dw, dw->listview, DUPE_COLUMN_DATE, _("Date"), FALSE, TRUE);
4706         dupe_listview_add_column(dw, dw->listview, DUPE_COLUMN_DIMENSIONS, _("Dimensions"), FALSE, FALSE);
4707         dupe_listview_add_column(dw, dw->listview, DUPE_COLUMN_PATH, _("Path"), FALSE, FALSE);
4708         dupe_listview_add_column(dw, dw->listview, DUPE_COLUMN_SET, _("Set"), FALSE, FALSE);
4709
4710         g_signal_connect(gtk_tree_view_get_column(GTK_TREE_VIEW(dw->listview), DUPE_COLUMN_RANK - 1), "clicked", (GCallback)column_clicked_cb, dw);
4711         g_signal_connect(gtk_tree_view_get_column(GTK_TREE_VIEW(dw->listview), DUPE_COLUMN_NAME - 1), "clicked", (GCallback)column_clicked_cb, dw);
4712         g_signal_connect(gtk_tree_view_get_column(GTK_TREE_VIEW(dw->listview), DUPE_COLUMN_SIZE - 1), "clicked", (GCallback)column_clicked_cb, dw);
4713         g_signal_connect(gtk_tree_view_get_column(GTK_TREE_VIEW(dw->listview), DUPE_COLUMN_DATE - 1), "clicked", (GCallback)column_clicked_cb, dw);
4714         g_signal_connect(gtk_tree_view_get_column(GTK_TREE_VIEW(dw->listview), DUPE_COLUMN_DIMENSIONS - 1), "clicked", (GCallback)column_clicked_cb, dw);
4715         g_signal_connect(gtk_tree_view_get_column(GTK_TREE_VIEW(dw->listview), DUPE_COLUMN_PATH - 1), "clicked", (GCallback)column_clicked_cb, dw);
4716
4717         gtk_container_add(GTK_CONTAINER(scrolled), dw->listview);
4718         gtk_widget_show(dw->listview);
4719
4720         dw->second_vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
4721         gtk_table_attach_defaults(GTK_TABLE(dw->table), dw->second_vbox, 2, 3, 0, 1);
4722         if (dw->second_set)
4723                 {
4724                 gtk_table_set_col_spacings(GTK_TABLE(dw->table), PREF_PAD_GAP);
4725                 gtk_widget_show(dw->second_vbox);
4726                 }
4727         else
4728                 {
4729                 gtk_table_set_col_spacings(GTK_TABLE(dw->table), 0);
4730                 }
4731
4732         scrolled = gtk_scrolled_window_new(NULL, NULL);
4733         gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled), GTK_SHADOW_IN);
4734         gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
4735         gtk_box_pack_start(GTK_BOX(dw->second_vbox), scrolled, TRUE, TRUE, 0);
4736         gtk_widget_show(scrolled);
4737
4738         store = gtk_list_store_new(2, G_TYPE_POINTER, G_TYPE_STRING);
4739         dw->second_listview = gtk_tree_view_new_with_model(GTK_TREE_MODEL(store));
4740
4741         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(dw->second_listview));
4742         gtk_tree_selection_set_mode(GTK_TREE_SELECTION(selection), GTK_SELECTION_MULTIPLE);
4743
4744         gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(dw->second_listview), TRUE);
4745         gtk_tree_view_set_enable_search(GTK_TREE_VIEW(dw->second_listview), FALSE);
4746
4747         dupe_listview_add_column(dw, dw->second_listview, 1, _("Compare to:"), FALSE, FALSE);
4748
4749         gtk_container_add(GTK_CONTAINER(scrolled), dw->second_listview);
4750         gtk_widget_show(dw->second_listview);
4751
4752         dw->second_status_label = gtk_label_new("");
4753         gtk_box_pack_start(GTK_BOX(dw->second_vbox), dw->second_status_label, FALSE, FALSE, 0);
4754         gtk_widget_show(dw->second_status_label);
4755
4756         pref_line(dw->second_vbox, GTK_ORIENTATION_HORIZONTAL);
4757
4758         status_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
4759         gtk_box_pack_start(GTK_BOX(vbox), status_box, FALSE, FALSE, 0);
4760         gtk_widget_show(status_box);
4761
4762         frame = gtk_frame_new(NULL);
4763         DEBUG_NAME(frame);
4764         gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_IN);
4765         gtk_box_pack_start(GTK_BOX(status_box), frame, TRUE, TRUE, 0);
4766         gtk_widget_show(frame);
4767
4768         dw->status_label = gtk_label_new("");
4769         gtk_container_add(GTK_CONTAINER(frame), dw->status_label);
4770         gtk_widget_show(dw->status_label);
4771
4772         dw->extra_label = gtk_progress_bar_new();
4773         gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(dw->extra_label), 0.0);
4774         gtk_progress_bar_set_text(GTK_PROGRESS_BAR(dw->extra_label), "");
4775         gtk_progress_bar_set_show_text(GTK_PROGRESS_BAR(dw->extra_label), TRUE);
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_box_new(GTK_ORIENTATION_HORIZONTAL, 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 *UNUSED(widget), gpointer UNUSED(data))
4903 {
4904         /* do nothing */
4905 }
4906
4907 static void confirm_dir_list_add(GtkWidget *UNUSED(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 *UNUSED(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 *UNUSED(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 *UNUSED(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 *UNUSED(context),
4996                               GtkSelectionData *selection_data, guint info,
4997                               guint UNUSED(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 UNUSED(x), gint UNUSED(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 *UNUSED(widget), GdkDragContext *UNUSED(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 *UNUSED(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 *UNUSED(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 *UNUSED(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: */