2 * Copyright (C) 2006 John Ellis
3 * Copyright (C) 2008 - 2016 The Geeqie Team
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.
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.
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.
22 #include "pan-view-filter.h"
29 #include "main-defines.h"
33 #include "ui-fileops.h"
35 #include "ui-tabcomp.h"
37 PanViewFilterUi *pan_filter_ui_new(PanWindow *pw)
39 auto ui = g_new0(PanViewFilterUi, 1);
44 /* Since we're using the GHashTable as a HashSet (in which key and value pointers
45 * are always identical), specifying key _and_ value destructor callbacks will
46 * cause a double-free.
50 ui->filter_mode_model = gtk_list_store_new(3, G_TYPE_INT, G_TYPE_STRING, G_TYPE_STRING);
51 gtk_list_store_append(ui->filter_mode_model, &iter);
52 gtk_list_store_set(ui->filter_mode_model, &iter,
53 0, PAN_VIEW_FILTER_REQUIRE, 1, _("Require"), 2, _("R"), -1);
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_EXCLUDE, 1, _("Exclude"), 2, _("E"), -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_INCLUDE, 1, _("Include"), 2, _("I"), -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_GROUP, 1, _("Group"), 2, _("G"), -1);
64 ui->filter_mode_combo = gtk_combo_box_new_with_model(GTK_TREE_MODEL(ui->filter_mode_model));
65 gtk_widget_set_focus_on_click(ui->filter_mode_combo, FALSE);
66 gtk_combo_box_set_active(GTK_COMBO_BOX(ui->filter_mode_combo), 0);
68 GtkCellRenderer *render = gtk_cell_renderer_text_new();
69 gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(ui->filter_mode_combo), render, TRUE);
70 gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(ui->filter_mode_combo), render, "text", 1, NULL);
73 // Build the actual filter UI.
74 ui->filter_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, PREF_PAD_SPACE);
75 pref_spacer(ui->filter_box, 0);
76 pref_label_new(ui->filter_box, _("Keyword Filter:"));
78 gq_gtk_box_pack_start(GTK_BOX(ui->filter_box), ui->filter_mode_combo, FALSE, FALSE, 0);
79 gtk_widget_show(ui->filter_mode_combo);
81 hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, PREF_PAD_SPACE);
82 gq_gtk_box_pack_start(GTK_BOX(ui->filter_box), hbox, TRUE, TRUE, 0);
83 gtk_widget_show(hbox);
85 combo = tab_completion_new_with_history(&ui->filter_entry, "", "pan_view_filter", -1,
86 pan_filter_activate_cb, pw);
87 gq_gtk_box_pack_start(GTK_BOX(hbox), combo, TRUE, TRUE, 0);
88 gtk_widget_show(combo);
90 ui->filter_label = gtk_label_new("");/** @todo (xsdg): Figure out whether it's useful to keep this label around. */
92 ui->filter_kw_hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, PREF_PAD_SPACE);
93 gq_gtk_box_pack_start(GTK_BOX(hbox), ui->filter_kw_hbox, TRUE, TRUE, 0);
94 gtk_widget_show(ui->filter_kw_hbox);
96 // Build the spin-button to show/hide the filter UI.
97 ui->filter_button = gtk_toggle_button_new();
98 gtk_button_set_relief(GTK_BUTTON(ui->filter_button), GTK_RELIEF_NONE);
99 gtk_widget_set_focus_on_click(ui->filter_button, FALSE);
100 hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, PREF_PAD_GAP);
101 gq_gtk_container_add(GTK_WIDGET(ui->filter_button), hbox);
102 gtk_widget_show(hbox);
103 ui->filter_button_arrow = gtk_image_new_from_icon_name(GQ_ICON_PAN_UP, GTK_ICON_SIZE_BUTTON);
104 gq_gtk_box_pack_start(GTK_BOX(hbox), ui->filter_button_arrow, FALSE, FALSE, 0);
105 gtk_widget_show(ui->filter_button_arrow);
106 pref_label_new(hbox, _("Filter"));
108 g_signal_connect(G_OBJECT(ui->filter_button), "clicked",
109 G_CALLBACK(pan_filter_toggle_cb), pw);
111 // Add check buttons for filtering by image class
112 for (i = 0; i < FILE_FORMAT_CLASSES; i++)
114 ui->filter_check_buttons[i] = gtk_check_button_new_with_label(_(format_class_list[i]));
115 gq_gtk_box_pack_start(GTK_BOX(ui->filter_box), ui->filter_check_buttons[i], FALSE, FALSE, 0);
116 gtk_widget_show(ui->filter_check_buttons[i]);
119 gtk_toggle_button_set_active(reinterpret_cast<GtkToggleButton *>(ui->filter_check_buttons[FORMAT_CLASS_IMAGE]), TRUE);
120 gtk_toggle_button_set_active(reinterpret_cast<GtkToggleButton *>(ui->filter_check_buttons[FORMAT_CLASS_RAWIMAGE]), TRUE);
121 gtk_toggle_button_set_active(reinterpret_cast<GtkToggleButton *>(ui->filter_check_buttons[FORMAT_CLASS_VIDEO]), TRUE);
122 ui->filter_classes = (1 << FORMAT_CLASS_IMAGE) | (1 << FORMAT_CLASS_RAWIMAGE) | (1 << FORMAT_CLASS_VIDEO);
124 // Connecting the signal before setting the state causes segfault as pw is not yet prepared
125 for (i = 0; i < FILE_FORMAT_CLASSES; i++)
126 g_signal_connect((GtkToggleButton *)(ui->filter_check_buttons[i]), "toggled", G_CALLBACK(pan_filter_toggle_button_cb), pw);
131 void pan_filter_ui_destroy(PanViewFilterUi **ui_ptr)
133 if (ui_ptr == nullptr || *ui_ptr == nullptr) return;
139 static void pan_filter_status(PanWindow *pw, const gchar *text)
141 gtk_label_set_text(GTK_LABEL(pw->filter_ui->filter_label), (text) ? text : "");
144 static void pan_filter_kw_button_cb(GtkButton *widget, gpointer data)
146 auto cb_state = static_cast<PanFilterCallbackState *>(data);
147 PanWindow *pw = cb_state->pw;
148 PanViewFilterUi *ui = pw->filter_ui;
150 /** @todo (xsdg): Fix filter element pointed object memory leak. */
151 ui->filter_elements = g_list_delete_link(ui->filter_elements, cb_state->filter_element);
152 g_object_unref(GTK_WIDGET(widget));
155 pan_filter_status(pw, _("Removed keyword…"));
156 pan_layout_update(pw);
159 void pan_filter_activate_cb(const gchar *text, gpointer data)
161 GtkWidget *kw_button;
162 auto pw = static_cast<PanWindow *>(data);
163 PanViewFilterUi *ui = pw->filter_ui;
168 // Get all relevant state and reset UI.
169 gtk_combo_box_get_active_iter(GTK_COMBO_BOX(ui->filter_mode_combo), &iter);
170 gq_gtk_entry_set_text(GTK_ENTRY(ui->filter_entry), "");
171 tab_completion_append_to_history(ui->filter_entry, text);
173 // Add new filter element.
174 auto element = g_new0(PanViewFilterElement, 1);
175 gtk_tree_model_get(GTK_TREE_MODEL(ui->filter_mode_model), &iter, 0, &element->mode, -1);
176 element->keyword = g_strdup(text);
177 if (g_strcmp0(text, g_regex_escape_string(text, -1)))
179 // It's an actual regex, so compile
180 element->kw_regex = g_regex_new(text, static_cast<GRegexCompileFlags>(G_REGEX_ANCHORED | G_REGEX_OPTIMIZE), G_REGEX_MATCH_ANCHORED, nullptr);
182 ui->filter_elements = g_list_append(ui->filter_elements, element);
184 // Get the short version of the mode value.
186 gtk_tree_model_get(GTK_TREE_MODEL(ui->filter_mode_model), &iter, 2, &short_mode, -1);
188 // Create the button.
189 /** @todo (xsdg): Use MVC so that the button list is an actual representation of the GList */
190 gchar *label = g_strdup_printf("(%s) %s", short_mode, text);
191 kw_button = gtk_button_new_with_label(label);
192 g_clear_pointer(&label, g_free);
194 gq_gtk_box_pack_start(GTK_BOX(ui->filter_kw_hbox), kw_button, FALSE, FALSE, 0);
195 gtk_widget_show(kw_button);
197 auto cb_state = g_new0(PanFilterCallbackState, 1);
199 cb_state->filter_element = g_list_last(ui->filter_elements);
201 g_signal_connect(G_OBJECT(kw_button), "clicked",
202 G_CALLBACK(pan_filter_kw_button_cb), cb_state);
204 pan_layout_update(pw);
207 #pragma GCC diagnostic push
208 #pragma GCC diagnostic ignored "-Wunused-function"
209 void pan_filter_activate_unused(PanWindow *pw)
213 text = g_strdup(gq_gtk_entry_get_text(GTK_ENTRY(pw->filter_ui->filter_entry)));
214 pan_filter_activate_cb(text, pw);
217 #pragma GCC diagnostic pop
219 void pan_filter_toggle_cb(GtkWidget *button, gpointer data)
221 auto pw = static_cast<PanWindow *>(data);
222 PanViewFilterUi *ui = pw->filter_ui;
226 visible = gtk_widget_get_visible(ui->filter_box);
227 if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button)) == visible) return;
231 gtk_widget_hide(ui->filter_box);
233 parent = gtk_widget_get_parent(ui->filter_button_arrow);
235 g_object_unref(ui->filter_button_arrow);
236 ui->filter_button_arrow = gtk_image_new_from_icon_name(GQ_ICON_PAN_UP, GTK_ICON_SIZE_BUTTON);
238 gq_gtk_box_pack_start(GTK_BOX(parent), ui->filter_button_arrow, FALSE, FALSE, 0);
239 gtk_box_reorder_child(GTK_BOX(parent), ui->filter_button_arrow, 0);
241 gtk_widget_show(ui->filter_button_arrow);
245 gtk_widget_show(ui->filter_box);
247 parent = gtk_widget_get_parent(ui->filter_button_arrow);
249 g_object_unref(ui->filter_button_arrow);
250 ui->filter_button_arrow = gtk_image_new_from_icon_name(GQ_ICON_PAN_DOWN, GTK_ICON_SIZE_BUTTON);
252 gq_gtk_box_pack_start(GTK_BOX(parent), ui->filter_button_arrow, FALSE, FALSE, 0);
253 gtk_box_reorder_child(GTK_BOX(parent), ui->filter_button_arrow, 0);
255 gtk_widget_show(ui->filter_button_arrow);
256 gtk_widget_grab_focus(ui->filter_entry);
260 #pragma GCC diagnostic push
261 #pragma GCC diagnostic ignored "-Wunused-function"
262 void pan_filter_toggle_visible_unused(PanWindow *pw, gboolean enable)
264 PanViewFilterUi *ui = pw->filter_ui;
269 if (gtk_widget_get_visible(ui->filter_box))
271 gtk_widget_grab_focus(ui->filter_entry);
275 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(ui->filter_button), TRUE);
280 if (gtk_widget_get_visible(ui->filter_entry))
282 if (gtk_widget_has_focus(ui->filter_entry))
284 gtk_widget_grab_focus(GTK_WIDGET(pw->imd->widget));
286 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(ui->filter_button), FALSE);
290 #pragma GCC diagnostic pop
292 void pan_filter_toggle_button_cb(GtkWidget *, gpointer data)
294 auto pw = static_cast<PanWindow *>(data);
295 PanViewFilterUi *ui = pw->filter_ui;
297 gint old_classes = ui->filter_classes;
298 ui->filter_classes = 0;
300 for (gint i = 0; i < FILE_FORMAT_CLASSES; i++)
302 ui->filter_classes |= gtk_toggle_button_get_active(reinterpret_cast<GtkToggleButton *>(ui->filter_check_buttons[i])) ? 1 << i : 0;
305 if (ui->filter_classes != old_classes)
306 pan_layout_update(pw);
309 static gboolean pan_view_list_contains_kw_pattern(GList *haystack, PanViewFilterElement *filter, gchar **found_kw)
311 if (filter->kw_regex)
313 // regex compile succeeded; attempt regex match.
314 GList *work = g_list_first(haystack);
317 auto keyword = static_cast<gchar *>(work->data);
319 if (g_regex_match(filter->kw_regex, keyword, static_cast<GRegexMatchFlags>(0), nullptr))
321 if (found_kw) *found_kw = keyword;
328 // regex compile failed; fall back to exact string match.
329 GList *found_elem = g_list_find_custom(haystack, filter->keyword, reinterpret_cast<GCompareFunc>(g_strcmp0));
330 if (found_elem && found_kw) *found_kw = static_cast<gchar *>(found_elem->data);
334 gboolean pan_filter_fd_list(GList **fd_list, GList *filter_elements, gint filter_classes)
337 gboolean modified = FALSE;
338 GHashTable *seen_kw_table = nullptr;
340 if (!fd_list || !*fd_list) return modified;
342 // seen_kw_table is only valid in this scope, so don't take ownership of any strings.
344 seen_kw_table = g_hash_table_new_full(g_str_hash, g_str_equal, nullptr, nullptr);
349 auto fd = static_cast<FileData *>(work->data);
350 GList *last_work = work;
353 gboolean should_reject = FALSE;
354 gchar *group_kw = nullptr;
356 if (!((1 << fd -> format_class) & filter_classes))
358 should_reject = TRUE;
360 else if (filter_elements)
362 /** @todo (xsdg): OPTIMIZATION Do the search inside of metadata.cc to avoid a bunch of string list copies. */
363 GList *img_keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
365 /** @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. */
366 GList *filter_element = filter_elements;
368 while (filter_element)
370 auto filter = static_cast<PanViewFilterElement *>(filter_element->data);
371 filter_element = filter_element->next;
372 gchar *found_kw = nullptr;
373 gboolean has_kw = pan_view_list_contains_kw_pattern(img_keywords, filter, &found_kw);
375 switch (filter->mode)
377 case PAN_VIEW_FILTER_REQUIRE:
378 should_reject |= !has_kw;
380 case PAN_VIEW_FILTER_EXCLUDE:
381 should_reject |= has_kw;
383 case PAN_VIEW_FILTER_INCLUDE:
384 if (has_kw) should_reject = FALSE;
386 case PAN_VIEW_FILTER_GROUP:
389 if (g_hash_table_contains(seen_kw_table, found_kw))
391 should_reject = TRUE;
393 else if (group_kw == nullptr)
401 g_list_free_full(img_keywords, g_free);
402 if (!should_reject && group_kw != nullptr) g_hash_table_add(seen_kw_table, group_kw);
403 group_kw = nullptr; // group_kw references an item from img_keywords.
408 *fd_list = g_list_delete_link(*fd_list, last_work);
414 g_hash_table_destroy(seen_kw_table);