view src/search.c @ 1388:2496a345c452

Try to improve tab completion: when a key is pressed while tab completion menu is shown, the number of entries is reduced, dhowing all matching entries but the menu is no more closed after each key pressure. Number of possible entries in this menu was increased from 500 to 1000. Pressing TAB when path entry is empty now adds / (root directory).
author zas_
date Fri, 06 Mar 2009 22:34:38 +0000
parents fe4da037be21
children a0bd58a6535f
line wrap: on
line source

/*
 * Geeqie
 * (C) 2005 John Ellis
 * Copyright (C) 2008 - 2009 The Geeqie Team
 *
 * Author: John Ellis
 *
 * This software is released under the GNU General Public License (GNU GPL).
 * Please read the included file COPYING for more information.
 * This software comes with no warranty of any kind, use at your own risk!
 */


#include "main.h"
#include "search.h"

#include "cache.h"
#include "collect.h"
#include "collect-table.h"
#include "dnd.h"
#include "dupe.h"
#include "editors.h"
#include "filedata.h"
#include "image-load.h"
#include "img-view.h"
#include "layout_image.h"
#include "menu.h"
#include "metadata.h"
#include "misc.h"
#include "print.h"
#include "thumb.h"
#include "ui_bookmark.h"
#include "ui_fileops.h"
#include "ui_menu.h"
#include "ui_misc.h"
#include "ui_spinner.h"
#include "ui_tabcomp.h"
#include "ui_tree_edit.h"
#include "uri_utils.h"
#include "utilops.h"
#include "window.h"
#include "bar_keywords.h"

#include <gdk/gdkkeysyms.h> /* for keyboard values */


#define DEF_SEARCH_WIDTH  700
#define DEF_SEARCH_HEIGHT 450

#define SEARCH_BUFFER_MATCH_LOAD 20
#define SEARCH_BUFFER_MATCH_HIT  5
#define SEARCH_BUFFER_MATCH_MISS 1
#define SEARCH_BUFFER_FLUSH_SIZE 99


typedef enum {
	SEARCH_MATCH_NONE,
	SEARCH_MATCH_EQUAL,
	SEARCH_MATCH_CONTAINS,
	SEARCH_MATCH_UNDER,
	SEARCH_MATCH_OVER,
	SEARCH_MATCH_BETWEEN,
	SEARCH_MATCH_ALL,
	SEARCH_MATCH_ANY
} MatchType;

enum {
	SEARCH_COLUMN_POINTER = 0,
	SEARCH_COLUMN_RANK,
	SEARCH_COLUMN_THUMB,
	SEARCH_COLUMN_NAME,
	SEARCH_COLUMN_SIZE,
	SEARCH_COLUMN_DATE,
	SEARCH_COLUMN_DIMENSIONS,
	SEARCH_COLUMN_PATH,
	SEARCH_COLUMN_COUNT	/* total columns */
};

typedef struct _SearchData SearchData;
struct _SearchData
{
	GtkWidget *window;

	GtkWidget *button_thumbs;
	GtkWidget *label_status;
	GtkWidget *label_progress;
	GtkWidget *button_start;
	GtkWidget *button_stop;
	GtkWidget *spinner;

	GtkWidget *box_search;

	GtkWidget *menu_path;
	GtkWidget *path_entry;
	GtkWidget *check_recurse;

	GtkWidget *result_view;

	GtkWidget *check_name;
	GtkWidget *menu_name;
	GtkWidget *entry_name;
	GtkWidget *check_name_match_case;

	GtkWidget *check_size;
	GtkWidget *menu_size;
	GtkWidget *spin_size;
	GtkWidget *spin_size_end;

	GtkWidget *check_date;
	GtkWidget *menu_date;
	GtkWidget *date_sel;
	GtkWidget *date_sel_end;

	GtkWidget *check_dimensions;
	GtkWidget *menu_dimensions;
	GtkWidget *spin_width;
	GtkWidget *spin_height;
	GtkWidget *spin_width_end;
	GtkWidget *spin_height_end;

	GtkWidget *check_similarity;
	GtkWidget *spin_similarity;
	GtkWidget *entry_similarity;

	GtkWidget *check_keywords;
	GtkWidget *menu_keywords;
	GtkWidget *entry_keywords;

	GtkWidget *check_comment;
	GtkWidget *menu_comment;
	GtkWidget *entry_comment;

	FileData *search_dir_fd;
	gint   search_path_recurse;
	gchar *search_name;
	gint   search_name_match_case;
	gint64 search_size;
	gint64 search_size_end;
	gint   search_date_y;
	gint   search_date_m;
	gint   search_date_d;
	gint   search_date_end_y;
	gint   search_date_end_m;
	gint   search_date_end_d;
	gint   search_width;
	gint   search_height;
	gint   search_width_end;
	gint   search_height_end;
	gint   search_similarity;
	gchar *search_similarity_path;
	CacheData *search_similarity_cd;
	GList *search_keyword_list;
	gchar *search_comment;
	gint   search_comment_match_case;

	MatchType search_type;

	MatchType match_name;
	MatchType match_size;
	MatchType match_date;
	MatchType match_dimensions;
	MatchType match_keywords;
	MatchType match_comment;

	gboolean match_name_enable;
	gboolean match_size_enable;
	gboolean match_date_enable;
	gboolean match_dimensions_enable;
	gboolean match_similarity_enable;
	gboolean match_keywords_enable;
	gboolean match_comment_enable;

	GList *search_folder_list;
	GList *search_done_list;
	GList *search_file_list;
	GList *search_buffer_list;

	gint search_count;
	gint search_total;
	gint search_buffer_count;

	gint search_idle_id;
	gint update_idle_id;

	ImageLoader *img_loader;
	CacheData   *img_cd;

	FileData *click_fd;

	ThumbLoader *thumb_loader;
	gint thumb_enable;
	FileData *thumb_fd;
};

typedef struct _MatchFileData MatchFileData;
struct _MatchFileData
{
	FileData *fd;
	gint width;
	gint height;
	gint rank;
};

typedef struct _MatchList MatchList;
struct _MatchList
{
	const gchar *text;
	const MatchType type;
};

static const MatchList text_search_menu_path[] = {
	{ N_("folder"),		SEARCH_MATCH_NONE },
	{ N_("comments"),	SEARCH_MATCH_ALL },
	{ N_("results"),	SEARCH_MATCH_CONTAINS }
};

static const MatchList text_search_menu_name[] = {
	{ N_("contains"),	SEARCH_MATCH_CONTAINS },
	{ N_("is"),		SEARCH_MATCH_EQUAL }
};

static const MatchList text_search_menu_size[] = {
	{ N_("equal to"),	SEARCH_MATCH_EQUAL },
	{ N_("less than"),	SEARCH_MATCH_UNDER },
	{ N_("greater than"),	SEARCH_MATCH_OVER },
	{ N_("between"),	SEARCH_MATCH_BETWEEN }
};

static const MatchList text_search_menu_date[] = {
	{ N_("equal to"),	SEARCH_MATCH_EQUAL },
	{ N_("before"),		SEARCH_MATCH_UNDER },
	{ N_("after"),		SEARCH_MATCH_OVER },
	{ N_("between"),	SEARCH_MATCH_BETWEEN }
};

static const MatchList text_search_menu_keyword[] = {
	{ N_("match all"),	SEARCH_MATCH_ALL },
	{ N_("match any"),	SEARCH_MATCH_ANY },
	{ N_("exclude"),	SEARCH_MATCH_NONE }
};

static const MatchList text_search_menu_comment[] = {
	{ N_("contains"),	SEARCH_MATCH_CONTAINS },
	{ N_("miss"),		SEARCH_MATCH_NONE }
};

static GList *search_window_list = NULL;


static gint search_result_selection_count(SearchData *sd, gint64 *bytes);
static gint search_result_count(SearchData *sd, gint64 *bytes);

static void search_window_close(SearchData *sd);

static void search_notify_cb(FileData *fd, NotifyType type, gpointer data);

/*
 *-------------------------------------------------------------------
 * utils
 *-------------------------------------------------------------------
 */

static time_t convert_dmy_to_time(gint day, gint month, gint year)
{
	struct tm lt;

	lt.tm_sec = 0;
	lt.tm_min = 0;
	lt.tm_hour = 0;
	lt.tm_mday = day;
	lt.tm_mon = month - 1;
	lt.tm_year = year - 1900;
	lt.tm_isdst = 0;

	return mktime(&lt);
}

static void search_status_update(SearchData *sd)
{
	gchar *buf;
	gint t;
	gint s;
	gint64 t_bytes;
	gint64 s_bytes;
	gchar *tt;

	t = search_result_count(sd, &t_bytes);
	s = search_result_selection_count(sd, &s_bytes);
	
	tt = text_from_size_abrev(t_bytes);

	if (s > 0)
		{
		gchar *ts = text_from_size_abrev(s_bytes);
		buf = g_strdup_printf(_("%s, %d files (%s, %d)"), tt, t, ts, s);
		g_free(ts);
		}
	else
		{
		buf = g_strdup_printf(_("%s, %d files"), tt, t);
		}

	g_free(tt);

	gtk_label_set_text(GTK_LABEL(sd->label_status), buf);
	g_free(buf);
}

static void search_progress_update(SearchData *sd, gint search, gdouble thumbs)
{

	if (search || thumbs >= 0.0)
		{
		gchar *buf;
		const gchar *message;

		if (search && (sd->search_folder_list || sd->search_file_list))
			message = _("Searching...");
		else if (thumbs >= 0.0)
			message = _("Loading thumbs...");
		else
			message = "";

		buf = g_strdup_printf("%s(%d / %d)", message, sd->search_count, sd->search_total);
		gtk_progress_bar_set_text(GTK_PROGRESS_BAR(sd->label_progress), buf);
		gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(sd->label_progress),
					      (thumbs >= 0.0) ? thumbs : 0.0);
		g_free(buf);
		}
	else
		{
		gtk_progress_bar_set_text(GTK_PROGRESS_BAR(sd->label_progress), "");
		gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(sd->label_progress), 0.0);
		}
}

/*
 *-------------------------------------------------------------------
 * result list
 *-------------------------------------------------------------------
 */

static gint search_result_find_row(SearchData *sd, FileData *fd, GtkTreeIter *iter)
{
	GtkTreeModel *store;
	gint valid;
	gint n = 0;

	store = gtk_tree_view_get_model(GTK_TREE_VIEW(sd->result_view));
	valid = gtk_tree_model_get_iter_first(store, iter);
	while (valid)
		{
		MatchFileData *mfd;
		n++;

		gtk_tree_model_get(store, iter, SEARCH_COLUMN_POINTER, &mfd, -1);
		if (mfd->fd == fd) return n;
		valid = gtk_tree_model_iter_next(store, iter);
		}

	return -1;
}

static gint search_result_row_selected(SearchData *sd, FileData *fd)
{
	GtkTreeModel *store;
	GtkTreeSelection *selection;
	GList *slist;
	GList *work;
	gint found = FALSE;

	selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(sd->result_view));
	slist = gtk_tree_selection_get_selected_rows(selection, &store);
	work = slist;
	while (!found && work)
		{
		GtkTreePath *tpath = work->data;
		MatchFileData *mfd_n;
		GtkTreeIter iter;

		gtk_tree_model_get_iter(store, &iter, tpath);
		gtk_tree_model_get(store, &iter, SEARCH_COLUMN_POINTER, &mfd_n, -1);
		if (mfd_n->fd == fd) found = TRUE;
		work = work->next;
		}
	g_list_foreach(slist, (GFunc)gtk_tree_path_free, NULL);
	g_list_free(slist);

	return found;
}

