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