dffd07c88b39a7fcdaecdfe81fd2fd3c0b328d8d
[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 "main.h"
23 #include "dupe.h"
24
25 #include "cache.h"
26 #include "collect.h"
27 #include "collect-table.h"
28 #include "dnd.h"
29 #include "editors.h"
30 #include "filedata.h"
31 #include "image-load.h"
32 #include "img-view.h"
33 #include "layout.h"
34 #include "layout_image.h"
35 #include "layout_util.h"
36 #include "md5-util.h"
37 #include "menu.h"
38 #include "misc.h"
39 #include "pixbuf_util.h"
40 #include "print.h"
41 #include "thumb.h"
42 #include "ui_fileops.h"
43 #include "ui_menu.h"
44 #include "ui_misc.h"
45 #include "ui_tree_edit.h"
46 #include "uri_utils.h"
47 #include "utilops.h"
48 #include "window.h"
49
50 #include <gdk/gdkkeysyms.h> /* for keyboard values */
51
52
53 #include <math.h>
54
55
56 #define DUPE_DEF_WIDTH 800
57 #define DUPE_DEF_HEIGHT 400
58
59 /* column assignment order (simply change them here) */
60 enum {
61         DUPE_COLUMN_POINTER = 0,
62         DUPE_COLUMN_RANK,
63         DUPE_COLUMN_THUMB,
64         DUPE_COLUMN_NAME,
65         DUPE_COLUMN_SIZE,
66         DUPE_COLUMN_DATE,
67         DUPE_COLUMN_DIMENSIONS,
68         DUPE_COLUMN_PATH,
69         DUPE_COLUMN_COLOR,
70         DUPE_COLUMN_COUNT       /* total columns */
71 };
72
73
74 static GList *dupe_window_list = NULL;  /* list of open DupeWindow *s */
75
76 /*
77  * Well, after adding the 'compare two sets' option things got a little sloppy in here
78  * because we have to account for two 'modes' everywhere. (be careful).
79  */
80
81 static void dupe_match_unlink(DupeItem *a, DupeItem *b);
82 static DupeItem *dupe_match_find_parent(DupeWindow *dw, DupeItem *child);
83
84 static gint dupe_match(DupeItem *a, DupeItem *b, DupeMatchType mask, gdouble *rank, gint fast);
85
86 static void dupe_thumb_step(DupeWindow *dw);
87 static gint dupe_check_cb(gpointer data);
88
89 static void dupe_second_add(DupeWindow *dw, DupeItem *di);
90 static void dupe_second_remove(DupeWindow *dw, DupeItem *di);
91 static GtkWidget *dupe_menu_popup_second(DupeWindow *dw, DupeItem *di);
92
93 static void dupe_dnd_init(DupeWindow *dw);
94
95 static void dupe_notify_cb(FileData *fd, NotifyType type, gpointer data);
96
97 /*
98  * ------------------------------------------------------------------
99  * Window updates
100  * ------------------------------------------------------------------
101  */
102
103
104 static void dupe_window_update_count(DupeWindow *dw, gboolean count_only)
105 {
106         gchar *text;
107
108         if (!dw->list)
109                 {
110                 text = g_strdup(_("Drop files to compare them."));
111                 }
112         else if (count_only)
113                 {
114                 text = g_strdup_printf(_("%d files"), g_list_length(dw->list));
115                 }
116         else
117                 {
118                 text = g_strdup_printf(_("%d matches found in %d files"), g_list_length(dw->dupes), g_list_length(dw->list));
119                 }
120
121         if (dw->second_set)
122                 {
123                 gchar *buf = g_strconcat(text, " ", _("[set 1]"), NULL);
124                 g_free(text);
125                 text = buf;
126                 }
127         gtk_label_set_text(GTK_LABEL(dw->status_label), text);
128
129         g_free(text);
130 }
131
132 static guint64 msec_time(void)
133 {
134         struct timeval tv;
135
136         if (gettimeofday(&tv, NULL) == -1) return 0;
137
138         return (guint64)tv.tv_sec * 1000000 + (guint64)tv.tv_usec;
139 }
140
141 static gint dupe_iterations(gint n)
142 {
143         return (n * ((n + 1) / 2));
144 }
145
146 static void dupe_window_update_progress(DupeWindow *dw, const gchar *status, gdouble value, gboolean force)
147 {
148         const gchar *status_text;
149
150         if (status)
151                 {
152                 guint64 new_time = 0;
153
154                 if (dw->setup_n % 10 == 0)
155                         {
156                         new_time = msec_time() - dw->setup_time;
157                         }
158
159                 if (!force &&
160                     value != 0.0 &&
161                     dw->setup_count > 0 &&
162                     new_time > 2000000)
163                         {
164                         gchar *buf;
165                         gint t;
166                         gint d;
167                         guint32 rem;
168
169                         if (new_time - dw->setup_time_count < 250000) return;
170                         dw->setup_time_count = new_time;
171
172                         if (dw->setup_done)
173                                 {
174                                 if (dw->second_set)
175                                         {
176                                         t = dw->setup_count;
177                                         d = dw->setup_count - dw->setup_n;
178                                         }
179                                 else
180                                         {
181                                         t = dupe_iterations(dw->setup_count);
182                                         d = dupe_iterations(dw->setup_count - dw->setup_n);
183                                         }
184                                 }
185                         else
186                                 {
187                                 t = dw->setup_count;
188                                 d = dw->setup_count - dw->setup_n;
189                                 }
190
191                         rem = (t - d) ? ((gdouble)(dw->setup_time_count / 1000000) / (t - d)) * d : 0;
192
193                         gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(dw->extra_label), value);
194
195                         buf = g_strdup_printf("%s %d:%02d ", status, rem / 60, rem % 60);
196                         gtk_progress_bar_set_text(GTK_PROGRESS_BAR(dw->extra_label), buf);
197                         g_free(buf);
198
199                         return;
200                         }
201                 else if (force ||
202                          value == 0.0 ||
203                          dw->setup_count == 0 ||
204                          dw->setup_time_count == 0 ||
205                          (new_time > 0 && new_time - dw->setup_time_count >= 250000))
206                         {
207                         if (dw->setup_time_count == 0) dw->setup_time_count = 1;
208                         if (new_time > 0) dw->setup_time_count = new_time;
209                         gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(dw->extra_label), value);
210                         status_text = status;
211                         }
212                 else
213                         {
214                         status_text = NULL;
215                         }
216                 }
217         else
218                 {
219                 gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(dw->extra_label), 0.0);
220                 status_text = " ";
221                 }
222
223         if (status_text) gtk_progress_bar_set_text(GTK_PROGRESS_BAR(dw->extra_label), status_text);
224 }
225
226 static void widget_set_cursor(GtkWidget *widget, gint icon)
227 {
228         GdkCursor *cursor;
229
230         if (!gtk_widget_get_window(widget)) return;
231
232         if (icon == -1)
233                 {
234                 cursor = NULL;
235                 }
236         else
237                 {
238                 cursor = gdk_cursor_new(icon);
239                 }
240
241         gdk_window_set_cursor(gtk_widget_get_window(widget), cursor);
242
243         if (cursor) gdk_cursor_unref(cursor);
244 }
245
246 /*
247  * ------------------------------------------------------------------
248  * row color utils
249  * ------------------------------------------------------------------
250  */
251
252 static void dupe_listview_realign_colors(DupeWindow *dw)
253 {
254         GtkTreeModel *store;
255         GtkTreeIter iter;
256         gboolean color_set = TRUE;
257         DupeItem *parent = NULL;
258         gboolean valid;
259
260         store = gtk_tree_view_get_model(GTK_TREE_VIEW(dw->listview));
261         valid = gtk_tree_model_get_iter_first(store, &iter);
262         while (valid)
263                 {
264                 DupeItem *child;
265                 DupeItem *child_parent;
266
267                 gtk_tree_model_get(store, &iter, DUPE_COLUMN_POINTER, &child, -1);
268                 child_parent = dupe_match_find_parent(dw, child);
269                 if (!parent || parent != child_parent)
270                         {
271                         if (!parent)
272                                 {
273                                 /* keep the first row as it is */
274                                 gtk_tree_model_get(store, &iter, DUPE_COLUMN_COLOR, &color_set, -1);
275                                 }
276                         else
277                                 {
278                                 color_set = !color_set;
279                                 }
280                         parent = dupe_match_find_parent(dw, child);
281                         }
282                 gtk_list_store_set(GTK_LIST_STORE(store), &iter, DUPE_COLUMN_COLOR, color_set, -1);
283
284                 valid = gtk_tree_model_iter_next(GTK_TREE_MODEL(store), &iter);
285                 }
286 }
287
288 /*
289  * ------------------------------------------------------------------
290  * Dupe item utils
291  * ------------------------------------------------------------------
292  */
293
294 static DupeItem *dupe_item_new(FileData *fd)
295 {
296         DupeItem *di;
297
298         di = g_new0(DupeItem, 1);
299
300         di->fd = file_data_ref(fd);
301         di->group_rank = 0.0;
302
303         return di;
304 }
305
306 static void dupe_item_free(DupeItem *di)
307 {
308         file_data_unref(di->fd);
309         image_sim_free(di->simd);
310         g_free(di->md5sum);
311         if (di->pixbuf) g_object_unref(di->pixbuf);
312
313         g_free(di);
314 }
315
316 static void dupe_list_free(GList *list)
317 {
318         GList *work = list;
319         while (work)
320                 {
321                 DupeItem *di = work->data;
322                 work = work->next;
323                 dupe_item_free(di);
324                 }
325         g_list_free(list);
326 }
327
328 /*
329 static DupeItem *dupe_item_find_fd_by_list(FileData *fd, GList *work)
330 {
331         while (work)
332                 {
333                 DupeItem *di = work->data;
334
335                 if (di->fd == fd) return di;
336
337                 work = work->next;
338                 }
339
340         return NULL;
341 }
342 */
343
344 /*
345 static DupeItem *dupe_item_find_fd(DupeWindow *dw, FileData *fd)
346 {
347         DupeItem *di;
348
349         di = dupe_item_find_fd_by_list(fd, dw->list);
350         if (!di && dw->second_set) di = dupe_item_find_fd_by_list(fd, dw->second_list);
351
352         return di;
353 }
354 */
355
356 static DupeItem *dupe_item_find_path_by_list(const gchar *path, GList *work)
357 {
358         while (work)
359                 {
360                 DupeItem *di = work->data;
361
362                 if (strcmp(di->fd->path, path) == 0) return di;
363
364                 work = work->next;
365                 }
366
367         return NULL;
368 }
369
370 static DupeItem *dupe_item_find_path(DupeWindow *dw, const gchar *path)
371 {
372         DupeItem *di;
373
374         di = dupe_item_find_path_by_list(path, dw->list);
375         if (!di && dw->second_set) di = dupe_item_find_path_by_list(path, dw->second_list);
376
377         return di;
378 }
379
380 /*
381  * ------------------------------------------------------------------
382  * Image property cache
383  * ------------------------------------------------------------------
384  */
385
386 static void dupe_item_read_cache(DupeItem *di)
387 {
388         gchar *path;
389         CacheData *cd;
390
391         if (!di) return;
392
393         path = cache_find_location(CACHE_TYPE_SIM, di->fd->path);
394         if (!path) return;
395
396         if (filetime(di->fd->path) != filetime(path))
397                 {
398                 g_free(path);
399                 return;
400                 }
401
402         cd = cache_sim_data_load(path);
403         g_free(path);
404
405         if (cd)
406                 {
407                 if (!di->simd && cd->sim)
408                         {
409                         di->simd = cd->sim;
410                         cd->sim = NULL;
411                         }
412                 if (di->width == 0 && di->height == 0 && cd->dimensions)
413                         {
414                         di->width = cd->width;
415                         di->height = cd->height;
416                         }
417                 if (!di->md5sum && cd->have_md5sum)
418                         {
419                         di->md5sum = md5_digest_to_text(cd->md5sum);
420                         }
421                 cache_sim_data_free(cd);
422                 }
423 }
424
425 static void dupe_item_write_cache(DupeItem *di)
426 {
427         gchar *base;
428         mode_t mode = 0755;
429
430         if (!di) return;
431
432         base = cache_get_location(CACHE_TYPE_SIM, di->fd->path, FALSE, &mode);
433         if (recursive_mkdir_if_not_exists(base, mode))
434                 {
435                 CacheData *cd;
436
437                 cd = cache_sim_data_new();
438                 cd->path = cache_get_location(CACHE_TYPE_SIM, di->fd->path, TRUE, NULL);
439
440                 if (di->width != 0) cache_sim_data_set_dimensions(cd, di->width, di->height);
441                 if (di->md5sum)
442                         {
443                         guchar digest[16];
444                         if (md5_digest_from_text(di->md5sum, digest)) cache_sim_data_set_md5sum(cd, digest);
445                         }
446                 if (di->simd) cache_sim_data_set_similarity(cd, di->simd);
447
448                 if (cache_sim_data_save(cd))
449                         {
450                         filetime_set(cd->path, filetime(di->fd->path));
451                         }
452                 cache_sim_data_free(cd);
453                 }
454         g_free(base);
455 }
456
457 /*
458  * ------------------------------------------------------------------
459  * Window list utils
460  * ------------------------------------------------------------------
461  */
462
463 static gint dupe_listview_find_item(GtkListStore *store, DupeItem *item, GtkTreeIter *iter)
464 {
465         gboolean valid;
466         gint row = 0;
467
468         valid = gtk_tree_model_get_iter_first(GTK_TREE_MODEL(store), iter);
469         while (valid)
470                 {
471                 DupeItem *item_n;
472                 gtk_tree_model_get(GTK_TREE_MODEL(store), iter, DUPE_COLUMN_POINTER, &item_n, -1);
473                 if (item_n == item) return row;
474
475                 valid = gtk_tree_model_iter_next(GTK_TREE_MODEL(store), iter);
476                 row++;
477                 }
478
479         return -1;
480 }
481
482 static void dupe_listview_add(DupeWindow *dw, DupeItem *parent, DupeItem *child)
483 {
484         DupeItem *di;
485         gint row;
486         gchar *text[DUPE_COLUMN_COUNT];
487         GtkListStore *store;
488         GtkTreeIter iter;
489         gboolean color_set = FALSE;
490         gint rank;
491
492         if (!parent) return;
493
494         store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->listview)));
495
496         if (child)
497                 {
498                 DupeMatch *dm;
499
500                 row = dupe_listview_find_item(store, parent, &iter);
501                 gtk_tree_model_get(GTK_TREE_MODEL(store), &iter, DUPE_COLUMN_COLOR, &color_set, -1);
502
503                 row++;
504
505                 if (child->group)
506                         {
507                         dm = child->group->data;
508                         rank = (gint)floor(dm->rank);
509                         }
510                 else
511                         {
512                         rank = 1;
513                         log_printf("NULL group in item!\n");
514                         }
515                 }
516         else
517                 {
518                 if (gtk_tree_model_get_iter_first(GTK_TREE_MODEL(store), &iter))
519                         {
520                         gtk_tree_model_get(GTK_TREE_MODEL(store), &iter, DUPE_COLUMN_COLOR, &color_set, -1);
521                         color_set = !color_set;
522                         }
523                 else
524                         {
525                         color_set = FALSE;
526                         }
527                 row = 0;
528                 rank = 0;
529                 }
530
531         di = (child) ? child : parent;
532
533         if (!child && dw->second_set)
534                 {
535                 text[DUPE_COLUMN_RANK] = g_strdup("[1]");
536                 }
537         else if (rank == 0)
538                 {
539                 text[DUPE_COLUMN_RANK] = g_strdup((di->second) ? "(2)" : "");
540                 }
541         else
542                 {
543                 text[DUPE_COLUMN_RANK] = g_strdup_printf("%d%s", rank, (di->second) ? " (2)" : "");
544                 }
545
546         text[DUPE_COLUMN_THUMB] = "";
547         text[DUPE_COLUMN_NAME] = (gchar *)di->fd->name;
548         text[DUPE_COLUMN_SIZE] = text_from_size(di->fd->size);
549         text[DUPE_COLUMN_DATE] = (gchar *)text_from_time(di->fd->date);
550         if (di->width > 0 && di->height > 0)
551                 {
552                 text[DUPE_COLUMN_DIMENSIONS] = g_strdup_printf("%d x %d", di->width, di->height);
553                 }
554         else
555                 {
556                 text[DUPE_COLUMN_DIMENSIONS] = g_strdup("");
557                 }
558         text[DUPE_COLUMN_PATH] = di->fd->path;
559         text[DUPE_COLUMN_COLOR] = NULL;
560
561         gtk_list_store_insert(store, &iter, row);
562         gtk_list_store_set(store, &iter,
563                                 DUPE_COLUMN_POINTER, di,
564                                 DUPE_COLUMN_RANK, text[DUPE_COLUMN_RANK],
565                                 DUPE_COLUMN_THUMB, NULL,
566                                 DUPE_COLUMN_NAME, text[DUPE_COLUMN_NAME],
567                                 DUPE_COLUMN_SIZE, text[DUPE_COLUMN_SIZE],
568                                 DUPE_COLUMN_DATE, text[DUPE_COLUMN_DATE],
569                                 DUPE_COLUMN_DIMENSIONS, text[DUPE_COLUMN_DIMENSIONS],
570                                 DUPE_COLUMN_PATH, text[DUPE_COLUMN_PATH],
571                                 DUPE_COLUMN_COLOR, color_set,
572                                 -1);
573
574         g_free(text[DUPE_COLUMN_RANK]);
575         g_free(text[DUPE_COLUMN_SIZE]);
576         g_free(text[DUPE_COLUMN_DIMENSIONS]);
577 }
578
579 static void dupe_listview_select_dupes(DupeWindow *dw, DupeSelectType parents);
580
581 static void dupe_listview_populate(DupeWindow *dw)
582 {
583         GtkListStore *store;
584         GList *work;
585
586         store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->listview)));
587         gtk_list_store_clear(store);
588
589         work = g_list_last(dw->dupes);
590         while (work)
591                 {
592                 DupeItem *parent = work->data;
593                 GList *temp;
594
595                 dupe_listview_add(dw, parent, NULL);
596
597                 temp = g_list_last(parent->group);
598                 while (temp)
599                         {
600                         DupeMatch *dm = temp->data;
601                         DupeItem *child;
602
603                         child = dm->di;
604
605                         dupe_listview_add(dw, parent, child);
606
607                         temp = temp->prev;
608                         }
609
610                 work = work->prev;
611                 }
612
613         gtk_tree_view_columns_autosize(GTK_TREE_VIEW(dw->listview));
614
615         if (options->duplicates_select_type == DUPE_SELECT_GROUP1)
616                 {
617                 dupe_listview_select_dupes(dw, DUPE_SELECT_GROUP1);
618                 }
619         else if (options->duplicates_select_type == DUPE_SELECT_GROUP2)
620                 {
621                 dupe_listview_select_dupes(dw, DUPE_SELECT_GROUP2);
622                 }
623
624 }
625
626 static void dupe_listview_remove(DupeWindow *dw, DupeItem *di)
627 {
628         GtkListStore *store;
629         GtkTreeIter iter;
630         gint row;
631
632         if (!di) return;
633
634         store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->listview)));
635         row = dupe_listview_find_item(store, di, &iter);
636         if (row < 0) return;
637
638         tree_view_move_cursor_away(GTK_TREE_VIEW(dw->listview), &iter, TRUE);
639         gtk_list_store_remove(store, &iter);
640
641         if (g_list_find(dw->dupes, di) != NULL)
642                 {
643                 if (!dw->color_frozen) dupe_listview_realign_colors(dw);
644                 }
645 }
646
647
648 static GList *dupe_listview_get_filelist(DupeWindow *dw, GtkWidget *listview)
649 {
650         GtkTreeModel *store;
651         GtkTreeIter iter;
652         gboolean valid;
653         GList *list = NULL;
654
655         store = gtk_tree_view_get_model(GTK_TREE_VIEW(listview));
656         valid = gtk_tree_model_get_iter_first(store, &iter);
657         while (valid)
658                 {
659                 DupeItem *di;
660                 gtk_tree_model_get(store, &iter, DUPE_COLUMN_POINTER, &di, -1);
661                 list = g_list_prepend(list, file_data_ref(di->fd));
662
663                 valid = gtk_tree_model_iter_next(store, &iter);
664                 }
665
666         return g_list_reverse(list);
667 }
668
669
670 static GList *dupe_listview_get_selection(DupeWindow *dw, GtkWidget *listview)
671 {
672         GtkTreeModel *store;
673         GtkTreeSelection *selection;
674         GList *slist;
675         GList *list = NULL;
676         GList *work;
677
678         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(listview));
679         slist = gtk_tree_selection_get_selected_rows(selection, &store);
680         work = slist;
681         while (work)
682                 {
683                 GtkTreePath *tpath = work->data;
684                 DupeItem *di = NULL;
685                 GtkTreeIter iter;
686
687                 gtk_tree_model_get_iter(store, &iter, tpath);
688                 gtk_tree_model_get(store, &iter, DUPE_COLUMN_POINTER, &di, -1);
689                 if (di)
690                         {
691                         list = g_list_prepend(list, file_data_ref(di->fd));
692                         }
693                 work = work->next;
694                 }
695         g_list_foreach(slist, (GFunc)gtk_tree_path_free, NULL);
696         g_list_free(slist);
697
698         return g_list_reverse(list);
699 }
700
701 static gboolean dupe_listview_item_is_selected(DupeWindow *dw, DupeItem *di, GtkWidget *listview)
702 {
703         GtkTreeModel *store;
704         GtkTreeSelection *selection;
705         GList *slist;
706         GList *work;
707         gboolean found = FALSE;
708
709         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(listview));
710         slist = gtk_tree_selection_get_selected_rows(selection, &store);
711         work = slist;
712         while (!found && work)
713                 {
714                 GtkTreePath *tpath = work->data;
715                 DupeItem *di_n;
716                 GtkTreeIter iter;
717
718                 gtk_tree_model_get_iter(store, &iter, tpath);
719                 gtk_tree_model_get(store, &iter, DUPE_COLUMN_POINTER, &di_n, -1);
720                 if (di_n == di) found = TRUE;
721                 work = work->next;
722                 }
723         g_list_foreach(slist, (GFunc)gtk_tree_path_free, NULL);
724         g_list_free(slist);
725
726         return found;
727 }
728
729 static void dupe_listview_select_dupes(DupeWindow *dw, DupeSelectType parents)
730 {
731         GtkTreeModel *store;
732         GtkTreeSelection *selection;
733         GtkTreeIter iter;
734         gboolean valid;
735
736         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(dw->listview));
737         gtk_tree_selection_unselect_all(selection);
738
739         store = gtk_tree_view_get_model(GTK_TREE_VIEW(dw->listview));
740         valid = gtk_tree_model_get_iter_first(store, &iter);
741         while (valid)
742                 {
743                 DupeItem *di;
744
745                 gtk_tree_model_get(store, &iter, DUPE_COLUMN_POINTER, &di, -1);
746                 if ((dupe_match_find_parent(dw, di) == di) == (parents == DUPE_SELECT_GROUP1))
747                         {
748                         gtk_tree_selection_select_iter(selection, &iter);
749                         }
750                 valid = gtk_tree_model_iter_next(store, &iter);
751                 }
752 }
753
754 /*
755  * ------------------------------------------------------------------
756  * Match group manipulation
757  * ------------------------------------------------------------------
758  */
759
760 static DupeMatch *dupe_match_find_match(DupeItem *child, DupeItem *parent)
761 {
762         GList *work;
763
764         work = parent->group;
765         while (work)
766                 {
767                 DupeMatch *dm = work->data;
768                 if (dm->di == child) return dm;
769                 work = work->next;
770                 }
771         return NULL;
772 }
773
774 static void dupe_match_link_child(DupeItem *child, DupeItem *parent, gdouble rank)
775 {
776         DupeMatch *dm;
777
778         dm = g_new0(DupeMatch, 1);
779         dm->di = child;
780         dm->rank = rank;
781         parent->group = g_list_append(parent->group, dm);
782 }
783
784 static void dupe_match_link(DupeItem *a, DupeItem *b, gdouble rank)
785 {
786         dupe_match_link_child(a, b, rank);
787         dupe_match_link_child(b, a, rank);
788 }
789
790 static void dupe_match_unlink_child(DupeItem *child, DupeItem *parent)
791 {
792         DupeMatch *dm;
793
794         dm = dupe_match_find_match(child, parent);
795         if (dm)
796                 {
797                 parent->group = g_list_remove(parent->group, dm);
798                 g_free(dm);
799                 }
800 }
801
802 static void dupe_match_unlink(DupeItem *a, DupeItem *b)
803 {
804         dupe_match_unlink_child(a, b);
805         dupe_match_unlink_child(b, a);
806 }
807
808 static void dupe_match_link_clear(DupeItem *parent, gboolean unlink_children)
809 {
810         GList *work;
811
812         work = parent->group;
813         while (work)
814                 {
815                 DupeMatch *dm = work->data;
816                 work = work->next;
817
818                 if (unlink_children) dupe_match_unlink_child(parent, dm->di);
819
820                 g_free(dm);
821                 }
822
823         g_list_free(parent->group);
824         parent->group = NULL;
825         parent->group_rank = 0.0;
826 }
827
828 static gint dupe_match_link_exists(DupeItem *child, DupeItem *parent)
829 {
830         return (dupe_match_find_match(child, parent) != NULL);
831 }
832
833 static gdouble dupe_match_link_rank(DupeItem *child, DupeItem *parent)
834 {
835         DupeMatch *dm;
836
837         dm = dupe_match_find_match(child, parent);
838         if (dm) return dm->rank;
839
840         return 0.0;
841 }
842
843 static DupeItem *dupe_match_highest_rank(DupeItem *child)
844 {
845         DupeMatch *dr;
846         GList *work;
847
848         dr = NULL;
849         work = child->group;
850         while (work)
851                 {
852                 DupeMatch *dm = work->data;
853                 if (!dr || dm->rank > dr->rank) dr = dm;
854                 work = work->next;
855                 }
856
857         return (dr) ? dr->di : NULL;
858 }
859
860 static void dupe_match_rank_update(DupeItem *parent)
861 {
862         GList *work;
863         gdouble rank = 0.0;
864         gint c = 0;
865
866         work = parent->group;
867         while (work)
868                 {
869                 DupeMatch *dm = work->data;
870                 work = work->next;
871                 rank += dm->rank;
872                 c++;
873                 }
874
875         if (c > 0)
876                 {
877                 parent->group_rank = rank / c;
878                 }
879         else
880                 {
881                 parent->group_rank = 0.0;
882                 }
883 }
884
885 static DupeItem *dupe_match_find_parent(DupeWindow *dw, DupeItem *child)
886 {
887         GList *work;
888
889         if (g_list_find(dw->dupes, child)) return child;
890
891         work = child->group;
892         while (work)
893                 {
894                 DupeMatch *dm = work->data;
895                 if (g_list_find(dw->dupes, dm->di)) return dm->di;
896                 work = work->next;
897                 }
898
899         return NULL;
900 }
901
902 static void dupe_match_reset_list(GList *work)
903 {
904         while (work)
905                 {
906                 DupeItem *di = work->data;
907                 work = work->next;
908
909                 dupe_match_link_clear(di, FALSE);
910                 }
911 }
912
913 static void dupe_match_reparent(DupeWindow *dw, DupeItem *old, DupeItem *new)
914 {
915         GList *work;
916
917         if (!old || !new || !dupe_match_link_exists(old, new)) return;
918
919         dupe_match_link_clear(new, TRUE);
920         work = old->group;
921         while (work)
922                 {
923                 DupeMatch *dm = work->data;
924                 dupe_match_unlink_child(old, dm->di);
925                 dupe_match_link_child(new, dm->di, dm->rank);
926                 work = work->next;
927                 }
928
929         new->group = old->group;
930         old->group = NULL;
931
932         work = g_list_find(dw->dupes, old);
933         if (work) work->data = new;
934 }
935
936 static void dupe_match_print_group(DupeItem *di)
937 {
938         GList *work;
939
940         log_printf("+ %f %s\n", di->group_rank, di->fd->name);
941
942         work = di->group;
943         while (work)
944                 {
945                 DupeMatch *dm = work->data;
946                 work = work->next;
947
948                 log_printf("  %f %s\n", dm->rank, dm->di->fd->name);
949                 }
950
951         log_printf("\n");
952 }
953
954 static void dupe_match_print_list(GList *list)
955 {
956         GList *work;
957
958         work = list;
959         while (work)
960                 {
961                 DupeItem *di = work->data;
962                 dupe_match_print_group(di);
963                 work = work->next;
964                 }
965 }
966
967 /* level 3, unlinking and orphan handling */
968 static GList *dupe_match_unlink_by_rank(DupeItem *child, DupeItem *parent, GList *list, DupeWindow *dw)
969 {
970         DupeItem *best;
971
972         best = dupe_match_highest_rank(parent);
973         if (best == child || dupe_match_highest_rank(child) == parent)
974                 {
975                 GList *work;
976                 gdouble rank;
977
978                 DEBUG_2("link found %s to %s [%d]", child->fd->name, parent->fd->name, g_list_length(parent->group));
979
980                 work = parent->group;
981                 while (work)
982                         {
983                         DupeMatch *dm = work->data;
984                         DupeItem *orphan;
985
986                         work = work->next;
987                         orphan = dm->di;
988                         if (orphan != child && g_list_length(orphan->group) < 2)
989                                 {
990                                 dupe_match_link_clear(orphan, TRUE);
991                                 if (!dw->second_set || orphan->second)
992                                         {
993                                         dupe_match(orphan, child, dw->match_mask, &rank, FALSE);
994                                         dupe_match_link(orphan, child, rank);
995                                         }
996                                 list = g_list_remove(list, orphan);
997                                 }
998                         }
999
1000                 rank = dupe_match_link_rank(child, parent);
1001                 dupe_match_link_clear(parent, TRUE);
1002                 dupe_match_link(child, parent, rank);
1003                 list = g_list_remove(list, parent);
1004                 }
1005         else
1006                 {
1007                 DEBUG_2("unlinking %s and %s", child->fd->name, parent->fd->name);
1008
1009                 dupe_match_unlink(child, parent);
1010                 }
1011
1012         return list;
1013 }
1014
1015 /* level 2 */
1016 static GList *dupe_match_group_filter(GList *list, DupeItem *di, DupeWindow *dw)
1017 {
1018         GList *work;
1019
1020         work = g_list_last(di->group);
1021         while (work)
1022                 {
1023                 DupeMatch *dm = work->data;
1024                 work = work->prev;
1025                 list = dupe_match_unlink_by_rank(di, dm->di, list, dw);
1026                 }
1027
1028         return list;
1029 }
1030
1031 /* level 1 (top) */
1032 static GList *dupe_match_group_trim(GList *list, DupeWindow *dw)
1033 {
1034         GList *work;
1035
1036         work = list;
1037         while (work)
1038                 {
1039                 DupeItem *di = work->data;
1040                 if (!di->second) list = dupe_match_group_filter(list, di, dw);
1041                 work = work->next;
1042                 if (di->second) list = g_list_remove(list, di);
1043                 }
1044
1045         return list;
1046 }
1047
1048 static gint dupe_match_sort_groups_cb(gconstpointer a, gconstpointer b)
1049 {
1050         DupeMatch *da = (DupeMatch *)a;
1051         DupeMatch *db = (DupeMatch *)b;
1052
1053         if (da->rank > db->rank) return -1;
1054         if (da->rank < db->rank) return 1;
1055         return 0;
1056 }
1057
1058 static void dupe_match_sort_groups(GList *list)
1059 {
1060         GList *work;
1061
1062         work = list;
1063         while (work)
1064                 {
1065                 DupeItem *di = work->data;
1066                 di->group = g_list_sort(di->group, dupe_match_sort_groups_cb);
1067                 work = work->next;
1068                 }
1069 }
1070
1071 static gint dupe_match_totals_sort_cb(gconstpointer a, gconstpointer b)
1072 {
1073         DupeItem *da = (DupeItem *)a;
1074         DupeItem *db = (DupeItem *)b;
1075
1076         if (g_list_length(da->group) > g_list_length(db->group)) return -1;
1077         if (g_list_length(da->group) < g_list_length(db->group)) return 1;
1078
1079         if (da->group_rank < db->group_rank) return -1;
1080         if (da->group_rank > db->group_rank) return 1;
1081
1082         return 0;
1083 }
1084
1085 static gint dupe_match_rank_sort_cb(gconstpointer a, gconstpointer b)
1086 {
1087         DupeItem *da = (DupeItem *)a;
1088         DupeItem *db = (DupeItem *)b;
1089
1090         if (da->group_rank > db->group_rank) return -1;
1091         if (da->group_rank < db->group_rank) return 1;
1092         return 0;
1093 }
1094
1095 /* returns allocated GList of dupes sorted by rank */
1096 static GList *dupe_match_rank_sort(GList *source_list)
1097 {
1098         GList *list = NULL;
1099         GList *work;
1100
1101         work = source_list;
1102         while (work)
1103                 {
1104                 DupeItem *di = work->data;
1105
1106                 if (di->group)
1107                         {
1108                         dupe_match_rank_update(di);
1109                         list = g_list_prepend(list, di);
1110                         }
1111
1112                 work = work->next;
1113                 }
1114
1115         return g_list_sort(list, dupe_match_rank_sort_cb);
1116 }
1117
1118 /* returns allocated GList of dupes sorted by totals */
1119 static GList *dupe_match_totals_sort(GList *source_list)
1120 {
1121         source_list = g_list_sort(source_list, dupe_match_totals_sort_cb);
1122
1123         source_list = g_list_first(source_list);
1124         return g_list_reverse(source_list);
1125 }
1126
1127 static void dupe_match_rank(DupeWindow *dw)
1128 {
1129         GList *list;
1130
1131         list = dupe_match_rank_sort(dw->list);
1132
1133         if (required_debug_level(2)) dupe_match_print_list(list);
1134
1135         DEBUG_1("Similar items: %d", g_list_length(list));
1136         list = dupe_match_group_trim(list, dw);
1137         DEBUG_1("Unique groups: %d", g_list_length(list));
1138
1139         dupe_match_sort_groups(list);
1140
1141         if (required_debug_level(2)) dupe_match_print_list(list);
1142
1143         list = dupe_match_rank_sort(list);
1144         if (options->sort_totals)
1145                 {
1146                 list = dupe_match_totals_sort(list);
1147                 }
1148         if (required_debug_level(2)) dupe_match_print_list(list);
1149
1150         g_list_free(dw->dupes);
1151         dw->dupes = list;
1152 }
1153
1154 /*
1155  * ------------------------------------------------------------------
1156  * Match group tests
1157  * ------------------------------------------------------------------
1158  */
1159
1160 static gboolean dupe_match(DupeItem *a, DupeItem *b, DupeMatchType mask, gdouble *rank, gint fast)
1161 {
1162         *rank = 0.0;
1163
1164         if (a->fd->path == b->fd->path) return FALSE;
1165
1166         if (mask & DUPE_MATCH_PATH)
1167                 {
1168                 if (utf8_compare(a->fd->path, b->fd->path, TRUE) != 0) return FALSE;
1169                 }
1170         if (mask & DUPE_MATCH_NAME)
1171                 {
1172                 if (strcmp(a->fd->collate_key_name, b->fd->collate_key_name) != 0) return FALSE;
1173                 }
1174         if (mask & DUPE_MATCH_NAME_CI)
1175                 {
1176                 if (strcmp(a->fd->collate_key_name_nocase, b->fd->collate_key_name_nocase) != 0) return FALSE;
1177                 }
1178         if (mask & DUPE_MATCH_SIZE)
1179                 {
1180                 if (a->fd->size != b->fd->size) return FALSE;
1181                 }
1182         if (mask & DUPE_MATCH_DATE)
1183                 {
1184                 if (a->fd->date != b->fd->date) return FALSE;
1185                 }
1186         if (mask & DUPE_MATCH_SUM)
1187                 {
1188                 if (!a->md5sum) a->md5sum = md5_text_from_file_utf8(a->fd->path, "");
1189                 if (!b->md5sum) b->md5sum = md5_text_from_file_utf8(b->fd->path, "");
1190                 if (a->md5sum[0] == '\0' ||
1191                     b->md5sum[0] == '\0' ||
1192                     strcmp(a->md5sum, b->md5sum) != 0) return FALSE;
1193                 }
1194         if (mask & DUPE_MATCH_DIM)
1195                 {
1196                 if (a->width == 0) image_load_dimensions(a->fd, &a->width, &a->height);
1197                 if (b->width == 0) image_load_dimensions(b->fd, &b->width, &b->height);
1198                 if (a->width != b->width || a->height != b->height) return FALSE;
1199                 }
1200         if (mask & DUPE_MATCH_SIM_HIGH ||
1201             mask & DUPE_MATCH_SIM_MED ||
1202             mask & DUPE_MATCH_SIM_LOW ||
1203             mask & DUPE_MATCH_SIM_CUSTOM)
1204                 {
1205                 gdouble f;
1206                 gdouble m;
1207
1208                 if (mask & DUPE_MATCH_SIM_HIGH) m = 0.95;
1209                 else if (mask & DUPE_MATCH_SIM_MED) m = 0.90;
1210                 else if (mask & DUPE_MATCH_SIM_CUSTOM) m = (gdouble)options->duplicates_similarity_threshold / 100.0;
1211                 else m = 0.85;
1212
1213                 if (fast)
1214                         {
1215                         f = image_sim_compare_fast(a->simd, b->simd, m);
1216                         }
1217                 else
1218                         {
1219                         f = image_sim_compare(a->simd, b->simd);
1220                         }
1221
1222                 *rank = f * 100.0;
1223
1224                 if (f < m) return FALSE;
1225
1226                 DEBUG_3("similar: %32s %32s = %f", a->fd->name, b->fd->name, f);
1227                 }
1228
1229         return TRUE;
1230 }
1231
1232 static void dupe_list_check_match(DupeWindow *dw, DupeItem *needle, GList *start)
1233 {
1234         GList *work;
1235
1236         if (dw->second_set)
1237                 {
1238                 work = dw->second_list;
1239                 }
1240         else if (start)
1241                 {
1242                 work = start;
1243                 }
1244         else
1245                 {
1246                 work = g_list_last(dw->list);
1247                 }
1248
1249         while (work)
1250                 {
1251                 DupeItem *di = work->data;
1252
1253                 /* speed opt: forward for second set, back for simple compare */
1254                 if (dw->second_set)
1255                         work = work->next;
1256                 else
1257                         work = work->prev;
1258
1259                 if (!dupe_match_link_exists(needle, di))
1260                         {
1261                         gdouble rank;
1262
1263                         if (dupe_match(di, needle, dw->match_mask, &rank, TRUE))
1264                                 {
1265                                 dupe_match_link(di, needle, rank);
1266                                 }
1267                         }
1268                 }
1269 }
1270
1271 /*
1272  * ------------------------------------------------------------------
1273  * Thumbnail handling
1274  * ------------------------------------------------------------------
1275  */
1276
1277 static void dupe_listview_set_thumb(DupeWindow *dw, DupeItem *di, GtkTreeIter *iter)
1278 {
1279         GtkListStore *store;
1280         GtkTreeIter iter_n;
1281
1282         store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->listview)));
1283         if (!iter)
1284                 {
1285                 if (dupe_listview_find_item(store, di, &iter_n) >= 0)
1286                         {
1287                         iter = &iter_n;
1288                         }
1289                 }
1290
1291         if (iter) gtk_list_store_set(store, iter, DUPE_COLUMN_THUMB, di->pixbuf, -1);
1292 }
1293
1294 static void dupe_thumb_do(DupeWindow *dw)
1295 {
1296         DupeItem *di;
1297
1298         if (!dw->thumb_loader || !dw->thumb_item) return;
1299         di = dw->thumb_item;
1300
1301         if (di->pixbuf) g_object_unref(di->pixbuf);
1302         di->pixbuf = thumb_loader_get_pixbuf(dw->thumb_loader);
1303
1304         dupe_listview_set_thumb(dw, di, NULL);
1305 }
1306
1307 static void dupe_thumb_error_cb(ThumbLoader *tl, gpointer data)
1308 {
1309         DupeWindow *dw = data;
1310
1311         dupe_thumb_do(dw);
1312         dupe_thumb_step(dw);
1313 }
1314
1315 static void dupe_thumb_done_cb(ThumbLoader *tl, gpointer data)
1316 {
1317         DupeWindow *dw = data;
1318
1319         dupe_thumb_do(dw);
1320         dupe_thumb_step(dw);
1321 }
1322
1323 static void dupe_thumb_step(DupeWindow *dw)
1324 {
1325         GtkTreeModel *store;
1326         GtkTreeIter iter;
1327         DupeItem *di = NULL;
1328         gboolean valid;
1329         gint row = 0;
1330         gint length = 0;
1331
1332         store = gtk_tree_view_get_model(GTK_TREE_VIEW(dw->listview));
1333         valid = gtk_tree_model_get_iter_first(store, &iter);
1334
1335         while (!di && valid)
1336                 {
1337                 GdkPixbuf *pixbuf;
1338
1339                 length++;
1340                 gtk_tree_model_get(store, &iter, DUPE_COLUMN_POINTER, &di, DUPE_COLUMN_THUMB, &pixbuf, -1);
1341                 if (pixbuf || di->pixbuf)
1342                         {
1343                         if (!pixbuf) gtk_list_store_set(GTK_LIST_STORE(store), &iter, DUPE_COLUMN_THUMB, di->pixbuf, -1);
1344                         row++;
1345                         di = NULL;
1346                         }
1347                 valid = gtk_tree_model_iter_next(store, &iter);
1348                 }
1349         if (valid)
1350                 {
1351                 while (gtk_tree_model_iter_next(store, &iter)) length++;
1352                 }
1353
1354         if (!di)
1355                 {
1356                 dw->thumb_item = NULL;
1357                 thumb_loader_free(dw->thumb_loader);
1358                 dw->thumb_loader = NULL;
1359
1360                 dupe_window_update_progress(dw, NULL, 0.0, FALSE);
1361                 return;
1362                 }
1363
1364         dupe_window_update_progress(dw, _("Loading thumbs..."),
1365                                     length == 0 ? 0.0 : (gdouble)(row) / length, FALSE);
1366
1367         dw->thumb_item = di;
1368         thumb_loader_free(dw->thumb_loader);
1369         dw->thumb_loader = thumb_loader_new(options->thumbnails.max_width, options->thumbnails.max_height);
1370
1371         thumb_loader_set_callbacks(dw->thumb_loader,
1372                                    dupe_thumb_done_cb,
1373                                    dupe_thumb_error_cb,
1374                                    NULL,
1375                                    dw);
1376
1377         /* start it */
1378         if (!thumb_loader_start(dw->thumb_loader, di->fd))
1379                 {
1380                 /* error, handle it, do next */
1381                 DEBUG_1("error loading thumb for %s", di->fd->path);
1382                 dupe_thumb_do(dw);
1383                 dupe_thumb_step(dw);
1384                 }
1385 }
1386
1387 /*
1388  * ------------------------------------------------------------------
1389  * Dupe checking loop
1390  * ------------------------------------------------------------------
1391  */
1392
1393 static void dupe_check_stop(DupeWindow *dw)
1394 {
1395         if (dw->idle_id || dw->img_loader || dw->thumb_loader)
1396                 {
1397                 g_source_remove(dw->idle_id);
1398                 dw->idle_id = 0;
1399                 dupe_window_update_progress(dw, NULL, 0.0, FALSE);
1400                 widget_set_cursor(dw->listview, -1);
1401                 }
1402
1403         thumb_loader_free(dw->thumb_loader);
1404         dw->thumb_loader = NULL;
1405
1406         image_loader_free(dw->img_loader);
1407         dw->img_loader = NULL;
1408 }
1409
1410 static void dupe_loader_done_cb(ImageLoader *il, gpointer data)
1411 {
1412         DupeWindow *dw = data;
1413         GdkPixbuf *pixbuf;
1414
1415         pixbuf = image_loader_get_pixbuf(il);
1416
1417         if (dw->setup_point)
1418                 {
1419                 DupeItem *di = dw->setup_point->data;
1420
1421                 if (!di->simd)
1422                         {
1423                         di->simd = image_sim_new_from_pixbuf(pixbuf);
1424                         }
1425                 else
1426                         {
1427                         image_sim_fill_data(di->simd, pixbuf);
1428                         }
1429
1430                 if (di->width == 0 && di->height == 0)
1431                         {
1432                         di->width = gdk_pixbuf_get_width(pixbuf);
1433                         di->height = gdk_pixbuf_get_height(pixbuf);
1434                         }
1435                 if (options->thumbnails.enable_caching)
1436                         {
1437                         dupe_item_write_cache(di);
1438                         }
1439
1440                 image_sim_alternate_processing(di->simd);
1441                 }
1442
1443         image_loader_free(dw->img_loader);
1444         dw->img_loader = NULL;
1445
1446         dw->idle_id = g_idle_add(dupe_check_cb, dw);
1447 }
1448
1449 static void dupe_setup_reset(DupeWindow *dw)
1450 {
1451         dw->setup_point = NULL;
1452         dw->setup_n = 0;
1453         dw->setup_time = msec_time();
1454         dw->setup_time_count = 0;
1455 }
1456
1457 static GList *dupe_setup_point_step(DupeWindow *dw, GList *p)
1458 {
1459         if (!p) return NULL;
1460
1461         if (p->next) return p->next;
1462
1463         if (dw->second_set && g_list_first(p) == dw->list) return dw->second_list;
1464
1465         return NULL;
1466 }
1467
1468 static gboolean dupe_check_cb(gpointer data)
1469 {
1470         DupeWindow *dw = data;
1471
1472         if (!dw->idle_id) return FALSE;
1473
1474         if (!dw->setup_done)
1475                 {
1476                 if ((dw->match_mask & DUPE_MATCH_SUM) &&
1477                     !(dw->setup_mask & DUPE_MATCH_SUM) )
1478                         {
1479                         if (!dw->setup_point) dw->setup_point = dw->list;
1480
1481                         while (dw->setup_point)
1482                                 {
1483                                 DupeItem *di = dw->setup_point->data;
1484
1485                                 dw->setup_point = dupe_setup_point_step(dw, dw->setup_point);
1486                                 dw->setup_n++;
1487
1488                                 if (!di->md5sum)
1489                                         {
1490                                         dupe_window_update_progress(dw, _("Reading checksums..."),
1491                                                 dw->setup_count == 0 ? 0.0 : (gdouble)(dw->setup_n - 1) / dw->setup_count, FALSE);
1492
1493                                         if (options->thumbnails.enable_caching)
1494                                                 {
1495                                                 dupe_item_read_cache(di);
1496                                                 if (di->md5sum) return TRUE;
1497                                                 }
1498
1499                                         di->md5sum = md5_text_from_file_utf8(di->fd->path, "");
1500                                         if (options->thumbnails.enable_caching)
1501                                                 {
1502                                                 dupe_item_write_cache(di);
1503                                                 }
1504                                         return TRUE;
1505                                         }
1506                                 }
1507                         dw->setup_mask |= DUPE_MATCH_SUM;
1508                         dupe_setup_reset(dw);
1509                         }
1510                 if ((dw->match_mask & DUPE_MATCH_DIM) &&
1511                     !(dw->setup_mask & DUPE_MATCH_DIM) )
1512                         {
1513                         if (!dw->setup_point) dw->setup_point = dw->list;
1514
1515                         while (dw->setup_point)
1516                                 {
1517                                 DupeItem *di = dw->setup_point->data;
1518
1519                                 dw->setup_point = dupe_setup_point_step(dw, dw->setup_point);
1520                                 dw->setup_n++;
1521                                 if (di->width == 0 && di->height == 0)
1522                                         {
1523                                         dupe_window_update_progress(dw, _("Reading dimensions..."),
1524                                                 dw->setup_count == 0 ? 0.0 : (gdouble)(dw->setup_n - 1) / dw->setup_count, FALSE);
1525
1526                                         if (options->thumbnails.enable_caching)
1527                                                 {
1528                                                 dupe_item_read_cache(di);
1529                                                 if (di->width != 0 || di->height != 0) return TRUE;
1530                                                 }
1531
1532                                         image_load_dimensions(di->fd, &di->width, &di->height);
1533                                         if (options->thumbnails.enable_caching)
1534                                                 {
1535                                                 dupe_item_write_cache(di);
1536                                                 }
1537                                         return TRUE;
1538                                         }
1539                                 }
1540                         dw->setup_mask |= DUPE_MATCH_DIM;
1541                         dupe_setup_reset(dw);
1542                         }
1543                 if ((dw->match_mask & DUPE_MATCH_SIM_HIGH ||
1544                      dw->match_mask & DUPE_MATCH_SIM_MED ||
1545                      dw->match_mask & DUPE_MATCH_SIM_LOW ||
1546                      dw->match_mask & DUPE_MATCH_SIM_CUSTOM) &&
1547                     !(dw->setup_mask & DUPE_MATCH_SIM_MED) )
1548                         {
1549                         if (!dw->setup_point) dw->setup_point = dw->list;
1550
1551                         while (dw->setup_point)
1552                                 {
1553                                 DupeItem *di = dw->setup_point->data;
1554
1555                                 if (!di->simd)
1556                                         {
1557                                         dupe_window_update_progress(dw, _("Reading similarity data..."),
1558                                                 dw->setup_count == 0 ? 0.0 : (gdouble)dw->setup_n / dw->setup_count, FALSE);
1559
1560                                         if (options->thumbnails.enable_caching)
1561                                                 {
1562                                                 dupe_item_read_cache(di);
1563                                                 if (cache_sim_data_filled(di->simd))
1564                                                         {
1565                                                         image_sim_alternate_processing(di->simd);
1566                                                         return TRUE;
1567                                                         }
1568                                                 }
1569
1570                                         dw->img_loader = image_loader_new(di->fd);
1571                                         image_loader_set_buffer_size(dw->img_loader, 8);
1572                                         g_signal_connect(G_OBJECT(dw->img_loader), "error", (GCallback)dupe_loader_done_cb, dw);
1573                                         g_signal_connect(G_OBJECT(dw->img_loader), "done", (GCallback)dupe_loader_done_cb, dw);
1574
1575                                         if (!image_loader_start(dw->img_loader))
1576                                                 {
1577                                                 image_sim_free(di->simd);
1578                                                 di->simd = image_sim_new();
1579                                                 image_loader_free(dw->img_loader);
1580                                                 dw->img_loader = NULL;
1581                                                 return TRUE;
1582                                                 }
1583                                         dw->idle_id = 0;
1584                                         return FALSE;
1585                                         }
1586
1587                                 dw->setup_point = dupe_setup_point_step(dw, dw->setup_point);
1588                                 dw->setup_n++;
1589                                 }
1590                         dw->setup_mask |= DUPE_MATCH_SIM_MED;
1591                         dupe_setup_reset(dw);
1592                         }
1593                 dupe_window_update_progress(dw, _("Comparing..."), 0.0, FALSE);
1594                 dw->setup_done = TRUE;
1595                 dupe_setup_reset(dw);
1596                 dw->setup_count = g_list_length(dw->list);
1597                 }
1598
1599         if (!dw->working)
1600                 {
1601                 if (dw->setup_count > 0)
1602                         {
1603                         dw->setup_count = 0;
1604                         dupe_window_update_progress(dw, _("Sorting..."), 1.0, TRUE);
1605                         return TRUE;
1606                         }
1607                 dw->idle_id = 0;
1608                 dupe_window_update_progress(dw, NULL, 0.0, FALSE);
1609
1610                 dupe_match_rank(dw);
1611                 dupe_window_update_count(dw, FALSE);
1612
1613                 dupe_listview_populate(dw);
1614
1615                 /* check thumbs */
1616                 if (dw->show_thumbs) dupe_thumb_step(dw);
1617
1618                 widget_set_cursor(dw->listview, -1);
1619
1620                 return FALSE;
1621                 }
1622
1623         dupe_list_check_match(dw, (DupeItem *)dw->working->data, dw->working);
1624         dupe_window_update_progress(dw, _("Comparing..."), dw->setup_count == 0 ? 0.0 : (gdouble) dw->setup_n / dw->setup_count, FALSE);
1625         dw->setup_n++;
1626
1627         dw->working = dw->working->prev;
1628
1629         return TRUE;
1630 }
1631
1632 static void dupe_check_start(DupeWindow *dw)
1633 {
1634         dw->setup_done = FALSE;
1635
1636         dw->setup_count = g_list_length(dw->list);
1637         if (dw->second_set) dw->setup_count += g_list_length(dw->second_list);
1638
1639         dw->setup_mask = 0;
1640         dupe_setup_reset(dw);
1641
1642         dw->working = g_list_last(dw->list);
1643
1644         dupe_window_update_count(dw, TRUE);
1645         widget_set_cursor(dw->listview, GDK_WATCH);
1646
1647         if (dw->idle_id) return;
1648
1649         dw->idle_id = g_idle_add(dupe_check_cb, dw);
1650 }
1651
1652 /*
1653  * ------------------------------------------------------------------
1654  * Item addition, removal
1655  * ------------------------------------------------------------------
1656  */
1657
1658 static void dupe_item_remove(DupeWindow *dw, DupeItem *di)
1659 {
1660         if (!di) return;
1661
1662         /* handle things that may be in progress... */
1663         if (dw->working && dw->working->data == di)
1664                 {
1665                 dw->working = dw->working->prev;
1666                 }
1667         if (dw->thumb_loader && dw->thumb_item == di)
1668                 {
1669                 dupe_thumb_step(dw);
1670                 }
1671         if (dw->setup_point && dw->setup_point->data == di)
1672                 {
1673                 dw->setup_point = dupe_setup_point_step(dw, dw->setup_point);
1674                 if (dw->img_loader)
1675                         {
1676                         image_loader_free(dw->img_loader);
1677                         dw->img_loader = NULL;
1678                         dw->idle_id = g_idle_add(dupe_check_cb, dw);
1679                         }
1680                 }
1681
1682         if (di->group && dw->dupes)
1683                 {
1684                 /* is a dupe, must remove from group/reset children if a parent */
1685                 DupeItem *parent;
1686
1687                 parent = dupe_match_find_parent(dw, di);
1688                 if (di == parent)
1689                         {
1690                         if (g_list_length(parent->group) < 2)
1691                                 {
1692                                 DupeItem *child;
1693
1694                                 child = dupe_match_highest_rank(parent);
1695                                 dupe_match_link_clear(child, TRUE);
1696                                 dupe_listview_remove(dw, child);
1697
1698                                 dupe_match_link_clear(parent, TRUE);
1699                                 dupe_listview_remove(dw, parent);
1700                                 dw->dupes = g_list_remove(dw->dupes, parent);
1701                                 }
1702                         else
1703                                 {
1704                                 DupeItem *new_parent;
1705                                 DupeMatch *dm;
1706
1707                                 dm = parent->group->data;
1708                                 new_parent = dm->di;
1709                                 dupe_match_reparent(dw, parent, new_parent);
1710                                 dupe_listview_remove(dw, parent);
1711                                 }
1712                         }
1713                 else
1714                         {
1715                         if (g_list_length(parent->group) < 2)
1716                                 {
1717                                 dupe_match_link_clear(parent, TRUE);
1718                                 dupe_listview_remove(dw, parent);
1719                                 dw->dupes = g_list_remove(dw->dupes, parent);
1720                                 }
1721                         dupe_match_link_clear(di, TRUE);
1722                         dupe_listview_remove(dw, di);
1723                         }
1724                 }
1725         else
1726                 {
1727                 /* not a dupe, or not sorted yet, simply reset */
1728                 dupe_match_link_clear(di, TRUE);
1729                 }
1730
1731         if (dw->second_list && g_list_find(dw->second_list, di))
1732                 {
1733                 dupe_second_remove(dw, di);
1734                 }
1735         else
1736                 {
1737                 dw->list = g_list_remove(dw->list, di);
1738                 }
1739         dupe_item_free(di);
1740
1741         dupe_window_update_count(dw, FALSE);
1742 }
1743
1744 static gboolean dupe_item_remove_by_path(DupeWindow *dw, const gchar *path)
1745 {
1746         DupeItem *di;
1747
1748         di = dupe_item_find_path(dw, path);
1749         if (!di) return FALSE;
1750
1751         dupe_item_remove(dw, di);
1752
1753         return TRUE;
1754 }
1755
1756 static void dupe_files_add(DupeWindow *dw, CollectionData *collection, CollectInfo *info,
1757                            FileData *fd, gboolean recurse)
1758 {
1759         DupeItem *di = NULL;
1760
1761         if (info)
1762                 {
1763                 di = dupe_item_new(info->fd);
1764                 }
1765         else if (fd)
1766                 {
1767                 if (isfile(fd->path))
1768                         {
1769                         di = dupe_item_new(fd);
1770                         }
1771                 else if (isdir(fd->path) && recurse)
1772                         {
1773                         GList *f, *d;
1774                         if (filelist_read(fd, &f, &d))
1775                                 {
1776                                 GList *work;
1777
1778                                 f = filelist_filter(f, FALSE);
1779                                 d = filelist_filter(d, TRUE);
1780
1781                                 work = f;
1782                                 while (work)
1783                                         {
1784                                         dupe_files_add(dw, NULL, NULL, (FileData *)work->data, TRUE);
1785                                         work = work->next;
1786                                         }
1787                                 filelist_free(f);
1788                                 work = d;
1789                                 while (work)
1790                                         {
1791                                         dupe_files_add(dw, NULL, NULL, (FileData *)work->data, TRUE);
1792                                         work = work->next;
1793                                         }
1794                                 filelist_free(d);
1795                                 }
1796                         }
1797                 }
1798
1799         if (!di) return;
1800
1801         /* Ensure images in the lists have unique FileDatas */
1802         GList *work;
1803         DupeItem *di_list;
1804         work = g_list_first(dw->list);
1805         while (work)
1806                 {
1807                 di_list = work->data;
1808                 if (di_list->fd == di->fd)
1809                         {
1810                         return;
1811                         }
1812                 else
1813                         {
1814                         work = work->next;
1815                         }
1816                 }
1817
1818         if (dw->second_list)
1819                 {
1820                 work = g_list_first(dw->second_list);
1821                 while (work)
1822                         {
1823                         di_list = work->data;
1824                         if (di_list->fd == di->fd)
1825                                 {
1826                                 return;
1827                                 }
1828                         else
1829                                 {
1830                                 work = work->next;
1831                                 }
1832                         }
1833                 }
1834
1835         if (dw->second_drop)
1836                 {
1837                 dupe_second_add(dw, di);
1838                 }
1839         else
1840                 {
1841                 dw->list = g_list_prepend(dw->list, di);
1842                 }
1843 }
1844
1845 void dupe_window_add_collection(DupeWindow *dw, CollectionData *collection)
1846 {
1847         CollectInfo *info;
1848
1849         info = collection_get_first(collection);
1850         while (info)
1851                 {
1852                 dupe_files_add(dw, collection, info, NULL, FALSE);
1853                 info = collection_next_by_info(collection, info);
1854                 }
1855
1856         dupe_check_start(dw);
1857 }
1858
1859 void dupe_window_add_files(DupeWindow *dw, GList *list, gboolean recurse)
1860 {
1861         GList *work;
1862
1863         work = list;
1864         while (work)
1865                 {
1866                 FileData *fd = work->data;
1867                 work = work->next;
1868
1869                 dupe_files_add(dw, NULL, NULL, fd, recurse);
1870                 }
1871
1872         dupe_check_start(dw);
1873 }
1874
1875 static void dupe_item_update(DupeWindow *dw, DupeItem *di)
1876 {
1877         if ( (dw->match_mask & DUPE_MATCH_NAME) || (dw->match_mask & DUPE_MATCH_PATH || (dw->match_mask & DUPE_MATCH_NAME_CI)) )
1878                 {
1879                 /* only effects matches on name or path */
1880 /*
1881                 FileData *fd = file_data_ref(di->fd);
1882                 gint second;
1883
1884                 second = di->second;
1885                 dupe_item_remove(dw, di);
1886
1887                 dw->second_drop = second;
1888                 dupe_files_add(dw, NULL, NULL, fd, FALSE);
1889                 dw->second_drop = FALSE;
1890
1891                 file_data_unref(fd);
1892 */
1893                 dupe_check_start(dw);
1894                 }
1895         else
1896                 {
1897                 GtkListStore *store;
1898                 GtkTreeIter iter;
1899                 gint row;
1900                 /* update the listview(s) */
1901
1902                 store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->listview)));
1903                 row = dupe_listview_find_item(store, di, &iter);
1904                 if (row >= 0)
1905                         {
1906                         gtk_list_store_set(store, &iter,
1907                                            DUPE_COLUMN_NAME, di->fd->name,
1908                                            DUPE_COLUMN_PATH, di->fd->path, -1);
1909                         }
1910
1911                 if (dw->second_listview)
1912                         {
1913                         store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->second_listview)));
1914                         row = dupe_listview_find_item(store, di, &iter);
1915                         if (row >= 0)
1916                                 {
1917                                 gtk_list_store_set(store, &iter, 1, di->fd->path, -1);
1918                                 }
1919                         }
1920                 }
1921
1922 }
1923
1924 static void dupe_item_update_fd_in_list(DupeWindow *dw, FileData *fd, GList *work)
1925 {
1926         while (work)
1927                 {
1928                 DupeItem *di = work->data;
1929
1930                 if (di->fd == fd)
1931                         dupe_item_update(dw, di);
1932
1933                 work = work->next;
1934                 }
1935 }
1936
1937 static void dupe_item_update_fd(DupeWindow *dw, FileData *fd)
1938 {
1939         dupe_item_update_fd_in_list(dw, fd, dw->list);
1940         if (dw->second_set) dupe_item_update_fd_in_list(dw, fd, dw->second_list);
1941 }
1942
1943
1944 /*
1945  * ------------------------------------------------------------------
1946  * Misc.
1947  * ------------------------------------------------------------------
1948  */
1949
1950 static GtkWidget *dupe_display_label(GtkWidget *vbox, const gchar *description, const gchar *text)
1951 {
1952         GtkWidget *hbox;
1953         GtkWidget *label;
1954
1955         hbox = gtk_hbox_new(FALSE, 10);
1956
1957         label = gtk_label_new(description);
1958         gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 0);
1959         gtk_widget_show(label);
1960
1961         label = gtk_label_new(text);
1962         gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 0);
1963         gtk_widget_show(label);
1964
1965         gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0);
1966         gtk_widget_show(hbox);
1967
1968         return label;
1969 }
1970
1971 static void dupe_display_stats(DupeWindow *dw, DupeItem *di)
1972 {
1973         GenericDialog *gd;
1974         gchar *buf;
1975
1976         if (!di) return;
1977
1978         gd = file_util_gen_dlg("Image thumbprint debug info", "thumbprint",
1979                                dw->window, TRUE,
1980                                NULL, NULL);
1981         generic_dialog_add_button(gd, GTK_STOCK_CLOSE, NULL, NULL, TRUE);
1982
1983         dupe_display_label(gd->vbox, "name:", di->fd->name);
1984         buf = text_from_size(di->fd->size);
1985         dupe_display_label(gd->vbox, "size:", buf);
1986         g_free(buf);
1987         dupe_display_label(gd->vbox, "date:", text_from_time(di->fd->date));
1988         buf = g_strdup_printf("%d x %d", di->width, di->height);
1989         dupe_display_label(gd->vbox, "dimensions:", buf);
1990         g_free(buf);
1991         dupe_display_label(gd->vbox, "md5sum:", (di->md5sum) ? di->md5sum : "not generated");
1992
1993         dupe_display_label(gd->vbox, "thumbprint:", (di->simd) ? "" : "not generated");
1994         if (di->simd)
1995                 {
1996                 GtkWidget *image;
1997                 GdkPixbuf *pixbuf;
1998                 gint x, y;
1999                 guchar *d_pix;
2000                 guchar *dp;
2001                 gint rs;
2002                 gint sp;
2003
2004                 pixbuf = gdk_pixbuf_new(GDK_COLORSPACE_RGB, FALSE, 8, 32, 32);
2005                 rs = gdk_pixbuf_get_rowstride(pixbuf);
2006                 d_pix = gdk_pixbuf_get_pixels(pixbuf);
2007
2008                 for (y = 0; y < 32; y++)
2009                         {
2010                         dp = d_pix + (y * rs);
2011                         sp = y * 32;
2012                         for (x = 0; x < 32; x++)
2013                                 {
2014                                 *(dp++) = di->simd->avg_r[sp + x];
2015                                 *(dp++) = di->simd->avg_g[sp + x];
2016                                 *(dp++) = di->simd->avg_b[sp + x];
2017                                 }
2018                         }
2019
2020                 image = gtk_image_new_from_pixbuf(pixbuf);
2021                 gtk_box_pack_start(GTK_BOX(gd->vbox), image, FALSE, FALSE, 0);
2022                 gtk_widget_show(image);
2023
2024                 g_object_unref(pixbuf);
2025                 }
2026
2027         gtk_widget_show(gd->dialog);
2028 }
2029
2030 static void dupe_window_recompare(DupeWindow *dw)
2031 {
2032         GtkListStore *store;
2033
2034         dupe_check_stop(dw);
2035
2036         store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->listview)));
2037         gtk_list_store_clear(store);
2038
2039         g_list_free(dw->dupes);
2040         dw->dupes = NULL;
2041
2042         dupe_match_reset_list(dw->list);
2043         dupe_match_reset_list(dw->second_list);
2044
2045         dupe_check_start(dw);
2046 }
2047
2048 static void dupe_menu_view(DupeWindow *dw, DupeItem *di, GtkWidget *listview, gint new_window)
2049 {
2050         if (!di) return;
2051
2052         if (di->collection && collection_info_valid(di->collection, di->info))
2053                 {
2054                 if (new_window)
2055                         {
2056                         view_window_new_from_collection(di->collection, di->info);
2057                         }
2058                 else
2059                         {
2060                         layout_image_set_collection(NULL, di->collection, di->info);
2061                         }
2062                 }
2063         else
2064                 {
2065                 if (new_window)
2066                         {
2067                         GList *list;
2068
2069                         list = dupe_listview_get_selection(dw, listview);
2070                         view_window_new_from_list(list);
2071                         filelist_free(list);
2072                         }
2073                 else
2074                         {
2075                         layout_set_fd(NULL, di->fd);
2076                         }
2077                 }
2078 }
2079
2080 static void dupe_window_remove_selection(DupeWindow *dw, GtkWidget *listview)
2081 {
2082         GtkTreeSelection *selection;
2083         GtkTreeModel *store;
2084         GtkTreeIter iter;
2085         GList *slist;
2086         GList *list = NULL;
2087         GList *work;
2088
2089         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(listview));
2090         slist = gtk_tree_selection_get_selected_rows(selection, &store);
2091         work = slist;
2092         while (work)
2093                 {
2094                 GtkTreePath *tpath = work->data;
2095                 DupeItem *di = NULL;
2096
2097                 gtk_tree_model_get_iter(store, &iter, tpath);
2098                 gtk_tree_model_get(store, &iter, DUPE_COLUMN_POINTER, &di, -1);
2099                 if (di) list = g_list_prepend(list, di);
2100                 work = work->next;
2101                 }
2102         g_list_foreach(slist, (GFunc)gtk_tree_path_free, NULL);
2103         g_list_free(slist);
2104
2105         dw->color_frozen = TRUE;
2106         work = list;
2107         while (work)
2108                 {
2109                 DupeItem *di;
2110
2111                 di = work->data;
2112                 work = work->next;
2113                 dupe_item_remove(dw, di);
2114                 }
2115         dw->color_frozen = FALSE;
2116
2117         g_list_free(list);
2118
2119         dupe_listview_realign_colors(dw);
2120 }
2121
2122 static void dupe_window_edit_selected(DupeWindow *dw, const gchar *key)
2123 {
2124         file_util_start_editor_from_filelist(key, dupe_listview_get_selection(dw, dw->listview), NULL, dw->window);
2125 }
2126
2127 static void dupe_window_collection_from_selection(DupeWindow *dw)
2128 {
2129         CollectWindow *w;
2130         GList *list;
2131
2132         list = dupe_listview_get_selection(dw, dw->listview);
2133         w = collection_window_new(NULL);
2134         collection_table_add_filelist(w->table, list);
2135         filelist_free(list);
2136 }
2137
2138 static void dupe_window_append_file_list(DupeWindow *dw, gint on_second)
2139 {
2140         GList *list;
2141
2142         dw->second_drop = (dw->second_set && on_second);
2143
2144         list = layout_list(NULL);
2145         dupe_window_add_files(dw, list, FALSE);
2146         filelist_free(list);
2147 }
2148
2149 /*
2150  *-------------------------------------------------------------------
2151  * main pop-up menu callbacks
2152  *-------------------------------------------------------------------
2153  */
2154
2155 static void dupe_menu_view_cb(GtkWidget *widget, gpointer data)
2156 {
2157         DupeWindow *dw = data;
2158
2159         if (dw->click_item) dupe_menu_view(dw, dw->click_item, dw->listview, FALSE);
2160 }
2161
2162 static void dupe_menu_viewnew_cb(GtkWidget *widget, gpointer data)
2163 {
2164         DupeWindow *dw = data;
2165
2166         if (dw->click_item) dupe_menu_view(dw, dw->click_item, dw->listview, TRUE);
2167 }
2168
2169 static void dupe_menu_select_all_cb(GtkWidget *widget, gpointer data)
2170 {
2171         DupeWindow *dw = data;
2172         GtkTreeSelection *selection;
2173
2174         options->duplicates_select_type = DUPE_SELECT_NONE;
2175         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(dw->listview));
2176         gtk_tree_selection_select_all(selection);
2177 }
2178
2179 static void dupe_menu_select_none_cb(GtkWidget *widget, gpointer data)
2180 {
2181         DupeWindow *dw = data;
2182         GtkTreeSelection *selection;
2183
2184         options->duplicates_select_type = DUPE_SELECT_NONE;
2185         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(dw->listview));
2186         gtk_tree_selection_unselect_all(selection);
2187 }
2188
2189 static void dupe_menu_select_dupes_set1_cb(GtkWidget *widget, gpointer data)
2190 {
2191         DupeWindow *dw = data;
2192
2193         options->duplicates_select_type = DUPE_SELECT_GROUP1;
2194         dupe_listview_select_dupes(dw, DUPE_SELECT_GROUP1);
2195 }
2196
2197 static void dupe_menu_select_dupes_set2_cb(GtkWidget *widget, gpointer data)
2198 {
2199         DupeWindow *dw = data;
2200
2201         options->duplicates_select_type = DUPE_SELECT_GROUP2;
2202         dupe_listview_select_dupes(dw, DUPE_SELECT_GROUP2);
2203 }
2204
2205 static void dupe_menu_edit_cb(GtkWidget *widget, gpointer data)
2206 {
2207         DupeWindow *dw;
2208         const gchar *key = data;
2209
2210         dw = submenu_item_get_data(widget);
2211         if (!dw) return;
2212
2213         dupe_window_edit_selected(dw, key);
2214 }
2215
2216 static void dupe_menu_print_cb(GtkWidget *widget, gpointer data)
2217 {
2218         DupeWindow *dw = data;
2219         FileData *fd;
2220
2221         fd = (dw->click_item) ? dw->click_item->fd : NULL;
2222
2223         print_window_new(fd,
2224                          dupe_listview_get_selection(dw, dw->listview),
2225                          dupe_listview_get_filelist(dw, dw->listview), dw->window);
2226 }
2227
2228 static void dupe_menu_copy_cb(GtkWidget *widget, gpointer data)
2229 {
2230         DupeWindow *dw = data;
2231
2232         file_util_copy(NULL, dupe_listview_get_selection(dw, dw->listview), NULL, dw->window);
2233 }
2234
2235 static void dupe_menu_move_cb(GtkWidget *widget, gpointer data)
2236 {
2237         DupeWindow *dw = data;
2238
2239         file_util_move(NULL, dupe_listview_get_selection(dw, dw->listview), NULL, dw->window);
2240 }
2241
2242 static void dupe_menu_rename_cb(GtkWidget *widget, gpointer data)
2243 {
2244         DupeWindow *dw = data;
2245
2246         file_util_rename(NULL, dupe_listview_get_selection(dw, dw->listview), dw->window);
2247 }
2248
2249 static void dupe_menu_delete_cb(GtkWidget *widget, gpointer data)
2250 {
2251         DupeWindow *dw = data;
2252
2253         options->file_ops.safe_delete_enable = FALSE;
2254         file_util_delete(NULL, dupe_listview_get_selection(dw, dw->listview), dw->window);
2255 }
2256
2257 static void dupe_menu_move_to_trash_cb(GtkWidget *widget, gpointer data)
2258 {
2259         DupeWindow *dw = data;
2260
2261         options->file_ops.safe_delete_enable = TRUE;
2262         file_util_delete(NULL, dupe_listview_get_selection(dw, dw->listview), dw->window);
2263 }
2264
2265 static void dupe_menu_copy_path_cb(GtkWidget *widget, gpointer data)
2266 {
2267         DupeWindow *dw = data;
2268
2269         file_util_copy_path_list_to_clipboard(dupe_listview_get_selection(dw, dw->listview), TRUE);
2270 }
2271
2272 static void dupe_menu_copy_path_unquoted_cb(GtkWidget *widget, gpointer data)
2273 {
2274         DupeWindow *dw = data;
2275
2276         file_util_copy_path_list_to_clipboard(dupe_listview_get_selection(dw, dw->listview), FALSE);
2277 }
2278
2279 static void dupe_menu_remove_cb(GtkWidget *widget, gpointer data)
2280 {
2281         DupeWindow *dw = data;
2282
2283         dupe_window_remove_selection(dw, dw->listview);
2284 }
2285
2286 static void dupe_menu_clear_cb(GtkWidget *widget, gpointer data)
2287 {
2288         DupeWindow *dw = data;
2289
2290         dupe_window_clear(dw);
2291 }
2292
2293 static void dupe_menu_close_cb(GtkWidget *widget, gpointer data)
2294 {
2295         DupeWindow *dw = data;
2296
2297         dupe_window_close(dw);
2298 }
2299
2300 static void dupe_menu_popup_destroy_cb(GtkWidget *widget, gpointer data)
2301 {
2302         GList *editmenu_fd_list = data;
2303
2304         filelist_free(editmenu_fd_list);
2305 }
2306
2307 static GList *dupe_window_get_fd_list(DupeWindow *dw)
2308 {
2309         GList *list;
2310
2311         if (gtk_widget_has_focus(dw->second_listview))
2312                 {
2313                 list = dupe_listview_get_selection(dw, dw->second_listview);
2314                 }
2315         else
2316                 {
2317                 list = dupe_listview_get_selection(dw, dw->listview);
2318                 }
2319
2320         return list;
2321 }
2322
2323 /**
2324  * @brief Add file selection list to a collection
2325  * @param[in] widget 
2326  * @param[in] data Index to the collection list menu item selected, or -1 for new collection
2327  * 
2328  * 
2329  */
2330 static void dupe_pop_menu_collections_cb(GtkWidget *widget, gpointer data)
2331 {
2332         DupeWindow *dw;
2333         GList *selection_list;
2334
2335         dw = submenu_item_get_data(widget);
2336         selection_list = dupe_listview_get_selection(dw, dw->listview);
2337         pop_menu_collections(selection_list, data);
2338
2339         filelist_free(selection_list);
2340 }
2341
2342 static GtkWidget *dupe_menu_popup_main(DupeWindow *dw, DupeItem *di)
2343 {
2344         GtkWidget *menu;
2345         GtkWidget *item;
2346         gint on_row;
2347         GList *editmenu_fd_list;
2348         GtkWidget *submenu;
2349
2350         on_row = (di != NULL);
2351
2352         menu = popup_menu_short_lived();
2353
2354         menu_item_add_sensitive(menu, _("_View"), on_row,
2355                                 G_CALLBACK(dupe_menu_view_cb), dw);
2356         menu_item_add_stock_sensitive(menu, _("View in _new window"), GTK_STOCK_NEW, on_row,
2357                                 G_CALLBACK(dupe_menu_viewnew_cb), dw);
2358         menu_item_add_divider(menu);
2359         menu_item_add_sensitive(menu, _("Select all"), (dw->dupes != NULL),
2360                                 G_CALLBACK(dupe_menu_select_all_cb), dw);
2361         menu_item_add_sensitive(menu, _("Select none"), (dw->dupes != NULL),
2362                                 G_CALLBACK(dupe_menu_select_none_cb), dw);
2363         menu_item_add_sensitive(menu, _("Select group _1 duplicates"), (dw->dupes != NULL),
2364                                 G_CALLBACK(dupe_menu_select_dupes_set1_cb), dw);
2365         menu_item_add_sensitive(menu, _("Select group _2 duplicates"), (dw->dupes != NULL),
2366                                 G_CALLBACK(dupe_menu_select_dupes_set2_cb), dw);
2367         menu_item_add_divider(menu);
2368
2369         editmenu_fd_list = dupe_window_get_fd_list(dw);
2370         g_signal_connect(G_OBJECT(menu), "destroy",
2371                          G_CALLBACK(dupe_menu_popup_destroy_cb), editmenu_fd_list);
2372         submenu_add_edit(menu, &item, G_CALLBACK(dupe_menu_edit_cb), dw, editmenu_fd_list);
2373         if (!on_row) gtk_widget_set_sensitive(item, FALSE);
2374
2375         submenu_add_collections(menu, &item,
2376                                                                 G_CALLBACK(dupe_pop_menu_collections_cb), dw);
2377         gtk_widget_set_sensitive(item, on_row);
2378
2379         menu_item_add_stock_sensitive(menu, _("Print..."), GTK_STOCK_PRINT, on_row,
2380                                 G_CALLBACK(dupe_menu_print_cb), dw);
2381         menu_item_add_divider(menu);
2382         menu_item_add_stock_sensitive(menu, _("_Copy..."), GTK_STOCK_COPY, on_row,
2383                                 G_CALLBACK(dupe_menu_copy_cb), dw);
2384         menu_item_add_sensitive(menu, _("_Move..."), on_row,
2385                                 G_CALLBACK(dupe_menu_move_cb), dw);
2386         menu_item_add_sensitive(menu, _("_Rename..."), on_row,
2387                                 G_CALLBACK(dupe_menu_rename_cb), dw);
2388         menu_item_add_sensitive(menu, _("_Copy path"), on_row,
2389                                 G_CALLBACK(dupe_menu_copy_path_cb), dw);
2390         menu_item_add_sensitive(menu, _("_Copy path unquoted"), on_row,
2391                                 G_CALLBACK(dupe_menu_copy_path_unquoted_cb), dw);
2392
2393         menu_item_add_divider(menu);
2394         menu_item_add_stock_sensitive(menu,
2395                                 options->file_ops.confirm_move_to_trash ? _("Move to Trash...") :
2396                                         _("Move to Trash"), PIXBUF_INLINE_ICON_TRASH, on_row,
2397                                 G_CALLBACK(dupe_menu_move_to_trash_cb), dw);
2398         menu_item_add_stock_sensitive(menu,
2399                                 options->file_ops.confirm_delete ? _("_Delete...") :
2400                                         _("_Delete"), GTK_STOCK_DELETE, on_row,
2401                                 G_CALLBACK(dupe_menu_delete_cb), dw);
2402
2403         menu_item_add_divider(menu);
2404         menu_item_add_stock_sensitive(menu, _("Rem_ove"), GTK_STOCK_REMOVE, on_row,
2405                                 G_CALLBACK(dupe_menu_remove_cb), dw);
2406         menu_item_add_stock_sensitive(menu, _("C_lear"), GTK_STOCK_CLEAR, (dw->list != NULL),
2407                                 G_CALLBACK(dupe_menu_clear_cb), dw);
2408         menu_item_add_divider(menu);
2409         menu_item_add_stock(menu, _("Close _window"), GTK_STOCK_CLOSE,
2410                             G_CALLBACK(dupe_menu_close_cb), dw);
2411
2412         return menu;
2413 }
2414
2415 static gboolean dupe_listview_press_cb(GtkWidget *widget, GdkEventButton *bevent, gpointer data)
2416 {
2417         DupeWindow *dw = data;
2418         GtkTreeModel *store;
2419         GtkTreePath *tpath;
2420         GtkTreeIter iter;
2421         DupeItem *di = NULL;
2422
2423         store = gtk_tree_view_get_model(GTK_TREE_VIEW(widget));
2424
2425         if (gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(widget), bevent->x, bevent->y,
2426                                           &tpath, NULL, NULL, NULL))
2427                 {
2428                 gtk_tree_model_get_iter(store, &iter, tpath);
2429                 gtk_tree_model_get(store, &iter, DUPE_COLUMN_POINTER, &di, -1);
2430                 gtk_tree_path_free(tpath);
2431                 }
2432
2433         dw->click_item = di;
2434
2435         if (bevent->button == MOUSE_BUTTON_RIGHT)
2436                 {
2437                 /* right click menu */
2438                 GtkWidget *menu;
2439
2440                 if (bevent->state & GDK_CONTROL_MASK && bevent->state & GDK_SHIFT_MASK)
2441                         {
2442                         dupe_display_stats(dw, di);
2443                         return TRUE;
2444                         }
2445                 if (widget == dw->listview)
2446                         {
2447                         menu = dupe_menu_popup_main(dw, di);
2448                         }
2449                 else
2450                         {
2451                         menu = dupe_menu_popup_second(dw, di);
2452                         }
2453                 gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL, bevent->button, bevent->time);
2454                 }
2455
2456         if (!di) return FALSE;
2457
2458         if (bevent->button == MOUSE_BUTTON_LEFT &&
2459             bevent->type == GDK_2BUTTON_PRESS)
2460                 {
2461                 dupe_menu_view(dw, di, widget, FALSE);
2462                 }
2463
2464         if (bevent->button == MOUSE_BUTTON_MIDDLE) return TRUE;
2465
2466         if (bevent->button == MOUSE_BUTTON_RIGHT)
2467                 {
2468                 if (!dupe_listview_item_is_selected(dw, di, widget))
2469                         {
2470                         GtkTreeSelection *selection;
2471
2472                         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(widget));
2473                         gtk_tree_selection_unselect_all(selection);
2474                         gtk_tree_selection_select_iter(selection, &iter);
2475
2476                         tpath = gtk_tree_model_get_path(GTK_TREE_MODEL(store), &iter);
2477                         gtk_tree_view_set_cursor(GTK_TREE_VIEW(widget), tpath, NULL, FALSE);
2478                         gtk_tree_path_free(tpath);
2479                         }
2480
2481                 return TRUE;
2482                 }
2483
2484         if (bevent->button == MOUSE_BUTTON_LEFT &&
2485             bevent->type == GDK_BUTTON_PRESS &&
2486             !(bevent->state & GDK_SHIFT_MASK ) &&
2487             !(bevent->state & GDK_CONTROL_MASK ) &&
2488             dupe_listview_item_is_selected(dw, di, widget))
2489                 {
2490                 /* this selection handled on release_cb */
2491                 gtk_widget_grab_focus(widget);
2492                 return TRUE;
2493                 }
2494
2495         return FALSE;
2496 }
2497
2498 static gboolean dupe_listview_release_cb(GtkWidget *widget, GdkEventButton *bevent, gpointer data)
2499 {
2500         DupeWindow *dw = data;
2501         GtkTreeModel *store;
2502         GtkTreePath *tpath;
2503         GtkTreeIter iter;
2504         DupeItem *di = NULL;
2505
2506         if (bevent->button != MOUSE_BUTTON_LEFT && bevent->button != MOUSE_BUTTON_MIDDLE) return TRUE;
2507
2508         store = gtk_tree_view_get_model(GTK_TREE_VIEW(widget));
2509
2510         if ((bevent->x != 0 || bevent->y != 0) &&
2511             gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(widget), bevent->x, bevent->y,
2512                                           &tpath, NULL, NULL, NULL))
2513                 {
2514                 gtk_tree_model_get_iter(store, &iter, tpath);
2515                 gtk_tree_model_get(store, &iter, DUPE_COLUMN_POINTER, &di, -1);
2516                 gtk_tree_path_free(tpath);
2517                 }
2518
2519         if (bevent->button == MOUSE_BUTTON_MIDDLE)
2520                 {
2521                 if (di && dw->click_item == di)
2522                         {
2523                         GtkTreeSelection *selection;
2524
2525                         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(widget));
2526                         if (dupe_listview_item_is_selected(dw, di, widget))
2527                                 {
2528                                 gtk_tree_selection_unselect_iter(selection, &iter);
2529                                 }
2530                         else
2531                                 {
2532                                 gtk_tree_selection_select_iter(selection, &iter);
2533                                 }
2534                         }
2535                 return TRUE;
2536                 }
2537
2538         if (di && dw->click_item == di &&
2539             !(bevent->state & GDK_SHIFT_MASK ) &&
2540             !(bevent->state & GDK_CONTROL_MASK ) &&
2541             dupe_listview_item_is_selected(dw, di, widget))
2542                 {
2543                 GtkTreeSelection *selection;
2544
2545                 selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(widget));
2546                 gtk_tree_selection_unselect_all(selection);
2547                 gtk_tree_selection_select_iter(selection, &iter);
2548
2549                 tpath = gtk_tree_model_get_path(store, &iter);
2550                 gtk_tree_view_set_cursor(GTK_TREE_VIEW(widget), tpath, NULL, FALSE);
2551                 gtk_tree_path_free(tpath);
2552
2553                 return TRUE;
2554                 }
2555
2556         return FALSE;
2557 }
2558
2559 /*
2560  *-------------------------------------------------------------------
2561  * second set stuff
2562  *-------------------------------------------------------------------
2563  */
2564
2565 static void dupe_second_update_status(DupeWindow *dw)
2566 {
2567         gchar *buf;
2568
2569         buf = g_strdup_printf(_("%d files (set 2)"), g_list_length(dw->second_list));
2570         gtk_label_set_text(GTK_LABEL(dw->second_status_label), buf);
2571         g_free(buf);
2572 }
2573
2574 static void dupe_second_add(DupeWindow *dw, DupeItem *di)
2575 {
2576         GtkListStore *store;
2577         GtkTreeIter iter;
2578
2579         if (!di) return;
2580
2581         di->second = TRUE;
2582         dw->second_list = g_list_prepend(dw->second_list, di);
2583
2584         store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->second_listview)));
2585         gtk_list_store_append(store, &iter);
2586         gtk_list_store_set(store, &iter, DUPE_COLUMN_POINTER, di, 1, di->fd->path, -1);
2587
2588         dupe_second_update_status(dw);
2589 }
2590
2591 static void dupe_second_remove(DupeWindow *dw, DupeItem *di)
2592 {
2593         GtkListStore *store;
2594         GtkTreeIter iter;
2595
2596         store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->second_listview)));
2597         if (dupe_listview_find_item(store, di, &iter) >= 0)
2598                 {
2599                 tree_view_move_cursor_away(GTK_TREE_VIEW(dw->second_listview), &iter, TRUE);
2600                 gtk_list_store_remove(store, &iter);
2601                 }
2602
2603         dw->second_list = g_list_remove(dw->second_list, di);
2604
2605         dupe_second_update_status(dw);
2606 }
2607
2608 static void dupe_second_clear(DupeWindow *dw)
2609 {
2610         GtkListStore *store;
2611
2612         store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->second_listview)));
2613         gtk_list_store_clear(store);
2614         gtk_tree_view_columns_autosize(GTK_TREE_VIEW(dw->second_listview));
2615
2616         g_list_free(dw->dupes);
2617         dw->dupes = NULL;
2618
2619         dupe_list_free(dw->second_list);
2620         dw->second_list = NULL;
2621
2622         dupe_match_reset_list(dw->list);
2623
2624         dupe_second_update_status(dw);
2625 }
2626
2627 static void dupe_second_menu_view_cb(GtkWidget *widget, gpointer data)
2628 {
2629         DupeWindow *dw = data;
2630
2631         if (dw->click_item) dupe_menu_view(dw, dw->click_item, dw->second_listview, FALSE);
2632 }
2633
2634 static void dupe_second_menu_viewnew_cb(GtkWidget *widget, gpointer data)
2635 {
2636         DupeWindow *dw = data;
2637
2638         if (dw->click_item) dupe_menu_view(dw, dw->click_item, dw->second_listview, TRUE);
2639 }
2640
2641 static void dupe_second_menu_select_all_cb(GtkWidget *widget, gpointer data)
2642 {
2643         GtkTreeSelection *selection;
2644         DupeWindow *dw = data;
2645
2646         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(dw->second_listview));
2647         gtk_tree_selection_select_all(selection);
2648 }
2649
2650 static void dupe_second_menu_select_none_cb(GtkWidget *widget, gpointer data)
2651 {
2652         GtkTreeSelection *selection;
2653         DupeWindow *dw = data;
2654
2655         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(dw->second_listview));
2656         gtk_tree_selection_unselect_all(selection);
2657 }
2658
2659 static void dupe_second_menu_remove_cb(GtkWidget *widget, gpointer data)
2660 {
2661         DupeWindow *dw = data;
2662
2663         dupe_window_remove_selection(dw, dw->second_listview);
2664 }
2665
2666 static void dupe_second_menu_clear_cb(GtkWidget *widget, gpointer data)
2667 {
2668         DupeWindow *dw = data;
2669
2670         dupe_second_clear(dw);
2671         dupe_window_recompare(dw);
2672 }
2673
2674 static GtkWidget *dupe_menu_popup_second(DupeWindow *dw, DupeItem *di)
2675 {
2676         GtkWidget *menu;
2677         gboolean notempty = (dw->second_list != NULL);
2678         gboolean on_row = (di != NULL);
2679
2680         menu = popup_menu_short_lived();
2681         menu_item_add_sensitive(menu, _("_View"), on_row,
2682                                 G_CALLBACK(dupe_second_menu_view_cb), dw);
2683         menu_item_add_stock_sensitive(menu, _("View in _new window"), GTK_STOCK_NEW, on_row,
2684                                 G_CALLBACK(dupe_second_menu_viewnew_cb), dw);
2685         menu_item_add_divider(menu);
2686         menu_item_add_sensitive(menu, _("Select all"), notempty,
2687                                 G_CALLBACK(dupe_second_menu_select_all_cb), dw);
2688         menu_item_add_sensitive(menu, _("Select none"), notempty,
2689                                 G_CALLBACK(dupe_second_menu_select_none_cb), dw);
2690         menu_item_add_divider(menu);
2691         menu_item_add_stock_sensitive(menu, _("Rem_ove"), GTK_STOCK_REMOVE, on_row,
2692                                       G_CALLBACK(dupe_second_menu_remove_cb), dw);
2693         menu_item_add_stock_sensitive(menu, _("C_lear"), GTK_STOCK_CLEAR, notempty,
2694                                    G_CALLBACK(dupe_second_menu_clear_cb), dw);
2695         menu_item_add_divider(menu);
2696         menu_item_add_stock(menu, _("Close _window"), GTK_STOCK_CLOSE,
2697                             G_CALLBACK(dupe_menu_close_cb), dw);
2698
2699         return menu;
2700 }
2701
2702 static void dupe_second_set_toggle_cb(GtkWidget *widget, gpointer data)
2703 {
2704         DupeWindow *dw = data;
2705
2706         dw->second_set = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget));
2707
2708         if (dw->second_set)
2709                 {
2710                 dupe_second_update_status(dw);
2711                 gtk_table_set_col_spacings(GTK_TABLE(dw->table), PREF_PAD_GAP);
2712                 gtk_widget_show(dw->second_vbox);
2713                 }
2714         else
2715                 {
2716                 gtk_table_set_col_spacings(GTK_TABLE(dw->table), 0);
2717                 gtk_widget_hide(dw->second_vbox);
2718                 dupe_second_clear(dw);
2719                 }
2720
2721         dupe_window_recompare(dw);
2722 }
2723
2724 static void dupe_sort_totals_toggle_cb(GtkWidget *widget, gpointer data)
2725 {
2726         DupeWindow *dw = data;
2727
2728         options->sort_totals = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget));
2729         dupe_window_recompare(dw);
2730
2731 }
2732
2733 /*
2734  *-------------------------------------------------------------------
2735  * match type menu
2736  *-------------------------------------------------------------------
2737  */
2738
2739 enum {
2740         DUPE_MENU_COLUMN_NAME = 0,
2741         DUPE_MENU_COLUMN_MASK
2742 };
2743
2744 static void dupe_menu_type_cb(GtkWidget *combo, gpointer data)
2745 {
2746         DupeWindow *dw = data;
2747         GtkTreeModel *store;
2748         GtkTreeIter iter;
2749
2750         store = gtk_combo_box_get_model(GTK_COMBO_BOX(combo));
2751         if (!gtk_combo_box_get_active_iter(GTK_COMBO_BOX(combo), &iter)) return;
2752         gtk_tree_model_get(store, &iter, DUPE_MENU_COLUMN_MASK, &dw->match_mask, -1);
2753
2754         options->duplicates_match = dw->match_mask;
2755
2756         dupe_window_recompare(dw);
2757 }
2758
2759 static void dupe_menu_add_item(GtkListStore *store, const gchar *text, DupeMatchType type, DupeWindow *dw)
2760 {
2761         GtkTreeIter iter;
2762
2763         gtk_list_store_append(store, &iter);
2764         gtk_list_store_set(store, &iter, DUPE_MENU_COLUMN_NAME, text,
2765                                          DUPE_MENU_COLUMN_MASK, type, -1);
2766
2767         if (dw->match_mask == type) gtk_combo_box_set_active_iter(GTK_COMBO_BOX(dw->combo), &iter);
2768 }
2769
2770 static void dupe_menu_setup(DupeWindow *dw)
2771 {
2772         GtkListStore *store;
2773         GtkCellRenderer *renderer;
2774
2775         store = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_INT);
2776         dw->combo = gtk_combo_box_new_with_model(GTK_TREE_MODEL(store));
2777         g_object_unref(store);
2778
2779         renderer = gtk_cell_renderer_text_new();
2780         gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(dw->combo), renderer, TRUE);
2781         gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(dw->combo), renderer,
2782                                        "text", DUPE_MENU_COLUMN_NAME, NULL);
2783
2784         dupe_menu_add_item(store, _("Name"), DUPE_MATCH_NAME, dw);
2785         dupe_menu_add_item(store, _("Name case-insensitive"), DUPE_MATCH_NAME_CI, dw);
2786         dupe_menu_add_item(store, _("Size"), DUPE_MATCH_SIZE, dw);
2787         dupe_menu_add_item(store, _("Date"), DUPE_MATCH_DATE, dw);
2788         dupe_menu_add_item(store, _("Dimensions"), DUPE_MATCH_DIM, dw);
2789         dupe_menu_add_item(store, _("Checksum"), DUPE_MATCH_SUM, dw);
2790         dupe_menu_add_item(store, _("Path"), DUPE_MATCH_PATH, dw);
2791         dupe_menu_add_item(store, _("Similarity (high)"), DUPE_MATCH_SIM_HIGH, dw);
2792         dupe_menu_add_item(store, _("Similarity"), DUPE_MATCH_SIM_MED, dw);
2793         dupe_menu_add_item(store, _("Similarity (low)"), DUPE_MATCH_SIM_LOW, dw);
2794         dupe_menu_add_item(store, _("Similarity (custom)"), DUPE_MATCH_SIM_CUSTOM, dw);
2795
2796         g_signal_connect(G_OBJECT(dw->combo), "changed",
2797                          G_CALLBACK(dupe_menu_type_cb), dw);
2798 }
2799
2800 /*
2801  *-------------------------------------------------------------------
2802  * list view columns
2803  *-------------------------------------------------------------------
2804  */
2805
2806 /* this overrides the low default of a GtkCellRenderer from 100 to CELL_HEIGHT_OVERRIDE, something sane for our purposes */
2807
2808 #define CELL_HEIGHT_OVERRIDE 512
2809
2810 void cell_renderer_height_override(GtkCellRenderer *renderer)
2811 {
2812         GParamSpec *spec;
2813
2814         spec = g_object_class_find_property(G_OBJECT_GET_CLASS(G_OBJECT(renderer)), "height");
2815         if (spec && G_IS_PARAM_SPEC_INT(spec))
2816                 {
2817                 GParamSpecInt *spec_int;
2818
2819                 spec_int = G_PARAM_SPEC_INT(spec);
2820                 if (spec_int->maximum < CELL_HEIGHT_OVERRIDE) spec_int->maximum = CELL_HEIGHT_OVERRIDE;
2821                 }
2822 }
2823
2824 static GdkColor *dupe_listview_color_shifted(GtkWidget *widget)
2825 {
2826         static GdkColor color;
2827         static GtkWidget *done = NULL;
2828
2829         if (done != widget)
2830                 {
2831                 GtkStyle *style;
2832
2833                 style = gtk_widget_get_style(widget);
2834                 memcpy(&color, &style->base[GTK_STATE_NORMAL], sizeof(color));
2835                 shift_color(&color, -1, 0);
2836                 done = widget;
2837                 }
2838
2839         return &color;
2840 }
2841
2842 static void dupe_listview_color_cb(GtkTreeViewColumn *tree_column, GtkCellRenderer *cell,
2843                                    GtkTreeModel *tree_model, GtkTreeIter *iter, gpointer data)
2844 {
2845         DupeWindow *dw = data;
2846         gboolean set;
2847
2848         gtk_tree_model_get(tree_model, iter, DUPE_COLUMN_COLOR, &set, -1);
2849         g_object_set(G_OBJECT(cell),
2850                      "cell-background-gdk", dupe_listview_color_shifted(dw->listview),
2851                      "cell-background-set", set, NULL);
2852 }
2853
2854 static void dupe_listview_add_column(DupeWindow *dw, GtkWidget *listview, gint n, const gchar *title, gboolean image, gboolean right_justify)
2855 {
2856         GtkTreeViewColumn *column;
2857         GtkCellRenderer *renderer;
2858
2859         column = gtk_tree_view_column_new();
2860         gtk_tree_view_column_set_title(column, title);
2861         gtk_tree_view_column_set_min_width(column, 4);
2862
2863         if (n != DUPE_COLUMN_RANK &&
2864             n != DUPE_COLUMN_THUMB)
2865                 {
2866                 gtk_tree_view_column_set_resizable(column, TRUE);
2867                 }
2868
2869         if (!image)
2870                 {
2871                 gtk_tree_view_column_set_sizing(column, GTK_TREE_VIEW_COLUMN_GROW_ONLY);
2872                 renderer = gtk_cell_renderer_text_new();
2873                 if (right_justify)
2874                         {
2875                         g_object_set(G_OBJECT(renderer), "xalign", 1.0, NULL);
2876                         }
2877                 gtk_tree_view_column_pack_start(column, renderer, TRUE);
2878                 gtk_tree_view_column_add_attribute(column, renderer, "text", n);
2879                 }
2880         else
2881                 {
2882                 gtk_tree_view_column_set_sizing(column, GTK_TREE_VIEW_COLUMN_FIXED);
2883                 renderer = gtk_cell_renderer_pixbuf_new();
2884                 cell_renderer_height_override(renderer);
2885                 gtk_tree_view_column_pack_start(column, renderer, TRUE);
2886                 gtk_tree_view_column_add_attribute(column, renderer, "pixbuf", n);
2887                 }
2888
2889         if (listview == dw->listview)
2890                 {
2891                 /* sets background before rendering */
2892                 gtk_tree_view_column_set_cell_data_func(column, renderer, dupe_listview_color_cb, dw, NULL);
2893                 }
2894
2895         gtk_tree_view_append_column(GTK_TREE_VIEW(listview), column);
2896 }
2897
2898 static void dupe_listview_set_height(GtkWidget *listview, gboolean thumb)
2899 {
2900         GtkTreeViewColumn *column;
2901         GtkCellRenderer *cell;
2902         GList *list;
2903
2904         column = gtk_tree_view_get_column(GTK_TREE_VIEW(listview), DUPE_COLUMN_THUMB - 1);
2905         if (!column) return;
2906
2907         gtk_tree_view_column_set_fixed_width(column, (thumb) ? options->thumbnails.max_width : 4);
2908
2909         list = gtk_cell_layout_get_cells(GTK_CELL_LAYOUT(column));
2910         if (!list) return;
2911         cell = list->data;
2912         g_list_free(list);
2913
2914         g_object_set(G_OBJECT(cell), "height", (thumb) ? options->thumbnails.max_height : -1, NULL);
2915         gtk_tree_view_columns_autosize(GTK_TREE_VIEW(listview));
2916 }
2917
2918
2919 /*
2920  *-------------------------------------------------------------------
2921  * misc cb
2922  *-------------------------------------------------------------------
2923  */
2924
2925 static void dupe_window_show_thumb_cb(GtkWidget *widget, gpointer data)
2926 {
2927         DupeWindow *dw = data;
2928
2929         dw->show_thumbs = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget));
2930         options->duplicates_thumbnails = dw->show_thumbs;
2931
2932         if (dw->show_thumbs)
2933                 {
2934                 if (!dw->working) dupe_thumb_step(dw);
2935                 }
2936         else
2937                 {
2938                 GtkTreeModel *store;
2939                 GtkTreeIter iter;
2940                 gboolean valid;
2941
2942                 thumb_loader_free(dw->thumb_loader);
2943                 dw->thumb_loader = NULL;
2944
2945                 store = gtk_tree_view_get_model(GTK_TREE_VIEW(dw->listview));
2946                 valid = gtk_tree_model_get_iter_first(store, &iter);
2947
2948                 while (valid)
2949                         {
2950                         gtk_list_store_set(GTK_LIST_STORE(store), &iter, DUPE_COLUMN_THUMB, NULL, -1);
2951                         valid = gtk_tree_model_iter_next(store, &iter);
2952                         }
2953                 dupe_window_update_progress(dw, NULL, 0.0, FALSE);
2954                 }
2955
2956         dupe_listview_set_height(dw->listview, dw->show_thumbs);
2957 }
2958
2959 static void dupe_window_rotation_invariant_cb(GtkWidget *widget, gpointer data)
2960 {
2961         DupeWindow *dw = data;
2962
2963         options->rot_invariant_sim = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget));
2964         dupe_window_recompare(dw);
2965 }
2966
2967 static void dupe_window_custom_threshold_cb(GtkWidget *widget, gpointer data)
2968 {
2969         DupeWindow *dw = data;
2970         DupeMatchType match_type;
2971         GtkTreeModel *store;
2972         gboolean valid;
2973         GtkTreeIter iter;
2974
2975         options->duplicates_similarity_threshold = gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(widget));
2976         dw->match_mask = DUPE_MATCH_SIM_CUSTOM;
2977
2978         store = gtk_combo_box_get_model(GTK_COMBO_BOX(dw->combo));
2979         valid = gtk_tree_model_get_iter_first(store, &iter);
2980         while (valid)
2981                 {
2982                 gtk_tree_model_get(store, &iter, DUPE_MENU_COLUMN_MASK, &match_type, -1);
2983                 if (match_type == DUPE_MATCH_SIM_CUSTOM)
2984                         {
2985                         break;
2986                         }
2987                 valid = gtk_tree_model_iter_next(store, &iter);
2988                 }
2989
2990         gtk_combo_box_set_active_iter(GTK_COMBO_BOX(dw->combo), &iter);
2991         dupe_window_recompare(dw);
2992 }
2993
2994 static void dupe_popup_menu_pos_cb(GtkMenu *menu, gint *x, gint *y, gboolean *push_in, gpointer data)
2995 {
2996         GtkWidget *view = data;
2997         GtkTreePath *tpath;
2998         gint cx, cy, cw, ch;
2999         gint column;
3000
3001         gtk_tree_view_get_cursor(GTK_TREE_VIEW(view), &tpath, NULL);
3002         if (!tpath) return;
3003
3004         if (gtk_tree_view_get_column(GTK_TREE_VIEW(view), DUPE_COLUMN_NAME - 1) != NULL)
3005                 {
3006                 column = DUPE_COLUMN_NAME - 1;
3007                 }
3008         else
3009                 {
3010                 /* dw->second_listview */
3011                 column = 0;
3012                 }
3013         tree_view_get_cell_clamped(GTK_TREE_VIEW(view), tpath, column, TRUE, &cx, &cy, &cw, &ch);
3014         gtk_tree_path_free(tpath);
3015         cy += ch;
3016         popup_menu_position_clamp(menu, &cx, &cy, 0);
3017         *x = cx;
3018         *y = cy;
3019 }
3020
3021 static gboolean dupe_window_keypress_cb(GtkWidget *widget, GdkEventKey *event, gpointer data)
3022 {
3023         DupeWindow *dw = data;
3024         gboolean stop_signal = FALSE;
3025         gboolean on_second;
3026         GtkWidget *listview;
3027         GtkTreeModel *store;
3028         GtkTreeSelection *selection;
3029         GList *slist;
3030         DupeItem *di = NULL;
3031
3032         on_second = gtk_widget_has_focus(dw->second_listview);
3033
3034         if (on_second)
3035                 {
3036                 listview = dw->second_listview;
3037                 }
3038         else
3039                 {
3040                 listview = dw->listview;
3041                 }
3042
3043         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(listview));
3044         slist = gtk_tree_selection_get_selected_rows(selection, &store);
3045         if (slist)
3046                 {
3047                 GtkTreePath *tpath;
3048                 GtkTreeIter iter;
3049                 GList *last;
3050
3051                 last = g_list_last(slist);
3052                 tpath = last->data;
3053
3054                 /* last is newest selected file */
3055                 gtk_tree_model_get_iter(store, &iter, tpath);
3056                 gtk_tree_model_get(store, &iter, DUPE_COLUMN_POINTER, &di, -1);
3057                 }
3058         g_list_foreach(slist, (GFunc)gtk_tree_path_free, NULL);
3059         g_list_free(slist);
3060
3061         if (event->state & GDK_CONTROL_MASK)
3062                 {
3063                 if (!on_second)
3064                         {
3065                         stop_signal = TRUE;
3066                         switch (event->keyval)
3067                                 {
3068                                 case '1':
3069                                 case '2':
3070                                 case '3':
3071                                 case '4':
3072                                 case '5':
3073                                 case '6':
3074                                 case '7':
3075                                 case '8':
3076                                 case '9':
3077                                 case '0':
3078                                         break;
3079                                 case 'C': case 'c':
3080                                         file_util_copy(NULL, dupe_listview_get_selection(dw, listview),
3081                                                        NULL, dw->window);
3082                                         break;
3083                                 case 'M': case 'm':
3084                                         file_util_move(NULL, dupe_listview_get_selection(dw, listview),
3085                                                        NULL, dw->window);
3086                                         break;
3087                                 case 'R': case 'r':
3088                                         file_util_rename(NULL, dupe_listview_get_selection(dw, listview), dw->window);
3089                                         break;
3090                                 case 'D': case 'd':
3091                                         options->file_ops.safe_delete_enable = TRUE;
3092                                         file_util_delete(NULL, dupe_listview_get_selection(dw, listview), dw->window);
3093                                         break;
3094                                 default:
3095                                         stop_signal = FALSE;
3096                                         break;
3097                                 }
3098                         }
3099
3100                 if (!stop_signal)
3101                         {
3102                         stop_signal = TRUE;
3103                         switch (event->keyval)
3104                                 {
3105                                 case 'A': case 'a':
3106                                         if (event->state & GDK_SHIFT_MASK)
3107                                                 {
3108                                                 gtk_tree_selection_unselect_all(selection);
3109                                                 }
3110                                         else
3111                                                 {
3112                                                 gtk_tree_selection_select_all(selection);
3113                                                 }
3114                                         break;
3115                                 case GDK_KEY_Delete: case GDK_KEY_KP_Delete:
3116                                         if (on_second)
3117                                                 {
3118                                                 dupe_second_clear(dw);
3119                                                 dupe_window_recompare(dw);
3120                                                 }
3121                                         else
3122                                                 {
3123                                                 dupe_window_clear(dw);
3124                                                 }
3125                                         break;
3126                                 case 'L': case 'l':
3127                                         dupe_window_append_file_list(dw, FALSE);
3128                                         break;
3129                                 case 'T': case 't':
3130                                         gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(dw->button_thumbs),
3131                                                 !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(dw->button_thumbs)));
3132                                         break;
3133                                 case 'W': case 'w':
3134                                         dupe_window_close(dw);
3135                                         break;
3136                                 default:
3137                                         stop_signal = FALSE;
3138                                         break;
3139                                 }
3140                         }
3141                 }
3142         else
3143                 {
3144                 stop_signal = TRUE;
3145                 switch (event->keyval)
3146                         {
3147                         case GDK_KEY_Return: case GDK_KEY_KP_Enter:
3148                                 dupe_menu_view(dw, di, listview, FALSE);
3149                                 break;
3150                         case 'V': case 'v':
3151                                 dupe_menu_view(dw, di, listview, TRUE);
3152                                 break;
3153                         case GDK_KEY_Delete: case GDK_KEY_KP_Delete:
3154                                 dupe_window_remove_selection(dw, listview);
3155                                 break;
3156                         case 'C': case 'c':
3157                                 if (!on_second)
3158                                         {
3159                                         dupe_window_collection_from_selection(dw);
3160                                         }
3161                                 break;
3162                         case '1':
3163                                 options->duplicates_select_type = DUPE_SELECT_GROUP1;
3164                                 dupe_listview_select_dupes(dw, DUPE_SELECT_GROUP1);
3165                                 break;
3166                         case '2':
3167                                 options->duplicates_select_type = DUPE_SELECT_GROUP2;
3168                                 dupe_listview_select_dupes(dw, DUPE_SELECT_GROUP2);
3169                                 break;
3170                         case GDK_KEY_Menu:
3171                         case GDK_KEY_F10:
3172                                 if (!on_second)
3173                                         {
3174                                         GtkWidget *menu;
3175
3176                                         menu = dupe_menu_popup_main(dw, di);
3177                                         gtk_menu_popup(GTK_MENU(menu), NULL, NULL,
3178                                                        dupe_popup_menu_pos_cb, listview, 0, GDK_CURRENT_TIME);
3179                                         }
3180                                 else
3181                                         {
3182                                         GtkWidget *menu;
3183
3184                                         menu = dupe_menu_popup_second(dw, di);
3185                                         gtk_menu_popup(GTK_MENU(menu), NULL, NULL,
3186                                                        dupe_popup_menu_pos_cb, listview, 0, GDK_CURRENT_TIME);
3187                                         }
3188                                 break;
3189                         default:
3190                                 stop_signal = FALSE;
3191                                 break;
3192                         }
3193                 }
3194         if (!stop_signal && is_help_key(event))
3195                 {
3196                 help_window_show("GuideImageSearchFindingDuplicates.html");
3197                 stop_signal = TRUE;
3198                 }
3199
3200         return stop_signal;
3201 }
3202
3203
3204 void dupe_window_clear(DupeWindow *dw)
3205 {
3206         GtkListStore *store;
3207
3208         dupe_check_stop(dw);
3209
3210         store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->listview)));
3211         gtk_list_store_clear(store);
3212         gtk_tree_view_columns_autosize(GTK_TREE_VIEW(dw->listview));
3213
3214         g_list_free(dw->dupes);
3215         dw->dupes = NULL;
3216
3217         dupe_list_free(dw->list);
3218         dw->list = NULL;
3219
3220         dupe_match_reset_list(dw->second_list);
3221
3222         dupe_window_update_count(dw, FALSE);
3223         dupe_window_update_progress(dw, NULL, 0.0, FALSE);
3224 }
3225
3226 void dupe_window_close(DupeWindow *dw)
3227 {
3228         dupe_check_stop(dw);
3229
3230         dupe_window_list = g_list_remove(dupe_window_list, dw);
3231         gtk_widget_destroy(dw->window);
3232
3233         g_list_free(dw->dupes);
3234         dupe_list_free(dw->list);
3235
3236         dupe_list_free(dw->second_list);
3237
3238         file_data_unregister_notify_func(dupe_notify_cb, dw);
3239
3240         g_free(dw);
3241 }
3242
3243 static gint dupe_window_delete(GtkWidget *widget, GdkEvent *event, gpointer data)
3244 {
3245         DupeWindow *dw = data;
3246         dupe_window_close(dw);
3247
3248         return TRUE;
3249 }
3250
3251 /* collection and files can be NULL */
3252 DupeWindow *dupe_window_new()
3253 {
3254         DupeWindow *dw;
3255         GtkWidget *vbox;
3256         GtkWidget *scrolled;
3257         GtkWidget *frame;
3258         GtkWidget *status_box;
3259         GtkWidget *label;
3260         GtkWidget *button;
3261         GtkListStore *store;
3262         GtkTreeSelection *selection;
3263         GdkGeometry geometry;
3264
3265         dw = g_new0(DupeWindow, 1);
3266
3267         dw->match_mask = DUPE_MATCH_NAME;
3268         if (options->duplicates_match == DUPE_MATCH_NAME) dw->match_mask = DUPE_MATCH_NAME;
3269         if (options->duplicates_match == DUPE_MATCH_SIZE) dw->match_mask = DUPE_MATCH_SIZE;
3270         if (options->duplicates_match == DUPE_MATCH_DATE) dw->match_mask = DUPE_MATCH_DATE;
3271         if (options->duplicates_match == DUPE_MATCH_DIM) dw->match_mask = DUPE_MATCH_DIM;
3272         if (options->duplicates_match == DUPE_MATCH_SUM) dw->match_mask = DUPE_MATCH_SUM;
3273         if (options->duplicates_match == DUPE_MATCH_PATH) dw->match_mask = DUPE_MATCH_PATH;
3274         if (options->duplicates_match == DUPE_MATCH_SIM_HIGH) dw->match_mask = DUPE_MATCH_SIM_HIGH;
3275         if (options->duplicates_match == DUPE_MATCH_SIM_MED) dw->match_mask = DUPE_MATCH_SIM_MED;
3276         if (options->duplicates_match == DUPE_MATCH_SIM_LOW) dw->match_mask = DUPE_MATCH_SIM_LOW;
3277         if (options->duplicates_match == DUPE_MATCH_SIM_CUSTOM) dw->match_mask = DUPE_MATCH_SIM_CUSTOM;
3278         if (options->duplicates_match == DUPE_MATCH_NAME_CI) dw->match_mask = DUPE_MATCH_NAME_CI;
3279
3280         dw->window = window_new(GTK_WINDOW_TOPLEVEL, "dupe", NULL, NULL, _("Find duplicates"));
3281
3282         geometry.min_width = DEFAULT_MINIMAL_WINDOW_SIZE;
3283         geometry.min_height = DEFAULT_MINIMAL_WINDOW_SIZE;
3284         geometry.base_width = DUPE_DEF_WIDTH;
3285         geometry.base_height = DUPE_DEF_HEIGHT;
3286         gtk_window_set_geometry_hints(GTK_WINDOW(dw->window), NULL, &geometry,
3287                                       GDK_HINT_MIN_SIZE | GDK_HINT_BASE_SIZE);
3288
3289         gtk_window_set_default_size(GTK_WINDOW(dw->window), DUPE_DEF_WIDTH, DUPE_DEF_HEIGHT);
3290
3291         gtk_window_set_resizable(GTK_WINDOW(dw->window), TRUE);
3292         gtk_container_set_border_width(GTK_CONTAINER(dw->window), 0);
3293
3294         g_signal_connect(G_OBJECT(dw->window), "delete_event",
3295                          G_CALLBACK(dupe_window_delete), dw);
3296         g_signal_connect(G_OBJECT(dw->window), "key_press_event",
3297                          G_CALLBACK(dupe_window_keypress_cb), dw);
3298
3299         vbox = gtk_vbox_new(FALSE, 0);
3300         gtk_container_add(GTK_CONTAINER(dw->window), vbox);
3301         gtk_widget_show(vbox);
3302
3303         dw->table = gtk_table_new(1, 3, FALSE);
3304         gtk_box_pack_start(GTK_BOX(vbox), dw->table, TRUE, TRUE, 0);
3305         gtk_widget_show(dw->table);
3306
3307         scrolled = gtk_scrolled_window_new(NULL, NULL);
3308         gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled), GTK_SHADOW_IN);
3309         gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
3310         gtk_table_attach_defaults(GTK_TABLE(dw->table), scrolled, 0, 2, 0, 1);
3311         gtk_widget_show(scrolled);
3312
3313         store = gtk_list_store_new(9, G_TYPE_POINTER, G_TYPE_STRING, GDK_TYPE_PIXBUF,
3314                                    G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING,
3315                                    G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN);
3316         dw->listview = gtk_tree_view_new_with_model(GTK_TREE_MODEL(store));
3317         g_object_unref(store);
3318
3319         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(dw->listview));
3320         gtk_tree_selection_set_mode(GTK_TREE_SELECTION(selection), GTK_SELECTION_MULTIPLE);
3321         gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(dw->listview), TRUE);
3322         gtk_tree_view_set_enable_search(GTK_TREE_VIEW(dw->listview), FALSE);
3323
3324         dupe_listview_add_column(dw, dw->listview, DUPE_COLUMN_RANK, "", FALSE, TRUE);
3325         dupe_listview_add_column(dw, dw->listview, DUPE_COLUMN_THUMB, "", TRUE, FALSE);
3326         dupe_listview_add_column(dw, dw->listview, DUPE_COLUMN_NAME, _("Name"), FALSE, FALSE);
3327         dupe_listview_add_column(dw, dw->listview, DUPE_COLUMN_SIZE, _("Size"), FALSE, TRUE);
3328         dupe_listview_add_column(dw, dw->listview, DUPE_COLUMN_DATE, _("Date"), FALSE, TRUE);
3329         dupe_listview_add_column(dw, dw->listview, DUPE_COLUMN_DIMENSIONS, _("Dimensions"), FALSE, FALSE);
3330         dupe_listview_add_column(dw, dw->listview, DUPE_COLUMN_PATH, _("Path"), FALSE, FALSE);
3331
3332         gtk_container_add(GTK_CONTAINER(scrolled), dw->listview);
3333         gtk_widget_show(dw->listview);
3334
3335         dw->second_vbox = gtk_vbox_new(FALSE, 0);
3336         gtk_table_attach_defaults(GTK_TABLE(dw->table), dw->second_vbox, 2, 3, 0, 1);
3337         if (dw->second_set)
3338                 {
3339                 gtk_table_set_col_spacings(GTK_TABLE(dw->table), PREF_PAD_GAP);
3340                 gtk_widget_show(dw->second_vbox);
3341                 }
3342         else
3343                 {
3344                 gtk_table_set_col_spacings(GTK_TABLE(dw->table), 0);
3345                 }
3346
3347         scrolled = gtk_scrolled_window_new(NULL, NULL);
3348         gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled), GTK_SHADOW_IN);
3349         gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
3350         gtk_box_pack_start(GTK_BOX(dw->second_vbox), scrolled, TRUE, TRUE, 0);
3351         gtk_widget_show(scrolled);
3352
3353         store = gtk_list_store_new(2, G_TYPE_POINTER, G_TYPE_STRING);
3354         dw->second_listview = gtk_tree_view_new_with_model(GTK_TREE_MODEL(store));
3355
3356         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(dw->second_listview));
3357         gtk_tree_selection_set_mode(GTK_TREE_SELECTION(selection), GTK_SELECTION_MULTIPLE);
3358
3359         gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(dw->second_listview), TRUE);
3360         gtk_tree_view_set_enable_search(GTK_TREE_VIEW(dw->second_listview), FALSE);
3361
3362         dupe_listview_add_column(dw, dw->second_listview, 1, _("Compare to:"), FALSE, FALSE);
3363
3364         gtk_container_add(GTK_CONTAINER(scrolled), dw->second_listview);
3365         gtk_widget_show(dw->second_listview);
3366
3367         dw->second_status_label = gtk_label_new("");
3368         gtk_box_pack_start(GTK_BOX(dw->second_vbox), dw->second_status_label, FALSE, FALSE, 0);
3369         gtk_widget_show(dw->second_status_label);
3370
3371         pref_line(dw->second_vbox, GTK_ORIENTATION_HORIZONTAL);
3372
3373         status_box = pref_box_new(vbox, FALSE, GTK_ORIENTATION_HORIZONTAL, 0);
3374
3375         label = gtk_label_new(_("Compare by:"));
3376         gtk_box_pack_start(GTK_BOX(status_box), label, FALSE, FALSE, PREF_PAD_SPACE);
3377         gtk_widget_show(label);
3378
3379         dupe_menu_setup(dw);
3380         gtk_box_pack_start(GTK_BOX(status_box), dw->combo, FALSE, FALSE, 0);
3381         gtk_widget_show(dw->combo);
3382
3383         dw->button_thumbs = gtk_check_button_new_with_label(_("Thumbnails"));
3384         dw->show_thumbs = options->duplicates_thumbnails;
3385         gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(dw->button_thumbs), dw->show_thumbs);
3386         g_signal_connect(G_OBJECT(dw->button_thumbs), "toggled",
3387                          G_CALLBACK(dupe_window_show_thumb_cb), dw);
3388         gtk_box_pack_start(GTK_BOX(status_box), dw->button_thumbs, FALSE, FALSE, PREF_PAD_SPACE);
3389         gtk_widget_show(dw->button_thumbs);
3390
3391         dw->button_rotation_invariant = gtk_check_button_new_with_label(_("Ignore Rotation"));
3392         gtk_widget_set_tooltip_text(GTK_WIDGET(dw->button_rotation_invariant), "Ignore image orientation");
3393         gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(dw->button_rotation_invariant), options->rot_invariant_sim);
3394         g_signal_connect(G_OBJECT(dw->button_rotation_invariant), "toggled",
3395                          G_CALLBACK(dupe_window_rotation_invariant_cb), dw);
3396         gtk_box_pack_start(GTK_BOX(status_box), dw->button_rotation_invariant, FALSE, FALSE, PREF_PAD_SPACE);
3397         gtk_widget_show(dw->button_rotation_invariant);
3398
3399         button = gtk_check_button_new_with_label(_("Compare two file sets"));
3400         gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(button), dw->second_set);
3401         g_signal_connect(G_OBJECT(button), "toggled",
3402                          G_CALLBACK(dupe_second_set_toggle_cb), dw);
3403         gtk_box_pack_end(GTK_BOX(status_box), button, FALSE, FALSE, PREF_PAD_SPACE);
3404         gtk_widget_show(button);
3405
3406         status_box = gtk_hbox_new(FALSE, 0);
3407         gtk_box_pack_start(GTK_BOX(vbox), status_box, FALSE, FALSE, 0);
3408         gtk_widget_show(status_box);
3409
3410         frame = gtk_frame_new(NULL);
3411         gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_IN);
3412         gtk_box_pack_start(GTK_BOX(status_box), frame, TRUE, TRUE, 0);
3413         gtk_widget_show(frame);
3414
3415         dw->status_label = gtk_label_new("");
3416         gtk_container_add(GTK_CONTAINER(frame), dw->status_label);
3417         gtk_widget_show(dw->status_label);
3418
3419         button = gtk_check_button_new_with_label(_("Sort"));
3420         gtk_widget_set_tooltip_text(GTK_WIDGET(button), "Sort by group totals");
3421         gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(button), options->sort_totals);
3422         g_signal_connect(G_OBJECT(button), "toggled",
3423                          G_CALLBACK(dupe_sort_totals_toggle_cb), dw);
3424         gtk_box_pack_start(GTK_BOX(status_box), button, FALSE, FALSE, PREF_PAD_SPACE);
3425         gtk_widget_show(button);
3426
3427         label = gtk_label_new(_("Custom Threshold"));
3428         gtk_box_pack_start(GTK_BOX(status_box), label, FALSE, FALSE, PREF_PAD_SPACE);
3429         gtk_widget_show(label);
3430         dw->custom_threshold = gtk_spin_button_new_with_range(1, 100, 1);
3431         gtk_widget_set_tooltip_text(GTK_WIDGET(dw->custom_threshold), "Custom similarity threshold");
3432         gtk_spin_button_set_value(GTK_SPIN_BUTTON(dw->custom_threshold), options->duplicates_similarity_threshold);
3433         g_signal_connect(G_OBJECT(dw->custom_threshold), "value_changed",
3434                                                                                                         G_CALLBACK(dupe_window_custom_threshold_cb), dw);
3435         gtk_box_pack_start(GTK_BOX(status_box), dw->custom_threshold, FALSE, FALSE, PREF_PAD_SPACE);
3436         gtk_widget_show(dw->custom_threshold);
3437
3438         dw->extra_label = gtk_progress_bar_new();
3439         gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(dw->extra_label), 0.0);
3440 #if GTK_CHECK_VERSION(3,0,0)
3441         gtk_progress_bar_set_text(GTK_PROGRESS_BAR(dw->extra_label), "");
3442         gtk_progress_bar_set_show_text(GTK_PROGRESS_BAR(dw->extra_label), TRUE);
3443 #endif
3444         gtk_box_pack_end(GTK_BOX(status_box), dw->extra_label, FALSE, FALSE, 0);
3445         gtk_widget_show(dw->extra_label);
3446
3447         dupe_dnd_init(dw);
3448
3449         /* order is important here, dnd_init should be seeing mouse
3450          * presses before we possibly handle (and stop) the signal
3451          */
3452         g_signal_connect(G_OBJECT(dw->listview), "button_press_event",
3453                          G_CALLBACK(dupe_listview_press_cb), dw);
3454         g_signal_connect(G_OBJECT(dw->listview), "button_release_event",
3455                          G_CALLBACK(dupe_listview_release_cb), dw);
3456         g_signal_connect(G_OBJECT(dw->second_listview), "button_press_event",
3457                          G_CALLBACK(dupe_listview_press_cb), dw);
3458         g_signal_connect(G_OBJECT(dw->second_listview), "button_release_event",
3459                          G_CALLBACK(dupe_listview_release_cb), dw);
3460
3461         gtk_widget_show(dw->window);
3462
3463         dupe_window_update_count(dw, TRUE);
3464         dupe_window_update_progress(dw, NULL, 0.0, FALSE);
3465
3466         dupe_window_list = g_list_append(dupe_window_list, dw);
3467
3468         file_data_register_notify_func(dupe_notify_cb, dw, NOTIFY_PRIORITY_MEDIUM);
3469
3470         return dw;
3471 }
3472
3473 /*
3474  *-------------------------------------------------------------------
3475  * dnd confirm dir
3476  *-------------------------------------------------------------------
3477  */
3478
3479 typedef struct {
3480         DupeWindow *dw;
3481         GList *list;
3482 } CDupeConfirmD;
3483
3484 static void confirm_dir_list_cancel(GtkWidget *widget, gpointer data)
3485 {
3486         /* do nothing */
3487 }
3488
3489 static void confirm_dir_list_add(GtkWidget *widget, gpointer data)
3490 {
3491         CDupeConfirmD *d = data;
3492         GList *work;
3493
3494         dupe_window_add_files(d->dw, d->list, FALSE);
3495
3496         work = d->list;
3497         while (work)
3498                 {
3499                 FileData *fd = work->data;
3500                 work = work->next;
3501                 if (isdir(fd->path))
3502                         {
3503                         GList *list;
3504
3505                         filelist_read(fd, &list, NULL);
3506                         list = filelist_filter(list, FALSE);
3507                         if (list)
3508                                 {
3509                                 dupe_window_add_files(d->dw, list, FALSE);
3510                                 filelist_free(list);
3511                                 }
3512                         }
3513                 }
3514 }
3515
3516 static void confirm_dir_list_recurse(GtkWidget *widget, gpointer data)
3517 {
3518         CDupeConfirmD *d = data;
3519         dupe_window_add_files(d->dw, d->list, TRUE);
3520 }
3521
3522 static void confirm_dir_list_skip(GtkWidget *widget, gpointer data)
3523 {
3524         CDupeConfirmD *d = data;
3525         dupe_window_add_files(d->dw, d->list, FALSE);
3526 }
3527
3528 static void confirm_dir_list_destroy(GtkWidget *widget, gpointer data)
3529 {
3530         CDupeConfirmD *d = data;
3531         filelist_free(d->list);
3532         g_free(d);
3533 }
3534
3535 static GtkWidget *dupe_confirm_dir_list(DupeWindow *dw, GList *list)
3536 {
3537         GtkWidget *menu;
3538         CDupeConfirmD *d;
3539
3540         d = g_new0(CDupeConfirmD, 1);
3541         d->dw = dw;
3542         d->list = list;
3543
3544         menu = popup_menu_short_lived();
3545         g_signal_connect(G_OBJECT(menu), "destroy",
3546                          G_CALLBACK(confirm_dir_list_destroy), d);
3547
3548         menu_item_add_stock(menu, _("Dropped list includes folders."), GTK_STOCK_DND_MULTIPLE, NULL, NULL);
3549         menu_item_add_divider(menu);
3550         menu_item_add_stock(menu, _("_Add contents"), GTK_STOCK_OK, G_CALLBACK(confirm_dir_list_add), d);
3551         menu_item_add_stock(menu, _("Add contents _recursive"), GTK_STOCK_ADD, G_CALLBACK(confirm_dir_list_recurse), d);
3552         menu_item_add_stock(menu, _("_Skip folders"), GTK_STOCK_REMOVE, G_CALLBACK(confirm_dir_list_skip), d);
3553         menu_item_add_divider(menu);
3554         menu_item_add_stock(menu, _("Cancel"), GTK_STOCK_CANCEL, G_CALLBACK(confirm_dir_list_cancel), d);
3555
3556         return menu;
3557 }
3558
3559 /*
3560  *-------------------------------------------------------------------
3561  * dnd
3562  *-------------------------------------------------------------------
3563  */
3564
3565 static GtkTargetEntry dupe_drag_types[] = {
3566         { "text/uri-list", 0, TARGET_URI_LIST },
3567         { "text/plain", 0, TARGET_TEXT_PLAIN }
3568 };
3569 static gint n_dupe_drag_types = 2;
3570
3571 static GtkTargetEntry dupe_drop_types[] = {
3572         { TARGET_APP_COLLECTION_MEMBER_STRING, 0, TARGET_APP_COLLECTION_MEMBER },
3573         { "text/uri-list", 0, TARGET_URI_LIST }
3574 };
3575 static gint n_dupe_drop_types = 2;
3576
3577 static void dupe_dnd_data_set(GtkWidget *widget, GdkDragContext *context,
3578                               GtkSelectionData *selection_data, guint info,
3579                               guint time, gpointer data)
3580 {
3581         DupeWindow *dw = data;
3582         GList *list;
3583
3584         switch (info)
3585                 {
3586                 case TARGET_URI_LIST:
3587                 case TARGET_TEXT_PLAIN:
3588                         list = dupe_listview_get_selection(dw, widget);
3589                         if (!list) return;
3590                         uri_selection_data_set_uris_from_filelist(selection_data, list);
3591                         filelist_free(list);
3592                         break;
3593                 default:
3594                         break;
3595                 }
3596 }
3597
3598 static void dupe_dnd_data_get(GtkWidget *widget, GdkDragContext *context,
3599                               gint x, gint y,
3600                               GtkSelectionData *selection_data, guint info,
3601                               guint time, gpointer data)
3602 {
3603         DupeWindow *dw = data;
3604         GtkWidget *source;
3605         GList *list = NULL;
3606         GList *work;
3607
3608         source = gtk_drag_get_source_widget(context);
3609         if (source == dw->listview || source == dw->second_listview) return;
3610
3611         dw->second_drop = (dw->second_set && widget == dw->second_listview);
3612
3613         switch (info)
3614                 {
3615                 case TARGET_APP_COLLECTION_MEMBER:
3616                         collection_from_dnd_data((gchar *)gtk_selection_data_get_data(selection_data), &list, NULL);
3617                         break;
3618                 case TARGET_URI_LIST:
3619                         list = uri_filelist_from_gtk_selection_data(selection_data);
3620                         work = list;
3621                         while (work)
3622                                 {
3623                                 FileData *fd = work->data;
3624                                 if (isdir(fd->path))
3625                                         {
3626                                         GtkWidget *menu;
3627                                         menu = dupe_confirm_dir_list(dw, list);
3628                                         gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL, 0, time);
3629                                         return;
3630                                         }
3631                                 work = work->next;
3632                                 }
3633                         break;
3634                 default:
3635                         list = NULL;
3636                         break;
3637                 }
3638
3639         if (list)
3640                 {
3641                 dupe_window_add_files(dw, list, FALSE);
3642                 filelist_free(list);
3643                 }
3644 }
3645
3646 static void dupe_dest_set(GtkWidget *widget, gboolean enable)
3647 {
3648         if (enable)
3649                 {
3650                 gtk_drag_dest_set(widget,
3651                         GTK_DEST_DEFAULT_MOTION | GTK_DEST_DEFAULT_HIGHLIGHT | GTK_DEST_DEFAULT_DROP,
3652                         dupe_drop_types, n_dupe_drop_types,
3653                         GDK_ACTION_COPY | GDK_ACTION_MOVE | GDK_ACTION_ASK);
3654
3655                 }
3656         else
3657                 {
3658                 gtk_drag_dest_unset(widget);
3659                 }
3660 }
3661
3662 static void dupe_dnd_begin(GtkWidget *widget, GdkDragContext *context, gpointer data)
3663 {
3664         DupeWindow *dw = data;
3665         dupe_dest_set(dw->listview, FALSE);
3666         dupe_dest_set(dw->second_listview, FALSE);
3667
3668         if (dw->click_item && !dupe_listview_item_is_selected(dw, dw->click_item, widget))
3669                 {
3670                 GtkListStore *store;
3671                 GtkTreeIter iter;
3672
3673                 store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(widget)));
3674                 if (dupe_listview_find_item(store, dw->click_item, &iter) >= 0)
3675                         {
3676                         GtkTreeSelection *selection;
3677                         GtkTreePath *tpath;
3678
3679                         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(widget));
3680                         gtk_tree_selection_unselect_all(selection);
3681                         gtk_tree_selection_select_iter(selection, &iter);
3682
3683                         tpath = gtk_tree_model_get_path(GTK_TREE_MODEL(store), &iter);
3684                         gtk_tree_view_set_cursor(GTK_TREE_VIEW(widget), tpath, NULL, FALSE);
3685                         gtk_tree_path_free(tpath);
3686                         }
3687                 }
3688
3689         if (dw->show_thumbs &&
3690             widget == dw->listview &&
3691             dw->click_item && dw->click_item->pixbuf)
3692                 {
3693                 GtkTreeSelection *selection;
3694                 gint items;
3695
3696                 selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(widget));
3697                 items = gtk_tree_selection_count_selected_rows(selection);
3698                 dnd_set_drag_icon(widget, context, dw->click_item->pixbuf, items);
3699                 }
3700 }
3701
3702 static void dupe_dnd_end(GtkWidget *widget, GdkDragContext *context, gpointer data)
3703 {
3704         DupeWindow *dw = data;
3705         dupe_dest_set(dw->listview, TRUE);
3706         dupe_dest_set(dw->second_listview, TRUE);
3707 }
3708
3709 static void dupe_dnd_init(DupeWindow *dw)
3710 {
3711         gtk_drag_source_set(dw->listview, GDK_BUTTON1_MASK | GDK_BUTTON2_MASK,
3712                             dupe_drag_types, n_dupe_drag_types,
3713                             GDK_ACTION_COPY | GDK_ACTION_MOVE | GDK_ACTION_LINK);
3714         g_signal_connect(G_OBJECT(dw->listview), "drag_data_get",
3715                          G_CALLBACK(dupe_dnd_data_set), dw);
3716         g_signal_connect(G_OBJECT(dw->listview), "drag_begin",
3717                          G_CALLBACK(dupe_dnd_begin), dw);
3718         g_signal_connect(G_OBJECT(dw->listview), "drag_end",
3719                          G_CALLBACK(dupe_dnd_end), dw);
3720
3721         dupe_dest_set(dw->listview, TRUE);
3722         g_signal_connect(G_OBJECT(dw->listview), "drag_data_received",
3723                          G_CALLBACK(dupe_dnd_data_get), dw);
3724
3725         gtk_drag_source_set(dw->second_listview, GDK_BUTTON1_MASK | GDK_BUTTON2_MASK,
3726                             dupe_drag_types, n_dupe_drag_types,
3727                             GDK_ACTION_COPY | GDK_ACTION_MOVE | GDK_ACTION_LINK);
3728         g_signal_connect(G_OBJECT(dw->second_listview), "drag_data_get",
3729                          G_CALLBACK(dupe_dnd_data_set), dw);
3730         g_signal_connect(G_OBJECT(dw->second_listview), "drag_begin",
3731                          G_CALLBACK(dupe_dnd_begin), dw);
3732         g_signal_connect(G_OBJECT(dw->second_listview), "drag_end",
3733                          G_CALLBACK(dupe_dnd_end), dw);
3734
3735         dupe_dest_set(dw->second_listview, TRUE);
3736         g_signal_connect(G_OBJECT(dw->second_listview), "drag_data_received",
3737                          G_CALLBACK(dupe_dnd_data_get), dw);
3738 }
3739
3740 /*
3741  *-------------------------------------------------------------------
3742  * maintenance (move, delete, etc.)
3743  *-------------------------------------------------------------------
3744  */
3745
3746 static void dupe_notify_cb(FileData *fd, NotifyType type, gpointer data)
3747 {
3748         DupeWindow *dw = data;
3749
3750         if (!(type & NOTIFY_CHANGE) || !fd->change) return;
3751
3752         DEBUG_1("Notify dupe: %s %04x", fd->path, type);
3753
3754         switch (fd->change->type)
3755                 {
3756                 case FILEDATA_CHANGE_MOVE:
3757                 case FILEDATA_CHANGE_RENAME:
3758                         dupe_item_update_fd(dw, fd);
3759                         break;
3760                 case FILEDATA_CHANGE_COPY:
3761                         break;
3762                 case FILEDATA_CHANGE_DELETE:
3763                         while (dupe_item_remove_by_path(dw, fd->path));
3764                         break;
3765                 case FILEDATA_CHANGE_UNSPECIFIED:
3766                 case FILEDATA_CHANGE_WRITE_METADATA:
3767                         break;
3768                 }
3769
3770 }
3771 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */