clang-tidy: bugprone-integer-division
[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
1845                         image_loader_free(sd->img_loader);
1846                         sd->img_loader = nullptr;
1847                         }
1848                 }
1849
1850         if (sd->match_broken_enable)
1851                 {
1852                 tested = TRUE;
1853                 tmatch = FALSE;
1854                 if (sd->match_class == SEARCH_MATCH_EQUAL && sd->img_cd->width == -1)
1855                         {
1856                         tmatch = TRUE;
1857                         }
1858                 else if (sd->match_class == SEARCH_MATCH_NONE && sd->img_cd->width != -1)
1859                         {
1860                         tmatch = TRUE;
1861                         }
1862                 }
1863
1864         if (tmatch && sd->match_dimensions_enable && sd->img_cd->dimensions)
1865                 {
1866                 CacheData *cd = sd->img_cd;
1867
1868                 tmatch = FALSE;
1869                 tested = TRUE;
1870
1871                 if (sd->match_dimensions == SEARCH_MATCH_EQUAL)
1872                         {
1873                         tmatch = (cd->width == sd->search_width && cd->height == sd->search_height);
1874                         }
1875                 else if (sd->match_dimensions == SEARCH_MATCH_UNDER)
1876                         {
1877                         tmatch = (cd->width < sd->search_width && cd->height < sd->search_height);
1878                         }
1879                 else if (sd->match_dimensions == SEARCH_MATCH_OVER)
1880                         {
1881                         tmatch = (cd->width > sd->search_width && cd->height > sd->search_height);
1882                         }
1883                 else if (sd->match_dimensions == SEARCH_MATCH_BETWEEN)
1884                         {
1885                         tmatch = (MATCH_IS_BETWEEN(cd->width, sd->search_width, sd->search_width_end) &&
1886                                   MATCH_IS_BETWEEN(cd->height, sd->search_height, sd->search_height_end));
1887                         }
1888                 }
1889
1890         if (tmatch && sd->match_similarity_enable && sd->img_cd->similarity)
1891                 {
1892                 gdouble value = 0.0;
1893
1894                 tmatch = FALSE;
1895                 tested = TRUE;
1896
1897                 /** @FIXME implement similarity checking */
1898                 if (sd->search_similarity_cd && sd->search_similarity_cd->similarity)
1899                         {
1900                         gdouble result;
1901
1902                         result = image_sim_compare_fast(sd->search_similarity_cd->sim, sd->img_cd->sim,
1903                                                         static_cast<gdouble>(sd->search_similarity) / 100.0);
1904                         result *= 100.0;
1905                         if (result >= static_cast<gdouble>(sd->search_similarity))
1906                                 {
1907                                 tmatch = TRUE;
1908                                 value = static_cast<gint>(result);
1909                                 }
1910                         }
1911
1912                 if (simval) *simval = value;
1913                 }
1914
1915         if (sd->img_cd->dimensions)
1916                 {
1917                 if (width) *width = sd->img_cd->width;
1918                 if (height) *height = sd->img_cd->height;
1919                 }
1920
1921         cache_sim_data_free(sd->img_cd);
1922         sd->img_cd = nullptr;
1923
1924         *match = (tmatch && tested);
1925
1926         return FALSE;
1927 }
1928
1929 static gboolean search_file_next(SearchData *sd)
1930 {
1931         FileData *fd;
1932         gboolean match = TRUE;
1933         gboolean tested = FALSE;
1934         gboolean extra_only = FALSE;
1935         gint width = 0;
1936         gint height = 0;
1937         gint sim = 0;
1938         time_t file_date;
1939
1940         if (!sd->search_file_list) return FALSE;
1941
1942         if (sd->img_cd)
1943                 {
1944                 /* on end of a CacheData load, skip recomparing non-extra match types */
1945                 extra_only = TRUE;
1946                 match = FALSE;
1947                 }
1948         else
1949                 {
1950                 sd->search_total++;
1951                 }
1952
1953         fd = static_cast<FileData *>(sd->search_file_list->data);
1954
1955         if (match && sd->match_name_enable && sd->search_name)
1956                 {
1957                 tested = TRUE;
1958                 match = FALSE;
1959
1960                 if (!sd->search_name_symbolic_link || (sd->search_name_symbolic_link && islink(fd->path)))
1961                         {
1962                         if (sd->match_name == SEARCH_MATCH_NAME_EQUAL)
1963                                 {
1964                                 if (sd->search_name_match_case)
1965                                         {
1966                                         match = (strcmp(fd->name, sd->search_name) == 0);
1967                                         }
1968                                 else
1969                                         {
1970                                         match = (g_ascii_strcasecmp(fd->name, sd->search_name) == 0);
1971                                         }
1972                                 }
1973                         else if (sd->match_name == SEARCH_MATCH_NAME_CONTAINS || sd->match_name == SEARCH_MATCH_PATH_CONTAINS)
1974                                 {
1975                                 const gchar *fd_name_or_path;
1976                                 if (sd->match_name == SEARCH_MATCH_NAME_CONTAINS)
1977                                         {
1978                                         fd_name_or_path = fd->name;
1979                                         }
1980                                 else
1981                                         {
1982                                         fd_name_or_path = fd->path;
1983                                         }
1984                                 if (sd->search_name_match_case)
1985                                         {
1986                                         match = g_regex_match(sd->search_name_regex, fd_name_or_path, static_cast<GRegexMatchFlags>(0), nullptr);
1987                                         }
1988                                 else
1989                                         {
1990                                         /* sd->search_name is converted in search_start() */
1991                                         gchar *haystack = g_utf8_strdown(fd_name_or_path, -1);
1992                                         match = g_regex_match(sd->search_name_regex, haystack, static_cast<GRegexMatchFlags>(0), nullptr);
1993                                         g_free(haystack);
1994                                         }
1995                                 }
1996                         }
1997                 }
1998
1999         if (match && sd->match_size_enable)
2000                 {
2001                 tested = TRUE;
2002                 match = FALSE;
2003
2004                 if (sd->match_size == SEARCH_MATCH_EQUAL)
2005                         {
2006                         match = (fd->size == sd->search_size);
2007                         }
2008                 else if (sd->match_size == SEARCH_MATCH_UNDER)
2009                         {
2010                         match = (fd->size < sd->search_size);
2011                         }
2012                 else if (sd->match_size == SEARCH_MATCH_OVER)
2013                         {
2014                         match = (fd->size > sd->search_size);
2015                         }
2016                 else if (sd->match_size == SEARCH_MATCH_BETWEEN)
2017                         {
2018                         match = MATCH_IS_BETWEEN(fd->size, sd->search_size, sd->search_size_end);
2019                         }
2020                 }
2021
2022         if (match && sd->match_date_enable)
2023                 {
2024                 tested = TRUE;
2025                 match = FALSE;
2026
2027                 if (g_strcmp0(gtk_combo_box_text_get_active_text(
2028                                                 GTK_COMBO_BOX_TEXT(sd->date_type)), _("Changed")) == 0)
2029                         {
2030                         file_date = fd->cdate;
2031                         }
2032                 else if (g_strcmp0(gtk_combo_box_text_get_active_text(
2033                                                 GTK_COMBO_BOX_TEXT(sd->date_type)), _("Original")) == 0)
2034                         {
2035                         read_exif_time_data(fd);
2036                         file_date = fd->exifdate;
2037                         }
2038                 else if (g_strcmp0(gtk_combo_box_text_get_active_text(
2039                                                 GTK_COMBO_BOX_TEXT(sd->date_type)), _("Digitized")) == 0)
2040                         {
2041                         read_exif_time_digitized_data(fd);
2042                         file_date = fd->exifdate_digitized;
2043                         }
2044                 else
2045                         {
2046                         file_date = fd->date;
2047                         }
2048
2049                 if (sd->match_date == SEARCH_MATCH_EQUAL)
2050                         {
2051                         struct tm *lt;
2052
2053                         lt = localtime(&file_date);
2054                         match = (lt &&
2055                                  lt->tm_year == sd->search_date_y - 1900 &&
2056                                  lt->tm_mon == sd->search_date_m - 1 &&
2057                                  lt->tm_mday == sd->search_date_d);
2058                         }
2059                 else if (sd->match_date == SEARCH_MATCH_UNDER)
2060                         {
2061                         match = (file_date < convert_dmy_to_time(sd->search_date_d, sd->search_date_m, sd->search_date_y));
2062                         }
2063                 else if (sd->match_date == SEARCH_MATCH_OVER)
2064                         {
2065                         match = (file_date > convert_dmy_to_time(sd->search_date_d, sd->search_date_m, sd->search_date_y) + 60 * 60 * 24 - 1);
2066                         }
2067                 else if (sd->match_date == SEARCH_MATCH_BETWEEN)
2068                         {
2069                         time_t a = convert_dmy_to_time(sd->search_date_d, sd->search_date_m, sd->search_date_y);
2070                         time_t b = convert_dmy_to_time(sd->search_date_end_d, sd->search_date_end_m, sd->search_date_end_y);
2071
2072                         if (b >= a)
2073                                 {
2074                                 b += 60 * 60 * 24 - 1;
2075                                 }
2076                         else
2077                                 {
2078                                 a += 60 * 60 * 24 - 1;
2079                                 }
2080                         match = MATCH_IS_BETWEEN(file_date, a, b);
2081                         }
2082                 }
2083
2084         if (match && sd->match_keywords_enable && sd->search_keyword_list)
2085                 {
2086                 GList *list;
2087
2088                 tested = TRUE;
2089                 match = FALSE;
2090
2091                 list = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
2092
2093                 if (list)
2094                         {
2095                         GList *needle;
2096                         GList *haystack;
2097
2098                         if (sd->match_keywords == SEARCH_MATCH_ALL)
2099                                 {
2100                                 gboolean found = TRUE;
2101
2102                                 needle = sd->search_keyword_list;
2103                                 while (needle && found)
2104                                         {
2105                                         found = FALSE;
2106                                         haystack = list;
2107                                         while (haystack && !found)
2108                                                 {
2109                                                 found = (g_ascii_strcasecmp(static_cast<gchar *>(needle->data),
2110                                                                     static_cast<gchar *>(haystack->data)) == 0);
2111                                                 haystack = haystack->next;
2112                                                 }
2113                                         needle = needle->next;
2114                                         }
2115
2116                                 match = found;
2117                                 }
2118                         else if (sd->match_keywords == SEARCH_MATCH_ANY)
2119                                 {
2120                                 gboolean found = FALSE;
2121
2122                                 needle = sd->search_keyword_list;
2123                                 while (needle && !found)
2124                                         {
2125                                         haystack = list;
2126                                         while (haystack && !found)
2127                                                 {
2128                                                 found = (g_ascii_strcasecmp(static_cast<gchar *>(needle->data),
2129                                                                     static_cast<gchar *>(haystack->data)) == 0);
2130                                                 haystack = haystack->next;
2131                                                 }
2132                                         needle = needle->next;
2133                                         }
2134
2135                                 match = found;
2136                                 }
2137                         else if (sd->match_keywords == SEARCH_MATCH_NONE)
2138                                 {
2139                                 gboolean found = FALSE;
2140
2141                                 needle = sd->search_keyword_list;
2142                                 while (needle && !found)
2143                                         {
2144                                         haystack = list;
2145                                         while (haystack && !found)
2146                                                 {
2147                                                 found = (g_ascii_strcasecmp(static_cast<gchar *>(needle->data),
2148                                                                     static_cast<gchar *>(haystack->data)) == 0);
2149                                                 haystack = haystack->next;
2150                                                 }
2151                                         needle = needle->next;
2152                                         }
2153
2154                                 match = !found;
2155                                 }
2156                         g_list_free_full(list, g_free);
2157                         }
2158                 else
2159                         {
2160                         match = (sd->match_keywords == SEARCH_MATCH_NONE);
2161                         }
2162                 }
2163
2164         if (match && sd->match_comment_enable && sd->search_comment && strlen(sd->search_comment))
2165                 {
2166                 gchar *comment;
2167
2168                 tested = TRUE;
2169                 match = FALSE;
2170
2171                 comment = metadata_read_string(fd, COMMENT_KEY, METADATA_PLAIN);
2172
2173                 if (comment)
2174                         {
2175                         if (!sd->search_comment_match_case)
2176                                 {
2177                                 gchar *tmp = g_utf8_strdown(comment, -1);
2178                                 g_free(comment);
2179                                 comment = tmp;
2180                                 }
2181
2182                         if (sd->match_comment == SEARCH_MATCH_CONTAINS)
2183                                 {
2184                                 match = g_regex_match(sd->search_comment_regex, comment, static_cast<GRegexMatchFlags>(0), nullptr);
2185                                 }
2186                         else if (sd->match_comment == SEARCH_MATCH_NONE)
2187                                 {
2188                                 match = !g_regex_match(sd->search_comment_regex, comment, static_cast<GRegexMatchFlags>(0), nullptr);
2189                                 }
2190                         g_free(comment);
2191                         }
2192                 else
2193                         {
2194                         match = (sd->match_comment == SEARCH_MATCH_NONE);
2195                         }
2196                 }
2197
2198         if (match && sd->match_rating_enable)
2199                 {
2200                 tested = TRUE;
2201                 match = FALSE;
2202                 gint rating;
2203
2204                 rating = metadata_read_int(fd, RATING_KEY, 0);
2205                 if (sd->match_rating == SEARCH_MATCH_EQUAL)
2206                         {
2207                         match = (rating == sd->search_rating);
2208                         }
2209                 else if (sd->match_rating == SEARCH_MATCH_UNDER)
2210                         {
2211                         match = (rating < sd->search_rating);
2212                         }
2213                 else if (sd->match_rating == SEARCH_MATCH_OVER)
2214                         {
2215                         match = (rating > sd->search_rating);
2216                         }
2217                 else if (sd->match_rating == SEARCH_MATCH_BETWEEN)
2218                         {
2219                         match = MATCH_IS_BETWEEN(rating, sd->search_rating, sd->search_rating_end);
2220                         }
2221                 }
2222
2223         if (match && sd->match_class_enable)
2224                 {
2225                 tested = TRUE;
2226                 match = FALSE;
2227                 FileFormatClass format_class;
2228                 FileFormatClass search_class;
2229
2230                 if (g_strcmp0(gtk_combo_box_text_get_active_text(
2231                                                 GTK_COMBO_BOX_TEXT(sd->class_type)), _("Image")) == 0)
2232                         {
2233                         search_class = FORMAT_CLASS_IMAGE;
2234                         }
2235                 else if (g_strcmp0(gtk_combo_box_text_get_active_text(
2236                                                 GTK_COMBO_BOX_TEXT(sd->class_type)), _("Raw Image")) == 0)
2237                         {
2238                         search_class = FORMAT_CLASS_RAWIMAGE;
2239                         }
2240                 else if (g_strcmp0(gtk_combo_box_text_get_active_text(
2241                                                 GTK_COMBO_BOX_TEXT(sd->class_type)), _("Video")) == 0)
2242                         {
2243                         search_class = FORMAT_CLASS_VIDEO;
2244                         }
2245                 else if (g_strcmp0(gtk_combo_box_text_get_active_text(
2246                                                 GTK_COMBO_BOX_TEXT(sd->class_type)), _("Document")) == 0)
2247                         {
2248                         search_class = FORMAT_CLASS_DOCUMENT;
2249                         }
2250                 else if (g_strcmp0(gtk_combo_box_text_get_active_text(
2251                                                 GTK_COMBO_BOX_TEXT(sd->class_type)), _("Metadata")) == 0)
2252                         {
2253                         search_class = FORMAT_CLASS_META;
2254                         }
2255                 else if (g_strcmp0(gtk_combo_box_text_get_active_text(
2256                                                 GTK_COMBO_BOX_TEXT(sd->class_type)), _("Unknown")) == 0)
2257                         {
2258                         search_class = FORMAT_CLASS_UNKNOWN;
2259                         }
2260                 else
2261                         {
2262                         search_class = FORMAT_CLASS_BROKEN;
2263                         }
2264
2265                 if (search_class != FORMAT_CLASS_BROKEN)
2266                         {
2267                         format_class = fd->format_class;
2268                         if (sd->match_class == SEARCH_MATCH_EQUAL)
2269                                 {
2270                                 match = (format_class == search_class);
2271                                 }
2272                         else if (sd->match_class == SEARCH_MATCH_NONE)
2273                                 {
2274                                 match = (format_class != search_class);
2275                                 }
2276                         }
2277                 else
2278                         {
2279                         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)
2280                                 {
2281                                 sd->match_broken_enable = TRUE;
2282                                 match = TRUE;
2283                                 }
2284                         else
2285                                 {
2286                                 sd->match_broken_enable = FALSE;
2287                                 }
2288                         }
2289                 }
2290
2291         if (match && sd->match_marks_enable)
2292                 {
2293                 tested = TRUE;
2294                 match = FALSE;
2295                 gint search_marks = -1;
2296                 gint i = 0;
2297                 gchar *marks_string = nullptr;
2298
2299                 if (g_strcmp0(gtk_combo_box_text_get_active_text(
2300                                                 GTK_COMBO_BOX_TEXT(sd->marks_type)), _("Any mark")) == 0)
2301                         {
2302                         search_marks = -1;
2303                         }
2304                 else
2305                         {
2306                         for (i = 0; i < FILEDATA_MARKS_SIZE; i++)
2307                                 {
2308                                 marks_string = g_strdup_printf("%s%d", _("Mark "), i + 1);
2309                                 if (g_strcmp0(marks_string, options->marks_tooltips[i]) != 0)
2310                                         {
2311                                         g_free(marks_string);
2312                                         marks_string = g_strdup_printf("%s%d %s", _("Mark "), i + 1,
2313                                                                                                         options->marks_tooltips[i]);
2314                                         }
2315
2316                                 if (g_strcmp0(gtk_combo_box_text_get_active_text(
2317                                                                 GTK_COMBO_BOX_TEXT(sd->marks_type)),
2318                                                                 marks_string) == 0)
2319                                         {
2320                                         search_marks = 1 << i;
2321                                         }
2322                                 g_free(marks_string);
2323                                 marks_string = nullptr;
2324                                 }
2325                         }
2326
2327                 if (sd->match_marks == SEARCH_MATCH_EQUAL)
2328                         {
2329                         match = (fd->marks & search_marks);
2330                         }
2331                 else
2332                         {
2333                         if (search_marks == -1)
2334                                 {
2335                                 match = fd->marks ? FALSE : TRUE;
2336                                 }
2337                         else
2338                                 {
2339                                 match = (fd->marks & search_marks) ? FALSE : TRUE;
2340                                 }
2341                         }
2342                 }
2343
2344         if (match && sd->match_gps_enable)
2345                 {
2346                 /* Calculate the distance the image is from the specified origin.
2347                 * This is a standard algorithm. A simplified one may be faster.
2348                 */
2349                 #define RADIANS  0.0174532925
2350                 #define KM_EARTH_RADIUS 6371
2351                 #define MILES_EARTH_RADIUS 3959
2352                 #define NAUTICAL_MILES_EARTH_RADIUS 3440
2353
2354                 gdouble latitude, longitude, range, conversion;
2355
2356                 if (g_strcmp0(gtk_combo_box_text_get_active_text(
2357                                                 GTK_COMBO_BOX_TEXT(sd->units_gps)), _("km")) == 0)
2358                         {
2359                         conversion = KM_EARTH_RADIUS;
2360                         }
2361                 else if (g_strcmp0(gtk_combo_box_text_get_active_text(
2362                                                 GTK_COMBO_BOX_TEXT(sd->units_gps)), _("miles")) == 0)
2363                         {
2364                         conversion = MILES_EARTH_RADIUS;
2365                         }
2366                 else
2367                         {
2368                         conversion = NAUTICAL_MILES_EARTH_RADIUS;
2369                         }
2370
2371                 tested = TRUE;
2372                 match = FALSE;
2373
2374                 latitude = metadata_read_GPS_coord(fd, "Xmp.exif.GPSLatitude", 1000);
2375                 longitude = metadata_read_GPS_coord(fd, "Xmp.exif.GPSLongitude", 1000);
2376                 if (latitude != 1000 && longitude != 1000)
2377                         {
2378                         range = conversion * acos(sin(latitude * RADIANS) *
2379                                                 sin(sd->search_lat * RADIANS) + cos(latitude * RADIANS) *
2380                                                 cos(sd->search_lat * RADIANS) * cos((sd->search_lon -
2381                                                 longitude) * RADIANS));
2382                         if (sd->match_gps == SEARCH_MATCH_UNDER)
2383                                 {
2384                                 if (sd->search_gps >= range)
2385                                         match = TRUE;
2386                                 }
2387                         else if (sd->match_gps == SEARCH_MATCH_OVER)
2388                                 {
2389                                 if (sd->search_gps < range)
2390                                         match = TRUE;
2391                                 }
2392                         }
2393                 else if (sd->match_gps == SEARCH_MATCH_NONE)
2394                         {
2395                         match = TRUE;
2396                         }
2397                 }
2398
2399         if ((match || extra_only) && (sd->match_dimensions_enable || sd->match_similarity_enable || sd->match_broken_enable))
2400                 {
2401                 tested = TRUE;
2402
2403                 if (search_file_do_extra(sd, fd, &match, &width, &height, &sim))
2404                         {
2405                         sd->search_buffer_count += SEARCH_BUFFER_MATCH_LOAD;
2406                         return TRUE;
2407                         }
2408                 }
2409
2410         sd->search_file_list = g_list_remove(sd->search_file_list, fd);
2411
2412         if (tested && match)
2413                 {
2414                 auto mfd = g_new(MatchFileData, 1);
2415                 mfd->fd = fd;
2416
2417                 mfd->width = width;
2418                 mfd->height = height;
2419                 mfd->rank = sim;
2420
2421                 sd->search_buffer_list = g_list_prepend(sd->search_buffer_list, mfd);
2422                 sd->search_buffer_count += SEARCH_BUFFER_MATCH_HIT;
2423                 sd->search_count++;
2424                 search_progress_update(sd, TRUE, -1.0);
2425                 }
2426         else
2427                 {
2428                 file_data_unref(fd);
2429                 sd->search_buffer_count += SEARCH_BUFFER_MATCH_MISS;
2430                 }
2431
2432         return FALSE;
2433 }
2434
2435 static gboolean search_step_cb(gpointer data)
2436 {
2437         auto sd = static_cast<SearchData *>(data);
2438         FileData *fd;
2439
2440         if (sd->search_buffer_count > SEARCH_BUFFER_FLUSH_SIZE)
2441                 {
2442                 search_buffer_flush(sd);
2443                 search_progress_update(sd, TRUE, -1.0);
2444                 }
2445
2446         if (sd->search_file_list)
2447                 {
2448                 if (search_file_next(sd))
2449                         {
2450                         sd->search_idle_id = 0;
2451                         return G_SOURCE_REMOVE;
2452                         }
2453                 return G_SOURCE_CONTINUE;
2454                 }
2455
2456         if (!sd->search_file_list && !sd->search_folder_list)
2457                 {
2458                 sd->search_idle_id = 0;
2459
2460                 search_stop(sd);
2461                 search_result_thumb_step(sd);
2462
2463                 return G_SOURCE_REMOVE;
2464                 }
2465
2466         fd = static_cast<FileData *>(sd->search_folder_list->data);
2467
2468         if (g_list_find(sd->search_done_list, fd) == nullptr)
2469                 {
2470                 GList *list = nullptr;
2471                 GList *dlist = nullptr;
2472                 gboolean success = FALSE;
2473
2474                 sd->search_done_list = g_list_prepend(sd->search_done_list, fd);
2475
2476                 if (sd->search_type == SEARCH_MATCH_NONE)
2477                         {
2478                         success = filelist_read(fd, &list, &dlist);
2479                         }
2480                 else if (sd->search_type == SEARCH_MATCH_ALL &&
2481                          sd->search_dir_fd &&
2482                          strlen(fd->path) >= strlen(sd->search_dir_fd->path))
2483                         {
2484                         const gchar *path;
2485
2486                         path = fd->path + strlen(sd->search_dir_fd->path);
2487                         if (path != fd->path)
2488                                 {
2489                                 FileData *dir_fd = file_data_new_dir(path);
2490                                 success = filelist_read(dir_fd, &list, nullptr);
2491                                 file_data_unref(dir_fd);
2492                                 }
2493                         success |= filelist_read(fd, nullptr, &dlist);
2494                         if (success)
2495                                 {
2496                                 GList *work;
2497
2498                                 work = list;
2499                                 while (work)
2500                                         {
2501                                         FileData *fdp;
2502                                         GList *link;
2503                                         gchar *meta_path;
2504
2505                                         fdp = static_cast<FileData *>(work->data);
2506                                         link = work;
2507                                         work = work->next;
2508
2509                                         meta_path = cache_find_location(CACHE_TYPE_METADATA, fdp->path);
2510                                         if (!meta_path)
2511                                                 {
2512                                                 list = g_list_delete_link(list, link);
2513                                                 file_data_unref(fdp);
2514                                                 }
2515                                         g_free(meta_path);
2516                                         }
2517                                 }
2518                         }
2519
2520                 if (success)
2521                         {
2522                         list = filelist_sort(list, SORT_NAME, TRUE, TRUE);
2523                         sd->search_file_list = list;
2524
2525                         if (sd->search_path_recurse)
2526                                 {
2527                                 dlist = filelist_sort(dlist, SORT_NAME, TRUE, TRUE);
2528                                 sd->search_folder_list = g_list_concat(dlist, sd->search_folder_list);
2529                                 }
2530                         else
2531                                 {
2532                                 filelist_free(dlist);
2533                                 }
2534                         }
2535                 }
2536         else
2537                 {
2538                 sd->search_folder_list = g_list_remove(sd->search_folder_list, fd);
2539                 sd->search_done_list = g_list_remove(sd->search_done_list, fd);
2540                 file_data_unref(fd);
2541                 }
2542
2543         return G_SOURCE_CONTINUE;
2544 }
2545
2546 static void search_similarity_load_done_cb(ImageLoader *, gpointer data)
2547 {
2548         auto sd = static_cast<SearchData *>(data);
2549         search_file_load_process(sd, sd->search_similarity_cd);
2550 }
2551
2552 static void search_start(SearchData *sd)
2553 {
2554         GError *error = nullptr;
2555
2556         search_stop(sd);
2557         search_result_clear(sd);
2558
2559         if (sd->search_dir_fd)
2560                 {
2561                 sd->search_folder_list = g_list_prepend(sd->search_folder_list, file_data_ref(sd->search_dir_fd));
2562                 }
2563
2564         if (!sd->search_name_match_case)
2565                 {
2566                 /* convert to lowercase here, so that this is only done once per search */
2567                 gchar *tmp = g_utf8_strdown(sd->search_name, -1);
2568                 g_free(sd->search_name);
2569                 sd->search_name = tmp;
2570                 }
2571
2572         if(sd->search_name_regex)
2573                 {
2574                 g_regex_unref(sd->search_name_regex);
2575                 }
2576
2577         sd->search_name_regex = g_regex_new(sd->search_name, static_cast<GRegexCompileFlags>(0), static_cast<GRegexMatchFlags>(0), &error);
2578         if (error)
2579                 {
2580                 log_printf("Error: could not compile regular expression %s\n%s\n", sd->search_name, error->message);
2581                 g_error_free(error);
2582                 error = nullptr;
2583                 sd->search_name_regex = g_regex_new("", static_cast<GRegexCompileFlags>(0), static_cast<GRegexMatchFlags>(0), nullptr);
2584                 }
2585
2586         if (!sd->search_comment_match_case)
2587                 {
2588                 /* convert to lowercase here, so that this is only done once per search */
2589                 gchar *tmp = g_utf8_strdown(sd->search_comment, -1);
2590                 g_free(sd->search_comment);
2591                 sd->search_comment = tmp;
2592                 }
2593
2594         if(sd->search_comment_regex)
2595                 {
2596                 g_regex_unref(sd->search_comment_regex);
2597                 }
2598
2599         sd->search_comment_regex = g_regex_new(sd->search_comment, static_cast<GRegexCompileFlags>(0), static_cast<GRegexMatchFlags>(0), &error);
2600         if (error)
2601                 {
2602                 log_printf("Error: could not compile regular expression %s\n%s\n", sd->search_comment, error->message);
2603                 g_error_free(error);
2604                 error = nullptr;
2605                 sd->search_comment_regex = g_regex_new("", static_cast<GRegexCompileFlags>(0), static_cast<GRegexMatchFlags>(0), nullptr);
2606                 }
2607
2608         sd->search_count = 0;
2609         sd->search_total = 0;
2610
2611         gtk_widget_set_sensitive(sd->box_search, FALSE);
2612         gtk_spinner_start(GTK_SPINNER(sd->spinner));
2613         gtk_widget_set_sensitive(sd->button_start, FALSE);
2614         gtk_widget_set_sensitive(sd->button_stop, TRUE);
2615         search_progress_update(sd, TRUE, -1.0);
2616
2617         if (sd->match_similarity_enable &&
2618             !sd->search_similarity_cd &&
2619             isfile(sd->search_similarity_path))
2620                 {
2621                 gchar *cd_path;
2622
2623                 cd_path = cache_find_location(CACHE_TYPE_SIM, sd->search_similarity_path);
2624                 if (cd_path && filetime(sd->search_similarity_path) == filetime(cd_path))
2625                         {
2626                         sd->search_similarity_cd = cache_sim_data_load(cd_path);
2627                         }
2628                 g_free(cd_path);
2629
2630                 if (!sd->search_similarity_cd || !sd->search_similarity_cd->similarity)
2631                         {
2632                         if (!sd->search_similarity_cd)
2633                                 {
2634                                 sd->search_similarity_cd = cache_sim_data_new();
2635                                 }
2636
2637                         sd->img_loader = image_loader_new(file_data_new_group(sd->search_similarity_path));
2638                         g_signal_connect(G_OBJECT(sd->img_loader), "error", (GCallback)search_similarity_load_done_cb, sd);
2639                         g_signal_connect(G_OBJECT(sd->img_loader), "done", (GCallback)search_similarity_load_done_cb, sd);
2640                         if (image_loader_start(sd->img_loader))
2641                                 {
2642                                 return;
2643                                 }
2644                         image_loader_free(sd->img_loader);
2645                         sd->img_loader = nullptr;
2646                         }
2647
2648                 }
2649
2650         sd->search_idle_id = g_idle_add(search_step_cb, sd);
2651 }
2652
2653 static void search_start_cb(GtkWidget *, gpointer data)
2654 {
2655         auto sd = static_cast<SearchData *>(data);
2656         gchar *collection;
2657         gchar *entry_text;
2658         gchar *path;
2659         GDateTime *date;
2660         GtkTreeViewColumn *column;
2661
2662         if (sd->search_folder_list)
2663                 {
2664                 search_stop(sd);
2665                 search_result_thumb_step(sd);
2666                 return;
2667                 }
2668
2669         if (sd->match_name_enable) history_combo_append_history(sd->entry_name, nullptr);
2670         g_free(sd->search_name);
2671         sd->search_name = g_strdup(gq_gtk_entry_get_text(GTK_ENTRY(sd->entry_name)));
2672
2673         /* XXX */
2674         g_free(sd->search_comment);
2675         sd->search_comment = g_strdup(gq_gtk_entry_get_text(GTK_ENTRY(sd->entry_comment)));
2676
2677         g_free(sd->search_similarity_path);
2678         sd->search_similarity_path = g_strdup(gq_gtk_entry_get_text(GTK_ENTRY(sd->entry_similarity)));
2679         if (sd->match_similarity_enable)
2680                 {
2681                 if (!isfile(sd->search_similarity_path))
2682                         {
2683                         file_util_warning_dialog(_("File not found"),
2684                                                  _("Please enter an existing file for image content."),
2685                                                  GQ_ICON_DIALOG_WARNING, sd->window);
2686                         return;
2687                         }
2688                 tab_completion_append_to_history(sd->entry_similarity, sd->search_similarity_path);
2689                 }
2690
2691         /* Check the coordinate entry.
2692         * If the result is not sensible, it should get blocked.
2693         */
2694         if (sd->match_gps_enable)
2695                 {
2696                 if (sd->match_gps != SEARCH_MATCH_NONE)
2697                         {
2698                         entry_text = decode_geo_parameters(gq_gtk_entry_get_text(
2699                                                                                 GTK_ENTRY(sd->entry_gps_coord)));
2700
2701                         sd->search_lat = 1000;
2702                         sd->search_lon = 1000;
2703                         sscanf(entry_text," %lf  %lf ", &sd->search_lat, &sd->search_lon );
2704                         if (entry_text == nullptr || g_strstr_len(entry_text, -1, "Error") ||
2705                                                 sd->search_lat < -90 || sd->search_lat > 90 ||
2706                                                 sd->search_lon < -180 || sd->search_lon > 180)
2707                                 {
2708                                 file_util_warning_dialog(_(
2709                                                 "Entry does not contain a valid lat/long value"),
2710                                                         entry_text, GQ_ICON_DIALOG_WARNING, sd->window);
2711                                 return;
2712                                 }
2713                         g_free(entry_text);
2714                         }
2715                 }
2716
2717         g_list_free_full(sd->search_keyword_list, g_free);
2718         sd->search_keyword_list = keyword_list_pull(sd->entry_keywords);
2719
2720         date = date_selection_get(sd->date_sel);
2721         sd->search_date_d = g_date_time_get_day_of_month(date);
2722         sd->search_date_m = g_date_time_get_month(date);
2723         sd->search_date_y = g_date_time_get_year(date);
2724         g_date_time_unref(date);
2725
2726         date = date_selection_get(sd->date_sel_end);
2727         sd->search_date_end_d = g_date_time_get_day_of_month(date);
2728         sd->search_date_end_m = g_date_time_get_month(date);
2729         sd->search_date_end_y = g_date_time_get_year(date);
2730         g_date_time_unref(date);
2731
2732         column = gtk_tree_view_get_column(GTK_TREE_VIEW(sd->result_view), SEARCH_COLUMN_DIMENSIONS - 1);
2733         gtk_tree_view_column_set_visible(column, sd->match_dimensions_enable);
2734
2735         column = gtk_tree_view_get_column(GTK_TREE_VIEW(sd->result_view), SEARCH_COLUMN_RANK - 1);
2736         gtk_tree_view_column_set_visible(column, sd->match_similarity_enable);
2737         if (!sd->match_similarity_enable)
2738                 {
2739                 GtkTreeSortable *sortable;
2740                 gint id;
2741                 GtkSortType order;
2742
2743                 sortable = GTK_TREE_SORTABLE(gtk_tree_view_get_model(GTK_TREE_VIEW(sd->result_view)));
2744                 if (gtk_tree_sortable_get_sort_column_id(sortable, &id, &order) &&
2745                     id == SEARCH_COLUMN_RANK)
2746                         {
2747                         gtk_tree_sortable_set_sort_column_id(sortable, SEARCH_COLUMN_PATH, GTK_SORT_ASCENDING);
2748                         }
2749                 }
2750
2751         if (sd->search_type == SEARCH_MATCH_NONE)
2752                 {
2753                 /* search path */
2754
2755                 path = remove_trailing_slash(gq_gtk_entry_get_text(GTK_ENTRY(sd->path_entry)));
2756                 if (isdir(path))
2757                         {
2758                         file_data_unref(sd->search_dir_fd);
2759                         sd->search_dir_fd = file_data_new_dir(path);
2760
2761                         tab_completion_append_to_history(sd->path_entry, sd->search_dir_fd->path);
2762
2763                         search_start(sd);
2764                         }
2765                 else
2766                         {
2767                         file_util_warning_dialog(_("Folder not found"),
2768                                                  _("Please enter an existing folder to search."),
2769                                                  GQ_ICON_DIALOG_WARNING, sd->window);
2770                         }
2771
2772                 g_free(path);
2773                 }
2774         else if (sd->search_type == SEARCH_MATCH_ALL)
2775                 {
2776                 /* search metadata */
2777                 file_data_unref(sd->search_dir_fd);
2778                 sd->search_dir_fd = file_data_new_dir(get_metadata_cache_dir());
2779                 search_start(sd);
2780                 }
2781         else if (sd->search_type == SEARCH_MATCH_CONTAINS)
2782                 {
2783                 /* search current result list */
2784                 GList *list;
2785
2786                 list = search_result_refine_list(sd);
2787
2788                 file_data_unref(sd->search_dir_fd);
2789                 sd->search_dir_fd = nullptr;
2790
2791                 search_start(sd);
2792
2793                 sd->search_file_list = g_list_concat(sd->search_file_list, list);
2794                 }
2795         else if (sd->search_type == SEARCH_MATCH_COLLECTION)
2796                 {
2797                 collection = g_strdup(gq_gtk_entry_get_text(GTK_ENTRY(sd->collection_entry)));
2798
2799                 if (is_collection(collection))
2800                         {
2801                         GList *list = nullptr;
2802
2803                         list = collection_contents_fd(collection);
2804
2805                         file_data_unref(sd->search_dir_fd);
2806                         sd->search_dir_fd = nullptr;
2807
2808                         search_start(sd);
2809
2810                         sd->search_file_list = g_list_concat(sd->search_file_list, list);
2811                         }
2812                 else
2813                         {
2814                         file_util_warning_dialog(_("Collection not found"), _("Please enter an existing collection name."), GQ_ICON_DIALOG_WARNING, sd->window);
2815                         }
2816                 g_free(collection);
2817                 }
2818 }
2819
2820 /*
2821  *-------------------------------------------------------------------
2822  * window construct
2823  *-------------------------------------------------------------------
2824  */
2825
2826 enum {
2827         MENU_CHOICE_COLUMN_NAME = 0,
2828         MENU_CHOICE_COLUMN_VALUE
2829 };
2830
2831 static void search_thumb_toggle_cb(GtkWidget *button, gpointer data)
2832 {
2833         auto sd = static_cast<SearchData *>(data);
2834
2835         search_result_thumb_enable(sd, gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button)));
2836 }
2837
2838 static gint sort_matchdata_dimensions(MatchFileData *a, MatchFileData *b)
2839 {
2840         gint sa;
2841         gint sb;
2842
2843         sa = a->width * a->height;
2844         sb = b->width * b->height;
2845
2846         if (sa > sb) return 1;
2847         if (sa < sb) return -1;
2848         return 0;
2849 }
2850
2851 static gint search_result_sort_cb(GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, gpointer data)
2852 {
2853         gint n = GPOINTER_TO_INT(data);
2854         MatchFileData *fda;
2855         MatchFileData *fdb;
2856
2857         gtk_tree_model_get(model, a, SEARCH_COLUMN_POINTER, &fda, -1);
2858         gtk_tree_model_get(model, b, SEARCH_COLUMN_POINTER, &fdb, -1);
2859
2860         if (!fda || !fdb) return 0;
2861
2862         switch (n)
2863                 {
2864                 case SEARCH_COLUMN_RANK:
2865                         if ((fda)->rank > (fdb)->rank) return 1;
2866                         if ((fda)->rank < (fdb)->rank) return -1;
2867                         return 0;
2868                         break;
2869                 case SEARCH_COLUMN_NAME:
2870                         if (options->file_sort.case_sensitive)
2871                                 return strcmp(fda->fd->collate_key_name, fdb->fd->collate_key_name);
2872                         else
2873                                 return strcmp(fda->fd->collate_key_name_nocase, fdb->fd->collate_key_name_nocase);
2874                         break;
2875                 case SEARCH_COLUMN_SIZE:
2876                         if (fda->fd->size > fdb->fd->size) return 1;
2877                         if (fda->fd->size < fdb->fd->size) return -1;
2878                         return 0;
2879                         break;
2880                 case SEARCH_COLUMN_DATE:
2881                         if (fda->fd->date > fdb->fd->date) return 1;
2882                         if (fda->fd->date < fdb->fd->date) return -1;
2883                         return 0;
2884                         break;
2885                 case SEARCH_COLUMN_DIMENSIONS:
2886                         return sort_matchdata_dimensions(fda, fdb);
2887                         break;
2888                 case SEARCH_COLUMN_PATH:
2889                         return utf8_compare(fda->fd->path, fdb->fd->path, TRUE);
2890                         break;
2891                 default:
2892                         break;
2893                 }
2894
2895         return 0;
2896 }
2897
2898 static void search_result_add_column(SearchData * sd, gint n, const gchar *title, gboolean image, gboolean right_justify)
2899 {
2900         GtkTreeViewColumn *column;
2901         GtkCellRenderer *renderer;
2902
2903         column = gtk_tree_view_column_new();
2904         gtk_tree_view_column_set_title(column, title);
2905         gtk_tree_view_column_set_min_width(column, 4);
2906
2907         if (n != SEARCH_COLUMN_THUMB) gtk_tree_view_column_set_resizable(column, TRUE);
2908
2909         if (!image)
2910                 {
2911                 gtk_tree_view_column_set_sizing(column, GTK_TREE_VIEW_COLUMN_GROW_ONLY);
2912                 renderer = gtk_cell_renderer_text_new();
2913                 if (right_justify) g_object_set(G_OBJECT(renderer), "xalign", 1.0, NULL);
2914                 gtk_tree_view_column_pack_start(column, renderer, TRUE);
2915                 gtk_tree_view_column_add_attribute(column, renderer, "text", n);
2916
2917                 gtk_tree_view_column_set_sort_column_id(column, n);
2918                 }
2919         else
2920                 {
2921                 gtk_tree_view_column_set_sizing(column, GTK_TREE_VIEW_COLUMN_FIXED);
2922                 renderer = gtk_cell_renderer_pixbuf_new();
2923                 cell_renderer_height_override(renderer);
2924                 gtk_tree_view_column_pack_start(column, renderer, TRUE);
2925                 gtk_tree_view_column_add_attribute(column, renderer, "pixbuf", n);
2926                 }
2927
2928         gtk_tree_view_append_column(GTK_TREE_VIEW(sd->result_view), column);
2929 }
2930
2931 static void menu_choice_set_visible(GtkWidget *widget, gboolean visible)
2932 {
2933         if (visible)
2934                 {
2935                 if (!gtk_widget_get_visible(widget)) gtk_widget_show(widget);
2936                 }
2937         else
2938                 {
2939                 if (gtk_widget_get_visible(widget)) gtk_widget_hide(widget);
2940                 }
2941 }
2942
2943 static gboolean menu_choice_get_match_type(GtkWidget *combo, MatchType *type)
2944 {
2945         GtkTreeModel *store;
2946         GtkTreeIter iter;
2947
2948         store = gtk_combo_box_get_model(GTK_COMBO_BOX(combo));
2949         if (!gtk_combo_box_get_active_iter(GTK_COMBO_BOX(combo), &iter)) return FALSE;
2950         gtk_tree_model_get(store, &iter, MENU_CHOICE_COLUMN_VALUE, type, -1);
2951         return TRUE;
2952 }
2953
2954 static void menu_choice_path_cb(GtkWidget *combo, gpointer data)
2955 {
2956         auto sd = static_cast<SearchData *>(data);
2957
2958         if (!menu_choice_get_match_type(combo, &sd->search_type)) return;
2959
2960         menu_choice_set_visible(gtk_widget_get_parent(sd->check_recurse),
2961                                 (sd->search_type == SEARCH_MATCH_NONE));
2962         menu_choice_set_visible(sd->collection, (sd->search_type == SEARCH_MATCH_COLLECTION));
2963 }
2964
2965 static void menu_choice_name_cb(GtkWidget *combo, gpointer data)
2966 {
2967         auto sd = static_cast<SearchData *>(data);
2968
2969         if (!menu_choice_get_match_type(combo, &sd->match_name)) return;
2970 }
2971
2972 static void menu_choice_size_cb(GtkWidget *combo, gpointer data)
2973 {
2974         auto sd = static_cast<SearchData *>(data);
2975
2976         if (!menu_choice_get_match_type(combo, &sd->match_size)) return;
2977
2978         menu_choice_set_visible(gtk_widget_get_parent(sd->spin_size_end),
2979                                 (sd->match_size == SEARCH_MATCH_BETWEEN));
2980 }
2981
2982 static void menu_choice_rating_cb(GtkWidget *combo, gpointer data)
2983 {
2984         auto sd = static_cast<SearchData *>(data);
2985
2986         if (!menu_choice_get_match_type(combo, &sd->match_rating)) return;
2987
2988         menu_choice_set_visible(gtk_widget_get_parent(sd->spin_rating_end),
2989                                 (sd->match_rating == SEARCH_MATCH_BETWEEN));
2990 }
2991
2992 static void menu_choice_class_cb(GtkWidget *combo, gpointer data)
2993 {
2994         auto sd = static_cast<SearchData *>(data);
2995
2996         if (!menu_choice_get_match_type(combo, &sd->match_class)) return;
2997 }
2998
2999 static void menu_choice_marks_cb(GtkWidget *combo, gpointer data)
3000 {
3001         auto sd = static_cast<SearchData *>(data);
3002
3003         if (!menu_choice_get_match_type(combo, &sd->match_marks)) return;
3004 }
3005
3006 static void menu_choice_date_cb(GtkWidget *combo, gpointer data)
3007 {
3008         auto sd = static_cast<SearchData *>(data);
3009
3010         if (!menu_choice_get_match_type(combo, &sd->match_date)) return;
3011
3012         menu_choice_set_visible(gtk_widget_get_parent(sd->date_sel_end),
3013                                 (sd->match_date == SEARCH_MATCH_BETWEEN));
3014 }
3015
3016 static void menu_choice_dimensions_cb(GtkWidget *combo, gpointer data)
3017 {
3018         auto sd = static_cast<SearchData *>(data);
3019
3020         if (!menu_choice_get_match_type(combo, &sd->match_dimensions)) return;
3021
3022         menu_choice_set_visible(gtk_widget_get_parent(sd->spin_width_end),
3023                                 (sd->match_dimensions == SEARCH_MATCH_BETWEEN));
3024 }
3025
3026 static void menu_choice_keyword_cb(GtkWidget *combo, gpointer data)
3027 {
3028         auto sd = static_cast<SearchData *>(data);
3029
3030         if (!menu_choice_get_match_type(combo, &sd->match_keywords)) return;
3031 }
3032
3033 static void menu_choice_comment_cb(GtkWidget *combo, gpointer data)
3034 {
3035         auto sd = static_cast<SearchData *>(data);
3036
3037         if (!menu_choice_get_match_type(combo, &sd->match_comment)) return;
3038 }
3039
3040 static void menu_choice_spin_cb(GtkAdjustment *adjustment, gpointer data)
3041 {
3042         auto value = static_cast<gint *>(data);
3043
3044         *value = static_cast<gint>(gtk_adjustment_get_value(adjustment));
3045 }
3046
3047 static void menu_choice_gps_cb(GtkWidget *combo, gpointer data)
3048 {
3049         auto sd = static_cast<SearchData *>(data);
3050
3051         if (!menu_choice_get_match_type(combo, &sd->match_gps)) return;
3052
3053         menu_choice_set_visible(gtk_widget_get_parent(sd->spin_gps),
3054                                         (sd->match_gps != SEARCH_MATCH_NONE));
3055 }
3056
3057 static GtkWidget *menu_spin(GtkWidget *box, gdouble min, gdouble max, gint value,
3058                             GCallback func, gpointer data)
3059 {
3060         GtkWidget *spin;
3061         GtkAdjustment *adj;
3062
3063         spin = gtk_spin_button_new_with_range(min, max, 1);
3064         gtk_spin_button_set_value(GTK_SPIN_BUTTON(spin), static_cast<gdouble>(value));
3065         adj = gtk_spin_button_get_adjustment(GTK_SPIN_BUTTON(spin));
3066         if (func) g_signal_connect(G_OBJECT(adj), "value_changed",
3067                                    G_CALLBACK(func), data);
3068         gq_gtk_box_pack_start(GTK_BOX(box), spin, FALSE, FALSE, 0);
3069         gtk_widget_show(spin);
3070
3071         return spin;
3072 }
3073
3074 static void menu_choice_check_cb(GtkWidget *button, gpointer data)
3075 {
3076         auto widget = static_cast<GtkWidget *>(data);
3077         gboolean active;
3078         gboolean *value;
3079
3080         active = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button));
3081         gtk_widget_set_sensitive(widget, active);
3082
3083         value = static_cast<gboolean *>(g_object_get_data(G_OBJECT(button), "check_var"));
3084         if (value) *value = active;
3085 }
3086
3087 static GtkWidget *menu_choice_menu(const MatchList *items, gint item_count,
3088                                    GCallback func, gpointer data)
3089 {
3090         GtkWidget *combo;
3091         GtkCellRenderer *renderer;
3092         GtkListStore *store;
3093         gint i;
3094
3095         store = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_INT);
3096         combo = gtk_combo_box_new_with_model(GTK_TREE_MODEL(store));
3097         g_object_unref(store);
3098
3099         renderer = gtk_cell_renderer_text_new();
3100         gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(combo), renderer, TRUE);
3101         gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(combo), renderer,
3102                                        "text", MENU_CHOICE_COLUMN_NAME, NULL);
3103
3104         for (i = 0; i < item_count; i++)
3105                 {
3106                 GtkTreeIter iter;
3107
3108                 gtk_list_store_append(store, &iter);
3109                 gtk_list_store_set(store, &iter, MENU_CHOICE_COLUMN_NAME, _(items[i].text),
3110                                                  MENU_CHOICE_COLUMN_VALUE, items[i].type, -1);
3111                 }
3112
3113         gtk_combo_box_set_active(GTK_COMBO_BOX(combo), 0);
3114
3115         if (func) g_signal_connect(G_OBJECT(combo), "changed",
3116                                    G_CALLBACK(func), data);
3117
3118         return combo;
3119 }
3120
3121 static GtkWidget *menu_choice(GtkWidget *box, GtkWidget **check, GtkWidget **menu,
3122                               const gchar *text, gboolean *value,
3123                               const MatchList *items, gint item_count,
3124                               GCallback func, gpointer data)
3125 {
3126         GtkWidget *base_box;
3127         GtkWidget *hbox;
3128         GtkWidget *button;
3129         GtkWidget *option;
3130
3131         base_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, PREF_PAD_GAP);
3132         gq_gtk_box_pack_start(GTK_BOX(box), base_box, FALSE, FALSE, 0);
3133         gtk_widget_show(base_box);
3134
3135         button = gtk_check_button_new();
3136         if (value) gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(button), *value);
3137         gq_gtk_box_pack_start(GTK_BOX(base_box), button, FALSE, FALSE, 0);
3138         gtk_widget_show(button);
3139         if (check) *check = button;
3140         if (value) g_object_set_data(G_OBJECT(button), "check_var", value);
3141
3142         hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, PREF_PAD_SPACE);
3143         gq_gtk_box_pack_start(GTK_BOX(base_box), hbox, TRUE, TRUE, 0);
3144         gtk_widget_show(hbox);
3145
3146         g_signal_connect(G_OBJECT(button), "toggled",
3147                          G_CALLBACK(menu_choice_check_cb), hbox);
3148         gtk_widget_set_sensitive(hbox, (value) ? *value : FALSE);
3149
3150         pref_label_new(hbox, text);
3151
3152         if (!items && !menu) return hbox;
3153
3154         option = menu_choice_menu(items, item_count, func, data);
3155         gq_gtk_box_pack_start(GTK_BOX(hbox), option, FALSE, FALSE, 0);
3156         gtk_widget_show(option);
3157         if (menu) *menu = option;
3158
3159         return hbox;
3160 }
3161
3162 static void search_window_get_geometry(SearchData *sd)
3163 {
3164         GdkWindow *window;
3165         LayoutWindow *lw = nullptr;
3166
3167         layout_valid(&lw);
3168
3169         if (!sd || !lw) return;
3170
3171         window = gtk_widget_get_window(sd->window);
3172         gdk_window_get_position(window, &lw->options.search_window.x, &lw->options.search_window.y);
3173         lw->options.search_window.w = gdk_window_get_width(window);
3174         lw->options.search_window.h = gdk_window_get_height(window);
3175 }
3176
3177 static void search_window_close(SearchData *sd)
3178 {
3179
3180         search_window_get_geometry(sd);
3181
3182         gq_gtk_widget_destroy(sd->window);
3183 }
3184
3185 static void search_window_close_cb(GtkWidget *, gpointer data)
3186 {
3187         auto sd = static_cast<SearchData *>(data);
3188
3189         search_window_close(sd);
3190 }
3191
3192 static void search_window_help_cb(GtkWidget *, gpointer)
3193 {
3194         help_window_show("GuideImageSearchSearch.html");
3195 }
3196
3197 static gboolean search_window_delete_cb(GtkWidget *, GdkEventAny *, gpointer data)
3198 {
3199         auto sd = static_cast<SearchData *>(data);
3200
3201         search_window_close(sd);
3202         return TRUE;
3203 }
3204
3205 static void search_window_destroy_cb(GtkWidget *, gpointer data)
3206 {
3207         auto sd = static_cast<SearchData *>(data);
3208
3209         search_window_list = g_list_remove(search_window_list, sd);
3210
3211         search_result_update_idle_cancel(sd);
3212
3213         mfd_list_free(sd->search_buffer_list);
3214         sd->search_buffer_list = nullptr;
3215
3216         search_stop(sd);
3217         search_result_clear(sd);
3218
3219         file_data_unref(sd->search_dir_fd);
3220
3221         g_free(sd->search_name);
3222         if(sd->search_name_regex)
3223                 {
3224                 g_regex_unref(sd->search_name_regex);
3225                 }
3226         g_free(sd->search_comment);
3227         if(sd->search_comment_regex)
3228                 {
3229                 g_regex_unref(sd->search_comment_regex);
3230                 }
3231         g_free(sd->search_similarity_path);
3232         g_list_free_full(sd->search_keyword_list, g_free);
3233
3234         file_data_unregister_notify_func(search_notify_cb, sd);
3235
3236         g_free(sd);
3237 }
3238
3239 static void select_collection_dialog_close_cb(FileDialog *fdlg, gpointer)
3240 {
3241         file_dialog_close(fdlg);
3242 }
3243
3244 static void select_collection_dialog_ok_cb(FileDialog *fdlg, gpointer data)
3245 {
3246         auto sd = static_cast<SearchData *>(data);
3247         gchar *path;
3248         gchar *path_noext;
3249         gchar *collection;
3250
3251         path = g_strdup(gq_gtk_entry_get_text(GTK_ENTRY(fdlg->entry)));
3252         path_noext = remove_extension_from_path(path);
3253         collection = g_path_get_basename(path_noext);
3254
3255         gq_gtk_entry_set_text(GTK_ENTRY(sd->collection_entry), collection);
3256         file_dialog_close(fdlg);
3257
3258         g_free(path);
3259         g_free(path_noext);
3260         g_free(collection);
3261 }
3262
3263 static void select_collection_clicked_cb(GtkWidget *, gpointer data)
3264 {
3265         auto sd = static_cast<SearchData *>(data);
3266         FileDialog *fdlg;
3267         const gchar *title;
3268         const gchar *btntext;
3269         gpointer btnfunc;
3270         const gchar *icon_name;
3271
3272         title = _("Select collection");
3273         btntext = nullptr;
3274         btnfunc = reinterpret_cast<void *>(select_collection_dialog_ok_cb);
3275         icon_name = GQ_ICON_OK;
3276
3277         fdlg = file_util_file_dlg(title, "dlg_collection", sd->window, select_collection_dialog_close_cb, sd);
3278
3279         generic_dialog_add_message(GENERIC_DIALOG(fdlg), nullptr, title, nullptr, FALSE);
3280         file_dialog_add_button(fdlg, icon_name, btntext, reinterpret_cast<void(*)(FileDialog *, gpointer)>(btnfunc), TRUE);
3281
3282         file_dialog_add_path_widgets(fdlg, get_collections_dir(), nullptr, "search_collection", GQ_COLLECTION_EXT, _("Collection Files"));
3283
3284         gtk_widget_show(GENERIC_DIALOG(fdlg)->dialog);
3285 }
3286
3287 void search_new(FileData *dir_fd, FileData *example_file)
3288 {
3289         GtkWidget *vbox;
3290         GtkWidget *hbox;
3291         GtkWidget *hbox2;
3292         GtkWidget *pad_box;
3293         GtkWidget *frame;
3294         GtkWidget *scrolled;
3295         GtkListStore *store;
3296         GtkTreeSortable *sortable;
3297         GtkTreeSelection *selection;
3298         GtkTreeViewColumn *column;
3299         GtkWidget *combo;
3300         GdkGeometry geometry;
3301         gint i;
3302         gchar *marks_string;
3303         LayoutWindow *lw = nullptr;
3304
3305         layout_valid(&lw);
3306
3307         auto sd = g_new0(SearchData, 1);
3308
3309         sd->search_dir_fd = file_data_ref(dir_fd);
3310         sd->search_path_recurse = TRUE;
3311         sd->search_size = 0;
3312         sd->search_width = 640;
3313         sd->search_height = 480;
3314         sd->search_width_end = 1024;
3315         sd->search_height_end = 768;
3316
3317         sd->search_type = SEARCH_MATCH_NONE;
3318
3319         sd->match_name = SEARCH_MATCH_NAME_CONTAINS;
3320         sd->match_size = SEARCH_MATCH_EQUAL;
3321         sd->match_date = SEARCH_MATCH_EQUAL;
3322         sd->match_dimensions = SEARCH_MATCH_EQUAL;
3323         sd->match_keywords = SEARCH_MATCH_ALL;
3324         sd->match_comment = SEARCH_MATCH_CONTAINS;
3325         sd->match_rating = SEARCH_MATCH_EQUAL;
3326         sd->match_class = SEARCH_MATCH_EQUAL;
3327         sd->match_marks = SEARCH_MATCH_EQUAL;
3328
3329         sd->match_name_enable = TRUE;
3330
3331         sd->search_similarity = 95;
3332
3333         sd->search_gps = 1;
3334         sd->match_gps = SEARCH_MATCH_NONE;
3335
3336         if (example_file)
3337                 {
3338                 sd->search_similarity_path = g_strdup(example_file->path);
3339                 }
3340
3341         sd->window = window_new("search", nullptr, nullptr, _("Image search"));
3342         DEBUG_NAME(sd->window);
3343
3344         gtk_window_set_resizable(GTK_WINDOW(sd->window), TRUE);
3345
3346         geometry.min_width = DEFAULT_MINIMAL_WINDOW_SIZE;
3347         geometry.min_height = DEFAULT_MINIMAL_WINDOW_SIZE;
3348         geometry.base_width = DEF_SEARCH_WIDTH;
3349         geometry.base_height = DEF_SEARCH_HEIGHT;
3350         gtk_window_set_geometry_hints(GTK_WINDOW(sd->window), nullptr, &geometry,
3351                                       static_cast<GdkWindowHints>(GDK_HINT_MIN_SIZE | GDK_HINT_BASE_SIZE));
3352
3353         if (lw && options->save_window_positions)
3354                 {
3355                 gtk_window_set_default_size(GTK_WINDOW(sd->window), lw->options.search_window.w, lw->options.search_window.h);
3356                 gq_gtk_window_move(GTK_WINDOW(sd->window), lw->options.search_window.x, lw->options.search_window.y);
3357                 }
3358         else
3359                 {
3360                 gtk_window_set_default_size(GTK_WINDOW(sd->window), DEF_SEARCH_WIDTH, DEF_SEARCH_HEIGHT);
3361                 }
3362
3363         g_signal_connect(G_OBJECT(sd->window), "delete_event",
3364                          G_CALLBACK(search_window_delete_cb), sd);
3365         g_signal_connect(G_OBJECT(sd->window), "destroy",
3366                          G_CALLBACK(search_window_destroy_cb), sd);
3367
3368         g_signal_connect(G_OBJECT(sd->window), "key_press_event",
3369                          G_CALLBACK(search_window_keypress_cb), sd);
3370
3371         vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, PREF_PAD_GAP);
3372         gtk_container_set_border_width(GTK_CONTAINER(vbox), PREF_PAD_GAP);
3373         gq_gtk_container_add(GTK_WIDGET(sd->window), vbox);
3374         gtk_widget_show(vbox);
3375
3376         sd->box_search = pref_box_new(vbox, FALSE, GTK_ORIENTATION_VERTICAL, PREF_PAD_GAP);
3377
3378         hbox = pref_box_new(sd->box_search, FALSE, GTK_ORIENTATION_HORIZONTAL, PREF_PAD_SPACE);
3379
3380         pref_label_new(hbox, _("Search:"));
3381
3382         sd->menu_path = menu_choice_menu(text_search_menu_path, sizeof(text_search_menu_path) / sizeof(MatchList),
3383                                          G_CALLBACK(menu_choice_path_cb), sd);
3384         gq_gtk_box_pack_start(GTK_BOX(hbox), sd->menu_path, FALSE, FALSE, 0);
3385         gtk_widget_show(sd->menu_path);
3386
3387         hbox2 = pref_box_new(hbox, TRUE, GTK_ORIENTATION_HORIZONTAL, PREF_PAD_SPACE);
3388         combo = tab_completion_new_with_history(&sd->path_entry, sd->search_dir_fd->path,
3389                                                 "search_path", -1,
3390                                                 nullptr, nullptr);
3391         tab_completion_add_select_button(sd->path_entry, nullptr, TRUE);
3392         gq_gtk_box_pack_start(GTK_BOX(hbox2), combo, TRUE, TRUE, 0);
3393         gtk_widget_show(combo);
3394         sd->check_recurse = pref_checkbox_new_int(hbox2, _("Recurse"),
3395                                                   sd->search_path_recurse, &sd->search_path_recurse);
3396
3397         sd->collection = pref_box_new(hbox, TRUE, GTK_ORIENTATION_HORIZONTAL, PREF_PAD_SPACE);
3398         sd->collection_entry = gtk_entry_new();
3399         gq_gtk_entry_set_text(GTK_ENTRY(sd->collection_entry), "");
3400         gq_gtk_box_pack_start(GTK_BOX(sd->collection), sd->collection_entry, TRUE, TRUE, 0);
3401         gtk_widget_show(sd->collection_entry);
3402
3403         sd->fd_button = gtk_button_new_with_label("...");
3404         g_signal_connect(G_OBJECT(sd->fd_button), "clicked", G_CALLBACK(select_collection_clicked_cb), sd);
3405         gq_gtk_box_pack_start(GTK_BOX(sd->collection), sd->fd_button, FALSE, FALSE, 0);
3406         gtk_widget_show(sd->fd_button);
3407
3408         gtk_widget_hide(sd->collection);
3409
3410         /* Search for file name */
3411         hbox = menu_choice(sd->box_search, &sd->check_name, &sd->menu_name,
3412                            _("File"), &sd->match_name_enable,
3413                            text_search_menu_name, sizeof(text_search_menu_name) / sizeof(MatchList),
3414                            G_CALLBACK(menu_choice_name_cb), sd);
3415         combo = history_combo_new(&sd->entry_name, "", "search_name", -1);
3416         gq_gtk_box_pack_start(GTK_BOX(hbox), combo, TRUE, TRUE, 0);
3417         gtk_widget_show(combo);
3418         pref_checkbox_new_int(hbox, _("Match case"),
3419                               sd->search_name_match_case, &sd->search_name_match_case);
3420         pref_checkbox_new_int(hbox, _("Symbolic link"), sd->search_name_symbolic_link, &sd->search_name_symbolic_link);
3421         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.");
3422
3423         /* Search for file size */
3424         hbox = menu_choice(sd->box_search, &sd->check_size, &sd->menu_size,
3425                            _("File size is"), &sd->match_size_enable,
3426                            text_search_menu_size, sizeof(text_search_menu_size) / sizeof(MatchList),
3427                            G_CALLBACK(menu_choice_size_cb), sd);
3428         sd->spin_size = menu_spin(hbox, 0, 1024*1024*1024, sd->search_size,
3429                                   G_CALLBACK(menu_choice_spin_cb), &sd->search_size);
3430         hbox2 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, PREF_PAD_SPACE);
3431         gq_gtk_box_pack_start(GTK_BOX(hbox), hbox2, FALSE, FALSE, 0);
3432         pref_label_new(hbox2, _("and"));
3433         sd->spin_size_end = menu_spin(hbox2, 0, 1024*1024*1024, sd->search_size_end,
3434                                       G_CALLBACK(menu_choice_spin_cb), &sd->search_size_end);
3435
3436         /* Search for file date */
3437         hbox = menu_choice(sd->box_search, &sd->check_date, &sd->menu_date,
3438                            _("File date is"), &sd->match_date_enable,
3439                            text_search_menu_date, sizeof(text_search_menu_date) / sizeof(MatchList),
3440                            G_CALLBACK(menu_choice_date_cb), sd);
3441
3442         sd->date_sel = date_selection_new();
3443         date_selection_time_set(sd->date_sel, time(nullptr));
3444         gq_gtk_box_pack_start(GTK_BOX(hbox), sd->date_sel, FALSE, FALSE, 0);
3445         gtk_widget_show(sd->date_sel);
3446
3447         hbox2 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, PREF_PAD_SPACE);
3448         gq_gtk_box_pack_start(GTK_BOX(hbox), hbox2, FALSE, FALSE, 0);
3449         pref_label_new(hbox2, _("and"));
3450         sd->date_sel_end = date_selection_new();
3451         date_selection_time_set(sd->date_sel_end, time(nullptr));
3452         gq_gtk_box_pack_start(GTK_BOX(hbox2), sd->date_sel_end, FALSE, FALSE, 0);
3453         gtk_widget_show(sd->date_sel_end);
3454
3455         sd->date_type = gtk_combo_box_text_new();
3456         gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(sd->date_type), _("Modified"));
3457         gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(sd->date_type), _("Status Changed"));
3458         gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(sd->date_type), _("Original"));
3459         gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(sd->date_type), _("Digitized"));
3460         gq_gtk_box_pack_start(GTK_BOX(hbox), sd->date_type, FALSE, FALSE, 0);
3461         gtk_combo_box_set_active(GTK_COMBO_BOX(sd->date_type), 0);
3462         gtk_widget_set_tooltip_text(sd->date_type, "Modified (mtime)\nStatus Changed (ctime)\nOriginal (Exif.Photo.DateTimeOriginal)\nDigitized (Exif.Photo.DateTimeDigitized)");
3463         gtk_widget_show(sd->date_type);
3464
3465         /* Search for image dimensions */
3466         hbox = menu_choice(sd->box_search, &sd->check_dimensions, &sd->menu_dimensions,
3467                            _("Image dimensions are"), &sd->match_dimensions_enable,
3468                            text_search_menu_size, sizeof(text_search_menu_size) / sizeof(MatchList),
3469                            G_CALLBACK(menu_choice_dimensions_cb), sd);
3470         pad_box = pref_box_new(hbox, FALSE, GTK_ORIENTATION_HORIZONTAL, 2);
3471         sd->spin_width = menu_spin(pad_box, 0, 1000000, sd->search_width,
3472                                    G_CALLBACK(menu_choice_spin_cb), &sd->search_width);
3473         pref_label_new(pad_box, "x");
3474         sd->spin_height = menu_spin(pad_box, 0, 1000000, sd->search_height,
3475                                     G_CALLBACK(menu_choice_spin_cb), &sd->search_height);
3476         hbox2 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 2);
3477         gq_gtk_box_pack_start(GTK_BOX(hbox), hbox2, FALSE, FALSE, 0);
3478         pref_label_new(hbox2, _("and"));
3479         pref_spacer(hbox2, PREF_PAD_SPACE - 2*2);
3480         sd->spin_width_end = menu_spin(hbox2, 0, 1000000, sd->search_width_end,
3481                                        G_CALLBACK(menu_choice_spin_cb), &sd->search_width_end);
3482         pref_label_new(hbox2, "x");
3483         sd->spin_height_end = menu_spin(hbox2, 0, 1000000, sd->search_height_end,
3484                                         G_CALLBACK(menu_choice_spin_cb), &sd->search_height_end);
3485
3486         /* Search for image similarity */
3487         hbox = menu_choice(sd->box_search, &sd->check_similarity, nullptr,
3488                            _("Image content is"), &sd->match_similarity_enable,
3489                            nullptr, 0, nullptr, sd);
3490         sd->spin_similarity = menu_spin(hbox, 80, 100, sd->search_similarity,
3491                                         G_CALLBACK(menu_choice_spin_cb), &sd->search_similarity);
3492
3493         /* xgettext:no-c-format */
3494         pref_label_new(hbox, _("% similar to"));
3495
3496         combo = tab_completion_new_with_history(&sd->entry_similarity,
3497                                                 (sd->search_similarity_path) ? sd->search_similarity_path : "",
3498                                                 "search_similarity_path", -1, nullptr, nullptr);
3499         tab_completion_add_select_button(sd->entry_similarity, nullptr, FALSE);
3500         gq_gtk_box_pack_start(GTK_BOX(hbox), combo, TRUE, TRUE, 0);
3501         gtk_widget_show(combo);
3502         pref_checkbox_new_int(hbox, _("Ignore rotation"),
3503                                 options->rot_invariant_sim, &options->rot_invariant_sim);
3504
3505         /* Search for image keywords */
3506         hbox = menu_choice(sd->box_search, &sd->check_keywords, &sd->menu_keywords,
3507                            _("Keywords"), &sd->match_keywords_enable,
3508                            text_search_menu_keyword, sizeof(text_search_menu_keyword) / sizeof(MatchList),
3509                            G_CALLBACK(menu_choice_keyword_cb), sd);
3510         sd->entry_keywords = gtk_entry_new();
3511         gq_gtk_box_pack_start(GTK_BOX(hbox), sd->entry_keywords, TRUE, TRUE, 0);
3512         gtk_widget_set_sensitive(sd->entry_keywords, sd->match_keywords_enable);
3513         g_signal_connect(G_OBJECT(sd->check_keywords), "toggled",
3514                          G_CALLBACK(menu_choice_check_cb), sd->entry_keywords);
3515         gtk_widget_show(sd->entry_keywords);
3516
3517         /* Search for image comment */
3518         hbox = menu_choice(sd->box_search, &sd->check_comment, &sd->menu_comment,
3519                         _("Comment"), &sd->match_comment_enable,
3520                         text_search_menu_comment, sizeof(text_search_menu_comment) / sizeof(MatchList),
3521                         G_CALLBACK(menu_choice_comment_cb), sd);
3522         sd->entry_comment = gtk_entry_new();
3523         gq_gtk_box_pack_start(GTK_BOX(hbox), sd->entry_comment, TRUE, TRUE, 0);
3524         gtk_widget_set_sensitive(sd->entry_comment, sd->match_comment_enable);
3525         g_signal_connect(G_OBJECT(sd->check_comment), "toggled",
3526                         G_CALLBACK(menu_choice_check_cb), sd->entry_comment);
3527         gtk_widget_show(sd->entry_comment);
3528         pref_checkbox_new_int(hbox, _("Match case"),
3529                               sd->search_comment_match_case, &sd->search_comment_match_case);
3530         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.");
3531
3532         /* Search for image rating */
3533         hbox = menu_choice(sd->box_search, &sd->check_rating, &sd->menu_rating,
3534                            _("Image rating is"), &sd->match_rating_enable,
3535                            text_search_menu_rating, sizeof(text_search_menu_rating) / sizeof(MatchList),
3536                            G_CALLBACK(menu_choice_rating_cb), sd);
3537         sd->spin_size = menu_spin(hbox, -1, 5, sd->search_rating,
3538                                   G_CALLBACK(menu_choice_spin_cb), &sd->search_rating);
3539         hbox2 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, PREF_PAD_SPACE);
3540         gq_gtk_box_pack_start(GTK_BOX(hbox), hbox2, FALSE, FALSE, 0);
3541         pref_label_new(hbox2, _("and"));
3542         sd->spin_rating_end = menu_spin(hbox2, -1, 5, sd->search_rating_end,
3543                                       G_CALLBACK(menu_choice_spin_cb), &sd->search_rating_end);
3544
3545         /* Search for images within a specified range of a lat/long coordinate
3546         */
3547         hbox = menu_choice(sd->box_search, &sd->check_gps, &sd->menu_gps,
3548                            _("Image is"), &sd->match_gps_enable,
3549                            text_search_menu_gps, sizeof(text_search_menu_gps) / sizeof(MatchList),
3550                            G_CALLBACK(menu_choice_gps_cb), sd);
3551
3552         hbox2 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, PREF_PAD_SPACE);
3553         gq_gtk_box_pack_start(GTK_BOX(hbox), hbox2, FALSE, FALSE, 0);
3554         sd->spin_gps = menu_spin(hbox2, 1, 9999, sd->search_gps,
3555                                                                    G_CALLBACK(menu_choice_spin_cb), &sd->search_gps);
3556
3557         sd->units_gps = gtk_combo_box_text_new();
3558         gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(sd->units_gps), _("km"));
3559         gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(sd->units_gps), _("miles"));
3560         gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(sd->units_gps), _("n.m."));
3561         gq_gtk_box_pack_start(GTK_BOX(hbox2), sd->units_gps, FALSE, FALSE, 0);
3562         gtk_combo_box_set_active(GTK_COMBO_BOX(sd->units_gps), 0);
3563         gtk_widget_set_tooltip_text(sd->units_gps, "kilometres, miles or nautical miles");
3564         gtk_widget_show(sd->units_gps);
3565
3566         pref_label_new(hbox2, _("from"));
3567
3568         sd->entry_gps_coord = gtk_entry_new();
3569         gtk_editable_set_editable(GTK_EDITABLE(sd->entry_gps_coord), TRUE);
3570         gtk_widget_set_has_tooltip(sd->entry_gps_coord, TRUE);
3571         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"));
3572         gq_gtk_box_pack_start(GTK_BOX(hbox2), sd->entry_gps_coord, TRUE, TRUE, 0);
3573         gtk_widget_set_sensitive(sd->entry_gps_coord, TRUE);
3574
3575         gtk_widget_show(sd->entry_gps_coord);
3576
3577         /* Search for image class */
3578         hbox = menu_choice(sd->box_search, &sd->check_class, &sd->menu_class,
3579                            _("Image class"), &sd->match_class_enable,
3580                            text_search_menu_class, sizeof(text_search_menu_class) / sizeof(MatchList),
3581                            G_CALLBACK(menu_choice_class_cb), sd);
3582
3583         sd->class_type = gtk_combo_box_text_new();
3584         gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(sd->class_type), _("Image"));
3585         gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(sd->class_type), _("Raw Image"));
3586         gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(sd->class_type), _("Video"));
3587         gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(sd->class_type), _("Document"));
3588         gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(sd->class_type), _("Metadata"));
3589         gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(sd->class_type), _("Unknown"));
3590         gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(sd->class_type), _("Broken"));
3591         gq_gtk_box_pack_start(GTK_BOX(hbox), sd->class_type, FALSE, FALSE, 0);
3592         gtk_combo_box_set_active(GTK_COMBO_BOX(sd->class_type), 0);
3593         gtk_widget_show(sd->class_type);
3594
3595         /* Search for image marks */
3596         hbox = menu_choice(sd->box_search, &sd->check_class, &sd->menu_marks,
3597                            _("Marks"), &sd->match_marks_enable,
3598                            text_search_menu_marks, sizeof(text_search_menu_marks) / sizeof(MatchList),
3599                            G_CALLBACK(menu_choice_marks_cb), sd);
3600
3601         sd->marks_type = gtk_combo_box_text_new();
3602         gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(sd->marks_type), _("Any mark"));
3603         for (i = 0; i < FILEDATA_MARKS_SIZE; i++)
3604                 {
3605                 marks_string = g_strdup_printf("%s%d", _("Mark "), i + 1);
3606                 if (g_strcmp0(marks_string, options->marks_tooltips[i]) != 0)
3607                         {
3608                         g_free(marks_string);
3609                         marks_string = g_strdup_printf("%s%d %s", _("Mark "), i + 1,
3610                                                                                         options->marks_tooltips[i]);
3611                         gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(sd->marks_type), marks_string);
3612                         }
3613                 else
3614                         {
3615                         gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(sd->marks_type), marks_string);
3616                         }
3617                 g_free(marks_string);
3618                 }
3619         gq_gtk_box_pack_start(GTK_BOX(hbox), sd->marks_type, FALSE, FALSE, 0);
3620         gtk_combo_box_set_active(GTK_COMBO_BOX(sd->marks_type), 0);
3621         gtk_widget_show(sd->marks_type);
3622
3623         /* Done the types of searches */
3624
3625         scrolled = gq_gtk_scrolled_window_new(nullptr, nullptr);
3626         gq_gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled), GTK_SHADOW_IN);
3627         gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled),
3628                                        GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
3629         gq_gtk_box_pack_start(GTK_BOX(vbox), scrolled, TRUE, TRUE, 0);
3630         gtk_widget_show(scrolled);
3631
3632         store = gtk_list_store_new(8, G_TYPE_POINTER, G_TYPE_INT, GDK_TYPE_PIXBUF,
3633                                    G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING,
3634                                    G_TYPE_STRING, G_TYPE_STRING);
3635
3636         /* set up sorting */
3637         sortable = GTK_TREE_SORTABLE(store);
3638         gtk_tree_sortable_set_sort_func(sortable, SEARCH_COLUMN_RANK, search_result_sort_cb,
3639                                   GINT_TO_POINTER(SEARCH_COLUMN_RANK), nullptr);
3640         gtk_tree_sortable_set_sort_func(sortable, SEARCH_COLUMN_NAME, search_result_sort_cb,
3641                                   GINT_TO_POINTER(SEARCH_COLUMN_NAME), nullptr);
3642         gtk_tree_sortable_set_sort_func(sortable, SEARCH_COLUMN_SIZE, search_result_sort_cb,
3643                                   GINT_TO_POINTER(SEARCH_COLUMN_SIZE), nullptr);
3644         gtk_tree_sortable_set_sort_func(sortable, SEARCH_COLUMN_DATE, search_result_sort_cb,
3645                                   GINT_TO_POINTER(SEARCH_COLUMN_DATE), nullptr);
3646         gtk_tree_sortable_set_sort_func(sortable, SEARCH_COLUMN_DIMENSIONS, search_result_sort_cb,
3647                                   GINT_TO_POINTER(SEARCH_COLUMN_DIMENSIONS), nullptr);
3648         gtk_tree_sortable_set_sort_func(sortable, SEARCH_COLUMN_PATH, search_result_sort_cb,
3649                                   GINT_TO_POINTER(SEARCH_COLUMN_PATH), nullptr);
3650
3651 #if 0
3652         /* by default, search results are unsorted until user selects a sort column - for speed,
3653          * using sort slows search speed by an order of magnitude with 1000's of results :-/
3654          */
3655         gtk_tree_sortable_set_sort_column_id(sortable, SEARCH_COLUMN_PATH, GTK_SORT_ASCENDING);
3656 #endif
3657
3658         sd->result_view = gtk_tree_view_new_with_model(GTK_TREE_MODEL(store));
3659         g_object_unref(store);
3660         gq_gtk_container_add(GTK_WIDGET(scrolled), sd->result_view);
3661         gtk_widget_show(sd->result_view);
3662
3663         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(sd->result_view));
3664         gtk_tree_selection_set_mode(GTK_TREE_SELECTION(selection), GTK_SELECTION_MULTIPLE);
3665         gtk_tree_selection_set_select_function(selection, search_result_select_cb, sd, nullptr);
3666
3667         gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(sd->result_view), TRUE);
3668         gtk_tree_view_set_enable_search(GTK_TREE_VIEW(sd->result_view), FALSE);
3669
3670         search_result_add_column(sd, SEARCH_COLUMN_RANK, _("Rank"), FALSE, FALSE);
3671         search_result_add_column(sd, SEARCH_COLUMN_THUMB, _("Thumb"), TRUE, FALSE);
3672         search_result_add_column(sd, SEARCH_COLUMN_NAME, _("Name"), FALSE, FALSE);
3673         search_result_add_column(sd, SEARCH_COLUMN_SIZE, _("Size"), FALSE, TRUE);
3674         search_result_add_column(sd, SEARCH_COLUMN_DATE, _("Date"), FALSE, TRUE);
3675         search_result_add_column(sd, SEARCH_COLUMN_DIMENSIONS, _("Dimensions"), FALSE, FALSE);
3676         search_result_add_column(sd, SEARCH_COLUMN_PATH, _("Path"), FALSE, FALSE);
3677
3678         search_dnd_init(sd);
3679
3680         g_signal_connect(G_OBJECT(sd->result_view), "button_press_event",
3681                          G_CALLBACK(search_result_press_cb), sd);
3682         g_signal_connect(G_OBJECT(sd->result_view), "button_release_event",
3683                          G_CALLBACK(search_result_release_cb), sd);
3684         g_signal_connect(G_OBJECT(sd->result_view), "key_press_event",
3685                          G_CALLBACK(search_result_keypress_cb), sd);
3686
3687         hbox = pref_box_new(vbox, FALSE, GTK_ORIENTATION_HORIZONTAL, 0);
3688
3689         sd->button_thumbs = pref_checkbox_new(hbox, _("Thumbnails"), FALSE,
3690                                               G_CALLBACK(search_thumb_toggle_cb), sd);
3691         gtk_widget_set_tooltip_text(GTK_WIDGET(sd->button_thumbs), "Ctrl-T");
3692
3693         frame = gtk_frame_new(nullptr);
3694         DEBUG_NAME(frame);
3695         gq_gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_IN);
3696         gq_gtk_box_pack_start(GTK_BOX(hbox), frame, TRUE, TRUE, PREF_PAD_SPACE);
3697         gtk_widget_show(frame);
3698
3699         sd->label_status = gtk_label_new("");
3700         gtk_widget_set_size_request(sd->label_status, 50, -1);
3701         gq_gtk_container_add(GTK_WIDGET(frame), sd->label_status);
3702         gtk_widget_show(sd->label_status);
3703
3704         sd->label_progress = gtk_progress_bar_new();
3705         gtk_widget_set_size_request(sd->label_progress, 50, -1);
3706
3707         gtk_progress_bar_set_text(GTK_PROGRESS_BAR(sd->label_progress), "");
3708         gtk_progress_bar_set_show_text(GTK_PROGRESS_BAR(sd->label_progress), TRUE);
3709
3710         gq_gtk_box_pack_start(GTK_BOX(hbox), sd->label_progress, TRUE, TRUE, 0);
3711         gtk_widget_show(sd->label_progress);
3712
3713         sd->spinner = gtk_spinner_new();
3714         gq_gtk_box_pack_start(GTK_BOX(hbox), sd->spinner, FALSE, FALSE, 0);
3715         gtk_widget_show(sd->spinner);
3716
3717         sd->button_help = pref_button_new(hbox, GQ_ICON_HELP, _("Help"), G_CALLBACK(search_window_help_cb), sd);
3718         gtk_widget_set_tooltip_text(GTK_WIDGET(sd->button_help), "F1");
3719         gtk_widget_set_sensitive(sd->button_help, TRUE);
3720         pref_spacer(hbox, PREF_PAD_BUTTON_GAP);
3721         sd->button_start = pref_button_new(hbox, GQ_ICON_FIND, _("Find"),
3722                                            G_CALLBACK(search_start_cb), sd);
3723         gtk_widget_set_tooltip_text(GTK_WIDGET(sd->button_start), "Ctrl-Return");
3724         pref_spacer(hbox, PREF_PAD_BUTTON_GAP);
3725         sd->button_stop = pref_button_new(hbox, GQ_ICON_STOP, _("Stop"),
3726                                           G_CALLBACK(search_start_cb), sd);
3727         gtk_widget_set_tooltip_text(GTK_WIDGET(sd->button_stop), "Ctrl-Return");
3728         gtk_widget_set_sensitive(sd->button_stop, FALSE);
3729         pref_spacer(hbox, PREF_PAD_BUTTON_GAP);
3730         sd->button_close = pref_button_new(hbox, GQ_ICON_CLOSE, _("Close"), G_CALLBACK(search_window_close_cb), sd);
3731         gtk_widget_set_tooltip_text(GTK_WIDGET(sd->button_close), "Ctrl-W");
3732         gtk_widget_set_sensitive(sd->button_close, TRUE);
3733
3734         search_result_thumb_enable(sd, TRUE);
3735         search_result_thumb_enable(sd, FALSE);
3736         column = gtk_tree_view_get_column(GTK_TREE_VIEW(sd->result_view), SEARCH_COLUMN_RANK - 1);
3737         gtk_tree_view_column_set_visible(column, FALSE);
3738
3739         search_status_update(sd);
3740         search_progress_update(sd, FALSE, -1.0);
3741
3742         search_window_list = g_list_append(search_window_list, sd);
3743
3744         file_data_register_notify_func(search_notify_cb, sd, NOTIFY_PRIORITY_MEDIUM);
3745
3746         gtk_widget_show(sd->window);
3747 }
3748
3749 /*
3750  *-------------------------------------------------------------------
3751  * maintenance (move, delete, etc.)
3752  *-------------------------------------------------------------------
3753  */
3754
3755 static void search_result_change_path(SearchData *sd, FileData *fd)
3756 {
3757         GtkTreeModel *store;
3758         GtkTreeIter iter;
3759         gboolean valid;
3760
3761         store = gtk_tree_view_get_model(GTK_TREE_VIEW(sd->result_view));
3762         valid = gtk_tree_model_get_iter_first(store, &iter);
3763         while (valid)
3764                 {
3765                 GtkTreeIter current;
3766                 MatchFileData *mfd;
3767
3768                 current = iter;
3769                 valid = gtk_tree_model_iter_next(store, &iter);
3770
3771                 gtk_tree_model_get(store, &current, SEARCH_COLUMN_POINTER, &mfd, -1);
3772                 if (mfd->fd == fd)
3773                         {
3774                         if (fd->change && fd->change->dest)
3775                                 {
3776                                 gtk_list_store_set(GTK_LIST_STORE(store), &current,
3777                                                    SEARCH_COLUMN_NAME, mfd->fd->name,
3778                                                    SEARCH_COLUMN_PATH, mfd->fd->path, -1);
3779                                 }
3780                         else
3781                                 {
3782                                 search_result_remove_item(sd, mfd, &current);
3783                                 }
3784                         }
3785                 }
3786 }
3787
3788 static void search_notify_cb(FileData *fd, NotifyType type, gpointer data)
3789 {
3790         auto sd = static_cast<SearchData *>(data);
3791
3792         if (!(type & NOTIFY_CHANGE) || !fd->change) return;
3793
3794         DEBUG_1("Notify search: %s %04x", fd->path, type);
3795
3796         switch (fd->change->type)
3797                 {
3798                 case FILEDATA_CHANGE_MOVE:
3799                 case FILEDATA_CHANGE_RENAME:
3800                 case FILEDATA_CHANGE_DELETE:
3801                         search_result_change_path(sd, fd);
3802                         break;
3803                 case FILEDATA_CHANGE_COPY:
3804                 case FILEDATA_CHANGE_UNSPECIFIED:
3805                 case FILEDATA_CHANGE_WRITE_METADATA:
3806                         break;
3807                 }
3808 }
3809
3810 void mfd_list_free(GList *list)
3811 {
3812         GList *work;
3813
3814         work = list;
3815         while (work)
3816                 {
3817                 auto mfd = static_cast<MatchFileData *>(work->data);
3818                 file_data_unref((FileData *)mfd->fd);
3819                 work = work->next;
3820                 }
3821
3822         g_list_free(list);
3823 }
3824
3825 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */