changeset 2530:949b146aaa23

Adds a keyword filtering feature to Timeline PanView. UI needs some work, and currently only supports intersection between multiple keywords. Would be easy to support keyword unions, if the UI supported it. Should probably hide the button in modes that don't yet support it.
author Omari Stephens <xsdg@google.com>
date Sun, 25 Dec 2016 08:25:13 +0000
parents 74d73f2f5667
children b885cab426e8
files src/pan-view/Makefile.am src/pan-view/pan-timeline.c src/pan-view/pan-types.h src/pan-view/pan-view-filter.c src/pan-view/pan-view-filter.h src/pan-view/pan-view-search.c src/pan-view/pan-view.c src/pan-view/pan-view.h
diffstat 8 files changed, 316 insertions(+), 18 deletions(-) [+]
line wrap: on
line diff
--- a/src/pan-view/Makefile.am	Sat Dec 24 22:37:21 2016 +0000
+++ b/src/pan-view/Makefile.am	Sun Dec 25 08:25:13 2016 +0000
@@ -14,6 +14,8 @@
 	%D%/pan-util.h	\
 	%D%/pan-view.c	\
 	%D%/pan-view.h	\
+	%D%/pan-view-filter.c	\
+	%D%/pan-view-filter.h	\
 	%D%/pan-view-search.c	\
 	%D%/pan-view-search.h
 
--- a/src/pan-view/pan-timeline.c	Sat Dec 24 22:37:21 2016 +0000
+++ b/src/pan-view/pan-timeline.c	Sun Dec 25 08:25:13 2016 +0000
@@ -21,16 +21,21 @@
 
 #include "pan-timeline.h"
 
+#include "metadata.h"
 #include "pan-item.h"
 #include "pan-util.h"
 #include "pan-view.h"
