changeset 2535:973e851c2ffa

Merge merge-requests '495' and '458' * github/merge-requests/495: Avoid c99 feature *phew* commented and simplified. Hopefully make it not crash It compiles! Remove all references to "IconData" update .gitignore Refactor: move view_file implementations to their own subdirectory. * github/merge-requests/458: Add the ability to use regular expressions for Pan View keyword filtering. Add pan filtering to all of the pan view modes Revamp pan view filtering to support different modes and grouping. Move filter code into pan-fiew-filter.{c,h} Adds a keyword filtering feature to Timeline PanView. Pull the search UI construction code out into a distinct function. Start moving pan view search code to its own module
author Klaus Ethgen <Klaus@Ethgen.de>
date Sat, 08 Jul 2017 10:24:19 +0100
parents 8a49df0fafeb (current diff) 0b95ae9541e9 (diff)
children b66e00ca9ba9
files src/view_file_icon.c src/view_file_icon.h src/view_file_list.c src/view_file_list.h
diffstat 12 files changed, 1003 insertions(+), 444 deletions(-) [+]
line wrap: on
line diff
--- a/src/pan-view/Makefile.am	Fri Jul 07 13:48:00 2017 +0000
+++ b/src/pan-view/Makefile.am	Sat Jul 08 10:24:19 2017 +0100
@@ -13,4 +13,9 @@
 	%D%/pan-util.c	\
 	%D%/pan-util.h	\
 	%D%/pan-view.c	\
-	%D%/pan-view.h
+	%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-calendar.c	Fri Jul 07 13:48:00 2017 +0000
+++ b/src/pan-view/pan-calendar.c	Sat Jul 08 10:24:19 2017 +0100
@@ -26,6 +26,7 @@
 
 #include "pan-util.h"
 #include "pan-view.h"
+#include "pan-view-filter.h"
 #include "pixbuf_util.h"
 
 #define PAN_CAL_POPUP_COLOR 220, 220, 220
@@ -200,6 +201,7 @@
 	gint end_month = 0;
 
 	list = pan_list_tree(dir_fd, SORT_NONE, TRUE, pw->ignore_symlinks);
