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