+#include "ui_fileops.h"
 
 void pan_timeline_compute(PanWindow *pw, FileData *dir_fd, gint *width, gint *height)
 {
 	GList *list;
 	GList *work;
+	GHashTable *filter_kw_table;
+	GHashTableIter filter_kw_iter;
+	gchar *filter_kw;
 	gint x, y;
-	time_t tc;
+	time_t group_start_date;
 	gint total;
 	gint count;
 	PanItem *pi_month = NULL;
@@ -41,6 +46,7 @@
 	gint y_height;
 
 	list = pan_list_tree(dir_fd, SORT_NONE, TRUE, pw->ignore_symlinks);
+	filter_kw_table = pw->filter_ui->filter_kw_table;  // Shorthand.
 
 	if (pw->cache_list && pw->exif_date_enable)
 		{
@@ -61,7 +67,8 @@
 	day_start = month_start;
 	x_width = 0;
 	y_height = 0;
-	tc = 0;
+	group_start_date = 0;
+	// total and count are used to enforce a stride of PAN_GROUP_MAX thumbs.
 	total = 0;
 	count = 0;
 	work = list;
@@ -73,13 +80,44 @@
 		fd = work->data;
 		work = work->next;
 
-		if (!pan_date_compare(fd->date, tc, PAN_DATE_LENGTH_DAY))
+		// Don't show images that fail the keyword test.
+		if (g_hash_table_size(filter_kw_table) > 0)
 			{
+			gint match_count = 0;
+			gint miss_count = 0;
+			// TODO(xsdg): OPTIMIZATION Do the search inside of metadata.c to avoid a
+			// bunch of string list copies.
+			// TODO(xsdg): Allow user to switch between union and intersection.
+			GList *img_keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
+			if (!img_keywords) continue;
+
+			g_hash_table_iter_init(&filter_kw_iter, filter_kw_table);
+			while (g_hash_table_iter_next(&filter_kw_iter, (void**)&filter_kw, NULL))
+				{
+				if (g_list_find_custom(img_keywords, filter_kw, (GCompareFunc)g_strcmp0))
+					{
+					++match_count;
+					}
+				else
+					{
+					++miss_count;
+					}
+				if (miss_count > 0) break;
+				}
+
+			string_list_free(img_keywords);
+			if (miss_count > 0 || match_count == 0) continue;
+			}
+
+		if (!pan_date_compare(fd->date, group_start_date, PAN_DATE_LENGTH_DAY))
+			{
+			// FD starts a new day group.
 			GList *needle;
 			gchar *buf;
 
-			if (!pan_date_compare(fd->date, tc, PAN_DATE_LENGTH_MONTH))
+			if (!pan_date_compare(fd->date, group_start_date, PAN_DATE_LENGTH_MONTH))
 				{
+				// FD starts a new month group.
 				pi_day = NULL;
 
 				if (pi_month)
@@ -114,7 +152,7 @@
 
 			if (pi_day) x = pi_day->x + pi_day->width + PAN_BOX_BORDER;
 
-			tc = fd->date;
+			group_start_date = fd->date;
 			total = 1;
 			count = 0;
 
@@ -124,7 +162,7 @@
 				FileData *nfd;
 
 				nfd = needle->data;
-				if (pan_date_compare(nfd->date, tc, PAN_DATE_LENGTH_DAY))
+				if (pan_date_compare(nfd->date, group_start_date, PAN_DATE_LENGTH_DAY))
 					{
 					needle = needle->next;
 					total++;
--- a/src/pan-view/pan-types.h	Sat Dec 24 22:37:21 2016 +0000
+++ b/src/pan-view/pan-types.h	Sun Dec 25 08:25:13 2016 +0000
@@ -179,6 +179,18 @@
 	GtkWidget *search_button_arrow;
 };
 
+typedef struct _PanViewFilterUi PanViewFilterUi;
+struct _PanViewFilterUi
+{
+	GtkWidget *filter_box;
+	GtkWidget *filter_entry;
+	GtkWidget *filter_label;
+	GtkWidget *filter_button;
+	GtkWidget *filter_button_arrow;
+	GHashTable *filter_kw_table;
+	GtkWidget *filter_kw_hbox;
+};
+
 typedef struct _PanWindow PanWindow;
 struct _PanWindow
 {
@@ -193,6 +205,7 @@
 	GtkWidget *label_zoom;
 
 	PanViewSearchUi *search_ui;
+	PanViewFilterUi *filter_ui;
 
 	GtkWidget *date_button;
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pan-view/pan-view-filter.c	Sun Dec 25 08:25:13 2016 +0000
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2006 John Ellis
+ * Copyright (C) 2008 - 2016 The Geeqie Team
+ *
+ * Author: John Ellis
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "pan-view-filter.h"
+
+#include "image.h"
+#include "pan-item.h"
+#include "pan-util.h"
+#include "pan-view.h"
+#include "ui_tabcomp.h"
+#include "ui_misc.h"
+
+PanViewFilterUi *pan_filter_ui_new(PanWindow *pw)
+{
+	PanViewFilterUi *ui = g_new0(PanViewFilterUi, 1);
+	GtkWidget *combo;
+	GtkWidget *hbox;
+
+	// Build the actual filter UI.
+	ui->filter_box = gtk_hbox_new(FALSE, PREF_PAD_SPACE);
+	pref_spacer(ui->filter_box, 0);
+	pref_label_new(ui->filter_box, _("Keyword Filter:"));
+
+	hbox = gtk_hbox_new(TRUE, PREF_PAD_SPACE);
+	gtk_box_pack_start(GTK_BOX(ui->filter_box), hbox, TRUE, TRUE, 0);
+	gtk_widget_show(hbox);
+
+	combo = tab_completion_new_with_history(&ui->filter_entry, "", "pan_view_filter", -1,
+						pan_filter_activate_cb, pw);
+	gtk_box_pack_start(GTK_BOX(hbox), combo, TRUE, TRUE, 0);
+	gtk_widget_show(combo);
+
+	// TODO(xsdg): Figure out whether it's useful to keep this label around.
+	ui->filter_label = gtk_label_new("");
+	//gtk_box_pack_start(GTK_BOX(hbox), ui->filter_label, FALSE, FALSE, 0);
+	//gtk_widget_show(ui->filter_label);
+
+	ui->filter_kw_hbox = gtk_hbox_new(FALSE, PREF_PAD_SPACE);
+	gtk_box_pack_start(GTK_BOX(hbox), ui->filter_kw_hbox, TRUE, TRUE, 0);
+	gtk_widget_show(ui->filter_kw_hbox);
+
+	// Build the spin-button to show/hide the filter UI.
+	ui->filter_button = gtk_toggle_button_new();
+	gtk_button_set_relief(GTK_BUTTON(ui->filter_button), GTK_RELIEF_NONE);
+	gtk_button_set_focus_on_click(GTK_BUTTON(ui->filter_button), FALSE);
+	hbox = gtk_hbox_new(FALSE, PREF_PAD_GAP);
+	gtk_container_add(GTK_CONTAINER(ui->filter_button), hbox);
+	gtk_widget_show(hbox);
+	ui->filter_button_arrow = gtk_arrow_new(GTK_ARROW_UP, GTK_SHADOW_NONE);
+	gtk_box_pack_start(GTK_BOX(hbox), ui->filter_button_arrow, FALSE, FALSE, 0);
+	gtk_widget_show(ui->filter_button_arrow);
+	pref_label_new(hbox, _("Filter"));
+
+	g_signal_connect(G_OBJECT(ui->filter_button), "clicked",
+			 G_CALLBACK(pan_filter_toggle_cb), pw);
+
+	/* Since we're using the GHashTable as a HashSet (in which key and value pointers
+	 * are always identical), specifying key _and_ value destructor callbacks will
+	 * cause a double-free.
+	 */
+	ui->filter_kw_table = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
+
+	return ui;
+}
+
+void pan_filter_ui_destroy(PanViewFilterUi **ui_ptr)
+{
+	if (ui_ptr == NULL || *ui_ptr == NULL) return;
+
+	// Note that g_clear_pointer handles already-NULL pointers.
+	g_clear_pointer(&(*ui_ptr)->filter_kw_table, g_hash_table_destroy);
+
+	g_free(*ui_ptr);
+	*ui_ptr = NULL;
+}
+
+static void pan_filter_status(PanWindow *pw, const gchar *text)
+{
+	gtk_label_set_text(GTK_LABEL(pw->filter_ui->filter_label), (text) ? text : "");
+}
+
+static void pan_filter_kw_button_cb(GtkButton *widget, gpointer data)
+{
+	PanWindow *pw = data;
+	PanViewFilterUi *ui = pw->filter_ui;
+
+	g_hash_table_remove(ui->filter_kw_table, gtk_button_get_label(GTK_BUTTON(widget)));
+	gtk_widget_destroy(GTK_WIDGET(widget));
+
+	pan_filter_status(pw, _("Removed keyword…"));
+	pan_layout_update(pw);
+}
+
+void pan_filter_activate_cb(const gchar *text, gpointer data)
+{
+	GtkWidget *kw_button;
+	PanWindow *pw = data;
+	PanViewFilterUi *ui = pw->filter_ui;
+
+	if (!text) return;
+
+	gtk_entry_set_text(GTK_ENTRY(ui->filter_entry), "");
+
+	if (g_hash_table_contains(ui->filter_kw_table, text))
+		{
+		pan_filter_status(pw, _("Already added…"));
+		return;
+		}
+
+	tab_completion_append_to_history(ui->filter_entry, text);
+
+	g_hash_table_add(ui->filter_kw_table, g_strdup(text));
+
+	kw_button = gtk_button_new_with_label(text);
+	gtk_box_pack_start(GTK_BOX(ui->filter_kw_hbox), kw_button, FALSE, FALSE, 0);
+	gtk_widget_show(kw_button);
+
+	g_signal_connect(G_OBJECT(kw_button), "clicked",
+			 G_CALLBACK(pan_filter_kw_button_cb), pw);
+
+	pan_filter_status(pw, _("Added keyword…"));
+	pan_layout_update(pw);
+}
+
+void pan_filter_activate(PanWindow *pw)
+{
+	gchar *text;
+
+	text = g_strdup(gtk_entry_get_text(GTK_ENTRY(pw->filter_ui->filter_entry)));
+	pan_filter_activate_cb(text, pw);
+	g_free(text);
+}
+
+void pan_filter_toggle_cb(GtkWidget *button, gpointer data)
+{
+	PanWindow *pw = data;
+	PanViewFilterUi *ui = pw->filter_ui;
+	gboolean visible;
+
+	visible = gtk_widget_get_visible(ui->filter_box);
+	if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button)) == visible) return;
+
+	if (visible)
+		{
+		gtk_widget_hide(ui->filter_box);
+		gtk_arrow_set(GTK_ARROW(ui->filter_button_arrow), GTK_ARROW_UP, GTK_SHADOW_NONE);
+		}
+	else
+		{
+		gtk_widget_show(ui->filter_box);
+		gtk_arrow_set(GTK_ARROW(ui->filter_button_arrow), GTK_ARROW_DOWN, GTK_SHADOW_NONE);
+		gtk_widget_grab_focus(ui->filter_entry);
+		}
+}
+
+void pan_filter_toggle_visible(PanWindow *pw, gboolean enable)
+{
+	PanViewFilterUi *ui = pw->filter_ui;
+	if (pw->fs) return;
+
+	if (enable)
+		{
+		if (gtk_widget_get_visible(ui->filter_box))
+			{
+			gtk_widget_grab_focus(ui->filter_entry);
+			}
+		else
+			{
+			gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(ui->filter_button), TRUE);
+			}
+		}
+	else
+		{
+		if (gtk_widget_get_visible(ui->filter_entry))
+			{
+			if (gtk_widget_has_focus(ui->filter_entry))
+				{
+				gtk_widget_grab_focus(GTK_WIDGET(pw->imd->widget));
+				}
+			gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(ui->filter_button), FALSE);
+			}
+		}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pan-view/pan-view-filter.h	Sun Dec 25 08:25:13 2016 +0000
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2006 John Ellis
+ * Copyright (C) 2008 - 2016 The Geeqie Team
+ *
+ * Author: John Ellis
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef PAN_VIEW_PAN_VIEW_FILTER_H
+#define PAN_VIEW_PAN_VIEW_FILTER_H
+
+#include "main.h"
+#include "pan-types.h"
+
+void pan_filter_toggle_visible(PanWindow *pw, gboolean enable);
+void pan_filter_activate(PanWindow *pw);
+void pan_filter_activate_cb(const gchar *text, gpointer data);
+void pan_filter_toggle_cb(GtkWidget *button, gpointer data);
+
+// Creates a new PanViewFilterUi instance and returns it.
+PanViewFilterUi *pan_filter_ui_new(PanWindow *pw);
+
+// Destroys the specified PanViewFilterUi and sets the pointer to NULL.
+void pan_filter_ui_destroy(PanViewFilterUi **ui);
+
+#endif
+/* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */
--- a/src/pan-view/pan-view-search.c	Sat Dec 24 22:37:21 2016 +0000
+++ b/src/pan-view/pan-view-search.c	Sun Dec 25 08:25:13 2016 +0000
@@ -75,16 +75,7 @@
 {
 	if (ui_ptr == NULL || *ui_ptr == NULL) return;
 
-	PanViewSearchUi *ui = *ui_ptr;  // For convenience.
-
-	// Note that g_clear_object handles already-NULL pointers.
-	g_clear_object(&ui->search_label);
-	g_clear_object(&ui->search_button);
-	g_clear_object(&ui->search_box);
-	g_clear_object(&ui->search_button_arrow);
-	g_clear_object(&ui->search_button);
-
-	g_free(ui);
+	g_free(*ui_ptr);
 	*ui_ptr = NULL;
 }
 
--- a/src/pan-view/pan-view.c	Sat Dec 24 22:37:21 2016 +0000
+++ b/src/pan-view/pan-view.c	Sun Dec 25 08:25:13 2016 +0000
@@ -38,6 +38,7 @@
 #include "pan-item.h"
 #include "pan-timeline.h"
 #include "pan-util.h"
+#include "pan-view-filter.h"
 #include "pan-view-search.h"
 #include "pixbuf-renderer.h"
 #include "pixbuf_util.h"
@@ -1070,7 +1071,7 @@
 		}
 }
 
