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