static gint search_result_selection_util(SearchData *sd, gint64 *bytes, GList **list)
{
	GtkTreeModel *store;
	GtkTreeSelection *selection;
	GList *slist;
	GList *work;
	gint n = 0;
	gint64 total = 0;
	GList *plist = NULL;

	store = gtk_tree_view_get_model(GTK_TREE_VIEW(sd->result_view));
	selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(sd->result_view));
	slist = gtk_tree_selection_get_selected_rows(selection, &store);
	work = slist;
	while (work)
		{
		n++;

		if (bytes || list)
			{
			GtkTreePath *tpath = work->data;
			MatchFileData *mfd;
			GtkTreeIter iter;

			gtk_tree_model_get_iter(store, &iter, tpath);
			gtk_tree_model_get(store, &iter, SEARCH_COLUMN_POINTER, &mfd, -1);
			total += mfd->fd->size;

			if (list) plist = g_list_prepend(plist, file_data_ref(mfd->fd));
			}

		work = work->next;
		}
	g_list_foreach(slist, (GFunc)gtk_tree_path_free, NULL);
	g_list_free(slist);

	if (bytes) *bytes = total;
	if (list) *list = g_list_reverse(plist);

	return n;
}

static GList *search_result_selection_list(SearchData *sd)
{
	GList *list;

	search_result_selection_util(sd, NULL, &list);
	return list;
}

static gint search_result_selection_count(SearchData *sd, gint64 *bytes)
{
	return search_result_selection_util(sd, bytes, NULL);
}

static gint search_result_util(SearchData *sd, gint64 *bytes, GList **list)
{
	GtkTreeModel *store;
	GtkTreeIter iter;
	gint valid;
	gint n = 0;
	gint64 total = 0;
	GList *plist = NULL;

	store = gtk_tree_view_get_model(GTK_TREE_VIEW(sd->result_view));

	valid = gtk_tree_model_get_iter_first(store, &iter);
	while (valid)
		{
		n++;
		if (bytes || list)
			{
			MatchFileData *mfd;

			gtk_tree_model_get(store, &iter, SEARCH_COLUMN_POINTER, &mfd, -1);
			total += mfd->fd->size;

			if (list) plist = g_list_prepend(plist, file_data_ref(mfd->fd));
			}
		valid = gtk_tree_model_iter_next(store, &iter);
		}

	if (bytes) *bytes = total;
	if (list) *list = g_list_reverse(plist);

	return n;
}

static GList *search_result_get_filelist(SearchData *sd)
{
	GList *list = NULL;

	search_result_util(sd, NULL, &list);
	return list;
}

static gint search_result_count(SearchData *sd, gint64 *bytes)
{
	return search_result_util(sd, bytes, NULL);
}

static void search_result_append(SearchData *sd, MatchFileData *mfd)
{
	FileData *fd;
	GtkListStore *store;
	GtkTreeIter iter;
	gchar *text_size;
	gchar *text_dim = NULL;

	fd = mfd->fd;

	if (!fd) return;

	text_size = text_from_size(fd->size);
	if (mfd->width > 0 && mfd->height > 0) text_dim = g_strdup_printf("%d x %d", mfd->width, mfd->height);

	store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(sd->result_view)));
	gtk_list_store_append(store, &iter);
	gtk_list_store_set(store, &iter,
				SEARCH_COLUMN_POINTER, mfd,
				SEARCH_COLUMN_RANK, mfd->rank,
				SEARCH_COLUMN_THUMB, fd->thumb_pixbuf,
				SEARCH_COLUMN_NAME, fd->name,
				SEARCH_COLUMN_SIZE, text_size,
				SEARCH_COLUMN_DATE, text_from_time(fd->date),
				SEARCH_COLUMN_DIMENSIONS, text_dim,
				SEARCH_COLUMN_PATH, fd->path,
				-1);

	g_free(text_size);
	g_free(text_dim);
}

static GList *search_result_refine_list(SearchData *sd)
{
	GList *list = NULL;
	GtkTreeModel *store;
	GtkTreeIter iter;
	gint valid;

	store = gtk_tree_view_get_model(GTK_TREE_VIEW(sd->result_view));

	valid = gtk_tree_model_get_iter_first(store, &iter);
	while (valid)
		{
		MatchFileData *mfd;

		gtk_tree_model_get(store, &iter, SEARCH_COLUMN_POINTER, &mfd, -1);
		list = g_list_prepend(list, mfd->fd);

		valid = gtk_tree_model_iter_next(store, &iter);
		}

	/* clear it here, so that the FileData in list is not freed */
	gtk_list_store_clear(GTK_LIST_STORE(store));

	return g_list_reverse(list);
}

static gboolean search_result_free_node(GtkTreeModel *store, GtkTreePath *tpath,
					GtkTreeIter *iter, gpointer data)
{
	MatchFileData *mfd;

	gtk_tree_model_get(store, iter, SEARCH_COLUMN_POINTER, &mfd, -1);
	file_data_unref(mfd->fd);
	g_free(mfd);

	return FALSE;
}

static void search_result_clear(SearchData *sd)
{
	GtkListStore *store;

	store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(sd->result_view)));

	gtk_tree_model_foreach(GTK_TREE_MODEL(store), search_result_free_node, sd);
	gtk_list_store_clear(store);

	sd->click_fd = NULL;

	thumb_loader_free(sd->thumb_loader);
	sd->thumb_loader = NULL;
	sd->thumb_fd = NULL;

	search_status_update(sd);
}

static void search_result_remove_item(SearchData *sd, MatchFileData *mfd, GtkTreeIter *iter)
{
	GtkTreeModel *store;

	if (!mfd || !iter) return;

	store = gtk_tree_view_get_model(GTK_TREE_VIEW(sd->result_view));

	tree_view_move_cursor_away(GTK_TREE_VIEW(sd->result_view), iter, TRUE);

	gtk_list_store_remove(GTK_LIST_STORE(store), iter);
	if (sd->click_fd == mfd->fd) sd->click_fd = NULL;
	if (sd->thumb_fd == mfd->fd) sd->thumb_fd = NULL;
	file_data_unref(mfd->fd);
	g_free(mfd);
}

static void search_result_remove(SearchData *sd, FileData *fd)
{
	GtkTreeModel *store;
	GtkTreeIter iter;
	gint valid;

	store = gtk_tree_view_get_model(GTK_TREE_VIEW(sd->result_view));
	valid = gtk_tree_model_get_iter_first(store, &iter);
	while (valid)
		{
		MatchFileData *mfd;

		gtk_tree_model_get(store, &iter, SEARCH_COLUMN_POINTER, &mfd, -1);
		if (mfd->fd == fd)
			{
			search_result_remove_item(sd, mfd, &iter);
			return;
			}

		valid = gtk_tree_model_iter_next(GTK_TREE_MODEL(store), &iter);
		}
}

static void search_result_remove_selection(SearchData *sd)
{
	GtkTreeSelection *selection;
	GtkTreeModel *store;
	GList *slist;
	GList *flist = NULL;
	GList *work;

	selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(sd->result_view));
	slist = gtk_tree_selection_get_selected_rows(selection, &store);
	work = slist;
	while (work)
		{
		GtkTreePath *tpath = work->data;
		GtkTreeIter iter;
		MatchFileData *mfd;

		gtk_tree_model_get_iter(store, &iter, tpath);
		gtk_tree_model_get(store, &iter, SEARCH_COLUMN_POINTER, &mfd, -1);
		flist = g_list_prepend(flist, mfd->fd);
		work = work->next;
		}
	g_list_foreach(slist, (GFunc)gtk_tree_path_free, NULL);
	g_list_free(slist);

	work = flist;
	while (work)
		{
		FileData *fd = work->data;
		work = work->next;

		search_result_remove(sd, fd);
		}
	g_list_free(flist);

	search_status_update(sd);
}

static void search_result_edit_selected(SearchData *sd, const gchar *key)
{
	GList *list;

	list = search_result_selection_list(sd);
	file_util_start_editor_from_filelist(key, list, sd->window);
	filelist_free(list);
}

static void search_result_collection_from_selection(SearchData *sd)
{
	CollectWindow *w;
	GList *list;

	list = search_result_selection_list(sd);
	w = collection_window_new(NULL);
	collection_table_add_filelist(w->table, list);
	filelist_free(list);
}

static gint search_result_update_idle_cb(gpointer data)
{
	SearchData *sd = data;

	search_status_update(sd);

	sd->update_idle_id = -1;
	return FALSE;
}

static void search_result_update_idle_cancel(SearchData *sd)
{
	if (sd->update_idle_id != -1) g_source_remove(sd->update_idle_id);
	sd->update_idle_id = -1;
}

static gboolean search_result_select_cb(GtkTreeSelection *selection, GtkTreeModel *store,
					GtkTreePath *tpath, gboolean selected, gpointer data)
{
	SearchData *sd = data;

	if (sd->update_idle_id == -1)
		{
		sd->update_idle_id = g_idle_add(search_result_update_idle_cb, sd);
		}

	return TRUE;
}

/*
 *-------------------------------------------------------------------
 * result list thumbs
 *-------------------------------------------------------------------
 */

static void search_result_thumb_step(SearchData *sd);


static void search_result_thumb_set(SearchData *sd, FileData *fd, GtkTreeIter *iter)
{
	GtkListStore *store;
	GtkTreeIter iter_n;

	store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(sd->result_view)));
	if (!iter)
		{
		if (search_result_find_row(sd, fd, &iter_n) >= 0) iter = &iter_n;
		}

	if (iter) gtk_list_store_set(store, iter, SEARCH_COLUMN_THUMB, fd->thumb_pixbuf, -1);
}

static void search_result_thumb_do(SearchData *sd)
{
	FileData *fd;

	if (!sd->thumb_loader || !sd->thumb_fd) return;
	fd = sd->thumb_fd;

	search_result_thumb_set(sd, fd, NULL);
}

static void search_result_thumb_done_cb(ThumbLoader *tl, gpointer data)
{
	SearchData *sd = data;

	search_result_thumb_do(sd);
	search_result_thumb_step(sd);
}

static void search_result_thumb_step(SearchData *sd)
{
	GtkTreeModel *store;
	GtkTreeIter iter;
	MatchFileData *mfd = NULL;
	gint valid;
	gint row = 0;
	gint length = 0;

	store = gtk_tree_view_get_model(GTK_TREE_VIEW(sd->result_view));
	valid = gtk_tree_model_get_iter_first(store, &iter);
	if (!sd->thumb_enable)
		{
		while (valid)
			{
			gtk_list_store_set(GTK_LIST_STORE(store), &iter, SEARCH_COLUMN_THUMB, NULL, -1);
			valid = gtk_tree_model_iter_next(store, &iter);
			}
		return;
		}

	while (!mfd && valid)
		{
		GdkPixbuf *pixbuf;

		length++;
		gtk_tree_model_get(store, &iter, SEARCH_COLUMN_POINTER, &mfd, SEARCH_COLUMN_THUMB, &pixbuf, -1);
		if (pixbuf || mfd->fd->thumb_pixbuf)
			{
			if (!pixbuf) gtk_list_store_set(GTK_LIST_STORE(store), &iter, SEARCH_COLUMN_THUMB, mfd->fd->thumb_pixbuf, -1);
			row++;
			mfd = NULL;
			}
		valid = gtk_tree_model_iter_next(store, &iter);
		}
	if (valid)
		{
		while (gtk_tree_model_iter_next(store, &iter)) length++;
		}

	if (!mfd)
		{
		sd->thumb_fd = NULL;
		thumb_loader_free(sd->thumb_loader);
		sd->thumb_loader = NULL;

		search_progress_update(sd, TRUE, -1.0);
		return;
		}

	search_progress_update(sd, FALSE, (gdouble)row/length);

	sd->thumb_fd = mfd->fd;
	thumb_loader_free(sd->thumb_loader);
	sd->thumb_loader = thumb_loader_new(options->thumbnails.max_width, options->thumbnails.max_height);

	thumb_loader_set_callbacks(sd->thumb_loader,
				   search_result_thumb_done_cb,
				   search_result_thumb_done_cb,
				   NULL,
				   sd);
	if (!thumb_loader_start(sd->thumb_loader, mfd->fd))
		{
		search_result_thumb_do(sd);
		search_result_thumb_step(sd);
		}
}

static void search_result_thumb_height(SearchData *sd)
{
	GtkTreeViewColumn *column;
	GtkCellRenderer *cell;
	GList *list;

	column = gtk_tree_view_get_column(GTK_TREE_VIEW(sd->result_view), SEARCH_COLUMN_THUMB - 1);
	if (!column) return;

	gtk_tree_view_column_set_fixed_width(column, (sd->thumb_enable) ? options->thumbnails.max_width : 4);

	list = gtk_tree_view_column_get_cell_renderers(column);
	if (!list) return;
	cell = list->data;
	g_list_free(list);

	g_object_set(G_OBJECT(cell), "height", (sd->thumb_enable) ? options->thumbnails.max_height : -1, NULL);
	gtk_tree_view_columns_autosize(GTK_TREE_VIEW(sd->result_view));
}