-static void pan_layout_update(PanWindow *pw)
+void pan_layout_update(PanWindow *pw)
 {
 	pan_window_message(pw, _("Sorting images..."));
 	pan_layout_update_idle(pw);
@@ -1132,7 +1133,8 @@
 	imd_widget = gtk_container_get_focus_child(GTK_CONTAINER(pw->imd->widget));
 	focused = (pw->fs || (imd_widget && gtk_widget_has_focus(imd_widget)));
 	on_entry = (gtk_widget_has_focus(pw->path_entry) ||
-		    gtk_widget_has_focus(pw->search_ui->search_entry));
+		    gtk_widget_has_focus(pw->search_ui->search_entry) ||
+		    gtk_widget_has_focus(pw->filter_ui->filter_entry));
 
 	if (focused)
 		{
@@ -1735,6 +1737,8 @@
 		}
 
 	pan_fullscreen_toggle(pw, TRUE);
+	pan_search_ui_destroy(&pw->search_ui);
+	pan_filter_ui_destroy(&pw->filter_ui);
 	gtk_widget_destroy(pw->window);
 
 	pan_window_items_free(pw);
@@ -1883,6 +1887,10 @@
 	pw->search_ui = pan_search_ui_new(pw);
 	gtk_box_pack_start(GTK_BOX(vbox), pw->search_ui->search_box, FALSE, FALSE, 2);
 
+    /* filter bar */
+    pw->filter_ui = pan_filter_ui_new(pw);
+    gtk_box_pack_start(GTK_BOX(vbox), pw->filter_ui->filter_box, FALSE, FALSE, 2);
+
 	/* status bar */
 
 	box = pref_box_new(vbox, FALSE, GTK_ORIENTATION_HORIZONTAL, 0);
@@ -1914,6 +1922,10 @@
 	gtk_box_pack_end(GTK_BOX(box), pw->search_ui->search_button, FALSE, FALSE, 0);
 	gtk_widget_show(pw->search_ui->search_button);
 
+	// Add the "Filter" button to the status bar area.
+	gtk_box_pack_end(GTK_BOX(box), pw->filter_ui->filter_button, FALSE, FALSE, 0);
+	gtk_widget_show(pw->filter_ui->filter_button);
+
 	g_signal_connect(G_OBJECT(pw->window), "delete_event",
 			 G_CALLBACK(pan_window_delete_cb), pw);
 	g_signal_connect(G_OBJECT(pw->window), "key_press_event",
--- a/src/pan-view/pan-view.h	Sat Dec 24 22:37:21 2016 +0000
+++ b/src/pan-view/pan-view.h	Sun Dec 25 08:25:13 2016 +0000
@@ -25,6 +25,7 @@
 #include "main.h"
 #include "pan-types.h"
 
+void pan_layout_update(PanWindow *pw);
 GList *pan_layout_intersect(PanWindow *pw, gint x, gint y, gint width, gint height);
 void pan_layout_resize(PanWindow *pw);