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