4d21789fcfc55a670f00ccc9177c96e977af5ffe
[geeqie.git] / src / pan-view / pan-view-search.cc
1 /*
2  * Copyright (C) 2006 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 "pan-view-search.h"
23
24 #include "image.h"
25 #include "pan-calendar.h"
26 #include "pan-util.h"
27 #include "pan-view.h"
28 #include "ui-tabcomp.h"
29 #include "ui-misc.h"
30
31 PanViewSearchUi *pan_search_ui_new(PanWindow *pw)
32 {
33         auto ui = g_new0(PanViewSearchUi, 1);
34         GtkWidget *combo;
35         GtkWidget *hbox;
36
37         // Build the actual search UI.
38         ui->search_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, PREF_PAD_SPACE);
39         pref_spacer(ui->search_box, 0);
40         pref_label_new(ui->search_box, _("Find:"));
41
42         hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, PREF_PAD_SPACE);
43         gtk_box_pack_start(GTK_BOX(ui->search_box), hbox, TRUE, TRUE, 0);
44         gtk_widget_show(hbox);
45
46         combo = tab_completion_new_with_history(&ui->search_entry, "", "pan_view_search", -1,
47                                                 pan_search_activate_cb, pw);
48         gtk_box_pack_start(GTK_BOX(hbox), combo, TRUE, TRUE, 0);
49         gtk_widget_show(combo);
50
51         ui->search_label = gtk_label_new("");
52         gtk_box_pack_start(GTK_BOX(hbox), ui->search_label, TRUE, TRUE, 0);
53         gtk_widget_show(ui->search_label);
54
55         // Build the spin-button to show/hide the search UI.
56         ui->search_button = gtk_toggle_button_new();
57         gtk_button_set_relief(GTK_BUTTON(ui->search_button), GTK_RELIEF_NONE);
58         gtk_button_set_focus_on_click(GTK_BUTTON(ui->search_button), FALSE);
59         hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, PREF_PAD_GAP);
60         gtk_container_add(GTK_CONTAINER(ui->search_button), hbox);
61         gtk_widget_show(hbox);
62         ui->search_button_arrow = gtk_image_new_from_icon_name("pan-up", GTK_ICON_SIZE_BUTTON);
63         gtk_box_pack_start(GTK_BOX(hbox), ui->search_button_arrow, FALSE, FALSE, 0);
64         gtk_widget_show(ui->search_button_arrow);
65         pref_label_new(hbox, _("Find"));
66
67         g_signal_connect(G_OBJECT(ui->search_button), "clicked",
68                          G_CALLBACK(pan_search_toggle_cb), pw);
69
70         return ui;
71 }
72
73 void pan_search_ui_destroy(PanViewSearchUi **ui_ptr)
74 {
75         if (ui_ptr == nullptr || *ui_ptr == nullptr) return;
76
77         g_free(*ui_ptr);
78         *ui_ptr = nullptr;
79 }
80
81 static void pan_search_status(PanWindow *pw, const gchar *text)
82 {
83         gtk_label_set_text(GTK_LABEL(pw->search_ui->search_label), (text) ? text : "");
84 }
85
86 static gint pan_search_by_path(PanWindow *pw, const gchar *path)
87 {
88         PanItem *pi;
89         GList *list;
90         GList *found;
91         PanItemType type;
92         gchar *buf;
93
94         type = (pw->size > PAN_IMAGE_SIZE_THUMB_LARGE) ? PAN_ITEM_IMAGE : PAN_ITEM_THUMB;
95
96         list = pan_item_find_by_path(pw, type, path, FALSE, FALSE);
97         if (!list) return FALSE;
98
99         found = g_list_find(list, pw->click_pi);
100         if (found && found->next)
101                 {
102                 found = found->next;
103                 pi = static_cast<PanItem *>(found->data);
104                 }
105         else
106                 {
107                 pi = static_cast<PanItem *>(list->data);
108                 }
109
110         pan_info_update(pw, pi);
111         image_scroll_to_point(pw->imd, pi->x + pi->width / 2, pi->y + pi->height / 2, 0.5, 0.5);
112
113         buf = g_strdup_printf("%s ( %d / %d )",
114                               (path[0] == G_DIR_SEPARATOR) ? _("path found") : _("filename found"),
115                               g_list_index(list, pi) + 1,
116                               g_list_length(list));
117         pan_search_status(pw, buf);
118         g_free(buf);
119
120         g_list_free(list);
121
122         return TRUE;
123 }
124
125 static gboolean pan_search_by_partial(PanWindow *pw, const gchar *text)
126 {
127         PanItem *pi;
128         GList *list;
129         GList *found;
130         PanItemType type;
131         gchar *buf;
132
133         type = (pw->size > PAN_IMAGE_SIZE_THUMB_LARGE) ? PAN_ITEM_IMAGE : PAN_ITEM_THUMB;
134
135         list = pan_item_find_by_path(pw, type, text, TRUE, FALSE);
136         if (!list) list = pan_item_find_by_path(pw, type, text, FALSE, TRUE);
137         if (!list)
138                 {
139                 gchar *needle;
140
141                 needle = g_utf8_strdown(text, -1);
142                 list = pan_item_find_by_path(pw, type, needle, TRUE, TRUE);
143                 g_free(needle);
144                 }
145         if (!list) return FALSE;
146
147         found = g_list_find(list, pw->click_pi);
148         if (found && found->next)
149                 {
150                 found = found->next;
151                 pi = static_cast<PanItem *>(found->data);
152                 }
153         else
154                 {
155                 pi = static_cast<PanItem *>(list->data);
156                 }
157
158         pan_info_update(pw, pi);
159         image_scroll_to_point(pw->imd, pi->x + pi->width / 2, pi->y + pi->height / 2, 0.5, 0.5);
160
161         buf = g_strdup_printf("%s ( %d / %d )",
162                               _("partial match"),
163                               g_list_index(list, pi) + 1,
164                               g_list_length(list));
165         pan_search_status(pw, buf);
166         g_free(buf);
167
168         g_list_free(list);
169
170         return TRUE;
171 }
172
173 static gboolean valid_date_separator(gchar c)
174 {
175         return (c == '/' || c == '-' || c == ' ' || c == '.' || c == ',');
176 }
177
178 static GList *pan_search_by_date_val(PanWindow *pw, PanItemType type,
179                                      gint year, gint month, gint day,
180                                      const gchar *key)
181 {
182         GList *list = nullptr;
183         GList *work;
184
185         work = g_list_last(pw->list_static);
186         while (work)
187                 {
188                 PanItem *pi;
189
190                 pi = static_cast<PanItem *>(work->data);
191                 work = work->prev;
192
193                 if (pi->fd && (pi->type == type || type == PAN_ITEM_NONE) &&
194                     ((!key && !pi->key) || (key && pi->key && strcmp(key, pi->key) == 0)))
195                         {
196                         struct tm *tl;
197
198                         tl = localtime(&pi->fd->date);
199                         if (tl)
200                                 {
201                                 gint match;
202
203                                 match = (tl->tm_year == year - 1900);
204                                 if (match && month >= 0) match = (tl->tm_mon == month - 1);
205                                 if (match && day > 0) match = (tl->tm_mday == day);
206
207                                 if (match) list = g_list_prepend(list, pi);
208                                 }
209                         }
210                 }
211
212         return g_list_reverse(list);
213 }
214
215 static gboolean pan_search_by_date(PanWindow *pw, const gchar *text)
216 {
217         PanItem *pi = nullptr;
218         GList *list = nullptr;
219         GList *found;
220         gint year;
221         gint month = -1;
222         gint day = -1;
223         gchar *ptr;
224         gchar *mptr;
225         struct tm *lt;
226         time_t t;
227         gchar *message;
228         gchar *buf;
229         gchar *buf_count;
230
231         if (!text) return FALSE;
232
233         ptr = const_cast<gchar *>(text);
234         while (*ptr != '\0')
235                 {
236                 if (!g_unichar_isdigit(*ptr) && !valid_date_separator(*ptr)) return FALSE;
237                 ptr++;
238                 }
239
240         t = time(nullptr);
241         if (t == -1) return FALSE;
242         lt = localtime(&t);
243         if (!lt) return FALSE;
244
245         if (valid_date_separator(*text))
246                 {
247                 year = -1;
248                 mptr = const_cast<gchar *>(text);
249                 }
250         else
251                 {
252                 year = static_cast<gint>(strtol(text, &mptr, 10));
253                 if (mptr == text) return FALSE;
254                 }
255
256         if (*mptr != '\0' && valid_date_separator(*mptr))
257                 {
258                 gchar *dptr;
259
260                 mptr++;
261                 month = strtol(mptr, &dptr, 10);
262                 if (dptr == mptr)
263                         {
264                         if (valid_date_separator(*dptr))
265                                 {
266                                 month = lt->tm_mon + 1;
267                                 dptr++;
268                                 }
269                         else
270                                 {
271                                 month = -1;
272                                 }
273                         }
274                 if (dptr != mptr && *dptr != '\0' && valid_date_separator(*dptr))
275                         {
276                         gchar *eptr;
277                         dptr++;
278                         day = strtol(dptr, &eptr, 10);
279                         if (dptr == eptr)
280                                 {
281                                 day = lt->tm_mday;
282                                 }
283                         }
284                 }
285
286         if (year == -1)
287                 {
288                 year = lt->tm_year + 1900;
289                 }
290         else if (year < 100)
291                 {
292                 if (year > 70)
293                         year+= 1900;
294                 else
295                         year+= 2000;
296                 }
297
298         if (year < 1970 ||
299             month < -1 || month == 0 || month > 12 ||
300             day < -1 || day == 0 || day > 31) return FALSE;
301
302         t = pan_date_to_time(year, month, day);
303         if (t < 0) return FALSE;
304
305         if (pw->layout == PAN_LAYOUT_CALENDAR)
306                 {
307                 list = pan_search_by_date_val(pw, PAN_ITEM_BOX, year, month, day, "day");
308                 }
309         else
310                 {
311                 PanItemType type;
312
313                 type = (pw->size > PAN_IMAGE_SIZE_THUMB_LARGE) ? PAN_ITEM_IMAGE : PAN_ITEM_THUMB;
314                 list = pan_search_by_date_val(pw, type, year, month, day, nullptr);
315                 }
316
317         if (list)
318                 {
319                 found = g_list_find(list, pw->search_pi);
320                 if (found && found->next)
321                         {
322                         found = found->next;
323                         pi = static_cast<PanItem *>(found->data);
324                         }
325                 else
326                         {
327                         pi = static_cast<PanItem *>(list->data);
328                         }
329                 }
330
331         pw->search_pi = pi;
332
333         if (pw->layout == PAN_LAYOUT_CALENDAR && pi && pi->type == PAN_ITEM_BOX)
334                 {
335                 pan_info_update(pw, nullptr);
336                 pan_calendar_update(pw, pi);
337                 image_scroll_to_point(pw->imd,
338                                       pi->x + pi->width / 2,
339                                       pi->y + pi->height / 2, 0.5, 0.5);
340                 }
341         else if (pi)
342                 {
343                 pan_info_update(pw, pi);
344                 image_scroll_to_point(pw->imd,
345                                       pi->x - PAN_BOX_BORDER * 5 / 2,
346                                       pi->y, 0.0, 0.5);
347                 }
348
349         if (month > 0)
350                 {
351                 buf = pan_date_value_string(t, PAN_DATE_LENGTH_MONTH);
352                 if (day > 0)
353                         {
354                         gchar *tmp;
355                         tmp = buf;
356                         buf = g_strdup_printf("%d %s", day, tmp);
357                         g_free(tmp);
358                         }
359                 }
360         else
361                 {
362                 buf = pan_date_value_string(t, PAN_DATE_LENGTH_YEAR);
363                 }
364
365         if (pi)
366                 {
367                 buf_count = g_strdup_printf("( %d / %d )",
368                                             g_list_index(list, pi) + 1,
369                                             g_list_length(list));
370                 }
371         else
372                 {
373                 buf_count = g_strdup_printf("(%s)", _("no match"));
374                 }
375
376         message = g_strdup_printf("%s %s %s", _("Date:"), buf, buf_count);
377         g_free(buf);
378         g_free(buf_count);
379         pan_search_status(pw, message);
380         g_free(message);
381
382         g_list_free(list);
383
384         return TRUE;
385 }
386
387 void pan_search_activate_cb(const gchar *text, gpointer data)
388 {
389         auto pw = static_cast<PanWindow *>(data);
390
391         if (!text) return;
392
393         tab_completion_append_to_history(pw->search_ui->search_entry, text);
394
395         if (pan_search_by_path(pw, text)) return;
396
397         if ((pw->layout == PAN_LAYOUT_TIMELINE ||
398              pw->layout == PAN_LAYOUT_CALENDAR) &&
399             pan_search_by_date(pw, text))
400                 {
401                 return;
402                 }
403
404         if (pan_search_by_partial(pw, text)) return;
405
406         pan_search_status(pw, _("no match"));
407 }
408
409 void pan_search_activate(PanWindow *pw)
410 {
411         gchar *text;
412
413         text = g_strdup(gtk_entry_get_text(GTK_ENTRY(pw->search_ui->search_entry)));
414         pan_search_activate_cb(text, pw);
415         g_free(text);
416 }
417
418 void pan_search_toggle_cb(GtkWidget *button, gpointer data)
419 {
420         auto pw = static_cast<PanWindow *>(data);
421         PanViewSearchUi *ui = pw->search_ui;
422         gboolean visible;
423         GtkWidget *parent;
424
425         visible = gtk_widget_get_visible(ui->search_box);
426         if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button)) == visible) return;
427
428         if (visible)
429                 {
430                 gtk_widget_hide(ui->search_box);
431
432                 parent = gtk_widget_get_parent(ui->search_button_arrow);
433
434                 gtk_widget_destroy(ui->search_button_arrow);
435                 ui->search_button_arrow = gtk_image_new_from_icon_name("pan-up", GTK_ICON_SIZE_BUTTON);
436
437                 gtk_box_pack_start(GTK_BOX(parent), ui->search_button_arrow, FALSE, FALSE, 0);
438                 gtk_box_reorder_child(GTK_BOX(parent), ui->search_button_arrow, 0);
439
440                 gtk_widget_show(ui->search_button_arrow);
441                 }
442         else
443                 {
444                 gtk_widget_show(ui->search_box);
445
446                 parent = gtk_widget_get_parent(ui->search_button_arrow);
447
448                 gtk_widget_destroy(ui->search_button_arrow);
449                 ui->search_button_arrow = gtk_image_new_from_icon_name("pan-down", GTK_ICON_SIZE_BUTTON);
450
451                 gtk_box_pack_start(GTK_BOX(parent), ui->search_button_arrow, FALSE, FALSE, 0);
452                 gtk_box_reorder_child(GTK_BOX(parent), ui->search_button_arrow, 0);
453
454                 gtk_widget_show(ui->search_button_arrow);
455                 gtk_widget_grab_focus(ui->search_entry);
456                 }
457 }
458
459 void pan_search_toggle_visible(PanWindow *pw, gboolean enable)
460 {
461         PanViewSearchUi *ui = pw->search_ui;
462         if (pw->fs) return;
463
464         if (enable)
465                 {
466                 if (gtk_widget_get_visible(ui->search_box))
467                         {
468                         gtk_widget_grab_focus(ui->search_entry);
469                         }
470                 else
471                         {
472                         gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(ui->search_button), TRUE);
473                         }
474                 }
475         else
476                 {
477                 if (gtk_widget_get_visible(ui->search_entry))
478                         {
479                         if (gtk_widget_has_focus(ui->search_entry))
480                                 {
481                                 gtk_widget_grab_focus(GTK_WIDGET(pw->imd->widget));
482                                 }
483                         gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(ui->search_button), FALSE);
484                         }
485                 }
486 }