/* $Id: e2p_thumbs.c 854 2008-04-19 11:45:33Z tpgww $

Copyright (C) 2007-2008 tooar <tooar@gmx.net>

This file is part of emelFM2.
emelFM2 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 3, or (at your option)
any later version.

emelFM2 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 emelFM2; see the file GPL. If not, contact the Free Software
Foundation, 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
/**
@file plugins/optional/e2p_thumbs.c
@brief image-viewer plugin
*/

/*TODO
selection transfers TO iconview? NO NEED
a hook function to trigger auto refreshing without setting up an infinite loop of refreshing icons and filelist
(for now, we just poll directory mtime)
local sort funcs and icons-liststore rationalisation
bias initial rendering order to visible rectangle ?
bias rerendering order to visible rectangle NO NEED
warn user about plugin-load failure ?
confirm efficient cleanups (how to look into pixbufs?)
efficient pixbuf saving ok?
DnD ?
*/

#include "emelfm2.h"
#include <string.h>
#include <libgimpthumb/gimpthumb.h>
#include "e2_context_menu.h"
#include "e2_filelist.h"
#include "e2_task.h"
#include "e2_filetype.h"
#include "e2_plugins.h"
#include "e2_dialog.h"

#define E2_SOFTWARE_ID PROGNAME" file manager"
#define E2_SCALEUP_SIZE 32

/*dialog data listore columns enumerator ...
 so that the same sort functions can be used for this store and the filelist
 store, we need to provide columns up to FINFO in this store, but provide
 different or non-uses for many of them */
enum
{
//	FILENAME = 0 ... FINFO are enumerated elswhere
	PIXBUF = 1, THUMBNAIL, THUMBCOLCOUNT = FINFO+1
};

typedef struct _E2_ThumbDialogRuntime
{
	GtkWidget *dialog;		//the displayed dialog
	GtkWidget *iconview;	//images iconview
	GtkListStore *store;	//images data store
	GtkWidget *sortbtn;		//action-area sort-button
//	GtkWidget *hiddenbtn;	//action-area hide-button
//	gboolean show_hidden;	//TRUE to show hidden items in the iconview
	ViewInfo *view;			//data for displayed file pane
	gboolean clamp;			//TRUE to clamp image display into range 32..128 px
	gboolean replicate;		//TRUE to cause iconview selections to be replicated in related treeview
	gint sort_type;			//to determine the correct sort func, corresponds to column in original store
	GtkSortType sort_order;
	gboolean blocked;		//flag to prevent recursive refreshed
	guint refreshtimer_id;
	time_t dir_mtime;
#ifdef E2_VFSTMP
	//FIXME path when dir not mounted local
#else
	gchar *path;			//path of dir currently displayed, same format as view->dir
#endif
	GSList *oldstores;
} E2_ThumbDialogRuntime;

//aname must be confined to this module
static gchar *aname;
//static gboolean show_hidden = FALSE;	//session-static value to use in dialogs
//static assuming last-closed window sets size for next one in this session only
static gint window_width = -1;
static gint window_height = -1;

#ifdef E2_TRANSIENTKEYS
#include "e2_keybinding.h"
static void _e2p_thumbs_keybindings (E2_OptionSet *set);
#endif
static void _e2p_thumbs_selection_change_cb (GtkIconView *iconview,
	E2_ThumbDialogRuntime *rt);
//static gboolean _e2p_thumbs_refresh_hook (ViewInfo *view,
//	E2_ThumbDialogRuntime *rt);

  /*********************/
 /***** utilities *****/
/*********************/

/**
@brief create empty liststore to hold iconview data

@return the new store
*/
static GtkListStore *_e2p_thumbs_make_store (void)
{
 /* Most columns are for support data and are not displayed.
	FILENAME, NAMEKEY and FINFO are in the same positions as for the
	filelist stores, to allow the same sort functions to be used */
	GtkListStore *store = gtk_list_store_new (THUMBCOLCOUNT,
		G_TYPE_STRING,  //FILENAME displayed
		GDK_TYPE_PIXBUF, //PIXBUF displayed in place of SIZE
		GIMP_TYPE_THUMBNAIL, //THUMBNAIL in place of PERM
		G_TYPE_POINTER,  //OWNER unused
		G_TYPE_POINTER,  //GROUP unused
		G_TYPE_POINTER,  //MODIFIED unused
		G_TYPE_POINTER,  //ACCESSED unused
		G_TYPE_POINTER,  //CHANGED unused
		G_TYPE_STRING,  //NAMEKEY for i18n name sorts
		G_TYPE_POINTER  //FINFO pr to FileInfo for the item
		);
	return store;
}
/**
@brief empty and destroy list store

@param store pointer to the liststore to be killed

@return
*/
static void _e2p_thumbs_clear_store (GtkListStore *store)
{
	printd (DEBUG, "_e2p_thumbs_clear_store");
	GtkTreeModel *mdl = GTK_TREE_MODEL (store);	//FIXME BAD model warning
	GtkTreeIter iter;
	if (gtk_tree_model_get_iter_first (mdl, &iter))
	{	//it's not empty already
		//clear data in the store
		//CHECKME need to clear anything else?
		FileInfo *info;
		GdkPixbuf *pxb;
		GimpThumbnail *thm;
		do
		{
			gtk_tree_model_get (mdl, &iter, FINFO, &info, PIXBUF, &pxb, THUMBNAIL, &thm, -1);
			DEALLOCATE (FileInfo, info);
//			if (pxb != NULL)
				g_object_unref (G_OBJECT (pxb));
//			if (thm != NULL)
			//CHECKME cache the pixbufs here instead of when filling the store
				g_object_unref (G_OBJECT (thm));
		} while (gtk_tree_model_iter_next (mdl, &iter));
		gtk_list_store_clear (store);	//NEEDED ??
	}
	g_object_unref (G_OBJECT (store));
}
/**
@brief idle function to clear and eliminate old liststores

@param oldstores list of superseded liststores to be cleaned

@return FALSE, to stop the callbacks
*/
static gboolean _e2p_thumbs_clear_old_stores (GSList *oldstores)
{
#ifdef DEBUG_MESSAGES
	gint debug = g_slist_length (oldstores);
#endif
	GSList *member;
	for (member = oldstores; member != NULL; member = member->next)
	{
		_e2p_thumbs_clear_store ((GtkListStore *)member->data);
	}
	g_slist_free (oldstores);
	printd (DEBUG, "%d old images-liststore(s) cleared", debug);
	return FALSE;
}
/**
@brief create, populate and apply an iconview-compatible liststore
Rows are added for each relevant non-dir item in the corresponding filelist's
liststore.
The existing liststore is replaced, and queued for cleanup.
BGL is expected to be open
@param rt pointer to data struct for dialog

@return pointer to the liststore, or NULL if there's a problem
*/
static GtkListStore *_e2p_thumbs_replace_store (E2_ThumbDialogRuntime *rt)
{
	//FIXME some access here causes inotify to notice that refresh is needed
	rt->blocked = TRUE;	//prevent re-entrance
//	printd (DEBUG, "start store fill");
	GtkListStore *store = _e2p_thumbs_make_store ();
	if (store == NULL)
	{
		rt->blocked = FALSE;
		return NULL;
	}

	E2_ListChoice pane = (rt->view == &app.pane1_view) ? PANE1 : PANE2;
	e2_filelist_disable_one_refresh (pane);
//	gboolean ref = rt->view->refresh_requested;

	//transfer data from filelist store to iconview store
	GtkTreeIter itert;
//	GtkTreeModel *mdlt = GTK_TREE_MODEL (rt->view->store);	//the child model (everything)
	GtkTreeModel *mdlt = rt->view->model;	//the filter-model

	if (gtk_tree_model_get_iter_first (mdlt, &itert))
	{
		FileInfo *info;
		GdkPixbuf *pxb;
		GimpThumbnail* thumbnail;
		GError *error;
		gboolean makepxb;
		gchar *filename, *namekey, *dlocal, *localpath;
		GtkTreeIter iteri;

		gdk_threads_enter ();
		e2_dialog_set_cursor (rt->dialog, GDK_WATCH);
		gdk_threads_leave ();

		//make new model unsorted to speed up addition
		GtkTreeSortable *sortablei = GTK_TREE_SORTABLE (store);
		gtk_tree_sortable_set_sort_column_id (sortablei,
			GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID, GTK_SORT_ASCENDING);

		do
		{
			gtk_tree_model_get (mdlt, &itert,
				FILENAME, &filename,
				NAMEKEY, &namekey,
				FINFO, &info,	//FIXME handle refreshing which changes the address of info's
				-1);
			//do a bit of filtering to reduce irrelevant pixbuf attempts ...
			switch (info->statbuf.st_mode & S_IFMT)
			{
				case S_IFLNK:
/*			//no special treatment needed for links
#ifdef E2_VFSTMP
		//FIXME vfs
#else
					dlocal = F_FILENAME_TO_LOCALE (rt->view->dir);
#endif
					localpath = e2_utils_strcat (dlocal, info->filename);
					len = strlen (localpath) - 1;
					if (localpath[len] == G_DIR_SEPARATOR)
						localpath[len] = '\0';
			FIXME use e2_fs_walk_link ();
					len = e2_fs_readlink (localpath, target, sizeof (target));
					if (len <= 0)
					{
						F_FREE (dlocal);
						g_free (localpath);
						break;
					}
					target[len] = '\0';
					//check the real target, looking thru chained links
					if (e2_fs_stat (target, &statbuf2 E2_ERR_NONE())	//stat failed
						|| !S_ISREG(statbuf2.st_mode))
					{
						F_FREE (dlocal);
						g_free (localpath);
						break;
					}
					makepxb = TRUE;
					break;
*/
				case S_IFREG:
#ifdef E2_VFSTMP
		//FIXME vfs
#else
					dlocal = F_FILENAME_TO_LOCALE (rt->view->dir);
#endif
					localpath = e2_utils_strcat (dlocal, info->filename);
					makepxb = TRUE;
					break;
				default:
					makepxb = FALSE;
					break;
			}

			if (makepxb)
			{
				thumbnail = gimp_thumbnail_new ();
				error = NULL;
				if (!gimp_thumbnail_set_filename (thumbnail, localpath, &error))
				{
					if (error != NULL)
					{
						//FIXME warn the user
						g_error_free (error);
					}
					makepxb = FALSE;
				}
				if (makepxb)
				{
					gint width, height;
					if (gdk_pixbuf_get_file_info (localpath, &width, &height) != NULL)
					{
						gboolean scaled;
						gint scaleto;
recreate:
						if (rt->clamp)
						{
							if (width < E2_SCALEUP_SIZE && height < E2_SCALEUP_SIZE)
							{
								scaled = TRUE;
								scaleto = E2_SCALEUP_SIZE;
							}
							else if (width > GIMP_THUMB_SIZE_NORMAL || height > GIMP_THUMB_SIZE_NORMAL)
							{
								scaled = TRUE;
								scaleto = GIMP_THUMB_SIZE_NORMAL;
							}
							else
							{
								scaled = FALSE;
								scaleto = MAX (width, height);
							}
						}
						else
						{
							scaled = FALSE;
							scaleto = MAX (width, height);
						}
//						if (gimp_thumbnail_peek_thumb (thumbnail, scaleto) != GIMP_THUMB_STATE_EXISTS)
						if (gimp_thumbnail_check_thumb (thumbnail, scaleto) != GIMP_THUMB_STATE_OK)
						{
rescale:
							if (scaled)
								pxb = gdk_pixbuf_new_from_file_at_scale (localpath,
									scaleto, scaleto, TRUE, &error);
							else
								pxb = gdk_pixbuf_new_from_file (localpath, &error);

							if (pxb != NULL)
							{
//								local save >> into CWD/.thumblocal/normal, maybe not creatable ?! BUG?
//								if (!gimp_thumbnail_save_thumb_local (thumbnail, pxb,
								//CHECKME save pixbuf later, so we can startup more quickly
								if (!gimp_thumbnail_save_thumb (thumbnail, pxb,
									E2_SOFTWARE_ID, &error))
								{
									//warn the user
									if (error != NULL)
										g_error_free (error);
									makepxb = FALSE;
								}
							}
							else	//pixbuf creation failed
							{
								//warn the user
								if (error != NULL)
									g_error_free (error);
								makepxb = FALSE;
							}
						}
						else	//a maybe-valid cached image found
						{
							//some hacky stuff to work around the lib's approach to reporting
							//non-standard-size thumbnails ??
							if (gimp_thumbnail_peek_image (thumbnail) == GIMP_THUMB_STATE_EXISTS)
							{
								width = MAX (thumbnail->image_width, thumbnail->image_height);
								if (width == 0)	//0 is reprorted for non-standard-size ?
								{
									height = width = scaleto;
									goto rescale;
								}
								//if (width != 0	//0 is reprorted for non-standard-size ?
								//	&&
								else if (width < E2_SCALEUP_SIZE
									  || width > GIMP_THUMB_SIZE_NORMAL
									  || width != scaleto)
								{
									height = width;
									goto recreate;
								}
							}
							pxb = gimp_thumbnail_load_thumb (thumbnail, scaleto, &error);
							if (pxb == NULL)
							{
								//warn the user
								if (error != NULL)
									g_error_free (error);
								makepxb = FALSE;
							}
						}
					}
					else //no file info
					{
						//FIXME warn the user
						makepxb = FALSE;
					}
				}
				if (makepxb)
				{
					//copy the FileInfo, in case a refresh clobbers the original
					FileInfo *info2 = ALLOCATE (FileInfo);
					CHECKALLOCATEDWARN (info2, return NULL;);
					*info2 = *info;
					//store the data
					gtk_list_store_insert_with_values (store, &iteri, -1,
						FILENAME, filename,
						NAMEKEY, namekey,
						FINFO, info2,
						PIXBUF, pxb,
						THUMBNAIL, thumbnail,
						-1);
				}
				else
					g_object_unref (G_OBJECT (thumbnail));	//CHECKME any specific contents to clean ?

				F_FREE (dlocal);
				g_free (localpath);
			}
			g_free (filename);
			g_free (namekey);
		} while (gtk_tree_model_iter_next (mdlt, &itert));

		//arrange to sort new store same as old one
		gtk_tree_sortable_set_sort_func (sortablei, FILENAME,
			e2_all_columns[rt->sort_type].sort_func, &rt->sort_order, NULL);
		//do the sort
		gtk_tree_sortable_set_sort_column_id (sortablei, FILENAME, rt->sort_order);

		g_object_ref (G_OBJECT (rt->store));	//preserve the store for proper cleanup in idle
		gdk_threads_enter ();
		//connect view to new model
		gtk_icon_view_set_model (GTK_ICON_VIEW (rt->iconview), GTK_TREE_MODEL (store));
		gdk_threads_leave ();
		g_object_unref (G_OBJECT (store));	//kill the ref from model assignment

		//arrange to cleanup old store, later
		//FIXME manage shared access to superseded-stores list
		rt->oldstores = g_slist_append (rt->oldstores, rt->store); //&& some test for cleanup not underway now)
		g_idle_add ((GSourceFunc) _e2p_thumbs_clear_old_stores, rt->oldstores);
		rt->store = store;
		rt->oldstores = NULL;	//start afresh

		gdk_threads_enter ();
		e2_dialog_set_cursor (rt->dialog, GDK_LEFT_PTR);
		gdk_threads_leave ();
	}

	rt->dir_mtime = rt->view->dir_mtime;	//update refresh guide
/*#ifdef E2_FAM
# ifdef E2_VFSTMP
	//FIXME
# else
#  ifdef E2_FAM_KERNEL
	//this is a bad hack to prevent extra refresh
	if (!ref)
		e2_fs_FAM_clean_reports (rt->view->dir);
#  endif
# endif
#endif */
	e2_filelist_enable_one_refresh (pane);

	rt->blocked = FALSE;
//	printd (DEBUG, "finish icons store fill");
	return store;
}
/**
@brief refresh icons-view liststore
The existing liststore is replaced, and queued for cleanup.
BGL is expected to be open
@param rt pointer to data struct for dialog

@return
*/
static void _e2p_thumbs_refresh_store (E2_ThumbDialogRuntime *rt)
{
	gchar *name;
	GtkTreePath *tp;
	GtkTreeModel *mdl = GTK_TREE_MODEL (rt->store);
	GtkTreeIter iter;
	GList *member, *selnames = NULL;
	GList *selpaths = gtk_icon_view_get_selected_items (GTK_ICON_VIEW (rt->iconview));
	if (g_list_length (selpaths) > 0)
	{
		//record currently-selected data so we can re-select in new store
		for (member = selpaths; member != NULL; member = member->next)
		{
			tp = (GtkTreePath *)member->data;
			gtk_tree_model_get_iter (mdl, &iter, tp);
			gtk_tree_model_get (mdl, &iter, FILENAME, &name, -1);
			selnames = g_list_append (selnames, name);
			gtk_tree_path_free (tp);
		}
		g_list_free (selpaths);
	}

	//update the store
	_e2p_thumbs_replace_store (rt);

	//reselect things FIXME do this smarter
	if (g_list_length (selnames) > 0)
	{
		mdl = GTK_TREE_MODEL (rt->store);
		if (gtk_tree_model_iter_n_children (mdl, NULL))
		{
			//reselect all paths where we can
			gtk_tree_model_get_iter_first (mdl, &iter);
			for (member = selnames; member != NULL; member = member->next)
			{
				name = (gchar *)member->data;
				if (e2_tree_find_iter_from_str_same (mdl, FILENAME, name, &iter))
				{
					tp = gtk_tree_model_get_path (mdl, &iter);
					gdk_threads_enter ();
					gtk_icon_view_select_path (GTK_ICON_VIEW (rt->iconview), tp);
					gdk_threads_leave ();
					gtk_tree_path_free (tp);
					gtk_tree_model_get_iter_first (mdl, &iter); //ready for next search
				}
				g_free (name);
			}
/*			//goto former position
			if (0) //FIXME
			{
				tp = (GtkTreePath *) selpaths->data;
				gtk_icon_view_scroll_to_path (GTK_ICON_VIEW (rt->iconview),
					tp, TRUE, 0.3, 0.5);
			} */
		}
		else
			g_list_foreach (selnames, (GFunc) g_free, NULL);
		g_list_free (selnames);
	}
}
/**
@brief timer callback to do a store refresh if the dir looks dirty
@param rt pointer to dialog's data struct
@return TRUE unless the dialog is destroyed already
*/
static gboolean _e2p_thumbs_check_dirty (E2_ThumbDialogRuntime *rt)
{
	printd (DEBUG, "_e2p_thumbs_check_dirty");
	if (!GTK_IS_WIDGET (rt->dialog))
		return FALSE;
	static gboolean busy = FALSE;
	if (!busy)
	{
#ifdef E2_VFSTMP
	//FIXME when dir is not mounted local
#endif
		if (rt->dir_mtime < rt->view->dir_mtime
			&& !rt->blocked
			&& g_str_equal (rt->path, rt->view->dir))
		{
			busy = TRUE;
			_e2p_thumbs_refresh_store (rt);
			busy = FALSE;
		}
	}
	return TRUE;
}
/**
@brief timer callback to turn auto-refresh back on
@param rt pointer to dialog's data struct
@return FALSE
*/
/*static gboolean _e2p_thumbs_enable_refresh (E2_ThumbDialogRuntime *rt)
{
	if (GTK_IS_WIDGET (rt->dialog))
		e2_hook_register (&rt->view->hook_refresh, _e2p_thumbs_refresh_hook, rt);
	return FALSE;
} */
/**
@brief timer callback to do a store replacement after refreshing is unblocked
@param rt pointer to dialog's data struct
@return TRUE if the associated-filelist refresh is still in progress
*/
static gboolean _e2p_thumbs_wait_to_replace (E2_ThumbDialogRuntime *rt)
{
	if (GTK_IS_WIDGET (rt->dialog) && !rt->blocked)
	{
		LISTS_LOCK
		gboolean busy = rt->view->listcontrols.refresh_working
					 || rt->view->listcontrols.cd_working;
		LISTS_UNLOCK
		if (busy)
			return TRUE;
		static gboolean working = FALSE;	//simple blocker instead of killing timer
		//to prevent automatic refreshing of 2 views from feeding off each other,
		//suspend for a while the automatic refreshing here
		//(too bad if we miss something in the meantime)
//		printd (DEBUG, "doing deferred replication of CHANGE-DIR for icons liststore");
//		e2_hook_unregister (&rt->view->hook_refresh, _e2p_thumbs_refresh_hook, rt, TRUE);
		if (!working)
		{	//previous timer callback hasn't yet finished
			working = TRUE;
			_e2p_thumbs_replace_store (rt);
			working = FALSE;
		}
/*#ifdef USE_GLIB2_14
		g_timeout_add_seconds (10,
#else
		g_timeout_add (10000,
#endif
		 (GSourceFunc)_e2p_thumbs_enable_refresh, rt);
*/
	}
	return FALSE;
}
/**
@brief hook function for app.paneX.hook_change_dir
This is initiated with BGL off/open
@param newpath UNUSED path of opened directory, utf-8 string
@param rt pointer to dialog's data struct
@return TRUE always
*/
static gboolean _e2p_thumbs_change_dir_hook (gchar *newpath, E2_ThumbDialogRuntime *rt)
{
	printd (DEBUG, "replicating CHANGE-DIR for icons liststore");
	g_free (rt->path);
	rt->path = g_strdup (newpath);
	LISTS_LOCK
	gboolean busy = rt->view->listcontrols.refresh_working
				 || rt->view->listcontrols.cd_working;
	LISTS_UNLOCK
	if (!busy)
	{
		//to prevent automatic refreshing of 2 views from feeding off each other,
		//suspend for a while the automatic refreshing here
		//(too bad if we miss something in the meantime)
//		e2_hook_unregister (&rt->view->hook_refresh, _e2p_thumbs_refresh_hook, rt, TRUE);
		_e2p_thumbs_replace_store (rt);
/*#ifdef USE_GLIB2_14
		g_timeout_add_seconds (10,
#else
		g_timeout_add (10000,
#endif
		 (GSourceFunc)_e2p_thumbs_enable_refresh, rt);
*/
	}
	else
	{
		printd (DEBUG, "when the current filelist refresh is finished");
		g_timeout_add (200, (GSourceFunc)_e2p_thumbs_wait_to_replace, rt);
	}

	return TRUE;
}
/**
@brief timer callback to do a store refresh after refreshing is unblocked
@param rt pointer to dialog's data struct
@return TRUE if the associated-filelist refresh is still in progress
*/
static gboolean _e2p_thumbs_wait_to_refresh (E2_ThumbDialogRuntime *rt)
{
	if (GTK_IS_WIDGET (rt->dialog) && !rt->blocked)
	{
		LISTS_LOCK
		gboolean disabled = rt->view->listcontrols.norefresh;	//CHECKME ->listing ?
		LISTS_UNLOCK
		if (disabled)
			return TRUE;
		printd (DEBUG, "deferred replication of REFRESH of icons liststore");
		//to prevent automatic refreshing of 2 views from feeding off each other,
		//suspend for a while the automatic refreshing here
		//(too bad if we miss something in the meantime)
//		e2_hook_unregister (&rt->view->hook_refresh, _e2p_thumbs_refresh_hook, rt, TRUE);

		static gboolean working = FALSE;	//simple blocker instead of killing timer
		if (!working)
		{	//previous timer callback hasn't yet finished
			working = TRUE;
			_e2p_thumbs_refresh_store (rt);
			working = FALSE;
		}
/*#ifdef USE_GLIB2_14
		g_timeout_add_seconds (10,
#else
		g_timeout_add (10000,
#endif
		 (GSourceFunc)_e2p_thumbs_enable_refresh, rt);
*/
	}
	return FALSE;
}
/* *
@brief hook function for view.hook_refresh
This is initiated from an idle-callback, so BGL is off
@param view pointer to data struct for view whose filelist has been refeshed
@param rt pointer to dialog's data struct
@return TRUE always
*/
/*static gboolean _e2p_thumbs_refresh_hook (ViewInfo *view, E2_ThumbDialogRuntime *rt)
{
	printd (DEBUG, "replicating REFRESH of icons liststore");
	if (!rt->view->norefresh)	//CHECKME ->listing ?
	{
		//to prevent automatic refreshing of 2 views from feeding off each other,
		//suspend for a while the automatic refreshing here
		//(too bad if we miss something in the meantime)
		e2_hook_unregister (&rt->view->hook_refresh, _e2p_thumbs_refresh_hook, rt, TRUE);
		_e2p_thumbs_refresh_store (rt);
#ifdef USE_GLIB2_14
		g_timeout_add_seconds (10,
#else
		g_timeout_add (10000,
#endif
		 (GSourceFunc)_e2p_thumbs_enable_refresh, rt);
	}
	else
	{
		printd (DEBUG, "when the current filelist refresh is finished");
		g_timeout_add (200, (GSourceFunc)_e2p_thumbs_wait_to_refresh, rt);
	}

	return TRUE;
} */
/**
@brief migrate iconview selection to associated filelist

@param rt pointer to dialog's data struct

@return TRUE if something was selected in the iconview
*/
static gboolean _e2p_thumbs_transfer_selection (E2_ThumbDialogRuntime *rt)
{
	gboolean retval = FALSE;
	GtkTreeSelection *listsel = gtk_tree_view_get_selection (GTK_TREE_VIEW (rt->view->treeview));
	gtk_tree_selection_unselect_all (listsel);
	GList *sel = gtk_icon_view_get_selected_items (GTK_ICON_VIEW (rt->iconview));
	if (sel != NULL && g_list_length (sel) > 0)
	{
		gchar *name;
		GList *member;
		GtkTreePath *tp;
		GtkTreeModel *mdl = GTK_TREE_MODEL (rt->store);
		GtkTreeModel *listmdl = gtk_tree_view_get_model (GTK_TREE_VIEW (rt->view->treeview)); //filter model
		GtkTreeIter iter, listiter;

		if (gtk_tree_model_get_iter_first (listmdl, &listiter))
		{
			for (member = sel; member != NULL; member = member->next)
			{
				tp = (GtkTreePath *)member->data;
				gtk_tree_model_get_iter (mdl, &iter, tp);
				gtk_tree_model_get (mdl, &iter, FILENAME, &name, -1);
				if (e2_tree_find_iter_from_str_same (listmdl, FILENAME, name, &listiter))
				{
					gtk_tree_selection_select_iter (listsel, &listiter);
					gtk_tree_model_get_iter_first (listmdl, &listiter);
				}
				g_free (name);
				gtk_tree_path_free (tp);
			}
		}
		else
			g_list_foreach (sel, (GFunc) gtk_tree_path_free, NULL);

		g_list_free (sel);
		retval = TRUE;
	}
	return retval;
}
/**
@brief iconview selection foreach function to reselect then clean each selected path
This is needed because selection is cleared when view is disconnected from model
@param data pointer to list item data, a gtk tree path
@param rt pointer to dialog data struct

@return
*/
static void _e2p_thumbs_cleanpath (GtkTreePath *data, E2_ThumbDialogRuntime *rt)
{
	gtk_icon_view_select_path (GTK_ICON_VIEW (rt->iconview), data);
	gtk_tree_path_free (data);
}
/**
@brief rotate or flip selected thumbnails in accord with @a type
No change is made to cached data
@param type enumerator of the type of change, rotate + or -, default is flip vertical
@param rt pointer to dialog data struct

@return
*/
static void _e2p_thumbs_transform (GdkPixbufRotation type, E2_ThumbDialogRuntime *rt)
{
	GList *selpaths = gtk_icon_view_get_selected_items (GTK_ICON_VIEW (rt->iconview));
	gboolean selection = (selpaths != NULL && g_list_length (selpaths) > 0);
	if (selection)
	{
		GtkTreePath *tp;
		GtkTreeIter iter;
		GdkPixbuf *oldpxb, *newpxb;
		GList *member;

		g_object_ref (G_OBJECT (rt->store));
		gtk_icon_view_set_model (GTK_ICON_VIEW (rt->iconview), NULL);

		for (member = selpaths; member != NULL; member = member->next)
		{
			tp = (GtkTreePath *)member->data;
			gtk_tree_model_get_iter (GTK_TREE_MODEL (rt->store), &iter, tp);
			gtk_tree_model_get (GTK_TREE_MODEL (rt->store), &iter,
				PIXBUF, &oldpxb, -1);
			switch (type)
			{
				case GDK_PIXBUF_ROTATE_CLOCKWISE:
					newpxb = gdk_pixbuf_rotate_simple (oldpxb, GDK_PIXBUF_ROTATE_CLOCKWISE);
					break;
				case GDK_PIXBUF_ROTATE_COUNTERCLOCKWISE:
					newpxb = gdk_pixbuf_rotate_simple (oldpxb, GDK_PIXBUF_ROTATE_COUNTERCLOCKWISE);
					break;
				default:
					newpxb = gdk_pixbuf_flip (oldpxb, FALSE);
					break;
			}
			if (newpxb != NULL)
			{
				g_object_unref (G_OBJECT (oldpxb));
				gtk_list_store_set (rt->store, &iter, PIXBUF, newpxb, -1);
			}
		}

		gtk_icon_view_set_model (GTK_ICON_VIEW (rt->iconview), GTK_TREE_MODEL (rt->store));
		g_object_unref (G_OBJECT (rt->store));

		g_signal_handlers_block_by_func (G_OBJECT (rt->iconview),
			_e2p_thumbs_selection_change_cb, rt);
		g_list_foreach (selpaths, (GFunc)_e2p_thumbs_cleanpath, rt);
		g_signal_handlers_unblock_by_func (G_OBJECT (rt->iconview),
			_e2p_thumbs_selection_change_cb, rt);
	}
	g_list_free (selpaths);
}

  /********************/
 /*** context menu ***/