+	pan_filter_fd_list(&list, pw->filter_ui->filter_elements);
 
 	if (pw->cache_list && pw->exif_date_enable)
 		{
--- a/src/pan-view/pan-folder.c	Fri Jul 07 13:48:00 2017 +0000
+++ b/src/pan-view/pan-folder.c	Sat Jul 08 10:24:19 2017 +0100
@@ -25,6 +25,7 @@
 
 #include "pan-item.h"
 #include "pan-util.h"
+#include "pan-view-filter.h"
 
 static void pan_flower_size(PanWindow *pw, gint *width, gint *height)
 {
@@ -242,6 +243,8 @@
 	f = filelist_sort(f, SORT_NAME, TRUE);
 	d = filelist_sort(d, SORT_NAME, TRUE);
 
+	pan_filter_fd_list(&f, pw->filter_ui->filter_elements);
+
 	pi_box = pan_item_text_new(pw, x, y, dir_fd->path, PAN_TEXT_ATTR_NONE,
 				   PAN_TEXT_BORDER_SIZE,
 				   PAN_TEXT_COLOR, 255);
@@ -386,6 +389,8 @@
 	f = filelist_sort(f, SORT_NAME, TRUE);
 	d = filelist_sort(d, SORT_NAME, TRUE);
 
+	pan_filter_fd_list(&f, pw->filter_ui->filter_elements);
+
 	*x = PAN_BOX_BORDER + ((*level) * MAX(PAN_BOX_BORDER, PAN_THUMB_GAP));
 
 	pi_box = pan_item_text_new(pw, *x, *y, dir_fd->path, PAN_TEXT_ATTR_NONE,
--- a/src/pan-view/pan-grid.c	Fri Jul 07 13:48:00 2017 +0000
+++ b/src/pan-view/pan-grid.c	Sat Jul 08 10:24:19 2017 +0100
@@ -25,6 +25,7 @@
 
 #include "pan-item.h"
 #include "pan-util.h"
+#include "pan-view-filter.h"
 
 void pan_grid_compute(PanWindow *pw, FileData *dir_fd, gint *width, gint *height)
 {
@@ -35,6 +36,7 @@
 	gint next_y;
 
 	list = pan_list_tree(dir_fd, SORT_NAME, TRUE, pw->ignore_symlinks);
+	pan_filter_fd_list(&list, pw->filter_ui->filter_elements);
 
 	grid_size = (gint)sqrt((gdouble)g_list_length(list));
 	if (pw->size > PAN_IMAGE_SIZE_THUMB_LARGE)
--- a/src/pan-view/pan-timeline.c	Fri Jul 07 13:48:00 2017 +0000
+++ b/src/pan-view/pan-timeline.c	Sat Jul 08 10:24:19 2017 +0100
@@ -24,13 +24,14 @@
 #include "pan-item.h"
 #include "pan-util.h"
 #include "pan-view.h"
+#include "pan-view-filter.h"
 
 void pan_timeline_compute(PanWindow *pw, FileData *dir_fd, gint *width, gint *height)
 {
 	GList *list;
 	GList *work;
 	gint x, y;
-	time_t tc;
+	time_t group_start_date;
 	gint total;
 	gint count;
 	PanItem *pi_month = NULL;
@@ -41,6 +42,7 @@
 	gint y_height;
 
 	list = pan_list_tree(dir_fd, SORT_NONE, TRUE, pw->ignore_symlinks);
+	pan_filter_fd_list(&list, pw->filter_ui->filter_elements);
 
 	if (pw->cache_list && pw->exif_date_enable)
 		{
@@ -61,7 +63,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 +76,15 @@
 		fd = work->data;
 		work = work->next;
 
-		if (!pan_date_compare(fd->date, tc, PAN_DATE_LENGTH_DAY))
+		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 +119,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 +129,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	Fri Jul 07 13:48:00 2017 +0000
+++ b/src/pan-view/pan-types.h	Sat Jul 08 10:24:19 2017 +0100
@@ -169,6 +169,19 @@
 	gboolean queued;
 };
 
+typedef struct _PanViewSearchUi PanViewSearchUi;
+struct _PanViewSearchUi
+{
+	GtkWidget *search_box;
+	GtkWidget *search_entry;
+	GtkWidget *search_label;
+	GtkWidget *search_button;
+	GtkWidget *search_button_arrow;
+};
+
+// Defined in pan-view-filter.h
+typedef struct _PanViewFilterUi PanViewFilterUi;
+
 typedef struct _PanWindow PanWindow;
 struct _PanWindow
 {
@@ -182,11 +195,8 @@
 	GtkWidget *label_message;
 	GtkWidget *label_zoom;
 
-	GtkWidget *search_box;
-	GtkWidget *search_entry;
-	GtkWidget *search_label;
-	GtkWidget *search_button;
-	GtkWidget *search_button_arrow;
+	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	Sat Jul 08 10:24:19 2017 +0100
@@ -0,0 +1,353 @@
+/*
+ * 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 "metadata.h"
+#include "pan-item.h"
+#include "pan-util.h"
+#include "pan-view.h"
+#include "ui_fileops.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;
+
+	/* 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.
+	 */
+	{
+		GtkTreeIter iter;
+		ui->filter_mode_model = gtk_list_store_new(3, G_TYPE_INT, G_TYPE_STRING, G_TYPE_STRING);
+		gtk_list_store_append(ui->filter_mode_model, &iter);
+		gtk_list_store_set(ui->filter_mode_model, &iter,
+				   0, PAN_VIEW_FILTER_REQUIRE, 1, _("Require"), 2, _("R"), -1);
+		gtk_list_store_append(ui->filter_mode_model, &iter);
+		gtk_list_store_set(ui->filter_mode_model, &iter,
+				   0, PAN_VIEW_FILTER_EXCLUDE, 1, _("Exclude"), 2, _("E"), -1);
+		gtk_list_store_append(ui->filter_mode_model, &iter);
+		gtk_list_store_set(ui->filter_mode_model, &iter,
+				   0, PAN_VIEW_FILTER_INCLUDE, 1, _("Include"), 2, _("I"), -1);
+		gtk_list_store_append(ui->filter_mode_model, &iter);
+		gtk_list_store_set(ui->filter_mode_model, &iter,
+				   0, PAN_VIEW_FILTER_GROUP, 1, _("Group"), 2, _("G"), -1);
+
+		ui->filter_mode_combo = gtk_combo_box_new_with_model(GTK_TREE_MODEL(ui->filter_mode_model));
+		gtk_combo_box_set_focus_on_click(GTK_COMBO_BOX(ui->filter_mode_combo), FALSE);
+		gtk_combo_box_set_active(GTK_COMBO_BOX(ui->filter_mode_combo), 0);
+
+		GtkCellRenderer *render = gtk_cell_renderer_text_new();
+		gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(ui->filter_mode_combo), render, TRUE);
+		gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(ui->filter_mode_combo), render, "text", 1, NULL);
+	}
+
+	// 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:"));
+
+	gtk_box_pack_start(GTK_BOX(ui->filter_box), ui->filter_mode_combo, TRUE, TRUE, 0);
+	gtk_widget_show(ui->filter_mode_combo);
+
+	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);
+
+	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)
+{
+	PanFilterCallbackState *cb_state = data;
+	PanWindow *pw = cb_state->pw;
+	PanViewFilterUi *ui = pw->filter_ui;
+
+	// TODO(xsdg): Fix filter element pointed object memory leak.
+	ui->filter_elements = g_list_delete_link(ui->filter_elements, cb_state->filter_element);
+	gtk_widget_destroy(GTK_WIDGET(widget));
+	g_free(cb_state);
+
+	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;
+	GtkTreeIter iter;
+
+	if (!text) return;
+
+	// Get all relevant state and reset UI.
+	gtk_combo_box_get_active_iter(GTK_COMBO_BOX(ui->filter_mode_combo), &iter);
+	gtk_entry_set_text(GTK_ENTRY(ui->filter_entry), "");
+	tab_completion_append_to_history(ui->filter_entry, text);
+
+	// Add new filter element.
+	PanViewFilterElement *element = g_new0(PanViewFilterElement, 1);
+	gtk_tree_model_get(GTK_TREE_MODEL(ui->filter_mode_model), &iter, 0, &element->mode, -1);
+	element->keyword = g_strdup(text);
+	if (g_strcmp0(text, g_regex_escape_string(text, -1)))
+		{
+		// It's an actual regex, so compile
+		element->kw_regex = g_regex_new(text, G_REGEX_ANCHORED | G_REGEX_OPTIMIZE, G_REGEX_MATCH_ANCHORED, NULL);
+		}
+	ui->filter_elements = g_list_append(ui->filter_elements, element);
+
+	// Get the short version of the mode value.
+	gchar *short_mode;
+	gtk_tree_model_get(GTK_TREE_MODEL(ui->filter_mode_model), &iter, 2, &short_mode, -1);
+
+	// Create the button.
+	// TODO(xsdg): Use MVC so that the button list is an actual representation of the GList
+	gchar *label = g_strdup_printf("(%s) %s", short_mode, text);
+	kw_button = gtk_button_new_with_label(label);
+	g_clear_pointer(&label, g_free);
+
+	gtk_box_pack_start(GTK_BOX(ui->filter_kw_hbox), kw_button, FALSE, FALSE, 0);
+	gtk_widget_show(kw_button);
+
+	PanFilterCallbackState *cb_state = g_new0(PanFilterCallbackState, 1);
+	cb_state->pw = pw;
+	cb_state->filter_element = g_list_last(ui->filter_elements);
+
+	g_signal_connect(G_OBJECT(kw_button), "clicked",
+			 G_CALLBACK(pan_filter_kw_button_cb), cb_state);
+
+	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);
+			}
+		}
+}
+
+static gboolean pan_view_list_contains_kw_pattern(GList *haystack, PanViewFilterElement *filter, gchar **found_kw)
+{
+	if (filter->kw_regex)
+		{
+		// regex compile succeeded; attempt regex match.
+		GList *work = g_list_first(haystack);
+		while (work)
+			{
+			gchar *keyword = work->data;
+			work = work->next;
+			if (g_regex_match(filter->kw_regex, keyword, 0x0, NULL))
+				{
+				if (found_kw) *found_kw = keyword;
+				return TRUE;
+				}
+			}
+		return FALSE;
+		}
+	else
+		{
+		// regex compile failed; fall back to exact string match.
+		GList *found_elem = g_list_find_custom(haystack, filter->keyword, (GCompareFunc)g_strcmp0);
+		if (found_elem && found_kw) *found_kw = found_elem->data;
+		return !!found_elem;
+		}
+}
+
+gboolean pan_filter_fd_list(GList **fd_list, GList *filter_elements)
+{
+	GList *work;
+	gboolean modified = FALSE;
+	GHashTable *seen_kw_table = NULL;
+
+	if (!fd_list || !*fd_list || !filter_elements) return modified;
+
+	// seen_kw_table is only valid in this scope, so don't take ownership of any strings.
+	seen_kw_table = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, NULL);
+
+	work = *fd_list;
+	while (work)
+		{
+		FileData *fd = work->data;
+		GList *last_work = work;
+		work = work->next;
+
+		// TODO(xsdg): OPTIMIZATION Do the search inside of metadata.c to avoid a
+		// bunch of string list copies.
+		GList *img_keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
+
+		// 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.
+		gboolean should_reject = FALSE;
+		gchar *group_kw = NULL;
+		GList *filter_element = filter_elements;
+		while (filter_element)
+			{
+			PanViewFilterElement *filter = filter_element->data;
+			filter_element = filter_element->next;
+			gchar *found_kw = NULL;
+			gboolean has_kw = pan_view_list_contains_kw_pattern(img_keywords, filter, &found_kw);
+
+			switch (filter->mode)
+				{
+				case PAN_VIEW_FILTER_REQUIRE:
+					should_reject |= !has_kw;
+					break;
+				case PAN_VIEW_FILTER_EXCLUDE:
+					should_reject |= has_kw;
+					break;
+				case PAN_VIEW_FILTER_INCLUDE:
+					if (has_kw) should_reject = FALSE;
+					break;
+				case PAN_VIEW_FILTER_GROUP:
+					if (has_kw)
+						{
+						if (g_hash_table_contains(seen_kw_table, found_kw))
+							{
+							should_reject = TRUE;
+							}
+						else if (group_kw == NULL)
+							{
+							group_kw = found_kw;
+							}
+						}
+					break;
+				}
+			}
+
+		if (!should_reject && group_kw != NULL) g_hash_table_add(seen_kw_table, group_kw);
+
+		group_kw = NULL;  // group_kw references an item from img_keywords.
+		string_list_free(img_keywords);
+
+		if (should_reject)
+			{
+			*fd_list = g_list_delete_link(*fd_list, last_work);
+			modified = TRUE;
+			}
+		}
+
+	g_hash_table_destroy(seen_kw_table);
+	return modified;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pan-view/pan-view-filter.h	Sat Jul 08 10:24:19 2017 +0100
@@ -0,0 +1,77 @@
+/*
+ * 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"
+
+typedef enum {
+	PAN_VIEW_FILTER_REQUIRE,
+	PAN_VIEW_FILTER_EXCLUDE,
+	PAN_VIEW_FILTER_INCLUDE,
+	PAN_VIEW_FILTER_GROUP
+} PanViewFilterMode;
+
+typedef struct _PanViewFilterElement PanViewFilterElement;
+struct _PanViewFilterElement
+{
+	PanViewFilterMode mode;
+	gchar *keyword;
+	GRegex *kw_regex;
+};
+
+typedef struct _PanFilterCallbackState PanFilterCallbackState;
+struct _PanFilterCallbackState
+{
+	PanWindow *pw;
+	GList *filter_element;
+};
+
+struct _PanViewFilterUi
+{
+	GtkWidget *filter_box;
+	GtkWidget *filter_entry;
+	GtkWidget *filter_label;
+	GtkWidget *filter_button;
+	GtkWidget *filter_button_arrow;
+	GtkWidget *filter_kw_hbox;
+	GtkListStore *filter_mode_model;
+	GtkWidget *filter_mode_combo;
+	GList *filter_elements;  // List of PanViewFilterElement.
+};
+
+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);
+
+gboolean pan_filter_fd_list(GList **fd_list, GList *filter_elements);
+
+#endif
+/* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pan-view/pan-view-search.c	Sat Jul 08 10:24:19 2017 +0100
@@ -0,0 +1,468 @@
+/*
+ * 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-search.h"
+
+#include "image.h"
+#include "pan-calendar.h"
+#include "pan-item.h"
+#include "pan-util.h"
+#include "pan-view.h"
+#include "ui_tabcomp.h"
+#include "ui_misc.h"
+
+PanViewSearchUi *pan_search_ui_new(PanWindow *pw)
+{
+	PanViewSearchUi *ui = g_new0(PanViewSearchUi, 1);
+	GtkWidget *combo;
+	GtkWidget *hbox;
+
+	// Build the actual search UI.
+	ui->search_box = gtk_hbox_new(FALSE, PREF_PAD_SPACE);
+	pref_spacer(ui->search_box, 0);
+	pref_label_new(ui->search_box, _("Find:"));
+
+	hbox = gtk_hbox_new(TRUE, PREF_PAD_SPACE);
+	gtk_box_pack_start(GTK_BOX(ui->search_box), hbox, TRUE, TRUE, 0);
+	gtk_widget_show(hbox);
+
+	combo = tab_completion_new_with_history(&ui->search_entry, "", "pan_view_search", -1,
+						pan_search_activate_cb, pw);
+	gtk_box_pack_start(GTK_BOX(hbox), combo, TRUE, TRUE, 0);
+	gtk_widget_show(combo);
+
+	ui->search_label = gtk_label_new("");
+	gtk_box_pack_start(GTK_BOX(hbox), ui->search_label, TRUE, TRUE, 0);
+	gtk_widget_show(ui->search_label);
+
+	// Build the spin-button to show/hide the search UI.
+	ui->search_button = gtk_toggle_button_new();
+	gtk_button_set_relief(GTK_BUTTON(ui->search_button), GTK_RELIEF_NONE);
+	gtk_button_set_focus_on_click(GTK_BUTTON(ui->search_button), FALSE);
+	hbox = gtk_hbox_new(FALSE, PREF_PAD_GAP);
+	gtk_container_add(GTK_CONTAINER(ui->search_button), hbox);
+	gtk_widget_show(hbox);
+	ui->search_button_arrow = gtk_arrow_new(GTK_ARROW_UP, GTK_SHADOW_NONE);
+	gtk_box_pack_start(GTK_BOX(hbox), ui->search_button_arrow, FALSE, FALSE, 0);
+	gtk_widget_show(ui->search_button_arrow);
+	pref_label_new(hbox, _("Find"));
+
+	g_signal_connect(G_OBJECT(ui->search_button), "clicked",
+			 G_CALLBACK(pan_search_toggle_cb), pw);
+
+	return ui;
+}
+
+void pan_search_ui_destroy(PanViewSearchUi **ui_ptr)
+{
+	if (ui_ptr == NULL || *ui_ptr == NULL) return;
+
+	g_free(*ui_ptr);
+	*ui_ptr = NULL;
+}
+
+static void pan_search_status(PanWindow *pw, const gchar *text)
+{
+	gtk_label_set_text(GTK_LABEL(pw->search_ui->search_label), (text) ? text : "");
+}
+
+static gint pan_search_by_path(PanWindow *pw, const gchar *path)
+{
+	PanItem *pi;
+	GList *list;
+	GList *found;
+	PanItemType type;
+	gchar *buf;
+
+	type = (pw->size > PAN_IMAGE_SIZE_THUMB_LARGE) ? PAN_ITEM_IMAGE : PAN_ITEM_THUMB;
+
+	list = pan_item_find_by_path(pw, type, path, FALSE, FALSE);
+	if (!list) return FALSE;
+
+	found = g_list_find(list, pw->click_pi);
+	if (found && found->next)
+		{
+		found = found->next;
+		pi = found->data;
+		}
+	else
+		{
+		pi = list->data;
+		}
+
+	pan_info_update(pw, pi);
+	image_scroll_to_point(pw->imd, pi->x + pi->width / 2, pi->y + pi->height / 2, 0.5, 0.5);
+
+	buf = g_strdup_printf("%s ( %d / %d )",
+			      (path[0] == G_DIR_SEPARATOR) ? _("path found") : _("filename found"),
+			      g_list_index(list, pi) + 1,
+			      g_list_length(list));
+	pan_search_status(pw, buf);
+	g_free(buf);
+
+	g_list_free(list);
+
+	return TRUE;
+}
+
+static gboolean pan_search_by_partial(PanWindow *pw, const gchar *text)
+{
+	PanItem *pi;
+	GList *list;
+	GList *found;
+	PanItemType type;
+	gchar *buf;
+
+	type = (pw->size > PAN_IMAGE_SIZE_THUMB_LARGE) ? PAN_ITEM_IMAGE : PAN_ITEM_THUMB;
+
+	list = pan_item_find_by_path(pw, type, text, TRUE, FALSE);
+	if (!list) list = pan_item_find_by_path(pw, type, text, FALSE, TRUE);
+	if (!list)
+		{
+		gchar *needle;
+
+		needle = g_utf8_strdown(text, -1);
+		list = pan_item_find_by_path(pw, type, needle, TRUE, TRUE);
+		g_free(needle);
+		}
+	if (!list) return FALSE;
+
+	found = g_list_find(list, pw->click_pi);
+	if (found && found->next)
+		{
+		found = found->next;
+		pi = found->data;
+		}
+	else
+		{
+		pi = list->data;
+		}
+
+	pan_info_update(pw, pi);
+	image_scroll_to_point(pw->imd, pi->x + pi->width / 2, pi->y + pi->height / 2, 0.5, 0.5);
+
+	buf = g_strdup_printf("%s ( %d / %d )",
+			      _("partial match"),
+			      g_list_index(list, pi) + 1,
+			      g_list_length(list));
+	pan_search_status(pw, buf);
+	g_free(buf);
+
+	g_list_free(list);
+
+	return TRUE;
+}
+
+static gboolean valid_date_separator(gchar c)
+{
+	return (c == '/' || c == '-' || c == ' ' || c == '.' || c == ',');
+}
+
+static GList *pan_search_by_date_val(PanWindow *pw, PanItemType type,
+				     gint year, gint month, gint day,
+				     const gchar *key)
+{
+	GList *list = NULL;
+	GList *work;
+
+	work = g_list_last(pw->list_static);
+	while (work)
+		{
+		PanItem *pi;
+
+		pi = work->data;
+		work = work->prev;
+
+		if (pi->fd && (pi->type == type || type == PAN_ITEM_NONE) &&
+		    ((!key && !pi->key) || (key && pi->key && strcmp(key, pi->key) == 0)))
+			{
+			struct tm *tl;
+
+			tl = localtime(&pi->fd->date);
+			if (tl)
+				{
+				gint match;
+
+				match = (tl->tm_year == year - 1900);
+				if (match && month >= 0) match = (tl->tm_mon == month - 1);
+				if (match && day > 0) match = (tl->tm_mday == day);
+
+				if (match) list = g_list_prepend(list, pi);
+				}
+			}
+		}
+
+	return g_list_reverse(list);
+}
+
+static gboolean pan_search_by_date(PanWindow *pw, const gchar *text)
+{
+	PanItem *pi = NULL;
+	GList *list = NULL;
+	GList *found;
+	gint year;
+	gint month = -1;
+	gint day = -1;
+	gchar *ptr;
+	gchar *mptr;
+	struct tm *lt;
+	time_t t;
+	gchar *message;
+	gchar *buf;
+	gchar *buf_count;
+
+	if (!text) return FALSE;
+
+	ptr = (gchar *)text;
+	while (*ptr != '\0')
+		{
+		if (!g_unichar_isdigit(*ptr) && !valid_date_separator(*ptr)) return FALSE;
+		ptr++;
+		}
+
+	t = time(NULL);
+	if (t == -1) return FALSE;
+	lt = localtime(&t);
+	if (!lt) return FALSE;
+
+	if (valid_date_separator(*text))
+		{
+		year = -1;
+		mptr = (gchar *)text;
+		}
+	else
+		{
+		year = (gint)strtol(text, &mptr, 10);
+		if (mptr == text) return FALSE;
+		}
+
+	if (*mptr != '\0' && valid_date_separator(*mptr))
+		{
+		gchar *dptr;
+
+		mptr++;
+		month = strtol(mptr, &dptr, 10);
+		if (dptr == mptr)
+			{
+			if (valid_date_separator(*dptr))
+				{
+				month = lt->tm_mon + 1;
+				dptr++;
+				}
+			else
+				{
+				month = -1;
+				}
+			}
+		if (dptr != mptr && *dptr != '\0' && valid_date_separator(*dptr))
+			{
+			gchar *eptr;
+			dptr++;
+			day = strtol(dptr, &eptr, 10);
+			if (dptr == eptr)
+				{
+				day = lt->tm_mday;
+				}
+			}
+		}
+
+	if (year == -1)
+		{
+		year = lt->tm_year + 1900;
+		}
+	else if (year < 100)
+		{
+		if (year > 70)
+			year+= 1900;
+		else
+			year+= 2000;
+		}
+
+	if (year < 1970 ||
+	    month < -1 || month == 0 || month > 12 ||
+	    day < -1 || day == 0 || day > 31) return FALSE;
+
+	t = pan_date_to_time(year, month, day);
+	if (t < 0) return FALSE;
+
+	if (pw->layout == PAN_LAYOUT_CALENDAR)
+		{
+		list = pan_search_by_date_val(pw, PAN_ITEM_BOX, year, month, day, "day");
+		}
+	else
+		{
+		PanItemType type;
+
+		type = (pw->size > PAN_IMAGE_SIZE_THUMB_LARGE) ? PAN_ITEM_IMAGE : PAN_ITEM_THUMB;
+		list = pan_search_by_date_val(pw, type, year, month, day, NULL);
+		}
+
+	if (list)
+		{
+		found = g_list_find(list, pw->search_pi);
+		if (found && found->next)
+			{
+			found = found->next;
+			pi = found->data;
+			}
+		else
+			{
+			pi = list->data;
+			}
+		}
+
+	pw->search_pi = pi;
+
+	if (pw->layout == PAN_LAYOUT_CALENDAR && pi && pi->type == PAN_ITEM_BOX)
+		{
+		pan_info_update(pw, NULL);
+		pan_calendar_update(pw, pi);
+		image_scroll_to_point(pw->imd,
+				      pi->x + pi->width / 2,
+				      pi->y + pi->height / 2, 0.5, 0.5);
+		}
+	else if (pi)
+		{
+		pan_info_update(pw, pi);
+		image_scroll_to_point(pw->imd,
+				      pi->x - PAN_BOX_BORDER * 5 / 2,
+				      pi->y, 0.0, 0.5);
+		}
+
+	if (month > 0)
+		{
+		buf = pan_date_value_string(t, PAN_DATE_LENGTH_MONTH);
+		if (day > 0)
+			{
+			gchar *tmp;
+			tmp = buf;
+			buf = g_strdup_printf("%d %s", day, tmp);
+			g_free(tmp);
+			}
+		}
+	else
+		{
+		buf = pan_date_value_string(t, PAN_DATE_LENGTH_YEAR);
+		}
+
+	if (pi)
+		{
+		buf_count = g_strdup_printf("( %d / %d )",
+					    g_list_index(list, pi) + 1,
+					    g_list_length(list));
+		}
+	else
+		{
+		buf_count = g_strdup_printf("(%s)", _("no match"));
+		}
+
+	message = g_strdup_printf("%s %s %s", _("Date:"), buf, buf_count);
+	g_free(buf);
+	g_free(buf_count);
+	pan_search_status(pw, message);
+	g_free(message);
+
+	g_list_free(list);
+
+	return TRUE;
+}
+
+void pan_search_activate_cb(const gchar *text, gpointer data)
+{
+	PanWindow *pw = data;
+
+	if (!text) return;
+
+	tab_completion_append_to_history(pw->search_ui->search_entry, text);
+
+	if (pan_search_by_path(pw, text)) return;
+
+	if ((pw->layout == PAN_LAYOUT_TIMELINE ||
+	     pw->layout == PAN_LAYOUT_CALENDAR) &&
+	    pan_search_by_date(pw, text))
+		{
+		return;
+		}
+
+	if (pan_search_by_partial(pw, text)) return;
+
+	pan_search_status(pw, _("no match"));
+}
+
+void pan_search_activate(PanWindow *pw)
+{
+	gchar *text;
+
+	text = g_strdup(gtk_entry_get_text(GTK_ENTRY(pw->search_ui->search_entry)));
+	pan_search_activate_cb(text, pw);
+	g_free(text);
+}
+
+void pan_search_toggle_cb(GtkWidget *button, gpointer data)
+{
+	PanWindow *pw = data;
+	PanViewSearchUi *ui = pw->search_ui;
+	gboolean visible;
+
+	visible = gtk_widget_get_visible(ui->search_box);
+	if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button)) == visible) return;
+
+	if (visible)
+		{
+		gtk_widget_hide(ui->search_box);
+		gtk_arrow_set(GTK_ARROW(ui->search_button_arrow), GTK_ARROW_UP, GTK_SHADOW_NONE);
+		}
+	else
+		{
+		gtk_widget_show(ui->search_box);
+		gtk_arrow_set(GTK_ARROW(ui->search_button_arrow), GTK_ARROW_DOWN, GTK_SHADOW_NONE);
+		gtk_widget_grab_focus(ui->search_entry);
+		}
+}
+
+void pan_search_toggle_visible(PanWindow *pw, gboolean enable)
+{
+	PanViewSearchUi *ui = pw->search_ui;
+	if (pw->fs) return;
+
+	if (enable)
+		{
+		if (gtk_widget_get_visible(ui->search_box))
+			{
+			gtk_widget_grab_focus(ui->search_entry);
+			}
+		else
+			{
+			gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(ui->search_button), TRUE);
+			}
+		}
+	else
+		{
+		if (gtk_widget_get_visible(ui->search_entry))
+			{
+			if (gtk_widget_has_focus(ui->search_entry))
+				{
+				gtk_widget_grab_focus(GTK_WIDGET(pw->imd->widget));
+				}
+			gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(ui->search_button), FALSE);
+			}
+		}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pan-view/pan-view-search.h	Sat Jul 08 10:24:19 2017 +0100
@@ -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_SEARCH_H
+#define PAN_VIEW_PAN_VIEW_SEARCH_H
+
+#include "main.h"
+#include "pan-types.h"
+
+void pan_search_toggle_visible(PanWindow *pw, gboolean enable);
+void pan_search_activate(PanWindow *pw);
+void pan_search_activate_cb(const gchar *text, gpointer data);
+void pan_search_toggle_cb(GtkWidget *button, gpointer data);
+
+// Creates a new PanViewSearchUi instance and returns it.
+PanViewSearchUi *pan_search_ui_new(PanWindow *pw);
+
+// Destroys the specified PanViewSearchUi and sets the pointer to NULL.
+void pan_search_ui_destroy(PanViewSearchUi **ui);
+
+#endif
+/* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */
--- a/src/pan-view/pan-view.c	Fri Jul 07 13:48:00 2017 +0000
+++ b/src/pan-view/pan-view.c	Sat Jul 08 10:24:19 2017 +0100
@@ -38,6 +38,8 @@
 #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"
 #include "thumb.h"
