1c66e61509c9df8004a85c89266c19f50361653c
[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
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.
42          */
43         {
44                 GtkTreeIter iter;
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);
58
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);
62
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);
66         }
67
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:"));
72
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);
75
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);
79
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);
84
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);
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         return ui;
110 }
111
112 void pan_filter_ui_destroy(PanViewFilterUi **ui_ptr)
113 {
114         if (ui_ptr == NULL || *ui_ptr == NULL) return;
115
116         // Note that g_clear_pointer handles already-NULL pointers.
117         //g_clear_pointer(&(*ui_ptr)->filter_kw_table, g_hash_table_destroy);
118
119         g_free(*ui_ptr);
120         *ui_ptr = NULL;
121 }
122
123 static void pan_filter_status(PanWindow *pw, const gchar *text)
124 {
125         gtk_label_set_text(GTK_LABEL(pw->filter_ui->filter_label), (text) ? text : "");
126 }
127
128 static void pan_filter_kw_button_cb(GtkButton *widget, gpointer data)
129 {
130         PanFilterCallbackState *cb_state = data;
131         PanWindow *pw = cb_state->pw;
132         PanViewFilterUi *ui = pw->filter_ui;
133
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));
137         g_free(cb_state);
138
139         pan_filter_status(pw, _("Removed keyword…"));
140         pan_layout_update(pw);
141 }
142
143 void pan_filter_activate_cb(const gchar *text, gpointer data)
144 {
145         GtkWidget *kw_button;
146         PanWindow *pw = data;
147         PanViewFilterUi *ui = pw->filter_ui;
148         GtkTreeIter iter;
149
150         if (!text) return;
151
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);
156
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         ui->filter_elements = g_list_append(ui->filter_elements, element);
162
163         // Get the short version of the mode value.
164         gchar *short_mode;
165         gtk_tree_model_get(GTK_TREE_MODEL(ui->filter_mode_model), &iter, 2, &short_mode, -1);
166
167         // Create the button.
168         // TODO(xsdg): Use MVC so that the button list is an actual representation of the GList
169         gchar *label = g_strdup_printf("(%s) %s", short_mode, text);
170         kw_button = gtk_button_new_with_label(label);
171         g_clear_pointer(&label, g_free);
172
173         gtk_box_pack_start(GTK_BOX(ui->filter_kw_hbox), kw_button, FALSE, FALSE, 0);
174         gtk_widget_show(kw_button);
175
176         PanFilterCallbackState *cb_state = g_new0(PanFilterCallbackState, 1);
177         cb_state->pw = pw;
178         cb_state->filter_element = g_list_last(ui->filter_elements);
179
180         g_signal_connect(G_OBJECT(kw_button), "clicked",
181                          G_CALLBACK(pan_filter_kw_button_cb), cb_state);
182
183         pan_layout_update(pw);
184 }
185
186 void pan_filter_activate(PanWindow *pw)
187 {
188         gchar *text;
189
190         text = g_strdup(gtk_entry_get_text(GTK_ENTRY(pw->filter_ui->filter_entry)));
191         pan_filter_activate_cb(text, pw);
192         g_free(text);
193 }
194
195 void pan_filter_toggle_cb(GtkWidget *button, gpointer data)
196 {
197         PanWindow *pw = data;
198         PanViewFilterUi *ui = pw->filter_ui;
199         gboolean visible;
200
201         visible = gtk_widget_get_visible(ui->filter_box);
202         if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button)) == visible) return;
203
204         if (visible)
205                 {
206                 gtk_widget_hide(ui->filter_box);
207                 gtk_arrow_set(GTK_ARROW(ui->filter_button_arrow), GTK_ARROW_UP, GTK_SHADOW_NONE);
208                 }
209         else
210                 {
211                 gtk_widget_show(ui->filter_box);
212                 gtk_arrow_set(GTK_ARROW(ui->filter_button_arrow), GTK_ARROW_DOWN, GTK_SHADOW_NONE);
213                 gtk_widget_grab_focus(ui->filter_entry);
214                 }
215 }
216
217 void pan_filter_toggle_visible(PanWindow *pw, gboolean enable)
218 {
219         PanViewFilterUi *ui = pw->filter_ui;
220         if (pw->fs) return;
221
222         if (enable)
223                 {
224                 if (gtk_widget_get_visible(ui->filter_box))
225                         {
226                         gtk_widget_grab_focus(ui->filter_entry);
227                         }
228                 else
229                         {
230                         gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(ui->filter_button), TRUE);
231                         }
232                 }
233         else
234                 {
235                 if (gtk_widget_get_visible(ui->filter_entry))
236                         {
237                         if (gtk_widget_has_focus(ui->filter_entry))
238                                 {
239                                 gtk_widget_grab_focus(GTK_WIDGET(pw->imd->widget));
240                                 }
241                         gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(ui->filter_button), FALSE);
242                         }
243                 }
244 }
245
246 gboolean pan_filter_fd_list(GList **fd_list, GList *filter_elements)
247 {
248         GList *work;
249         gboolean modified = FALSE;
250         GHashTable *seen_kw_table = NULL;
251
252         if (!fd_list || !*fd_list || !filter_elements) return modified;
253
254         // seen_kw_table is only valid in this scope, so don't take ownership of any strings.
255         seen_kw_table = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, NULL);
256
257         work = *fd_list;
258         while (work)
259                 {
260                 FileData *fd = work->data;
261                 GList *last_work = work;
262                 work = work->next;
263
264                 // TODO(xsdg): OPTIMIZATION Do the search inside of metadata.c to avoid a
265                 // bunch of string list copies.
266                 GList *img_keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
267
268                 // TODO(xsdg): OPTIMIZATION Determine a heuristic for when to linear-search the
269                 // keywords list, and when to build a hash table for the image's keywords.
270                 gboolean should_reject = FALSE;
271                 gchar *group_kw = NULL;
272                 GList *filter_element = filter_elements;
273                 while (filter_element)
274                         {
275                         PanViewFilterElement *filter = filter_element->data;
276                         filter_element = filter_element->next;
277                         gboolean has_kw = !!g_list_find_custom(img_keywords, filter->keyword, (GCompareFunc)g_strcmp0);
278
279                         switch (filter->mode)
280                                 {
281                                 case PAN_VIEW_FILTER_REQUIRE:
282                                         should_reject |= !has_kw;
283                                         break;
284                                 case PAN_VIEW_FILTER_EXCLUDE:
285                                         should_reject |= has_kw;
286                                         break;
287                                 case PAN_VIEW_FILTER_INCLUDE:
288                                         if (has_kw) should_reject = FALSE;
289                                         break;
290                                 case PAN_VIEW_FILTER_GROUP:
291                                         if (has_kw)
292                                                 {
293                                                 if (g_hash_table_contains(seen_kw_table, filter->keyword))
294                                                         {
295                                                         should_reject = TRUE;
296                                                         }
297                                                 else if (group_kw == NULL)
298                                                         {
299                                                         group_kw = filter->keyword;
300                                                         }
301                                                 }
302                                         break;
303                                 }
304                         }
305
306                 if (!should_reject && group_kw != NULL) g_hash_table_add(seen_kw_table, group_kw);
307
308                 string_list_free(img_keywords);
309
310                 if (should_reject)
311                         {
312                         *fd_list = g_list_delete_link(*fd_list, last_work);
313                         modified = TRUE;
314                         }
315                 }
316
317         g_hash_table_destroy(seen_kw_table);
318         return modified;
319 }