Use GdkRectangle for LayoutOptions::log_window
[geeqie.git] / src / search.cc
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 "search.h"
24
25 #include "bar-keywords.h"
26 #include "cache.h"
27 #include "collect-table.h"
28 #include "dnd.h"
29 #include "dupe.h"
30 #include "editors.h"
31 #include "filedata.h"
32 #include "image-load.h"
33 #include "img-view.h"
34 #include "layout-util.h"
35 #include "menu.h"
36 #include "metadata.h"
37 #include "misc.h"
38 #include "pixbuf-util.h"
39 #include "print.h"
40 #include "thumb.h"
41 #include "ui-bookmark.h"
42 #include "ui-fileops.h"
43 #include "ui-menu.h"
44 #include "ui-misc.h"
45 #include "ui-tabcomp.h"
46 #include "ui-tree-edit.h"
47 #include "uri-utils.h"
48 #include "utilops.h"
49 #include "window.h"
50
51 #include <cmath>
52
53 #define DEF_SEARCH_WIDTH  700
54 #define DEF_SEARCH_HEIGHT 650
55
56 #define SEARCH_BUFFER_MATCH_LOAD 20
57 #define SEARCH_BUFFER_MATCH_HIT  5
58 #define SEARCH_BUFFER_MATCH_MISS 1
59 #define SEARCH_BUFFER_FLUSH_SIZE 99
60
61 #define FORMAT_CLASS_BROKEN static_cast<FileFormatClass>(FILE_FORMAT_CLASSES + 1)
62
63 enum MatchType {
64         SEARCH_MATCH_NONE,
65         SEARCH_MATCH_EQUAL,
66         SEARCH_MATCH_CONTAINS,
67         SEARCH_MATCH_NAME_EQUAL,
68         SEARCH_MATCH_NAME_CONTAINS,
69         SEARCH_MATCH_PATH_CONTAINS,
70         SEARCH_MATCH_UNDER,
71         SEARCH_MATCH_OVER,
72         SEARCH_MATCH_BETWEEN,
73         SEARCH_MATCH_ALL,
74         SEARCH_MATCH_ANY,
75         SEARCH_MATCH_COLLECTION
76 };
77
78 enum {
79         SEARCH_COLUMN_POINTER = 0,
80         SEARCH_COLUMN_RANK,
81         SEARCH_COLUMN_THUMB,
82         SEARCH_COLUMN_NAME,
83         SEARCH_COLUMN_SIZE,
84         SEARCH_COLUMN_DATE,
85         SEARCH_COLUMN_DIMENSIONS,
86         SEARCH_COLUMN_PATH,
87         SEARCH_COLUMN_COUNT     /* total columns */
88 };
89
90 struct SearchData
91 {
92         GtkWidget *window;
93
94         GtkWidget *button_thumbs;
95         GtkWidget *label_status;
96         GtkWidget *label_progress;
97         GtkWidget *button_start;
98         GtkWidget *button_stop;
99         GtkWidget *button_close;
100         GtkWidget *button_help;
101         GtkWidget *spinner;
102
103         GtkWidget *collection;
104         GtkWidget *fd_button;
105         GtkWidget *collection_entry;
106
107         GtkWidget *box_search;
108
109         GtkWidget *menu_path;
110         GtkWidget *path_entry;
111         GtkWidget *check_recurse;
112
113         GtkWidget *result_view;
114
115         GtkWidget *check_name;
116         GtkWidget *menu_name;
117         GtkWidget *entry_name;
118         GtkWidget *check_name_match_case;
119
120         GtkWidget *check_size;
121         GtkWidget *menu_size;
122         GtkWidget *spin_size;
123         GtkWidget *spin_size_end;
124
125         GtkWidget *check_date;
126         GtkWidget *menu_date;
127         GtkWidget *date_sel;
128         GtkWidget *date_sel_end;
129         GtkWidget *date_type;
130
131         GtkWidget *check_dimensions;
132         GtkWidget *menu_dimensions;
133         GtkWidget *spin_width;
134         GtkWidget *spin_height;
135         GtkWidget *spin_width_end;
136         GtkWidget *spin_height_end;
137
138         GtkWidget *check_similarity;
139         GtkWidget *spin_similarity;
140         GtkWidget *entry_similarity;
141
142         GtkWidget *check_keywords;
143         GtkWidget *menu_keywords;
144         GtkWidget *entry_keywords;
145
146         GtkWidget *check_comment;
147         GtkWidget *menu_comment;
148         GtkWidget *entry_comment;
149
150         GtkWidget *check_rating;
151         GtkWidget *menu_rating;
152         GtkWidget *spin_rating;
153         GtkWidget *spin_rating_end;
154
155         GtkWidget *check_class;
156         GtkWidget *menu_class;
157         GtkWidget *class_type;
158         GtkWidget *marks_type;
159         GtkWidget *menu_marks;
160
161         FileData *search_dir_fd;
162         gboolean   search_path_recurse;
163         gchar *search_name;
164         GRegex *search_name_regex;
165         gboolean   search_name_match_case;
166         gboolean   search_name_symbolic_link;
167         gint64 search_size;
168         gint64 search_size_end;
169         gint   search_date_y;
170         gint   search_date_m;
171         gint   search_date_d;
172         gint   search_date_end_y;
173         gint   search_date_end_m;
174         gint   search_date_end_d;
175         gint   search_width;
176         gint   search_height;
177         gint   search_width_end;
178         gint   search_height_end;
179         gint   search_similarity;
180         gchar *search_similarity_path;
181         CacheData *search_similarity_cd;
182         GList *search_keyword_list;
183         gchar *search_comment;
184         GRegex *search_comment_regex;
185         gint   search_rating;
186         gint   search_rating_end;
187         gboolean   search_comment_match_case;
188
189         MatchType search_type;
190
191         MatchType match_name;
192         MatchType match_size;
193         MatchType match_date;
194         MatchType match_dimensions;
195         MatchType match_keywords;
196         MatchType match_comment;
197         MatchType match_rating;
198         MatchType match_gps;
199         MatchType match_class;
200         MatchType match_marks;
201
202         gboolean match_name_enable;
203         gboolean match_size_enable;
204         gboolean match_date_enable;
205         gboolean match_dimensions_enable;
206         gboolean match_similarity_enable;
207         gboolean match_keywords_enable;
208         gboolean match_comment_enable;
209         gboolean match_rating_enable;
210         gboolean match_class_enable;
211         gboolean match_marks_enable;
212         gboolean match_broken_enable;
213
214         GList *search_folder_list;
215         GList *search_done_list;
216         GList *search_file_list;
217         GList *search_buffer_list;
218
219         gint search_count;
220         gint search_total;
221         gint search_buffer_count;
222
223         guint search_idle_id; /* event source id */
224         guint update_idle_id; /* event source id */
225
226         ImageLoader *img_loader;
227         CacheData   *img_cd;
228
229         FileData *click_fd;
230
231         ThumbLoader *thumb_loader;
232         gboolean thumb_enable;
233         FileData *thumb_fd;
234
235         /* Used for lat/long coordinate search
236         */
237         gint search_gps;
238         gdouble search_lat, search_lon;
239         GtkWidget *entry_gps_coord;
240         GtkWidget *check_gps;
241         GtkWidget *spin_gps;
242         GtkWidget *units_gps;
243         GtkWidget *menu_gps;
244         gboolean match_gps_enable;
245
246 };
247
248 struct MatchFileData
249 {
250         FileData *fd;
251         gint width;
252         gint height;
253         gint rank;
254 };
255
256 struct MatchList
257 {
258         const gchar *text;
259         MatchType type;
260 };
261
262 static const MatchList text_search_menu_path[] = {
263         { N_("folder"),         SEARCH_MATCH_NONE },
264         { N_("comments"),       SEARCH_MATCH_ALL },
265         { N_("results"),        SEARCH_MATCH_CONTAINS },
266         { N_("collection"),     SEARCH_MATCH_COLLECTION }
267 };
268
269 static const MatchList text_search_menu_name[] = {
270         { N_("name contains"),  SEARCH_MATCH_NAME_CONTAINS },
271         { N_("name is"),        SEARCH_MATCH_NAME_EQUAL },
272         { N_("path contains"),  SEARCH_MATCH_PATH_CONTAINS }
273 };
274
275 static const MatchList text_search_menu_size[] = {
276         { N_("equal to"),       SEARCH_MATCH_EQUAL },
277         { N_("less than"),      SEARCH_MATCH_UNDER },
278         { N_("greater than"),   SEARCH_MATCH_OVER },
279         { N_("between"),        SEARCH_MATCH_BETWEEN }
280 };
281
282 static const MatchList text_search_menu_date[] = {
283         { N_("equal to"),       SEARCH_MATCH_EQUAL },
284         { N_("before"),         SEARCH_MATCH_UNDER },
285         { N_("after"),          SEARCH_MATCH_OVER },
286         { N_("between"),        SEARCH_MATCH_BETWEEN }
287 };
288
289 static const MatchList text_search_menu_keyword[] = {
290         { N_("match all"),      SEARCH_MATCH_ALL },
291         { N_("match any"),      SEARCH_MATCH_ANY },
292         { N_("exclude"),        SEARCH_MATCH_NONE }
293 };
294
295 static const MatchList text_search_menu_comment[] = {
296         { N_("contains"),       SEARCH_MATCH_CONTAINS },
297         { N_("miss"),           SEARCH_MATCH_NONE }
298 };
299
300
301 static const MatchList text_search_menu_rating[] = {
302         { N_("equal to"),       SEARCH_MATCH_EQUAL },
303         { N_("less than"),      SEARCH_MATCH_UNDER },
304         { N_("greater than"),   SEARCH_MATCH_OVER },
305         { N_("between"),        SEARCH_MATCH_BETWEEN }
306 };
307
308 static const MatchList text_search_menu_gps[] = {
309         { N_("not geocoded"),   SEARCH_MATCH_NONE },
310         { N_("less than"),      SEARCH_MATCH_UNDER },
311         { N_("greater than"),   SEARCH_MATCH_OVER }
312 };
313
314 static const MatchList text_search_menu_class[] = {
315         { N_("is"),     SEARCH_MATCH_EQUAL },
316         { N_("is not"), SEARCH_MATCH_NONE }
317 };
318
319 static const MatchList text_search_menu_marks[] = {
320         { N_("is"),     SEARCH_MATCH_EQUAL },
321         { N_("is not"), SEARCH_MATCH_NONE }
322 };
323
324 static GList *search_window_list = nullptr;
325
326
327 static gint search_result_selection_count(SearchData *sd, gint64 *bytes);
328 static gint search_result_count(SearchData *sd, gint64 *bytes);
329
330 static void search_window_close(SearchData *sd);
331
332 static void search_notify_cb(FileData *fd, NotifyType type, gpointer data);
333 static void search_start_cb(GtkWidget *widget, gpointer data);
334 void mfd_list_free(GList *list);
335
336
337 /**
338  * This array must be kept in sync with the contents of:\n
339  * @link search_result_press_cb @endlink \n
340  * @link search_window_keypress_cb @endlink \n
341  * @link search_result_menu @endlink
342  *
343  * See also @link hard_coded_window_keys @endlink
344  **/
345
346 hard_coded_window_keys search_window_keys[] = {
347         {GDK_CONTROL_MASK, 'C', N_("Copy")},
348         {GDK_CONTROL_MASK, 'M', N_("Move")},
349         {GDK_CONTROL_MASK, 'R', N_("Rename")},
350         {GDK_CONTROL_MASK, 'D', N_("Move to Trash")},
351         {GDK_SHIFT_MASK, GDK_KEY_Delete, N_("Delete")},
352         {static_cast<GdkModifierType>(0), GDK_KEY_Delete, N_("Remove")},
353         {GDK_CONTROL_MASK, 'A', N_("Select all")},
354         {static_cast<GdkModifierType>(GDK_CONTROL_MASK + GDK_SHIFT_MASK), 'A', N_("Select none")},
355         {GDK_CONTROL_MASK, GDK_KEY_Delete, N_("Clear")},
356         {GDK_CONTROL_MASK, 'T', N_("Toggle thumbs")},
357         {GDK_CONTROL_MASK, 'W', N_("Close window")},
358         {static_cast<GdkModifierType>(0), GDK_KEY_Return, N_("View")},
359         {static_cast<GdkModifierType>(0), 'V', N_("View in new window")},
360         {static_cast<GdkModifierType>(0), 'C', N_("Collection from selection")},
361         {GDK_CONTROL_MASK, GDK_KEY_Return, N_("Start/stop search")},
362         {static_cast<GdkModifierType>(0), GDK_KEY_F3, N_("Find duplicates")},
363         {static_cast<GdkModifierType>(0), 0, nullptr}
364 };
365 /*
366  *-------------------------------------------------------------------
367  * utils
368  *-------------------------------------------------------------------
369  */
370
371 static time_t convert_dmy_to_time(gint day, gint month, gint year)
372 {
373         struct tm lt;
374
375         lt.tm_sec = 0;
376         lt.tm_min = 0;
377         lt.tm_hour = 0;
378         lt.tm_mday = day;
379         lt.tm_mon = month - 1;
380         lt.tm_year = year - 1900;
381         lt.tm_isdst = 0;
382
383         return mktime(&lt);
384 }
385
386 static void search_status_update(SearchData *sd)
387 {
388         gchar *buf;
389         gint t;
390         gint s;
391         gint64 t_bytes;
392         gint64 s_bytes;
393         gchar *tt;
394
395         t = search_result_count(sd, &t_bytes);
396         s = search_result_selection_count(sd, &s_bytes);
397
398         tt = text_from_size_abrev(t_bytes);
399
400         if (s > 0)
401                 {
402                 gchar *ts = text_from_size_abrev(s_bytes);
403                 buf = g_strdup_printf(_("%s, %d files (%s, %d)"), tt, t, ts, s);
404                 g_free(ts);
405                 }
406         else
407                 {
408                 buf = g_strdup_printf(_("%s, %d files"), tt, t);
409                 }
410
411         g_free(tt);
412
413         gtk_label_set_text(GTK_LABEL(sd->label_status), buf);
414         g_free(buf);
415 }
416
417 static void search_progress_update(SearchData *sd, gboolean search, gdouble thumbs)
418 {
419
420         if (search || thumbs >= 0.0)
421                 {
422                 gchar *buf;
423                 const gchar *message;
424
425                 if (search && (sd->search_folder_list || sd->search_file_list))
426                         message = _("Searching...");
427                 else if (thumbs >= 0.0)
428                         message = _("Loading thumbs...");
429                 else
430                         message = "";
431
432                 buf = g_strdup_printf("%s(%d / %d)", message, sd->search_count, sd->search_total);
433                 gtk_progress_bar_set_text(GTK_PROGRESS_BAR(sd->label_progress), buf);
434                 gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(sd->label_progress),
435                                               (thumbs >= 0.0) ? thumbs : 0.0);
436                 g_free(buf);
437                 }
438         else
439                 {
440                 gtk_progress_bar_set_text(GTK_PROGRESS_BAR(sd->label_progress), "");
441                 gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(sd->label_progress), 0.0);
442                 }
443 }
444
445 /*
446  *-------------------------------------------------------------------
447  * result list
448  *-------------------------------------------------------------------
449  */
450
451 static gint search_result_find_row(SearchData *sd, FileData *fd, GtkTreeIter *iter)
452 {
453         GtkTreeModel *store;
454         gboolean valid;
455         gint n = 0;
456
457         store = gtk_tree_view_get_model(GTK_TREE_VIEW(sd->result_view));
458         valid = gtk_tree_model_get_iter_first(store, iter);
459         while (valid)
460                 {
461                 MatchFileData *mfd;
462                 n++;
463
464                 gtk_tree_model_get(store, iter, SEARCH_COLUMN_POINTER, &mfd, -1);
465                 if (mfd->fd == fd) return n;
466                 valid = gtk_tree_model_iter_next(store, iter);
467                 }
468
469         return -1;
470 }
471
472
473 static gboolean search_result_row_selected(SearchData *sd, FileData *fd)
474 {
475         GtkTreeModel *store;
476         GtkTreeSelection *selection;
477         GList *slist;
478         GList *work;
479         gboolean found = FALSE;
480
481         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(sd->result_view));
482         slist = gtk_tree_selection_get_selected_rows(selection, &store);
483         work = slist;
484         while (!found && work)
485                 {
486                 auto tpath = static_cast<GtkTreePath *>(work->data);
487                 MatchFileData *mfd_n;
488                 GtkTreeIter iter;
489
490                 gtk_tree_model_get_iter(store, &iter, tpath);
491                 gtk_tree_model_get(store, &iter, SEARCH_COLUMN_POINTER, &mfd_n, -1);
492                 if (mfd_n->fd == fd) found = TRUE;
493                 work = work->next;
494                 }
495         g_list_free_full(slist, reinterpret_cast<GDestroyNotify>(gtk_tree_path_free));
496
497         return found;
498 }
499
500 static gint search_result_selection_util(SearchData *sd, gint64 *bytes, GList **list)
501 {
502         GtkTreeModel *store;
503         GtkTreeSelection *selection;
504         GList *slist;
505         GList *work;
506         gint n = 0;
507         gint64 total = 0;
508         GList *plist = nullptr;
509
510         store = gtk_tree_view_get_model(GTK_TREE_VIEW(sd->result_view));
511         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(sd->result_view));
512         slist = gtk_tree_selection_get_selected_rows(selection, &store);
513         work = slist;
514         while (work)
515                 {
516                 n++;
517
518                 if (bytes || list)
519                         {
520                         auto tpath = static_cast<GtkTreePath *>(work->data);
521                         MatchFileData *mfd;
522                         GtkTreeIter iter;
523
524                         gtk_tree_model_get_iter(store, &iter, tpath);
525                         gtk_tree_model_get(store, &iter, SEARCH_COLUMN_POINTER, &mfd, -1);
526                         total += mfd->fd->size;
527
528                         if (list) plist = g_list_prepend(plist, file_data_ref(mfd->fd));
529                         }
530
531                 work = work->next;
532                 }
533         g_list_free_full(slist, reinterpret_cast<GDestroyNotify>(gtk_tree_path_free));
534
535         if (bytes) *bytes = total;
536         if (list) *list = g_list_reverse(plist);
537
538         return n;
539 }
540
541 static GList *search_result_selection_list(SearchData *sd)
542 {
543         GList *list;
544
545         search_result_selection_util(sd, nullptr, &list);
546         return list;
547 }
548
549 static gint search_result_selection_count(SearchData *sd, gint64 *bytes)
550 {
551         return search_result_selection_util(sd, bytes, nullptr);
552 }
553
554 static gint search_result_util(SearchData *sd, gint64 *bytes, GList **list)
555 {
556         GtkTreeModel *store;
557         GtkTreeIter iter;
558         gboolean valid;
559         gint n = 0;
560         gint64 total = 0;
561         GList *plist = nullptr;
562
563         store = gtk_tree_view_get_model(GTK_TREE_VIEW(sd->result_view));
564
565         valid = gtk_tree_model_get_iter_first(store, &iter);
566         while (valid)
567                 {
568                 n++;
569                 if (bytes || list)
570                         {
571                         MatchFileData *mfd;
572
573                         gtk_tree_model_get(store, &iter, SEARCH_COLUMN_POINTER, &mfd, -1);
574                         total += mfd->fd->size;
575
576                         if (list) plist = g_list_prepend(plist, file_data_ref(mfd->fd));
577                         }
578                 valid = gtk_tree_model_iter_next(store, &iter);
579                 }
580
581         if (bytes) *bytes = total;
582         if (list) *list = g_list_reverse(plist);
583
584         return n;
585 }
586
587 static GList *search_result_get_filelist(SearchData *sd)
588 {
589         GList *list = nullptr;
590
591         search_result_util(sd, nullptr, &list);
592         return list;
593 }
594
595 static gint search_result_count(SearchData *sd, gint64 *bytes)
596 {
597         return search_result_util(sd, bytes, nullptr);
598 }
599
600 static void search_result_append(SearchData *sd, MatchFileData *mfd)
601 {
602         FileData *fd;
603         GtkListStore *store;
604         GtkTreeIter iter;
605         gchar *text_size;
606         gchar *text_dim = nullptr;
607
608         fd = mfd->fd;
609
610         if (!fd) return;
611
612         text_size = text_from_size(fd->size);
613         if (mfd->width > 0 && mfd->height > 0) text_dim = g_strdup_printf("%d x %d", mfd->width, mfd->height);
614
615         store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(sd->result_view)));
616         gtk_list_store_append(store, &iter);
617         gtk_list_store_set(store, &iter,
618                                 SEARCH_COLUMN_POINTER, mfd,
619                                 SEARCH_COLUMN_RANK, mfd->rank,
620                                 SEARCH_COLUMN_THUMB, fd->thumb_pixbuf,
621                                 SEARCH_COLUMN_NAME, fd->name,
622                                 SEARCH_COLUMN_SIZE, text_size,
623                                 SEARCH_COLUMN_DATE, text_from_time(fd->date),
624                                 SEARCH_COLUMN_DIMENSIONS, text_dim,
625                                 SEARCH_COLUMN_PATH, fd->path,
626                                 -1);
627
628         g_free(text_size);
629         g_free(text_dim);
630 }
631
632 static GList *search_result_refine_list(SearchData *sd)
633 {
634         GList *list = nullptr;
635         GtkTreeModel *store;
636         GtkTreeIter iter;
637         gboolean valid;
638
639         store = gtk_tree_view_get_model(GTK_TREE_VIEW(sd->result_view));
640
641         valid = gtk_tree_model_get_iter_first(store, &iter);
642         while (valid)
643                 {
644                 MatchFileData *mfd;
645
646                 gtk_tree_model_get(store, &iter, SEARCH_COLUMN_POINTER, &mfd, -1);
647                 list = g_list_prepend(list, mfd->fd);
648
649                 valid = gtk_tree_model_iter_next(store, &iter);
650                 }
651
652         /* clear it here, so that the FileData in list is not freed */
653         gtk_list_store_clear(GTK_LIST_STORE(store));
654
655         return g_list_reverse(list);
656 }
657
658 static gboolean search_result_free_node(GtkTreeModel *store, GtkTreePath *, GtkTreeIter *iter, gpointer)
659 {
660         MatchFileData *mfd;
661
662         gtk_tree_model_get(store, iter, SEARCH_COLUMN_POINTER, &mfd, -1);
663         file_data_unref(mfd->fd);
664         g_free(mfd);
665
666         return FALSE;
667 }
668
669 static void search_result_clear(SearchData *sd)
670 {
671         GtkListStore *store;
672
673         store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(sd->result_view)));
674
675         gtk_tree_model_foreach(GTK_TREE_MODEL(store), search_result_free_node, sd);
676         gtk_list_store_clear(store);
677
678         sd->click_fd = nullptr;
679
680         thumb_loader_free(sd->thumb_loader);
681         sd->thumb_loader = nullptr;
682         sd->thumb_fd = nullptr;
683
684         search_status_update(sd);
685 }
686
687 static void search_result_remove_item(SearchData *sd, MatchFileData *mfd, GtkTreeIter *iter)
688 {
689         GtkTreeModel *store;
690
691         if (!mfd || !iter) return;
692
693         store = gtk_tree_view_get_model(GTK_TREE_VIEW(sd->result_view));
694
695         tree_view_move_cursor_away(GTK_TREE_VIEW(sd->result_view), iter, TRUE);
696
697         gtk_list_store_remove(GTK_LIST_STORE(store), iter);
698         if (sd->click_fd == mfd->fd) sd->click_fd = nullptr;
699         if (sd->thumb_fd == mfd->fd) sd->thumb_fd = nullptr;
700         file_data_unref(mfd->fd);
701         g_free(mfd);
702 }
703
704 static void search_result_remove(SearchData *sd, FileData *fd)
705 {
706         GtkTreeModel *store;
707         GtkTreeIter iter;
708         gboolean valid;
709
710         store = gtk_tree_view_get_model(GTK_TREE_VIEW(sd->result_view));
711         valid = gtk_tree_model_get_iter_first(store, &iter);
712         while (valid)
713                 {
714                 MatchFileData *mfd;
715
716                 gtk_tree_model_get(store, &iter, SEARCH_COLUMN_POINTER, &mfd, -1);
717                 if (mfd->fd == fd)
718                         {
719                         search_result_remove_item(sd, mfd, &iter);
720                         return;
721                         }
722
723                 valid = gtk_tree_model_iter_next(GTK_TREE_MODEL(store), &iter);
724                 }
725 }
726
727 static void search_result_remove_selection(SearchData *sd)
728 {
729         GtkTreeSelection *selection;
730         GtkTreeModel *store;
731         GList *slist;
732         GList *flist = nullptr;
733         GList *work;
734
735         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(sd->result_view));
736         slist = gtk_tree_selection_get_selected_rows(selection, &store);
737         work = slist;
738         while (work)
739                 {
740                 auto tpath = static_cast<GtkTreePath *>(work->data);
741                 GtkTreeIter iter;
742                 MatchFileData *mfd;
743
744                 gtk_tree_model_get_iter(store, &iter, tpath);
745                 gtk_tree_model_get(store, &iter, SEARCH_COLUMN_POINTER, &mfd, -1);
746                 flist = g_list_prepend(flist, mfd->fd);
747                 work = work->next;
748                 }
749         g_list_free_full(slist, reinterpret_cast<GDestroyNotify>(gtk_tree_path_free));
750
751         work = flist;
752         while (work)
753                 {
754                 auto fd = static_cast<FileData *>(work->data);
755                 work = work->next;
756
757                 search_result_remove(sd, fd);
758                 }
759         g_list_free(flist);
760
761         search_status_update(sd);
762 }
763
764 static void search_result_edit_selected(SearchData *sd, const gchar *key)
765 {
766         file_util_start_editor_from_filelist(key, search_result_selection_list(sd), nullptr, sd->window);
767 }
768
769 static void search_result_collection_from_selection(SearchData *sd)
770 {
771         CollectWindow *w;
772         GList *list;
773
774         list = search_result_selection_list(sd);
775         w = collection_window_new(nullptr);
776         collection_table_add_filelist(w->table, list);
777         filelist_free(list);
778 }
779
780 static gboolean search_result_update_idle_cb(gpointer data)
781 {
782         auto sd = static_cast<SearchData *>(data);
783
784         search_status_update(sd);
785
786         sd->update_idle_id = 0;
787         return G_SOURCE_REMOVE;
788 }
789
790 static void search_result_update_idle_cancel(SearchData *sd)
791 {
792         if (sd->update_idle_id)
793                 {
794                 g_source_remove(sd->update_idle_id);
795                 sd->update_idle_id = 0;
796                 }
797 }
798
799 static gboolean search_result_select_cb(GtkTreeSelection *, GtkTreeModel *, GtkTreePath *, gboolean, gpointer data)
800 {
801         auto sd = static_cast<SearchData *>(data);
802
803         if (!sd->update_idle_id)
804                 {
805                 sd->update_idle_id = g_idle_add(search_result_update_idle_cb, sd);
806                 }
807
808         return TRUE;
809 }
810
811 /*
812  *-------------------------------------------------------------------
813  * result list thumbs
814  *-------------------------------------------------------------------
815  */
816
817 static void search_result_thumb_step(SearchData *sd);
818
819
820 static void search_result_thumb_set(SearchData *sd, FileData *fd, GtkTreeIter *iter)
821 {
822         GtkListStore *store;
823         GtkTreeIter iter_n;
824
825         store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(sd->result_view)));
826         if (!iter)
827                 {
828                 if (search_result_find_row(sd, fd, &iter_n) >= 0) iter = &iter_n;
829                 }
830
831         if (iter) gtk_list_store_set(store, iter, SEARCH_COLUMN_THUMB, fd->thumb_pixbuf, -1);
832 }
833
834 static void search_result_thumb_do(SearchData *sd)
835 {
836         FileData *fd;
837
838         if (!sd->thumb_loader || !sd->thumb_fd) return;
839         fd = sd->thumb_fd;
840
841         search_result_thumb_set(sd, fd, nullptr);
842 }
843
844 static void search_result_thumb_done_cb(ThumbLoader *, gpointer data)
845 {
846         auto sd = static_cast<SearchData *>(data);
847
848         search_result_thumb_do(sd);
849         search_result_thumb_step(sd);
850 }
851
852 static void search_result_thumb_step(SearchData *sd)
853 {
854         GtkTreeModel *store;
855         GtkTreeIter iter;
856         MatchFileData *mfd = nullptr;
857         gboolean valid;
858         gint row = 0;
859         gint length = 0;
860
861         store = gtk_tree_view_get_model(GTK_TREE_VIEW(sd->result_view));
862         valid = gtk_tree_model_get_iter_first(store, &iter);
863         if (!sd->thumb_enable)
864                 {
865                 while (valid)
866                         {
867                         gtk_list_store_set(GTK_LIST_STORE(store), &iter, SEARCH_COLUMN_THUMB, NULL, -1);
868                         valid = gtk_tree_model_iter_next(store, &iter);
869                         }
870                 return;
871                 }
872
873         while (!mfd && valid)
874                 {
875                 GdkPixbuf *pixbuf;
876
877                 length++;
878                 gtk_tree_model_get(store, &iter, SEARCH_COLUMN_POINTER, &mfd, SEARCH_COLUMN_THUMB, &pixbuf, -1);
879                 if (pixbuf || mfd->fd->thumb_pixbuf)
880                         {
881                         if (!pixbuf) gtk_list_store_set(GTK_LIST_STORE(store), &iter, SEARCH_COLUMN_THUMB, mfd->fd->thumb_pixbuf, -1);
882                         row++;
883                         mfd = nullptr;
884                         }
885                 valid = gtk_tree_model_iter_next(store, &iter);
886                 }
887         if (valid)
888                 {
889                 while (gtk_tree_model_iter_next(store, &iter)) length++;
890                 }
891
892         if (!mfd)
893                 {
894                 sd->thumb_fd = nullptr;
895                 thumb_loader_free(sd->thumb_loader);
896                 sd->thumb_loader = nullptr;
897
898                 search_progress_update(sd, TRUE, -1.0);
899                 return;
900                 }
901
902         search_progress_update(sd, FALSE, static_cast<gdouble>(row)/length);
903
904         sd->thumb_fd = mfd->fd;
905         thumb_loader_free(sd->thumb_loader);
906         sd->thumb_loader = thumb_loader_new(options->thumbnails.max_width, options->thumbnails.max_height);
907
908         thumb_loader_set_callbacks(sd->thumb_loader,
909                                    search_result_thumb_done_cb,
910                                    search_result_thumb_done_cb,
911                                    nullptr,
912                                    sd);
913         if (!thumb_loader_start(sd->thumb_loader, mfd->fd))
914                 {
915                 search_result_thumb_do(sd);
916                 search_result_thumb_step(sd);
917                 }
918 }
919
920 static void search_result_thumb_height(SearchData *sd)
921 {
922         GtkTreeViewColumn *column;
923         GtkCellRenderer *cell;
924         GList *list;
925
926         column = gtk_tree_view_get_column(GTK_TREE_VIEW(sd->result_view), SEARCH_COLUMN_THUMB - 1);
927         if (!column) return;
928
929         gtk_tree_view_column_set_fixed_width(column, (sd->thumb_enable) ? options->thumbnails.max_width : 4);
930
931         list = gtk_cell_layout_get_cells(GTK_CELL_LAYOUT(column));
932         if (!list) return;
933         cell = static_cast<GtkCellRenderer *>(list->data);
934         g_list_free(list);
935
936         g_object_set(G_OBJECT(cell), "height", (sd->thumb_enable) ? options->thumbnails.max_height : -1, NULL);
937         gtk_tree_view_columns_autosize(GTK_TREE_VIEW(sd->result_view));
938 }
939
940 static void search_result_thumb_enable(SearchData *sd, gboolean enable)
941 {
942         GtkTreeViewColumn *column;
943
944         if (sd->thumb_enable == enable) return;
945
946         if (sd->thumb_enable)
947                 {
948                 GtkTreeModel *store;
949                 GtkTreeIter iter;
950                 gboolean valid;
951
952                 thumb_loader_free(sd->thumb_loader);
953                 sd->thumb_loader = nullptr;
954
955                 store = gtk_tree_view_get_model(GTK_TREE_VIEW(sd->result_view));
956                 valid = gtk_tree_model_get_iter_first(store, &iter);
957                 while (valid)
958                         {
959                         gtk_list_store_set(GTK_LIST_STORE(store), &iter, SEARCH_COLUMN_THUMB, NULL, -1);
960                         valid = gtk_tree_model_iter_next(store, &iter);
961                         }
962                 search_progress_update(sd, TRUE, -1.0);
963                 }
964
965         column = gtk_tree_view_get_column(GTK_TREE_VIEW(sd->result_view), SEARCH_COLUMN_THUMB - 1);
966         if (column)
967                 {
968                 gtk_tree_view_column_set_visible(column, enable);
969                 }
970
971         sd->thumb_enable = enable;
972
973         search_result_thumb_height(sd);
974         if (!sd->search_folder_list && !sd->search_file_list) search_result_thumb_step(sd);
975 }
976
977 /*
978  *-------------------------------------------------------------------
979  * result list menu
980  *-------------------------------------------------------------------
981  */
982
983 static void sr_menu_view_cb(GtkWidget *, gpointer data)
984 {
985         auto sd = static_cast<SearchData *>(data);
986
987         if (sd->click_fd) layout_set_fd(nullptr, sd->click_fd);
988 }
989
990 static void sr_menu_viewnew_cb(GtkWidget *, gpointer data)
991 {
992         auto sd = static_cast<SearchData *>(data);
993         GList *list;
994
995         list = search_result_selection_list(sd);
996         view_window_new_from_list(list);
997         filelist_free(list);
998 }
999
1000 static void sr_menu_select_all_cb(GtkWidget *, gpointer data)
1001 {
1002         auto sd = static_cast<SearchData *>(data);
1003         GtkTreeSelection *selection;
1004
1005         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(sd->result_view));
1006         gtk_tree_selection_select_all(selection);
1007 }
1008
1009 static void sr_menu_select_none_cb(GtkWidget *, gpointer data)
1010 {
1011         auto sd = static_cast<SearchData *>(data);
1012         GtkTreeSelection *selection;
1013
1014         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(sd->result_view));
1015         gtk_tree_selection_unselect_all(selection);
1016 }
1017
1018 static void sr_menu_edit_cb(GtkWidget *widget, gpointer data)
1019 {
1020         SearchData *sd;
1021         auto key = static_cast<const gchar *>(data);
1022
1023         sd = static_cast<SearchData *>(submenu_item_get_data(widget));
1024         if (!sd) return;
1025
1026         search_result_edit_selected(sd, key);
1027 }
1028
1029 static void sr_menu_print_cb(GtkWidget *, gpointer data)
1030 {
1031         auto sd = static_cast<SearchData *>(data);
1032
1033         print_window_new(sd->click_fd, search_result_selection_list(sd),
1034                          search_result_get_filelist(sd), sd->window);
1035 }
1036
1037 static void sr_menu_copy_cb(GtkWidget *, gpointer data)
1038 {
1039         auto sd = static_cast<SearchData *>(data);
1040
1041         file_util_copy(nullptr, search_result_selection_list(sd), nullptr, sd->window);
1042 }
1043
1044 static void sr_menu_move_cb(GtkWidget *, gpointer data)
1045 {
1046         auto sd = static_cast<SearchData *>(data);
1047
1048         file_util_move(nullptr, search_result_selection_list(sd), nullptr, sd->window);
1049 }
1050
1051 static void sr_menu_rename_cb(GtkWidget *, gpointer data)
1052 {
1053         auto sd = static_cast<SearchData *>(data);
1054
1055         file_util_rename(nullptr, search_result_selection_list(sd), sd->window);
1056 }
1057
1058 static void sr_menu_delete_cb(GtkWidget *, gpointer data)
1059 {
1060         auto sd = static_cast<SearchData *>(data);
1061
1062         options->file_ops.safe_delete_enable = FALSE;
1063         file_util_delete(nullptr, search_result_selection_list(sd), sd->window);
1064 }
1065
1066 static void sr_menu_move_to_trash_cb(GtkWidget *, gpointer data)
1067 {
1068         auto sd = static_cast<SearchData *>(data);
1069
1070         options->file_ops.safe_delete_enable = TRUE;
1071         file_util_delete(nullptr, search_result_selection_list(sd), sd->window);
1072 }
1073
1074 static void sr_menu_copy_path_cb(GtkWidget *, gpointer data)
1075 {
1076         auto sd = static_cast<SearchData *>(data);
1077
1078         file_util_copy_path_list_to_clipboard(search_result_selection_list(sd), TRUE);
1079 }
1080
1081 static void sr_menu_copy_path_unquoted_cb(GtkWidget *, gpointer data)
1082 {
1083         auto sd = static_cast<SearchData *>(data);
1084
1085         file_util_copy_path_list_to_clipboard(search_result_selection_list(sd), FALSE);
1086 }
1087
1088 static void sr_menu_play_cb(GtkWidget *, gpointer data)
1089 {
1090         auto sd = static_cast<SearchData *>(data);
1091
1092         start_editor_from_file(options->image_l_click_video_editor, sd->click_fd);
1093 }
1094
1095 static void search_result_menu_destroy_cb(GtkWidget *, gpointer data)
1096 {
1097         auto editmenu_fd_list = static_cast<GList *>(data);
1098
1099         filelist_free(editmenu_fd_list);
1100 }
1101
1102 /**
1103  * @brief Add file selection list to a collection
1104  * @param[in] widget
1105  * @param[in] data Index to the collection list menu item selected, or -1 for new collection
1106  *
1107  *
1108  */
1109 static void search_pop_menu_collections_cb(GtkWidget *widget, gpointer data)
1110 {
1111         SearchData *sd;
1112         GList *selection_list;
1113
1114         sd = static_cast<SearchData *>(submenu_item_get_data(widget));
1115         selection_list = search_result_selection_list(sd);
1116         pop_menu_collections(selection_list, data);
1117
1118         filelist_free(selection_list);
1119 }
1120
1121 static GtkWidget *search_result_menu(SearchData *sd, gboolean on_row, gboolean empty)
1122 {
1123         GtkWidget *menu;
1124         GtkWidget *item;
1125         GList *editmenu_fd_list;
1126         gboolean video;
1127         GtkAccelGroup *accel_group;
1128
1129         menu = popup_menu_short_lived();
1130         accel_group = gtk_accel_group_new();
1131         gtk_menu_set_accel_group(GTK_MENU(menu), accel_group);
1132
1133         g_object_set_data(G_OBJECT(menu), "window_keys", search_window_keys);
1134         g_object_set_data(G_OBJECT(menu), "accel_group", accel_group);
1135
1136         video = (on_row && sd->click_fd && sd->click_fd->format_class == FORMAT_CLASS_VIDEO);
1137         menu_item_add_icon_sensitive(menu, _("_Play"), GQ_ICON_PREV_PAGE , video,
1138                             G_CALLBACK(sr_menu_play_cb), sd);
1139         menu_item_add_divider(menu);
1140
1141         menu_item_add_sensitive(menu, _("_View"), on_row,
1142                                 G_CALLBACK(sr_menu_view_cb), sd);
1143         menu_item_add_icon_sensitive(menu, _("View in _new window"), GQ_ICON_NEW, on_row,
1144                                       G_CALLBACK(sr_menu_viewnew_cb), sd);
1145         menu_item_add_divider(menu);
1146         menu_item_add_sensitive(menu, _("Select all"), !empty,
1147                                 G_CALLBACK(sr_menu_select_all_cb), sd);
1148         menu_item_add_sensitive(menu, _("Select none"), !empty,
1149                                 G_CALLBACK(sr_menu_select_none_cb), sd);
1150         menu_item_add_divider(menu);
1151
1152         editmenu_fd_list = search_result_selection_list(sd);
1153         g_signal_connect(G_OBJECT(menu), "destroy",
1154                          G_CALLBACK(search_result_menu_destroy_cb), editmenu_fd_list);
1155         submenu_add_edit(menu, &item, G_CALLBACK(sr_menu_edit_cb), sd, editmenu_fd_list);
1156         if (!on_row) gtk_widget_set_sensitive(item, FALSE);
1157
1158         submenu_add_collections(menu, &item,
1159                                 G_CALLBACK(search_pop_menu_collections_cb), sd);
1160         gtk_widget_set_sensitive(item, on_row);
1161
1162         menu_item_add_icon_sensitive(menu, _("Print..."), GQ_ICON_PRINT, on_row,
1163                                       G_CALLBACK(sr_menu_print_cb), sd);
1164         menu_item_add_divider(menu);
1165         menu_item_add_icon_sensitive(menu, _("_Copy..."), GQ_ICON_COPY, on_row,
1166                                       G_CALLBACK(sr_menu_copy_cb), sd);
1167         menu_item_add_sensitive(menu, _("_Move..."), on_row,
1168                                 G_CALLBACK(sr_menu_move_cb), sd);
1169         menu_item_add_sensitive(menu, _("_Rename..."), on_row,
1170                                 G_CALLBACK(sr_menu_rename_cb), sd);
1171         menu_item_add_sensitive(menu, _("_Copy path"), on_row,
1172                                 G_CALLBACK(sr_menu_copy_path_cb), sd);
1173         menu_item_add_sensitive(menu, _("_Copy path unquoted"), on_row,
1174                                 G_CALLBACK(sr_menu_copy_path_unquoted_cb), sd);
1175
1176         menu_item_add_divider(menu);
1177         menu_item_add_icon_sensitive(menu,
1178                                 options->file_ops.confirm_move_to_trash ? _("Move to Trash...") :
1179                                         _("Move to Trash"), GQ_ICON_DELETE, on_row,
1180                                 G_CALLBACK(sr_menu_move_to_trash_cb), sd);
1181         menu_item_add_icon_sensitive(menu,
1182                                 options->file_ops.confirm_delete ? _("_Delete...") :
1183                                         _("_Delete"), GQ_ICON_DELETE_SHRED, on_row,
1184                                 G_CALLBACK(sr_menu_delete_cb), sd);
1185
1186         return menu;
1187 }
1188
1189 /*
1190  *-------------------------------------------------------------------
1191  * result list input
1192  *-------------------------------------------------------------------
1193  */
1194
1195 static gboolean search_result_press_cb(GtkWidget *widget, GdkEventButton *bevent, gpointer data)
1196 {
1197         auto sd = static_cast<SearchData *>(data);
1198         GtkTreeModel *store;
1199         GtkTreePath *tpath;
1200         GtkTreeIter iter;
1201         MatchFileData *mfd = nullptr;
1202
1203         store = gtk_tree_view_get_model(GTK_TREE_VIEW(widget));
1204
1205         if (gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(widget), bevent->x, bevent->y,
1206                                           &tpath, nullptr, nullptr, nullptr))
1207                 {
1208                 gtk_tree_model_get_iter(store, &iter, tpath);
1209                 gtk_tree_model_get(store, &iter, SEARCH_COLUMN_POINTER, &mfd, -1);
1210                 gtk_tree_path_free(tpath);
1211                 }
1212
1213         sd->click_fd = mfd ? mfd->fd : nullptr;
1214
1215         if (bevent->button == MOUSE_BUTTON_RIGHT)
1216                 {
1217                 GtkWidget *menu;
1218
1219                 menu = search_result_menu(sd, (mfd != nullptr), (search_result_count(sd, nullptr) == 0));
1220                 gtk_menu_popup_at_pointer(GTK_MENU(menu), nullptr);
1221                 }
1222
1223         if (!mfd) return FALSE;
1224
1225         if (bevent->button == MOUSE_BUTTON_LEFT && bevent->type == GDK_2BUTTON_PRESS)
1226                 {
1227                 layout_set_fd(nullptr, mfd->fd);
1228                 }
1229
1230         if (bevent->button == MOUSE_BUTTON_MIDDLE) return TRUE;
1231
1232         if (bevent->button == MOUSE_BUTTON_RIGHT)
1233                 {
1234                 if (!search_result_row_selected(sd, mfd->fd))
1235                         {
1236                         GtkTreeSelection *selection;
1237
1238                         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(widget));
1239                         gtk_tree_selection_unselect_all(selection);
1240                         gtk_tree_selection_select_iter(selection, &iter);
1241
1242                         tpath = gtk_tree_model_get_path(store, &iter);
1243                         gtk_tree_view_set_cursor(GTK_TREE_VIEW(widget), tpath, nullptr, FALSE);
1244                         gtk_tree_path_free(tpath);
1245                         }
1246                 return TRUE;
1247                 }
1248
1249         if (bevent->button == MOUSE_BUTTON_LEFT && bevent->type == GDK_BUTTON_PRESS &&
1250             !(bevent->state & GDK_SHIFT_MASK ) &&
1251             !(bevent->state & GDK_CONTROL_MASK ) &&
1252             search_result_row_selected(sd, mfd->fd))
1253                 {
1254                 /* this selection handled on release_cb */
1255                 gtk_widget_grab_focus(widget);
1256                 return TRUE;
1257                 }
1258
1259         return FALSE;
1260 }
1261
1262 static gboolean search_result_release_cb(GtkWidget *widget, GdkEventButton *bevent, gpointer data)
1263 {
1264         auto sd = static_cast<SearchData *>(data);
1265         GtkTreeModel *store;
1266         GtkTreePath *tpath;
1267         GtkTreeIter iter;
1268
1269         MatchFileData *mfd = nullptr;
1270
1271         if (bevent->button != MOUSE_BUTTON_LEFT && bevent->button != MOUSE_BUTTON_MIDDLE) return TRUE;
1272
1273         store = gtk_tree_view_get_model(GTK_TREE_VIEW(widget));
1274
1275         if ((bevent->x != 0 || bevent->y != 0) &&
1276             gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(widget), bevent->x, bevent->y,
1277                                           &tpath, nullptr, nullptr, nullptr))
1278                 {
1279                 gtk_tree_model_get_iter(store, &iter, tpath);
1280                 gtk_tree_model_get(store, &iter, SEARCH_COLUMN_POINTER, &mfd, -1);
1281                 gtk_tree_path_free(tpath);
1282                 }
1283
1284         if (bevent->button == MOUSE_BUTTON_MIDDLE)
1285                 {
1286                 if (mfd && sd->click_fd == mfd->fd)
1287                         {
1288                         GtkTreeSelection *selection;
1289
1290                         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(widget));
1291                         if (search_result_row_selected(sd, mfd->fd))
1292                                 {
1293                                 gtk_tree_selection_unselect_iter(selection, &iter);
1294                                 }
1295                         else
1296                                 {
1297                                 gtk_tree_selection_select_iter(selection, &iter);
1298                                 }
1299                         }
1300                 return TRUE;
1301                 }
1302
1303         if (mfd && sd->click_fd == mfd->fd &&
1304             !(bevent->state & GDK_SHIFT_MASK ) &&
1305             !(bevent->state & GDK_CONTROL_MASK ) &&
1306             search_result_row_selected(sd, mfd->fd))
1307                 {
1308                 GtkTreeSelection *selection;
1309
1310                 selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(widget));
1311                 gtk_tree_selection_unselect_all(selection);
1312                 gtk_tree_selection_select_iter(selection, &iter);
1313
1314                 tpath = gtk_tree_model_get_path(store, &iter);
1315                 gtk_tree_view_set_cursor(GTK_TREE_VIEW(widget), tpath, nullptr, FALSE);
1316                 gtk_tree_path_free(tpath);
1317
1318                 return TRUE;
1319                 }
1320
1321         return FALSE;
1322 }
1323
1324
1325 static gboolean search_result_keypress_cb(GtkWidget *widget, GdkEventKey *event, gpointer data)
1326 {
1327         auto sd = static_cast<SearchData *>(data);
1328         gboolean stop_signal = FALSE;
1329         GtkTreeModel *store;
1330         GtkTreeSelection *selection;
1331         GList *slist;
1332         MatchFileData *mfd = nullptr;
1333
1334         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(sd->result_view));
1335         slist = gtk_tree_selection_get_selected_rows(selection, &store);
1336         if (slist)
1337                 {
1338                 GtkTreePath *tpath;
1339                 GtkTreeIter iter;
1340                 GList *last;
1341
1342                 last = g_list_last(slist);
1343                 tpath = static_cast<GtkTreePath *>(last->data);
1344
1345                 /* last is newest selected file */
1346                 gtk_tree_model_get_iter(store, &iter, tpath);
1347                 gtk_tree_model_get(store, &iter, SEARCH_COLUMN_POINTER, &mfd, -1);
1348                 }
1349         g_list_free_full(slist, reinterpret_cast<GDestroyNotify>(gtk_tree_path_free));
1350
1351         if (event->state & GDK_CONTROL_MASK)
1352                 {
1353                 stop_signal = TRUE;
1354                 switch (event->keyval)
1355                         {
1356                         case '1':
1357                         case '2':
1358                         case '3':
1359                         case '4':
1360                         case '5':
1361                         case '6':
1362                         case '7':
1363                         case '8':
1364                         case '9':
1365                         case '0':
1366                                 break;
1367                         case 'C': case 'c':
1368                                 file_util_copy(nullptr, search_result_selection_list(sd), nullptr, widget);
1369                                 break;
1370                         case 'M': case 'm':
1371                                 file_util_move(nullptr, search_result_selection_list(sd), nullptr, widget);
1372                                 break;
1373                         case 'R': case 'r':
1374                                 file_util_rename(nullptr, search_result_selection_list(sd), widget);
1375                                 break;
1376                         case 'D': case 'd':
1377                                 options->file_ops.safe_delete_enable = TRUE;
1378                                 file_util_delete(nullptr, search_result_selection_list(sd), widget);
1379                                 break;
1380                         case 'A': case 'a':
1381                                 if (event->state & GDK_SHIFT_MASK)
1382                                         {
1383                                         gtk_tree_selection_unselect_all(selection);
1384                                         }
1385                                 else
1386                                         {
1387                                         gtk_tree_selection_select_all(selection);
1388                                         }
1389                                 break;
1390                         case GDK_KEY_Delete: case GDK_KEY_KP_Delete:
1391                                 search_result_clear(sd);
1392                                 break;
1393                         default:
1394                                 stop_signal = FALSE;
1395                                 break;
1396                         }
1397                 }
1398         else
1399                 {
1400                 stop_signal = TRUE;
1401                 switch (event->keyval)
1402                         {
1403                         case GDK_KEY_Return: case GDK_KEY_KP_Enter:
1404                                 if (mfd) layout_set_fd(nullptr, mfd->fd);
1405                                 break;
1406                         case 'V': case 'v':
1407                                 {
1408                                 GList *list;
1409
1410                                 list = search_result_selection_list(sd);
1411                                 view_window_new_from_list(list);
1412                                 filelist_free(list);
1413                                 }
1414                                 break;
1415                         case GDK_KEY_Delete: case GDK_KEY_KP_Delete:
1416                                 search_result_remove_selection(sd);
1417                                 break;
1418                         case 'C': case 'c':
1419                                 search_result_collection_from_selection(sd);
1420                                 break;
1421                         case GDK_KEY_Menu:
1422                         case GDK_KEY_F10:
1423                                 {
1424                                 GtkWidget *menu;
1425
1426                                 if (mfd)
1427                                         sd->click_fd = mfd->fd;
1428                                 else
1429                                         sd->click_fd = nullptr;
1430
1431                                 menu = search_result_menu(sd, (mfd != nullptr), (search_result_count(sd, nullptr) > 0));
1432
1433                                 gtk_menu_popup_at_widget(GTK_MENU(menu), widget, GDK_GRAVITY_EAST, GDK_GRAVITY_CENTER, nullptr);
1434                                 }
1435                                 break;
1436                         default:
1437                                 stop_signal = FALSE;
1438                                 break;
1439                         }
1440                 }
1441
1442         return stop_signal;
1443 }
1444
1445 static gboolean search_window_keypress_cb(GtkWidget *, GdkEventKey *event, gpointer data)
1446 {
1447         auto sd = static_cast<SearchData *>(data);
1448         gboolean stop_signal = FALSE;
1449
1450         if (event->state & GDK_CONTROL_MASK)
1451                 {
1452                 stop_signal = TRUE;
1453                 switch (event->keyval)
1454                         {
1455                         case 'T': case 't':
1456                                 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(sd->button_thumbs),
1457                                         !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(sd->button_thumbs)));
1458                                 break;
1459                         case 'W': case 'w':
1460                                 search_window_close(sd);
1461                                 break;
1462                         case GDK_KEY_Return: case GDK_KEY_KP_Enter:
1463                                 search_start_cb(nullptr, sd);
1464                                 break;
1465                         default:
1466                                 stop_signal = FALSE;
1467                                 break;
1468                         }
1469                 }
1470         if (!stop_signal && is_help_key(event))
1471                 {
1472                 help_window_show("GuideImageSearchSearch.html");
1473                 stop_signal = TRUE;
1474                 }
1475
1476         return stop_signal;
1477 }
1478
1479 /*
1480  *-------------------------------------------------------------------
1481  * dnd
1482  *-------------------------------------------------------------------
1483  */
1484
1485 static GtkTargetEntry result_drag_types[] = {
1486         { const_cast<gchar *>("text/uri-list"), 0, TARGET_URI_LIST },
1487         { const_cast<gchar *>("text/plain"), 0, TARGET_TEXT_PLAIN }
1488 };
1489 static gint n_result_drag_types = 2;
1490
1491 static GtkTargetEntry result_drop_types[] = {
1492         { const_cast<gchar *>("text/uri-list"), 0, TARGET_URI_LIST },
1493         { const_cast<gchar *>("text/plain"), 0, TARGET_TEXT_PLAIN }
1494 };
1495 static gint n_result_drop_types = 2;
1496
1497 static void search_dnd_data_set(GtkWidget *, GdkDragContext *,
1498                                 GtkSelectionData *selection_data, guint,
1499                                 guint, gpointer data)
1500 {
1501         auto sd = static_cast<SearchData *>(data);
1502         GList *list;
1503
1504         list = search_result_selection_list(sd);
1505         if (!list) return;
1506
1507         uri_selection_data_set_uris_from_filelist(selection_data, list);
1508         filelist_free(list);
1509 }
1510
1511 static void search_dnd_begin(GtkWidget *widget, GdkDragContext *context, gpointer data)
1512 {
1513         auto sd = static_cast<SearchData *>(data);
1514
1515         if (sd->click_fd && !search_result_row_selected(sd, sd->click_fd))
1516                 {
1517                 GtkListStore *store;
1518                 GtkTreeIter iter;
1519
1520                 store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(widget)));
1521                 if (search_result_find_row(sd, sd->click_fd, &iter) >= 0)
1522                         {
1523                         GtkTreeSelection *selection;
1524                         GtkTreePath *tpath;
1525
1526                         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(widget));
1527                         gtk_tree_selection_unselect_all(selection);
1528                         gtk_tree_selection_select_iter(selection, &iter);
1529
1530                         tpath = gtk_tree_model_get_path(GTK_TREE_MODEL(store), &iter);
1531                         gtk_tree_view_set_cursor(GTK_TREE_VIEW(widget), tpath, nullptr, FALSE);
1532                         gtk_tree_path_free(tpath);
1533                         }
1534                 }
1535
1536         if (sd->thumb_enable &&
1537             sd->click_fd && sd->click_fd->thumb_pixbuf)
1538                 {
1539                 dnd_set_drag_icon(widget, context, sd->click_fd->thumb_pixbuf, search_result_selection_count(sd, nullptr));
1540                 }
1541 }
1542
1543 static void search_gps_dnd_received_cb(GtkWidget *, GdkDragContext *,
1544                                                                                 gint, gint,
1545                                                                                 GtkSelectionData *selection_data, guint info,
1546                                                                                 guint, gpointer data)
1547 {
1548         auto sd = static_cast<SearchData *>(data);
1549         GList *list;
1550         gdouble latitude, longitude;
1551         FileData *fd;
1552
1553         if (info == TARGET_URI_LIST)
1554                 {
1555                 list = uri_filelist_from_gtk_selection_data(selection_data);
1556
1557                 /* If more than one file, use only the first file in a list.
1558                 */
1559                 if (list != nullptr)
1560                         {
1561                         fd = static_cast<FileData *>(list->data);
1562                         latitude = metadata_read_GPS_coord(fd, "Xmp.exif.GPSLatitude", 1000);
1563                         longitude = metadata_read_GPS_coord(fd, "Xmp.exif.GPSLongitude", 1000);
1564                         if (latitude != 1000 && longitude != 1000)
1565                                 {
1566                                 gq_gtk_entry_set_text(GTK_ENTRY(sd->entry_gps_coord),
1567                                                         g_strdup_printf("%lf %lf", latitude, longitude));
1568                                 }
1569                         else
1570                                 {
1571                                 gq_gtk_entry_set_text(GTK_ENTRY(sd->entry_gps_coord), "Image is not geocoded");
1572                                 }
1573                         }
1574                 }
1575
1576         if (info == TARGET_TEXT_PLAIN)
1577                 {
1578                 gq_gtk_entry_set_text(GTK_ENTRY(sd->entry_gps_coord),"");
1579                 }
1580 }
1581
1582 static void search_path_entry_dnd_received_cb(GtkWidget *, GdkDragContext *,
1583                                                                                 gint, gint,
1584                                                                                 GtkSelectionData *selection_data, guint info,
1585                                                                                 guint, gpointer data)
1586 {
1587         auto sd = static_cast<SearchData *>(data);
1588         GList *list;
1589         FileData *fd;
1590
1591         if (info == TARGET_URI_LIST)
1592                 {
1593                 list = uri_filelist_from_gtk_selection_data(selection_data);
1594                 /* If more than one file, use only the first file in a list.
1595                 */
1596                 if (list != nullptr)
1597                         {
1598                         fd = static_cast<FileData *>(list->data);
1599                         gq_gtk_entry_set_text(GTK_ENTRY(sd->path_entry),
1600                                                 g_strdup_printf("%s", fd->path));
1601                         gtk_widget_set_tooltip_text(GTK_WIDGET(sd->path_entry),g_strdup_printf("%s", fd->path));
1602                         }
1603                 }
1604
1605         if (info == TARGET_TEXT_PLAIN)
1606                 {
1607                 gq_gtk_entry_set_text(GTK_ENTRY(sd->path_entry),"");
1608                 }
1609 }
1610
1611 static void search_image_content_dnd_received_cb(GtkWidget *, GdkDragContext *,
1612                                                                                 gint, gint,
1613                                                                                 GtkSelectionData *selection_data, guint info,
1614                                                                                 guint, gpointer data)
1615 {
1616         auto sd = static_cast<SearchData *>(data);
1617         GList *list;
1618         FileData *fd;
1619
1620         if (info == TARGET_URI_LIST)
1621                 {
1622                 list = uri_filelist_from_gtk_selection_data(selection_data);
1623                 /* If more than one file, use only the first file in a list.
1624                 */
1625                 if (list != nullptr)
1626                         {
1627                         fd = static_cast<FileData *>(list->data);
1628                         gq_gtk_entry_set_text(GTK_ENTRY(sd->entry_similarity),
1629                                                 g_strdup_printf("%s", fd->path));
1630                         gtk_widget_set_tooltip_text(GTK_WIDGET(sd->entry_similarity),g_strdup_printf("%s", fd->path));
1631                         }
1632                 }
1633
1634         if (info == TARGET_TEXT_PLAIN)
1635                 {
1636                 gq_gtk_entry_set_text(GTK_ENTRY(sd->entry_similarity),"");
1637                 }
1638 }
1639
1640 static void search_dnd_init(SearchData *sd)
1641 {
1642         gtk_drag_source_set(sd->result_view, static_cast<GdkModifierType>(GDK_BUTTON1_MASK | GDK_BUTTON2_MASK),
1643                             result_drag_types, n_result_drag_types,
1644                             static_cast<GdkDragAction>(GDK_ACTION_COPY | GDK_ACTION_MOVE | GDK_ACTION_LINK));
1645         g_signal_connect(G_OBJECT(sd->result_view), "drag_data_get",
1646                          G_CALLBACK(search_dnd_data_set), sd);
1647         g_signal_connect(G_OBJECT(sd->result_view), "drag_begin",
1648                          G_CALLBACK(search_dnd_begin), sd);
1649
1650         gtk_drag_dest_set(GTK_WIDGET(sd->entry_gps_coord),
1651                                          GTK_DEST_DEFAULT_ALL,
1652                                           result_drop_types, n_result_drop_types,
1653                                          GDK_ACTION_COPY);
1654
1655         g_signal_connect(G_OBJECT(sd->entry_gps_coord), "drag_data_received",
1656                                         G_CALLBACK(search_gps_dnd_received_cb), sd);
1657
1658         gtk_drag_dest_set(GTK_WIDGET(sd->path_entry),
1659                                         GTK_DEST_DEFAULT_ALL,
1660                                         result_drop_types, n_result_drop_types,
1661                                         GDK_ACTION_COPY);
1662
1663         g_signal_connect(G_OBJECT(sd->path_entry), "drag_data_received",
1664                                         G_CALLBACK(search_path_entry_dnd_received_cb), sd);
1665
1666         gtk_drag_dest_set(GTK_WIDGET(sd->entry_similarity),
1667                                         GTK_DEST_DEFAULT_ALL,
1668                                         result_drop_types, n_result_drop_types,
1669                                         GDK_ACTION_COPY);
1670
1671         g_signal_connect(G_OBJECT(sd->entry_similarity), "drag_data_received",
1672                                         G_CALLBACK(search_image_content_dnd_received_cb), sd);
1673 }
1674
1675 /*
1676  *-------------------------------------------------------------------
1677  * search core
1678  *-------------------------------------------------------------------
1679  */
1680
1681 #define MATCH_IS_BETWEEN(val, a, b)  (b > a ? (val >= a && val <= b) : (val >= b && val <= a))
1682
1683 static gboolean search_step_cb(gpointer data);
1684
1685
1686 static void search_buffer_flush(SearchData *sd)
1687 {
1688         GList *work;
1689
1690         work = g_list_last(sd->search_buffer_list);
1691         while (work)
1692                 {
1693                 auto mfd = static_cast<MatchFileData *>(work->data);
1694                 work = work->prev;
1695
1696                 search_result_append(sd, mfd);
1697                 }
1698
1699         g_list_free(sd->search_buffer_list);
1700         sd->search_buffer_list = nullptr;
1701         sd->search_buffer_count = 0;
1702 }
1703
1704 static void search_stop(SearchData *sd)
1705 {
1706         if (sd->search_idle_id)
1707                 {
1708                 g_source_remove(sd->search_idle_id);
1709                 sd->search_idle_id = 0;
1710                 }
1711
1712         image_loader_free(sd->img_loader);
1713         sd->img_loader = nullptr;
1714         cache_sim_data_free(sd->img_cd);
1715         sd->img_cd = nullptr;
1716
1717         cache_sim_data_free(sd->search_similarity_cd);
1718         sd->search_similarity_cd = nullptr;
1719
1720         search_buffer_flush(sd);
1721
1722         filelist_free(sd->search_folder_list);
1723         sd->search_folder_list = nullptr;
1724
1725         g_list_free(sd->search_done_list);
1726         sd->search_done_list = nullptr;
1727
1728         filelist_free(sd->search_file_list);
1729         sd->search_file_list = nullptr;
1730
1731         sd->match_broken_enable = FALSE;
1732
1733         gtk_widget_set_sensitive(sd->box_search, TRUE);
1734         gtk_spinner_stop(GTK_SPINNER(sd->spinner));
1735         gtk_widget_set_sensitive(sd->button_start, TRUE);
1736         gtk_widget_set_sensitive(sd->button_stop, FALSE);
1737         search_progress_update(sd, TRUE, -1.0);
1738         search_status_update(sd);
1739 }
1740
1741 static void search_file_load_process(SearchData *sd, CacheData *cd)
1742 {
1743         GdkPixbuf *pixbuf;
1744
1745         pixbuf = image_loader_get_pixbuf(sd->img_loader);
1746
1747         /* Used to determine if image is broken
1748          */
1749         if (cd && !pixbuf)
1750                 {
1751                 if (!cd->dimensions)
1752                         {
1753                         cache_sim_data_set_dimensions(cd, -1, -1);
1754                         }
1755                 }
1756         else if (cd && pixbuf)
1757                 {
1758                 if (!cd->dimensions)
1759                         {
1760                         cache_sim_data_set_dimensions(cd, gdk_pixbuf_get_width(pixbuf),
1761                                                           gdk_pixbuf_get_height(pixbuf));
1762                         }
1763
1764                 if (sd->match_similarity_enable && !cd->similarity)
1765                         {
1766                         ImageSimilarityData *sim;
1767
1768                         sim = image_sim_new_from_pixbuf(pixbuf);
1769                         cache_sim_data_set_similarity(cd, sim);
1770                         image_sim_free(sim);
1771                         }
1772
1773                 if (options->thumbnails.enable_caching &&
1774                     sd->img_loader && image_loader_get_fd(sd->img_loader))
1775                         {
1776                         gchar *base;
1777                         const gchar *path;
1778                         mode_t mode = 0755;
1779
1780                         path = image_loader_get_fd(sd->img_loader)->path;
1781                         base = cache_get_location(CACHE_TYPE_SIM, path, FALSE, &mode);
1782                         if (recursive_mkdir_if_not_exists(base, mode))
1783                                 {
1784                                 g_free(cd->path);
1785                                 cd->path = cache_get_location(CACHE_TYPE_SIM, path, TRUE, nullptr);
1786                                 if (cache_sim_data_save(cd))
1787                                         {
1788                                         filetime_set(cd->path, filetime(image_loader_get_fd(sd->img_loader)->path));
1789                                         }
1790                                 }
1791                         g_free(base);
1792                         }
1793                 }
1794
1795         image_loader_free(sd->img_loader);
1796         sd->img_loader = nullptr;
1797
1798         sd->search_idle_id = g_idle_add(search_step_cb, sd);
1799 }
1800
1801 static void search_file_load_done_cb(ImageLoader *, gpointer data)
1802 {
1803         auto sd = static_cast<SearchData *>(data);
1804         search_file_load_process(sd, sd->img_cd);
1805 }
1806
1807 static gboolean search_file_do_extra(SearchData *sd, FileData *fd, gint *match,
1808                                      gint *width, gint *height, gint *simval)
1809 {
1810         gboolean new_data = FALSE;
1811         gboolean tmatch = TRUE;
1812         gboolean tested = FALSE;
1813
1814         if (!sd->img_cd)
1815                 {
1816                 gchar *cd_path;
1817
1818                 new_data = TRUE;
1819
1820                 cd_path = cache_find_location(CACHE_TYPE_SIM, fd->path);
1821                 if (cd_path && filetime(fd->path) == filetime(cd_path))
1822                         {
1823                         sd->img_cd = cache_sim_data_load(cd_path);
1824                         }
1825                 g_free(cd_path);
1826                 }
1827
1828         if (!sd->img_cd)
1829                 {
1830                 sd->img_cd = cache_sim_data_new();
1831                 }
1832
1833         if (new_data)
1834                 {
1835                 if ((sd->match_dimensions_enable && !sd->img_cd->dimensions) || (sd->match_similarity_enable && !sd->img_cd->similarity) || sd->match_broken_enable)
1836                         {
1837                         sd->img_loader = image_loader_new(fd);
1838                         g_signal_connect(G_OBJECT(sd->img_loader), "error", (GCallback)search_file_load_done_cb, sd);
1839                         g_signal_connect(G_OBJECT(sd->img_loader), "done", (GCallback)search_file_load_done_cb, sd);
1840                         if (image_loader_start(sd->img_loader))
1841                                 {
1842                                 return TRUE;
1843                                 }
1844                         else
1845                                 {
1846                                 image_loader_free(sd->img_loader);
1847                                 sd->img_loader = nullptr;
1848                                 }
1849                         }
1850                 }
1851
1852         if (sd->match_broken_enable)
1853                 {
1854                 tested = TRUE;
1855                 tmatch = FALSE;
1856                 if (sd->match_class == SEARCH_MATCH_EQUAL && sd->img_cd->width == -1)
1857                         {
1858                         tmatch = TRUE;
1859                         }
1860                 else if (sd->match_class == SEARCH_MATCH_NONE && sd->img_cd->width != -1)
1861                         {
1862                         tmatch = TRUE;
1863                         }
1864                 }
1865
1866         if (tmatch && sd->match_dimensions_enable && sd->img_cd->dimensions)
1867                 {
1868                 CacheData *cd = sd->img_cd;
1869
1870                 tmatch = FALSE;
1871                 tested = TRUE;
1872
1873                 if (sd->match_dimensions == SEARCH_MATCH_EQUAL)
1874                         {
1875                         tmatch = (cd->width == sd->search_width && cd->height == sd->search_height);
1876                         }
1877                 else if (sd->match_dimensions == SEARCH_MATCH_UNDER)
1878                         {
1879                         tmatch = (cd->width < sd->search_width && cd->height < sd->search_height);
1880                         }
1881                 else if (sd->match_dimensions == SEARCH_MATCH_OVER)
1882                         {
1883                         tmatch = (cd->width > sd->search_width && cd->height > sd->search_height);
1884                         }
1885                 else if (sd->match_dimensions == SEARCH_MATCH_BETWEEN)
1886                         {
1887                         tmatch = (MATCH_IS_BETWEEN(cd->width, sd->search_width, sd->search_width_end) &&
1888                                   MATCH_IS_BETWEEN(cd->height, sd->search_height, sd->search_height_end));
1889                         }
1890                 }
1891
1892         if (tmatch && sd->match_similarity_enable && sd->img_cd->similarity)
1893                 {
1894                 gdouble value = 0.0;
1895
1896                 tmatch = FALSE;
1897                 tested = TRUE;
1898
1899                 /** @FIXME implement similarity checking */
1900                 if (sd->search_similarity_cd && sd->search_similarity_cd->similarity)
1901                         {
1902                         gdouble result;
1903
1904                         result = image_sim_compare_fast(sd->search_similarity_cd->sim, sd->img_cd->sim,
1905                                                         static_cast<gdouble>(sd->search_similarity) / 100.0);
1906                         result *= 100.0;
1907                         if (result >= static_cast<gdouble>(sd->search_similarity))
1908                                 {
1909                                 tmatch = TRUE;
1910                                 value = static_cast<gint>(result);
1911                                 }
1912                         }
1913
1914                 if (simval) *simval = value;
1915                 }
1916
1917         if (sd->img_cd->dimensions)
1918                 {
1919                 if (width) *width = sd->img_cd->width;
1920                 if (height) *height = sd->img_cd->height;
1921                 }
1922
1923         cache_sim_data_free(sd->img_cd);
1924         sd->img_cd = nullptr;
1925
1926         *match = (tmatch && tested);
1927
1928         return FALSE;
1929 }
1930
1931 static gboolean search_file_next(SearchData *sd)
1932 {
1933         FileData *fd;
1934         gboolean match = TRUE;
1935         gboolean tested = FALSE;
1936         gboolean extra_only = FALSE;
1937         gint width = 0;
1938         gint height = 0;
1939         gint sim = 0;
1940         time_t file_date;
1941
1942         if (!sd->search_file_list) return FALSE;
1943
1944         if (sd->img_cd)
1945                 {
1946                 /* on end of a CacheData load, skip recomparing non-extra match types */
1947                 extra_only = TRUE;
1948                 match = FALSE;
1949                 }
1950         else
1951                 {
1952                 sd->search_total++;
1953                 }
1954
1955         fd = static_cast<FileData *>(sd->search_file_list->data);
1956
1957         if (match && sd->match_name_enable && sd->search_name)
1958                 {
1959                 tested = TRUE;
1960                 match = FALSE;
1961
1962                 if (!sd->search_name_symbolic_link || (sd->search_name_symbolic_link && islink(fd->path)))
1963                         {
1964                         if (sd->match_name == SEARCH_MATCH_NAME_EQUAL)
1965                                 {
1966                                 if (sd->search_name_match_case)
1967                                         {
1968                                         match = (strcmp(fd->name, sd->search_name) == 0);
1969                                         }
1970                                 else
1971                                         {
1972                                         match = (g_ascii_strcasecmp(fd->name, sd->search_name) == 0);
1973                                         }
1974                                 }
1975                         else if (sd->match_name == SEARCH_MATCH_NAME_CONTAINS || sd->match_name == SEARCH_MATCH_PATH_CONTAINS)
1976                                 {
1977                                 const gchar *fd_name_or_path;
1978                                 if (sd->match_name == SEARCH_MATCH_NAME_CONTAINS)
1979                                         {
1980                                         fd_name_or_path = fd->name;
1981                                         }
1982                                 else
1983                                         {
1984                                         fd_name_or_path = fd->path;
1985                                         }
1986                                 if (sd->search_name_match_case)
1987                                         {
1988                                         match = g_regex_match(sd->search_name_regex, fd_name_or_path, static_cast<GRegexMatchFlags>(0), nullptr);
1989                                         }
1990                                 else
1991                                         {
1992                                         /* sd->search_name is converted in search_start() */
1993                                         gchar *haystack = g_utf8_strdown(fd_name_or_path, -1);
1994                                         match = g_regex_match(sd->search_name_regex, haystack, static_cast<GRegexMatchFlags>(0), nullptr);
1995                                         g_free(haystack);
1996                                         }
1997                                 }
1998                         }
1999                 }
2000
2001         if (match && sd->match_size_enable)
2002                 {
2003                 tested = TRUE;
2004                 match = FALSE;
2005
2006                 if (sd->match_size == SEARCH_MATCH_EQUAL)
2007                         {
2008                         match = (fd->size == sd->search_size);
2009                         }
2010                 else if (sd->match_size == SEARCH_MATCH_UNDER)
2011                         {
2012                         match = (fd->size < sd->search_size);
2013                         }
2014                 else if (sd->match_size == SEARCH_MATCH_OVER)
2015                         {
2016                         match = (fd->size > sd->search_size);
2017                         }
2018                 else if (sd->match_size == SEARCH_MATCH_BETWEEN)
2019                         {
2020                         match = MATCH_IS_BETWEEN(fd->size, sd->search_size, sd->search_size_end);
2021                         }
2022                 }
2023
2024         if (match && sd->match_date_enable)
2025                 {
2026                 tested = TRUE;
2027                 match = FALSE;
2028
2029                 if (g_strcmp0(gtk_combo_box_text_get_active_text(
2030                                                 GTK_COMBO_BOX_TEXT(sd->date_type)), _("Changed")) == 0)
2031                         {
2032                         file_date = fd->cdate;
2033                         }
2034                 else if (g_strcmp0(gtk_combo_box_text_get_active_text(
2035                                                 GTK_COMBO_BOX_TEXT(sd->date_type)), _("Original")) == 0)
2036                         {
2037                         read_exif_time_data(fd);
2038                         file_date = fd->exifdate;
2039                         }
2040                 else if (g_strcmp0(gtk_combo_box_text_get_active_text(
2041                                                 GTK_COMBO_BOX_TEXT(sd->date_type)), _("Digitized")) == 0)
2042                         {
2043                         read_exif_time_digitized_data(fd);
2044                         file_date = fd->exifdate_digitized;
2045                         }
2046                 else
2047                         {
2048                         file_date = fd->date;
2049                         }
2050
2051                 if (sd->match_date == SEARCH_MATCH_EQUAL)
2052                         {
2053                         struct tm *lt;
2054
2055                         lt = localtime(&file_date);
2056                         match = (lt &&
2057                                  lt->tm_year == sd->search_date_y - 1900 &&
2058                                  lt->tm_mon == sd->search_date_m - 1 &&
2059                                  lt->tm_mday == sd->search_date_d);
2060                         }
2061                 else if (sd->match_date == SEARCH_MATCH_UNDER)
2062                         {
2063                         match = (file_date < convert_dmy_to_time(sd->search_date_d, sd->search_date_m, sd->search_date_y));
2064                         }
2065                 else if (sd->match_date == SEARCH_MATCH_OVER)
2066                         {
2067                         match = (file_date > convert_dmy_to_time(sd->search_date_d, sd->search_date_m, sd->search_date_y) + 60 * 60 * 24 - 1);
2068                         }
2069                 else if (sd->match_date == SEARCH_MATCH_BETWEEN)
2070                         {
2071                         time_t a = convert_dmy_to_time(sd->search_date_d, sd->search_date_m, sd->search_date_y);
2072                         time_t b = convert_dmy_to_time(sd->search_date_end_d, sd->search_date_end_m, sd->search_date_end_y);
2073
2074                         if (b >= a)
2075                                 {
2076                                 b += 60 * 60 * 24 - 1;
2077                                 }
2078                         else
2079                                 {
2080                                 a += 60 * 60 * 24 - 1;
2081                                 }
2082                         match = MATCH_IS_BETWEEN(file_date, a, b);
2083                         }
2084                 }
2085
2086         if (match && sd->match_keywords_enable && sd->search_keyword_list)
2087                 {
2088                 GList *list;
2089
2090                 tested = TRUE;
2091                 match = FALSE;
2092
2093                 list = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
2094
2095                 if (list)
2096                         {
2097                         GList *needle;
2098                         GList *haystack;
2099
2100                         if (sd->match_keywords == SEARCH_MATCH_ALL)
2101                                 {
2102                                 gboolean found = TRUE;
2103
2104                                 needle = sd->search_keyword_list;
2105                                 while (needle && found)
2106                                         {
2107                                         found = FALSE;
2108                                         haystack = list;
2109                                         while (haystack && !found)
2110                                                 {
2111                                                 found = (g_ascii_strcasecmp(static_cast<gchar *>(needle->data),
2112                                                                     static_cast<gchar *>(haystack->data)) == 0);
2113                                                 haystack = haystack->next;
2114                                                 }
2115                                         needle = needle->next;
2116                                         }
2117
2118                                 match = found;
2119                                 }
2120                         else if (sd->match_keywords == SEARCH_MATCH_ANY)
2121                                 {
2122                                 gboolean found = FALSE;
2123
2124                                 needle = sd->search_keyword_list;
2125                                 while (needle && !found)
2126                                         {
2127                                         haystack = list;
2128                                         while (haystack && !found)
2129                                                 {
2130                                                 found = (g_ascii_strcasecmp(static_cast<gchar *>(needle->data),
2131                                                                     static_cast<gchar *>(haystack->data)) == 0);
2132                                                 haystack = haystack->next;
2133                                                 }
2134                                         needle = needle->next;
2135                                         }
2136
2137                                 match = found;
2138                                 }
2139                         else if (sd->match_keywords == SEARCH_MATCH_NONE)
2140                                 {
2141                                 gboolean found = FALSE;
2142
2143                                 needle = sd->search_keyword_list;
2144                                 while (needle && !found)
2145                                         {
2146                                         haystack = list;
2147                                         while (haystack && !found)
2148                                                 {
2149                                                 found = (g_ascii_strcasecmp(static_cast<gchar *>(needle->data),
2150                                                                     static_cast<gchar *>(haystack->data)) == 0);
2151                                                 haystack = haystack->next;
2152                                                 }
2153                                         needle = needle->next;
2154                                         }
2155
2156                                 match = !found;
2157                                 }
2158                         g_list_free_full(list, g_free);
2159                         }
2160                 else
2161                         {
2162                         match = (sd->match_keywords == SEARCH_MATCH_NONE);
2163                         }
2164                 }
2165
2166         if (match && sd->match_comment_enable && sd->search_comment && strlen(sd->search_comment))
2167                 {
2168                 gchar *comment;
2169
2170                 tested = TRUE;
2171                 match = FALSE;
2172
2173                 comment = metadata_read_string(fd, COMMENT_KEY, METADATA_PLAIN);
2174
2175                 if (comment)
2176                         {
2177                         if (!sd->search_comment_match_case)
2178                                 {
2179                                 gchar *tmp = g_utf8_strdown(comment, -1);
2180                                 g_free(comment);
2181                                 comment = tmp;
2182                                 }
2183
2184                         if (sd->match_comment == SEARCH_MATCH_CONTAINS)
2185                                 {
2186                                 match = g_regex_match(sd->search_comment_regex, comment, static_cast<GRegexMatchFlags>(0), nullptr);
2187                                 }
2188                         else if (sd->match_comment == SEARCH_MATCH_NONE)
2189                                 {
2190                                 match = !g_regex_match(sd->search_comment_regex, comment, static_cast<GRegexMatchFlags>(0), nullptr);
2191                                 }
2192                         g_free(comment);
2193                         }
2194                 else
2195                         {
2196                         match = (sd->match_comment == SEARCH_MATCH_NONE);
2197                         }
2198                 }
2199
2200         if (match && sd->match_rating_enable)
2201                 {
2202                 tested = TRUE;
2203                 match = FALSE;
2204                 gint rating;
2205
2206                 rating = metadata_read_int(fd, RATING_KEY, 0);
2207                 if (sd->match_rating == SEARCH_MATCH_EQUAL)
2208                         {
2209                         match = (rating == sd->search_rating);
2210                         }
2211                 else if (sd->match_rating == SEARCH_MATCH_UNDER)
2212                         {
2213                         match = (rating < sd->search_rating);
2214                         }
2215                 else if (sd->match_rating == SEARCH_MATCH_OVER)
2216                         {
2217                         match = (rating > sd->search_rating);
2218                         }
2219                 else if (sd->match_rating == SEARCH_MATCH_BETWEEN)
2220                         {
2221                         match = MATCH_IS_BETWEEN(rating, sd->search_rating, sd->search_rating_end);
2222                         }
2223                 }
2224
2225         if (match && sd->match_class_enable)
2226                 {
2227                 tested = TRUE;
2228                 match = FALSE;
2229                 FileFormatClass format_class;
2230                 FileFormatClass search_class;
2231
2232                 if (g_strcmp0(gtk_combo_box_text_get_active_text(
2233                                                 GTK_COMBO_BOX_TEXT(sd->class_type)), _("Image")) == 0)
2234                         {
2235                         search_class = FORMAT_CLASS_IMAGE;
2236                         }
2237                 else if (g_strcmp0(gtk_combo_box_text_get_active_text(
2238                                                 GTK_COMBO_BOX_TEXT(sd->class_type)), _("Raw Image")) == 0)
2239                         {
2240                         search_class = FORMAT_CLASS_RAWIMAGE;
2241                         }
2242                 else if (g_strcmp0(gtk_combo_box_text_get_active_text(
2243                                                 GTK_COMBO_BOX_TEXT(sd->class_type)), _("Video")) == 0)
2244                         {
2245                         search_class = FORMAT_CLASS_VIDEO;
2246                         }
2247                 else if (g_strcmp0(gtk_combo_box_text_get_active_text(
2248                                                 GTK_COMBO_BOX_TEXT(sd->class_type)), _("Document")) == 0)
2249                         {
2250                         search_class = FORMAT_CLASS_DOCUMENT;
2251                         }
2252                 else if (g_strcmp0(gtk_combo_box_text_get_active_text(
2253                                                 GTK_COMBO_BOX_TEXT(sd->class_type)), _("Metadata")) == 0)
2254                         {
2255                         search_class = FORMAT_CLASS_META;
2256                         }
2257                 else if (g_strcmp0(gtk_combo_box_text_get_active_text(
2258                                                 GTK_COMBO_BOX_TEXT(sd->class_type)), _("Unknown")) == 0)
2259                         {
2260                         search_class = FORMAT_CLASS_UNKNOWN;
2261                         }
2262                 else
2263                         {
2264                         search_class = FORMAT_CLASS_BROKEN;
2265                         }
2266
2267                 if (search_class != FORMAT_CLASS_BROKEN)
2268                         {
2269                         format_class = fd->format_class;
2270                         if (sd->match_class == SEARCH_MATCH_EQUAL)
2271                                 {
2272                                 match = (format_class == search_class);
2273                                 }
2274                         else if (sd->match_class == SEARCH_MATCH_NONE)
2275                                 {
2276                                 match = (format_class != search_class);
2277                                 }
2278                         }
2279                 else
2280                         {
2281                         if (fd->format_class == FORMAT_CLASS_IMAGE || fd->format_class == FORMAT_CLASS_RAWIMAGE || fd->format_class == FORMAT_CLASS_VIDEO || fd->format_class == FORMAT_CLASS_DOCUMENT)
2282                                 {
2283                                 sd->match_broken_enable = TRUE;
2284                                 match = TRUE;
2285                                 }
2286                         else
2287                                 {
2288                                 sd->match_broken_enable = FALSE;
2289                                 }
2290                         }
2291                 }
2292
2293         if (match && sd->match_marks_enable)
2294                 {
2295                 tested = TRUE;
2296                 match = FALSE;
2297                 gint search_marks = -1;
2298                 gint i = 0;
2299                 gchar *marks_string = nullptr;
2300
2301                 if (g_strcmp0(gtk_combo_box_text_get_active_text(
2302                                                 GTK_COMBO_BOX_TEXT(sd->marks_type)), _("Any mark")) == 0)
2303                         {
2304                         search_marks = -1;
2305                         }
2306                 else
2307                         {
2308                         for (i = 0; i < FILEDATA_MARKS_SIZE; i++)
2309                                 {
2310                                 marks_string = g_strdup_printf("%s%d", _("Mark "), i + 1);
2311                                 if (g_strcmp0(marks_string, options->marks_tooltips[i]) != 0)
2312                                         {
2313                                         g_free(marks_string);
2314                                         marks_string = g_strdup_printf("%s%d %s", _("Mark "), i + 1,
2315                                                                                                         options->marks_tooltips[i]);
2316                                         }
2317
2318                                 if (g_strcmp0(gtk_combo_box_text_get_active_text(
2319                                                                 GTK_COMBO_BOX_TEXT(sd->marks_type)),
2320                                                                 marks_string) == 0)
2321                                         {
2322                                         search_marks = 1 << i;
2323                                         }
2324                                 g_free(marks_string);
2325                                 marks_string = nullptr;
2326                                 }
2327                         }
2328
2329                 if (sd->match_marks == SEARCH_MATCH_EQUAL)
2330                         {
2331                         match = (fd->marks & search_marks);
2332                         }
2333                 else
2334                         {
2335                         if (search_marks == -1)
2336                                 {
2337                                 match = fd->marks ? FALSE : TRUE;
2338                                 }
2339                         else
2340                                 {
2341                                 match = (fd->marks & search_marks) ? FALSE : TRUE;
2342                                 }
2343                         }
2344                 }
2345
2346         if (match && sd->match_gps_enable)
2347                 {
2348                 /* Calculate the distance the image is from the specified origin.
2349                 * This is a standard algorithm. A simplified one may be faster.
2350                 */
2351                 #define RADIANS  0.0174532925
2352                 #define KM_EARTH_RADIUS 6371
2353                 #define MILES_EARTH_RADIUS 3959
2354                 #define NAUTICAL_MILES_EARTH_RADIUS 3440
2355
2356                 gdouble latitude, longitude, range, conversion;
2357
2358                 if (g_strcmp0(gtk_combo_box_text_get_active_text(
2359                                                 GTK_COMBO_BOX_TEXT(sd->units_gps)), _("km")) == 0)
2360                         {
2361                         conversion = KM_EARTH_RADIUS;
2362                         }
2363                 else if (g_strcmp0(gtk_combo_box_text_get_active_text(
2364                                                 GTK_COMBO_BOX_TEXT(sd->units_gps)), _("miles")) == 0)
2365                         {
2366                         conversion = MILES_EARTH_RADIUS;
2367                         }
2368                 else
2369                         {
2370                         conversion = NAUTICAL_MILES_EARTH_RADIUS;
2371                         }
2372
2373                 tested = TRUE;
2374                 match = FALSE;
2375
2376                 latitude = metadata_read_GPS_coord(fd, "Xmp.exif.GPSLatitude", 1000);
2377                 longitude = metadata_read_GPS_coord(fd, "Xmp.exif.GPSLongitude", 1000);
2378                 if (latitude != 1000 && longitude != 1000)
2379                         {
2380                         range = conversion * acos(sin(latitude * RADIANS) *
2381                                                 sin(sd->search_lat * RADIANS) + cos(latitude * RADIANS) *
2382                                                 cos(sd->search_lat * RADIANS) * cos((sd->search_lon -
2383                                                 longitude) * RADIANS));
2384                         if (sd->match_gps == SEARCH_MATCH_UNDER)
2385                                 {
2386                                 if (sd->search_gps >= range)
2387                                         match = TRUE;
2388                                 }
2389                         else if (sd->match_gps == SEARCH_MATCH_OVER)
2390                                 {
2391                                 if (sd->search_gps < range)
2392                                         match = TRUE;
2393                                 }
2394                         }
2395                 else if (sd->match_gps == SEARCH_MATCH_NONE)
2396                         {
2397                         match = TRUE;
2398                         }
2399                 }
2400
2401         if ((match || extra_only) && (sd->match_dimensions_enable || sd->match_similarity_enable || sd->match_broken_enable))
2402                 {
2403                 tested = TRUE;
2404
2405                 if (search_file_do_extra(sd, fd, &match, &width, &height, &sim))
2406                         {
2407                         sd->search_buffer_count += SEARCH_BUFFER_MATCH_LOAD;
2408                         return TRUE;
2409                         }
2410                 }
2411
2412         sd->search_file_list = g_list_remove(sd->search_file_list, fd);
2413
2414         if (tested && match)
2415                 {
2416                 auto mfd = g_new(MatchFileData, 1);
2417                 mfd->fd = fd;
2418
2419                 mfd->width = width;
2420                 mfd->height = height;
2421                 mfd->rank = sim;
2422
2423                 sd->search_buffer_list = g_list_prepend(sd->search_buffer_list, mfd);
2424                 sd->search_buffer_count += SEARCH_BUFFER_MATCH_HIT;
2425                 sd->search_count++;
2426                 search_progress_update(sd, TRUE, -1.0);
2427                 }
2428         else
2429                 {
2430                 file_data_unref(fd);
2431                 sd->search_buffer_count += SEARCH_BUFFER_MATCH_MISS;
2432                 }
2433
2434         return FALSE;
2435 }
2436
2437 static gboolean search_step_cb(gpointer data)
2438 {
2439         auto sd = static_cast<SearchData *>(data);
2440         FileData *fd;
2441
2442         if (sd->search_buffer_count > SEARCH_BUFFER_FLUSH_SIZE)
2443                 {
2444                 search_buffer_flush(sd);
2445                 search_progress_update(sd, TRUE, -1.0);
2446                 }
2447
2448         if (sd->search_file_list)
2449                 {
2450                 if (search_file_next(sd))
2451                         {
2452                         sd->search_idle_id = 0;
2453                         return G_SOURCE_REMOVE;
2454                         }
2455                 return G_SOURCE_CONTINUE;
2456                 }
2457
2458         if (!sd->search_file_list && !sd->search_folder_list)
2459                 {
2460                 sd->search_idle_id = 0;
2461
2462                 search_stop(sd);
2463                 search_result_thumb_step(sd);
2464
2465                 return G_SOURCE_REMOVE;
2466                 }
2467
2468         fd = static_cast<FileData *>(sd->search_folder_list->data);
2469
2470         if (g_list_find(sd->search_done_list, fd) == nullptr)
2471                 {
2472                 GList *list = nullptr;
2473                 GList *dlist = nullptr;
2474                 gboolean success = FALSE;
2475
2476                 sd->search_done_list = g_list_prepend(sd->search_done_list, fd);
2477
2478                 if (sd->search_type == SEARCH_MATCH_NONE)
2479                         {
2480                         success = filelist_read(fd, &list, &dlist);
2481                         }
2482                 else if (sd->search_type == SEARCH_MATCH_ALL &&
2483                          sd->search_dir_fd &&
2484                          strlen(fd->path) >= strlen(sd->search_dir_fd->path))
2485                         {
2486                         const gchar *path;
2487
2488                         path = fd->path + strlen(sd->search_dir_fd->path);
2489                         if (path != fd->path)
2490                                 {
2491                                 FileData *dir_fd = file_data_new_dir(path);
2492                                 success = filelist_read(dir_fd, &list, nullptr);
2493                                 file_data_unref(dir_fd);
2494                                 }
2495                         success |= filelist_read(fd, nullptr, &dlist);
2496                         if (success)
2497                                 {
2498                                 GList *work;
2499
2500                                 work = list;
2501                                 while (work)
2502                                         {
2503                                         FileData *fdp;
2504                                         GList *link;
2505                                         gchar *meta_path;
2506
2507                                         fdp = static_cast<FileData *>(work->data);
2508                                         link = work;
2509                                         work = work->next;
2510
2511                                         meta_path = cache_find_location(CACHE_TYPE_METADATA, fdp->path);
2512                                         if (!meta_path)
2513                                                 {
2514                                                 list = g_list_delete_link(list, link);
2515                                                 file_data_unref(fdp);
2516                                                 }
2517                                         g_free(meta_path);
2518                                         }
2519                                 }
2520                         }
2521
2522                 if (success)
2523                         {
2524                         list = filelist_sort(list, SORT_NAME, TRUE, TRUE);
2525                         sd->search_file_list = list;
2526
2527                         if (sd->search_path_recurse)
2528                                 {
2529                                 dlist = filelist_sort(dlist, SORT_NAME, TRUE, TRUE);
2530                                 sd->search_folder_list = g_list_concat(dlist, sd->search_folder_list);
2531                                 }
2532                         else
2533                                 {
2534                                 filelist_free(dlist);
2535                                 }
2536                         }
2537                 }
2538         else
2539                 {
2540                 sd->search_folder_list = g_list_remove(sd->search_folder_list, fd);
2541                 sd->search_done_list = g_list_remove(sd->search_done_list, fd);
2542                 file_data_unref(fd);
2543                 }
2544
2545         return G_SOURCE_CONTINUE;
2546 }
2547
2548 static void search_similarity_load_done_cb(ImageLoader *, gpointer data)
2549 {
2550         auto sd = static_cast<SearchData *>(data);
2551         search_file_load_process(sd, sd->search_similarity_cd);
2552 }
2553
2554 static void search_start(SearchData *sd)
2555 {
2556         GError *error = nullptr;
2557
2558         search_stop(sd);
2559         search_result_clear(sd);
2560
2561         if (sd->search_dir_fd)
2562                 {
2563                 sd->search_folder_list = g_list_prepend(sd->search_folder_list, file_data_ref(sd->search_dir_fd));
2564                 }
2565
2566         if (!sd->search_name_match_case)
2567                 {
2568                 /* convert to lowercase here, so that this is only done once per search */
2569                 gchar *tmp = g_utf8_strdown(sd->search_name, -1);
2570                 g_free(sd->search_name);
2571                 sd->search_name = tmp;
2572                 }
2573
2574         if(sd->search_name_regex)
2575                 {
2576                 g_regex_unref(sd->search_name_regex);
2577                 }
2578
2579         sd->search_name_regex = g_regex_new(sd->search_name, static_cast<GRegexCompileFlags>(0), static_cast<GRegexMatchFlags>(0), &error);
2580         if (error)
2581                 {
2582                 log_printf("Error: could not compile regular expression %s\n%s\n", sd->search_name, error->message);
2583                 g_error_free(error);
2584                 error = nullptr;
2585                 sd->search_name_regex = g_regex_new("", static_cast<GRegexCompileFlags>(0), static_cast<GRegexMatchFlags>(0), nullptr);
2586                 }
2587
2588         if (!sd->search_comment_match_case)
2589                 {
2590                 /* convert to lowercase here, so that this is only done once per search */
2591                 gchar *tmp = g_utf8_strdown(sd->search_comment, -1);
2592                 g_free(sd->search_comment);
2593                 sd->search_comment = tmp;
2594                 }
2595
2596         if(sd->search_comment_regex)
2597                 {
2598                 g_regex_unref(sd->search_comment_regex);
2599                 }
2600
2601         sd->search_comment_regex = g_regex_new(sd->search_comment, static_cast<GRegexCompileFlags>(0), static_cast<GRegexMatchFlags>(0), &error);
2602         if (error)
2603                 {
2604                 log_printf("Error: could not compile regular expression %s\n%s\n", sd->search_comment, error->message);
2605                 g_error_free(error);
2606                 error = nullptr;
2607                 sd->search_comment_regex = g_regex_new("", static_cast<GRegexCompileFlags>(0), static_cast<GRegexMatchFlags>(0), nullptr);
2608                 }
2609
2610         sd->search_count = 0;
2611         sd->search_total = 0;
2612
2613         gtk_widget_set_sensitive(sd->box_search, FALSE);
2614         gtk_spinner_start(GTK_SPINNER(sd->spinner));
2615         gtk_widget_set_sensitive(sd->button_start, FALSE);
2616         gtk_widget_set_sensitive(sd->button_stop, TRUE);
2617         search_progress_update(sd, TRUE, -1.0);
2618
2619         if (sd->match_similarity_enable &&
2620             !sd->search_similarity_cd &&
2621             isfile(sd->search_similarity_path))
2622                 {
2623                 gchar *cd_path;
2624
2625                 cd_path = cache_find_location(CACHE_TYPE_SIM, sd->search_similarity_path);
2626                 if (cd_path && filetime(sd->search_similarity_path) == filetime(cd_path))
2627                         {
2628                         sd->search_similarity_cd = cache_sim_data_load(cd_path);
2629                         }
2630                 g_free(cd_path);
2631
2632                 if (!sd->search_similarity_cd || !sd->search_similarity_cd->similarity)
2633                         {
2634                         if (!sd->search_similarity_cd)
2635                                 {
2636                                 sd->search_similarity_cd = cache_sim_data_new();
2637                                 }
2638
2639                         sd->img_loader = image_loader_new(file_data_new_group(sd->search_similarity_path));
2640                         g_signal_connect(G_OBJECT(sd->img_loader), "error", (GCallback)search_similarity_load_done_cb, sd);
2641                         g_signal_connect(G_OBJECT(sd->img_loader), "done", (GCallback)search_similarity_load_done_cb, sd);
2642                         if (image_loader_start(sd->img_loader))
2643                                 {
2644                                 return;
2645                                 }
2646                         image_loader_free(sd->img_loader);
2647                         sd->img_loader = nullptr;
2648                         }
2649
2650                 }
2651
2652         sd->search_idle_id = g_idle_add(search_step_cb, sd);
2653 }
2654
2655 static void search_start_cb(GtkWidget *, gpointer data)
2656 {
2657         auto sd = static_cast<SearchData *>(data);
2658         gchar *collection;
2659         gchar *entry_text;
2660         gchar *path;
2661         GDateTime *date;
2662         GtkTreeViewColumn *column;
2663
2664         if (sd->search_folder_list)
2665                 {
2666                 search_stop(sd);
2667                 search_result_thumb_step(sd);
2668                 return;
2669                 }
2670
2671         if (sd->match_name_enable) history_combo_append_history(sd->entry_name, nullptr);
2672         g_free(sd->search_name);
2673         sd->search_name = g_strdup(gq_gtk_entry_get_text(GTK_ENTRY(sd->entry_name)));
2674
2675         /* XXX */
2676         g_free(sd->search_comment);
2677         sd->search_comment = g_strdup(gq_gtk_entry_get_text(GTK_ENTRY(sd->entry_comment)));
2678
2679         g_free(sd->search_similarity_path);
2680         sd->search_similarity_path = g_strdup(gq_gtk_entry_get_text(GTK_ENTRY(sd->entry_similarity)));
2681         if (sd->match_similarity_enable)
2682                 {
2683                 if (!isfile(sd->search_similarity_path))
2684                         {
2685                         file_util_warning_dialog(_("File not found"),
2686                                                  _("Please enter an existing file for image content."),
2687                                                  GQ_ICON_DIALOG_WARNING, sd->window);
2688                         return;
2689                         }
2690                 tab_completion_append_to_history(sd->entry_similarity, sd->search_similarity_path);
2691                 }
2692
2693         /* Check the coordinate entry.
2694         * If the result is not sensible, it should get blocked.
2695         */
2696         if (sd->match_gps_enable)
2697                 {
2698                 if (sd->match_gps != SEARCH_MATCH_NONE)
2699                         {
2700                         entry_text = decode_geo_parameters(gq_gtk_entry_get_text(
2701                                                                                 GTK_ENTRY(sd->entry_gps_coord)));
2702
2703                         sd->search_lat = 1000;
2704                         sd->search_lon = 1000;
2705                         sscanf(entry_text," %lf  %lf ", &sd->search_lat, &sd->search_lon );
2706                         if (entry_text == nullptr || g_strstr_len(entry_text, -1, "Error") ||
2707                                                 sd->search_lat < -90 || sd->search_lat > 90 ||
2708                                                 sd->search_lon < -180 || sd->search_lon > 180)
2709                                 {
2710                                 file_util_warning_dialog(_(
2711                                                 "Entry does not contain a valid lat/long value"),
2712                                                         entry_text, GQ_ICON_DIALOG_WARNING, sd->window);
2713                                 return;
2714                                 }
2715                         g_free(entry_text);
2716                         }
2717                 }
2718
2719         g_list_free_full(sd->search_keyword_list, g_free);
2720         sd->search_keyword_list = keyword_list_pull(sd->entry_keywords);
2721
2722         date = date_selection_get(sd->date_sel);
2723         sd->search_date_d = g_date_time_get_day_of_month(date);
2724         sd->search_date_m = g_date_time_get_month(date);
2725         sd->search_date_y = g_date_time_get_year(date);
2726         g_date_time_unref(date);
2727
2728         date = date_selection_get(sd->date_sel_end);
2729         sd->search_date_end_d = g_date_time_get_day_of_month(date);
2730         sd->search_date_end_m = g_date_time_get_month(date);
2731         sd->search_date_end_y = g_date_time_get_year(date);
2732         g_date_time_unref(date);
2733
2734         column = gtk_tree_view_get_column(GTK_TREE_VIEW(sd->result_view), SEARCH_COLUMN_DIMENSIONS - 1);
2735         gtk_tree_view_column_set_visible(column, sd->match_dimensions_enable);
2736
2737         column = gtk_tree_view_get_column(GTK_TREE_VIEW(sd->result_view), SEARCH_COLUMN_RANK - 1);
2738         gtk_tree_view_column_set_visible(column, sd->match_similarity_enable);
2739         if (!sd->match_similarity_enable)
2740                 {
2741                 GtkTreeSortable *sortable;
2742                 gint id;
2743                 GtkSortType order;
2744
2745                 sortable = GTK_TREE_SORTABLE(gtk_tree_view_get_model(GTK_TREE_VIEW(sd->result_view)));
2746                 if (gtk_tree_sortable_get_sort_column_id(sortable, &id, &order) &&
2747                     id == SEARCH_COLUMN_RANK)
2748                         {
2749                         gtk_tree_sortable_set_sort_column_id(sortable, SEARCH_COLUMN_PATH, GTK_SORT_ASCENDING);
2750                         }
2751                 }
2752
2753         if (sd->search_type == SEARCH_MATCH_NONE)
2754                 {
2755                 /* search path */
2756
2757                 path = remove_trailing_slash(gq_gtk_entry_get_text(GTK_ENTRY(sd->path_entry)));
2758                 if (isdir(path))
2759                         {
2760                         file_data_unref(sd->search_dir_fd);
2761                         sd->search_dir_fd = file_data_new_dir(path);
2762
2763                         tab_completion_append_to_history(sd->path_entry, sd->search_dir_fd->path);
2764
2765                         search_start(sd);
2766                         }
2767                 else
2768                         {
2769                         file_util_warning_dialog(_("Folder not found"),
2770                                                  _("Please enter an existing folder to search."),
2771                                                  GQ_ICON_DIALOG_WARNING, sd->window);
2772                         }
2773
2774                 g_free(path);
2775                 }
2776         else if (sd->search_type == SEARCH_MATCH_ALL)
2777                 {
2778                 /* search metadata */
2779                 file_data_unref(sd->search_dir_fd);
2780                 sd->search_dir_fd = file_data_new_dir(get_metadata_cache_dir());
2781                 search_start(sd);
2782                 }
2783         else if (sd->search_type == SEARCH_MATCH_CONTAINS)
2784                 {
2785                 /* search current result list */
2786                 GList *list;
2787
2788                 list = search_result_refine_list(sd);
2789
2790                 file_data_unref(sd->search_dir_fd);
2791                 sd->search_dir_fd = nullptr;
2792
2793                 search_start(sd);
2794
2795                 sd->search_file_list = g_list_concat(sd->search_file_list, list);
2796                 }
2797         else if (sd->search_type == SEARCH_MATCH_COLLECTION)
2798                 {
2799                 collection = g_strdup(gq_gtk_entry_get_text(GTK_ENTRY(sd->collection_entry)));
2800
2801                 if (is_collection(collection))
2802                         {
2803                         GList *list = nullptr;
2804
2805                         list = collection_contents_fd(collection);
2806
2807                         file_data_unref(sd->search_dir_fd);
2808                         sd->search_dir_fd = nullptr;
2809
2810                         search_start(sd);
2811
2812                         sd->search_file_list = g_list_concat(sd->search_file_list, list);
2813                         }
2814                 else
2815                         {
2816                         file_util_warning_dialog(_("Collection not found"), _("Please enter an existing collection name."), GQ_ICON_DIALOG_WARNING, sd->window);
2817                         }
2818                 g_free(collection);
2819                 }
2820 }
2821
2822 /*
2823  *-------------------------------------------------------------------
2824  * window construct
2825  *-------------------------------------------------------------------
2826  */
2827
2828 enum {
2829         MENU_CHOICE_COLUMN_NAME = 0,
2830         MENU_CHOICE_COLUMN_VALUE
2831 };
2832
2833 static void search_thumb_toggle_cb(GtkWidget *button, gpointer data)
2834 {
2835         auto sd = static_cast<SearchData *>(data);
2836
2837         search_result_thumb_enable(sd, gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button)));
2838 }
2839
2840 static gint sort_matchdata_dimensions(MatchFileData *a, MatchFileData *b)
2841 {
2842         gint sa;
2843         gint sb;
2844
2845         sa = a->width * a->height;
2846         sb = b->width * b->height;
2847
2848         if (sa > sb) return 1;
2849         if (sa < sb) return -1;
2850         return 0;
2851 }
2852
2853 static gint search_result_sort_cb(GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, gpointer data)
2854 {
2855         gint n = GPOINTER_TO_INT(data);
2856         MatchFileData *fda;
2857         MatchFileData *fdb;
2858
2859         gtk_tree_model_get(model, a, SEARCH_COLUMN_POINTER, &fda, -1);
2860         gtk_tree_model_get(model, b, SEARCH_COLUMN_POINTER, &fdb, -1);
2861
2862         if (!fda || !fdb) return 0;
2863
2864         switch (n)
2865                 {
2866                 case SEARCH_COLUMN_RANK:
2867                         if ((fda)->rank > (fdb)->rank) return 1;
2868                         if ((fda)->rank < (fdb)->rank) return -1;
2869                         return 0;
2870                         break;
2871                 case SEARCH_COLUMN_NAME:
2872                         if (options->file_sort.case_sensitive)
2873                                 return strcmp(fda->fd->collate_key_name, fdb->fd->collate_key_name);
2874                         else
2875                                 return strcmp(fda->fd->collate_key_name_nocase, fdb->fd->collate_key_name_nocase);
2876                         break;
2877                 case SEARCH_COLUMN_SIZE:
2878                         if (fda->fd->size > fdb->fd->size) return 1;
2879                         if (fda->fd->size < fdb->fd->size) return -1;
2880                         return 0;
2881                         break;
2882                 case SEARCH_COLUMN_DATE:
2883                         if (fda->fd->date > fdb->fd->date) return 1;
2884                         if (fda->fd->date < fdb->fd->date) return -1;
2885                         return 0;
2886                         break;
2887                 case SEARCH_COLUMN_DIMENSIONS:
2888                         return sort_matchdata_dimensions(fda, fdb);
2889                         break;
2890                 case SEARCH_COLUMN_PATH:
2891                         return utf8_compare(fda->fd->path, fdb->fd->path, TRUE);
2892                         break;
2893                 default:
2894                         break;
2895                 }
2896
2897         return 0;
2898 }
2899
2900 static void search_result_add_column(SearchData * sd, gint n, const gchar *title, gboolean image, gboolean right_justify)
2901 {
2902         GtkTreeViewColumn *column;
2903         GtkCellRenderer *renderer;
2904
2905         column = gtk_tree_view_column_new();
2906         gtk_tree_view_column_set_title(column, title);
2907         gtk_tree_view_column_set_min_width(column, 4);
2908
2909         if (n != SEARCH_COLUMN_THUMB) gtk_tree_view_column_set_resizable(column, TRUE);
2910
2911         if (!image)
2912                 {
2913                 gtk_tree_view_column_set_sizing(column, GTK_TREE_VIEW_COLUMN_GROW_ONLY);
2914                 renderer = gtk_cell_renderer_text_new();
2915                 if (right_justify) g_object_set(G_OBJECT(renderer), "xalign", 1.0, NULL);
2916                 gtk_tree_view_column_pack_start(column, renderer, TRUE);
2917                 gtk_tree_view_column_add_attribute(column, renderer, "text", n);
2918
2919                 gtk_tree_view_column_set_sort_column_id(column, n);
2920                 }
2921         else
2922                 {
2923                 gtk_tree_view_column_set_sizing(column, GTK_TREE_VIEW_COLUMN_FIXED);
2924                 renderer = gtk_cell_renderer_pixbuf_new();
2925                 cell_renderer_height_override(renderer);
2926                 gtk_tree_view_column_pack_start(column, renderer, TRUE);
2927                 gtk_tree_view_column_add_attribute(column, renderer, "pixbuf", n);
2928                 }
2929
2930         gtk_tree_view_append_column(GTK_TREE_VIEW(sd->result_view), column);
2931 }
2932
2933 static void menu_choice_set_visible(GtkWidget *widget, gboolean visible)
2934 {
2935         if (visible)
2936                 {
2937                 if (!gtk_widget_get_visible(widget)) gtk_widget_show(widget);
2938                 }
2939         else
2940                 {
2941                 if (gtk_widget_get_visible(widget)) gtk_widget_hide(widget);
2942                 }
2943 }
2944
2945 static gboolean menu_choice_get_match_type(GtkWidget *combo, MatchType *type)
2946 {
2947         GtkTreeModel *store;
2948         GtkTreeIter iter;
2949
2950         store = gtk_combo_box_get_model(GTK_COMBO_BOX(combo));
2951         if (!gtk_combo_box_get_active_iter(GTK_COMBO_BOX(combo), &iter)) return FALSE;
2952         gtk_tree_model_get(store, &iter, MENU_CHOICE_COLUMN_VALUE, type, -1);
2953         return TRUE;
2954 }
2955
2956 static void menu_choice_path_cb(GtkWidget *combo, gpointer data)
2957 {
2958         auto sd = static_cast<SearchData *>(data);
2959
2960         if (!menu_choice_get_match_type(combo, &sd->search_type)) return;
2961
2962         menu_choice_set_visible(gtk_widget_get_parent(sd->check_recurse),
2963                                 (sd->search_type == SEARCH_MATCH_NONE));
2964         menu_choice_set_visible(sd->collection, (sd->search_type == SEARCH_MATCH_COLLECTION));
2965 }
2966
2967 static void menu_choice_name_cb(GtkWidget *combo, gpointer data)
2968 {
2969         auto sd = static_cast<SearchData *>(data);
2970
2971         if (!menu_choice_get_match_type(combo, &sd->match_name)) return;
2972 }
2973
2974 static void menu_choice_size_cb(GtkWidget *combo, gpointer data)
2975 {
2976         auto sd = static_cast<SearchData *>(data);
2977
2978         if (!menu_choice_get_match_type(combo, &sd->match_size)) return;
2979
2980         menu_choice_set_visible(gtk_widget_get_parent(sd->spin_size_end),
2981                                 (sd->match_size == SEARCH_MATCH_BETWEEN));
2982 }
2983
2984 static void menu_choice_rating_cb(GtkWidget *combo, gpointer data)
2985 {
2986         auto sd = static_cast<SearchData *>(data);
2987
2988         if (!menu_choice_get_match_type(combo, &sd->match_rating)) return;
2989
2990         menu_choice_set_visible(gtk_widget_get_parent(sd->spin_rating_end),
2991                                 (sd->match_rating == SEARCH_MATCH_BETWEEN));
2992 }
2993
2994 static void menu_choice_class_cb(GtkWidget *combo, gpointer data)
2995 {
2996         auto sd = static_cast<SearchData *>(data);
2997
2998         if (!menu_choice_get_match_type(combo, &sd->match_class)) return;
2999 }
3000
3001 static void menu_choice_marks_cb(GtkWidget *combo, gpointer data)
3002 {
3003         auto sd = static_cast<SearchData *>(data);
3004
3005         if (!menu_choice_get_match_type(combo, &sd->match_marks)) return;
3006 }
3007
3008 static void menu_choice_date_cb(GtkWidget *combo, gpointer data)
3009 {
3010         auto sd = static_cast<SearchData *>(data);
3011
3012         if (!menu_choice_get_match_type(combo, &sd->match_date)) return;
3013
3014         menu_choice_set_visible(gtk_widget_get_parent(sd->date_sel_end),
3015                                 (sd->match_date == SEARCH_MATCH_BETWEEN));
3016 }
3017
3018 static void menu_choice_dimensions_cb(GtkWidget *combo, gpointer data)
3019 {
3020         auto sd = static_cast<SearchData *>(data);
3021
3022         if (!menu_choice_get_match_type(combo, &sd->match_dimensions)) return;
3023
3024         menu_choice_set_visible(gtk_widget_get_parent(sd->spin_width_end),
3025                                 (sd->match_dimensions == SEARCH_MATCH_BETWEEN));
3026 }
3027
3028 static void menu_choice_keyword_cb(GtkWidget *combo, gpointer data)
3029 {
3030         auto sd = static_cast<SearchData *>(data);
3031
3032         if (!menu_choice_get_match_type(combo, &sd->match_keywords)) return;
3033 }
3034
3035 static void menu_choice_comment_cb(GtkWidget *combo, gpointer data)
3036 {
3037         auto sd = static_cast<SearchData *>(data);
3038
3039         if (!menu_choice_get_match_type(combo, &sd->match_comment)) return;
3040 }
3041
3042 static void menu_choice_spin_cb(GtkAdjustment *adjustment, gpointer data)
3043 {
3044         auto value = static_cast<gint *>(data);
3045
3046         *value = static_cast<gint>(gtk_adjustment_get_value(adjustment));
3047 }
3048
3049 static void menu_choice_gps_cb(GtkWidget *combo, gpointer data)
3050 {
3051         auto sd = static_cast<SearchData *>(data);
3052
3053         if (!menu_choice_get_match_type(combo, &sd->match_gps)) return;
3054
3055         menu_choice_set_visible(gtk_widget_get_parent(sd->spin_gps),
3056                                         (sd->match_gps != SEARCH_MATCH_NONE));
3057 }
3058
3059 static GtkWidget *menu_spin(GtkWidget *box, gdouble min, gdouble max, gint value,
3060                             GCallback func, gpointer data)
3061 {
3062         GtkWidget *spin;
3063         GtkAdjustment *adj;
3064
3065         spin = gtk_spin_button_new_with_range(min, max, 1);
3066         gtk_spin_button_set_value(GTK_SPIN_BUTTON(spin), static_cast<gdouble>(value));
3067         adj = gtk_spin_button_get_adjustment(GTK_SPIN_BUTTON(spin));
3068         if (func) g_signal_connect(G_OBJECT(adj), "value_changed",
3069                                    G_CALLBACK(func), data);
3070         gq_gtk_box_pack_start(GTK_BOX(box), spin, FALSE, FALSE, 0);
3071         gtk_widget_show(spin);
3072
3073         return spin;
3074 }
3075
3076 static void menu_choice_check_cb(GtkWidget *button, gpointer data)
3077 {
3078         auto widget = static_cast<GtkWidget *>(data);
3079         gboolean active;
3080         gboolean *value;
3081
3082         active = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button));
3083         gtk_widget_set_sensitive(widget, active);
3084
3085         value = static_cast<gboolean *>(g_object_get_data(G_OBJECT(button), "check_var"));
3086         if (value) *value = active;
3087 }
3088
3089 static GtkWidget *menu_choice_menu(const MatchList *items, gint item_count,
3090                                    GCallback func, gpointer data)
3091 {
3092         GtkWidget *combo;
3093         GtkCellRenderer *renderer;
3094         GtkListStore *store;
3095         gint i;
3096
3097         store = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_INT);
3098         combo = gtk_combo_box_new_with_model(GTK_TREE_MODEL(store));
3099         g_object_unref(store);
3100
3101         renderer = gtk_cell_renderer_text_new();
3102         gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(combo), renderer, TRUE);
3103         gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(combo), renderer,
3104                                        "text", MENU_CHOICE_COLUMN_NAME, NULL);
3105
3106         for (i = 0; i < item_count; i++)
3107                 {
3108                 GtkTreeIter iter;
3109
3110                 gtk_list_store_append(store, &iter);
3111                 gtk_list_store_set(store, &iter, MENU_CHOICE_COLUMN_NAME, _(items[i].text),
3112                                                  MENU_CHOICE_COLUMN_VALUE, items[i].type, -1);
3113                 }
3114
3115         gtk_combo_box_set_active(GTK_COMBO_BOX(combo), 0);
3116
3117         if (func) g_signal_connect(G_OBJECT(combo), "changed",
3118                                    G_CALLBACK(func), data);
3119
3120         return combo;
3121 }
3122
3123 static GtkWidget *menu_choice(GtkWidget *box, GtkWidget **check, GtkWidget **menu,
3124                               const gchar *text, gboolean *value,
3125                               const MatchList *items, gint item_count,
3126                               GCallback func, gpointer data)
3127 {
3128         GtkWidget *base_box;
3129         GtkWidget *hbox;
3130         GtkWidget *button;
3131         GtkWidget *option;
3132
3133         base_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, PREF_PAD_GAP);
3134         gq_gtk_box_pack_start(GTK_BOX(box), base_box, FALSE, FALSE, 0);
3135         gtk_widget_show(base_box);
3136
3137         button = gtk_check_button_new();
3138         if (value) gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(button), *value);
3139         gq_gtk_box_pack_start(GTK_BOX(base_box), button, FALSE, FALSE, 0);
3140         gtk_widget_show(button);
3141         if (check) *check = button;
3142         if (value) g_object_set_data(G_OBJECT(button), "check_var", value);
3143
3144         hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, PREF_PAD_SPACE);
3145         gq_gtk_box_pack_start(GTK_BOX(base_box), hbox, TRUE, TRUE, 0);
3146         gtk_widget_show(hbox);
3147
3148         g_signal_connect(G_OBJECT(button), "toggled",
3149                          G_CALLBACK(menu_choice_check_cb), hbox);
3150         gtk_widget_set_sensitive(hbox, (value) ? *value : FALSE);
3151
3152         pref_label_new(hbox, text);
3153
3154         if (!items && !menu) return hbox;
3155
3156         option = menu_choice_menu(items, item_count, func, data);
3157         gq_gtk_box_pack_start(GTK_BOX(hbox), option, FALSE, FALSE, 0);
3158         gtk_widget_show(option);
3159         if (menu) *menu = option;
3160
3161         return hbox;
3162 }
3163
3164 static void search_window_get_geometry(SearchData *sd)
3165 {
3166         GdkWindow *window;
3167         LayoutWindow *lw = nullptr;
3168
3169         layout_valid(&lw);
3170
3171         if (!sd || !lw) return;
3172
3173         window = gtk_widget_get_window(sd->window);
3174         gdk_window_get_position(window, &lw->options.search_window.x, &lw->options.search_window.y);
3175         lw->options.search_window.w = gdk_window_get_width(window);
3176         lw->options.search_window.h = gdk_window_get_height(window);
3177 }
3178
3179 static void search_window_close(SearchData *sd)
3180 {
3181
3182         search_window_get_geometry(sd);
3183
3184         gq_gtk_widget_destroy(sd->window);
3185 }
3186
3187 static void search_window_close_cb(GtkWidget *, gpointer data)
3188 {
3189         auto sd = static_cast<SearchData *>(data);
3190
3191         search_window_close(sd);
3192 }
3193
3194 static void search_window_help_cb(GtkWidget *, gpointer)
3195 {
3196         help_window_show("GuideImageSearchSearch.html");
3197 }
3198
3199 static gboolean search_window_delete_cb(GtkWidget *, GdkEventAny *, gpointer data)
3200 {
3201         auto sd = static_cast<SearchData *>(data);
3202
3203         search_window_close(sd);
3204         return TRUE;
3205 }
3206
3207 static void search_window_destroy_cb(GtkWidget *, gpointer data)
3208 {
3209         auto sd = static_cast<SearchData *>(data);
3210
3211         search_window_list = g_list_remove(search_window_list, sd);
3212
3213         search_result_update_idle_cancel(sd);
3214
3215         mfd_list_free(sd->search_buffer_list);
3216         sd->search_buffer_list = nullptr;
3217
3218         search_stop(sd);
3219         search_result_clear(sd);
3220
3221         file_data_unref(sd->search_dir_fd);
3222
3223         g_free(sd->search_name);
3224         if(sd->search_name_regex)
3225                 {
3226                 g_regex_unref(sd->search_name_regex);
3227                 }
3228         g_free(sd->search_comment);
3229         if(sd->search_comment_regex)
3230                 {
3231                 g_regex_unref(sd->search_comment_regex);
3232                 }
3233         g_free(sd->search_similarity_path);
3234         g_list_free_full(sd->search_keyword_list, g_free);
3235
3236         file_data_unregister_notify_func(search_notify_cb, sd);
3237
3238         g_free(sd);
3239 }
3240
3241 static void select_collection_dialog_close_cb(FileDialog *fdlg, gpointer)
3242 {
3243         file_dialog_close(fdlg);
3244 }
3245
3246 static void select_collection_dialog_ok_cb(FileDialog *fdlg, gpointer data)
3247 {
3248         auto sd = static_cast<SearchData *>(data);
3249         gchar *path;
3250         gchar *path_noext;
3251         gchar *collection;
3252
3253         path = g_strdup(gq_gtk_entry_get_text(GTK_ENTRY(fdlg->entry)));
3254         path_noext = remove_extension_from_path(path);
3255         collection = g_path_get_basename(path_noext);
3256
3257         gq_gtk_entry_set_text(GTK_ENTRY(sd->collection_entry), collection);
3258         file_dialog_close(fdlg);
3259
3260         g_free(path);
3261         g_free(path_noext);
3262         g_free(collection);
3263 }
3264
3265 static void select_collection_clicked_cb(GtkWidget *, gpointer data)
3266 {
3267         auto sd = static_cast<SearchData *>(data);
3268         FileDialog *fdlg;
3269         const gchar *title;
3270         const gchar *btntext;
3271         gpointer btnfunc;
3272         const gchar *icon_name;
3273
3274         title = _("Select collection");
3275         btntext = nullptr;
3276         btnfunc = reinterpret_cast<void *>(select_collection_dialog_ok_cb);
3277         icon_name = GQ_ICON_OK;
3278
3279         fdlg = file_util_file_dlg(title, "dlg_collection", sd->window, select_collection_dialog_close_cb, sd);
3280
3281         generic_dialog_add_message(GENERIC_DIALOG(fdlg), nullptr, title, nullptr, FALSE);
3282         file_dialog_add_button(fdlg, icon_name, btntext, reinterpret_cast<void(*)(FileDialog *, gpointer)>(btnfunc), TRUE);
3283
3284         file_dialog_add_path_widgets(fdlg, get_collections_dir(), nullptr, "search_collection", GQ_COLLECTION_EXT, _("Collection Files"));
3285
3286         gtk_widget_show(GENERIC_DIALOG(fdlg)->dialog);
3287 }
3288
3289 void search_new(FileData *dir_fd, FileData *example_file)
3290 {
3291         GtkWidget *vbox;
3292         GtkWidget *hbox;
3293         GtkWidget *hbox2;
3294         GtkWidget *pad_box;
3295         GtkWidget *frame;
3296         GtkWidget *scrolled;
3297         GtkListStore *store;
3298         GtkTreeSortable *sortable;
3299         GtkTreeSelection *selection;
3300         GtkTreeViewColumn *column;
3301         GtkWidget *combo;
3302         GdkGeometry geometry;
3303         gint i;
3304         gchar *marks_string;
3305         LayoutWindow *lw = nullptr;
3306
3307         layout_valid(&lw);
3308
3309         auto sd = g_new0(SearchData, 1);
3310
3311         sd->search_dir_fd = file_data_ref(dir_fd);
3312         sd->search_path_recurse = TRUE;
3313         sd->search_size = 0;
3314         sd->search_width = 640;
3315         sd->search_height = 480;
3316         sd->search_width_end = 1024;
3317         sd->search_height_end = 768;
3318
3319         sd->search_type = SEARCH_MATCH_NONE;
3320
3321         sd->match_name = SEARCH_MATCH_NAME_CONTAINS;
3322         sd->match_size = SEARCH_MATCH_EQUAL;
3323         sd->match_date = SEARCH_MATCH_EQUAL;
3324         sd->match_dimensions = SEARCH_MATCH_EQUAL;
3325         sd->match_keywords = SEARCH_MATCH_ALL;
3326         sd->match_comment = SEARCH_MATCH_CONTAINS;
3327         sd->match_rating = SEARCH_MATCH_EQUAL;
3328         sd->match_class = SEARCH_MATCH_EQUAL;
3329         sd->match_marks = SEARCH_MATCH_EQUAL;
3330
3331         sd->match_name_enable = TRUE;
3332
3333         sd->search_similarity = 95;
3334
3335         sd->search_gps = 1;
3336         sd->match_gps = SEARCH_MATCH_NONE;
3337
3338         if (example_file)
3339                 {
3340                 sd->search_similarity_path = g_strdup(example_file->path);
3341                 }
3342
3343         sd->window = window_new("search", nullptr, nullptr, _("Image search"));
3344         DEBUG_NAME(sd->window);
3345
3346         gtk_window_set_resizable(GTK_WINDOW(sd->window), TRUE);
3347
3348         geometry.min_width = DEFAULT_MINIMAL_WINDOW_SIZE;
3349         geometry.min_height = DEFAULT_MINIMAL_WINDOW_SIZE;
3350         geometry.base_width = DEF_SEARCH_WIDTH;
3351         geometry.base_height = DEF_SEARCH_HEIGHT;
3352         gtk_window_set_geometry_hints(GTK_WINDOW(sd->window), nullptr, &geometry,
3353                                       static_cast<GdkWindowHints>(GDK_HINT_MIN_SIZE | GDK_HINT_BASE_SIZE));
3354
3355         if (lw && options->save_window_positions)
3356                 {
3357                 gtk_window_set_default_size(GTK_WINDOW(sd->window), lw->options.search_window.w, lw->options.search_window.h);
3358                 gq_gtk_window_move(GTK_WINDOW(sd->window), lw->options.search_window.x, lw->options.search_window.y);
3359                 }
3360         else
3361                 {
3362                 gtk_window_set_default_size(GTK_WINDOW(sd->window), DEF_SEARCH_WIDTH, DEF_SEARCH_HEIGHT);
3363                 }
3364
3365         g_signal_connect(G_OBJECT(sd->window), "delete_event",
3366                          G_CALLBACK(search_window_delete_cb), sd);
3367         g_signal_connect(G_OBJECT(sd->window), "destroy",
3368                          G_CALLBACK(search_window_destroy_cb), sd);
3369
3370         g_signal_connect(G_OBJECT(sd->window), "key_press_event",
3371                          G_CALLBACK(search_window_keypress_cb), sd);
3372
3373         vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, PREF_PAD_GAP);
3374         gtk_container_set_border_width(GTK_CONTAINER(vbox), PREF_PAD_GAP);
3375         gq_gtk_container_add(GTK_WIDGET(sd->window), vbox);
3376         gtk_widget_show(vbox);
3377
3378         sd->box_search = pref_box_new(vbox, FALSE, GTK_ORIENTATION_VERTICAL, PREF_PAD_GAP);
3379
3380         hbox = pref_box_new(sd->box_search, FALSE, GTK_ORIENTATION_HORIZONTAL, PREF_PAD_SPACE);
3381
3382         pref_label_new(hbox, _("Search:"));
3383
3384         sd->menu_path = menu_choice_menu(text_search_menu_path, sizeof(text_search_menu_path) / sizeof(MatchList),
3385                                          G_CALLBACK(menu_choice_path_cb), sd);
3386         gq_gtk_box_pack_start(GTK_BOX(hbox), sd->menu_path, FALSE, FALSE, 0);
3387         gtk_widget_show(sd->menu_path);
3388
3389         hbox2 = pref_box_new(hbox, TRUE, GTK_ORIENTATION_HORIZONTAL, PREF_PAD_SPACE);
3390         combo = tab_completion_new_with_history(&sd->path_entry, sd->search_dir_fd->path,
3391                                                 "search_path", -1,
3392                                                 nullptr, nullptr);
3393         tab_completion_add_select_button(sd->path_entry, nullptr, TRUE);
3394         gq_gtk_box_pack_start(GTK_BOX(hbox2), combo, TRUE, TRUE, 0);
3395         gtk_widget_show(combo);
3396         sd->check_recurse = pref_checkbox_new_int(hbox2, _("Recurse"),
3397                                                   sd->search_path_recurse, &sd->search_path_recurse);
3398
3399         sd->collection = pref_box_new(hbox, TRUE, GTK_ORIENTATION_HORIZONTAL, PREF_PAD_SPACE);
3400         sd->collection_entry = gtk_entry_new();
3401         gq_gtk_entry_set_text(GTK_ENTRY(sd->collection_entry), "");
3402         gq_gtk_box_pack_start(GTK_BOX(sd->collection), sd->collection_entry, TRUE, TRUE, 0);
3403         gtk_widget_show(sd->collection_entry);
3404
3405         sd->fd_button = gtk_button_new_with_label("...");
3406         g_signal_connect(G_OBJECT(sd->fd_button), "clicked", G_CALLBACK(select_collection_clicked_cb), sd);
3407         gq_gtk_box_pack_start(GTK_BOX(sd->collection), sd->fd_button, FALSE, FALSE, 0);
3408         gtk_widget_show(sd->fd_button);
3409
3410         gtk_widget_hide(sd->collection);
3411
3412         /* Search for file name */
3413         hbox = menu_choice(sd->box_search, &sd->check_name, &sd->menu_name,
3414                            _("File"), &sd->match_name_enable,
3415                            text_search_menu_name, sizeof(text_search_menu_name) / sizeof(MatchList),
3416                            G_CALLBACK(menu_choice_name_cb), sd);
3417         combo = history_combo_new(&sd->entry_name, "", "search_name", -1);
3418         gq_gtk_box_pack_start(GTK_BOX(hbox), combo, TRUE, TRUE, 0);
3419         gtk_widget_show(combo);
3420         pref_checkbox_new_int(hbox, _("Match case"),
3421                               sd->search_name_match_case, &sd->search_name_match_case);
3422         pref_checkbox_new_int(hbox, _("Symbolic link"), sd->search_name_symbolic_link, &sd->search_name_symbolic_link);
3423         gtk_widget_set_tooltip_text(GTK_WIDGET(combo), "When set to \"contains\" or \"path contains\", this field uses Perl Compatible Regular Expressions.\ne.g. use \n.*\\.jpg\n and not \n*.jpg\n\nSee the Help file.");
3424
3425         /* Search for file size */
3426         hbox = menu_choice(sd->box_search, &sd->check_size, &sd->menu_size,
3427                            _("File size is"), &sd->match_size_enable,
3428                            text_search_menu_size, sizeof(text_search_menu_size) / sizeof(MatchList),
3429                            G_CALLBACK(menu_choice_size_cb), sd);
3430         sd->spin_size = menu_spin(hbox, 0, 1024*1024*1024, sd->search_size,
3431                                   G_CALLBACK(menu_choice_spin_cb), &sd->search_size);
3432         hbox2 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, PREF_PAD_SPACE);
3433         gq_gtk_box_pack_start(GTK_BOX(hbox), hbox2, FALSE, FALSE, 0);
3434         pref_label_new(hbox2, _("and"));
3435         sd->spin_size_end = menu_spin(hbox2, 0, 1024*1024*1024, sd->search_size_end,
3436                                       G_CALLBACK(menu_choice_spin_cb), &sd->search_size_end);
3437
3438         /* Search for file date */
3439         hbox = menu_choice(sd->box_search, &sd->check_date, &sd->menu_date,
3440                            _("File date is"), &sd->match_date_enable,
3441                            text_search_menu_date, sizeof(text_search_menu_date) / sizeof(MatchList),
3442                            G_CALLBACK(menu_choice_date_cb), sd);
3443
3444         sd->date_sel = date_selection_new();
3445         date_selection_time_set(sd->date_sel, time(nullptr));
3446         gq_gtk_box_pack_start(GTK_BOX(hbox), sd->date_sel, FALSE, FALSE, 0);
3447         gtk_widget_show(sd->date_sel);
3448
3449         hbox2 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, PREF_PAD_SPACE);
3450         gq_gtk_box_pack_start(GTK_BOX(hbox), hbox2, FALSE, FALSE, 0);
3451         pref_label_new(hbox2, _("and"));
3452         sd->date_sel_end = date_selection_new();
3453         date_selection_time_set(sd->date_sel_end, time(nullptr));
3454         gq_gtk_box_pack_start(GTK_BOX(hbox2), sd->date_sel_end, FALSE, FALSE, 0);
3455         gtk_widget_show(sd->date_sel_end);
3456
3457         sd->date_type = gtk_combo_box_text_new();
3458         gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(sd->date_type), _("Modified"));
3459         gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(sd->date_type), _("Status Changed"));
3460         gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(sd->date_type), _("Original"));
3461         gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(sd->date_type), _("Digitized"));
3462         gq_gtk_box_pack_start(GTK_BOX(hbox), sd->date_type, FALSE, FALSE, 0);
3463         gtk_combo_box_set_active(GTK_COMBO_BOX(sd->date_type), 0);
3464         gtk_widget_set_tooltip_text(sd->date_type, "Modified (mtime)\nStatus Changed (ctime)\nOriginal (Exif.Photo.DateTimeOriginal)\nDigitized (Exif.Photo.DateTimeDigitized)");
3465         gtk_widget_show(sd->date_type);
3466
3467         /* Search for image dimensions */
3468         hbox = menu_choice(sd->box_search, &sd->check_dimensions, &sd->menu_dimensions,
3469                            _("Image dimensions are"), &sd->match_dimensions_enable,
3470                            text_search_menu_size, sizeof(text_search_menu_size) / sizeof(MatchList),
3471                            G_CALLBACK(menu_choice_dimensions_cb), sd);
3472         pad_box = pref_box_new(hbox, FALSE, GTK_ORIENTATION_HORIZONTAL, 2);
3473         sd->spin_width = menu_spin(pad_box, 0, 1000000, sd->search_width,
3474                                    G_CALLBACK(menu_choice_spin_cb), &sd->search_width);
3475         pref_label_new(pad_box, "x");
3476         sd->spin_height = menu_spin(pad_box, 0, 1000000, sd->search_height,
3477                                     G_CALLBACK(menu_choice_spin_cb), &sd->search_height);
3478         hbox2 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 2);
3479         gq_gtk_box_pack_start(GTK_BOX(hbox), hbox2, FALSE, FALSE, 0);
3480         pref_label_new(hbox2, _("and"));
3481         pref_spacer(hbox2, PREF_PAD_SPACE - 2*2);
3482         sd->spin_width_end = menu_spin(hbox2, 0, 1000000, sd->search_width_end,
3483                                        G_CALLBACK(menu_choice_spin_cb), &sd->search_width_end);
3484         pref_label_new(hbox2, "x");
3485         sd->spin_height_end = menu_spin(hbox2, 0, 1000000, sd->search_height_end,
3486                                         G_CALLBACK(menu_choice_spin_cb), &sd->search_height_end);
3487
3488         /* Search for image similarity */
3489         hbox = menu_choice(sd->box_search, &sd->check_similarity, nullptr,
3490                            _("Image content is"), &sd->match_similarity_enable,
3491                            nullptr, 0, nullptr, sd);
3492         sd->spin_similarity = menu_spin(hbox, 80, 100, sd->search_similarity,
3493                                         G_CALLBACK(menu_choice_spin_cb), &sd->search_similarity);
3494
3495         /* xgettext:no-c-format */
3496         pref_label_new(hbox, _("% similar to"));
3497
3498         combo = tab_completion_new_with_history(&sd->entry_similarity,
3499                                                 (sd->search_similarity_path) ? sd->search_similarity_path : "",
3500                                                 "search_similarity_path", -1, nullptr, nullptr);
3501         tab_completion_add_select_button(sd->entry_similarity, nullptr, FALSE);
3502         gq_gtk_box_pack_start(GTK_BOX(hbox), combo, TRUE, TRUE, 0);
3503         gtk_widget_show(combo);
3504         pref_checkbox_new_int(hbox, _("Ignore rotation"),
3505                                 options->rot_invariant_sim, &options->rot_invariant_sim);
3506
3507         /* Search for image keywords */
3508         hbox = menu_choice(sd->box_search, &sd->check_keywords, &sd->menu_keywords,
3509                            _("Keywords"), &sd->match_keywords_enable,
3510                            text_search_menu_keyword, sizeof(text_search_menu_keyword) / sizeof(MatchList),
3511                            G_CALLBACK(menu_choice_keyword_cb), sd);
3512         sd->entry_keywords = gtk_entry_new();
3513         gq_gtk_box_pack_start(GTK_BOX(hbox), sd->entry_keywords, TRUE, TRUE, 0);
3514         gtk_widget_set_sensitive(sd->entry_keywords, sd->match_keywords_enable);
3515         g_signal_connect(G_OBJECT(sd->check_keywords), "toggled",
3516                          G_CALLBACK(menu_choice_check_cb), sd->entry_keywords);
3517         gtk_widget_show(sd->entry_keywords);
3518
3519         /* Search for image comment */
3520         hbox = menu_choice(sd->box_search, &sd->check_comment, &sd->menu_comment,
3521                         _("Comment"), &sd->match_comment_enable,
3522                         text_search_menu_comment, sizeof(text_search_menu_comment) / sizeof(MatchList),
3523                         G_CALLBACK(menu_choice_comment_cb), sd);
3524         sd->entry_comment = gtk_entry_new();
3525         gq_gtk_box_pack_start(GTK_BOX(hbox), sd->entry_comment, TRUE, TRUE, 0);
3526         gtk_widget_set_sensitive(sd->entry_comment, sd->match_comment_enable);
3527         g_signal_connect(G_OBJECT(sd->check_comment), "toggled",
3528                         G_CALLBACK(menu_choice_check_cb), sd->entry_comment);
3529         gtk_widget_show(sd->entry_comment);
3530         pref_checkbox_new_int(hbox, _("Match case"),
3531                               sd->search_comment_match_case, &sd->search_comment_match_case);
3532         gtk_widget_set_tooltip_text(GTK_WIDGET(sd->entry_comment), "This field uses Perl Compatible Regular Expressions.\ne.g. use \nabc.*ghk\n and not \nabc*ghk\n\nSee the Help file.");
3533
3534         /* Search for image rating */
3535         hbox = menu_choice(sd->box_search, &sd->check_rating, &sd->menu_rating,
3536                            _("Image rating is"), &sd->match_rating_enable,
3537                            text_search_menu_rating, sizeof(text_search_menu_rating) / sizeof(MatchList),
3538                            G_CALLBACK(menu_choice_rating_cb), sd);
3539         sd->spin_size = menu_spin(hbox, -1, 5, sd->search_rating,
3540                                   G_CALLBACK(menu_choice_spin_cb), &sd->search_rating);
3541         hbox2 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, PREF_PAD_SPACE);
3542         gq_gtk_box_pack_start(GTK_BOX(hbox), hbox2, FALSE, FALSE, 0);
3543         pref_label_new(hbox2, _("and"));
3544         sd->spin_rating_end = menu_spin(hbox2, -1, 5, sd->search_rating_end,
3545                                       G_CALLBACK(menu_choice_spin_cb), &sd->search_rating_end);
3546
3547         /* Search for images within a specified range of a lat/long coordinate
3548         */
3549         hbox = menu_choice(sd->box_search, &sd->check_gps, &sd->menu_gps,
3550                            _("Image is"), &sd->match_gps_enable,
3551                            text_search_menu_gps, sizeof(text_search_menu_gps) / sizeof(MatchList),
3552                            G_CALLBACK(menu_choice_gps_cb), sd);
3553
3554         hbox2 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, PREF_PAD_SPACE);
3555         gq_gtk_box_pack_start(GTK_BOX(hbox), hbox2, FALSE, FALSE, 0);
3556         sd->spin_gps = menu_spin(hbox2, 1, 9999, sd->search_gps,
3557                                                                    G_CALLBACK(menu_choice_spin_cb), &sd->search_gps);
3558
3559         sd->units_gps = gtk_combo_box_text_new();
3560         gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(sd->units_gps), _("km"));
3561         gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(sd->units_gps), _("miles"));
3562         gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(sd->units_gps), _("n.m."));
3563         gq_gtk_box_pack_start(GTK_BOX(hbox2), sd->units_gps, FALSE, FALSE, 0);
3564         gtk_combo_box_set_active(GTK_COMBO_BOX(sd->units_gps), 0);
3565         gtk_widget_set_tooltip_text(sd->units_gps, "kilometres, miles or nautical miles");
3566         gtk_widget_show(sd->units_gps);
3567
3568         pref_label_new(hbox2, _("from"));
3569
3570         sd->entry_gps_coord = gtk_entry_new();
3571         gtk_editable_set_editable(GTK_EDITABLE(sd->entry_gps_coord), TRUE);
3572         gtk_widget_set_has_tooltip(sd->entry_gps_coord, TRUE);
3573         gtk_widget_set_tooltip_text(sd->entry_gps_coord, _("Enter a coordinate in the form:\n89.123 179.456\nor drag-and-drop a geo-coded image\nor left-click on the map and paste\nor cut-and-paste or drag-and-drop\nan internet search URL\nSee the Help file"));
3574         gq_gtk_box_pack_start(GTK_BOX(hbox2), sd->entry_gps_coord, TRUE, TRUE, 0);
3575         gtk_widget_set_sensitive(sd->entry_gps_coord, TRUE);
3576
3577         gtk_widget_show(sd->entry_gps_coord);
3578
3579         /* Search for image class */
3580         hbox = menu_choice(sd->box_search, &sd->check_class, &sd->menu_class,
3581                            _("Image class"), &sd->match_class_enable,
3582                            text_search_menu_class, sizeof(text_search_menu_class) / sizeof(MatchList),
3583                            G_CALLBACK(menu_choice_class_cb), sd);
3584
3585         sd->class_type = gtk_combo_box_text_new();
3586         gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(sd->class_type), _("Image"));
3587         gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(sd->class_type), _("Raw Image"));
3588         gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(sd->class_type), _("Video"));
3589         gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(sd->class_type), _("Document"));
3590         gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(sd->class_type), _("Metadata"));
3591         gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(sd->class_type), _("Unknown"));
3592         gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(sd->class_type), _("Broken"));
3593         gq_gtk_box_pack_start(GTK_BOX(hbox), sd->class_type, FALSE, FALSE, 0);
3594         gtk_combo_box_set_active(GTK_COMBO_BOX(sd->class_type), 0);
3595         gtk_widget_show(sd->class_type);
3596
3597         /* Search for image marks */
3598         hbox = menu_choice(sd->box_search, &sd->check_class, &sd->menu_marks,
3599                            _("Marks"), &sd->match_marks_enable,
3600                            text_search_menu_marks, sizeof(text_search_menu_marks) / sizeof(MatchList),
3601                            G_CALLBACK(menu_choice_marks_cb), sd);
3602
3603         sd->marks_type = gtk_combo_box_text_new();
3604         gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(sd->marks_type), _("Any mark"));
3605         for (i = 0; i < FILEDATA_MARKS_SIZE; i++)
3606                 {
3607                 marks_string = g_strdup_printf("%s%d", _("Mark "), i + 1);
3608                 if (g_strcmp0(marks_string, options->marks_tooltips[i]) != 0)
3609                         {
3610                         g_free(marks_string);
3611                         marks_string = g_strdup_printf("%s%d %s", _("Mark "), i + 1,
3612                                                                                         options->marks_tooltips[i]);
3613                         gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(sd->marks_type), marks_string);
3614                         }
3615                 else
3616                         {
3617                         gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(sd->marks_type), marks_string);
3618                         }
3619                 g_free(marks_string);
3620                 }
3621         gq_gtk_box_pack_start(GTK_BOX(hbox), sd->marks_type, FALSE, FALSE, 0);
3622         gtk_combo_box_set_active(GTK_COMBO_BOX(sd->marks_type), 0);
3623         gtk_widget_show(sd->marks_type);
3624
3625         /* Done the types of searches */
3626
3627         scrolled = gq_gtk_scrolled_window_new(nullptr, nullptr);
3628         gq_gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled), GTK_SHADOW_IN);
3629         gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled),
3630                                        GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
3631         gq_gtk_box_pack_start(GTK_BOX(vbox), scrolled, TRUE, TRUE, 0);
3632         gtk_widget_show(scrolled);
3633
3634         store = gtk_list_store_new(8, G_TYPE_POINTER, G_TYPE_INT, GDK_TYPE_PIXBUF,
3635                                    G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING,
3636                                    G_TYPE_STRING, G_TYPE_STRING);
3637
3638         /* set up sorting */
3639         sortable = GTK_TREE_SORTABLE(store);
3640         gtk_tree_sortable_set_sort_func(sortable, SEARCH_COLUMN_RANK, search_result_sort_cb,
3641                                   GINT_TO_POINTER(SEARCH_COLUMN_RANK), nullptr);
3642         gtk_tree_sortable_set_sort_func(sortable, SEARCH_COLUMN_NAME, search_result_sort_cb,
3643                                   GINT_TO_POINTER(SEARCH_COLUMN_NAME), nullptr);
3644         gtk_tree_sortable_set_sort_func(sortable, SEARCH_COLUMN_SIZE, search_result_sort_cb,
3645                                   GINT_TO_POINTER(SEARCH_COLUMN_SIZE), nullptr);
3646         gtk_tree_sortable_set_sort_func(sortable, SEARCH_COLUMN_DATE, search_result_sort_cb,
3647                                   GINT_TO_POINTER(SEARCH_COLUMN_DATE), nullptr);
3648         gtk_tree_sortable_set_sort_func(sortable, SEARCH_COLUMN_DIMENSIONS, search_result_sort_cb,
3649                                   GINT_TO_POINTER(SEARCH_COLUMN_DIMENSIONS), nullptr);
3650         gtk_tree_sortable_set_sort_func(sortable, SEARCH_COLUMN_PATH, search_result_sort_cb,
3651                                   GINT_TO_POINTER(SEARCH_COLUMN_PATH), nullptr);
3652
3653 #if 0
3654         /* by default, search results are unsorted until user selects a sort column - for speed,
3655          * using sort slows search speed by an order of magnitude with 1000's of results :-/
3656          */
3657         gtk_tree_sortable_set_sort_column_id(sortable, SEARCH_COLUMN_PATH, GTK_SORT_ASCENDING);
3658 #endif
3659
3660         sd->result_view = gtk_tree_view_new_with_model(GTK_TREE_MODEL(store));
3661         g_object_unref(store);
3662         gq_gtk_container_add(GTK_WIDGET(scrolled), sd->result_view);
3663         gtk_widget_show(sd->result_view);
3664
3665         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(sd->result_view));
3666         gtk_tree_selection_set_mode(GTK_TREE_SELECTION(selection), GTK_SELECTION_MULTIPLE);
3667         gtk_tree_selection_set_select_function(selection, search_result_select_cb, sd, nullptr);
3668
3669         gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(sd->result_view), TRUE);
3670         gtk_tree_view_set_enable_search(GTK_TREE_VIEW(sd->result_view), FALSE);
3671
3672         search_result_add_column(sd, SEARCH_COLUMN_RANK, _("Rank"), FALSE, FALSE);
3673         search_result_add_column(sd, SEARCH_COLUMN_THUMB, _("Thumb"), TRUE, FALSE);
3674         search_result_add_column(sd, SEARCH_COLUMN_NAME, _("Name"), FALSE, FALSE);
3675         search_result_add_column(sd, SEARCH_COLUMN_SIZE, _("Size"), FALSE, TRUE);
3676         search_result_add_column(sd, SEARCH_COLUMN_DATE, _("Date"), FALSE, TRUE);
3677         search_result_add_column(sd, SEARCH_COLUMN_DIMENSIONS, _("Dimensions"), FALSE, FALSE);
3678         search_result_add_column(sd, SEARCH_COLUMN_PATH, _("Path"), FALSE, FALSE);
3679
3680         search_dnd_init(sd);
3681
3682         g_signal_connect(G_OBJECT(sd->result_view), "button_press_event",
3683                          G_CALLBACK(search_result_press_cb), sd);
3684         g_signal_connect(G_OBJECT(sd->result_view), "button_release_event",
3685                          G_CALLBACK(search_result_release_cb), sd);
3686         g_signal_connect(G_OBJECT(sd->result_view), "key_press_event",
3687                          G_CALLBACK(search_result_keypress_cb), sd);
3688
3689         hbox = pref_box_new(vbox, FALSE, GTK_ORIENTATION_HORIZONTAL, 0);
3690
3691         sd->button_thumbs = pref_checkbox_new(hbox, _("Thumbnails"), FALSE,
3692                                               G_CALLBACK(search_thumb_toggle_cb), sd);
3693         gtk_widget_set_tooltip_text(GTK_WIDGET(sd->button_thumbs), "Ctrl-T");
3694
3695         frame = gtk_frame_new(nullptr);
3696         DEBUG_NAME(frame);
3697         gq_gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_IN);
3698         gq_gtk_box_pack_start(GTK_BOX(hbox), frame, TRUE, TRUE, PREF_PAD_SPACE);
3699         gtk_widget_show(frame);
3700
3701         sd->label_status = gtk_label_new("");
3702         gtk_widget_set_size_request(sd->label_status, 50, -1);
3703         gq_gtk_container_add(GTK_WIDGET(frame), sd->label_status);
3704         gtk_widget_show(sd->label_status);
3705
3706         sd->label_progress = gtk_progress_bar_new();
3707         gtk_widget_set_size_request(sd->label_progress, 50, -1);
3708
3709         gtk_progress_bar_set_text(GTK_PROGRESS_BAR(sd->label_progress), "");
3710         gtk_progress_bar_set_show_text(GTK_PROGRESS_BAR(sd->label_progress), TRUE);
3711
3712         gq_gtk_box_pack_start(GTK_BOX(hbox), sd->label_progress, TRUE, TRUE, 0);
3713         gtk_widget_show(sd->label_progress);
3714
3715         sd->spinner = gtk_spinner_new();
3716         gq_gtk_box_pack_start(GTK_BOX(hbox), sd->spinner, FALSE, FALSE, 0);
3717         gtk_widget_show(sd->spinner);
3718
3719         sd->button_help = pref_button_new(hbox, GQ_ICON_HELP, _("Help"), G_CALLBACK(search_window_help_cb), sd);
3720         gtk_widget_set_tooltip_text(GTK_WIDGET(sd->button_help), "F1");
3721         gtk_widget_set_sensitive(sd->button_help, TRUE);
3722         pref_spacer(hbox, PREF_PAD_BUTTON_GAP);
3723         sd->button_start = pref_button_new(hbox, GQ_ICON_FIND, _("Find"),
3724                                            G_CALLBACK(search_start_cb), sd);
3725         gtk_widget_set_tooltip_text(GTK_WIDGET(sd->button_start), "Ctrl-Return");
3726         pref_spacer(hbox, PREF_PAD_BUTTON_GAP);
3727         sd->button_stop = pref_button_new(hbox, GQ_ICON_STOP, _("Stop"),
3728                                           G_CALLBACK(search_start_cb), sd);
3729         gtk_widget_set_tooltip_text(GTK_WIDGET(sd->button_stop), "Ctrl-Return");
3730         gtk_widget_set_sensitive(sd->button_stop, FALSE);
3731         pref_spacer(hbox, PREF_PAD_BUTTON_GAP);
3732         sd->button_close = pref_button_new(hbox, GQ_ICON_CLOSE, _("Close"), G_CALLBACK(search_window_close_cb), sd);
3733         gtk_widget_set_tooltip_text(GTK_WIDGET(sd->button_close), "Ctrl-W");
3734         gtk_widget_set_sensitive(sd->button_close, TRUE);
3735
3736         search_result_thumb_enable(sd, TRUE);
3737         search_result_thumb_enable(sd, FALSE);
3738         column = gtk_tree_view_get_column(GTK_TREE_VIEW(sd->result_view), SEARCH_COLUMN_RANK - 1);
3739         gtk_tree_view_column_set_visible(column, FALSE);
3740
3741         search_status_update(sd);
3742         search_progress_update(sd, FALSE, -1.0);
3743
3744         search_window_list = g_list_append(search_window_list, sd);
3745
3746         file_data_register_notify_func(search_notify_cb, sd, NOTIFY_PRIORITY_MEDIUM);
3747
3748         gtk_widget_show(sd->window);
3749 }
3750
3751 /*
3752  *-------------------------------------------------------------------
3753  * maintenance (move, delete, etc.)
3754  *-------------------------------------------------------------------
3755  */
3756
3757 static void search_result_change_path(SearchData *sd, FileData *fd)
3758 {
3759         GtkTreeModel *store;
3760         GtkTreeIter iter;
3761         gboolean valid;
3762
3763         store = gtk_tree_view_get_model(GTK_TREE_VIEW(sd->result_view));
3764         valid = gtk_tree_model_get_iter_first(store, &iter);
3765         while (valid)
3766                 {
3767                 GtkTreeIter current;
3768                 MatchFileData *mfd;
3769
3770                 current = iter;
3771                 valid = gtk_tree_model_iter_next(store, &iter);
3772
3773                 gtk_tree_model_get(store, &current, SEARCH_COLUMN_POINTER, &mfd, -1);
3774                 if (mfd->fd == fd)
3775                         {
3776                         if (fd->change && fd->change->dest)
3777                                 {
3778                                 gtk_list_store_set(GTK_LIST_STORE(store), &current,
3779                                                    SEARCH_COLUMN_NAME, mfd->fd->name,
3780                                                    SEARCH_COLUMN_PATH, mfd->fd->path, -1);
3781                                 }
3782                         else
3783                                 {
3784                                 search_result_remove_item(sd, mfd, &current);
3785                                 }
3786                         }
3787                 }
3788 }
3789
3790 static void search_notify_cb(FileData *fd, NotifyType type, gpointer data)
3791 {
3792         auto sd = static_cast<SearchData *>(data);
3793
3794         if (!(type & NOTIFY_CHANGE) || !fd->change) return;
3795
3796         DEBUG_1("Notify search: %s %04x", fd->path, type);
3797
3798         switch (fd->change->type)
3799                 {
3800                 case FILEDATA_CHANGE_MOVE:
3801                 case FILEDATA_CHANGE_RENAME:
3802                 case FILEDATA_CHANGE_DELETE:
3803                         search_result_change_path(sd, fd);
3804                         break;
3805                 case FILEDATA_CHANGE_COPY:
3806                 case FILEDATA_CHANGE_UNSPECIFIED:
3807                 case FILEDATA_CHANGE_WRITE_METADATA:
3808                         break;
3809                 }
3810 }
3811
3812 void mfd_list_free(GList *list)
3813 {
3814         GList *work;
3815
3816         work = list;
3817         while (work)
3818                 {
3819                 auto mfd = static_cast<MatchFileData *>(work->data);
3820                 file_data_unref((FileData *)mfd->fd);
3821                 work = work->next;
3822                 }
3823
3824         g_list_free(list);
3825 }
3826
3827 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */