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