@@ -78,9 +80,6 @@
 
 static void pan_fullscreen_toggle(PanWindow *pw, gboolean force_off);
 
-static void pan_search_toggle_visible(PanWindow *pw, gboolean enable);
-static void pan_search_activate(PanWindow *pw);
-
 static void pan_window_close(PanWindow *pw);
 
 static GtkWidget *pan_popup_menu(PanWindow *pw);
@@ -1072,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);
@@ -1134,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_entry));
+		    gtk_widget_has_focus(pw->search_ui->search_entry) ||
+		    gtk_widget_has_focus(pw->filter_ui->filter_entry));
 
 	if (focused)
 		{
@@ -1248,6 +1248,7 @@
 
 		if (stop_signal) return stop_signal;
 
+		// Don't steal characters from entry boxes.
 		if (!on_entry)
 			{
 			stop_signal = TRUE;
@@ -1326,7 +1327,7 @@
 }
 
 
-static void pan_info_update(PanWindow *pw, PanItem *pi)
+void pan_info_update(PanWindow *pw, PanItem *pi)
 {
 	PanTextAlignment *ta;
 	PanItem *pbox;
@@ -1452,399 +1453,6 @@
 
 /*
  *-----------------------------------------------------------------------------
- * search
- *-----------------------------------------------------------------------------
- */
-
-static void pan_search_status(PanWindow *pw, const gchar *text)
-{
-	gtk_label_set_text(GTK_LABEL(pw->search_label), (text) ? text : "");
-}
-
-static gint pan_search_by_path(PanWindow *pw, const gchar *path)
-{
-	PanItem *pi;
-	GList *list;
-	GList *found;
-	PanItemType type;
-	gchar *buf;
-
-	type = (pw->size > PAN_IMAGE_SIZE_THUMB_LARGE) ? PAN_ITEM_IMAGE : PAN_ITEM_THUMB;
-
-	list = pan_item_find_by_path(pw, type, path, FALSE, FALSE);
-	if (!list) return FALSE;
-
-	found = g_list_find(list, pw->click_pi);
-	if (found && found->next)
-		{
-		found = found->next;
-		pi = found->data;
-		}
-	else
-		{
-		pi = list->data;
-		}
-
-	pan_info_update(pw, pi);
-	image_scroll_to_point(pw->imd, pi->x + pi->width / 2, pi->y + pi->height / 2, 0.5, 0.5);
-
-	buf = g_strdup_printf("%s ( %d / %d )",
-			      (path[0] == G_DIR_SEPARATOR) ? _("path found") : _("filename found"),
-			      g_list_index(list, pi) + 1,
-			      g_list_length(list));
-	pan_search_status(pw, buf);
-	g_free(buf);
-
-	g_list_free(list);
-
-	return TRUE;
-}
-
-static gboolean pan_search_by_partial(PanWindow *pw, const gchar *text)
-{
-	PanItem *pi;
-	GList *list;
-	GList *found;
-	PanItemType type;
-	gchar *buf;
-
-	type = (pw->size > PAN_IMAGE_SIZE_THUMB_LARGE) ? PAN_ITEM_IMAGE : PAN_ITEM_THUMB;
-
-	list = pan_item_find_by_path(pw, type, text, TRUE, FALSE);
-	if (!list) list = pan_item_find_by_path(pw, type, text, FALSE, TRUE);
-	if (!list)
-		{
-		gchar *needle;
-
-		needle = g_utf8_strdown(text, -1);
-		list = pan_item_find_by_path(pw, type, needle, TRUE, TRUE);
-		g_free(needle);
-		}
-	if (!list) return FALSE;
-
-	found = g_list_find(list, pw->click_pi);
-	if (found && found->next)
-		{
-		found = found->next;
-		pi = found->data;
-		}
-	else
-		{
-		pi = list->data;
-		}
-
-	pan_info_update(pw, pi);
-	image_scroll_to_point(pw->imd, pi->x + pi->width / 2, pi->y + pi->height / 2, 0.5, 0.5);
-
-	buf = g_strdup_printf("%s ( %d / %d )",
-			      _("partial match"),
-			      g_list_index(list, pi) + 1,
-			      g_list_length(list));
-	pan_search_status(pw, buf);
-	g_free(buf);
-
-	g_list_free(list);
-
-	return TRUE;
-}
-
-static gboolean valid_date_separator(gchar c)
-{
-	return (c == '/' || c == '-' || c == ' ' || c == '.' || c == ',');
-}
-
-static GList *pan_search_by_date_val(PanWindow *pw, PanItemType type,
-				     gint year, gint month, gint day,
-				     const gchar *key)
-{
-	GList *list = NULL;
-	GList *work;
-
-	work = g_list_last(pw->list_static);
-	while (work)
-		{
-		PanItem *pi;
-
-		pi = work->data;
-		work = work->prev;
-
-		if (pi->fd && (pi->type == type || type == PAN_ITEM_NONE) &&
-		    ((!key && !pi->key) || (key && pi->key && strcmp(key, pi->key) == 0)))
-			{
-			struct tm *tl;
-
-			tl = localtime(&pi->fd->date);
-			if (tl)
-				{
-				gint match;
-
-				match = (tl->tm_year == year - 1900);
-				if (match && month >= 0) match = (tl->tm_mon == month - 1);
-				if (match && day > 0) match = (tl->tm_mday == day);
-
-				if (match) list = g_list_prepend(list, pi);
-				}
-			}
-		}
-
-	return g_list_reverse(list);
-}
-
-static gboolean pan_search_by_date(PanWindow *pw, const gchar *text)
-{
-	PanItem *pi = NULL;
-	GList *list = NULL;
-	GList *found;
-	gint year;
-	gint month = -1;
-	gint day = -1;
-	gchar *ptr;
-	gchar *mptr;
-	struct tm *lt;
-	time_t t;
-	gchar *message;
-	gchar *buf;
-	gchar *buf_count;
-
-	if (!text) return FALSE;
-
-	ptr = (gchar *)text;
-	while (*ptr != '\0')
-		{
-		if (!g_unichar_isdigit(*ptr) && !valid_date_separator(*ptr)) return FALSE;
-		ptr++;
-		}
-
-	t = time(NULL);
-	if (t == -1) return FALSE;
-	lt = localtime(&t);
-	if (!lt) return FALSE;
-
-	if (valid_date_separator(*text))
-		{
-		year = -1;
-		mptr = (gchar *)text;
-		}
-	else
-		{
-		year = (gint)strtol(text, &mptr, 10);
-		if (mptr == text) return FALSE;
-		}
-
-	if (*mptr != '\0' && valid_date_separator(*mptr))
-		{
-		gchar *dptr;
-
-		mptr++;
-		month = strtol(mptr, &dptr, 10);
-		if (dptr == mptr)
-			{
-			if (valid_date_separator(*dptr))
-				{
-				month = lt->tm_mon + 1;
-				dptr++;
-				}
-			else
-				{
-				month = -1;
-				}
-			}
-		if (dptr != mptr && *dptr != '\0' && valid_date_separator(*dptr))
-			{
-			gchar *eptr;
-			dptr++;
-			day = strtol(dptr, &eptr, 10);
-			if (dptr == eptr)
-				{
-				day = lt->tm_mday;
-				}
-			}
-		}
-
-	if (year == -1)
-		{
-		year = lt->tm_year + 1900;
-		}
-	else if (year < 100)
-		{
-		if (year > 70)
-			year+= 1900;
-		else
-			year+= 2000;
-		}
-
-	if (year < 1970 ||
-	    month < -1 || month == 0 || month > 12 ||
-	    day < -1 || day == 0 || day > 31) return FALSE;
-
-	t = pan_date_to_time(year, month, day);
-	if (t < 0) return FALSE;
-
-	if (pw->layout == PAN_LAYOUT_CALENDAR)
-		{
-		list = pan_search_by_date_val(pw, PAN_ITEM_BOX, year, month, day, "day");
-		}
-	else
-		{
-		PanItemType type;
-
-		type = (pw->size > PAN_IMAGE_SIZE_THUMB_LARGE) ? PAN_ITEM_IMAGE : PAN_ITEM_THUMB;
-		list = pan_search_by_date_val(pw, type, year, month, day, NULL);
-		}
-
-	if (list)
-		{
-		found = g_list_find(list, pw->search_pi);
-		if (found && found->next)
-			{
-			found = found->next;
-			pi = found->data;
-			}
-		else
-			{
-			pi = list->data;
-			}
-		}
-
-	pw->search_pi = pi;
-
-	if (pw->layout == PAN_LAYOUT_CALENDAR && pi && pi->type == PAN_ITEM_BOX)
-		{
-		pan_info_update(pw, NULL);
-		pan_calendar_update(pw, pi);
-		image_scroll_to_point(pw->imd,
-				      pi->x + pi->width / 2,
-				      pi->y + pi->height / 2, 0.5, 0.5);
-		}
-	else if (pi)
-		{
-		pan_info_update(pw, pi);
-		image_scroll_to_point(pw->imd,
-				      pi->x - PAN_BOX_BORDER * 5 / 2,
-				      pi->y, 0.0, 0.5);
-		}
-
-	if (month > 0)
-		{
-		buf = pan_date_value_string(t, PAN_DATE_LENGTH_MONTH);
-		if (day > 0)
-			{
-			gchar *tmp;
-			tmp = buf;
-			buf = g_strdup_printf("%d %s", day, tmp);
-			g_free(tmp);
-			}
-		}
-	else
-		{
-		buf = pan_date_value_string(t, PAN_DATE_LENGTH_YEAR);
-		}
-
-	if (pi)
-		{
-		buf_count = g_strdup_printf("( %d / %d )",
-					    g_list_index(list, pi) + 1,
-					    g_list_length(list));
-		}
-	else
-		{
-		buf_count = g_strdup_printf("(%s)", _("no match"));
-		}
-
-	message = g_strdup_printf("%s %s %s", _("Date:"), buf, buf_count);
-	g_free(buf);
-	g_free(buf_count);
-	pan_search_status(pw, message);
-	g_free(message);
-
-	g_list_free(list);
-
-	return TRUE;
-}
-
-static void pan_search_activate_cb(const gchar *text, gpointer data)
-{
-	PanWindow *pw = data;
-
-	if (!text) return;
-
-	tab_completion_append_to_history(pw->search_entry, text);
-
-	if (pan_search_by_path(pw, text)) return;
-
-	if ((pw->layout == PAN_LAYOUT_TIMELINE ||
-	     pw->layout == PAN_LAYOUT_CALENDAR) &&
-	    pan_search_by_date(pw, text))
-		{
-		return;
-		}
-
-	if (pan_search_by_partial(pw, text)) return;
-
-	pan_search_status(pw, _("no match"));
-}
-
-static void pan_search_activate(PanWindow *pw)
-{
-	gchar *text;
-
-	text = g_strdup(gtk_entry_get_text(GTK_ENTRY(pw->search_entry)));
-	pan_search_activate_cb(text, pw);
-	g_free(text);
-}
-
-static void pan_search_toggle_cb(GtkWidget *button, gpointer data)
-{
-	PanWindow *pw = data;
-	gboolean visible;
-
-	visible = gtk_widget_get_visible(pw->search_box);
-	if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button)) == visible) return;
-
-	if (visible)
-		{
-		gtk_widget_hide(pw->search_box);
-		gtk_arrow_set(GTK_ARROW(pw->search_button_arrow), GTK_ARROW_UP, GTK_SHADOW_NONE);
-		}
-	else
-		{
-		gtk_widget_show(pw->search_box);
-		gtk_arrow_set(GTK_ARROW(pw->search_button_arrow), GTK_ARROW_DOWN, GTK_SHADOW_NONE);
-		gtk_widget_grab_focus(pw->search_entry);
-		}
-}
-
-static void pan_search_toggle_visible(PanWindow *pw, gboolean enable)
-{
-	if (pw->fs) return;
-
-	if (enable)
-		{
-		if (gtk_widget_get_visible(pw->search_box))
-			{
-			gtk_widget_grab_focus(pw->search_entry);
-			}
-		else
-			{
-			gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(pw->search_button), TRUE);
-			}
-		}
-	else
-		{
-		if (gtk_widget_get_visible(pw->search_entry))
-			{
-			if (gtk_widget_has_focus(pw->search_entry))
-				{
-				gtk_widget_grab_focus(GTK_WIDGET(pw->imd->widget));
-				}
-			gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(pw->search_button), FALSE);
-			}
-		}
-}
-
-
-/*
- *-----------------------------------------------------------------------------
  * main window
  *-----------------------------------------------------------------------------
  */
@@ -2129,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);
@@ -2274,24 +1884,12 @@
 
 	/* find bar */
 
-	pw->search_box = gtk_hbox_new(FALSE, PREF_PAD_SPACE);
-	gtk_box_pack_start(GTK_BOX(vbox), pw->search_box, FALSE, FALSE, 2);
-
-	pref_spacer(pw->search_box, 0);
-	pref_label_new(pw->search_box, _("Find:"));
-
-	hbox = gtk_hbox_new(TRUE, PREF_PAD_SPACE);
-	gtk_box_pack_start(GTK_BOX(pw->search_box), hbox, TRUE, TRUE, 0);
-	gtk_widget_show(hbox);
-
-	combo = tab_completion_new_with_history(&pw->search_entry, "", "pan_view_search", -1,
-						pan_search_activate_cb, pw);
-	gtk_box_pack_start(GTK_BOX(hbox), combo, TRUE, TRUE, 0);
-	gtk_widget_show(combo);
-
-	pw->search_label = gtk_label_new("");
-	gtk_box_pack_start(GTK_BOX(hbox), pw->search_label, TRUE, TRUE, 0);
-	gtk_widget_show(pw->search_label);
+	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 */
 
@@ -2320,21 +1918,13 @@
 	gtk_container_add(GTK_CONTAINER(frame), pw->label_zoom);
 	gtk_widget_show(pw->label_zoom);
 
