/* $Id: e2_plugins.c 650 2007-09-05 03:31:53Z tpgww $

Copyright (C) 2003-2007 tooar <tooar@gmx.net>
Portions copyright (C) 1999 Michael Clark.

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 src/e2_plugins.c
@brief plugins-related functions

This file contains infrastructure for running plugins, but no actual plugins.
*/
/**
\page plugin_writing creating plugins

Here's an outline of what a plugin file must contain.\n\n
<em>1.</em> A suitable header. To enable versioning by subversion, the first header line \b must have just $ Id $ (without the spaces between the $'s, they're presented here just to prevent subversion from putting version info into this text). You should also provide a copyright-assertion statement, if not for yourself, then for tooar. Finally, a licence statement. EmelFM2 is licensed under the GPL. Get advice if you need to use something else.\n\n
<em>2.</em> Includes, to access required external functions and data. The first of these is mandatory.
\code
#include "e2_plugins.h"
#include "e2_other-needed-stuff.h"
#include <other-needed-stuff.h>
\endcode \n
<em>3.</em> The function that performs what you want the plugin to do when activated. This type of function \b must take two parameters:
 - a pointer to the button, menu item etc which was activated to initiate the action
 - a pointer to an <tt>E2_ActionRuntime</tt> data struct which will provide any action argument etc

and \b must return a \c gboolean indicating whether the action succeeded. The function need not be static if it's to be used from outside the plugin.\n
For example, if you want a plugin that prints "Hello World", then you would write this function.
\code
static gboolean _e2p_hello_world (gpointer from, E2_ActionRuntime *art)
{
	e2_output_print (&app.tab, _("Hello World"), NULL, TRUE, NULL);
	return TRUE;	//or FALSE if the action was not completed successfully
}
\endcode \n
<em>4.</em> An initialisation function like the following. This function \b must be called \c init_plugin, take a <tt>Plugin *</tt> argument, and return a \c gboolean which indicates whether the initialisation succeeded.
\code
//aname must be confined to this module
static gchar *aname; //a translated component of the action-name for the plugin
#define ANAME "HelloWorld"  //part of the registered action name, no spaces, not translated

gboolean init_plugin (Plugin *p)
{
	aname = _("demonstration");
	p->signature = ANAME VERSION;  //for detecting whether the plugin is loaded
	p->menu_name = _("_Hello World");  //the name for the plugins menu item, capitalize according to HIG is best
	p->description = _("prints \"Hello World\" on the output window");  //the tooltip for the plugins menu item
	p->icon = "plugin_"ANAME"_48.png";  //a non-standard path may be prepended. Just put "" for no icon

	//sometimes we load the plugin to get the data above, but don't want to use it ...
	if (p->action == NULL)
	{
		//No need to free this string, that's done as part of the registration
		/ *If the action is to apply to active-pane selected items, the first part
		  of the name must be _A(5) which is (in english) "file", in order to make
		  the plugins context-menu work properly. And the converse, do not use
		  _A(5) unless that condition applies * /
		gchar *action_name = g_strconcat (_A(13),".",aname,NULL);
		p->action = e2_plugins_action_register (action_name, E2_ACTION_TYPE_ITEM,
			_e2p_hello_world, NULL, FALSE, 0, NULL);

		//other initialization stuff, as appropriate ...

		return TRUE; //or FALSE, if the initialisation failed
	}
	return FALSE;
}
\endcode \n
<em>5.</em> A cleanup function like the following. This function \b must be called \c unload_plugin, take a <tt>Plugin *</tt> argument, and return a \c gboolean which indicates whether the cleanup succeeded.
\code
static gboolean unload_plugin (Plugin *p)
{
	gchar *action_name = g_strconcat (_A(13),"."aname,NULL); //the same name as was registered
	gboolean ret = e2_plugins_action_unregister (action_name);
	g_free (action_name);
	if (ret)
	{
		//other stuff, as appropriate ...
	}
	return ret;
}
\endcode \n
Plugins may have more than one action, which requires some extra detail in the \c init_plugin and \c unload_plugin functions. Refer to the cpbar plugin code for an example of this.
*/
/**
\page plugins plugins

A specific interface is required to anable plugins to be loaded and unloaded - see \ref plugin_writing

In general, plugins use functions and data from core e2, and therefore, such plugins are specific to the version of e2 used for building ??

Plugins may use bits of each other if appropriate (but this has never been tested, current plugins have little need for this). A desired-but-not-loaded plugin will be loaded if its path/name are in config data (the code is not smart enough to find the missing plugin, otherwise). Plugins are not smart enough to auto-unload other(s), if the plugin has previously auto-loaded those other(s).

In case there is some need for a plugin that the user is not aware of, loaded plugins are ref-counted, and any user instruction to unload is obeyed only when the count allows.

Historically, each plugin could perform only a single action or command, but now, more than one can be provided in any plugin. To minimise disruption, a hacky approach to this has been used. If more than one action is available, a "parent" plugin data struct is created, and that parent has "child" data structs each with one action. The children are listed in the parent's data struct. As of now, only the cpbar plugin uses this. Refer to its code for examples of tailored initialisation and unloading.

Plugins don't have to be user-action-oriented. For example, new-version automatic upgrades of config data are performed using a plugin that is discarded immediately after use.

Plugins' configuration data are loaded into the corresponding config treestore at session-start, and that store is incrementally updated after any plugin change via a config dialog. In addition, some plugin data are stored in a list (app.plugins). That's partly a relic from emelFM1, but it enables storage of data (plugin signature and action name) not stored in the config treestore (all data there are user-editable via a config dialog, we don't want the signature or action altered, it'd be better to fix the config treeview ...) The list is also used for plugins-menu creation (but that could readily be converted to treestore data if all relevant data were there).
*/

#include "e2_plugins.h"
#include <string.h>
#include "e2_action.h"
#include "e2_output.h"
#include "e2_dialog.h"
#include "e2_filelist.h"

typedef struct _E2P_Ref
{
	gchar *name;
	guint refcount;
} E2P_Ref;

static GList *action_refs = NULL;
static GList *option_refs = NULL;

/**
@brief find member of @a list which matches @a name

@param list list of E2P_Ref's to scan
@param name name of option or action to match

@return the matching list member, or NULL if no match
*/
static GList *_e2_plugins_find_ref (GList *list, gchar *name)
{
	GList *member;
	for (member = list; member != NULL; member = member->next)
	{
		E2P_Ref *data = member->data;
		if (g_str_equal (data->name, name))
			break;
	}
	return member;
}
/**
@brief increase refcount for action/item named @a name
A list member is added when needed
@param list address of list of E2P_Ref's to scan
@param name name of option or action to ref

@return the new refcount, or 0 in case of error
*/
static guint _e2_plugins_ref (GList **list, gchar *name)
{
	guint newrefcount;
	E2P_Ref *data;
	GList *member = _e2_plugins_find_ref (*list, name);
	if (member == NULL)
	{
		data = MALLOCATE (E2P_Ref);	//too small for slice
		CHECKALLOCATEDWARN (data, return 0;)
		if (data != NULL)
		{
			*list = g_list_append (*list, data);
			data->name = g_strdup (name);
			data->refcount = 1;
			return 1;
		}
		return 0;
	}
	else
	{
		data = (E2P_Ref *)member->data;
		newrefcount = ++(data->refcount);
		return newrefcount;
	}
}
/**
@brief decrease refcount for action/item named @a name
The list member is cleared when the new count reaches 0
@param list address of list of E2P_Ref's to scan
@param name name of option or action to ref

@return the new refcount, or -1 in case of error
*/
static gint _e2_plugins_unref (GList **list, gchar *name)
{
	guint newrefcount;
	E2P_Ref *data;
	GList *member = _e2_plugins_find_ref (*list, name);
	if (member == NULL)
		return -1;
	else
	{
		data = (E2P_Ref *)member->data;
		newrefcount = data->refcount - 1;
		if (newrefcount == 0)
		{
			g_free (data->name);
			DEMALLOCATE (E2P_Ref, data);
			*list = g_list_delete_link (*list, member);
		}
		else
			data->refcount = newrefcount;
		return (gint)newrefcount;
	}
}
/* *
@brief get function address

This enables a plugin to get the address of a main-program
function that the plugin wishes to use, which allows
plugins to be version-independent
BUT coverage is too limited

@param type integer defining which function to get

@return ptr to function sought, or NULL if not found
*/
/*void *e2_plugins_api_lookup (gint type)
{
	if (type >= E2API_POINTER_COUNT)
		return NULL;
	//these are in the same order as the enerator
	//FIXME add other relevant fns
	gpointer interface [E2API_POINTER_COUNT] = {
		e2_action_register_simple,
		e2_action_unregister,
		e2_action_get,
		e2_option_get,
		e2_option_register,
		e2_filelist_disable_refresh,
		e2_filelist_enable_refresh
	};
	return interface [type];
}
*/
/**
@brief obliterate specified "non-child" plugin

The plugin's action is unregistered, any 'unload' function in the plugin
is called, then the module is dumped and memory freed
Any child plugins are cleared, also from app.plugins
Adjusts app.plugins, so is not very suited to use during a walk of that list
Any related config treestore data are not affected - any update of those
must be done before coming here
Expects BGL on/closed

@param p plugin data struct
@param force TRUE to force removal even if not cleaned up by the plugin

@return TRUE if the plugin was unloaded
*/
gboolean e2_plugins_unload1 (Plugin *p, gboolean force)
{
	printd (DEBUG, "unload plugin: %s", p->menu_name);
	gboolean (*clean)(Plugin *);
	if (p->module != NULL) //this is not a child plugin
	{
		//FIXME handle plugins that are not allowed to be unloaded
		if ((g_module_symbol (p->module, "clean_plugin", (gpointer) &clean)
			&& clean (p)) || force)
		{
			g_module_close (p->module);
			GList *member;
			Plugin *pc;
			for (member = p->child_list; member != NULL; member = member->next)
			{
				pc = member->data;
				if (pc->cleanflags & E2P_CLEANICON)
					g_free (pc->icon);
				if (pc->cleanflags & E2P_CLEANLABEL)
					g_free (pc->menu_name);
				if (pc->cleanflags & E2P_CLEANTIP)
					g_free (pc->description);
				DEALLOCATE (Plugin, pc);
				app.plugins = g_list_remove (app.plugins, pc);
			}
			if (p->child_list != NULL)
				g_list_free (p->child_list);
			if (p->cleanflags & E2P_CLEANICON)
				g_free (p->icon);
			if (p->cleanflags & E2P_CLEANLABEL)
				g_free (p->menu_name);
			if (p->cleanflags & E2P_CLEANTIP)
				g_free (p->description);
			DEALLOCATE (Plugin, p);
			return TRUE;
		}
		gchar *msg = g_strdup_printf ("%s \"%s\"", _("Cannot unload plugin"),
			p->menu_name);
		e2_output_print_error (msg, TRUE);
	}
	return FALSE;
}
/**
@brief unload all plugins
To the extent possible, each plugin listed in app.plugins is removed, and the
list itself is cleared.
Related config treestore data for plugins are not affected.
@param force TRUE to force removal even if not cleaned up by the plugin

@return
*/
void e2_plugins_unload_all (gboolean force)
{
	gboolean (*clean)(Plugin *);
	Plugin *p;
	GList *member;
	for (member = app.plugins; member != NULL; member = member->next)
	{
		//FIXME handle plugins that are not allowed to be unloaded
		p = (Plugin *)member->data;
		if (p != NULL //not a child plugin already cleared
			&& p->module != NULL) //not a child plugin to be cleared
		{
			GList *childp, *mainmember;
			Plugin *pc;
			if ((g_module_symbol (p->module, "clean_plugin", (gpointer) &clean)
				&& clean (p)) || force)
			{
				g_module_close (p->module);
				for (childp = p->child_list; childp != NULL; childp = childp->next)
				{
					pc = childp->data;
					if (pc->cleanflags & E2P_CLEANICON)
						g_free (pc->icon);
					if (pc->cleanflags & E2P_CLEANLABEL)
						g_free (pc->menu_name);
					if (pc->cleanflags & E2P_CLEANTIP)
						g_free (pc->description);
					mainmember = g_list_find (app.plugins, pc);
					if (mainmember != NULL)
						mainmember->data = NULL;	//avoid double-cleans
					DEALLOCATE (Plugin, pc);	//FIXME leaks strings
				}
				if (p->child_list != NULL)
					g_list_free (p->child_list);
				if (p->cleanflags & E2P_CLEANICON)
					g_free (p->icon);
				if (p->cleanflags & E2P_CLEANLABEL)
					g_free (p->menu_name);
				if (p->cleanflags & E2P_CLEANTIP)
					g_free (p->description);

				DEALLOCATE (Plugin, p);
				member->data = NULL;
			}
		}
	}
	app.plugins = g_list_remove_all (app.plugins, NULL);
	if (app.plugins != NULL)
		printd (WARN, "Some plugin(s) data not cleared");
}
/* *
@brief cleanup all plugins data

@return
*/
/*uncomment this if proper session-end cleanups are implemented
void e2_plugins_clean (void)
{
	GList *member;
	for (member = app.plugins; member != NULL; member = member->next)
	{
		if (member->data != NULL)
		{
			//FIXME some strings in plugin data struct may sometimes be allocated
			DEALLOCATE (Plugin, member->data);
		}
	}
}
*/
/**
@brief open a specified plugin, if possible

@param filepath path+name string for the plugin, probably localised (API doc is silent)

@return plugin data struct for the loaded plugin, or NULL if plugin not found or has no initialize fn
*/
Plugin *e2_plugins_open1 (gchar *filepath)
{
	printd (DEBUG, "e2_plugins_open1: %s", filepath);

	gboolean (*init)(Plugin *);
	GModule *module = g_module_open (filepath, 0);
	if (module == NULL)
	{
		printd (DEBUG, "failed to open plugin file: %s", g_module_error());
		return NULL;
	}

	if (!g_module_symbol (module, "init_plugin", (gpointer)&init))
	{
		printd (DEBUG, "no initialise-function in plugin file: %s", g_module_error());
		return NULL;
	}

	Plugin *p = ALLOCATE0 (Plugin);
	CHECKALLOCATEDWARN (p, return NULL;)
	p->module = module;
	p->plugin_init = init;

	return p;
}
/**
@brief process child UI data into config store and cleanup
This does not affect app.plugins
@param model model for plugins config treestore
@param iter pointer to parent-iter in @a model
@param p pointer to data struct for parent plugin

@return
*/
void e2_plugins_store_child_data (GtkTreeModel *model, GtkTreeIter *iter, Plugin *p)
{
	if (p->child_list != NULL)
	{
		GtkTreeIter iter2;
		GList *member;
		for (member = p->child_list; member != NULL; member = member->next)
		{
			E2_Sextet *uidata = (E2_Sextet *)member->data;
			gtk_tree_store_insert_before (GTK_TREE_STORE (model), &iter2, iter, NULL);
			//loaded flag is irrelevant for child plugins, set to match on_menu
			gtk_tree_store_set (GTK_TREE_STORE (model), &iter2,
				0, p->show_in_menu, 1, p->show_in_menu, 2, uidata->a,
				3, uidata->b, 4, uidata->c, 5, "", 6, uidata->d, -1);
			e2_utils_sextet_destroy (uidata);
		}
		g_list_free (p->child_list);	//finished interim usage
		p->child_list = NULL;
	}
}
/**
@brief check if plugin data @a child has signature @a signature

@param child pointer to child plugin data struct
@param signature string to be found in @a child

@return 0 if @a child has signature @a signature
*/
static gint _e2_plugins_match_child (Plugin *child, gchar *signature)
{
	return (!g_str_equal (child->signature, signature));
}
/**
@brief process "child plugin" data when a plugin has > 1 action
As needed, this updates the config treestore data, and appends "child" plugins
to app.plugins
It doesn't matter whether or not the store already has child iters for @a iter
If there are child iters, they are checked for validity and cleaned
If there are no child iters, the data in @a p 's child-list are used to populate
the store
If there are no children for the plugin, any child store iter is removed
@param model treemodel for the plugins config data
@param iter ptr to iter referring to model/store row for parent plugin being processed
@param p ptr to parent-plugin data corresponding to @a iter
@param loaded TRUE if @a p represents a plugin that is being loaded, FALSE if just logging the data

@return
*/
static void _e2_plugins_record_children (GtkTreeModel *model, GtkTreeIter *iter,
	Plugin *p)
{
	GtkTreeIter iter2;
	GList *member;
	Plugin *pc;
	//check for multiple-commands in the plugin
	if (gtk_tree_model_iter_children (model, &iter2, iter))
	{	//there are child row(s) in the config treestore (whether or not still relevant)
		if (p->child_list != NULL)	//children recorded (in plugin setup)
		{	//multiple actions in the plugin, described by a list of Plugins
	/*reconcile model data with list ...
	(the number may be < = >, any may be bogus after user editing, and
	the order may be different)
	principle is - treestore data ORDER prevails over plugin init data in p->child_list
	*/
			GList *reordered = NULL;	//transfer matches from child_list to here
			do
			{
				gchar *childsig;
				gtk_tree_model_get (model, &iter2, 6, &childsig, -1); //from original p->signature
				//plugin init creates p->child_list in same order as child-signature-index
				//that index is a string of the form "n-ANAME" where n= 0,1, ...
				//but in the config store, the lines can be in any order
				member = g_list_find_custom (p->child_list, childsig,
					(GCompareFunc)_e2_plugins_match_child);
				if (member != NULL) //found a child plugin matching the store iter (member->data = NULL handled in search func))
				{
					gboolean childon;
					gtk_tree_model_get (model, iter, 1, &childon, -1);
					if (childon)	//parent is on-menu
						gtk_tree_model_get (model, &iter2, 1, &childon, -1); //keep original value
					else
						p->show_in_menu = FALSE;
					pc = (Plugin *)member->data;
					gtk_tree_store_set (GTK_TREE_STORE (model), &iter2,
						0, TRUE,	//for cosmetic reasons only, set this flag same as for parent
						1, childon,
						5, "",	//clear any default filename string
						//this repetition not really needed, but just in case the user edited the sig ...
						6, pc->signature,	//formatted as n-ANAME, n=0,1,....
						-1);
					pc->show_in_menu = childon;
					if (pc->cleanflags & E2P_CLEANICON)
						g_free (pc->icon);
					if (pc->cleanflags & E2P_CLEANLABEL)
						g_free (pc->menu_name);
					if (pc->cleanflags & E2P_CLEANTIP)
						g_free (pc->description);
					gtk_tree_model_get (model, &iter2,
//						1, &pc->show_in_menu,
						2, &pc->menu_name,
						3, &pc->icon,
						4, &pc->description,
						-1);
					pc->cleanflags = E2P_CLEANALL;
					//progressively build replacement list in same order as config data
					reordered = g_list_append (reordered, pc);
				}
				else	//no matching child plugin for this row
				{
					if (gtk_tree_store_remove (GTK_TREE_STORE (model), &iter2))
						//counter the loop-end iter_next
						e2_tree_iter_previous (model, &iter2);
				}
				g_free (childsig);
			} while (gtk_tree_model_iter_next (model, &iter2));
			//now add any additional plugin's data to reordered list and to treestore
			for (member = p->child_list; member != NULL; member = member->next)
			{
				pc = (Plugin *)member->data;
				if (!g_list_find (reordered, pc))
				{	//this one not already processed
					pc->show_in_menu = FALSE;
					reordered = g_list_append (reordered, pc);
#ifdef USE_GTK2_10
					gtk_tree_store_insert_with_values (GTK_TREE_STORE (model), &iter2, iter, -1,
#else
					gtk_tree_store_append (GTK_TREE_STORE (model), &iter2, iter);
					gtk_tree_store_set (GTK_TREE_STORE (model), &iter2,
#endif
						0, TRUE,	//just for config dialog cosmetics
						1, FALSE,
						2, (pc->menu_name != NULL) ? pc->menu_name : "",
						3, (pc->icon != NULL) ? pc->icon : "",
						4, (pc->description != NULL) ? pc->description : "",
						5, "",	//clear the default filename string
						//instead of path (the parent will cover that) log the matching-signature
						6, pc->signature,	//formatted as n-ANAME, n=0,1,....
						-1);
				}
			}
			g_list_free (p->child_list);	//everything now in reordered
			p->child_list = reordered;
			//append a copy, so that later plugins will not be appended to
			//this children list
			app.plugins = g_list_concat (app.plugins, g_list_copy (reordered));
		}
		else	//should be no child iters, clear them
		{
			do
			{
				gtk_tree_store_remove (GTK_TREE_STORE (model), &iter2);
			} while (gtk_tree_model_iter_children (model, &iter2, iter));
		}
	}
	else	//no child iter in the config store
		if (p->child_list != NULL)
	{	//multiple actions in the plugin, but not previously recorded
		//CHECKME create and set child signature here instead of in each plugin ?
		for (member = p->child_list; member != NULL; member = member->next)
		{
			pc = (Plugin *)member->data;
			pc->show_in_menu = p->show_in_menu;	//match the parent's setting
			gtk_tree_store_append (GTK_TREE_STORE (model), &iter2, iter);
			gtk_tree_store_set (GTK_TREE_STORE (model), &iter2,
				0, TRUE,
				1, pc->show_in_menu,
				2, (pc->menu_name != NULL) ? pc->menu_name : "",
				3, (pc->icon != NULL) ? pc->icon : "",
				4, (pc->description != NULL) ? pc->description : "",
				5, "",	//clear the default filename string
				//instead of path (the parent will cover that) provide a matching-string
				6, pc->signature,	//formatted as n-ANAME, n=0,1,....
				-1);
		}
		//append children to the main list, in the order defined by the init func
		app.plugins = g_list_concat (app.plugins, g_list_copy (p->child_list));
	}
}
/**
@brief load plugin described in plugins config-data @a model at row for @a iter

If the specified plugin file is found (at custom or default path), it is
loaded, relevant data is recorded, then the plugin is discarded if it is
not wanted.
This is to be applied only to top-level iters in @a model. Any children are
detected and processed here.

@param model treemodel for the plugins config data
@param iter ptr to iter referring to model/store row being processed

@return
*/
static void _e2_plugins_load1 (GtkTreeModel *model, GtkTreeIter *iter)
{
	void (*unload)(Plugin *);
	gboolean load_this, in_menu;
	gchar *label, *icon, *tip;	//these are for checking if currently in config
	gchar *utf_name, *utf_path, *localdir, *localpath;

	gtk_tree_model_get (model, iter, 0, &load_this, 1, &in_menu,
		2, &label, 3, &icon, 4, &tip, 5, &utf_name, 6, &utf_path, -1);

	if (*utf_path != '\0')
		localdir = F_FILENAME_TO_LOCALE (utf_path);
	else
		//no path specified, so use the default plugins path
		localdir = PLUGINS_DIR;	//localised

#ifdef E2_VFS
	VPATH data = { localdir, NULL };	//only local dirs for plugins
	if (e2_fs_is_dir3 (&data E2_ERR_NONE()))	//only local places for plugins
#else
	if (e2_fs_is_dir3 (localdir E2_ERR_NONE()))	//only local places for plugins
#endif
	{
		gchar *file_name = F_FILENAME_TO_LOCALE (utf_name);
		localpath = g_build_filename (localdir, file_name, NULL);
		F_FREE (file_name);

		if (load_this || in_menu //this is one we want to keep
			|| *label == '\0')	//no data logged CHECKME is this test ok ?
		{
			Plugin *p;
			if ((p = e2_plugins_open1 (localpath)) != NULL)
			{
				if (load_this || in_menu)
				{
	//				p->api_lookup = e2_plugins_api_lookup;
					p->action = NULL; //ensure "real" init
					if (p->plugin_init (p))
					{
						//update the UI data if need be
						p->show_in_menu = in_menu;
						if (*label == '\0' && p->menu_name != NULL)
							gtk_tree_store_set (GTK_TREE_STORE (model), iter,
								2, p->menu_name, -1);
						else if (p->menu_name == NULL || !g_str_equal (p->menu_name, label))
						{
							if (p->menu_name != NULL && (p->cleanflags & E2P_CLEANLABEL))
								g_free (p->menu_name);
							p->menu_name = g_strdup (label);
							p->cleanflags |= E2P_CLEANLABEL;
						}
						if (*icon == '\0' && p->icon != NULL)
							gtk_tree_store_set (GTK_TREE_STORE (model), iter,
								3, p->icon, -1);
						else if (p->icon == NULL || !g_str_equal (p->icon, icon))
						{
							if (p->icon != NULL && (p->cleanflags & E2P_CLEANICON))
								g_free (p->icon);
							p->icon = g_strdup (icon);
							p->cleanflags |= E2P_CLEANICON;
						}
						if (*tip == '\0' && p->description != NULL)
							gtk_tree_store_set (GTK_TREE_STORE (model), iter,
								4, p->description, -1);
						else if (p->description == NULL || !g_str_equal (p->description, tip))
						{
							if (p->description != NULL && (p->cleanflags & E2P_CLEANTIP))
								g_free (p->description);
							p->description = g_strdup (tip);
							p->cleanflags |= E2P_CLEANTIP;
						}

						app.plugins = g_list_append (app.plugins, p); //parent listed before any children

						if (p->child_list == NULL)
						{	//no child plugin(s)
							if (*tip == '\0' && p->description != NULL)
								gtk_tree_store_set (GTK_TREE_STORE (model), iter,
									4, p->description, -1);
							else if (p->description == NULL || !g_str_equal (p->description, tip))
								p->description = g_strdup (tip);
							//FIXME if there were children before, make sure gone
							//from store too
						}
						else
						{
							//when child plugin(s) exist, parent tip won't show in menu
							 //so make submenu item's tip blank
							gtk_tree_store_set (GTK_TREE_STORE (model), iter, 4, "", -1);
							//setup children data
							_e2_plugins_record_children (model, iter, p);
						}
					}
					else	//init function failed
					{
						printd (WARN, "can't initialize plugin: %s", utf_name);
						//gboolean QQQQcheckmestore;
						//CHECKME does this also remove any children from store
						gtk_tree_store_remove (GTK_TREE_STORE (model), iter);
						if (g_module_symbol (p->module, "clean_plugin", (gpointer) &unload))
						//CHECKME handle case where want to retain ??
							unload (p);
						g_module_close (p->module);
						DEALLOCATE (Plugin, p);
					}
				}
				else
				{	//this is one we don't want but we do want its data for the config dialog
					p->action = GINT_TO_POINTER (1); //set to non-NULL to just get config data
					//if (
						p->plugin_init (p);
					//)
					//{
						if (p->menu_name != NULL)
							gtk_tree_store_set (GTK_TREE_STORE (model), iter,
								2, p->menu_name, -1);
						if (*icon == '\0' && p->icon != NULL)
							gtk_tree_store_set (GTK_TREE_STORE (model), iter,
								3, p->icon, -1);
						if (*tip == '\0')
						{
							const gchar *faketip = (p->description != NULL) ?
								p->description : _("Plugin not loaded");	//some extra user info
							gtk_tree_store_set (GTK_TREE_STORE (model), iter,
								4, faketip, -1);
						}
						//get store and list data for children, if any
						p->show_in_menu = FALSE;	//set flags in store
						e2_plugins_store_child_data (model, iter, p);
						//junk it again, now, if not otherwise wanted
						//e2_plugins_unload1 (p, FALSE);
						g_module_close (p->module);
						DEALLOCATE (Plugin, p);
/*					}
					else
					{	//junk unitialised module
						g_module_close (p->module);
						DEALLOCATE (Plugin, p);
					} */
				}
			}
			else	//plugin file not loaded
			{
				printd (WARN, "plugin doesn't exist: %s", utf_name);
				gtk_tree_store_remove (GTK_TREE_STORE (model), iter);
			}
		}
		g_free (localpath);
	}
	else //no valid dir for the plugin
	{
		printd (WARN, "plugin dir doesn't exist: %s", utf_path);
		gtk_tree_store_remove (GTK_TREE_STORE (model), iter);
	}
	if (*utf_path != '\0')
		F_FREE (localdir);
	g_free (label);
	g_free (icon);
	g_free (tip);
	g_free (utf_name);
	g_free (utf_path);
}
/**
@brief load all plugins that are recorded in plugins config data

Plugins that are marked for loading are registered and kept.
For the rest, we just get missing data to show in the config dialog, then discard

@return
*/
void e2_plugins_load_all (void)
{
	E2_OptionSet *set = e2_option_get_simple ("plugins");
	GtkTreeIter iter;

	if (set != NULL &&
		gtk_tree_model_get_iter_first (set->ex.tree.model, &iter))
	{
		do
		{
			_e2_plugins_load1 (set->ex.tree.model, &iter);
		} while (gtk_tree_model_iter_next (set->ex.tree.model, &iter));

	}
	else
		printd (WARN, "plugins config doesn't exist");
}

/**
@brief update loaded plugins

This is the 'apply' fn for the plugins dialog, also used in general config dialog
It performs a differential load/unload relative to the status quo
Status quo is determined by checking menu name, which is in both Plugin and model
( so if a user edits the menu name, the plugin will be improperly, but not fatally,
reloaded )

@return
*/
void e2_plugins_update (void)
{
	gchar *icon, *name, *tip;
	gboolean load, menu, freestrings;
	GList *oldlist, *member;
	Plugin *p;
	E2_OptionSet *set = e2_option_get_simple ("plugins");
	GtkTreeIter iter, iter2;
	if (gtk_tree_model_get_iter_first (set->ex.tree.model, &iter))
	{
		oldlist = app.plugins;	//work with copy in case store row(s) deleted
		app.plugins = NULL;
		do
		{
			freestrings = TRUE;
			gtk_tree_model_get (set->ex.tree.model, &iter,
				0, &load, 1, &menu, 2, &name, 3, &icon, 4, &tip, -1);
			if (menu && !load) //fix any mistake by user
			{
				gtk_tree_store_set (GTK_TREE_STORE (set->ex.tree.model), &iter,
					0, TRUE, -1);
				load = TRUE;
			}
			//check if this one is already in play
			//FIXME menu_name is not a reliable indicator. Enable the signature to
			//be stored in treestore, but not displayed so user can't change it
			//(it's already stored, in column 6, for child plugins, but that's editable)
			if (name == NULL)
				member = NULL;
			else
			{
				for (member = oldlist; member != NULL; member = member->next)
				{
					if (g_str_equal (name, ((Plugin *)member->data)->menu_name))
						break;
				}
			}

			if (load && member != NULL)
			{	//this one is to keep
				oldlist = g_list_remove_link (oldlist, member);
				app.plugins = g_list_concat (app.plugins, member);
				p = (Plugin *)member->data;
				//update on-menu status in case was edited
				p->show_in_menu = menu;
				//update strings in case they were edited
				if (p->icon != NULL && (p->cleanflags & E2P_CLEANICON))
					g_free (p->icon);
				p->icon = icon;
				p->cleanflags |= E2P_CLEANICON;
				if (p->menu_name != NULL && (p->cleanflags & E2P_CLEANLABEL))
					g_free (p->menu_name);
				p->menu_name = name;
				p->cleanflags |= E2P_CLEANLABEL;
				if (p->description != NULL && (p->cleanflags & E2P_CLEANTIP))
					g_free (p->description);
				p->description = tip;
				p->cleanflags |= E2P_CLEANTIP;
				freestrings = FALSE;
				//process any child plugins
				if (p->child_list != NULL)
				{
					/*_e2_plugins_record_children() expects the child plugins
					not to be in app.plugins, and the parent plugin to be at
					the end of app.plugins. Should not be any child back in
					app.plugins yet, but in case the user has moved a child
					badly out of order ... */
					GList *childmember;
					for (childmember = p->child_list; childmember != NULL;
						childmember = childmember->next)
						app.plugins = g_list_remove_all (app.plugins, childmember->data);

					_e2_plugins_record_children (set->ex.tree.model, &iter, p);
				}
			}
			else	// !load and/or member == NULL
				if (member != NULL) //we need to unload this one if we can
			{
				//process any child plugins' flags before dumping
				p = (Plugin *)member->data;
				if (p->child_list != NULL)
				{
					GList *node;
					for (node = p->child_list; node != NULL; node = node->next)
					{
						p = (Plugin *)node->data;
						iter2 = iter;
						if (e2_tree_find_iter_from_str_simple (set->ex.tree.model,
							6, p->signature, &iter2, FALSE))
							gtk_tree_store_set (GTK_TREE_STORE (set->ex.tree.model),
								&iter2, 0, FALSE, 1, FALSE, -1);
					}
				}
				if (e2_plugins_unload1 (member->data, FALSE))
					oldlist = g_list_delete_link (oldlist, member);
				else
					//some strange unload problem, revert the status
					gtk_tree_store_set (GTK_TREE_STORE (set->ex.tree.model),
						&iter, 0, TRUE,-1);
					//TOO BAD ABOUT ANY CHILD PLUGINS' FLAGS, HERE
			}
			else	//member == NULL, load = ?
				if (load) //we need to load this one
			{
				_e2_plugins_load1 (set->ex.tree.model, &iter);
			}
			if (freestrings)
			{
				g_free (name);
				g_free (icon);
				g_free (tip);
			}
		} while (gtk_tree_model_iter_next (set->ex.tree.model, &iter));

		if (oldlist != NULL)
		{	//process plugins which are loaded but deleted from treestore
			for (member = oldlist; member != NULL; member = member->next)
			{
				e2_plugins_unload1 ((Plugin *)member->data, TRUE);
			}
			g_list_free (oldlist);
		}
	}
	else	//nothing in the store
		e2_plugins_unload_all (FALSE);	//cleanup any existing runtime data
}
/**
@brief get the list of installed plugins

This is intended to be used by plugins themselves, in case they
want to interact

@return pointer to glist, in which each ->data is a Plugin* for a loaded plugin
*/
GList *e2_plugins_get_list (void)
{
	return app.plugins;
}
/**
@brief check whether plugin with specified @a signature is loaded

This is intended to be used by plugins, if they want to interact

@param signature unique name string for the desired plugin

@return pointer to data struct for plugin which has the specified ID, else NULL
*/
Plugin *e2_plugins_check_installed (const gchar *signature)
{
	GList *tmp;
	Plugin *p;
	for (tmp = app.plugins; tmp != NULL; tmp = tmp->next)
	{
		p = tmp->data;
		if (g_str_equal (p->signature, signature))
			return p;
	}
	return NULL;
}
/**
@brief load plugin with specified @a signature, if it's not loaded already

@param signature unique ID string for the desired plugin, may be localised

@return pointer to data struct for plugin which has the specified ID, else NULL
*/
Plugin *e2_plugins_load_plugin (const gchar *signature)
{
	Plugin *p = e2_plugins_check_installed (signature);
	if (p == NULL)
	{
		gchar *ppath, *pname, *local, *s;

		ppath = NULL;
		s = strstr (signature, VERSION);
		s = g_strndup (signature, s-signature);
		pname = g_strconcat ("e2p_", s, ".so", NULL);
		g_free (s);
		GtkTreeIter iter;
		E2_OptionSet *set = e2_option_get ("plugins");
		GtkTreeModel *mdl = set->ex.tree.model;
		gboolean inconfig = FALSE;
		if (gtk_tree_model_get_iter_first (mdl, &iter))
		{
			if (e2_tree_find_iter_from_str_same (mdl, 5, pname, &iter))
			{
				inconfig = TRUE;
				gtk_tree_model_get (mdl, &iter, 6, &ppath, -1);
				if (ppath != NULL && *ppath != '\0')
				{	//plugin is recorded in config data
					s = ppath;
					local = F_FILENAME_TO_LOCALE (ppath);
					ppath = g_build_filename (local, pname, NULL); //create path from config data
					g_free (s);
					F_FREE (local);
				}
				else if (ppath != NULL)
				{
					g_free (ppath);
					ppath = NULL;
				}
			}
		}
		if (ppath == NULL)
		{	//try to get from default place
			ppath = e2_utils_strcat (PLUGINS_DIR G_DIR_SEPARATOR_S, pname);	//localised path
		}
		p = e2_plugins_open1 (ppath);
		g_free (ppath);
		if (p != NULL)
		{
			if (!p->plugin_init (p))
			{
				printd (ERROR, "Can't initialize plugin %s", pname);
				e2_plugins_unload1 (p, FALSE);
				p = NULL;
			}
			if (inconfig)
				gtk_tree_store_set (GTK_TREE_STORE (set->ex.tree.model), &iter, 0, TRUE, -1);
			else
			{
				//add to config data
				const gchar *label = (p->menu_name != NULL) ? p->menu_name : "";
				const gchar *icon = (p->icon != NULL) ? p->icon : "";
				const gchar *tip = (p->description != NULL) ? p->description : "";
				gchar *name = F_FILENAME_FROM_LOCALE (pname);
#ifdef USE_GTK2_10
				gtk_tree_store_insert_with_values (GTK_TREE_STORE (set->ex.tree.model),
					&iter, NULL, -1,
#else
				gtk_tree_store_insert (GTK_TREE_STORE (set->ex.tree.model), &iter, NULL, -1);
				gtk_tree_store_set (GTK_TREE_STORE (set->ex.tree.model), &iter,
#endif
					//if path is not already in config, then it must be default
					0, TRUE, 1, FALSE, 2, label, 3, icon, 4, tip, 5, name, 6, "", -1);

				F_FREE (name);
			}
		}
		else
			printd (ERROR, "Can't find plugin %s", pname);
	}
	return p;
}

/**
@brief find address of a function in another plugin

This is intended to be used by main code or other plugins which want to interact.
If the plugin with the desired function is not presently loaded, any unloaded
plugins recorded in config data are polled for a matching signature, and if so,
the plugin will be loaded

@param signature unique name string for the desired (parent) plugin
@param func_name the name of the function to find
@param address store for the located address

@return TRUE if the address is found
*/
gboolean e2_plugins_find_function (const gchar *signature, const gchar *func_name,
	gpointer *address)
{
	Plugin *p = e2_plugins_load_plugin (signature);
	if (p == NULL)
		return FALSE;
	if (!g_module_symbol (p->module, func_name, address))
	{
		printd (WARN, "couldn't find %s in module %s: %s", func_name, signature,
			g_module_error());
		return FALSE;
	}
	return TRUE;
}
/**
@brief create "child" Plugin data for use when @a parent has > 1 action

@param parent pointer to the parent

@return created child plugin, or NULL if error
*/
Plugin *e2_plugins_create_child (Plugin *parent)
{
	Plugin *p = ALLOCATE0 (Plugin);
	CHECKALLOCATEDWARN (p, return NULL;)
	p->show_in_menu = parent->show_in_menu;	//may be varied later via config dialog
	parent->child_list = g_list_append (parent->child_list, p);
	return p;
}
/**
@brief run a dialog showing plugins config data

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

@return TRUE if the dialog was established
*/
gboolean e2_plugins_configure (gpointer from, E2_ActionRuntime *art)
{
	return (
	e2_config_dialog_single ("plugins", e2_plugins_update, TRUE) //internal name, no translation
	!= NULL);
}
/**
@brief register plugin action

This is a wrapper for the standard action registration function
It also manages a refcount for the action

@param name freeable string with possible '.' separating 'parent/ and 'child' parts of the action
@param type flags for one of: item (?) submenu, file_actions, custom_command, plugins, separator, tear_off_menu, toggle, command_line, bookmarks, dummy
@param func the function to be called when the action is run
@param data pointer to data to be supplied to @a func, or NULL
@param has_arg boolean flag signalling whether ??WHAT has argument(s)
@param exclude flags indicating whether to exclude this action from toolbars, menus &| keybindings
@param data2 additional data needed by the action, or NULL

@return registered action E2_Action
*/
E2_Action *e2_plugins_action_register (gchar *name, E2_ACTION_TYPE type,
	void *func, gpointer data, gboolean has_arg, E2_ACTION_EXCLUDE exclude,
	gpointer data2)
{
	E2_Action *action;
	guint refcount = _e2_plugins_ref (&action_refs, name);
	if (refcount == 1)
		action = e2_action_register (name, type, func, data, has_arg,
			exclude, data2);
	else
		action = e2_action_get (name);
	return action;
}
/**
@brief unregister plugin action named @a name
This is a wrapper for the standard action deregistration function.
It also manages a refcount for the action.
@param name action name string

@return TRUE if the action was actually unregistered
*/
gboolean e2_plugins_action_unregister (gchar *name)
{
	guint refcount = _e2_plugins_unref (&action_refs, name);
	if (refcount == 0)
		return (e2_action_unregister (name));
	else
		return FALSE;
}
/**
@brief perform a plugin's action
This is the callback for all plugins-menu items
@param menu_item the activated item
@param action plugin-action data struct

@return
*/
void e2_plugins_do_action (GtkWidget *menu_item, E2_Action *action)
{
	//pdata this will generally be NULL, tho' action->data may be relevant
	gpointer pdata = g_object_get_data (G_OBJECT (menu_item), "e2-action-data");
	E2_ActionRuntime *rt = e2_action_pack_runtime (action, pdata, NULL);
	e2_action_run (menu_item, rt);	//completion code ignored
	e2_action_free_runtime (rt);
}
/**
@brief register a plugin config option
This is a wrapper for the standard option registration function
It also manages a refcount for the option

@param type flag for the type of set that it is
@param name name of the option, a constant string
@param group group the option belongs to, used in config dialog, a r-t string  FREEME
@param desc textual description of the option used in config dialog, a r-t _() string FREEME ?
@param tip tooltip used when displaying the config dialog, a _() string, or NULL
@param depends name of another option this one depends on, or NULL
@param flags bitflags determining how the option data will be handled

@return the option data struct
*/
E2_OptionSet *e2_plugins_option_register (E2_OptionType type, gchar *name,
	gchar *group, gchar *desc, gchar *tip, gchar *depends, E2_OptionFlags flags)
{
	E2_OptionSet *set;
	guint refcount = _e2_plugins_ref (&option_refs, name);
	if (refcount == 1)
		set = e2_option_register (type, name, group, desc, tip, depends, flags);
	else
		set = e2_option_get (name);
	return set;
	//NOTE caller needs special care for tree-options
}
/**
@brief unregister plugin option named @a name
This is a wrapper for the standard option deregistration function
It also manages a refcount for the option
@param name option name string

@return TRUE if the option was actually unregistered
*/
gboolean e2_plugins_option_unregister (gchar *name)
{
	guint refcount = _e2_plugins_unref (&option_refs, name);
	if (refcount == 0)
		return (e2_option_unregister (name));
	else
		return FALSE;
}
/**
@brief install default tree options for plugins
This function is called only if the default is missing from the config file
This is essentially a list of all the expected plugin filenames
the menu name and description at collected from the plugin file
the 'path' field at the end is empty if the plugins default dir is to be used
@param set pointer to set data

@return
*/
static void _e2_plugins_tree_defaults (E2_OptionSet *set)
{
	e2_option_tree_setup_defaults (set,
	g_strdup("plugins=<"), //internal name
	g_strdup ("true|true||||e2p_glob.so|"),
	g_strdup ("true|true||||e2p_rename_ext.so|"),
	g_strdup ("true|true||||e2p_for_each.so|"),
	g_strdup ("true|true||||e2p_pack.so|"),
	g_strdup ("true|true||||e2p_names_clip.so|"),
	g_strdup ("false|false||||e2p_sort_by_ext.so|"),
	g_strdup ("true|true||||e2p_du.so|"),
	g_strdup ("true|false||||e2p_unpack.so|"),
	g_strdup ("true|true||||e2p_find.so|"),
	g_strdup ("false|false||||e2p_track.so|"),
	g_strdup ("false|false||||e2p_cpbar.so|"),
	g_strdup ("false|false||||e2p_mvbar.so|"),
	g_strdup ("false|false||||e2p_times.so|"),
	g_strdup ("false|false||||e2p_dircmp.so|"),
	g_strdup ("false|false||||e2p_config.so|"),
	g_strdup ("false|false||||e2p_clone.so|"),
	g_strdup ("false|false||||e2p_view.so|"),
#ifdef E2_VFS
	g_strdup ("false|false||||e2p_vfs.so|"),
	g_strdup ("false|false||||e2p_gvfs.so|"),
#endif
	g_strdup(">"),
	NULL);
	//FIXME setup some mapping between filename and signature name to enable
	//lookup by signature
}
/**
@brief register plugins actions

@return
*/
void e2_plugins_options_register (void)
{
	//no screen rebuilds needed after any change to this option
	gchar *group_name = _C(33); //_("plugins")
	E2_OptionSet *set = e2_option_tree_register ("plugins", group_name,
		group_name, NULL, NULL, NULL,
		E2_OPTION_TREE_LIST | E2_OPTION_TREE_UP_DOWN | E2_OPTION_TREE_ADD_DEL,
		E2_OPTION_FLAG_ADVANCED | E2_OPTION_FLAG_BUILDPLUGS);
	e2_option_tree_add_column (set, _("Loaded"), E2_OPTION_TREE_TYPE_BOOL, TRUE, "true",
		0, NULL, NULL);
	e2_option_tree_add_column (set, _("Menu"), E2_OPTION_TREE_TYPE_BOOL, TRUE, "true",
		0, NULL, NULL);
	e2_option_tree_add_column (set, _("Label"), E2_OPTION_TREE_TYPE_STR, 0, "",
		0, NULL, NULL);
	e2_option_tree_add_column (set, _("Icon"), E2_OPTION_TREE_TYPE_ICON, 0, "",
		0, NULL, NULL);
	e2_option_tree_add_column (set, _("Tooltip"), E2_OPTION_TREE_TYPE_STR, 0, "",
		0, NULL, NULL);
	e2_option_tree_add_column (set, _("Filename"), E2_OPTION_TREE_TYPE_STR, 0, "?.so",
		0, NULL, NULL);
	e2_option_tree_add_column (set, _("Path"), E2_OPTION_TREE_TYPE_STR, 0, "",
		0, NULL, NULL);
	e2_option_tree_create_store (set);

	e2_option_tree_prepare_defaults (set, _e2_plugins_tree_defaults);
}
