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