-	pw->search_button = gtk_toggle_button_new();
-	gtk_button_set_relief(GTK_BUTTON(pw->search_button), GTK_RELIEF_NONE);
-	gtk_button_set_focus_on_click(GTK_BUTTON(pw->search_button), FALSE);
-	hbox = gtk_hbox_new(FALSE, PREF_PAD_GAP);
-	gtk_container_add(GTK_CONTAINER(pw->search_button), hbox);
-	gtk_widget_show(hbox);
-	pw->search_button_arrow = gtk_arrow_new(GTK_ARROW_UP, GTK_SHADOW_NONE);
-	gtk_box_pack_start(GTK_BOX(hbox), pw->search_button_arrow, FALSE, FALSE, 0);
-	gtk_widget_show(pw->search_button_arrow);
-	pref_label_new(hbox, _("Find"));
-
-	gtk_box_pack_end(GTK_BOX(box), pw->search_button, FALSE, FALSE, 0);
-	gtk_widget_show(pw->search_button);
-	g_signal_connect(G_OBJECT(pw->search_button), "clicked",
-			 G_CALLBACK(pan_search_toggle_cb), pw);
+	// Add the "Find" button to the status bar area.
+	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);
--- a/src/pan-view/pan-view.h	Fri Jul 07 13:48:00 2017 +0000
+++ b/src/pan-view/pan-view.h	Sat Jul 08 10:24:19 2017 +0100
@@ -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);
 
@@ -32,6 +33,7 @@
 
 GList *pan_cache_sort(GList *list, SortType method, gboolean ascend);
 
+void pan_info_update(PanWindow *pw, PanItem *pi);
 
 #endif
 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */