98a6f2952ca4c9d01b2d74e1e876c6864fc08ddd
[geeqie.git] / src / pan-view / pan-view-filter.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-filter.h"
23
24 #include "image.h"
25 #include "metadata.h"
26 #include "pan-item.h"
27 #include "pan-util.h"
28 #include "pan-view.h"
29 #include "ui_fileops.h"
30 #include "ui_tabcomp.h"
31 #include "ui_misc.h"
32
33 PanViewFilterUi *pan_filter_ui_new(PanWindow *pw)
34 {
35         PanViewFilterUi *ui = g_new0(PanViewFilterUi, 1);
36         GtkWidget *combo;
37         GtkWidget *hbox;
38         gint i;
39
40         /* Since we're using the GHashTable as a HashSet (in which key and value pointers
41          * are always identical), specifying key _and_ value destructor callbacks will
42          * cause a double-free.
43          */
44         {
45                 GtkTreeIter iter;
46                 ui->filter_mode_model = gtk_list_store_new(3, G_TYPE_INT, G_TYPE_STRING, G_TYPE_STRING);
47                 gtk_list_store_append(ui->filter_mode_model, &iter);
48                 gtk_list_store_set(ui->filter_mode_model, &iter,
49                                    0, PAN_VIEW_FILTER_REQUIRE, 1, _("Require"), 2, _("R"), -1);
50                 gtk_list_store_append(ui->filter_mode_model, &iter);
51                 gtk_list_store_set(ui->filter_mode_model, &iter,
52                                    0, PAN_VIEW_FILTER_EXCLUDE, 1, _("Exclude"), 2, _("E"), -1);
53                 gtk_list_store_append(ui->filter_mode_model, &iter);
54                 gtk_list_store_set(ui->filter_mode_model, &iter,
55                                    0, PAN_VIEW_FILTER_INCLUDE, 1, _("Include"), 2, _("I"), -1);
56                 gtk_list_store_append(ui->filter_mode_model, &iter);
57                 gtk_list_store_set(ui->filter_mode_model, &iter,
58                                    0, PAN_VIEW_FILTER_GROUP, 1, _("Group"), 2, _("G"), -1);
59
60                 ui->filter_mode_combo = gtk_combo_box_new_with_model(GTK_TREE_MODEL(ui->filter_mode_model));
61                 gtk_combo_box_set_focus_on_click(GTK_COMBO_BOX(ui->filter_mode_combo), FALSE);
62                 gtk_combo_box_set_active(GTK_COMBO_BOX(ui->filter_mode_combo), 0);
63
64                 GtkCellRenderer *render = gtk_cell_renderer_text_new();
65                 gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(ui->filter_mode_combo), render, TRUE);
66                 gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(ui->filter_mode_combo), render, "text", 1, NULL);
67         }
68
69         // Build the actual filter UI.
70         ui->filter_box = gtk_hbox_new(FALSE, PREF_PAD_SPACE);
71         pref_spacer(ui->filter_box, 0);
72         pref_label_new(ui->filter_box, _("Keyword Filter:"));
73
74         gtk_box_pack_start(GTK_BOX(ui->filter_box), ui->filter_mode_combo, FALSE, FALSE, 0);
75         gtk_widget_show(ui->filter_mode_combo);
76
77         hbox = gtk_hbox_new(TRUE, PREF_PAD_SPACE);
78         gtk_box_pack_start(GTK_BOX(ui->filter_box), hbox, TRUE, TRUE, 0);
79         gtk_widget_show(hbox);
80
81         combo = tab_completion_new_with_history(&ui->filter_entry, "", "pan_view_filter", -1,
82                                                 pan_filter_activate_cb, pw);
83         gtk_box_pack_start(GTK_BOX(hbox), combo, TRUE, TRUE, 0);
84         gtk_widget_show(combo);
85
86         ui->filter_label = gtk_label_new("");/** @todo (xsdg): Figure out whether it's useful to keep this label around. */
87         //gtk_box_pack_start(GTK_BOX(hbox), ui->filter_label, FALSE, FALSE, 0);
88         //gtk_widget_show(ui->filter_label);
89
90         ui->filter_kw_hbox = gtk_hbox_new(FALSE, PREF_PAD_SPACE);
91         gtk_box_pack_start(GTK_BOX(hbox), ui->filter_kw_hbox, TRUE, TRUE, 0);
92         gtk_widget_show(ui->filter_kw_hbox);
93
94         // Build the spin-button to show/hide the filter UI.
95         ui->filter_button = gtk_toggle_button_new();
96         gtk_button_set_relief(GTK_BUTTON(ui->filter_button), GTK_RELIEF_NONE);
97         gtk_button_set_focus_on_click(GTK_BUTTON(ui->filter_button), FALSE);
98         hbox = gtk_hbox_new(FALSE, PREF_PAD_GAP);
99         gtk_container_add(GTK_CONTAINER(ui->filter_button), hbox);
100         gtk_widget_show(hbox);
101         ui->filter_button_arrow = gtk_arrow_new(GTK_ARROW_UP, GTK_SHADOW_NONE);
102         gtk_box_pack_start(GTK_BOX(hbox), ui->filter_button_arrow, FALSE, FALSE, 0);
103         gtk_widget_show(ui->filter_button_arrow);
104         pref_label_new(hbox, _("Filter"));
105
106         g_signal_connect(G_OBJECT(ui->filter_button), "clicked",
107                          G_CALLBACK(pan_filter_toggle_cb), pw);
108
109         // Add check buttons for filtering by image class
110         for (i = 0; i < FILE_FORMAT_CLASSES; i++)
111         {
112                 ui->filter_check_buttons[i] = gtk_check_button_new_with_label(_(format_class_list[i]));
113                 gtk_box_pack_start(GTK_BOX(ui->filter_box), ui->filter_check_buttons[i], FALSE, FALSE, 0);
114                 gtk_widget_show(ui->filter_check_buttons[i]);
115         }
116
117         gtk_toggle_button_set_active((GtkToggleButton *)ui->filter_check_buttons[FORMAT_CLASS_IMAGE], TRUE);
118         gtk_toggle_button_set_active((GtkToggleButton *)ui->filter_check_buttons[FORMAT_CLASS_RAWIMAGE], TRUE);
119         gtk_toggle_button_set_active((GtkToggleButton *)ui->filter_check_buttons[FORMAT_CLASS_VIDEO], TRUE);
120         ui->filter_classes = (1 << FORMAT_CLASS_IMAGE) | (1 << FORMAT_CLASS_RAWIMAGE) | (1 << FORMAT_CLASS_VIDEO);
121
122         // Connecting the signal before setting the state causes segfault as pw is not yet prepared
123         for (i = 0; i < FILE_FORMAT_CLASSES; i++)
124                 g_signal_connect((GtkToggleButton *)(ui->filter_check_buttons[i]), "toggled", G_CALLBACK(pan_filter_toggle_button_cb), pw);
125
126         return ui;
127 }
128
129 void pan_filter_ui_destroy(PanViewFilterUi **ui_ptr)
130 {
131         if (ui_ptr == NULL || *ui_ptr == NULL) return;
132
133         // Note that g_clear_pointer handles already-NULL pointers.
134         //g_clear_pointer(&(*ui_ptr)->filter_kw_table, g_hash_table_destroy);
135
136         g_free(*ui_ptr);
137         *ui_ptr = NULL;
138 }
139
140 static void pan_filter_status(PanWindow *pw, const gchar *text)
141 {
142         gtk_label_set_text(GTK_LABEL(pw->filter_ui->filter_label), (text) ? text : "");
143 }
144
145 static void pan_filter_kw_button_cb(GtkButton *widget, gpointer data)
146 {
147         PanFilterCallbackState *cb_state = data;
148         PanWindow *pw = cb_state->pw;
149         PanViewFilterUi *ui = pw->filter_ui;
150
151         /** @todo (xsdg): Fix filter element pointed object memory leak. */
152         ui->filter_elements = g_list_delete_link(ui->filter_elements, cb_state->filter_element);
153         gtk_widget_destroy(GTK_WIDGET(widget));
154         g_free(cb_state);
155
156         pan_filter_status(pw, _("Removed keyword…"));
157         pan_layout_update(pw);
158 }
159
160 void pan_filter_activate_cb(const gchar *text, gpointer data)
161 {
162         GtkWidget *kw_button;
163         PanWindow *pw = data;
164         PanViewFilterUi *ui = pw->filter_ui;
165         GtkTreeIter iter;
166
167         if (!text) return;
168
169         // Get all relevant state and reset UI.
170         gtk_combo_box_get_active_iter(GTK_COMBO_BOX(ui->filter_mode_combo), &iter);
171         gtk_entry_set_text(GTK_ENTRY(ui->filter_entry), "");
172         tab_completion_append_to_history(ui->filter_entry, text);
173
174         // Add new filter element.
175         PanViewFilterElement *element = g_new0(PanViewFilterElement, 1);
176         gtk_tree_model_get(GTK_TREE_MODEL(ui->filter_mode_model), &iter, 0, &element->mode, -1);
177         element->keyword = g_strdup(text);
178         if (g_strcmp0(text, g_regex_escape_string(text, -1)))
179                 {
180                 // It's an actual regex, so compile
181                 element->kw_regex = g_regex_new(text, G_REGEX_ANCHORED | G_REGEX_OPTIMIZE, G_REGEX_MATCH_ANCHORED, NULL);
182                 }
183         ui->filter_elements = g_list_append(ui->filter_elements, element);
184
185         // Get the short version of the mode value.
186         gchar *short_mode;
187         gtk_tree_model_get(GTK_TREE_MODEL(ui->filter_mode_model), &iter, 2, &short_mode, -1);
188
189         // Create the button.
190         /** @todo (xsdg): Use MVC so that the button list is an actual representation of the GList */
191         gchar *label = g_strdup_printf("(%s) %s", short_mode, text);
192         kw_button = gtk_button_new_with_label(label);
193         g_clear_pointer(&label, g_free);
194
195         gtk_box_pack_start(GTK_BOX(ui->filter_kw_hbox), kw_button, FALSE, FALSE, 0);
196         gtk_widget_show(kw_button);
197
198         PanFilterCallbackState *cb_state = g_new0(PanFilterCallbackState, 1);
199         cb_state->pw = pw;
200         cb_state->filter_element = g_list_last(ui->filter_elements);
201
202         g_signal_connect(G_OBJECT(kw_button), "clicked",
203                          G_CALLBACK(pan_filter_kw_button_cb), cb_state);
204
205         pan_layout_update(pw);
206 }
207
208 void pan_filter_activate(PanWindow *pw)
209 {
210         gchar *text;
211
212         text = g_strdup(gtk_entry_get_text(GTK_ENTRY(pw->filter_ui->filter_entry)));
213         pan_filter_activate_cb(text, pw);
214         g_free(text);
215 }
216
217 void pan_filter_toggle_cb(GtkWidget *button, gpointer data)
218 {
219         PanWindow *pw = data;
220         PanViewFilterUi *ui = pw->filter_ui;
221         gboolean visible;
222
223         visible = gtk_widget_get_visible(ui->filter_box);
224         if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button)) == visible) return;
225
226         if (visible)
227                 {
228                 gtk_widget_hide(ui->filter_box);
229                 gtk_arrow_set(GTK_ARROW(ui->filter_button_arrow), GTK_ARROW_UP, GTK_SHADOW_NONE);
230                 }
231         else
232                 {
233                 gtk_widget_show(ui->filter_box);
234                 gtk_arrow_set(GTK_ARROW(ui->filter_button_arrow), GTK_ARROW_DOWN, GTK_SHADOW_NONE);
235                 gtk_widget_grab_focus(ui->filter_entry);
236                 }
237 }
238
239 void pan_filter_toggle_visible(PanWindow *pw, gboolean enable)
240 {
241         PanViewFilterUi *ui = pw->filter_ui;
242         if (pw->fs) return;
243
244         if (enable)
245                 {
246                 if (gtk_widget_get_visible(ui->filter_box))
247                         {
248                         gtk_widget_grab_focus(ui->filter_entry);
249                         }
250                 else
251                         {
252                         gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(ui->filter_button), TRUE);
253                         }
254                 }
255         else
256                 {
257                 if (gtk_widget_get_visible(ui->filter_entry))
258                         {
259                         if (gtk_widget_has_focus(ui->filter_entry))
260                                 {
261                                 gtk_widget_grab_focus(GTK_WIDGET(pw->imd->widget));
262                                 }
263                         gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(ui->filter_button), FALSE);
264                         }
265                 }
266 }
267
268 void pan_filter_toggle_button_cb(GtkWidget *button, gpointer data)
269 {
270         PanWindow *pw = data;
271         PanViewFilterUi *ui = pw->filter_ui;
272
273         gint old_classes = ui->filter_classes;
274         ui->filter_classes = 0;
275
276         for (gint i = 0; i < FILE_FORMAT_CLASSES; i++)
277         {
278                 ui->filter_classes |= gtk_toggle_button_get_active((GtkToggleButton *)ui->filter_check_buttons[i]) ? 1 << i : 0;
279         }
280
281         if (ui->filter_classes != old_classes) 
282                 pan_layout_update(pw);
283 }
284
285 static gboolean pan_view_list_contains_kw_pattern(GList *haystack, PanViewFilterElement *filter, gchar **found_kw)
286 {
287         if (filter->kw_regex)
288                 {
289                 // regex compile succeeded; attempt regex match.
290                 GList *work = g_list_first(haystack);
291                 while (work)
292                         {
293                         gchar *keyword = work->data;
294                         work = work->next;
295                         if (g_regex_match(filter->kw_regex, keyword, 0x0, NULL))
296                                 {
297                                 if (found_kw) *found_kw = keyword;
298                                 return TRUE;
299                                 }
300                         }
301                 return FALSE;
302                 }
303         else
304                 {
305                 // regex compile failed; fall back to exact string match.
306                 GList *found_elem = g_list_find_custom(haystack, filter->keyword, (GCompareFunc)g_strcmp0);
307                 if (found_elem && found_kw) *found_kw = found_elem->data;
308                 return !!found_elem;
309                 }
310 }
311
312 gboolean pan_filter_fd_list(GList **fd_list, GList *filter_elements, gint filter_classes)
313 {
314         GList *work;
315         gboolean modified = FALSE;
316         GHashTable *seen_kw_table = NULL;
317
318         if (!fd_list || !*fd_list) return modified;
319
320         // seen_kw_table is only valid in this scope, so don't take ownership of any strings.
321         if (filter_elements)
322                 seen_kw_table = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, NULL);
323
324         work = *fd_list;
325         while (work)
326                 {
327                 FileData *fd = work->data;
328                 GList *last_work = work;
329                 work = work->next;
330
331                 gboolean should_reject = FALSE;
332                 gchar *group_kw = NULL;
333
334                 if (!((1 << fd -> format_class) & filter_classes))
335                         {
336                         should_reject = TRUE;
337                         }
338                 else if (filter_elements)
339                         {
340                         /** @todo (xsdg): OPTIMIZATION Do the search inside of metadata.c to avoid a bunch of string list copies. */
341                         GList *img_keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
342
343                         /** @todo (xsdg): OPTIMIZATION Determine a heuristic for when to linear-search the keywords list, and when to build a hash table for the image's keywords. */
344                         GList *filter_element = filter_elements;
345
346                         while (filter_element)
347                                 {
348                                 PanViewFilterElement *filter = filter_element->data;
349                                 filter_element = filter_element->next;
350                                 gchar *found_kw = NULL;
351                                 gboolean has_kw = pan_view_list_contains_kw_pattern(img_keywords, filter, &found_kw);
352
353                                 switch (filter->mode)
354                                         {
355                                         case PAN_VIEW_FILTER_REQUIRE:
356                                                 should_reject |= !has_kw;
357                                                 break;
358                                         case PAN_VIEW_FILTER_EXCLUDE:
359                                                 should_reject |= has_kw;
360                                                 break;
361                                         case PAN_VIEW_FILTER_INCLUDE:
362                                                 if (has_kw) should_reject = FALSE;
363                                                 break;
364                                         case PAN_VIEW_FILTER_GROUP:
365                                                 if (has_kw)
366                                                         {
367                                                         if (g_hash_table_contains(seen_kw_table, found_kw))
368                                                                 {
369                                                                 should_reject = TRUE;
370                                                                 }
371                                                         else if (group_kw == NULL)
372                                                                 {
373                                                                 group_kw = found_kw;
374                                                                 }
375                                                         }
376                                                 break;
377                                         }
378                                 }
379                         string_list_free(img_keywords);
380                         if (!should_reject && group_kw != NULL) g_hash_table_add(seen_kw_table, group_kw);
381                         group_kw = NULL;  // group_kw references an item from img_keywords.
382                         }
383
384                 if (should_reject)
385                         {
386                         *fd_list = g_list_delete_link(*fd_list, last_work);
387                         modified = TRUE;
388                         }
389                 }
390
391         if (filter_elements)
392                 g_hash_table_destroy(seen_kw_table);
393
394         return modified;
395 }