static void search_result_thumb_enable(SearchData *sd, gint enable)
{
	if (sd->thumb_enable == enable) return;

	if (sd->thumb_enable)
		{
		GtkTreeModel *store;
		GtkTreeIter iter;
		gint valid;

		thumb_loader_free(sd->thumb_loader);
		sd->thumb_loader = NULL;

		store = gtk_tree_view_get_model(GTK_TREE_VIEW(sd->result_view));
		valid = gtk_tree_model_get_iter_first(store, &iter);
		while (valid)
			{
			gtk_list_store_set(GTK_LIST_STORE(store), &iter, SEARCH_COLUMN_THUMB, NULL, -1);
			valid = gtk_tree_model_iter_next(store, &iter);
			}
		search_progress_update(sd, TRUE, -1.0);
		}

	sd->thumb_enable = enable;

	search_result_thumb_height(sd);
	if (!sd->search_folder_list && !sd->search_file_list) search_result_thumb_step(sd);
}

/*
 *-------------------------------------------------------------------
 * result list menu
 *-------------------------------------------------------------------
 */

static void sr_menu_view_cb(GtkWidget *widget, gpointer data)
{
	SearchData *sd = data;

	if (sd->click_fd) layout_image_set_fd(NULL, sd->click_fd);
}

static void sr_menu_viewnew_cb(GtkWidget *widget, gpointer data)
{
	SearchData *sd = data;
	GList *list;

	list = search_result_selection_list(sd);
	view_window_new_from_list(list);
	filelist_free(list);
}

static void sr_menu_select_all_cb(GtkWidget *widget, gpointer data)
{
	SearchData *sd = data;
	GtkTreeSelection *selection;

	selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(sd->result_view));
	gtk_tree_selection_select_all(selection);
}

static void sr_menu_select_none_cb(GtkWidget *widget, gpointer data)
{
	SearchData *sd = data;
	GtkTreeSelection *selection;

	selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(sd->result_view));
	gtk_tree_selection_unselect_all(selection);
}

static void sr_menu_edit_cb(GtkWidget *widget, gpointer data)
{
	SearchData *sd;
	const gchar *key = data;

	sd = submenu_item_get_data(widget);
	if (!sd) return;

	search_result_edit_selected(sd, key);
}

static void sr_menu_collection_cb(GtkWidget *widget, gpointer data)
{
	SearchData *sd = data;

	search_result_collection_from_selection(sd);
}

static void sr_menu_print_cb(GtkWidget *widget, gpointer data)
{
	SearchData *sd = data;

	print_window_new(sd->click_fd, search_result_selection_list(sd),
			 search_result_get_filelist(sd), sd->window);
}

static void sr_menu_copy_cb(GtkWidget *widget, gpointer data)
{
	SearchData *sd = data;

	file_util_copy(NULL, search_result_selection_list(sd), NULL, sd->window);
}

static void sr_menu_move_cb(GtkWidget *widget, gpointer data)
{
	SearchData *sd = data;

	file_util_move(NULL, search_result_selection_list(sd), NULL, sd->window);
}

static void sr_menu_rename_cb(GtkWidget *widget, gpointer data)
{
	SearchData *sd = data;

	file_util_rename(NULL, search_result_selection_list(sd), sd->window);
}

static void sr_menu_delete_cb(GtkWidget *widget, gpointer data)
{
	SearchData *sd = data;

	file_util_delete(NULL, search_result_selection_list(sd), sd->window);
}

static void sr_menu_copy_path_cb(GtkWidget *widget, gpointer data)
{
	SearchData *sd = data;

	file_util_copy_path_list_to_clipboard(search_result_selection_list(sd));
}

static void sr_menu_remove_cb(GtkWidget *widget, gpointer data)
{
	SearchData *sd = data;

	search_result_remove_selection(sd);
}

static void sr_menu_clear_cb(GtkWidget *widget, gpointer data)
{
	SearchData *sd = data;

	search_result_clear(sd);
}

static GtkWidget *search_result_menu(SearchData *sd, gint on_row, gint empty)
{
	GtkWidget *menu;
	GtkWidget *item;

	menu = popup_menu_short_lived();
	menu_item_add_sensitive(menu, _("_View"), on_row,
				G_CALLBACK(sr_menu_view_cb), sd);
	menu_item_add_stock_sensitive(menu, _("View in _new window"), GTK_STOCK_NEW, on_row,
				      G_CALLBACK(sr_menu_viewnew_cb), sd);
	menu_item_add_divider(menu);
	menu_item_add_sensitive(menu, _("Select all"), !empty,
				G_CALLBACK(sr_menu_select_all_cb), sd);
	menu_item_add_sensitive(menu, _("Select none"), !empty,
				G_CALLBACK(sr_menu_select_none_cb), sd);
	menu_item_add_divider(menu);
	submenu_add_edit(menu, &item, G_CALLBACK(sr_menu_edit_cb), sd);
	if (!on_row) gtk_widget_set_sensitive(item, FALSE);
	menu_item_add_stock_sensitive(menu, _("Add to new collection"), GTK_STOCK_INDEX, on_row,
				      G_CALLBACK(sr_menu_collection_cb), sd);
	menu_item_add_stock_sensitive(menu, _("Print..."), GTK_STOCK_PRINT, on_row,
				      G_CALLBACK(sr_menu_print_cb), sd);
	menu_item_add_divider(menu);
	menu_item_add_stock_sensitive(menu, _("_Copy..."), GTK_STOCK_COPY, on_row,
				      G_CALLBACK(sr_menu_copy_cb), sd);
	menu_item_add_sensitive(menu, _("_Move..."), on_row,
				G_CALLBACK(sr_menu_move_cb), sd);
	menu_item_add_sensitive(menu, _("_Rename..."), on_row,
				G_CALLBACK(sr_menu_rename_cb), sd);
	menu_item_add_stock_sensitive(menu, _("_Delete..."), GTK_STOCK_DELETE, on_row,
				      G_CALLBACK(sr_menu_delete_cb), sd);
	if (options->show_copy_path)
		menu_item_add_sensitive(menu, _("_Copy path"), on_row,
					G_CALLBACK(sr_menu_copy_path_cb), sd);
	menu_item_add_divider(menu);
	menu_item_add_stock_sensitive(menu, _("Rem_ove"), GTK_STOCK_REMOVE, on_row,
				      G_CALLBACK(sr_menu_remove_cb), sd);
	menu_item_add_stock_sensitive(menu, _("C_lear"), GTK_STOCK_CLEAR, !empty,
				      G_CALLBACK(sr_menu_clear_cb), sd);

	return menu;
}

static void search_result_menu_pos_cb(GtkMenu *menu, gint *x, gint *y, gboolean *push_in, gpointer data)
{
	SearchData *sd = data;
	GtkTreePath *tpath;
	gint cx, cy, cw, ch;

	gtk_tree_view_get_cursor(GTK_TREE_VIEW(sd->result_view), &tpath, NULL);
	if (!tpath) return;

	tree_view_get_cell_clamped(GTK_TREE_VIEW(sd->result_view), tpath,
				   SEARCH_COLUMN_NAME - 1, TRUE, &cx, &cy, &cw, &ch);
	gtk_tree_path_free(tpath);
	cy += ch;
	popup_menu_position_clamp(menu, &cx, &cy, 0);
	*x = cx;
	*y = cy;
}

/*
 *-------------------------------------------------------------------
 * result list input
 *-------------------------------------------------------------------
 */

static gint search_result_press_cb(GtkWidget *widget, GdkEventButton *bevent, gpointer data)
{
	SearchData *sd = data;
	GtkTreeModel *store;
	GtkTreePath *tpath;
	GtkTreeIter iter;
	MatchFileData *mfd = NULL;

	store = gtk_tree_view_get_model(GTK_TREE_VIEW(widget));

	if (gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(widget), bevent->x, bevent->y,
					  &tpath, NULL, NULL, NULL))
		{
		gtk_tree_model_get_iter(store, &iter, tpath);
		gtk_tree_model_get(store, &iter, SEARCH_COLUMN_POINTER, &mfd, -1);
		gtk_tree_path_free(tpath);
		}

	sd->click_fd = mfd ? mfd->fd : NULL;

	if (bevent->button == MOUSE_BUTTON_RIGHT)
		{
		GtkWidget *menu;

		menu = search_result_menu(sd, (mfd != NULL), (search_result_count(sd, NULL) == 0));
		gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL, bevent->button, bevent->time);
		}

	if (!mfd) return FALSE;

	if (bevent->button == MOUSE_BUTTON_LEFT && bevent->type == GDK_2BUTTON_PRESS)
		{
		layout_image_set_fd(NULL, mfd->fd);
		}

	if (bevent->button == MOUSE_BUTTON_MIDDLE) return TRUE;

	if (bevent->button == MOUSE_BUTTON_RIGHT)
		{
		if (!search_result_row_selected(sd, mfd->fd))
			{
			GtkTreeSelection *selection;

			selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(widget));
			gtk_tree_selection_unselect_all(selection);
			gtk_tree_selection_select_iter(selection, &iter);

			tpath = gtk_tree_model_get_path(store, &iter);
			gtk_tree_view_set_cursor(GTK_TREE_VIEW(widget), tpath, NULL, FALSE);
			gtk_tree_path_free(tpath);
			}
		return TRUE;
		}

	if (bevent->button == MOUSE_BUTTON_LEFT && bevent->type == GDK_BUTTON_PRESS &&
	    !(bevent->state & GDK_SHIFT_MASK ) &&
	    !(bevent->state & GDK_CONTROL_MASK ) &&
	    search_result_row_selected(sd, mfd->fd))
		{
		/* this selection handled on release_cb */
		gtk_widget_grab_focus(widget);
		return TRUE;
		}

	return FALSE;
}

static gint search_result_release_cb(GtkWidget *widget, GdkEventButton *bevent, gpointer data)
{
	SearchData *sd = data;
	GtkTreeModel *store;
	GtkTreePath *tpath;
	GtkTreeIter iter;

	MatchFileData *mfd = NULL;

	if (bevent->button != MOUSE_BUTTON_LEFT && bevent->button != MOUSE_BUTTON_MIDDLE) return TRUE;

	store = gtk_tree_view_get_model(GTK_TREE_VIEW(widget));

	if ((bevent->x != 0 || bevent->y != 0) &&
	    gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(widget), bevent->x, bevent->y,
					  &tpath, NULL, NULL, NULL))
		{
		gtk_tree_model_get_iter(store, &iter, tpath);
		gtk_tree_model_get(store, &iter, SEARCH_COLUMN_POINTER, &mfd, -1);
		gtk_tree_path_free(tpath);
		}

	if (bevent->button == MOUSE_BUTTON_MIDDLE)
		{
		if (mfd && sd->click_fd == mfd->fd)
			{
			GtkTreeSelection *selection;

			selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(widget));
			if (search_result_row_selected(sd, mfd->fd))
				{
				gtk_tree_selection_unselect_iter(selection, &iter);
				}
			else
				{
				gtk_tree_selection_select_iter(selection, &iter);
				}
			}
		return TRUE;
		}

	if (mfd && sd->click_fd == mfd->fd &&
	    !(bevent->state & GDK_SHIFT_MASK ) &&
	    !(bevent->state & GDK_CONTROL_MASK ) &&
	    search_result_row_selected(sd, mfd->fd))
		{
		GtkTreeSelection *selection;

		selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(widget));
		gtk_tree_selection_unselect_all(selection);
		gtk_tree_selection_select_iter(selection, &iter);

		tpath = gtk_tree_model_get_path(store, &iter);
		gtk_tree_view_set_cursor(GTK_TREE_VIEW(widget), tpath, NULL, FALSE);
		gtk_tree_path_free(tpath);

		return TRUE;
		}

	return FALSE;
}