/********************/

/**
@brief set popup-menu position

This function is supplied when calling gtk_menu_popup(), to position
the displayed menu, after a menu-key press.
set @a push_in to TRUE for menu completely inside the screen,
FALSE for menu clamped to screen size

@param menu UNUSED the GtkMenu to be positioned
@param x	place to store gint representing the menu left
@param y  place to store gint representing the menu top
@param push_in place to store pushin flag
@param rt data struct for the dialog where the menu key was pressed

@return
*/
static void _e2p_thumbs_set_menu_position (GtkMenu *menu,
	gint *x, gint *y, gboolean *push_in, E2_ThumbDialogRuntime *rt)
{
	gint left, top;
	gtk_window_get_position (GTK_WINDOW (rt->dialog), &left, &top);
	GtkAllocation alloc = rt->iconview->allocation;
	*x = left + alloc.x + alloc.width/2;
	*y = top + alloc.y +alloc.height/2 - 30;
	*push_in = FALSE;
}
/**
@brief execute action corresponding to item selected from filetype tasks menu
This is the callback for handling a selection of a filetype action from
the context menu
@param widget the selected menu item widget
@param rt data struct for the dialog where the menu key was pressed

@return
*/
static void _e2p_thumbs_menu_choose_filetype_action_cb (GtkWidget *widget,
	E2_ThumbDialogRuntime *rt)
{
	printd (DEBUG, "context menu choose filetype action cb");
	GList *sel = gtk_icon_view_get_selected_items (GTK_ICON_VIEW (rt->iconview));
	if (sel != NULL && g_list_length (sel) > 0)
	{
		GString *command;
		gchar *name;
		GList *member;
		GtkTreePath *tp;
		GtkTreeModel *mdl = GTK_TREE_MODEL (rt->store);
		GtkTreeIter iter;

		command = g_string_sized_new (512);
		command = g_string_assign (command, gtk_widget_get_name (widget));
		for (member = sel; member != NULL; member = member->next)
		{
			tp = (GtkTreePath *)member->data;
			gtk_tree_model_get_iter (mdl, &iter, tp);
			gtk_tree_model_get (mdl, &iter, FILENAME, &name, -1);
			//FIXME there's a small chance that the dir may have changed
			//without syncing to the dialog content
			g_string_append_printf (command, " \"%s%s\"", rt->view->dir, name);
			g_free (name);
			gtk_tree_path_free (tp);
		}
		g_list_free (sel);

#ifdef E2_COMMANDQ
		e2_command_run (command->str, E2_COMMAND_RANGE_DEFAULT, FALSE);
#else
		e2_command_run (command->str, E2_COMMAND_RANGE_DEFAULT);
#endif
		g_string_free (command, TRUE);
	}
}
/**
@brief iconview un-select-all callback

@param widget UNUSED the menu item widget which activated the callback
@param rt pointer to dialog data struct

@return
*/
static void _e2p_thumbs_unselect_all_cb (GtkWidget *widget, E2_ThumbDialogRuntime *rt)
{
	gtk_icon_view_unselect_all (GTK_ICON_VIEW (rt->iconview));
}
/**
@brief iconview refresh callback

@param widget UNUSED the menu item widget which activated the callback
@param rt pointer to dialog data struct

@return
*/
static void _e2p_thumbs_refresh_cb (GtkWidget *widget, E2_ThumbDialogRuntime *rt)
{
	LISTS_LOCK
	gboolean busy = rt->view->listcontrols.norefresh;
	LISTS_UNLOCK
	if (!busy)
	{
		gdk_threads_leave ();
		_e2p_thumbs_refresh_store (rt);
		gdk_threads_enter ();
	}
	else
		g_timeout_add (200, (GSourceFunc)_e2p_thumbs_wait_to_refresh, rt);
}
/**
@brief rotate selected thumbnails 90 degrees clockwise
No change is made to cached data
@param widget UNUSWED the menu item widget which activated the callback
@param rt pointer to dialog data struct

@return
*/
static void _e2p_thumbs_turn_clockwise_cb (GtkWidget *widget, E2_ThumbDialogRuntime *rt)
{
	_e2p_thumbs_transform (GDK_PIXBUF_ROTATE_CLOCKWISE, rt);
}
/**
@brief rotate selected thumbnails 90 degrees anti-clockwise
No change is made to cached data
@param widget UNUSED the menu item widget which activated the callback
@param rt pointer to dialog data struct

@return
*/
static void _e2p_thumbs_turn_anticlockwise_cb (GtkWidget *widget, E2_ThumbDialogRuntime *rt)
{
	_e2p_thumbs_transform (GDK_PIXBUF_ROTATE_COUNTERCLOCKWISE, rt);
}
/**
@brief flip selected thumbnails top-to-bottom
No change is made to cached data
@param widget UNUSED the menu item widget which activated the callback
@param rt pointer to dialog data struct

@return
*/
static void _e2p_thumbs_flip_cb (GtkWidget *widget, E2_ThumbDialogRuntime *rt)
{
	_e2p_thumbs_transform (GDK_PIXBUF_ROTATE_NONE, rt);
}
/**
@brief iconview selection-replication callback

@param widget the menu item widget which activated the callback
@param rt pointer to dialog data struct

@return
*/
static void _e2p_thumbs_toggle_replication_cb (GtkWidget *widget, E2_ThumbDialogRuntime *rt)
{
	rt->replicate = gtk_check_menu_item_get_active (GTK_CHECK_MENU_ITEM (widget));
	//FIXME other adjustments
	if (rt->replicate)
	{
		_e2p_thumbs_transfer_selection (rt);
	}
//	else
//	{
//	}
}
/**
@brief iconview clamp-image-size toggle callback

@param widget the menu item widget which activated the callback
@param rt pointer to dialog data struct

@return
*/
static void _e2p_thumbs_toggle_clamp_cb (GtkWidget *widget, E2_ThumbDialogRuntime *rt)
{
	rt->clamp = gtk_check_menu_item_get_active (GTK_CHECK_MENU_ITEM (widget));
	_e2p_thumbs_refresh_cb (NULL, rt);
}
/**
@brief populate @a menu with items for the actions for a filetype
Can't use the standard function for this, it initiates commands using %f, which
may be wrong, and has no data for the callback
Each member of @a actions is like "command" or "label@command"
@param menu the menu widget to which the action menu-items are to be added
@param actions NULL-terminated array of utf8 strings, each a command for a filetype
@param rt data struct for the dialog where the menu key was pressed

@return
*/
static void _e2p_thumbs_menu_filetype_actions (GtkWidget *menu,
	const gchar **actions, E2_ThumbDialogRuntime *rt)
{
	gchar *sep;
	GtkWidget *menu_item;

	while (*actions != NULL)
	{
		if ((sep = strchr (*actions, '@')) != NULL)  //if always ascii @, don't need g_utf8_strchr()
		{
			*sep = '\0';
			menu_item = e2_menu_add (menu, (gchar *)*actions, NULL, NULL,
				_e2p_thumbs_menu_choose_filetype_action_cb, rt);
			*sep = '@';	//revert to original form (this is the 'source' data)
			gtk_widget_set_name (menu_item, sep+1);
		}
		else
		{
			menu_item = e2_menu_add (menu, (gchar *)*actions, NULL, NULL,
				_e2p_thumbs_menu_choose_filetype_action_cb, rt);
			gtk_widget_set_name (menu_item, *actions);
		}
		actions++;
	}
}
/**
@brief construct and show dialog context menu
This provides a subset of the filelist context menu, plus a couple of things
@param iconview the widget where the click happened
@param event_button which mouse button was clicked (0 for a menu key)
@param event_time time that the event happened (0 for a menu key)
@param rt runtime struct for the displayed dialog

@return
*/
static void _e2p_thumbs_show_context_menu (GtkWidget *iconview,
	guint event_button, gint event_time, E2_ThumbDialogRuntime *rt)
{
	GtkIconView *ivw = GTK_ICON_VIEW (iconview);
	GList *selpaths = gtk_icon_view_get_selected_items (ivw);
	gboolean selection = (selpaths != NULL && g_list_length (selpaths) > 0);
	GtkWidget *menu = gtk_menu_new ();
	GtkWidget *item;

	if (selection)
	{
		GtkTreeIter iter;
		GtkTreeModel *mdl = GTK_TREE_MODEL (rt->store);
		GtkTreePath *tpath = (GtkTreePath *) selpaths->data;
		if (gtk_tree_model_get_iter (mdl, &iter, tpath))
		{
			gchar *filename, *ext;
			const gchar **actions;
			gtk_tree_model_get (mdl, &iter, FILENAME, &filename, -1);
			ext = filename;
			while ((ext = strchr (ext, '.')) != NULL)
			{
				if (ext == filename)
				{	//hidden item, probably
					ext++;
					continue;
				}
				ext++;	//skip discovered dot, ascii '.'. always single char
				actions = e2_filetype_get_actions (ext);
				if (actions != NULL)
				{
					_e2p_thumbs_menu_filetype_actions (menu, actions, rt);
					break;
				}
			}
			g_free (filename);
		}
		gchar *aname = g_strconcat (_A(5),".",_A(62), NULL);
		e2_menu_add_action (menu, _("Open _with.."),"open_with_"E2IP".png", NULL,
			aname, NULL, NULL);
		e2_menu_add_separator (menu);

		e2_menu_add (menu, _("Rotate _+"), NULL,
			_("Rotate selected images quarter-turn clockwise"),
			_e2p_thumbs_turn_clockwise_cb, rt);
		e2_menu_add (menu, _("Rotate _-"), NULL,
			_("Rotate selected images quarter-turn anti-clockwise"),
			_e2p_thumbs_turn_anticlockwise_cb, rt);
		e2_menu_add (menu, _("_Flip"), NULL,
			_("Flip selected images top-to-bottom"),
			_e2p_thumbs_flip_cb, rt);
	}

	e2_menu_add (menu, _("_Refresh"), GTK_STOCK_REFRESH, NULL,
		_e2p_thumbs_refresh_cb, rt);

	if (selection)
	{
//		item =
		e2_menu_add (menu, _("_Unselect all"), GTK_STOCK_CLEAR, NULL,
			_e2p_thumbs_unselect_all_cb, rt);
//		if (!selection)
//			gtk_widget_set_sensitive (item, FALSE);
	}
	item =
	e2_menu_add_check (menu, _("Replicate _selection"),
		rt->replicate, _e2p_thumbs_toggle_replication_cb, rt);
#ifdef USE_GTK2_12TIPS
	gtk_widget_set_tooltip_text (
#else
	e2_widget_set_tooltip (NULL,
#endif
		item,
		_("If activated, items selected in this window will also be selected in the associated filelist"));

	item =
	e2_menu_add_check (menu, _("_Clamp size"),
		rt->clamp, _e2p_thumbs_toggle_clamp_cb, rt);
#ifdef USE_GTK2_12TIPS
	gtk_widget_set_tooltip_text (
#else
	e2_widget_set_tooltip (NULL,
#endif
		item,
		_("If activated, thumbnails will be scaled up or down if needed, into the range 32 to 128 pixels"));
/*	item =
	e2_menu_add_check (menu, _("_Hidden items"), rt->show_hidden,
		_e2_treedlg_toggle_strict_cb, rt);
	if (!rt->show_hidden)
#ifdef USE_GTK2_12TIPS
		gtk_widget_set_tooltip_text (
#else
		e2_widget_set_tooltip (NULL,
#endif
		item,
		_("If activated, hidden image files will be displayed"));
	gtk_widget_set_sensitive (item, !rt->show_hidden);
*/

	g_signal_connect (G_OBJECT (menu), "selection-done",
		G_CALLBACK (e2_menu_destroy_cb), NULL);

	if (event_button == 0)
		gtk_menu_popup (GTK_MENU (menu), NULL, NULL,
			(GtkMenuPositionFunc) _e2p_thumbs_set_menu_position, rt,
			0, event_time);
	else
		//this was a button-3 click
		gtk_menu_popup (GTK_MENU (menu), NULL, NULL,
			NULL, NULL, 3, event_time);

	if (selection)
		g_list_foreach (selpaths, (GFunc) gtk_tree_path_free, NULL);
	g_list_free (selpaths);
}

  /*********************/
 /***** callbacks *****/
/*********************/

/**
@brief mouse button press callback

@param iconview the widget where the button was pressed
@param event gdk event data
@param view rt data for the view to be worked on

@return TRUE (stop other handlers) for btn 3 press, else FALSE
*/
static gboolean _e2p_thumbs_button_press_cb (GtkWidget *iconview,
	GdkEventButton *event, E2_ThumbDialogRuntime *rt)
{
	printd (DEBUG, "callback: _e2p_thumbs mouse button press");
	if (event->button == 3)
	{
		_e2p_thumbs_show_context_menu (iconview, 3, event->time, rt);
		return TRUE;
	}
	return FALSE;
}
/**
@brief iconview key-press callback

@param iconview UNUSED the focused treeview widget when the key was pressed
@param event pointer to event data struct
@param rt pointer to dialog data struct

@return TRUE (stop other handlers) for menu key has, else FALSE
*/
/*static gboolean _e2p_thumbs_key_press_cb (GtkWidget *iconview,
	GdkEventKey *event, E2_ThumbDialogRuntime *rt)
{
	printd (DEBUG, "callback: _e2p_thumbs key press");
	return FALSE;
} */
/**
@brief menu-button press callback

@param iconview the widget where the press happened
@param rt dialog runtime data struct

@return TRUE always
*/
static gboolean _e2p_thumbs_popup_menu_cb (GtkWidget *iconview,
	E2_ThumbDialogRuntime *rt)
{
	gint event_time = gtk_get_current_event_time ();
	_e2p_thumbs_show_context_menu (iconview, 0, event_time, rt);
	return TRUE;
}
/**
@brief iconview item-activated callback
Activation is triggered when <Enter> is pressed or when a double-click happens
This causes the activated item to be opened
@param iconview the widget where the activation happened
@param path model path to the clicked item
@param view data struct for the view to be worked on

@return
*/
static void _e2p_thumbs_item_activated_cb (GtkIconView *iconview,
	GtkTreePath *tpath, ViewInfo *view)
{
	printd (DEBUG, "callback: _e2p_thumbs_item_activated");
	GtkTreeIter iter;
	GtkTreeModel *model = gtk_icon_view_get_model (iconview);
    if (gtk_tree_model_get_iter (model, &iter, tpath))
	{
		//get the activated item
		gchar *localpath;
		FileInfo *info;
		gtk_tree_model_get (model, &iter, FINFO, &info, -1);
		localpath = e2_utils_dircat (view, info->filename, TRUE);
#ifdef E2_VFS
		VPATH ddata = { localpath, view->spacedata };
		e2_task_backend_open (&ddata, TRUE);
#else
		e2_task_backend_open (localpath, TRUE);
#endif
		g_free (localpath);
	}
}
/**
@brief iconview selection-changed callback
This allows selection to be migrated to the associated filelist treeview

@param iconview the widget where the activation happened
@param rt data struct for the dialog

@return
*/
static void _e2p_thumbs_selection_change_cb (GtkIconView *iconview,
	E2_ThumbDialogRuntime *rt)
{
	if (rt->replicate)
		_e2p_thumbs_transfer_selection (rt);
}
/**
@brief iconview sort-column callback

@param widget the menu item widget which activated the callback
@param rt pointer to dialog data struct

@return
*/
static void _e2p_thumbs_change_sortcol_cb (GtkWidget *widget, E2_ThumbDialogRuntime *rt)
{
	gpointer col = g_object_get_data (G_OBJECT (widget), "sort-column");
	rt->sort_type = GPOINTER_TO_INT (col) - 1;
	GtkTreeSortable *sortable = GTK_TREE_SORTABLE (rt->store);
	//arrange to sort new store same as old one
	gtk_tree_sortable_set_sort_func (sortable, FILENAME,
		e2_all_columns[rt->sort_type].sort_func, &rt->sort_order, NULL);
	gtk_tree_sortable_set_sort_column_id (sortable, FILENAME, rt->sort_order);
}
/**
@brief iconview sort-order callback

@param widget the menu item widget which activated the callback
@param rt pointer to dialog data struct

@return
*/
static void _e2p_thumbs_toggle_sortorder_cb (GtkWidget *widget, E2_ThumbDialogRuntime *rt)
{
	gboolean newchoice = gtk_check_menu_item_get_active (GTK_CHECK_MENU_ITEM (widget));
	rt->sort_order = (newchoice) ? GTK_SORT_ASCENDING : GTK_SORT_DESCENDING;
	GtkTreeSortable *sortable = GTK_TREE_SORTABLE (rt->store);
	gtk_tree_sortable_set_sort_column_id (sortable, FILENAME, rt->sort_order);
	e2_button_set_image (rt->sortbtn, (newchoice) ?
		GTK_STOCK_SORT_DESCENDING : GTK_STOCK_SORT_ASCENDING);
}
/**
@brief create sort-options menu for dialog

@param rt data struct for dialog

@return the menu widget
*/
static GtkWidget *_e2p_thumbs_create_sorting_menu (E2_ThumbDialogRuntime *rt)
{
	GtkWidget *item;
	GtkWidget *menu = gtk_menu_new ();
	gint i;
	for (i = 0; i < MAX_COLUMNS; i++)
	{
		item =
		e2_menu_add_check (menu, gettext (e2_all_columns[i].title), (i == rt->sort_type),
			_e2p_thumbs_change_sortcol_cb, rt);
		//also pass the column no. (bumped to avoid 0=NULL)
		g_object_set_data (G_OBJECT (item), "sort-column", GINT_TO_POINTER (i+1));
	}
	item =
	e2_menu_add_check (menu, _("Ascending"), (rt->sort_order == GTK_SORT_ASCENDING),
		_e2p_thumbs_toggle_sortorder_cb, rt);
#ifdef USE_GTK2_12TIPS
	gtk_widget_set_tooltip_text (
#else
	e2_widget_set_tooltip (NULL,
#endif
		item,
		_("If activated, items are displayed in ascending order"));

	g_signal_connect (G_OBJECT (menu), "selection-done",
		G_CALLBACK (e2_menu_destroy_cb), NULL);

	return menu;
}
/**
@brief set popup menu position

This function is supplied when calling gtk_menu_popup(), to position
the displayed menu.
set @a push_in to TRUE for menu completely inside the screen,
FALSE for menu clamped to screen size

@param menu the GtkMenu to be positioned
@param x	place to store gint representing the menu left
@param y  place to store gint representing the menu top
@param push_in place to store pushin flag
@param button the activated dialog button

@return
*/
static void _e2p_thumbs_set_sortmenu_position (GtkMenu *menu, gint *x, gint *y,
	gboolean *push_in, GtkWidget *button)
{
	gint button_y;
	e2_utils_get_abs_pos (button, x, &button_y);
	GtkRequisition menu_size;
	gtk_widget_size_request (GTK_WIDGET (menu), &menu_size);

	//place below or above button, left-aligned
	if (button_y - menu_size.height <= 2)	//> gdk_screen_height ())
		*y = button_y + button->allocation.height + 2;
	else
		*y = button_y - menu_size.height - 2;
	*push_in = FALSE;
}
/**
@brief handle button click, window-close etc for directory-tree dialog
This is the callback for response signals emitted from @a dialog
@param dialog UNUSED the dialog where the response was generated
@param response the response returned from the dialog
@param rt pointer to data struct for the dialog

@return
*/
static void _e2p_thumbs_response_cb (GtkDialog *dialog, gint response,
	E2_ThumbDialogRuntime *rt)
{
	switch (response)
	{
/*		case E2_RESPONSE_USER1: //toggle display of hidden items
			rt->show_hidden = !rt->show_hidden;
			_e2_treedlg_refresh_cb (NULL, rt);	//do the content before changing button
			e2_button_set_image (rt->hiddenbtn, (rt->show_hidden) ?
				"hidden_noshow_"E2IP".png" : "hidden_show_"E2IP".png");
			break; */
		case E2_RESPONSE_USER2: //sort-button click
		{
			GtkWidget *menu = _e2p_thumbs_create_sorting_menu (rt);
			guint32 time = gtk_get_current_event_time ();
			gtk_menu_popup (GTK_MENU (menu), NULL, NULL,
				(GtkMenuPositionFunc) _e2p_thumbs_set_sortmenu_position, rt->sortbtn, 1, time);
		}
			break;
		default:
			g_source_remove (rt->refreshtimer_id);
			window_width = rt->dialog->allocation.width;
			window_height = rt->dialog->allocation.height;
#ifdef E2_TRANSIENTKEYS
			gchar *category = g_strconcat (_C(11), ".", aname, NULL);	//_(dialogs.view
			e2_keybinding_unregister (category, rt->iconview);
			g_free (category);
#endif
			//to prevent leaks, ensure underlying store is not zapped with the dialog
			g_object_ref (G_OBJECT (rt->store));
			gtk_widget_destroy (rt->dialog);

			GHookList thisview_cdhook = (rt->view == &app.pane1_view) ?
				app.pane1.hook_change_dir : app.pane2.hook_change_dir;
			e2_hook_unregister (&thisview_cdhook, _e2p_thumbs_change_dir_hook, rt, TRUE);
//			e2_hook_unregister (&rt->view->hook_refresh, _e2p_thumbs_refresh_hook, rt, TRUE);

//			show_hidden = rt->show_hidden;	//backups for later use this session
			//FIXME manage shared access to this list
			rt->oldstores = g_slist_append (rt->oldstores, rt->store);
			g_idle_add ((GSourceFunc) _e2p_thumbs_clear_old_stores, rt->oldstores);
			DEALLOCATE (E2_ThumbDialogRuntime, rt);
			break;
	}
}

/**
@brief establish and show icons view for contents of dir associated with @a view
This is a thread function
@param view data struct for file pane with which the iconview is to be associated

@return NULL
*/
static gpointer _e2p_thumbs_dialog_run (ViewInfo *view)
{
	printd (DEBUG, "create images preview dialog");
	E2_ThumbDialogRuntime *rt = ALLOCATE (E2_ThumbDialogRuntime);
	CHECKALLOCATEDWARN (rt, return NULL;);

	//create empty liststore framework for the dialog
	rt->store = _e2p_thumbs_make_store ();
	if (rt->store == NULL)
	{
		//FIXME warn user
		DEALLOCATE (E2_ThumbDialogRuntime, rt);
		return NULL;
	}

//	rt->show_hidden = show_hidden;	//before dialog is filled CHECKME use view->show_hidden ?
	rt->replicate = TRUE;	//cause iconview selections to be replicated in related treeview
	rt->clamp = TRUE;	//cause thumbs to be scaled up or down if outside 32..128 px
	rt->sort_type = view->sort_column;
	rt->sort_order = view->sort_order;
	rt->blocked = FALSE;
/* no need for this, before the store is initially filled
	GtkTreeSortable *sortable = GTK_TREE_SORTABLE (rt->store);
	gtk_tree_sortable_set_sort_func (sortable, FILENAME,
		e2_all_columns[rt->sort_type].sort_func, &rt->sort_order, NULL);
	//set initial sort arrangment before store is filled
	gtk_tree_sortable_set_sort_column_id (sortable, FILENAME, rt->sort_order);
*/
	rt->view = view;
	rt->path = g_strdup (view->dir);
	rt->oldstores = NULL;

	gchar *title = (view == &app.pane1_view) ? _("pane 1 images") : _("pane 2 images") ;
	rt->dialog = e2_dialog_create (NULL, NULL, title, _e2p_thumbs_response_cb, rt);

	//scrolled window for the treeview
	GtkWidget *sw = e2_widget_get_sw (GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC,
		GTK_SHADOW_ETCHED_IN);
	gtk_scrolled_window_set_placement (GTK_SCROLLED_WINDOW (sw),
		e2_option_int_get ("scrollbar-position"));

	gtk_container_add ((GTK_CONTAINER (GTK_DIALOG (rt->dialog)->vbox)), sw);

	/*now create iconsview */
//	gchar *fontstr = (e2_option_bool_get ("custom-list-font")) ?
//		e2_option_str_get ("list-font") : NULL;	//NULL will cause default font

	GtkTreeModel *mdl = GTK_TREE_MODEL (rt->store);
	//create iconview for the pane related to @a view
	rt->iconview = gtk_icon_view_new_with_model (mdl);
	gtk_container_add (GTK_CONTAINER (sw), rt->iconview);

	g_object_unref (G_OBJECT (rt->store));	//kill the ref from view creation

	//allow non-sorted display using GTK_TREE_SORTABLE_DEFAULT_SORT_COLUMN_ID
//	GtkTreeSortable *sortable = GTK_TREE_SORTABLE (mdl);
//	gtk_tree_sortable_set_default_sort_func (sortable, NULL, NULL, NULL);

	//set general iconview properties
	GtkIconView *iconview = GTK_ICON_VIEW (rt->iconview);
	gtk_icon_view_set_text_column (iconview, FILENAME);
	gtk_icon_view_set_pixbuf_column (iconview, PIXBUF);

	gtk_icon_view_set_reorderable (iconview, TRUE);
	gtk_icon_view_set_selection_mode (iconview, GTK_SELECTION_MULTIPLE);
#ifdef USE_GTK2_12
//this is for pango >= 1.16, Gtk 2.10.8?
	PangoContext *context = gtk_widget_get_pango_context (rt->iconview);
	const PangoMatrix *matrix = pango_context_get_matrix (context);
	PangoGravity grav = pango_gravity_get_for_matrix (matrix);
	if PANGO_GRAVITY_IS_VERTICAL(grav)
		gtk_icon_view_set_orientation (iconview, GTK_ORIENTATION_HORIZONTAL); //VERTICAL is the default
#endif
//	gtk_icon_view_set_columns (iconview, gint columns);
//	gtk_icon_view_set_item_width (iconview, gint item_width);
//	gtk_icon_view_set_spacing (iconview, gint spacing);
//	gtk_icon_view_set_row_spacing (iconview, gint row_spacing);
//	gtk_icon_view_set_column_spacing (iconview, gint column_spacing);
//	gtk_icon_view_set_margin (iconview, gint margin);

//	gtk_icon_view_select_path (iconview, GtkTreePath *path);
//	gtk_icon_view_scroll_to_path (iconview, GtkTreePath *path,
//		gboolean use_align, gfloat row_align, gfloat col_align);

//	gtk_icon_view_set_cursor (iconview, GtkTreePath *path,
//		GtkCellRenderer *cell, gboolean start_editing);

	g_signal_connect (G_OBJECT (rt->iconview), "popup-menu",
		G_CALLBACK (_e2p_thumbs_popup_menu_cb), rt);
#ifdef E2_TRANSIENTKEYS
	//add dialog-specific key bindings, before the key-press callback
	gchar *category = g_strconcat (_C(11), ".", aname, NULL);	//_(dialogs.view
	e2_keybinding_register_transient (category, rt->iconview,
		_e2p_thumbs_keybindings);
	g_free (category);
#endif
//	g_signal_connect_after (G_OBJECT (rt->iconview), "key-press-event",
//		G_CALLBACK (_e2_treedlg_key_press_cb), rt);
	g_signal_connect (G_OBJECT (rt->iconview), "button-press-event",
		G_CALLBACK (_e2p_thumbs_button_press_cb), rt);
	g_signal_connect (G_OBJECT (rt->iconview), "item-activated",
		G_CALLBACK (_e2p_thumbs_item_activated_cb), view);
	g_signal_connect (G_OBJECT (rt->iconview), "selection-changed",
		G_CALLBACK (_e2p_thumbs_selection_change_cb), rt);

/*
	//by default, type-ahead searching is enabled on column 0
	gtk_tree_view_set_search_equal_func	(GTK_ICON_VIEW (tvw),
		(GtkTreeViewSearchEqualFunc)_e2_fileview_match_filename, view, NULL);
	//DnD connections
//	gtk_drag_source_set (tvw, GDK_BUTTON1_MASK, target_table, n_targets,	//-1 if XDS is last,
//		GDK_ACTION_COPY); //can't use 2 of this fn, it seems
	gtk_drag_source_set (tvw, GDK_BUTTON1_MASK | GDK_BUTTON2_MASK,
		target_table, n_targets,	//-1, //the last target (XDS) is supported for dest only
		GDK_ACTION_COPY | GDK_ACTION_MOVE | GDK_ACTION_LINK | GDK_ACTION_ASK);
	gtk_drag_dest_set (tvw, GTK_DEST_DEFAULT_MOTION | GTK_DEST_DEFAULT_DROP,
		target_table, n_targets,
		GDK_ACTION_COPY | GDK_ACTION_MOVE | GDK_ACTION_LINK | GDK_ACTION_ASK);

	g_signal_connect (G_OBJECT (tvw), "drag-begin",
		G_CALLBACK (e2_dnd_drag_begin_cb), view);
	g_signal_connect (G_OBJECT (tvw), "drag-data-get",
		G_CALLBACK (e2_dnd_drag_data_get_cb), view);
	g_signal_connect (G_OBJECT (tvw), "drag-motion",
		G_CALLBACK (e2_dnd_drag_motion_cb), view);
	g_signal_connect (G_OBJECT (tvw), "drag-leave",
		G_CALLBACK (e2_dnd_drag_leave_cb), view);
//needed if GTK_DEST_DEFAULT_DROP not set, above
//	g_signal_connect (G_OBJECT (tvw), "drag-drop",
//		G_CALLBACK (e2_dnd_drag_drop_cb), view);

	g_signal_connect (G_OBJECT (tvw), "drag-data-received",
		 G_CALLBACK(e2_dnd_drag_data_received_cb), view);
	//FIXME do these once only
//	atom_text_uri_list = gdk_atom_intern (target_table[0,0], FALSE);
//	atom_text_plain = gdk_atom_intern (target_table[1,0], FALSE);
//	atom_XdndDirectSave0 = gdk_atom_intern (target_table[2,0], FALSE);

//	g_signal_connect (G_OBJECT (tvw), "drag-data-delete",
//		G_CALLBACK (e2_dnd_drag_delete_cb), view);

	//pick up any key-bindings before the general key-press
	//BUT CHECKME order probably does not matter

	g_signal_connect (G_OBJECT (rt->iconview), "cursor-changed",
		G_CALLBACK (_e2_fileview_cursor_change_cb), view);
*/
	//relate initial size to last-used, or if first, to filepanes size
	if (window_width == -1)
		window_width = view->treeview->allocation.width;
	if (window_height == -1)
		window_height = view->treeview->allocation.height;
	gtk_window_resize (GTK_WINDOW (rt->dialog), window_width, window_height);

/*
	rt->hiddenbtn = e2_dialog_add_undefined_button_custom
		(rt->dialog, FALSE, E2_RESPONSE_USER1,
		_("_Hidden"),
		(rt->show_hidden) ? "hidden_noshow_"E2IP".png" : "hidden_show_"E2IP".png",
		_("Toggle display of hidden directories"), NULL, NULL);
*/
	rt->sortbtn = e2_dialog_add_undefined_button_custom
		(rt->dialog, FALSE, E2_RESPONSE_USER2, _("_Sort"),
		(rt->sort_order == GTK_SORT_ASCENDING) ?
		GTK_STOCK_SORT_DESCENDING : GTK_STOCK_SORT_ASCENDING, NULL, NULL, NULL);

	E2_BUTTON_CLOSE.showflags |= E2_BTN_DEFAULT; //set default button
	e2_dialog_show (rt->dialog, app.main_window, E2_DIALOG_THREADED,
		&E2_BUTTON_CLOSE, NULL);

	//get the actual data for the iconview
	LISTS_LOCK
	gboolean busy = rt->view->listcontrols.refresh_working
				 || rt->view->listcontrols.cd_working;
	LISTS_UNLOCK
	if (!busy)
		_e2p_thumbs_replace_store (rt);
	else
		g_timeout_add_full (G_PRIORITY_HIGH, 100,
			(GSourceFunc)_e2p_thumbs_wait_to_replace, rt, NULL);

	//show and select the startup row corresponding to displayed dir
//	_e2_treedlg_show_path (rt->view->dir, TRUE, rt);

	GHookList thisview_cdhook = (view == &app.pane1_view) ?
		app.pane1.hook_change_dir : app.pane2.hook_change_dir;
	e2_hook_register (&thisview_cdhook, _e2p_thumbs_change_dir_hook, rt);
//	e2_hook_register (&rt->view->hook_refresh, _e2p_thumbs_refresh_hook, rt);
	rt->refreshtimer_id =
#ifdef USE_GLIB2_14
		g_timeout_add_seconds (E2_FILESCHECK_INTERVAL_S,
#else
		g_timeout_add (E2_FILESCHECK_INTERVAL,
#endif
		(GSourceFunc) _e2p_thumbs_check_dirty, rt);

	return NULL;
}
/**
@brief show tree dialog action
This creates a thread to produce the dialog, because the directories scan
can be slow

@param from UNUSED the button, menu item etc which was activated
@param art UNUSED action runtime data

@return TRUE
*/
static gboolean _e2p_thumbs_show_action (gpointer from, E2_ActionRuntime *art)
{
	g_thread_create ((GThreadFunc) _e2p_thumbs_dialog_run, curr_view, FALSE, NULL);
	return TRUE;
}

#ifdef E2_TRANSIENTKEYS
/**
@brief function to setup default key-bindings for icon-browser dialog
This is just to provide placeholders, the actual bindings are meaningless
@param set pointer to option data struct

@return
*/
static void _e2p_thumbs_keybindings (E2_OptionSet *set)
{
	//the key name strings are parsed by gtk, and no translation is possible
	e2_option_tree_setup_defaults (set,
	g_strdup("keybindings=<"),  //internal name
	//the category string(s) here need to match the binding name
	g_strconcat(_C(11),"||||",NULL),  //_("dialogs"
	g_strconcat("\t",aname,"||||",NULL),  //_("thumbs"
	g_strconcat("\t\t|<Control>j","||",_A(119),".",_A(120),"|<Control>a",NULL),
	g_strconcat("\t\t|<Control>k","||",_A(119),".",_A(120),"|<Control>c",NULL),
	g_strdup(">"),
	NULL);
}
#endif

  /****************/
 /**** public ****/
/****************/

/**
@brief plugin initialization function, called by main program

@param p ptr to plugin data struct

@return TRUE if the initialization succeeds, else FALSE
*/
gboolean init_plugin (Plugin *p)
{
#define ANAME "thumbnailer"
	aname = _("thumbs");

	p->signature = ANAME VERSION;
	p->menu_name = _("_Thumbnail..");
	p->description = _("Display thumbnails of image files in the active pane");
	p->icon = "plugin_thumbs_"E2IP".png"; //use icon file pathname if appropriate

	if (p->action == NULL)
	{
		if (gimp_thumb_init (E2_SOFTWARE_ID, NULL))
		{
			//no need to free this
			gchar *action_name = g_strconcat (_A(10),".",aname,NULL);
			p->action = e2_plugins_action_register
			  (action_name, E2_ACTION_TYPE_ITEM, _e2p_thumbs_show_action, NULL, FALSE, 0, NULL);
			return TRUE;
		}
	}
	return FALSE;
}
/**
@brief cleanup transient things for this plugin

@param p pointer to data struct for the plugin

@return TRUE if all cleanups were completed
*/
gboolean clean_plugin (Plugin *p)
{
#ifdef E2_TRANSIENTKEYS
	gchar *category = g_strconcat (_C(11), ".", aname, NULL);	//_(dialogs.view
	e2_keybinding_unregister (category, NULL);
	g_free (category);
#endif
	gchar *action_name = g_strconcat (_A(10),".",aname,NULL);
	gboolean ret = e2_plugins_action_unregister (action_name);
	g_free (action_name);
	return ret;
}
