/* $Id$ Copyright (C) 2003-2012 tooar 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, see http://www.gnu.org/licenses. */ /** @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 1. 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 2. 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 \endcode \n 3. 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 E2_ActionRuntime 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 4. An initialisation function like the following. This function \b must be called \c init_plugin, take a Plugin * 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(6) which is (in english) "file", in order to make the plugins context-menu work properly. And the converse, do not use _A(6) unless that condition applies * / E2_Action plugact = {g_strconcat (_A(14),".",aname,NULL),_e2p_hello_world,FALSE,0,NULL,NULL}; p->action = e2_plugins_action_register (&plugact); if (G_LIKELY(p->action != NULL)) { //other initialization stuff, as appropriate ... return TRUE; } g_free (plugact.name); } return FALSE; } \endcode \n 5. A cleanup function like the following. This function \b must be called \c unload_plugin, take a Plugin * 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(14),"."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 #include "e2_action.h" #include "e2_output.h" #include "e2_dialog.h" #include "e2_filelist.h" //show plugin file path and name in plugins GUI //#define SHOW_PLUGPATHS //plugins config treestore columns enum { LOAD_COL, MENU_COL, LABEL_COL, ICON_COL, TIP_COL, FILE_COL, PATH_COL, SIG_COL }; 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 (!strcmp (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 #if (CHECKALLOCATEDWARN) CHECKALLOCATEDWARN (data, return 0;) #else if (data != NULL) { #endif *list = g_list_append (*list, data); data->name = g_strdup (name); data->refcount = 1; return 1; #if !(CHECKALLOCATEDWARN) } return 0; #endif } 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 list foreach func to check if plugin data @a p has signature @a signature See also: e2_plugins_check_installed() @param p pointer to plugin data struct @param signature string to be found @return 0 if @a p has signature @a signature */ static gint _e2_plugins_match_sig (Plugin *p, gchar *signature) { return (strcmp (p->signature, signature)); } /* * @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, 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 (for any error message display) @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_CLEANLABEL) g_free (pc->menu_name); if (pc->cleanflags & E2P_CLEANICON) g_free (pc->icon); 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 allocated 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. @ap ->child_list is cleared @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 */ static 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); gtk_tree_store_set (GTK_TREE_STORE (model), &iter2, LOAD_COL, p->show_in_menu,//loaded flag is irrelevant for child plugins, set to match parent MENU_COL, p->show_in_menu, LABEL_COL, uidata->a, ICON_COL, uidata->b, TIP_COL, uidata->c, //#ifdef SHOW_PLUGPATHS // ? or always, to cleanup old-format config data FILE_COL, "", //clear display PATH_COL, "", //#endif SIG_COL, uidata->d, -1); e2_utils_sextet_destroy (uidata); } g_list_free (p->child_list); //finished interim usage p->child_list = NULL; } } /** @brief process a plugin's UI data into config store Any child-plugin data are also handled. This does not affect app.plugins. @param model model for plugins config treestore @param iter pointer to treeiter to use for updating @a model @param p pointer to data struct for plugin @param inmenu setting for the plugin's loaded/in-menu data @param localpath localized absolute path-string for the plugin file @return */ void e2_plugins_store_data (GtkTreeModel *model, GtkTreeIter *iter, Plugin *p, gboolean inmenu, gchar *localpath) { p->action = GINT_TO_POINTER (1); //non-NULL prevents full initialisation p->plugin_init (p); const gchar *menu_name = (p->menu_name != NULL) ? p->menu_name : ""; const gchar *icon = (p->icon != NULL) ? p->icon : ""; const gchar *description = (p->description != NULL) ? p->description : NULL; gchar *local = strrchr (localpath, G_DIR_SEPARATOR); //should never fail *local = '\0'; //truncate plocal at trailing / gchar *utfn, *utfp; #ifdef SHOW_PLUGPATHS utfn = F_FILENAME_FROM_LOCALE (local + 1); //NOT DISPLAYNAME if (!strcmp (localpath, PLUGINS_DIR)) //from Makefile PLUGINS_DIR is localised, no trailer utfp = ""; else utfp = F_FILENAME_FROM_LOCALE (localpath); #else utfn = local + 1; utfp = (!strcmp (localpath, PLUGINS_DIR)) ? "" : localpath; //from Makefile PLUGINS_DIR is localised, no trailer #endif gtk_tree_store_set (GTK_TREE_STORE (model), iter, LOAD_COL, inmenu, MENU_COL, inmenu, LABEL_COL, menu_name, ICON_COL, icon, TIP_COL, description, FILE_COL, utfn, PATH_COL, utfp, SIG_COL, p->signature, -1); p->show_in_menu = inmenu; //any child-plugin uses this //process any child-plugin data too _e2_plugins_store_child_data (model, iter, p); *local = G_DIR_SEPARATOR; #ifdef SHOW_PLUGPATHS if (*utfp != '\0') F_FREE (utfp, localpath); F_FREE (utfn, local + 1); #endif } /* * @brief reconcile on-disk plugin files with plugins-config-data @return */ /*void e2_plugins_freshen_data (void) { gboolean TODO; GList *member, *loaded = NULL; GtkTreeIter iter; E2_OptionSet *set = e2_option_get ("plugins"); GtkTreeModel *mdl = set->ex.tree.model; if (gtk_tree_model_get_iter_first (mdl, &iter)) { gchar *utfpath, *utfname; do { gtk_tree_model_get (model, &iter, FILE_COL, &utfname, PATH_COL, &utfpath, -1); if (*utfpath == '\0') { #ifdef SHOW_PLUGPATHS construct localpath #endif if stat() fails { remove iter from store g_free (localpath); } else loaded = g_list_prepend (loaded, localpath); } g_free (utfname); g_free (utfpath); } while gtk_tree_model_iter_next (mdl, &iter)); } / * foreach file e2p_*.so in PLUGINS_DIR { check if it's in loaded list if not { construct full path path; open Plugin *p; if (p) { append iter to model e2_plugins_store_data (mdl, &iter, p, FALSE, path); g_module_close (p->module); DEALLOCATE (Plugin, p); } } } * / } */ /** @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, SIG_COL, &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_sig); 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, MENU_COL, &childon, -1); if (childon) //parent is on-menu gtk_tree_model_get (model, &iter2, MENU_COL, &childon, -1); //keep original value else p->show_in_menu = FALSE; pc = (Plugin *)member->data; gtk_tree_store_set (GTK_TREE_STORE (model), &iter2, LOAD_COL, TRUE, //for cosmetic reasons only, set this flag same as for parent MENU_COL, childon, #ifdef SHOW_PLUGPATHS FILE_COL, "", //clear any default filename string PATH_COL, "", //and path #endif SIG_COL, 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, // MENU_COL, &pc->show_in_menu, LABEL_COL, &pc->menu_name, ICON_COL, &pc->icon, TIP_COL, &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 LOAD_COL, TRUE, //just for config dialog cosmetics MENU_COL, FALSE, LABEL_COL, (pc->menu_name != NULL) ? pc->menu_name : "", ICON_COL, (pc->icon != NULL) ? pc->icon : "", TIP_COL, (pc->description != NULL) ? pc->description : "", #ifdef SHOW_PLUGPATHS FILE_COL, "", //clear the default filename string PATH_COL, "", //and path #endif SIG_COL, 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 #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 LOAD_COL, TRUE, MENU_COL, pc->show_in_menu, LABEL_COL, (pc->menu_name != NULL) ? pc->menu_name : "", ICON_COL, (pc->icon != NULL) ? pc->icon : "", TIP_COL, (pc->description != NULL) ? pc->description : "", #ifdef SHOW_PLUGPATHS FILE_COL, "", //clear the default filename string PATH_COL, "", //and path #endif SIG_COL, 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 *pname, *pdir, *localdir, *localpath; gtk_tree_model_get (model, iter, LOAD_COL, &load_this, MENU_COL, &in_menu, LABEL_COL, &label, ICON_COL, &icon, TIP_COL, &tip, FILE_COL, &pname, PATH_COL, &pdir, -1); if (pdir != NULL && *pdir != '\0') #ifdef SHOW_PLUGPATHS localdir = F_FILENAME_TO_LOCALE (pdir); #else localdir = pdir; #endif 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 { #ifdef SHOW_PLUGPATHS gchar *local = F_FILENAME_TO_LOCALE (pname); localpath = g_build_filename (localdir, local, NULL); F_FREE (local, pname); #else localpath = g_build_filename (localdir, pname, NULL); #endif 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, LABEL_COL, p->menu_name, -1); else if (p->menu_name == NULL || strcmp (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, ICON_COL, p->icon, -1); else if (p->icon == NULL || strcmp (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, TIP_COL, p->description, -1); else if (p->description == NULL || strcmp (p->description, tip)) { if (p->description != NULL && (p->cleanflags & E2P_CLEANTIP)) g_free (p->description); p->description = g_strdup (tip); p->cleanflags |= E2P_CLEANTIP; } gtk_tree_store_set (GTK_TREE_STORE (model), iter, SIG_COL, p->signature, -1); if (e2_plugins_check_installed (p->signature) == NULL) { 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, TIP_COL, p->description, -1); else if (p->description == NULL || strcmp (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, TIP_COL, "", -1); //setup children data _e2_plugins_record_children (model, iter, p); } } else { printd (WARN, "aborted attempt to load 2nd copy of plugin: %s", p->signature); //CHECKME does this also remove any children from store gtk_tree_store_remove (GTK_TREE_STORE (model), iter); //CHECKME effect on store iteration goto killit; } } else //init function failed { printd (WARN, "can't initialize plugin: %s", pname); //mark it as un-available (i.e. don't completely remove from store) gtk_tree_store_set (GTK_TREE_STORE (model), iter, LOAD_COL, FALSE, MENU_COL, FALSE, SIG_COL, "", -1); GtkTreeIter iter2; if (gtk_tree_model_iter_children (model, &iter2, iter)) { //no need for deeper recursion do { gtk_tree_store_set (GTK_TREE_STORE (model), &iter2, LOAD_COL, FALSE, MENU_COL, FALSE, SIG_COL, "", -1); } while (gtk_tree_model_iter_next (model, &iter2)); } killit: if (g_module_symbol (p->module, "clean_plugin", (gpointer) &unload)) 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, LABEL_COL, p->menu_name, -1); if (*icon == '\0' && p->icon != NULL) gtk_tree_store_set (GTK_TREE_STORE (model), iter, ICON_COL, 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, TIP_COL, 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 %s doesn't exist or won't load", pname); gtk_tree_store_remove (GTK_TREE_STORE (model), iter); //CHECKME effect on store iteration } } g_free (localpath); } else //no valid dir for the plugin { printd (WARN, "plugin dir doesn't exist: %s", pdir); gtk_tree_store_remove (GTK_TREE_STORE (model), iter); //CHECKME effect on store iteration } #ifdef SHOW_PLUGPATHS if (pdir != NULL && *pdir != '\0') F_FREE (localdir, pdir); #endif g_free (label); g_free (icon); g_free (tip); g_free (pname); g_free (pdir); } /** @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, *label, *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 { gchar *sig; freestrings = TRUE; gtk_tree_model_get (set->ex.tree.model, &iter, LOAD_COL, &load, MENU_COL, &menu, LABEL_COL, &label, ICON_COL, &icon, TIP_COL, &tip, SIG_COL, &sig, -1); if (menu && !load) //fix any mistake by user { gtk_tree_store_set (GTK_TREE_STORE (set->ex.tree.model), &iter, LOAD_COL, TRUE, -1); load = TRUE; } //check if this one is already in play for (member = oldlist; member != NULL; member = member->next) { if (!strcmp (sig, ((Plugin *)member->data)->signature)) break; } if (member != NULL) { if (load) { //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 = label; 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 - 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, SIG_COL, p->signature, &iter2, FALSE)) gtk_tree_store_set (GTK_TREE_STORE (set->ex.tree.model), &iter2, LOAD_COL, FALSE, MENU_COL, FALSE, SIG_COL, "", -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, LOAD_COL, 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 (label); g_free (icon); g_free (tip); } g_free (sig); } 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 *node; for (node = app.plugins; node != NULL; node = node->next) { Plugin *p = node->data; if (!strcmp (p->signature, signature)) return p; } return NULL; } /** @brief load (non-child) 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, *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, FILE_COL, pname, &iter)) { inconfig = TRUE; gtk_tree_model_get (mdl, &iter, PATH_COL, &ppath, -1); if (ppath != NULL && *ppath != '\0') { //plugin is recorded in config data s = ppath; #ifdef SHOW_PLUGPATHS gchar *local = F_FILENAME_TO_LOCALE (s); ppath = g_build_filename (local, pname, NULL); //create path from config data F_FREE (local, s); #else ppath = g_build_filename (s, pname, NULL); //create path from config data #endif g_free (s); } 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, LOAD_COL, TRUE, SIG_COL, p->signature, -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 : ""; #ifdef SHOW_PLUGPATHS gchar *name = F_FILENAME_FROM_LOCALE (pname); #endif #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 LOAD_COL, TRUE, MENU_COL, FALSE, LABEL_COL, label, ICON_COL, icon, TIP_COL, tip, #ifdef SHOW_PLUGPATHS FILE_COL, name, #else FILE_COL, pname, #endif PATH_COL, "", SIG_COL, p->signature, -1); #ifdef SHOW_PLUGPATHS F_FREE (name, pname); #endif } } 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, and may free the name of @a newaction @param newaction pointer to original action data to be copied and heaped @return registered action E2_Action */ E2_Action *e2_plugins_action_register (const E2_Action *newaction) { guint refcount = _e2_plugins_ref (&action_refs, newaction->name); E2_Action *action; if (refcount == 1) action = e2_action_register (newaction); else { action = e2_action_get (newaction->name); if (G_LIKELY(action != NULL)) //prevent double-free upstream g_free (newaction->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 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 ex pointer to type-specific data for initialisation @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_OptionSetupExtra *ex, E2_OptionFlags flags) { E2_OptionSet *set; guint refcount = _e2_plugins_ref (&option_refs, name); if (refcount == 1) { switch (type) { case E2_OPTION_TYPE_BOOL: set = e2_option_bool_register (name, group, desc, tip, depends, ex->exbool, flags); break; case E2_OPTION_TYPE_INT: set = e2_option_int_register (name, group, desc, tip, depends, ex->exint.def, ex->exint.min, ex->exint.max, flags); break; case E2_OPTION_TYPE_SEL: set = e2_option_sel_register (name, group, desc, tip, depends, ex->exsel.def, ex->exsel.values, flags); break; case E2_OPTION_TYPE_STR: set = e2_option_str_register (name, group, desc, tip, depends, ex->exstr, flags); break; // case E2_OPTION_TYPE_FONT: // set = e2_option_font_register (name, group, desc, tip, depends, // ex->exstr, flags); // break; // case E2_OPTION_TYPE_COLOR: // set = e2_option_color_register (name, group, desc, tip, depends, // ex->exstr, flags); // break; // case E2_OPTION_TYPE_ICON: // set = // break; // case E2_OPTION_TYPE_TREE: // set = // break; default: printd (ERROR, "BAD attempt to register unsupported plugin option"); return NULL; } } 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_backup (name)); else return FALSE; } /** @brief (de)sensitize option tree buttons for selected option tree row Config dialog page buttons are de-sensitized if the row is not a 'child' plugin @param selection pointer to selection @param model UNUSED @param path @param path_currently_selected @param set data struct for the plugins option UNUSED @return TRUE always (the row is always selectable) */ static gboolean _e2_plugin_tree_selection_check_cb (GtkTreeSelection *selection, GtkTreeModel *model, GtkTreePath *path, gboolean path_currently_selected, E2_OptionSet *set) { GtkTreeView *view = gtk_tree_selection_get_tree_view (selection); gboolean result = gtk_tree_path_get_depth (path) == 1; e2_option_tree_adjust_buttons (view, result); return TRUE; } /** @brief decide whether tree row is draggable Checks whether row depth is 1 i.e. not a child plugin. If so, the row is draggable @param drag_source GtkTreeDragSource data struct @param path tree path to a row on which user is initiating a drag @return TRUE if the row can be dragged */ static gboolean _e2_plugin_tree_draggable_check_cb (GtkTreeDragSource *drag_source, GtkTreePath *path) { return (gtk_tree_path_get_depth (path) == 1); } /** @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, icon and tip are retrieved from the plugin file The 'path' field near 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 ("false|false||||e2p_selmatch.so||"), g_strdup ("false|false||||e2p_tag.so||"), g_strdup ("true|true||||e2p_for_each.so||"), g_strdup ("true|true||||e2p_rename.so||"), g_strdup ("true|true||||e2p_find.so||"), #ifdef E2_TRACKER g_strdup ("false|false||||e2p_track.so||"), #endif #ifdef E2_THUMBNAILS g_strdup ("true|true||||e2p_thumbs.so||"), #endif g_strdup ("true|true||||e2p_pack.so||"), g_strdup ("true|false||||e2p_unpack.so||"), g_strdup ("true|true||||e2p_names_clip.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||"), #ifdef E2_ACL g_strdup ("false|false||||e2p_acl.so||"), #endif g_strdup ("false|false||||e2p_config.so||"), //#ifdef E2_POLKIT // g_strdup ("false|false||||e2p_privilege.so||"), //#endif g_strdup ("false|false||||e2p_clone.so||"), g_strdup ("false|false||||e2p_view.so||"), g_strdup ("false|false||||e2p_sort_by_ext.so||"), g_strdup ("true|true||||e2p_du.so||"), #ifdef E2_VFS g_strdup ("false|false||||e2p_vfs.so||"), g_strdup ("false|false||||e2p_gvfs.so||"), #endif g_strdup(">"), NULL); } /** @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(34); //_("plugins") E2_OptionSet *set = e2_option_tree_register ("plugins", group_name, group_name, NULL, _e2_plugin_tree_selection_check_cb, _e2_plugin_tree_draggable_check_cb, E2_OPTION_TREE_LIST | E2_OPTION_TREE_UP_DOWN | E2_OPTION_TREE_ADD_DEL, E2_OPTION_FLAG_ADVANCED | E2_OPTION_FLAG_BUILDPLUGS); //must conform to *.COL enum 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); #ifdef SHOW_PLUGPATHS e2_option_tree_add_column (set, _("Filename"), E2_OPTION_TREE_TYPE_STR, 0, "?.so", 0, NULL, NULL); //E2_OPTION_TREE_COL_NOT_EDITABLE flag ? e2_option_tree_add_column (set, _("Location"), E2_OPTION_TREE_TYPE_STR, 0, "", 0, NULL, NULL); //file-path (for no-child- and parent-plugins) #else e2_option_tree_add_column (set, "", E2_OPTION_TREE_TYPE_HIDDENSTR, 0, "", 0, NULL, NULL); //E2_OPTION_TREE_COL_NOT_EDITABLE flag ? e2_option_tree_add_column (set, "", E2_OPTION_TREE_TYPE_HIDDENSTR, 0, "", 0, NULL, NULL); //file-path (no-child- and parent-plugins) #endif e2_option_tree_add_column (set, "", E2_OPTION_TREE_TYPE_HIDDENSTR, 0, "", 0, NULL, NULL); //signature e2_option_tree_create_store (set); e2_option_tree_prepare_defaults (set, _e2_plugins_tree_defaults); }