static gint search_result_keypress_cb(GtkWidget *widget, GdkEventKey *event, gpointer data)
{
	SearchData *sd = data;
	gint stop_signal = FALSE;
	GtkTreeModel *store;
	GtkTreeSelection *selection;
	GList *slist;
	MatchFileData *mfd = NULL;

	selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(sd->result_view));
	slist = gtk_tree_selection_get_selected_rows(selection, &store);
	if (slist)
		{
		GtkTreePath *tpath;
		GtkTreeIter iter;
		GList *last;

		last = g_list_last(slist);
		tpath = last->data;

		/* last is newest selected file */
		gtk_tree_model_get_iter(store, &iter, tpath);
		gtk_tree_model_get(store, &iter, SEARCH_COLUMN_POINTER, &mfd, -1);
		}
	g_list_foreach(slist, (GFunc)gtk_tree_path_free, NULL);
	g_list_free(slist);

	if (event->state & GDK_CONTROL_MASK)
		{
		gint edit_val = -1;

		stop_signal = TRUE;
		switch (event->keyval)
			{
			case '1':
				edit_val = 0;
				break;
			case '2':
				edit_val = 1;
				break;
			case '3':
				edit_val = 2;
				break;
			case '4':
				edit_val = 3;
				break;
			case '5':
				edit_val = 4;
				break;
			case '6':
				edit_val = 5;
				break;
			case '7':
				edit_val = 6;
				break;
			case '8':
				edit_val = 7;
				break;
			case '9':
				edit_val = 8;
				break;
			case '0':
				edit_val = 9;
				break;
			case 'C': case 'c':
				file_util_copy(NULL, search_result_selection_list(sd), NULL, widget);
				break;
			case 'M': case 'm':
				file_util_move(NULL, search_result_selection_list(sd), NULL, widget);
				break;
			case 'R': case 'r':
				file_util_rename(NULL, search_result_selection_list(sd), widget);
				break;
			case 'D': case 'd':
				file_util_delete(NULL, search_result_selection_list(sd), widget);
				break;
			case 'A': case 'a':
				if (event->state & GDK_SHIFT_MASK)
					{
					gtk_tree_selection_unselect_all(selection);
					}
				else
					{
					gtk_tree_selection_select_all(selection);
					}
				break;
			case GDK_Delete: case GDK_KP_Delete:
				search_result_clear(sd);
				break;
			default:
				stop_signal = FALSE;
				break;
			}
#if 0
		if (edit_val >= 0)
			{
			search_result_edit_selected(sd, edit_val);
			}
#endif
		}
	else
		{
		stop_signal = TRUE;
		switch (event->keyval)
			{
			case GDK_Return: case GDK_KP_Enter:
				if (mfd) layout_image_set_fd(NULL, mfd->fd);
				break;
			case 'V': case 'v':
				{
				GList *list;

				list = search_result_selection_list(sd);
				view_window_new_from_list(list);
				filelist_free(list);
				}
				break;
			case GDK_Delete: case GDK_KP_Delete:
				search_result_remove_selection(sd);
				break;
			case 'C': case 'c':
				search_result_collection_from_selection(sd);
				break;
			case GDK_Menu:
			case GDK_F10:
				{
				GtkWidget *menu;

				sd->click_fd = mfd->fd;
				menu = search_result_menu(sd, (mfd != NULL), (search_result_count(sd, NULL) > 0));
				gtk_menu_popup(GTK_MENU(menu), NULL, NULL,
					       search_result_menu_pos_cb, sd, 0, GDK_CURRENT_TIME);
				}
				break;
			default:
				stop_signal = FALSE;
				break;
			}
		}

	return stop_signal;
}

static gint search_window_keypress_cb(GtkWidget *widget, GdkEventKey *event, gpointer data)
{
	SearchData *sd = data;
	gint stop_signal = FALSE;

	if (event->state & GDK_CONTROL_MASK)
		{
		stop_signal = TRUE;
		switch (event->keyval)
			{
			case 'T': case 't':
				gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(sd->button_thumbs),
					!gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(sd->button_thumbs)));
				break;
			case 'W': case 'w':
				search_window_close(sd);
				break;
			default:
				stop_signal = FALSE;
				break;
			}
		}

	return stop_signal;
}

/*
 *-------------------------------------------------------------------
 * dnd
 *-------------------------------------------------------------------
 */

static GtkTargetEntry result_drag_types[] = {
	{ "text/uri-list", 0, TARGET_URI_LIST },
	{ "text/plain", 0, TARGET_TEXT_PLAIN }
};
static gint n_result_drag_types = 2;

static void search_dnd_data_set(GtkWidget *widget, GdkDragContext *context,
				GtkSelectionData *selection_data, guint info,
				guint time, gpointer data)
{
	SearchData *sd = data;
	gchar *uri_text;
	gint length;
	GList *list;

	switch (info)
		{
		case TARGET_URI_LIST:
		case TARGET_TEXT_PLAIN:
			list = search_result_selection_list(sd);
			if (!list) return;
			uri_text = uri_text_from_filelist(list, &length, (info == TARGET_TEXT_PLAIN));
			filelist_free(list);
			break;
		default:
			uri_text = NULL;
			break;
		}

	if (uri_text) gtk_selection_data_set(selection_data, selection_data->target,
					     8, (guchar *)uri_text, length);
	g_free(uri_text);
}

static void search_dnd_begin(GtkWidget *widget, GdkDragContext *context, gpointer data)
{
	SearchData *sd = data;

	if (sd->click_fd && !search_result_row_selected(sd, sd->click_fd))
		{
		GtkListStore *store;
		GtkTreeIter iter;

		store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(widget)));
		if (search_result_find_row(sd, sd->click_fd, &iter) >= 0)
			{
			GtkTreeSelection *selection;
			GtkTreePath *tpath;

			selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(widget));
			gtk_tree_selection_unselect_all(selection);
			gtk_tree_selection_select_iter(selection, &iter);

			tpath = gtk_tree_model_get_path(GTK_TREE_MODEL(store), &iter);
			gtk_tree_view_set_cursor(GTK_TREE_VIEW(widget), tpath, NULL, FALSE);
			gtk_tree_path_free(tpath);
			}
		}

	if (sd->thumb_enable &&
	    sd->click_fd && sd->click_fd->thumb_pixbuf)
		{
		dnd_set_drag_icon(widget, context, sd->click_fd->thumb_pixbuf, search_result_selection_count(sd, NULL));
		}
}

static void search_dnd_init(SearchData *sd)
{
	gtk_drag_source_set(sd->result_view, GDK_BUTTON1_MASK | GDK_BUTTON2_MASK,
			    result_drag_types, n_result_drag_types,
			    GDK_ACTION_COPY | GDK_ACTION_MOVE | GDK_ACTION_LINK);
	g_signal_connect(G_OBJECT(sd->result_view), "drag_data_get",
			 G_CALLBACK(search_dnd_data_set), sd);
	g_signal_connect(G_OBJECT(sd->result_view), "drag_begin",
			 G_CALLBACK(search_dnd_begin), sd);
#if 0
	g_signal_connect(G_OBJECT(sd->result_view), "drag_end",
			 G_CALLBACK(search_dnd_end), sd);
#endif

}

/*
 *-------------------------------------------------------------------
 * search core
 *-------------------------------------------------------------------
 */

#define MATCH_IS_BETWEEN(val, a, b)  (b > a ? (val >= a && val <= b) : (val >= b && val <= a))

static gint search_step_cb(gpointer data);


static void search_buffer_flush(SearchData *sd)
{
	GList *work;

	work = g_list_last(sd->search_buffer_list);
	while (work)
		{
		MatchFileData *mfd = work->data;
		work = work->prev;

		search_result_append(sd, mfd);
		}

	g_list_free(sd->search_buffer_list);
	sd->search_buffer_list = NULL;
	sd->search_buffer_count = 0;
}

static void search_stop(SearchData *sd)
{
	if (sd->search_idle_id != -1)
		{
		g_source_remove(sd->search_idle_id);
		sd->search_idle_id = -1;
		}

	image_loader_free(sd->img_loader);
	sd->img_loader = NULL;
	cache_sim_data_free(sd->img_cd);
	sd->img_cd = NULL;

	cache_sim_data_free(sd->search_similarity_cd);
	sd->search_similarity_cd = NULL;

	search_buffer_flush(sd);

	filelist_free(sd->search_folder_list);
	sd->search_folder_list = NULL;

	g_list_free(sd->search_done_list);
	sd->search_done_list = NULL;

	filelist_free(sd->search_file_list);
	sd->search_file_list = NULL;

	gtk_widget_set_sensitive(sd->box_search, TRUE);
	spinner_set_interval(sd->spinner, -1);
	gtk_widget_set_sensitive(sd->button_start, TRUE);
	gtk_widget_set_sensitive(sd->button_stop, FALSE);
	search_progress_update(sd, TRUE, -1.0);
	search_status_update(sd);
}

static void search_file_load_process(SearchData *sd, CacheData *cd)
{
	GdkPixbuf *pixbuf;

	pixbuf = image_loader_get_pixbuf(sd->img_loader);

	if (cd && pixbuf)
		{
		if (!cd->dimensions)
			{
			cache_sim_data_set_dimensions(cd, gdk_pixbuf_get_width(pixbuf),
							  gdk_pixbuf_get_height(pixbuf));
			}

		if (sd->match_similarity_enable && !cd->similarity)
			{
			ImageSimilarityData *sim;

			sim = image_sim_new_from_pixbuf(pixbuf);
			cache_sim_data_set_similarity(cd, sim);
			image_sim_free(sim);
			}

		if (options->thumbnails.enable_caching &&
		    sd->img_loader && image_loader_get_fd(sd->img_loader))
			{
			gchar *base;
			const gchar *path;
			mode_t mode = 0755;

			path = image_loader_get_fd(sd->img_loader)->path;
			base = cache_get_location(CACHE_TYPE_SIM, path, FALSE, &mode);
			if (recursive_mkdir_if_not_exists(base, mode))
				{
				g_free(cd->path);
				cd->path = cache_get_location(CACHE_TYPE_SIM, path, TRUE, NULL);
				if (cache_sim_data_save(cd))
					{
					filetime_set(cd->path, filetime(image_loader_get_fd(sd->img_loader)->path));
					}
				}
			g_free(base);
			}
		}

	image_loader_free(sd->img_loader);
	sd->img_loader = NULL;

	sd->search_idle_id = g_idle_add(search_step_cb, sd);
}

static void search_file_load_done_cb(ImageLoader *il, gpointer data)
{
	SearchData *sd = data;
	search_file_load_process(sd, sd->img_cd);
}

static gint search_file_do_extra(SearchData *sd, FileData *fd, gint *match,
				 gint *width, gint *height, gint *simval)
{
	gint new_data = FALSE;
	gint tmatch = TRUE;
	gint tested = FALSE;

	if (!sd->img_cd)
		{
		gchar *cd_path;

		new_data = TRUE;

		cd_path = cache_find_location(CACHE_TYPE_SIM, fd->path);
		if (cd_path && filetime(fd->path) == filetime(cd_path))
			{
			sd->img_cd = cache_sim_data_load(cd_path);
			}
		g_free(cd_path);
		}

	if (!sd->img_cd)
		{
		sd->img_cd = cache_sim_data_new();
		}

	if (new_data)
		{
		if ((sd->match_dimensions_enable && !sd->img_cd->dimensions) ||
		    (sd->match_similarity_enable && !sd->img_cd->similarity))
			{
			sd->img_loader = image_loader_new(fd);
			g_signal_connect(G_OBJECT(sd->img_loader), "error", (GCallback)search_file_load_done_cb, sd);
			g_signal_connect(G_OBJECT(sd->img_loader), "done", (GCallback)search_file_load_done_cb, sd);
			if (image_loader_start(sd->img_loader))
				{
				return TRUE;
				}
			else
				{
				image_loader_free(sd->img_loader);
				sd->img_loader = NULL;
				}
			}
		}

	if (tmatch && sd->match_dimensions_enable && sd->img_cd->dimensions)
		{
		CacheData *cd = sd->img_cd;

		tmatch = FALSE;
		tested = TRUE;

		if (sd->match_dimensions == SEARCH_MATCH_EQUAL)
			{
			tmatch = (cd->width == sd->search_width && cd->height == sd->search_height);
			}
		else if (sd->match_dimensions == SEARCH_MATCH_UNDER)
			{
			tmatch = (cd->width < sd->search_width && cd->height < sd->search_height);
			}
		else if (sd->match_dimensions == SEARCH_MATCH_OVER)
			{
			tmatch = (cd->width > sd->search_width && cd->height > sd->search_height);
			}
		else if (sd->match_dimensions == SEARCH_MATCH_BETWEEN)
			{
			tmatch = (MATCH_IS_BETWEEN(cd->width, sd->search_width, sd->search_width_end) &&
				  MATCH_IS_BETWEEN(cd->height, sd->search_height, sd->search_height_end));
			}
		}

	if (tmatch && sd->match_similarity_enable && sd->img_cd->similarity)
		{
		gdouble value = 0.0;

		tmatch = FALSE;
		tested = TRUE;

		/* fixme: implement similarity checking */
		if (sd->search_similarity_cd && sd->search_similarity_cd->similarity)
			{
			gdouble result;

			result = image_sim_compare_fast(sd->search_similarity_cd->sim, sd->img_cd->sim,
							(gdouble)sd->search_similarity / 100.0);
			result *= 100.0;
			if (result >= (gdouble)sd->search_similarity)
				{
				tmatch = TRUE;
				value = (gint)result;
				}
			}

		if (simval) *simval = value;
		}

	if (sd->img_cd->dimensions)
		{
		if (width) *width = sd->img_cd->width;
		if (height) *height = sd->img_cd->height;
		}

	cache_sim_data_free(sd->img_cd);
	sd->img_cd = NULL;

	*match = (tmatch && tested);

	return FALSE;
}

static gint search_file_next(SearchData *sd)
{
	FileData *fd;
	gint match = TRUE;
	gint tested = FALSE;
	gint extra_only = FALSE;
	gint width = 0;
	gint height = 0;
	gint sim = 0;

	if (!sd->search_file_list) return FALSE;

	if (sd->img_cd)
		{
		/* on end of a CacheData load, skip recomparing non-extra match types */
		extra_only = TRUE;
		match = FALSE;
		}
	else
		{
		sd->search_total++;
		}

	fd = sd->search_file_list->data;

	if (match && sd->match_name_enable && sd->search_name)
		{
		tested = TRUE;
		match = FALSE;

		if (sd->match_name == SEARCH_MATCH_EQUAL)
			{
			if (sd->search_name_match_case)
				{
				match = (strcmp(fd->name, sd->search_name) == 0);
				}
			else
				{
				match = (g_ascii_strcasecmp(fd->name, sd->search_name) == 0);
				}
			}
		else if (sd->match_name == SEARCH_MATCH_CONTAINS)
			{
			if (sd->search_name_match_case)
				{
				match = (strstr(fd->name, sd->search_name) != NULL);
				}
			else
				{
				/* sd->search_name is converted in search_start() */
				gchar *haystack = g_utf8_strdown(fd->name, -1);
				match = (strstr(haystack, sd->search_name) != NULL);
				g_free(haystack);
				}
			}
		}

	if (match && sd->match_size_enable)
		{
		tested = TRUE;
		match = FALSE;

		if (sd->match_size == SEARCH_MATCH_EQUAL)
			{
			match = (fd->size == sd->search_size);
			}
		else if (sd->match_size == SEARCH_MATCH_UNDER)
			{
			match = (fd->size < sd->search_size);
			}
		else if (sd->match_size == SEARCH_MATCH_OVER)
			{
			match = (fd->size > sd->search_size);
			}
		else if (sd->match_size == SEARCH_MATCH_BETWEEN)
			{
			match = MATCH_IS_BETWEEN(fd->size, sd->search_size, sd->search_size_end);
			}
		}

	if (match && sd->match_date_enable)
		{
		tested = TRUE;
		match = FALSE;

		if (sd->match_date == SEARCH_MATCH_EQUAL)
			{
			struct tm *lt;

			lt = localtime(&fd->date);
			match = (lt &&
				 lt->tm_year == sd->search_date_y - 1900 &&
				 lt->tm_mon == sd->search_date_m - 1 &&
				 lt->tm_mday == sd->search_date_d);
			}
		else if (sd->match_date == SEARCH_MATCH_UNDER)
			{
			match = (fd->date < convert_dmy_to_time(sd->search_date_d, sd->search_date_m, sd->search_date_y));
			}
		else if (sd->match_date == SEARCH_MATCH_OVER)
			{
			match = (fd->date > convert_dmy_to_time(sd->search_date_d, sd->search_date_m, sd->search_date_y) + 60 * 60 * 24 - 1);
			}
		else if (sd->match_date == SEARCH_MATCH_BETWEEN)
			{
			time_t a = convert_dmy_to_time(sd->search_date_d, sd->search_date_m, sd->search_date_y);
			time_t b = convert_dmy_to_time(sd->search_date_end_d, sd->search_date_end_m, sd->search_date_end_y);

			if (b >= a)
				{
				b += 60 * 60 * 24 - 1;
				}
			else
				{
				a += 60 * 60 * 24 - 1;
				}
			match = MATCH_IS_BETWEEN(fd->date, a, b);
			}
		}

	if (match && sd->match_keywords_enable && sd->search_keyword_list)
		{
		GList *list;

		tested = TRUE;
		match = FALSE;

		list = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);

		if (list)
			{
			GList *needle;
			GList *haystack;

			if (sd->match_keywords == SEARCH_MATCH_ALL)
				{
				gint found = TRUE;

				needle = sd->search_keyword_list;
				while (needle && found)
					{
					found = FALSE;
					haystack = list;
					while (haystack && !found)
						{
						found = (g_ascii_strcasecmp((gchar *)needle->data,
								    (gchar *)haystack->data) == 0);
						haystack = haystack->next;
						}
					needle = needle->next;
					}

				match = found;
				}
			else if (sd->match_keywords == SEARCH_MATCH_ANY)
				{
				gint found = FALSE;

				needle = sd->search_keyword_list;
				while (needle && !found)
					{
					haystack = list;
					while (haystack && !found)
						{
						found = (g_ascii_strcasecmp((gchar *)needle->data,
								    (gchar *)haystack->data) == 0);
						haystack = haystack->next;
						}
					needle = needle->next;
					}

				match = found;
				}
			else if (sd->match_keywords == SEARCH_MATCH_NONE)
				{
				gint found = FALSE;

				needle = sd->search_keyword_list;
				while (needle && !found)
					{
					haystack = list;
					while (haystack && !found)
						{
						found = (g_ascii_strcasecmp((gchar *)needle->data,
								    (gchar *)haystack->data) == 0);
						haystack = haystack->next;
						}
					needle = needle->next;
					}

				match = !found;
				}
			string_list_free(list);
			}
		else
			{
			match = (sd->match_keywords == SEARCH_MATCH_NONE);
			}
		}

	if (match && sd->match_comment_enable && sd->search_comment && strlen(sd->search_comment))
		{
		gchar *comment;

		tested = TRUE;
		match = FALSE;

		comment = metadata_read_string(fd, COMMENT_KEY, METADATA_PLAIN);

		if (comment)
			{
			if (! sd->search_comment_match_case)
				{
				gchar *tmp = g_utf8_strdown(comment, -1);
				g_free(comment);
				comment = tmp;
				}

			if (sd->match_comment == SEARCH_MATCH_CONTAINS)
				{
				match = (strstr(comment, sd->search_comment) != NULL);
				}
			else if (sd->match_comment == SEARCH_MATCH_NONE)
				{
				match = (strstr(comment, sd->search_comment) == NULL);
				}
			g_free(comment);
			}
		else
			{
			match = (sd->match_comment == SEARCH_MATCH_NONE);
			}
		}

	if ((match || extra_only) &&
	    (sd->match_dimensions_enable || sd->match_similarity_enable))
		{
		tested = TRUE;

		if (search_file_do_extra(sd, fd, &match, &width, &height, &sim))
			{
			sd->search_buffer_count += SEARCH_BUFFER_MATCH_LOAD;
			return TRUE;
			}
		}

	sd->search_file_list = g_list_remove(sd->search_file_list, fd);

	if (tested && match)
		{
		MatchFileData *mfd;

		mfd = g_new(MatchFileData, 1);
		mfd->fd = fd;

		mfd->width = width;
		mfd->height = height;
		mfd->rank = sim;

		sd->search_buffer_list = g_list_prepend(sd->search_buffer_list, mfd);
		sd->search_buffer_count += SEARCH_BUFFER_MATCH_HIT;
		sd->search_count++;
		search_progress_update(sd, TRUE, -1.0);
		}
	else
		{
		file_data_unref(fd);
		sd->search_buffer_count += SEARCH_BUFFER_MATCH_MISS;
		}

	return FALSE;
}

static gint search_step_cb(gpointer data)
{
	SearchData *sd = data;
	FileData *fd;

	if (sd->search_buffer_count > SEARCH_BUFFER_FLUSH_SIZE)
		{
		search_buffer_flush(sd);
		search_progress_update(sd, TRUE, -1.0);
		}

	if (sd->search_file_list)
		{
		if (search_file_next(sd))
			{
			sd->search_idle_id = -1;
			return FALSE;
			}
		return TRUE;
		}

	if (!sd->search_file_list && !sd->search_folder_list)
		{
		sd->search_idle_id = -1;

		search_stop(sd);
		search_result_thumb_step(sd);

		return FALSE;
		}

	fd = sd->search_folder_list->data;

	if (g_list_find(sd->search_done_list, fd) == NULL)
		{
		GList *list = NULL;
		GList *dlist = NULL;
		gint success = FALSE;

		sd->search_done_list = g_list_prepend(sd->search_done_list, fd);

		if (sd->search_type == SEARCH_MATCH_NONE)
			{
			success = filelist_read(fd, &list, &dlist);
			}
		else if (sd->search_type == SEARCH_MATCH_ALL &&
			 sd->search_dir_fd &&
			 strlen(fd->path) >= strlen(sd->search_dir_fd->path))
			{
			const gchar *path;

			path = fd->path + strlen(sd->search_dir_fd->path);
			if (path != fd->path)
				{
				FileData *dir_fd = file_data_new_simple(path);
				success = filelist_read(dir_fd, &list, NULL);
				file_data_unref(dir_fd);
				}
			success |= filelist_read(fd, NULL, &dlist);
			if (success)
				{
				GList *work;

				work = list;
				while (work)
					{
					FileData *fdp;
					GList *link;
					gchar *meta_path;

					fdp = work->data;
					link = work;
					work = work->next;

					meta_path = cache_find_location(CACHE_TYPE_METADATA, fdp->path);
					if (!meta_path)
						{
						list = g_list_delete_link(list, link);
						file_data_unref(fdp);
						}
					g_free(meta_path);
					}
				}
			}

		if (success)
			{
			list = filelist_sort(list, SORT_NAME, TRUE);
			sd->search_file_list = list;

			if (sd->search_path_recurse)
				{
				dlist = filelist_sort(dlist, SORT_NAME, TRUE);
				sd->search_folder_list = g_list_concat(dlist, sd->search_folder_list);
				}
			else
				{
				filelist_free(dlist);
				}
			}
		}
	else
		{
		sd->search_folder_list = g_list_remove(sd->search_folder_list, fd);
		sd->search_done_list = g_list_remove(sd->search_done_list, fd);
		file_data_unref(fd);
		}

	return TRUE;
}

static void search_similarity_load_done_cb(ImageLoader *il, gpointer data)
{
	SearchData *sd = data;
	search_file_load_process(sd, sd->search_similarity_cd);
}

