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 "ui_fileops.h"
30 #include "ui_tabcomp.h"
33 PanViewFilterUi *pan_filter_ui_new(PanWindow *pw)
35 PanViewFilterUi *ui = g_new0(PanViewFilterUi, 1);
39 /* Since we're using the GHashTable as a HashSet (in which key and value pointers
40 * are always identical), specifying key _and_ value destructor callbacks will
41 * cause a double-free.
45 ui->filter_mode_model = gtk_list_store_new(3, G_TYPE_INT, G_TYPE_STRING, G_TYPE_STRING);
46 gtk_list_store_append(ui->filter_mode_model, &iter);
47 gtk_list_store_set(ui->filter_mode_model, &iter,
48 0, PAN_VIEW_FILTER_REQUIRE, 1, _("Require"), 2, _("R"), -1);
49 gtk_list_store_append(ui->filter_mode_model, &iter);
50 gtk_list_store_set(ui->filter_mode_model, &iter,
51 0, PAN_VIEW_FILTER_EXCLUDE, 1, _("Exclude"), 2, _("E"), -1);
52 gtk_list_store_append(ui->filter_mode_model, &iter);
53 gtk_list_store_set(ui->filter_mode_model, &iter,
54 0, PAN_VIEW_FILTER_INCLUDE, 1, _("Include"), 2, _("I"), -1);
55 gtk_list_store_append(ui->filter_mode_model, &iter);
56 gtk_list_store_set(ui->filter_mode_model, &iter,
57 0, PAN_VIEW_FILTER_GROUP, 1, _("Group"), 2, _("G"), -1);
59 ui->filter_mode_combo = gtk_combo_box_new_with_model(GTK_TREE_MODEL(ui->filter_mode_model));
60 gtk_combo_box_set_focus_on_click(GTK_COMBO_BOX(ui->filter_mode_combo), FALSE);
61 gtk_combo_box_set_active(GTK_COMBO_BOX(ui->filter_mode_combo), 0);
63 GtkCellRenderer *render = gtk_cell_renderer_text_new();
64 gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(ui->filter_mode_combo), render, TRUE);
65 gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(ui->filter_mode_combo), render, "text", 1, NULL);
68 // Build the actual filter UI.
69 ui->filter_box = gtk_hbox_new(FALSE, PREF_PAD_SPACE);
70 pref_spacer(ui->filter_box, 0);
71 pref_label_new(ui->filter_box, _("Keyword Filter:"));
73 gtk_box_pack_start(GTK_BOX(ui->filter_box), ui->filter_mode_combo, TRUE, TRUE, 0);
74 gtk_widget_show(ui->filter_mode_combo);
76 hbox = gtk_hbox_new(TRUE, PREF_PAD_SPACE);
77 gtk_box_pack_start(GTK_BOX(ui->filter_box), hbox, TRUE, TRUE, 0);
78 gtk_widget_show(hbox);
80 combo = tab_completion_new_with_history(&ui->filter_entry, "", "pan_view_filter", -1,
81 pan_filter_activate_cb, pw);
82 gtk_box_pack_start(GTK_BOX(hbox), combo, TRUE, TRUE, 0);
83 gtk_widget_show(combo);
85 // TODO(xsdg): Figure out whether it's useful to keep this label around.
86 ui->filter_label = gtk_label_new("");
87 //gtk_box_pack_start(GTK_BOX(hbox), ui->filter_label, FALSE, FALSE, 0);
88 //gtk_widget_show(ui->filter_label);
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);
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"));
106 g_signal_connect(G_OBJECT(ui->filter_button), "clicked",
107 G_CALLBACK(pan_filter_toggle_cb), pw);
112 void pan_filter_ui_destroy(PanViewFilterUi **ui_ptr)
114 if (ui_ptr == NULL || *ui_ptr == NULL) return;
116 // Note that g_clear_pointer handles already-NULL pointers.
117 //g_clear_pointer(&(*ui_ptr)->filter_kw_table, g_hash_table_destroy);
123 static void pan_filter_status(PanWindow *pw, const gchar *text)
125 gtk_label_set_text(GTK_LABEL(pw->filter_ui->filter_label), (text) ? text : "");
128 static void pan_filter_kw_button_cb(GtkButton *widget, gpointer data)
130 PanFilterCallbackState *cb_state = data;
131 PanWindow *pw = cb_state->pw;
132 PanViewFilterUi *ui = pw->filter_ui;
134 // TODO(xsdg): Fix filter element pointed object memory leak.
135 ui->filter_elements = g_list_delete_link(ui->filter_elements, cb_state->filter_element);
136 gtk_widget_destroy(GTK_WIDGET(widget));
139 pan_filter_status(pw, _("Removed keyword…"));
140 pan_layout_update(pw);
143 void pan_filter_activate_cb(const gchar *text, gpointer data)
145 GtkWidget *kw_button;
146 PanWindow *pw = data;
147 PanViewFilterUi *ui = pw->filter_ui;
152 // Get all relevant state and reset UI.
153 gtk_combo_box_get_active_iter(GTK_COMBO_BOX(ui->filter_mode_combo), &iter);
154 gtk_entry_set_text(GTK_ENTRY(ui->filter_entry), "");
155 tab_completion_append_to_history(ui->filter_entry, text);
157 // Add new filter element.
158 PanViewFilterElement *element = g_new0(PanViewFilterElement, 1);
159 gtk_tree_model_get(GTK_TREE_MODEL(ui->filter_mode_model), &iter, 0, &element->mode, -1);
160 element->keyword = g_strdup(text);
161 if (g_strcmp0(text, g_regex_escape_string(text, -1)))
163 // It's an actual regex, so compile
164 element->kw_regex = g_regex_new(text, G_REGEX_ANCHORED | G_REGEX_OPTIMIZE, G_REGEX_MATCH_ANCHORED, NULL);
166 ui->filter_elements = g_list_append(ui->filter_elements, element);
168 // Get the short version of the mode value.
170 gtk_tree_model_get(GTK_TREE_MODEL(ui->filter_mode_model), &iter, 2, &short_mode, -1);
172 // Create the button.
173 // TODO(xsdg): Use MVC so that the button list is an actual representation of the GList
174 gchar *label = g_strdup_printf("(%s) %s", short_mode, text);
175 kw_button = gtk_button_new_with_label(label);
176 g_clear_pointer(&label, g_free);
178 gtk_box_pack_start(GTK_BOX(ui->filter_kw_hbox), kw_button, FALSE, FALSE, 0);
179 gtk_widget_show(kw_button);
181 PanFilterCallbackState *cb_state = g_new0(PanFilterCallbackState, 1);
183 cb_state->filter_element = g_list_last(ui->filter_elements);
185 g_signal_connect(G_OBJECT(kw_button), "clicked",
186 G_CALLBACK(pan_filter_kw_button_cb), cb_state);
188 pan_layout_update(pw);
191 void pan_filter_activate(PanWindow *pw)
195 text = g_strdup(gtk_entry_get_text(GTK_ENTRY(pw->filter_ui->filter_entry)));
196 pan_filter_activate_cb(text, pw);
200 void pan_filter_toggle_cb(GtkWidget *button, gpointer data)
202 PanWindow *pw = data;
203 PanViewFilterUi *ui = pw->filter_ui;
206 visible = gtk_widget_get_visible(ui->filter_box);
207 if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button)) == visible) return;
211 gtk_widget_hide(ui->filter_box);
212 gtk_arrow_set(GTK_ARROW(ui->filter_button_arrow), GTK_ARROW_UP, GTK_SHADOW_NONE);
216 gtk_widget_show(ui->filter_box);
217 gtk_arrow_set(GTK_ARROW(ui->filter_button_arrow), GTK_ARROW_DOWN, GTK_SHADOW_NONE);
218 gtk_widget_grab_focus(ui->filter_entry);
222 void pan_filter_toggle_visible(PanWindow *pw, gboolean enable)
224 PanViewFilterUi *ui = pw->filter_ui;
229 if (gtk_widget_get_visible(ui->filter_box))
231 gtk_widget_grab_focus(ui->filter_entry);
235 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(ui->filter_button), TRUE);
240 if (gtk_widget_get_visible(ui->filter_entry))
242 if (gtk_widget_has_focus(ui->filter_entry))
244 gtk_widget_grab_focus(GTK_WIDGET(pw->imd->widget));
246 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(ui->filter_button), FALSE);
251 static gboolean pan_view_list_contains_kw_pattern(GList *haystack, PanViewFilterElement *filter, gchar **found_kw)
253 if (filter->kw_regex)
255 // regex compile succeeded; attempt regex match.
256 GList *work = g_list_first(haystack);
259 gchar *keyword = work->data;
261 if (g_regex_match(filter->kw_regex, keyword, 0x0, NULL))
263 if (found_kw) *found_kw = keyword;
271 // regex compile failed; fall back to exact string match.
272 GList *found_elem = g_list_find_custom(haystack, filter->keyword, (GCompareFunc)g_strcmp0);
273 if (found_elem && found_kw) *found_kw = found_elem->data;
278 gboolean pan_filter_fd_list(GList **fd_list, GList *filter_elements)
281 gboolean modified = FALSE;
282 GHashTable *seen_kw_table = NULL;
284 if (!fd_list || !*fd_list || !filter_elements) return modified;
286 // seen_kw_table is only valid in this scope, so don't take ownership of any strings.
287 seen_kw_table = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, NULL);
292 FileData *fd = work->data;
293 GList *last_work = work;
296 // TODO(xsdg): OPTIMIZATION Do the search inside of metadata.c to avoid a
297 // bunch of string list copies.
298 GList *img_keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
300 // TODO(xsdg): OPTIMIZATION Determine a heuristic for when to linear-search the
301 // keywords list, and when to build a hash table for the image's keywords.
302 gboolean should_reject = FALSE;
303 gchar *group_kw = NULL;
304 GList *filter_element = filter_elements;
305 while (filter_element)
307 PanViewFilterElement *filter = filter_element->data;
308 filter_element = filter_element->next;
309 gchar *found_kw = NULL;
310 gboolean has_kw = pan_view_list_contains_kw_pattern(img_keywords, filter, &found_kw);
312 switch (filter->mode)
314 case PAN_VIEW_FILTER_REQUIRE:
315 should_reject |= !has_kw;
317 case PAN_VIEW_FILTER_EXCLUDE:
318 should_reject |= has_kw;
320 case PAN_VIEW_FILTER_INCLUDE:
321 if (has_kw) should_reject = FALSE;
323 case PAN_VIEW_FILTER_GROUP:
326 if (g_hash_table_contains(seen_kw_table, found_kw))
328 should_reject = TRUE;
330 else if (group_kw == NULL)
339 if (!should_reject && group_kw != NULL) g_hash_table_add(seen_kw_table, group_kw);
341 group_kw = NULL; // group_kw references an item from img_keywords.
342 string_list_free(img_keywords);
346 *fd_list = g_list_delete_link(*fd_list, last_work);
351 g_hash_table_destroy(seen_kw_table);