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