static void search_start(SearchData *sd)
{
	search_stop(sd);
	search_result_clear(sd);

	if (sd->search_dir_fd)
		{
		sd->search_folder_list = g_list_prepend(sd->search_folder_list, file_data_ref(sd->search_dir_fd));
		}

	if (!sd->search_name_match_case)
		{
		/* convert to lowercase here, so that this is only done once per search */
		gchar *tmp = g_utf8_strdown(sd->search_name, -1);
		g_free(sd->search_name);
		sd->search_name = tmp;
		}

	if (!sd->search_comment_match_case)
		{
		/* convert to lowercase here, so that this is only done once per search */
		gchar *tmp = g_utf8_strdown(sd->search_comment, -1);
		g_free(sd->search_comment);
		sd->search_comment = tmp;
		}

	sd->search_count = 0;
	sd->search_total = 0;

	gtk_widget_set_sensitive(sd->box_search, FALSE);
	spinner_set_interval(sd->spinner, SPINNER_SPEED);
	gtk_widget_set_sensitive(sd->button_start, FALSE);
	gtk_widget_set_sensitive(sd->button_stop, TRUE);
	search_progress_update(sd, TRUE, -1.0);

	if (sd->match_similarity_enable &&
	    !sd->search_similarity_cd &&
	    isfile(sd->search_similarity_path))
		{
		gchar *cd_path;

		cd_path = cache_find_location(CACHE_TYPE_SIM, sd->search_similarity_path);
		if (cd_path && filetime(sd->search_similarity_path) == filetime(cd_path))
			{
			sd->search_similarity_cd = cache_sim_data_load(cd_path);
			}
		g_free(cd_path);

		if (!sd->search_similarity_cd || !sd->search_similarity_cd->similarity)
			{
			if (!sd->search_similarity_cd)
				{
				sd->search_similarity_cd = cache_sim_data_new();
				}

			sd->img_loader = image_loader_new(file_data_new_simple(sd->search_similarity_path));
			g_signal_connect(G_OBJECT(sd->img_loader), "error", (GCallback)search_similarity_load_done_cb, sd);
			g_signal_connect(G_OBJECT(sd->img_loader), "done", (GCallback)search_similarity_load_done_cb, sd);
			if (image_loader_start(sd->img_loader))
				{
				return;
				}
			image_loader_free(sd->img_loader);
			sd->img_loader = NULL;
			}

		}

	sd->search_idle_id = g_idle_add(search_step_cb, sd);
}

static void search_start_cb(GtkWidget *widget, gpointer data)
{
	SearchData *sd = data;
	GtkTreeViewColumn *column;
	gchar *path;

	if (sd->search_folder_list)
		{
		search_stop(sd);
		search_result_thumb_step(sd);
		return;
		}

	if (sd->match_name_enable) history_combo_append_history(sd->entry_name, NULL);
	g_free(sd->search_name);
	sd->search_name = g_strdup(gtk_entry_get_text(GTK_ENTRY(sd->entry_name)));

	/* XXX */
	g_free(sd->search_comment);
	sd->search_comment = g_strdup(gtk_entry_get_text(GTK_ENTRY(sd->entry_comment)));

	g_free(sd->search_similarity_path);
	sd->search_similarity_path = g_strdup(gtk_entry_get_text(GTK_ENTRY(sd->entry_similarity)));
	if (sd->match_similarity_enable)
		{
		if (!isfile(sd->search_similarity_path))
			{
			file_util_warning_dialog(_("File not found"),
						 _("Please enter an existing file for image content."),
						 GTK_STOCK_DIALOG_WARNING, sd->window);
			return;
			}
		tab_completion_append_to_history(sd->entry_similarity, sd->search_similarity_path);
		}

	string_list_free(sd->search_keyword_list);
	sd->search_keyword_list = keyword_list_pull(sd->entry_keywords);

	date_selection_get(sd->date_sel, &sd->search_date_d, &sd->search_date_m, &sd->search_date_y);
	date_selection_get(sd->date_sel_end, &sd->search_date_end_d, &sd->search_date_end_m, &sd->search_date_end_y);

	column = gtk_tree_view_get_column(GTK_TREE_VIEW(sd->result_view), SEARCH_COLUMN_DIMENSIONS - 1);
	gtk_tree_view_column_set_visible(column, sd->match_dimensions_enable);

	column = gtk_tree_view_get_column(GTK_TREE_VIEW(sd->result_view), SEARCH_COLUMN_RANK - 1);
	gtk_tree_view_column_set_visible(column, sd->match_similarity_enable);
	if (!sd->match_similarity_enable)
		{
		GtkTreeSortable *sortable;
		gint id;
		GtkSortType order;

		sortable = GTK_TREE_SORTABLE(gtk_tree_view_get_model(GTK_TREE_VIEW(sd->result_view)));
		if (gtk_tree_sortable_get_sort_column_id(sortable, &id, &order) &&
		    id == SEARCH_COLUMN_RANK)
			{
			gtk_tree_sortable_set_sort_column_id(sortable, SEARCH_COLUMN_PATH, GTK_SORT_ASCENDING);
			}
		}

	if (sd->search_type == SEARCH_MATCH_NONE)
		{
		/* search path */

		path = remove_trailing_slash(gtk_entry_get_text(GTK_ENTRY(sd->path_entry)));
		if (isdir(path))
			{
			file_data_unref(sd->search_dir_fd);
			sd->search_dir_fd = file_data_new_simple(path);

			tab_completion_append_to_history(sd->path_entry, sd->search_dir_fd->path);

			search_start(sd);
			}
		else
			{
			file_util_warning_dialog(_("Folder not found"),
						 _("Please enter an existing folder to search."),
						 GTK_STOCK_DIALOG_WARNING, sd->window);
			}

		g_free(path);
		}
	else if (sd->search_type == SEARCH_MATCH_ALL)
		{
		/* search metadata */
		file_data_unref(sd->search_dir_fd);
		sd->search_dir_fd = file_data_new_simple(get_metadata_cache_dir());
		search_start(sd);
		}
	else if (sd->search_type == SEARCH_MATCH_CONTAINS)
		{
		/* search current result list */
		GList *list;

		list = search_result_refine_list(sd);

		file_data_unref(sd->search_dir_fd);
		sd->search_dir_fd = NULL;

		search_start(sd);

		sd->search_file_list = g_list_concat(sd->search_file_list, list);
		}
}

/*
 *-------------------------------------------------------------------
 * window construct
 *-------------------------------------------------------------------
 */

enum {
	MENU_CHOICE_COLUMN_NAME = 0,
	MENU_CHOICE_COLUMN_VALUE
};

static void search_thumb_toggle_cb(GtkWidget *button, gpointer data)
{
	SearchData *sd = data;

	search_result_thumb_enable(sd, gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button)));
}

static gint sort_matchdata_dimensions(MatchFileData *a, MatchFileData *b)
{
	gint sa;
	gint sb;

	sa = a->width * a->height;
	sb = b->width * b->height;

	if (sa > sb) return 1;
	if (sa < sb) return -1;
	return 0;
}

static gint search_result_sort_cb(GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, gpointer data)
{
	gint n = GPOINTER_TO_INT(data);
	MatchFileData *fda;
	MatchFileData *fdb;

	gtk_tree_model_get(model, a, SEARCH_COLUMN_POINTER, &fda, -1);
	gtk_tree_model_get(model, b, SEARCH_COLUMN_POINTER, &fdb, -1);

	if (!fda || !fdb) return 0;

	switch (n)
		{
		case SEARCH_COLUMN_RANK:
			if (((MatchFileData *)fda)->rank > (fdb)->rank) return 1;
			if (((MatchFileData *)fda)->rank < (fdb)->rank) return -1;
			return 0;
			break;
		case SEARCH_COLUMN_NAME:
			if (options->file_sort.case_sensitive)
				return strcmp(fda->fd->collate_key_name, fdb->fd->collate_key_name);
			else
				return strcmp(fda->fd->collate_key_name_nocase, fdb->fd->collate_key_name_nocase);
			break;
		case SEARCH_COLUMN_SIZE:
			if (fda->fd->size > fdb->fd->size) return 1;
			if (fda->fd->size < fdb->fd->size) return -1;
			return 0;
			break;
		case SEARCH_COLUMN_DATE:
			if (fda->fd->date > fdb->fd->date) return 1;
			if (fda->fd->date < fdb->fd->date) return -1;
			return 0;
			break;
		case SEARCH_COLUMN_DIMENSIONS:
			return sort_matchdata_dimensions(fda, fdb);
			break;
		case SEARCH_COLUMN_PATH:
			return utf8_compare(fda->fd->path, fdb->fd->path, options->file_sort.case_sensitive);
			break;
		default:
			break;
		}

	return 0;
}

static void search_result_add_column(SearchData * sd, gint n, const gchar *title, gint image, gint right_justify)
{
	GtkTreeViewColumn *column;
	GtkCellRenderer *renderer;

	column = gtk_tree_view_column_new();
	gtk_tree_view_column_set_title(column, title);
	gtk_tree_view_column_set_min_width(column, 4);

	if (n != SEARCH_COLUMN_THUMB) gtk_tree_view_column_set_resizable(column, TRUE);

	if (!image)
		{
		gtk_tree_view_column_set_sizing(column, GTK_TREE_VIEW_COLUMN_GROW_ONLY);
		renderer = gtk_cell_renderer_text_new();
		if (right_justify) g_object_set(G_OBJECT(renderer), "xalign", 1.0, NULL);
		gtk_tree_view_column_pack_start(column, renderer, TRUE);
		gtk_tree_view_column_add_attribute(column, renderer, "text", n);

		gtk_tree_view_column_set_sort_column_id(column, n);
		}
	else
		{
		gtk_tree_view_column_set_sizing(column, GTK_TREE_VIEW_COLUMN_FIXED);
		renderer = gtk_cell_renderer_pixbuf_new();
		cell_renderer_height_override(renderer);
		gtk_tree_view_column_pack_start(column, renderer, TRUE);
		gtk_tree_view_column_add_attribute(column, renderer, "pixbuf", n);
		}

	gtk_tree_view_append_column(GTK_TREE_VIEW(sd->result_view), column);
}

static void menu_choice_set_visible(GtkWidget *widget, gint visible)
{
	if (visible)
		{
		if (!GTK_WIDGET_VISIBLE(widget)) gtk_widget_show(widget);
		}
	else
		{
		if (GTK_WIDGET_VISIBLE(widget)) gtk_widget_hide(widget);
		}
}

static gboolean menu_choice_get_match_type(GtkWidget *combo, MatchType *type)
{
	GtkTreeModel *store;
	GtkTreeIter iter;

	store = gtk_combo_box_get_model(GTK_COMBO_BOX(combo));
	if (!gtk_combo_box_get_active_iter(GTK_COMBO_BOX(combo), &iter)) return FALSE;
	gtk_tree_model_get(store, &iter, MENU_CHOICE_COLUMN_VALUE, type, -1);
	return TRUE;
}

static void menu_choice_path_cb(GtkWidget *combo, gpointer data)
{
	SearchData *sd = data;

	if (!menu_choice_get_match_type(combo, &sd->search_type)) return;

	menu_choice_set_visible(gtk_widget_get_parent(sd->check_recurse),
				(sd->search_type == SEARCH_MATCH_NONE));
}

static void menu_choice_name_cb(GtkWidget *combo, gpointer data)
{
	SearchData *sd = data;

	if (!menu_choice_get_match_type(combo, &sd->match_name)) return;
}

static void menu_choice_size_cb(GtkWidget *combo, gpointer data)
{
	SearchData *sd = data;

	if (!menu_choice_get_match_type(combo, &sd->match_size)) return;

	menu_choice_set_visible(gtk_widget_get_parent(sd->spin_size_end),
				(sd->match_size == SEARCH_MATCH_BETWEEN));
}

static void menu_choice_date_cb(GtkWidget *combo, gpointer data)
{
	SearchData *sd = data;

	if (!menu_choice_get_match_type(combo, &sd->match_date)) return;

	menu_choice_set_visible(gtk_widget_get_parent(sd->date_sel_end),
				(sd->match_date == SEARCH_MATCH_BETWEEN));
}

static void menu_choice_dimensions_cb(GtkWidget *combo, gpointer data)
{
	SearchData *sd = data;

	if (!menu_choice_get_match_type(combo, &sd->match_dimensions)) return;

	menu_choice_set_visible(gtk_widget_get_parent(sd->spin_width_end),
				(sd->match_dimensions == SEARCH_MATCH_BETWEEN));
}

static void menu_choice_keyword_cb(GtkWidget *combo, gpointer data)
{
	SearchData *sd = data;

	if (!menu_choice_get_match_type(combo, &sd->match_keywords)) return;
}

static void menu_choice_comment_cb(GtkWidget *combo, gpointer data)
{
	SearchData *sd = data;

	if (!menu_choice_get_match_type(combo, &sd->match_comment)) return;
}

static void menu_choice_spin_cb(GtkAdjustment *adjustment, gpointer data)
{
	gint *value = data;

	*value = (gint)gtk_adjustment_get_value(adjustment);
}

static GtkWidget *menu_spin(GtkWidget *box, gdouble min, gdouble max, gint value,
			    GCallback func, gpointer data)
{
	GtkWidget *spin;
	GtkAdjustment *adj;

	spin = gtk_spin_button_new_with_range(min, max, 1);
	gtk_spin_button_set_value(GTK_SPIN_BUTTON(spin), (gdouble)value);
	adj = gtk_spin_button_get_adjustment(GTK_SPIN_BUTTON(spin));
	if (func) g_signal_connect(G_OBJECT(adj), "value_changed",
				   G_CALLBACK(func), data);
	gtk_box_pack_start(GTK_BOX(box), spin, FALSE, FALSE, 0);
	gtk_widget_show(spin);

	return spin;
}

static void menu_choice_check_cb(GtkWidget *button, gpointer data)
{
	GtkWidget *widget = data;
	gboolean active;
	gboolean *value;

	active = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button));
	gtk_widget_set_sensitive(widget, active);

	value = g_object_get_data(G_OBJECT(button), "check_var");
	if (value) *value = active;
}

static GtkWidget *menu_choice_menu(const MatchList *items, gint item_count,
				   GCallback func, gpointer data)
{
	GtkWidget *combo;
	GtkCellRenderer *renderer;
	GtkListStore *store;
	gint i;

	store = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_INT);
	combo = gtk_combo_box_new_with_model(GTK_TREE_MODEL(store));
	g_object_unref(store);

	renderer = gtk_cell_renderer_text_new();
	gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(combo), renderer, TRUE);
	gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(combo), renderer,
				       "text", MENU_CHOICE_COLUMN_NAME, NULL);

	for (i = 0; i < item_count; i++)
		{
		GtkTreeIter iter;

		gtk_list_store_append(store, &iter);
		gtk_list_store_set(store, &iter, MENU_CHOICE_COLUMN_NAME, _(items[i].text),
						 MENU_CHOICE_COLUMN_VALUE, items[i].type, -1);
		}

	gtk_combo_box_set_active(GTK_COMBO_BOX(combo), 0);

	if (func) g_signal_connect(G_OBJECT(combo), "changed",
				   G_CALLBACK(func), data);

	return combo;
}

static GtkWidget *menu_choice(GtkWidget *box, GtkWidget **check, GtkWidget **menu,
			      const gchar *text, gboolean *value,
			      const MatchList *items, gint item_count,
			      GCallback func, gpointer data)
{
	GtkWidget *base_box;
	GtkWidget *hbox;
	GtkWidget *button;
	GtkWidget *option;

	base_box = gtk_hbox_new(FALSE, PREF_PAD_GAP);
	gtk_box_pack_start(GTK_BOX(box), base_box, FALSE, FALSE, 0);
	gtk_widget_show(base_box);

	button = gtk_check_button_new();
	if (value) gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(button), *value);
	gtk_box_pack_start(GTK_BOX(base_box), button, FALSE, FALSE, 0);
	gtk_widget_show(button);
	if (check) *check = button;
	if (value) g_object_set_data(G_OBJECT(button), "check_var", value);

	hbox = gtk_hbox_new(FALSE, PREF_PAD_SPACE);
	gtk_box_pack_start(GTK_BOX(base_box), hbox, TRUE, TRUE, 0);
	gtk_widget_show(hbox);

	g_signal_connect(G_OBJECT(button), "toggled",
			 G_CALLBACK(menu_choice_check_cb), hbox);
	gtk_widget_set_sensitive(hbox, (value) ? *value : FALSE);

	pref_label_new(hbox, text);

	if (!items && !menu) return hbox;

	option = menu_choice_menu(items, item_count, func, data);
	gtk_box_pack_start(GTK_BOX(hbox), option, FALSE, FALSE, 0);
	gtk_widget_show(option);
	if (menu) *menu = option;

	return hbox;
}

static void search_window_close(SearchData *sd)
{
	gtk_widget_destroy(sd->window);
}

static gint search_window_delete_cb(GtkWidget *widget, GdkEventAny *event, gpointer data)
{
	SearchData *sd = data;

	search_window_close(sd);
	return TRUE;
}

static void search_window_destroy_cb(GtkWidget *widget, gpointer data)
{
	SearchData *sd = data;

	search_window_list = g_list_remove(search_window_list, sd);

	search_result_update_idle_cancel(sd);

	filelist_free(sd->search_buffer_list);
	sd->search_buffer_list = NULL;

	search_stop(sd);
	search_result_clear(sd);

	file_data_unref(sd->search_dir_fd);

	g_free(sd->search_name);
	g_free(sd->search_comment);
	g_free(sd->search_similarity_path);
	string_list_free(sd->search_keyword_list);

	file_data_unregister_notify_func(search_notify_cb, sd);

	g_free(sd);
}

void search_new(FileData *dir_fd, FileData *example_file)
{
	SearchData *sd;
	GtkWidget *vbox;
	GtkWidget *hbox;
	GtkWidget *hbox2;
	GtkWidget *pad_box;
	GtkWidget *frame;
	GtkWidget *scrolled;
	GtkListStore *store;
	GtkTreeSortable *sortable;
	GtkTreeSelection *selection;
	GtkWidget *combo;
	GdkGeometry geometry;

	sd = g_new0(SearchData, 1);

	sd->search_dir_fd = file_data_ref(dir_fd);
	sd->search_path_recurse = TRUE;
	sd->search_size = 0;
	sd->search_width = 640;
	sd->search_height = 480;
	sd->search_width_end = 1024;
	sd->search_height_end = 768;

	sd->search_type = SEARCH_MATCH_NONE;

	sd->match_name = SEARCH_MATCH_CONTAINS;
	sd->match_size = SEARCH_MATCH_EQUAL;
	sd->match_date = SEARCH_MATCH_EQUAL;
	sd->match_dimensions = SEARCH_MATCH_EQUAL;
	sd->match_keywords = SEARCH_MATCH_ALL;
	sd->match_comment = SEARCH_MATCH_CONTAINS;

	sd->match_name_enable = TRUE;

	sd->search_similarity = 95;
	
	if (example_file)
		{
		sd->search_similarity_path = g_strdup(example_file->path);
		}

	sd->search_idle_id = -1;
	sd->update_idle_id = -1;

	sd->window = window_new(GTK_WINDOW_TOPLEVEL, "search", NULL, NULL, _("Image search"));

	gtk_window_set_resizable(GTK_WINDOW(sd->window), TRUE);

	geometry.min_width = DEFAULT_MINIMAL_WINDOW_SIZE;
	geometry.min_height = DEFAULT_MINIMAL_WINDOW_SIZE;
	geometry.base_width = DEF_SEARCH_WIDTH;
	geometry.base_height = DEF_SEARCH_HEIGHT;
	gtk_window_set_geometry_hints(GTK_WINDOW(sd->window), NULL, &geometry,
				      GDK_HINT_MIN_SIZE | GDK_HINT_BASE_SIZE);

	gtk_window_set_default_size(GTK_WINDOW(sd->window), DEF_SEARCH_WIDTH, DEF_SEARCH_HEIGHT);

	g_signal_connect(G_OBJECT(sd->window), "delete_event",
			 G_CALLBACK(search_window_delete_cb), sd);
	g_signal_connect(G_OBJECT(sd->window), "destroy",
			 G_CALLBACK(search_window_destroy_cb), sd);

	g_signal_connect(G_OBJECT(sd->window), "key_press_event",
			 G_CALLBACK(search_window_keypress_cb), sd);

	vbox = gtk_vbox_new(FALSE, PREF_PAD_GAP);
	gtk_container_set_border_width(GTK_CONTAINER(vbox), PREF_PAD_GAP);
	gtk_container_add(GTK_CONTAINER(sd->window), vbox);
	gtk_widget_show(vbox);

	sd->box_search = pref_box_new(vbox, FALSE, GTK_ORIENTATION_VERTICAL, PREF_PAD_GAP);

	hbox = pref_box_new(sd->box_search, FALSE, GTK_ORIENTATION_HORIZONTAL, PREF_PAD_SPACE);

	pref_label_new(hbox, _("Search:"));

	sd->menu_path = menu_choice_menu(text_search_menu_path, sizeof(text_search_menu_path) / sizeof(MatchList),
					 G_CALLBACK(menu_choice_path_cb), sd);
	gtk_box_pack_start(GTK_BOX(hbox), sd->menu_path, FALSE, FALSE, 0);
	gtk_widget_show(sd->menu_path);

	hbox2 = pref_box_new(hbox, TRUE, GTK_ORIENTATION_HORIZONTAL, PREF_PAD_SPACE);
	combo = tab_completion_new_with_history(&sd->path_entry, sd->search_dir_fd->path,
						"search_path", -1,
						NULL, NULL);
	tab_completion_add_select_button(sd->path_entry, NULL, TRUE);
	gtk_box_pack_start(GTK_BOX(hbox2), combo, TRUE, TRUE, 0);
	gtk_widget_show(combo);
	sd->check_recurse = pref_checkbox_new_int(hbox2, _("Recurse"),
						  sd->search_path_recurse, &sd->search_path_recurse);

	/* Search for file name */
	hbox = menu_choice(sd->box_search, &sd->check_name, &sd->menu_name,
			   _("File name"), &sd->match_name_enable,
			   text_search_menu_name, sizeof(text_search_menu_name) / sizeof(MatchList),
			   G_CALLBACK(menu_choice_name_cb), sd);
	combo = history_combo_new(&sd->entry_name, "", "search_name", -1);
	gtk_box_pack_start(GTK_BOX(hbox), combo, TRUE, TRUE, 0);
	gtk_widget_show(combo);
	pref_checkbox_new_int(hbox, _("Match case"),
			      sd->search_name_match_case, &sd->search_name_match_case);

	/* Search for file size */
	hbox = menu_choice(sd->box_search, &sd->check_size, &sd->menu_size,
			   _("File size is"), &sd->match_size_enable,
			   text_search_menu_size, sizeof(text_search_menu_size) / sizeof(MatchList),
			   G_CALLBACK(menu_choice_size_cb), sd);
	sd->spin_size = menu_spin(hbox, 0, 1024*1024*1024, sd->search_size,
				  G_CALLBACK(menu_choice_spin_cb), &sd->search_size);
	hbox2 = gtk_hbox_new(FALSE, PREF_PAD_SPACE);
	gtk_box_pack_start(GTK_BOX(hbox), hbox2, FALSE, FALSE, 0);
	pref_label_new(hbox2, _("and"));
	sd->spin_size_end = menu_spin(hbox2, 0, 1024*1024*1024, sd->search_size_end,
				      G_CALLBACK(menu_choice_spin_cb), &sd->search_size_end);

	/* Search for file date */
	hbox = menu_choice(sd->box_search, &sd->check_date, &sd->menu_date,
			   _("File date is"), &sd->match_date_enable,
			   text_search_menu_date, sizeof(text_search_menu_date) / sizeof(MatchList),
			   G_CALLBACK(menu_choice_date_cb), sd);
	sd->date_sel = date_selection_new();
	date_selection_time_set(sd->date_sel, time(NULL));
	gtk_box_pack_start(GTK_BOX(hbox), sd->date_sel, FALSE, FALSE, 0);
	gtk_widget_show(sd->date_sel);

	hbox2 = gtk_hbox_new(FALSE, PREF_PAD_SPACE);
	gtk_box_pack_start(GTK_BOX(hbox), hbox2, FALSE, FALSE, 0);
	pref_label_new(hbox2, _("and"));
	sd->date_sel_end = date_selection_new();
	date_selection_time_set(sd->date_sel_end, time(NULL));
	gtk_box_pack_start(GTK_BOX(hbox2), sd->date_sel_end, FALSE, FALSE, 0);
	gtk_widget_show(sd->date_sel_end);

	/* Search for image dimensions */
	hbox = menu_choice(sd->box_search, &sd->check_dimensions, &sd->menu_dimensions,
			   _("Image dimensions are"), &sd->match_dimensions_enable,
			   text_search_menu_size, sizeof(text_search_menu_size) / sizeof(MatchList),
			   G_CALLBACK(menu_choice_dimensions_cb), sd);
	pad_box = pref_box_new(hbox, FALSE, GTK_ORIENTATION_HORIZONTAL, 2);
	sd->spin_width = menu_spin(pad_box, 0, 1000000, sd->search_width,
				   G_CALLBACK(menu_choice_spin_cb), &sd->search_width);
	pref_label_new(pad_box, "x");
	sd->spin_height = menu_spin(pad_box, 0, 1000000, sd->search_height,
				    G_CALLBACK(menu_choice_spin_cb), &sd->search_height);
	hbox2 = gtk_hbox_new(FALSE, 2);
	gtk_box_pack_start(GTK_BOX(hbox), hbox2, FALSE, FALSE, 0);
	pref_label_new(hbox2, _("and"));
	pref_spacer(hbox2, PREF_PAD_SPACE - 2*2);
	sd->spin_width_end = menu_spin(hbox2, 0, 1000000, sd->search_width_end,
				       G_CALLBACK(menu_choice_spin_cb), &sd->search_width_end);
	pref_label_new(hbox2, "x");
	sd->spin_height_end = menu_spin(hbox2, 0, 1000000, sd->search_height_end,
					G_CALLBACK(menu_choice_spin_cb), &sd->search_height_end);

	/* Search for image similarity */
	hbox = menu_choice(sd->box_search, &sd->check_similarity, NULL,
			   _("Image content is"), &sd->match_similarity_enable,
			   NULL, 0, NULL, sd);
	sd->spin_similarity = menu_spin(hbox, 80, 100, sd->search_similarity,
					G_CALLBACK(menu_choice_spin_cb), &sd->search_similarity);

	/* xgettext:no-c-format */
	pref_label_new(hbox, _("% similar to"));

	combo = tab_completion_new_with_history(&sd->entry_similarity,
						(sd->search_similarity_path) ? sd->search_similarity_path : "",
						"search_similarity_path", -1, NULL, NULL);
	tab_completion_add_select_button(sd->entry_similarity, NULL, FALSE);
	gtk_box_pack_start(GTK_BOX(hbox), combo, TRUE, TRUE, 0);
	gtk_widget_show(combo);

	/* Search for image keywords */
	hbox = menu_choice(sd->box_search, &sd->check_keywords, &sd->menu_keywords,
			   _("Keywords"), &sd->match_keywords_enable,
			   text_search_menu_keyword, sizeof(text_search_menu_keyword) / sizeof(MatchList),
			   G_CALLBACK(menu_choice_keyword_cb), sd);
	sd->entry_keywords = gtk_entry_new();
	gtk_box_pack_start(GTK_BOX(hbox), sd->entry_keywords, TRUE, TRUE, 0);
	gtk_widget_set_sensitive(sd->entry_keywords, sd->match_keywords_enable);
	g_signal_connect(G_OBJECT(sd->check_keywords), "toggled",
			 G_CALLBACK(menu_choice_check_cb), sd->entry_keywords);
	gtk_widget_show(sd->entry_keywords);

	/* Search for image comment */
	hbox = menu_choice(sd->box_search, &sd->check_comment, &sd->menu_comment,
			_("Comment"), &sd->match_comment_enable,
			text_search_menu_comment, sizeof(text_search_menu_comment) / sizeof(MatchList),
			G_CALLBACK(menu_choice_comment_cb), sd);
	sd->entry_comment = gtk_entry_new();
	gtk_box_pack_start(GTK_BOX(hbox), sd->entry_comment, TRUE, TRUE, 0);
	gtk_widget_set_sensitive(sd->entry_comment, sd->match_comment_enable);
	g_signal_connect(G_OBJECT(sd->check_comment), "toggled",
			G_CALLBACK(menu_choice_check_cb), sd->entry_comment);
	gtk_widget_show(sd->entry_comment);
	pref_checkbox_new_int(hbox, _("Match case"),
			      sd->search_comment_match_case, &sd->search_comment_match_case);

	/* Done the types of searches */

	scrolled = gtk_scrolled_window_new(NULL, NULL);
	gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled), GTK_SHADOW_IN);
	gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled),
				       GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
	gtk_box_pack_start(GTK_BOX(vbox), scrolled, TRUE, TRUE, 0);
	gtk_widget_show(scrolled);

	store = gtk_list_store_new(8, G_TYPE_POINTER, G_TYPE_INT, GDK_TYPE_PIXBUF,
				   G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING,
				   G_TYPE_STRING, G_TYPE_STRING);

	/* set up sorting */
	sortable = GTK_TREE_SORTABLE(store);
	gtk_tree_sortable_set_sort_func(sortable, SEARCH_COLUMN_RANK, search_result_sort_cb,
				  GINT_TO_POINTER(SEARCH_COLUMN_RANK), NULL);
	gtk_tree_sortable_set_sort_func(sortable, SEARCH_COLUMN_NAME, search_result_sort_cb,
				  GINT_TO_POINTER(SEARCH_COLUMN_NAME), NULL);
	gtk_tree_sortable_set_sort_func(sortable, SEARCH_COLUMN_SIZE, search_result_sort_cb,
				  GINT_TO_POINTER(SEARCH_COLUMN_SIZE), NULL);
	gtk_tree_sortable_set_sort_func(sortable, SEARCH_COLUMN_DATE, search_result_sort_cb,
				  GINT_TO_POINTER(SEARCH_COLUMN_DATE), NULL);
	gtk_tree_sortable_set_sort_func(sortable, SEARCH_COLUMN_DIMENSIONS, search_result_sort_cb,
				  GINT_TO_POINTER(SEARCH_COLUMN_DIMENSIONS), NULL);
	gtk_tree_sortable_set_sort_func(sortable, SEARCH_COLUMN_PATH, search_result_sort_cb,
				  GINT_TO_POINTER(SEARCH_COLUMN_PATH), NULL);

#if 0
	/* by default, search results are unsorted until user selects a sort column - for speed,
	 * using sort slows search speed by an order of magnitude with 1000's of results :-/
	 */
	gtk_tree_sortable_set_sort_column_id(sortable, SEARCH_COLUMN_PATH, GTK_SORT_ASCENDING);
#endif

	sd->result_view = gtk_tree_view_new_with_model(GTK_TREE_MODEL(store));
	g_object_unref(store);
	gtk_container_add(GTK_CONTAINER(scrolled), sd->result_view);
	gtk_widget_show(sd->result_view);

	selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(sd->result_view));
	gtk_tree_selection_set_mode(GTK_TREE_SELECTION(selection), GTK_SELECTION_MULTIPLE);
	gtk_tree_selection_set_select_function(selection, search_result_select_cb, sd, NULL);

	gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(sd->result_view), TRUE);
	gtk_tree_view_set_enable_search(GTK_TREE_VIEW(sd->result_view), FALSE);

#if 0
	gtk_tree_view_set_search_column(GTK_TREE_VIEW(sd->result_view), SEARCH_COLUMN_NAME);
#endif

	search_result_add_column(sd, SEARCH_COLUMN_RANK, _("Rank"), FALSE, FALSE);
	search_result_add_column(sd, SEARCH_COLUMN_THUMB, "", TRUE, FALSE);
	search_result_add_column(sd, SEARCH_COLUMN_NAME, _("Name"), FALSE, FALSE);
	search_result_add_column(sd, SEARCH_COLUMN_SIZE, _("Size"), FALSE, TRUE);
	search_result_add_column(sd, SEARCH_COLUMN_DATE, _("Date"), FALSE, TRUE);
	search_result_add_column(sd, SEARCH_COLUMN_DIMENSIONS, _("Dimensions"), FALSE, FALSE);
	search_result_add_column(sd, SEARCH_COLUMN_PATH, _("Path"), FALSE, FALSE);

	search_dnd_init(sd);

	g_signal_connect(G_OBJECT(sd->result_view), "button_press_event",
			 G_CALLBACK(search_result_press_cb), sd);
	g_signal_connect(G_OBJECT(sd->result_view), "button_release_event",
			 G_CALLBACK(search_result_release_cb), sd);
	g_signal_connect(G_OBJECT(sd->result_view), "key_press_event",
			 G_CALLBACK(search_result_keypress_cb), sd);

	hbox = pref_box_new(vbox, FALSE, GTK_ORIENTATION_HORIZONTAL, 0);

	sd->button_thumbs = pref_checkbox_new(hbox, _("Thumbnails"), FALSE,
					      G_CALLBACK(search_thumb_toggle_cb), sd);

	frame = gtk_frame_new(NULL);
	gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_IN);
	gtk_box_pack_start(GTK_BOX(hbox), frame, TRUE, TRUE, PREF_PAD_SPACE);
	gtk_widget_show(frame);

	sd->label_status = gtk_label_new("");
	gtk_widget_set_size_request(sd->label_status, 50, -1);
	gtk_container_add(GTK_CONTAINER(frame), sd->label_status);
	gtk_widget_show(sd->label_status);

	sd->label_progress = gtk_progress_bar_new();
	gtk_widget_set_size_request(sd->label_progress, 50, -1);
	gtk_box_pack_start(GTK_BOX(hbox), sd->label_progress, TRUE, TRUE, 0);
	gtk_widget_show(sd->label_progress);

	sd->spinner = spinner_new(NULL, -1);
	gtk_box_pack_start(GTK_BOX(hbox), sd->spinner, FALSE, FALSE, 0);
	gtk_widget_show(sd->spinner);

	sd->button_start = pref_button_new(hbox, GTK_STOCK_FIND, NULL, FALSE,
					   G_CALLBACK(search_start_cb), sd);
	pref_spacer(hbox, PREF_PAD_BUTTON_GAP);
	sd->button_stop = pref_button_new(hbox, GTK_STOCK_STOP, NULL, FALSE,
					  G_CALLBACK(search_start_cb), sd);
	gtk_widget_set_sensitive(sd->button_stop, FALSE);

	search_status_update(sd);
	search_progress_update(sd, FALSE, -1.0);

	search_window_list = g_list_append(search_window_list, sd);

	file_data_register_notify_func(search_notify_cb, sd, NOTIFY_PRIORITY_MEDIUM);

	gtk_widget_show(sd->window);
}

/*
 *-------------------------------------------------------------------
 * maintenance (move, delete, etc.)
 *-------------------------------------------------------------------
 */

static void search_result_change_path(SearchData *sd, FileData *fd)
{
	GtkTreeModel *store;
	GtkTreeIter iter;
	gint valid;

	store = gtk_tree_view_get_model(GTK_TREE_VIEW(sd->result_view));
	valid = gtk_tree_model_get_iter_first(store, &iter);
	while (valid)
		{
		GtkTreeIter current;
		MatchFileData *mfd;

		current = iter;
		valid = gtk_tree_model_iter_next(store, &iter);

		gtk_tree_model_get(store, &current, SEARCH_COLUMN_POINTER, &mfd, -1);
		if (mfd->fd == fd)
			{
			if (fd->change && fd->change->dest)
				{
				gtk_list_store_set(GTK_LIST_STORE(store), &current,
						   SEARCH_COLUMN_NAME, mfd->fd->name,
						   SEARCH_COLUMN_PATH, mfd->fd->path, -1);
				}
			else
				{
				search_result_remove_item(sd, mfd, &current);
				}
			}
		}
}

static void search_notify_cb(FileData *fd, NotifyType type, gpointer data)
{
	SearchData *sd = data;

	if (type != NOTIFY_TYPE_CHANGE || !fd->change) return;
	
	switch (fd->change->type)
		{
		case FILEDATA_CHANGE_MOVE:
		case FILEDATA_CHANGE_RENAME:
		case FILEDATA_CHANGE_DELETE:
			search_result_change_path(sd, fd);
			break;
		case FILEDATA_CHANGE_COPY:
		case FILEDATA_CHANGE_UNSPECIFIED:
		case FILEDATA_CHANGE_WRITE_METADATA:
			break;
		}
}
/* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */