[geany/geany] c6952c: Merge pull request #629 from kugel-/pluxy

Colomban Wendling git-noreply at xxxxx
Tue Oct 6 13:53:14 UTC 2015


Branch:      refs/heads/master
Author:      Colomban Wendling <ban at herbesfolles.org>
Committer:   Colomban Wendling <ban at herbesfolles.org>
Date:        Tue, 06 Oct 2015 13:53:14 UTC
Commit:      c6952c75999c3775a5d7342bc3892c5c9451fb39
             https://github.com/geany/geany/commit/c6952c75999c3775a5d7342bc3892c5c9451fb39

Log Message:
-----------
Merge pull request #629 from kugel-/pluxy

Add support for plugins acting as proxies for foreign plugins,
promoting foreign plugins to first-class citizen.


Modified Paths:
--------------
    doc/plugins.dox
    plugins/Makefile.am
    plugins/demoproxy.c
    plugins/demoproxytest.px
    po/POTFILES.skip
    src/plugindata.h
    src/pluginprivate.h
    src/plugins.c
    src/pluginutils.c
    src/utils.h

Modified: doc/plugins.dox
397 lines changed, 397 insertions(+), 0 deletions(-)
===================================================================
@@ -43,6 +43,7 @@ GeanyFuncs::cleanup functions).
 
 @section pluginsupport Plugin Support
 - @link howto Plugin HowTo @endlink - get started
+- @ref proxy
 - @ref legacy
 - @link plugindata.h Plugin Datatypes and Macros @endlink
 - @link pluginsignals.c Plugin Signals @endlink
@@ -725,4 +726,400 @@ void geany_load_module(GeanyPlugin *plugin)
 @endcode
 
 
+ at page proxy Proxy Plugin HowTo
+
+ at section proxy_intro Introduction
+
+Geany has built-in support for plugins. These plugins can alter the way Geany operates in many
+imaginable ways which leaves little to be desired.
+
+However, there is one significant short-coming. Due to the infrastructure, Geany's built-in support
+only covers plugins written in C, perhaps C++ and Vala. Basically all languages which can be
+compiled into native shared libraries and can link GTK libraries. This excludes dynamic languages
+such as Python.
+
+Geany provides a mechanism to enable support for those languages. Native plugins can register as
+proxy plugins by being a normal plugin to the Geany-side and by providing a bridge to write plugins
+in another language on the other side.
+
+These plugins are also called sub-plugins. This refers to the relation to their proxy.
+To Geany they are first-class citizens.
+
+ at section proxy_protocol Writing a Proxy Plugin
+
+The basic idea is that a proxy plugin provides methods to match, load and unload one or more
+sub-plugin plugins in an abstract manner:
+
+ - Matching consists of providing a list of supported file extensions for the sub-plugins and
+   a mechanism to resolve file extension uncertainty or ambiguity. The matching makes the plugin
+   visible to the user within the Plugin Manager.
+ - Loading consists of loading the sub-plugin's file, passing the file to some form of interpreter
+   and calling GEANY_PLUGIN_REGISTER() or GEANY_PLUGIN_REGISTER_FULL() on behalf of the sub-plugin
+   at some point.
+ - Unloading simply reverses the effect of loading.
+
+For providing these methods, GeanyPlugin has a field GeanyProxyFuncs which contains three function
+pointers which must be initialized proir to calling geany_plugin_register_proxy(). This should be
+done in the GeanyPluginFuncs::init function of the proxy plugin.
+
+ - In the call to geany_plugin_register_proxy() the proxy plugin passes a list of file extensions.
+   When Geany scans through its plugin directories as usual it will also look for files with
+   that extensions and consider found files as plugin candidate.
+ - GeanyProxyFuncs::probe may be implemented to probe if a plugin candidate (that has one of the
+   provided file extensions) is actually a plugin. This may depend on the plugin file itself in
+   case of ambiguity or availability of runtime dependencies or even configuration.
+   @ref PROXY_IGNORED or @ref PROXY_MATCHED should be returned, possibly in combination
+   with the @ref PROXY_NOLOAD flag. Not implementing GeanyProxyFuncs::probe at all is eqivalent to
+   always returning @ref PROXY_MATCHED.
+ - GeanyProxyFuncs::load must be implemented to actually load the plugin. It is called by Geany
+   when the user enables the sub-plugin. What "loading" means is entirely up to the proxy plugin and
+   probably depends on the interpreter of the dynamic language that shall be supported. After
+   setting everything up as necessary GEANY_PLUGIN_REGISTER() or GEANY_PLUGIN_REGISTER_FULL() must
+   be called to register the sub-plugin.
+ - GeanyProxyFuncs::unload must be implemented and is called when the user unchecks the sub-plugin
+   or when Geany exits. Here, the proxy should release any references or memory associated to the
+   sub-plugin. Note that if GeanyProxyFuncs::load didn't succeed, i.e. didn't successfully register
+   the sub-plugin, then this function won't be called.
+
+GeanyProxyFuncs::load and GeanyProxyFuncs::unload receive two GeanyPlugin pointers: One that
+corresponds to the proxy itself and another that corresponds to the sub-plugin. The sub-plugin's
+one may be used to call various API functions on behalf of the sub-plugin, including
+GEANY_PLUGIN_REGISTER() and GEANY_PLUGIN_REGISTER_FULL().
+
+GeanyProxyFuncs::load may return a pointer that is passed back to GeanyProxyFuncs::unload. This can
+be used to store proxy-defined but sub-plugin-specific data required for unloading. However, this
+pointer is not passed to the sub-plugin's GeanyPluginFuncs. To arrange for that, you want to call
+GEANY_PLUGIN_REGISTER_FULL(). This method is the key to enable proxy plugins to wrap the
+GeanyPluginFuncs of all sub-plugins and yet multiplex between multiple sub-plugin, for example by
+storing a per-sub-plugin interpreter context.
+
+ at note If the pointer returned from GeanyProxyFuncs::load is the same that is passed to
+GEANY_PLUGIN_REGISTER_FULL() then you must pass NULL as free_func, because that would be invoked
+prior to unloading. Insert the corresponding code into GeanyProxyFuncs::unload.
+
+ at section proxy_compat_guideline Guideline for Checking Compatiblity
+
+Determining if a plugin candidate is compatible is not a single test. There are multiple levels and
+each should be handled differently in order to give the user a consistent feedback.
+
+Consider the 5 basic cases:
+
+1) A candidate comes with a suitable file extension but is not a workable plugin file at all. For
+example, your proxy supports plugins written in a shell script (.sh) but the shebang of that script
+points to an incompatible shell (or even lacks a shebang). You should check for this in
+GeanyProxyFuncs::probe() and return @ref PROXY_IGNORED which hides that script from the Plugin
+Manager and allows other enabled proxy plugins to pick it up. GeanyProxyFuncs::probe() returning
+ at ref PROXY_IGNORED is an indication that the candidate is meant for another proxy, or the user
+placed the file by accident in one of Geany's plugin directories. In other words the candidate
+simply doesn't correspond to your proxy. Thus any noise by debug messages for this case is
+undesirable.
+
+2) A proxy plugin provides its own, versioned API to sub-plugin. The API version of the sub-plugin
+is not compatible with the API exposed by the proxy. GeanyProxyFuncs::probe() should never perform
+a version check because its sole purpose is to indicate a proxy's correspondence to a given
+candidate. It should return @ref PROXY_MATCHED instead. Later, Geany will invoke the
+GeanyProxyFuncs::load(), and this function is the right place for a version check. If it fails then
+you simply do not call GEANY_PLUGIN_REGISTER(), but rather print a debug message. The result is
+that the sub-plugin is not shown in the Plugin Manager at all. This is consistent with the
+treatment of native plugins by Geany.
+
+3) The sub-plugin is also depending on Geany's API version (whether it is or not depends on the
+design of the proxy). In this case do not do anything special but forward the API version the
+sub-plugin is written/compiled against to GEANY_PLUGIN_REGISTER(). Here, Geany will perform its own
+compatiblity check, allowing for a consistent user feedback. The result is again that the
+sub-plugin is hidden from the Plugin Manager, like in case 2. But Geany will print a debug message
+so you can skip that.
+
+
+If you have even more cases try to fit it into case 1 or 2, depending on whether other proxy
+plugins should get a chance to load the candidate or not.
+
+ at section proxy_dep_guideline Guideline for Runtime Errors
+
+A sub-plugin might not be able to run even if it's perfectly compatible with its proxy. This
+includes the case when it lacks certain runtime dependencies such as programs or modules but also
+syntactic problems or other errors.
+
+There are two basic classes:
+
+1) Runtime errors that can be determined at load time. For example, the shebang of a script
+indicates a specific interpeter version but that version is not installed on the system. Your proxy
+should respond the same way as for version-incompatible plugins: don't register the plugin at
+all, but leave a message the user suggesting what has to be installed in order to work. Handle
+syntax errors in the scripts of sub-plugins the same way if possible.
+
+2) Runtime errors that cannot be determined without actually running the plugin. An example would
+be missing modules in Python scripts. If your proxy has no way of foreseeing the problem the plugin
+will be registered normally. However, you can catch runtime errors by implementing
+GeanyPluginFuncs::init() on the plugin's behalf. This is called after user activation and allows to
+indicate errors by returning @c FALSE. However, allowing the user to enable a plugin and then
+disabling anyway is a poor user experience.
+
+Therefore, if possible, try to fail fast and disallow registration.
+
+ at section Proxy Plugin Example
+
+In this section a dumb example proxy plugin is shown in order to give a practical starting point.
+The sub-plugin are not actually code but rather a ini-style description of one or more menu items
+that are added to Geany's tools menu and a help dialog. Real world sub-plugins would contain actual
+code, usually written in a scripting language.
+
+A sub-plugin file looks like this:
+
+ at code{.ini}
+#!!PROXY_MAGIC!!
+
+[Init]
+item0 = Bam
+item1 = Foo
+item2 = Bar
+
+[Help]
+text = I'm a simple test. Nothing to see!
+
+[Info]
+name = Demo Proxy Tester
+description = I'm a simple test. Nothing to see!
+version = 0.1
+author = The Geany developer team
+ at endcode
+
+The first line acts as a verification that this file is truly a sub-plugin. Within the [Init] section
+there is the menu items for Geany's tools menu. The [Help] section declares the sub-plugins help
+text which is shown in its help dialog (via GeanyPluginFuncs::help). The [Info] section is
+used as-is for filling the sub-plugins PluginInfo fields.
+
+That's it, this dumb format is purely declarative and contains no logic. Yet we will create plugins
+from it.
+
+We start by registering the proxy plugin to Geany. There is nothing special to it compared to
+normal plugins. A proxy plugin must also fill its own @ref PluginInfo and @ref GeanyPluginFuncs,
+followed by registering through GEANY_PLUGIN_REGISTER().
+
+
+ at code{.c}
+
+/* Called by Geany to initialize the plugin. */
+static gboolean demoproxy_init(GeanyPlugin *plugin, gpointer pdata)
+{
+	// ...
+}
+
+
+/* Called by Geany before unloading the plugin. */
+static void demoproxy_cleanup(GeanyPlugin *plugin, gpointer data)
+{
+	// ...
+}
+
+
+G_MODULE_EXPORT
+void geany_load_module(GeanyPlugin *plugin)
+{
+	plugin->info->name = _("Demo Proxy");
+	plugin->info->description = _("Example Proxy.");
+	plugin->info->version = "0.1";
+	plugin->info->author = _("The Geany developer team");
+
+	plugin->funcs->init = demoproxy_init;
+	plugin->funcs->cleanup = demoproxy_cleanup;
+
+	GEANY_PLUGIN_REGISTER(plugin, 225);
+}
+
+ at endcode
+
+The next step is to actually register as a proxy plugin. This is done in demoproxy_init().
+As previously mentioned, it needs a list of accepted file extensions and a set of callback
+functions.
+
+ at code{.c}
+static gboolean demoproxy_init(GeanyPlugin *plugin, gpointer pdata)
+{
+	const gchar *extensions[] = { "ini", "px", NULL };
+
+	plugin->proxy_funcs->probe  = demoproxy_probe;
+	plugin->proxy_funcs->load   = demoproxy_load;
+	plugin->proxy_funcs->unload = demoproxy_unload;
+
+	return geany_plugin_register_proxy(plugin, extensions);
+}
+
+ at endcode
+
+The callback functions deserve a closer look.
+
+As already mentioned the file format includes a magic first line which must be present.
+GeanyProxyFuncs::probe() verifies that it's present and avoids showing the sub-plugin in the
+Plugin Manager if not.
+
+ at code{.c}
+static gint demoproxy_probe(GeanyPlugin *proxy, const gchar *filename, gpointer pdata)
+{
+	/* We know the extension is right (Geany checks that). For demo purposes we perform an
+	 * additional check. This is not necessary when the extension is unique enough. */
+	gboolean match = FALSE;
+	gchar linebuf[128];
+	FILE *f = fopen(filename, "r");
+	if (f != NULL)
+	{
+		if (fgets(linebuf, sizeof(linebuf), f) != NULL)
+			match = utils_str_equal(linebuf, "#!!PROXY_MAGIC!!\n");
+		fclose(f);
+	}
+	return match ? PROXY_MATCHED : PROXY_IGNORED;
+}
+ at endcode
+
+GeanyProxyFuncs::load is a bit more complex. It reads the file, fills the sub-plugin's PluginInfo
+fields and calls GEANY_PLUGIN_REGISTER_FULL(). Additionally, it creates a per-plugin context that
+holds GKeyFile instance (a poor man's interpeter context). You can also see that it does not call
+GEANY_PLUGIN_REGISTER_FULL() if g_key_file_load_from_file() found an error (probably a syntax
+problem) which means the sub-plugin cannot be enabled.
+
+It also installs wrapper functions for the sub-plugin's GeanyPluginFuncs as ini files aren't code.
+It's very likely that your proxy needs something similar because you can only install function
+pointers to native code.
+
+ at code{.c}
+typedef struct {
+	GKeyFile       *file;
+	gchar          *help_text;
+	GSList         *menu_items;
+}
+PluginContext;
+
+
+static gboolean proxy_init(GeanyPlugin *plugin, gpointer pdata);
+static void proxy_help(GeanyPlugin *plugin, gpointer pdata);
+static void proxy_cleanup(GeanyPlugin *plugin, gpointer pdata);
+
+
+static gpointer demoproxy_load(GeanyPlugin *proxy, GeanyPlugin *plugin,
+                               const gchar *filename, gpointer pdata)
+{
+	GKeyFile *file;
+	gboolean result;
+
+	file = g_key_file_new();
+	result = g_key_file_load_from_file(file, filename, 0, NULL);
+
+	if (result)
+	{
+		PluginContext *data = g_new0(PluginContext, 1);
+		data->file = file;
+
+		plugin->info->name = g_key_file_get_locale_string(data->file, "Info", "name", NULL, NULL);
+		plugin->info->description = g_key_file_get_locale_string(data->file, "Info", "description", NULL, NULL);
+		plugin->info->version = g_key_file_get_locale_string(data->file, "Info", "version", NULL, NULL);
+		plugin->info->author = g_key_file_get_locale_string(data->file, "Info", "author", NULL, NULL);
+
+		plugin->funcs->init = proxy_init;
+		plugin->funcs->help = proxy_help;
+		plugin->funcs->cleanup = proxy_cleanup;
+
+		/* Cannot pass g_free as free_func be Geany calls it before unloading, and since 
+		 * demoproxy_unload() accesses the data this would be catastrophic */
+		GEANY_PLUGIN_REGISTER_FULL(plugin, 225, data, NULL);
+		return data;
+	}
+
+	g_key_file_free(file);
+	return NULL;
+}
+ at endcode
+
+demoproxy_unload() simply releases all resources aquired in demoproxy_load(). It does not have to
+do anything else in for unloading.
+
+ at code{.c}
+static void demoproxy_unload(GeanyPlugin *proxy, GeanyPlugin *plugin, gpointer load_data, gpointer pdata)
+{
+	PluginContext *data = load_data;
+
+	g_free((gchar *)plugin->info->name);
+	g_free((gchar *)plugin->info->description);
+	g_free((gchar *)plugin->info->version);
+	g_free((gchar *)plugin->info->author);
+
+	g_key_file_free(data->file);
+	g_free(data);
+}
+ at endcode
+
+Finally the demo_proxy's wrapper GeanyPluginFuncs. They are called for each possible sub-plugin and
+therefore have to multiplex between each using the plugin-defined data pointer. Each is called by
+Geany as if it were an ordinary, native plugin.
+
+proxy_init() actually reads the sub-plugin's file using GKeyFile APIs. It prepares for the help
+dialog and installs the menu items. proxy_help() is called when the user clicks the help button in
+the Plugin Manager. Consequently, this fires up a suitable dialog, although with a dummy message.
+proxy_cleanup() frees all memory allocated in proxy_init().
+
+ at code{.c}
+static gboolean proxy_init(GeanyPlugin *plugin, gpointer pdata)
+{
+	PluginContext *data;
+	gint i = 0;
+	gchar *text;
+
+	data = (PluginContext *) pdata;
+
+	/* Normally, you would instruct the VM/interpreter to call into the actual plugin. The
+	 * plugin would be identified by pdata. Because there is no interpreter for
+	 * .ini files we do it inline, as this is just a demo */
+	data->help_text = g_key_file_get_locale_string(data->file, "Help", "text", NULL, NULL);
+	while (TRUE)
+	{
+		GtkWidget *item;
+		gchar *key = g_strdup_printf("item%d", i++);
+		text = g_key_file_get_locale_string(data->file, "Init", key, NULL, NULL);
+		g_free(key);
+
+		if (!text)
+			break;
+
+		item = gtk_menu_item_new_with_label(text);
+		gtk_widget_show(item);
+		gtk_container_add(GTK_CONTAINER(plugin->geany_data->main_widgets->tools_menu), item);
+		gtk_widget_set_sensitive(item, FALSE);
+		data->menu_items = g_slist_prepend(data->menu_items, (gpointer) item);
+		g_free(text);
+	}
+
+	return TRUE;
+}
+
+
+static void proxy_help(GeanyPlugin *plugin, gpointer pdata)
+{
+	PluginContext *data;
+	GtkWidget *dialog;
+
+	data = (PluginContext *) pdata;
+
+	dialog = gtk_message_dialog_new(
+		GTK_WINDOW(plugin->geany_data->main_widgets->window),
+		GTK_DIALOG_DESTROY_WITH_PARENT,
+		GTK_MESSAGE_INFO,
+		GTK_BUTTONS_OK,
+		"%s", data->help_text);
+	gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(dialog),
+		_("(From the %s plugin)"), plugin->info->name);
+
+	gtk_dialog_run(GTK_DIALOG(dialog));
+	gtk_widget_destroy(dialog);
+}
+
+
+static void proxy_cleanup(GeanyPlugin *plugin, gpointer pdata)
+{
+	PluginContext *data = (PluginContext *) pdata;
+
+	g_slist_free_full(data->menu_items, (GDestroyNotify) gtk_widget_destroy);
+	g_free(data->help_text);
+}
+ at endcode
+
+
 */


Modified: plugins/Makefile.am
10 lines changed, 8 insertions(+), 2 deletions(-)
===================================================================
@@ -1,7 +1,8 @@
 # Adapted from Pidgin's plugins/Makefile.am, thanks
 
 EXTRA_DIST = \
-	makefile.win32
+	makefile.win32 \
+	demoproxytest.px
 
 plugindir = $(libdir)/geany
 
@@ -11,6 +12,7 @@ plugins_include_HEADERS = \
 	geanyplugin.h
 
 demoplugin_la_LDFLAGS    = -module -avoid-version -no-undefined
+demoproxy_la_LDFLAGS     = -module -avoid-version -no-undefined
 classbuilder_la_LDFLAGS  = -module -avoid-version -no-undefined
 htmlchars_la_LDFLAGS     = -module -avoid-version -no-undefined
 export_la_LDFLAGS        = -module -avoid-version -no-undefined
@@ -30,9 +32,11 @@ plugin_LTLIBRARIES = \
 
 # Plugins not to be installed
 noinst_LTLIBRARIES = \
-	demoplugin.la
+	demoplugin.la  \
+	demoproxy.la
 
 demoplugin_la_SOURCES    = demoplugin.c
+demoproxy_la_SOURCES     = demoproxy.c
 classbuilder_la_SOURCES  = classbuilder.c
 htmlchars_la_SOURCES     = htmlchars.c
 export_la_SOURCES        = export.c
@@ -41,6 +45,7 @@ filebrowser_la_SOURCES   = filebrowser.c
 splitwindow_la_SOURCES   = splitwindow.c
 
 demoplugin_la_CFLAGS    = -DG_LOG_DOMAIN=\""Demoplugin"\" -DLOCALEDIR=\""$(LOCALEDIR)"\"
+demoproxy_la_CFLAGS     = -DG_LOG_DOMAIN=\""Demoproxy"\"
 classbuilder_la_CFLAGS  = -DG_LOG_DOMAIN=\""Classbuilder"\"
 htmlchars_la_CFLAGS     = -DG_LOG_DOMAIN=\""HTMLChars"\"
 export_la_CFLAGS        = -DG_LOG_DOMAIN=\""Export"\"
@@ -49,6 +54,7 @@ filebrowser_la_CFLAGS   = -DG_LOG_DOMAIN=\""FileBrowser"\"
 splitwindow_la_CFLAGS   = -DG_LOG_DOMAIN=\""SplitWindow"\"
 
 demoplugin_la_LIBADD    = $(top_builddir)/src/libgeany.la $(GTK_LIBS)
+demoproxy_la_LIBADD     = $(top_builddir)/src/libgeany.la $(GTK_LIBS)
 classbuilder_la_LIBADD  = $(top_builddir)/src/libgeany.la $(GTK_LIBS)
 htmlchars_la_LIBADD     = $(top_builddir)/src/libgeany.la $(GTK_LIBS)
 export_la_LIBADD        = $(top_builddir)/src/libgeany.la $(GTK_LIBS) -lm


Modified: plugins/demoproxy.c
202 lines changed, 202 insertions(+), 0 deletions(-)
===================================================================
@@ -0,0 +1,202 @@
+/*
+ *      demoproxy.c - this file is part of Geany, a fast and lightweight IDE
+ *
+ *      Copyright 2015 Thomas Martitz <kugel(at)rockbox(dot)org>
+ *
+ *      This program is free software; you can redistribute it and/or modify
+ *      it under the terms of the GNU General Public License as published by
+ *      the Free Software Foundation; either version 2 of the License, or
+ *      (at your option) any later version.
+ *
+ *      This program is distributed in the hope that it will be useful,
+ *      but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *      GNU General Public License for more details.
+ *
+ *      You should have received a copy of the GNU General Public License along
+ *      with this program; if not, write to the Free Software Foundation, Inc.,
+ *      51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+/**
+ * Demo proxy - example of a basic proxy plugin for Geany. Sub-plugins add menu items to the
+ * Tools menu and have a help dialog.
+ *
+ * Note: This is compiled but not installed by default. On Unix, you can install it by compiling
+ * Geany and then copying (or symlinking) to the plugins/demoproxy.so and
+ * plugins/demoproxytest.px files to ~/.config/geany/plugins
+ * - it will be loaded at next startup.
+ */
+
+/* plugin API, always comes first */
+#include "geanyplugin.h"
+
+typedef struct {
+	GKeyFile       *file;
+	gchar          *help_text;
+	GSList         *menu_items;
+}
+PluginContext;
+
+
+static gboolean proxy_init(GeanyPlugin *plugin, gpointer pdata)
+{
+	PluginContext *data;
+	gint i = 0;
+	gchar *text;
+
+	data = (PluginContext *) pdata;
+
+	/* Normally, you would instruct the VM/interpreter to call into the actual plugin. The
+	 * plugin would be identified by pdata. Because there is no interpreter for
+	 * .ini files we do it inline, as this is just a demo */
+	data->help_text = g_key_file_get_locale_string(data->file, "Help", "text", NULL, NULL);
+	while (TRUE)
+	{
+		GtkWidget *item;
+		gchar *key = g_strdup_printf("item%d", i++);
+		text = g_key_file_get_locale_string(data->file, "Init", key, NULL, NULL);
+		g_free(key);
+
+		if (!text)
+			break;
+
+		item = gtk_menu_item_new_with_label(text);
+		gtk_widget_show(item);
+		gtk_container_add(GTK_CONTAINER(plugin->geany_data->main_widgets->tools_menu), item);
+		gtk_widget_set_sensitive(item, FALSE);
+		data->menu_items = g_slist_prepend(data->menu_items, (gpointer) item);
+		g_free(text);
+	}
+
+	return TRUE;
+}
+
+
+static void proxy_help(GeanyPlugin *plugin, gpointer pdata)
+{
+	PluginContext *data;
+	GtkWidget *dialog;
+
+	data = (PluginContext *) pdata;
+
+	dialog = gtk_message_dialog_new(
+		GTK_WINDOW(plugin->geany_data->main_widgets->window),
+		GTK_DIALOG_DESTROY_WITH_PARENT,
+		GTK_MESSAGE_INFO,
+		GTK_BUTTONS_OK,
+		"%s", data->help_text);
+	gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(dialog),
+		_("(From the %s plugin)"), plugin->info->name);
+
+	gtk_dialog_run(GTK_DIALOG(dialog));
+	gtk_widget_destroy(dialog);
+}
+
+
+static void proxy_cleanup(GeanyPlugin *plugin, gpointer pdata)
+{
+	PluginContext *data = (PluginContext *) pdata;
+
+	g_slist_free_full(data->menu_items, (GDestroyNotify) gtk_widget_destroy);
+	g_free(data->help_text);
+}
+
+
+static gint demoproxy_probe(GeanyPlugin *proxy, const gchar *filename, gpointer pdata)
+{
+	/* We know the extension is right (Geany checks that). For demo purposes we perform an
+	 * additional check. This is not necessary when the extension is unique enough. */
+	gboolean match = FALSE;
+	gchar linebuf[128];
+	FILE *f = fopen(filename, "r");
+	if (f != NULL)
+	{
+		if (fgets(linebuf, sizeof(linebuf), f) != NULL)
+			match = utils_str_equal(linebuf, "#!!PROXY_MAGIC!!\n");
+		fclose(f);
+	}
+	return match ? PROXY_MATCHED : PROXY_IGNORED;
+}
+
+
+static gpointer demoproxy_load(GeanyPlugin *proxy, GeanyPlugin *plugin,
+                               const gchar *filename, gpointer pdata)
+{
+	GKeyFile *file;
+	gboolean result;
+
+	file = g_key_file_new();
+	result = g_key_file_load_from_file(file, filename, 0, NULL);
+
+	if (result)
+	{
+		PluginContext *data = g_new0(PluginContext, 1);
+		data->file = file;
+
+		plugin->info->name = g_key_file_get_locale_string(data->file, "Info", "name", NULL, NULL);
+		plugin->info->description = g_key_file_get_locale_string(data->file, "Info", "description", NULL, NULL);
+		plugin->info->version = g_key_file_get_locale_string(data->file, "Info", "version", NULL, NULL);
+		plugin->info->author = g_key_file_get_locale_string(data->file, "Info", "author", NULL, NULL);
+
+		plugin->funcs->init = proxy_init;
+		plugin->funcs->help = proxy_help;
+		plugin->funcs->cleanup = proxy_cleanup;
+
+		/* Cannot pass g_free as free_func be Geany calls it before unloading, and since 
+		 * demoproxy_unload() accesses the data this would be catastrophic */
+		GEANY_PLUGIN_REGISTER_FULL(plugin, 225, data, NULL);
+		return data;
+	}
+
+	g_key_file_free(file);
+	return NULL;
+}
+
+
+static void demoproxy_unload(GeanyPlugin *proxy, GeanyPlugin *plugin, gpointer load_data, gpointer pdata)
+{
+	PluginContext *data = load_data;
+
+	g_free((gchar *)plugin->info->name);
+	g_free((gchar *)plugin->info->description);
+	g_free((gchar *)plugin->info->version);
+	g_free((gchar *)plugin->info->author);
+
+	g_key_file_free(data->file);
+	g_free(data);
+}
+
+
+/* Called by Geany to initialize the plugin. */
+static gboolean demoproxy_init(GeanyPlugin *plugin, gpointer pdata)
+{
+	const gchar *extensions[] = { "ini", "px", NULL };
+
+	plugin->proxy_funcs->probe  = demoproxy_probe;
+	plugin->proxy_funcs->load   = demoproxy_load;
+	plugin->proxy_funcs->unload = demoproxy_unload;
+
+	return geany_plugin_register_proxy(plugin, extensions);
+}
+
+
+/* Called by Geany before unloading the plugin. */
+static void demoproxy_cleanup(GeanyPlugin *plugin, gpointer data)
+{
+}
+
+
+G_MODULE_EXPORT
+void geany_load_module(GeanyPlugin *plugin)
+{
+	plugin->info->name = _("Demo Proxy");
+	plugin->info->description = _("Example Proxy.");
+	plugin->info->version = "0.1";
+	plugin->info->author = _("The Geany developer team");
+
+	plugin->funcs->init = demoproxy_init;
+	plugin->funcs->cleanup = demoproxy_cleanup;
+
+	GEANY_PLUGIN_REGISTER(plugin, 225);
+}


Modified: plugins/demoproxytest.px
15 lines changed, 15 insertions(+), 0 deletions(-)
===================================================================
@@ -0,0 +1,15 @@
+#!!PLUXY_MAGIC!!
+
+[Init]
+item0 = Bam
+item1 = Foo
+item2 = Bar
+
+[Help]
+text = I'm a simple test. Nothing to see!
+
+[Info]
+name = Demo Pluxy Tester
+description = I'm a simple test. Nothing to see!
+version = 0.1
+author = The Geany developer team


Modified: po/POTFILES.skip
1 lines changed, 1 insertions(+), 0 deletions(-)
===================================================================
@@ -6,5 +6,6 @@ geany.desktop.in
 geany.glade
 # no need to translate these files
 plugins/demoplugin.c
+plugins/demoproxy.c
 doc/stash-example.c
 doc/stash-gui-example.c


Modified: src/plugindata.h
56 lines changed, 54 insertions(+), 2 deletions(-)
===================================================================
@@ -58,7 +58,7 @@ G_BEGIN_DECLS
  * @warning You should not test for values below 200 as previously
  * @c GEANY_API_VERSION was defined as an enum value, not a macro.
  */
-#define GEANY_API_VERSION 225
+#define GEANY_API_VERSION 226
 
 /* hack to have a different ABI when built with GTK3 because loading GTK2-linked plugins
  * with GTK3-linked Geany leads to crash */
@@ -240,6 +240,7 @@ GeanyData;
 #define geany			geany_data	/**< Simple macro for @c geany_data that reduces typing. */
 
 typedef struct GeanyPluginFuncs GeanyPluginFuncs;
+typedef struct GeanyProxyFuncs GeanyProxyFuncs;
 
 /** Basic information for the plugin and identification.
  * @see geany_plugin. */
@@ -248,7 +249,8 @@ typedef struct GeanyPlugin
 	PluginInfo	*info;	/**< Fields set in plugin_set_info(). */
 	GeanyData	*geany_data;	/**< Pointer to global GeanyData intance */
 	GeanyPluginFuncs *funcs;	/**< Functions implemented by the plugin, set in geany_load_module() */
-
+	GeanyProxyFuncs	*proxy_funcs; /**< Hooks implemented by the plugin if it wants to act as a proxy
+									   Must be set prior to calling geany_plugin_register_proxy() */
 	struct GeanyPluginPrivate *priv;	/* private */
 }
 GeanyPlugin;
@@ -347,6 +349,56 @@ void geany_plugin_set_data(GeanyPlugin *plugin, gpointer data, GDestroyNotify fr
 	geany_plugin_register_full((plugin), GEANY_API_VERSION, \
 	                           (min_api_version), GEANY_ABI_VERSION, (pdata), (free_func))
 
+/** Return values for GeanyProxyHooks::probe()
+ *
+ * Only @c PROXY_IGNORED, @c PROXY_MATCHED or @c PROXY_MATCHED|PROXY_NOLOAD
+ * are valid return values.
+ *
+ * @see geany_plugin_register_proxy() for a full description of the proxy plugin mechanisms.
+ *
+ * @since 1.26 (API 226)
+ */
+typedef enum
+{
+	/** The proxy is not responsible at all, and Geany or other plugins are free
+	 * to probe it.
+	 **/
+	PROXY_IGNORED,
+	/** The proxy is responsible for this file, and creates a plugin for it */
+	PROXY_MATCHED,
+
+	/** The proxy is does not directly load it, but it's still tied to the proxy
+	 *
+	 * This is for plugins that come in multiple files where only one of these
+	 * files is relevant for the plugin creation (for the PM dialog). The other
+	 * files should be ignored by Geany and other proxies. Example: libpeas has
+	 * a .plugin and a .so per plugin. Geany should not process the .so file
+	 * if there is a corresponding .plugin.
+	 */
+	PROXY_NOLOAD = 0x100,
+}
+GeanyProxyProbeResults;
+
+
+/** Hooks that need to be implemented by every proxy
+ *
+ * @see geany_plugin_register_proxy() for a full description of the proxy mechanism.
+ *
+ * @since 1.26 (API 226)
+ **/
+struct GeanyProxyFuncs
+{
+	/** Called to determine whether the proxy is truly responsible for the requested plugin.
+	 * A NULL pointer assumes the probe() function would always return @ref PROXY_MATCHED */
+	gint		(*probe)     (GeanyPlugin *proxy, const gchar *filename, gpointer pdata);
+	/** Called after probe(), to perform the actual job of loading the plugin */
+	gpointer	(*load)      (GeanyPlugin *proxy, GeanyPlugin *subplugin, const gchar *filename, gpointer pdata);
+	/** Called when the user initiates unloading of a plugin, e.g. on Geany exit */
+	void		(*unload)    (GeanyPlugin *proxy, GeanyPlugin *subplugin, gpointer load_data, gpointer pdata);
+};
+
+gint geany_plugin_register_proxy(GeanyPlugin *plugin, const gchar **extensions);
+
 /* Deprecated aliases */
 #ifndef GEANY_DISABLE_DEPRECATED
 


Modified: src/pluginprivate.h
16 lines changed, 12 insertions(+), 4 deletions(-)
===================================================================
@@ -46,9 +46,10 @@ typedef enum _LoadedFlags {
 }
 LoadedFlags;
 
+typedef struct GeanyPluginPrivate Plugin;	/* shorter alias */
+
 typedef struct GeanyPluginPrivate
 {
-	GModule 		*module;
 	gchar			*filename;				/* plugin filename (/path/libname.so) */
 	PluginInfo		info;				/* plugin name, description, etc */
 	GeanyPlugin		public;				/* fields the plugin can read */
@@ -66,6 +67,14 @@ typedef struct GeanyPluginPrivate
 	gpointer		cb_data;				/* user data passed back to functions in GeanyPluginFuncs */
 	GDestroyNotify	cb_data_destroy;		/* called when the plugin is unloaded, for cb_data */
 	LoadedFlags		flags;					/* bit-or of LoadedFlags */
+
+	/* proxy plugin support */
+	GeanyProxyFuncs	proxy_cbs;
+	Plugin			*proxy;					/* The proxy that handles this plugin */
+	gpointer		proxy_data;				/* Data passed to the proxy hooks of above proxy, so
+											 * this gives the proxy a pointer to each plugin */
+	gint			proxied_count;			/* count of active plugins this provides a proxy for
+											 * (a count because of possibly nested proxies) */
 }
 GeanyPluginPrivate;
 
@@ -73,10 +82,9 @@ GeanyPluginPrivate;
 #define PLUGIN_IS_LEGACY(p) (((p)->flags & IS_LEGACY) != 0)
 #define PLUGIN_HAS_LOAD_DATA(p) (((p)->flags & LOAD_DATA) != 0)
 
-typedef GeanyPluginPrivate Plugin;	/* shorter alias */
-
-
 void plugin_watch_object(Plugin *plugin, gpointer object);
+void plugin_make_resident(Plugin *plugin);
+gpointer plugin_get_module_symbol(Plugin *plugin, const gchar *sym);
 
 G_END_DECLS
 


Modified: src/plugins.c
697 lines changed, 564 insertions(+), 133 deletions(-)
===================================================================
@@ -75,8 +75,33 @@ static GtkWidget *menu_separator = NULL;
 static gchar *get_plugin_path(void);
 static void pm_show_dialog(GtkMenuItem *menuitem, gpointer user_data);
 
-static GeanyData geany_data;
+typedef struct {
+	gchar		extension[8];
+	Plugin		*plugin; /* &builtin_so_proxy_plugin for native plugins */
+} PluginProxy;
+
+
+static gpointer plugin_load_gmodule(GeanyPlugin *proxy, GeanyPlugin *plugin, const gchar *filename, gpointer pdata);
+static void plugin_unload_gmodule(GeanyPlugin *proxy, GeanyPlugin *plugin, gpointer load_data, gpointer pdata);
+
+static Plugin builtin_so_proxy_plugin = {
+	.proxy_cbs = {
+		.load = plugin_load_gmodule,
+		.unload = plugin_unload_gmodule,
+	},
+	/* rest of Plugin can be NULL/0 */
+};
 
+static PluginProxy builtin_so_proxy = {
+	.extension = G_MODULE_SUFFIX,
+	.plugin = &builtin_so_proxy_plugin,
+};
+
+static GQueue active_proxies = G_QUEUE_INIT;
+
+static void plugin_free(Plugin *plugin);
+
+static GeanyData geany_data;
 
 static void
 geany_data_init(void)
@@ -102,19 +127,42 @@ geany_data_init(void)
 }
 
 
+/* In order to have nested proxies work the count of dependent plugins must propagate up.
+ * This prevents that any plugin in the tree is unloaded while a leaf plugin is active. */
+static void proxied_count_inc(Plugin *proxy)
+{
+	do
+	{
+		proxy->proxied_count += 1;
+		proxy = proxy->proxy;
+	} while (proxy != NULL);
+}
+
+
+static void proxied_count_dec(Plugin *proxy)
+{
+	g_warn_if_fail(proxy->proxied_count > 0);
+
+	do
+	{
+		proxy->proxied_count -= 1;
+		proxy = proxy->proxy;
+	} while (proxy != NULL);
+}
+
+
 /* Prevent the same plugin filename being loaded more than once.
  * Note: g_module_name always returns the .so name, even when Plugin::filename is a .la file. */
 static gboolean
-plugin_loaded(GModule *module)
+plugin_loaded(Plugin *plugin)
 {
 	gchar *basename_module, *basename_loaded;
 	GList *item;
 
-	basename_module = g_path_get_basename(g_module_name(module));
+	basename_module = g_path_get_basename(plugin->filename);
 	for (item = plugin_list; item != NULL; item = g_list_next(item))
 	{
-		basename_loaded = g_path_get_basename(
-			g_module_name(((Plugin*)item->data)->module));
+		basename_loaded = g_path_get_basename(((Plugin*)item->data)->filename);
 
 		if (utils_str_equal(basename_module, basename_loaded))
 		{
@@ -131,7 +179,7 @@ plugin_loaded(GModule *module)
 	 * would cause a crash. */
 	for (item = active_plugin_list; item != NULL; item = g_list_next(item))
 	{
-		basename_loaded = g_path_get_basename(g_module_name(((Plugin*)item->data)->module));
+		basename_loaded = g_path_get_basename(((Plugin*)item->data)->filename);
 
 		if (utils_str_equal(basename_module, basename_loaded))
 		{
@@ -168,22 +216,27 @@ static Plugin *find_active_plugin_by_name(const gchar *filename)
 static gboolean
 plugin_check_version(Plugin *plugin, int plugin_version_code)
 {
-	GModule *module = plugin->module;
+	gboolean ret = TRUE;
 	if (plugin_version_code < 0)
 	{
+		gchar *name = g_path_get_basename(plugin->filename);
 		msgwin_status_add(_("The plugin \"%s\" is not binary compatible with this "
-			"release of Geany - please recompile it."), g_module_name(module));
+			"release of Geany - please recompile it."), name);
 		geany_debug("Plugin \"%s\" is not binary compatible with this "
-			"release of Geany - recompile it.", g_module_name(module));
-		return FALSE;
+			"release of Geany - recompile it.", name);
+		ret = FALSE;
+		g_free(name);
 	}
-	if (plugin_version_code > GEANY_API_VERSION)
+	else if (plugin_version_code > GEANY_API_VERSION)
 	{
+		gchar *name = g_path_get_basename(plugin->filename);
 		geany_debug("Plugin \"%s\" requires a newer version of Geany (API >= v%d).",
-			g_module_name(module), plugin_version_code);
-		return FALSE;
+			name, plugin_version_code);
+		ret = FALSE;
+		g_free(name);
 	}
-	return TRUE;
+
+	return ret;
 }
 
 
@@ -217,9 +270,10 @@ static void read_key_group(Plugin *plugin)
 {
 	GeanyKeyGroupInfo *p_key_info;
 	GeanyKeyGroup **p_key_group;
+	GModule *module = plugin->proxy_data;
 
-	g_module_symbol(plugin->module, "plugin_key_group_info", (void *) &p_key_info);
-	g_module_symbol(plugin->module, "plugin_key_group", (void *) &p_key_group);
+	g_module_symbol(module, "plugin_key_group_info", (void *) &p_key_info);
+	g_module_symbol(module, "plugin_key_group", (void *) &p_key_group);
 	if (p_key_info && p_key_group)
 	{
 		GeanyKeyGroupInfo *key_info = p_key_info;
@@ -307,8 +361,10 @@ gboolean geany_plugin_register(GeanyPlugin *plugin, gint api_version, gint min_a
 	/* Only init and cleanup callbacks are truly mandatory. */
 	if (! cbs->init || ! cbs->cleanup)
 	{
-		geany_debug("Plugin '%s' has no %s function - ignoring plugin!",
-				 g_module_name(p->module), cbs->init ? "cleanup" : "init");
+		gchar *name = g_path_get_basename(p->filename);
+		geany_debug("Plugin '%s' has no %s function - ignoring plugin!", name,
+		            cbs->init ? "cleanup" : "init");
+		g_free(name);
 	}
 	else
 	{
@@ -425,7 +481,7 @@ static void register_legacy_plugin(Plugin *plugin, GModule *module)
 	if (! g_module_symbol(module, "plugin_" #__x, (void *) (&p_##__x)))                   \
 	{                                                                                     \
 		geany_debug("Plugin \"%s\" has no plugin_" #__x "() function - ignoring plugin!", \
-				g_module_name(plugin->module));                                           \
+				g_module_name(module));                                                   \
 		return;                                                                           \
 	}
 	CHECK_FUNC(version_check);
@@ -481,21 +537,23 @@ static gboolean
 plugin_load(Plugin *plugin)
 {
 	gboolean init_ok = TRUE;
+
 	/* Start the plugin. Legacy plugins require additional cruft. */
-	if (PLUGIN_IS_LEGACY(plugin))
+	if (PLUGIN_IS_LEGACY(plugin) && plugin->proxy == &builtin_so_proxy_plugin)
 	{
 		GeanyPlugin **p_geany_plugin;
 		PluginInfo **p_info;
 		PluginFields **plugin_fields;
+		GModule *module = plugin->proxy_data;
 		/* set these symbols before plugin_init() is called
 		 * we don't set geany_data since it is set directly by plugin_new() */
-		g_module_symbol(plugin->module, "geany_plugin", (void *) &p_geany_plugin);
+		g_module_symbol(module, "geany_plugin", (void *) &p_geany_plugin);
 		if (p_geany_plugin)
 			*p_geany_plugin = &plugin->public;
-		g_module_symbol(plugin->module, "plugin_info", (void *) &p_info);
+		g_module_symbol(module, "plugin_info", (void *) &p_info);
 		if (p_info)
 			*p_info = &plugin->info;
-		g_module_symbol(plugin->module, "plugin_fields", (void *) &plugin_fields);
+		g_module_symbol(module, "plugin_fields", (void *) &plugin_fields);
 		if (plugin_fields)
 			*plugin_fields = &plugin->fields;
 		read_key_group(plugin);
@@ -525,26 +583,77 @@ plugin_load(Plugin *plugin)
 	 * keep list sorted so tools menu items and plugin preference tabs are
 	 * sorted by plugin name */
 	active_plugin_list = g_list_insert_sorted(active_plugin_list, plugin, cmp_plugin_names);
+	proxied_count_inc(plugin->proxy);
 
 	geany_debug("Loaded:   %s (%s)", plugin->filename, plugin->info.name);
 	return TRUE;
 }
 
 
+static gpointer plugin_load_gmodule(GeanyPlugin *proxy, GeanyPlugin *subplugin, const gchar *fname, gpointer pdata)
+{
+	GModule *module;
+	void (*p_geany_load_module)(GeanyPlugin *);
+
+	g_return_val_if_fail(g_module_supported(), NULL);
+	/* Don't use G_MODULE_BIND_LAZY otherwise we can get unresolved symbols at runtime,
+	 * causing a segfault. Without that flag the module will safely fail to load.
+	 * G_MODULE_BIND_LOCAL also helps find undefined symbols e.g. app when it would
+	 * otherwise not be detected due to the shadowing of Geany's app variable.
+	 * Also without G_MODULE_BIND_LOCAL calling public functions e.g. the old info()
+	 * function from a plugin will be shadowed. */
+	module = g_module_open(fname, G_MODULE_BIND_LOCAL);
+	if (!module)
+	{
+		geany_debug("Can't load plugin: %s", g_module_error());
+		return NULL;
+	}
+
+	/*geany_debug("Initializing plugin '%s'", plugin->info.name);*/
+	g_module_symbol(module, "geany_load_module", (void *) &p_geany_load_module);
+	if (p_geany_load_module)
+	{
+		/* This is a new style plugin. It should fill in plugin->info and then call
+		 * geany_plugin_register() in its geany_load_module() to successfully load.
+		 * The ABI and API checks are performed by geany_plugin_register() (i.e. by us).
+		 * We check the LOADED_OK flag separately to protect us against buggy plugins
+		 * who ignore the result of geany_plugin_register() and register anyway */
+		p_geany_load_module(subplugin);
+	}
+	else
+	{
+		/* This is the legacy / deprecated code path. It does roughly the same as
+		 * geany_load_module() and geany_plugin_register() together for the new ones */
+		register_legacy_plugin(subplugin->priv, module);
+	}
+	/* We actually check the LOADED_OK flag later */
+	return module;
+}
+
+
+static void plugin_unload_gmodule(GeanyPlugin *proxy, GeanyPlugin *subplugin, gpointer load_data, gpointer pdata)
+{
+	GModule *module = (GModule *) load_data;
+
+	g_return_if_fail(module != NULL);
+
+	if (! g_module_close(module))
+		g_warning("%s: %s", subplugin->priv->filename, g_module_error());
+}
+
+
 /* Load and optionally init a plugin.
  * load_plugin decides whether the plugin's plugin_init() function should be called or not. If it is
  * called, the plugin will be started, if not the plugin will be read only (for the list of
  * available plugins in the plugin manager).
  * When add_to_list is set, the plugin will be added to the plugin manager's plugin_list. */
 static Plugin*
-plugin_new(const gchar *fname, gboolean load_plugin, gboolean add_to_list)
+plugin_new(Plugin *proxy, const gchar *fname, gboolean load_plugin, gboolean add_to_list)
 {
 	Plugin *plugin;
-	GModule *module;
-	void (*p_geany_load_module)(GeanyPlugin *);
 
 	g_return_val_if_fail(fname, NULL);
-	g_return_val_if_fail(g_module_supported(), NULL);
+	g_return_val_if_fail(proxy, NULL);
 
 	/* find the plugin in the list of already loaded, active plugins and use it, otherwise
 	 * load the module */
@@ -563,64 +672,48 @@ plugin_new(const gchar *fname, gboolean load_plugin, gboolean add_to_list)
 		return plugin;
 	}
 
-	/* Don't use G_MODULE_BIND_LAZY otherwise we can get unresolved symbols at runtime,
-	 * causing a segfault. Without that flag the module will safely fail to load.
-	 * G_MODULE_BIND_LOCAL also helps find undefined symbols e.g. app when it would
-	 * otherwise not be detected due to the shadowing of Geany's app variable.
-	 * Also without G_MODULE_BIND_LOCAL calling public functions e.g. the old info()
-	 * function from a plugin will be shadowed. */
-	module = g_module_open(fname, G_MODULE_BIND_LOCAL);
-	if (! module)
-	{
-		geany_debug("Can't load plugin: %s", g_module_error());
-		return NULL;
-	}
-
-	if (plugin_loaded(module))
-	{
-		geany_debug("Plugin \"%s\" already loaded.", fname);
-
-		if (! g_module_close(module))
-			g_warning("%s: %s", fname, g_module_error());
-		return NULL;
-	}
-
 	plugin = g_new0(Plugin, 1);
-	plugin->module = module;
 	plugin->filename = g_strdup(fname);
+	plugin->proxy = proxy;
 	plugin->public.geany_data = &geany_data;
 	plugin->public.priv = plugin;
 	/* Fields of plugin->info/funcs must to be initialized by the plugin */
 	plugin->public.info = &plugin->info;
 	plugin->public.funcs = &plugin->cbs;
+	plugin->public.proxy_funcs = &plugin->proxy_cbs;
 
-	g_module_symbol(module, "geany_load_module", (void *) &p_geany_load_module);
-	if (p_geany_load_module)
+	if (plugin_loaded(plugin))
 	{
-		/* This is a new style plugin. It should fill in plugin->info and then call
-		 * geany_plugin_register() in its geany_load_module() to successfully load.
-		 * The ABI and API checks are performed by geany_plugin_register() (i.e. by us).
-		 * We check the LOADED_OK flag separately to protect us against buggy plugins
-		 * who ignore the result of geany_plugin_register() and register anyway */
-		p_geany_load_module(&plugin->public);
-	}
-	else
-	{
-		/* This is the legacy / deprecated code path. It does roughly the same as
-		 * geany_load_module() and geany_plugin_register() together for the new ones */
-		register_legacy_plugin(plugin, module);
+		geany_debug("Plugin \"%s\" already loaded.", fname);
+		goto err;
 	}
 
+	/* Load plugin, this should read its name etc. It must also call
+	 * geany_plugin_register() for the following PLUGIN_LOADED_OK condition */
+	plugin->proxy_data = proxy->proxy_cbs.load(&proxy->public, &plugin->public, fname, proxy->cb_data);
+
 	if (! PLUGIN_LOADED_OK(plugin))
 	{
 		geany_debug("Failed to load \"%s\" - ignoring plugin!", fname);
 		goto err;
 	}
 
+	/* The proxy assumes success, therefore we have to call unload from here
+	 * on in case of errors */
 	if (EMPTY(plugin->info.name))
 	{
 		geany_debug("No plugin name set for \"%s\" - ignoring plugin!", fname);
-		goto err;
+		goto err_unload;
+	}
+
+	/* cb_data_destroy() frees plugin->cb_data. If that pointer also passed to unload() afterwards
+	 * then that would become a use-after-free. Disallow this combination. If a proxy
+	 * needs the same pointer it must not use a destroy func but free manually in its unload(). */
+	if (plugin->proxy_data == proxy->cb_data && plugin->cb_data_destroy)
+	{
+		geany_debug("Proxy of plugin \"%s\" specified invalid data - ignoring plugin!", fname);
+		plugin->proxy_data = NULL;
+		goto err_unload;
 	}
 
 	if (load_plugin && !plugin_load(plugin))
@@ -628,7 +721,7 @@ plugin_new(const gchar *fname, gboolean load_plugin, gboolean add_to_list)
 		/* Handle failing init same as failing to load for now. In future we
 		 * could present a informational UI or something */
 		geany_debug("Plugin failed to initialize \"%s\" - ignoring plugin!", fname);
-		goto err;
+		goto err_unload;
 	}
 
 	if (add_to_list)
@@ -636,11 +729,11 @@ plugin_new(const gchar *fname, gboolean load_plugin, gboolean add_to_list)
 
 	return plugin;
 
-err:
+err_unload:
 	if (plugin->cb_data_destroy)
 		plugin->cb_data_destroy(plugin->cb_data);
-	if (! g_module_close(module))
-		g_warning("%s: %s", fname, g_module_error());
+	proxy->proxy_cbs.unload(&proxy->public, &plugin->public, plugin->proxy_data, proxy->cb_data);
+err:
 	g_free(plugin->filename);
 	g_free(plugin);
 	return NULL;
@@ -712,6 +805,40 @@ static void remove_sources(Plugin *plugin)
 }
 
 
+/* Make the GModule backing plugin resident (if it's GModule-backed at all) */
+void plugin_make_resident(Plugin *plugin)
+{
+	if (plugin->proxy == &builtin_so_proxy_plugin)
+	{
+		g_return_if_fail(plugin->proxy_data != NULL);
+		g_module_make_resident(plugin->proxy_data);
+	}
+	else
+		g_warning("Skipping g_module_make_resident() for non-native plugin");
+}
+
+
+/* Retrieve the address of a symbol sym located in plugin, if it's GModule-backed */
+gpointer plugin_get_module_symbol(Plugin *plugin, const gchar *sym)
+{
+	gpointer symbol;
+
+	if (plugin->proxy == &builtin_so_proxy_plugin)
+	{
+		g_return_val_if_fail(plugin->proxy_data != NULL, NULL);
+		if (g_module_symbol(plugin->proxy_data, sym, &symbol))
+			return symbol;
+		else
+			g_warning("Failed to locate signal handler for '%s': %s",
+				sym, g_module_error());
+	}
+	else /* TODO: Could possibly support this via a new proxy hook */
+		g_warning("Failed to locate signal handler for '%s': Not supported for non-native plugins",
+			sym);
+	return NULL;
+}
+
+
 static gboolean is_active_plugin(Plugin *plugin)
 {
 	return (g_list_find(active_plugin_list, plugin) != NULL);
@@ -747,32 +874,84 @@ plugin_cleanup(Plugin *plugin)
 		plugin->cb_data_destroy = NULL;
 	}
 
+	proxied_count_dec(plugin->proxy);
 	geany_debug("Unloaded: %s", plugin->filename);
 }
 
 
+/* Remove all plugins that proxy is a proxy for from plugin_list (and free) */
+static void free_subplugins(Plugin *proxy)
+{
+	GList *item;
+
+	item = plugin_list;
+	while (item)
+	{
+		GList *next = g_list_next(item);
+		if (proxy == ((Plugin *) item->data)->proxy)
+		{
+			/* plugin_free modifies plugin_list */
+			plugin_free((Plugin *) item->data);
+		}
+		item = next;
+	}
+}
+
+
+/* Returns true if the removal was successful (=> never for non-proxies) */
+static gboolean unregister_proxy(Plugin *proxy)
+{
+	gboolean is_proxy = FALSE;
+	GList *node;
+
+	/* Remove the proxy from the proxy list first. It might appear more than once (once
+	 * for each extension), but if it doesn't appear at all it's not actually a proxy */
+	foreach_list_safe(node, active_proxies.head)
+	{
+		PluginProxy *p = node->data;
+		if (p->plugin == proxy)
+		{
+			is_proxy = TRUE;
+			g_queue_delete_link(&active_proxies, node);
+		}
+	}
+	return is_proxy;
+}
+
+
+/* Cleanup a plugin and free all resources allocated on behalf of it.
+ *
+ * If the plugin is a proxy then this also takes special care to unload all
+ * subplugin loaded through it (make sure none of them is active!) */
 static void
 plugin_free(Plugin *plugin)
 {
+	Plugin *proxy;
+
 	g_return_if_fail(plugin);
-	g_return_if_fail(plugin->module);
+	g_return_if_fail(plugin->proxy);
+	g_return_if_fail(plugin->proxied_count == 0);
 
+	proxy = plugin->proxy;
+	/* If this a proxy remove all depending subplugins. We can assume none of them is *activated*
+	 * (but potentially loaded). Note that free_subplugins() might call us through recursion */
 	if (is_active_plugin(plugin))
+	{
+		if (unregister_proxy(plugin))
+			free_subplugins(plugin);
 		plugin_cleanup(plugin);
+	}
 
 	active_plugin_list = g_list_remove(active_plugin_list, plugin);
 	plugin_list = g_list_remove(plugin_list, plugin);
 
-	/* cb_data_destroy might be plugin code and must be called before unloading the module */
+	/* cb_data_destroy might be plugin code and must be called before unloading the module. */
 	if (plugin->cb_data_destroy)
 		plugin->cb_data_destroy(plugin->cb_data);
-
-	if (! g_module_close(plugin->module))
-		g_warning("%s: %s", plugin->filename, g_module_error());
+	proxy->proxy_cbs.unload(&proxy->public, &plugin->public, plugin->proxy_data, proxy->cb_data);
 
 	g_free(plugin->filename);
 	g_free(plugin);
-	plugin = NULL;
 }
 
 
@@ -830,25 +1009,87 @@ static gboolean check_plugin_path(const gchar *fname)
 }
 
 
+/* Retuns NULL if this ain't a plugin,
+ * otherwise it returns the appropriate PluginProxy instance to load it */
+static PluginProxy* is_plugin(const gchar *file)
+{
+	GList *node;
+	const gchar *ext;
+
+	/* extract file extension to avoid g_str_has_suffix() in the loop */
+	ext = (const gchar *)strrchr(file, '.');
+	if (ext == NULL)
+		return FALSE;
+	/* ensure the dot is really part of the filename */
+	else if (strchr(ext, G_DIR_SEPARATOR) != NULL)
+		return FALSE;
+
+	ext += 1;
+	/* O(n*m), (m being extensions per proxy) doesn't scale very well in theory
+	 * but not a problem in practice yet */
+	foreach_list(node, active_proxies.head)
+	{
+		PluginProxy *proxy = node->data;
+		if (utils_str_casecmp(ext, proxy->extension) == 0)
+		{
+			Plugin *p = proxy->plugin;
+			gint ret = PROXY_MATCHED;
+
+			if (p->proxy_cbs.probe)
+				ret = p->proxy_cbs.probe(&p->public, file, p->cb_data);
+			switch (ret)
+			{
+				case PROXY_MATCHED:
+					return proxy;
+				case PROXY_MATCHED|PROXY_NOLOAD:
+					return NULL;
+				default:
+					if (ret != PROXY_IGNORED)
+						g_warning("Ignoring bogus return from proxy probe!\n");
+					continue;
+			}
+		}
+	}
+	return NULL;
+}
+
+
 /* load active plugins at startup */
 static void
 load_active_plugins(void)
 {
-	guint i, len;
+	guint i, len, proxies;
 
 	if (active_plugins_pref == NULL || (len = g_strv_length(active_plugins_pref)) == 0)
 		return;
 
-	for (i = 0; i < len; i++)
+	/* If proxys are loaded we have to restart to load plugins that sort before their proxy */
+	do
 	{
-		const gchar *fname = active_plugins_pref[i];
-
-		if (!EMPTY(fname) && g_file_test(fname, G_FILE_TEST_EXISTS))
+		proxies = active_proxies.length;
+		g_list_free_full(failed_plugins_list, (GDestroyNotify) g_free);
+		failed_plugins_list = NULL;
+		for (i = 0; i < len; i++)
 		{
-			if (!check_plugin_path(fname) || plugin_new(fname, TRUE, FALSE) == NULL)
-				failed_plugins_list = g_list_prepend(failed_plugins_list, g_strdup(fname));
+			gchar *fname = active_plugins_pref[i];
+
+#ifdef G_OS_WIN32
+			/* ensure we have canonical paths */
+			gchar *p = fname;
+			while ((p = strchr(p, '/')) != NULL)
+				*p = G_DIR_SEPARATOR;
+#endif
+
+			if (!EMPTY(fname) && g_file_test(fname, G_FILE_TEST_EXISTS))
+			{
+				PluginProxy *proxy = NULL;
+				if (check_plugin_path(fname))
+					proxy = is_plugin(fname);
+				if (proxy == NULL || plugin_new(proxy->plugin, fname, TRUE, FALSE) == NULL)
+					failed_plugins_list = g_list_prepend(failed_plugins_list, g_strdup(fname));
+			}
 		}
-	}
+	} while (proxies != active_proxies.length);
 }
 
 
@@ -856,20 +1097,18 @@ static void
 load_plugins_from_path(const gchar *path)
 {
 	GSList *list, *item;
-	gchar *fname, *tmp;
 	gint count = 0;
 
 	list = utils_get_file_list(path, NULL, NULL);
 
 	for (item = list; item != NULL; item = g_slist_next(item))
 	{
-		tmp = strrchr(item->data, '.');
-		if (tmp == NULL || utils_str_casecmp(tmp, "." G_MODULE_SUFFIX) != 0)
-			continue;
+		gchar *fname = g_build_filename(path, item->data, NULL);
+		PluginProxy *proxy = is_plugin(fname);
 
-		fname = g_build_filename(path, item->data, NULL);
-		if (plugin_new(fname, FALSE, TRUE))
+		if (proxy != NULL && plugin_new(proxy->plugin, fname, FALSE, TRUE))
 			count++;
+
 		g_free(fname);
 	}
 
@@ -887,6 +1126,28 @@ static gchar *get_plugin_path(void)
 }
 
 
+/* See load_all_plugins(), this simply sorts items with lower hierarchy level first
+ * (where hierarchy level == number of intermediate proxies before the builtin so loader) */
+static gint cmp_plugin_by_proxy(gconstpointer a, gconstpointer b)
+{
+	const Plugin *pa = a;
+	const Plugin *pb = b;
+
+	while (TRUE)
+	{
+		if (pa->proxy == pb->proxy)
+			return 0;
+		else if (pa->proxy == &builtin_so_proxy_plugin)
+			return -1;
+		else if (pb->proxy == &builtin_so_proxy_plugin)
+			return 1;
+
+		pa = pa->proxy;
+		pb = pb->proxy;
+	}
+}
+
+
 /* Load (but don't initialize) all plugins for the Plugin Manager dialog */
 static void load_all_plugins(void)
 {
@@ -911,6 +1172,13 @@ static void load_all_plugins(void)
 	/* finally load plugins from $prefix/lib/geany */
 	load_plugins_from_path(plugin_path_system);
 
+	/* It is important to sort any plugins that are proxied after their proxy because
+	 * pm_populate() needs the proxy to be loaded and active (if selected by user) in order
+	 * to properly set the value for the PLUGIN_COLUMN_CAN_UNCHECK column. The order between
+	 * sub-plugins does not matter, only between sub-plugins and their proxy, thus
+	 * sorting by hierarchy level is perfectly sufficient */
+	plugin_list = g_list_sort(plugin_list, cmp_plugin_by_proxy);
+
 	g_free(plugin_path_config);
 	g_free(plugin_path_system);
 }
@@ -1028,6 +1296,17 @@ void plugins_init(void)
 
 	g_signal_connect(geany_object, "save-settings", G_CALLBACK(update_active_plugins_pref), NULL);
 	stash_group_add_string_vector(group, &active_plugins_pref, "active_plugins", NULL);
+
+	g_queue_push_head(&active_proxies, &builtin_so_proxy);
+}
+
+
+/* Same as plugin_free(), except it does nothing for proxies-in-use, to be called on
+ * finalize in a loop */
+static void plugin_free_leaf(Plugin *p)
+{
+	if (p->proxied_count == 0)
+		plugin_free(p);
 }
 
 
@@ -1039,11 +1318,11 @@ void plugins_finalize(void)
 		g_list_foreach(failed_plugins_list, (GFunc) g_free,	NULL);
 		g_list_free(failed_plugins_list);
 	}
-	if (active_plugin_list != NULL)
-	{
-		g_list_foreach(active_plugin_list, (GFunc) plugin_free,	NULL);
-		g_list_free(active_plugin_list);
-	}
+	/* Have to loop because proxys cannot be unloaded until after all their
+	 * plugins are unloaded as well (the second loop should should catch all the remaining ones) */
+	while (active_plugin_list != NULL)
+		g_list_foreach(active_plugin_list, (GFunc) plugin_free_leaf, NULL);
+
 	g_strfreev(active_plugins_pref);
 }
 
@@ -1072,6 +1351,7 @@ gboolean plugins_have_preferences(void)
 enum
 {
 	PLUGIN_COLUMN_CHECK = 0,
+	PLUGIN_COLUMN_CAN_UNCHECK,
 	PLUGIN_COLUMN_PLUGIN,
 	PLUGIN_N_COLUMNS,
 	PM_BUTTON_KEYBINDINGS,
@@ -1083,7 +1363,7 @@ typedef struct
 {
 	GtkWidget *dialog;
 	GtkWidget *tree;
-	GtkListStore *store;
+	GtkTreeStore *store;
 	GtkWidget *filter_entry;
 	GtkWidget *configure_button;
 	GtkWidget *keybindings_button;
@@ -1137,6 +1417,27 @@ static void pm_selection_changed(GtkTreeSelection *selection, gpointer user_data
 }
 
 
+static gboolean find_iter_for_plugin(Plugin *p, GtkTreeModel *model, GtkTreeIter *iter)
+{
+	Plugin *pp;
+	gboolean valid;
+
+	for (valid = gtk_tree_model_get_iter_first(model, iter);
+	     valid;
+	     valid = gtk_tree_model_iter_next(model, iter))
+	{
+		gtk_tree_model_get(model, iter, PLUGIN_COLUMN_PLUGIN, &pp, -1);
+		if (p == pp)
+			return TRUE;
+	}
+
+	return FALSE;
+}
+
+
+static void pm_populate(GtkTreeStore *store);
+
+
 static void pm_plugin_toggled(GtkCellRendererToggle *cell, gchar *pth, gpointer data)
 {
 	gboolean old_state, state;
@@ -1146,9 +1447,10 @@ static void pm_plugin_toggled(GtkCellRendererToggle *cell, gchar *pth, gpointer
 	GtkTreePath *path = gtk_tree_path_new_from_string(pth);
 	GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(pm_widgets.tree));
 	Plugin *p;
+	Plugin *proxy;
+	guint prev_num_proxies;
 
 	gtk_tree_model_get_iter(model, &iter, path);
-	gtk_tree_path_free(path);
 
 	gtk_tree_model_get(model, &iter,
 		PLUGIN_COLUMN_CHECK, &old_state,
@@ -1156,15 +1458,20 @@ static void pm_plugin_toggled(GtkCellRendererToggle *cell, gchar *pth, gpointer
 
 	/* no plugins item */
 	if (p == NULL)
+	{
+		gtk_tree_path_free(path);
 		return;
+	}
 
 	gtk_tree_model_filter_convert_iter_to_child_iter(
 		GTK_TREE_MODEL_FILTER(model), &store_iter, &iter);
 
 	state = ! old_state; /* toggle the state */
 
-	/* save the filename of the plugin */
+	/* save the filename and proxy of the plugin */
 	file_name = g_strdup(p->filename);
+	proxy = p->proxy;
+	prev_num_proxies = active_proxies.length;
 
 	/* unload plugin module */
 	if (!state)
@@ -1174,11 +1481,11 @@ static void pm_plugin_toggled(GtkCellRendererToggle *cell, gchar *pth, gpointer
 	plugin_free(p);
 
 	/* reload plugin module and initialize it if item is checked */
-	p = plugin_new(file_name, state, TRUE);
+	p = plugin_new(proxy, file_name, state, TRUE);
 	if (!p)
 	{
 		/* plugin file may no longer be on disk, or is now incompatible */
-		gtk_list_store_remove(pm_widgets.store, &store_iter);
+		gtk_tree_store_remove(pm_widgets.store, &store_iter);
 	}
 	else
 	{
@@ -1186,16 +1493,89 @@ static void pm_plugin_toggled(GtkCellRendererToggle *cell, gchar *pth, gpointer
 			keybindings_load_keyfile();		/* load shortcuts */
 
 		/* update model */
-		gtk_list_store_set(pm_widgets.store, &store_iter,
+		gtk_tree_store_set(pm_widgets.store, &store_iter,
 			PLUGIN_COLUMN_CHECK, state,
 			PLUGIN_COLUMN_PLUGIN, p, -1);
 
 		/* set again the sensitiveness of the configure and help buttons */
 		pm_update_buttons(p);
+
+		/* Depending on the state disable the checkbox for the proxy of this plugin, and
+		 * only re-enable if the proxy is not used by any other plugin */
+		if (p->proxy != &builtin_so_proxy_plugin)
+		{
+			GtkTreeIter parent;
+			gboolean can_uncheck;
+			GtkTreePath *store_path = gtk_tree_model_filter_convert_path_to_child_path(
+			                                GTK_TREE_MODEL_FILTER(model), path);
+
+			g_warn_if_fail(store_path != NULL);
+			if (gtk_tree_path_up(store_path))
+			{
+				gtk_tree_model_get_iter(GTK_TREE_MODEL(pm_widgets.store), &parent, store_path);
+
+				if (state)
+					can_uncheck = FALSE;
+				else
+					can_uncheck = p->proxy->proxied_count == 0;
+
+				gtk_tree_store_set(pm_widgets.store, &parent,
+					PLUGIN_COLUMN_CAN_UNCHECK, can_uncheck, -1);
+			}
+			gtk_tree_path_free(store_path);
+		}
 	}
+	/* We need to find out if a proxy was added or removed because that affects the plugin list
+	 * presented by the plugin manager */
+	if (prev_num_proxies != active_proxies.length)
+	{
+		/* Rescan the plugin list as we now support more. Gives some "already loaded" warnings
+		 * they are unproblematic */
+		if (prev_num_proxies < active_proxies.length)
+			load_all_plugins();
+
+		pm_populate(pm_widgets.store);
+		gtk_tree_view_expand_row(GTK_TREE_VIEW(pm_widgets.tree), path, FALSE);
+	}
+
+	gtk_tree_path_free(path);
 	g_free(file_name);
 }
 
+static void pm_populate(GtkTreeStore *store)
+{
+	GtkTreeIter iter;
+	GList *list;
+
+	gtk_tree_store_clear(store);
+	list = g_list_first(plugin_list);
+	if (list == NULL)
+	{
+		gtk_tree_store_append(store, &iter, NULL);
+		gtk_tree_store_set(store, &iter, PLUGIN_COLUMN_CHECK, FALSE,
+				PLUGIN_COLUMN_PLUGIN, NULL, -1);
+	}
+	else
+	{
+		for (; list != NULL; list = list->next)
+		{
+			Plugin *p = list->data;
+			GtkTreeIter parent;
+
+			if (p->proxy != &builtin_so_proxy_plugin
+			        && find_iter_for_plugin(p->proxy, GTK_TREE_MODEL(pm_widgets.store), &parent))
+				gtk_tree_store_append(store, &iter, &parent);
+			else
+				gtk_tree_store_append(store, &iter, NULL);
+
+			gtk_tree_store_set(store, &iter,
+				PLUGIN_COLUMN_CHECK, is_active_plugin(p),
+				PLUGIN_COLUMN_PLUGIN, p,
+				PLUGIN_COLUMN_CAN_UNCHECK, (p->proxied_count == 0),
+				-1);
+		}
+	}
+}
 
 static gboolean pm_treeview_query_tooltip(GtkWidget *widget, gint x, gint y,
 		gboolean keyboard_mode, GtkTooltip *tooltip, gpointer user_data)
@@ -1204,26 +1584,33 @@ static gboolean pm_treeview_query_tooltip(GtkWidget *widget, gint x, gint y,
 	GtkTreeIter iter;
 	GtkTreePath *path;
 	Plugin *p = NULL;
+	gboolean can_uncheck = TRUE;
 
 	if (! gtk_tree_view_get_tooltip_context(GTK_TREE_VIEW(widget), &x, &y, keyboard_mode,
 			&model, &path, &iter))
 		return FALSE;
 
-	gtk_tree_model_get(model, &iter, PLUGIN_COLUMN_PLUGIN, &p, -1);
+	gtk_tree_model_get(model, &iter, PLUGIN_COLUMN_PLUGIN, &p, PLUGIN_COLUMN_CAN_UNCHECK, &can_uncheck, -1);
 	if (p != NULL)
 	{
-		gchar *markup;
-		gchar *details;
+		gchar *prefix, *suffix, *details, *markup;
+		const gchar *uchk;
 
+		uchk = can_uncheck ?
+		       "" : _("\n<i>Other plugins depend on this. Disable them first to allow deactivation.</i>\n");
+		/* Four allocations is less than ideal but meh */
 		details = g_strdup_printf(_("Version:\t%s\nAuthor(s):\t%s\nFilename:\t%s"),
 			p->info.version, p->info.author, p->filename);
-		markup = g_markup_printf_escaped("<b>%s</b>\n%s\n<small><i>\n%s</i></small>",
-			p->info.name, p->info.description, details);
+		prefix = g_markup_printf_escaped("<b>%s</b>\n%s\n", p->info.name, p->info.description);
+		suffix = g_markup_printf_escaped("<small><i>\n%s</i></small>", details);
+		markup = g_strconcat(prefix, uchk, suffix, NULL);
 
 		gtk_tooltip_set_markup(tooltip, markup);
 		gtk_tree_view_set_tooltip_row(GTK_TREE_VIEW(widget), tooltip, path);
 
 		g_free(details);
+		g_free(suffix);
+		g_free(prefix);
 		g_free(markup);
 	}
 	gtk_tree_path_free(path);
@@ -1328,6 +1715,9 @@ static gboolean pm_tree_filter_func(GtkTreeModel *model, GtkTreeIter *iter, gpoi
 	gchar *haystack, *filename;
 
 	gtk_tree_model_get(model, iter, PLUGIN_COLUMN_PLUGIN, &plugin, -1);
+
+	if (!plugin)
+		return FALSE;
 	key = gtk_entry_get_text(GTK_ENTRY(pm_widgets.filter_entry));
 
 	filename = g_path_get_basename(plugin->filename);
@@ -1356,13 +1746,11 @@ static void on_pm_tree_filter_entry_icon_release_cb(GtkEntry *entry, GtkEntryIco
 }
 
 
-static void pm_prepare_treeview(GtkWidget *tree, GtkListStore *store)
+static void pm_prepare_treeview(GtkWidget *tree, GtkTreeStore *store)
 {
 	GtkCellRenderer *text_renderer, *checkbox_renderer;
 	GtkTreeViewColumn *column;
 	GtkTreeModel *filter_model;
-	GtkTreeIter iter;
-	GList *list;
 	GtkTreeSelection *sel;
 
 	g_signal_connect(tree, "query-tooltip", G_CALLBACK(pm_treeview_query_tooltip), NULL);
@@ -1371,7 +1759,8 @@ static void pm_prepare_treeview(GtkWidget *tree, GtkListStore *store)
 
 	checkbox_renderer = gtk_cell_renderer_toggle_new();
 	column = gtk_tree_view_column_new_with_attributes(
-		_("Active"), checkbox_renderer, "active", PLUGIN_COLUMN_CHECK, NULL);
+		_("Active"), checkbox_renderer,
+		"active", PLUGIN_COLUMN_CHECK, "activatable", PLUGIN_COLUMN_CAN_UNCHECK, NULL);
 	gtk_tree_view_append_column(GTK_TREE_VIEW(tree), column);
 	g_signal_connect(checkbox_renderer, "toggled", G_CALLBACK(pm_plugin_toggled), NULL);
 
@@ -1396,27 +1785,6 @@ static void pm_prepare_treeview(GtkWidget *tree, GtkListStore *store)
 
 	g_signal_connect(tree, "button-press-event", G_CALLBACK(pm_treeview_button_press_cb), NULL);
 
-	list = g_list_first(plugin_list);
-	if (list == NULL)
-	{
-		gtk_list_store_append(store, &iter);
-		gtk_list_store_set(store, &iter, PLUGIN_COLUMN_CHECK, FALSE,
-				PLUGIN_COLUMN_PLUGIN, NULL, -1);
-	}
-	else
-	{
-		Plugin *p;
-		for (; list != NULL; list = list->next)
-		{
-			p = list->data;
-
-			gtk_list_store_append(store, &iter);
-			gtk_list_store_set(store, &iter,
-				PLUGIN_COLUMN_CHECK, is_active_plugin(p),
-				PLUGIN_COLUMN_PLUGIN, p,
-				-1);
-		}
-	}
 	/* filter */
 	filter_model = gtk_tree_model_filter_new(GTK_TREE_MODEL(store), NULL);
 	gtk_tree_model_filter_set_visible_func(
@@ -1424,8 +1792,9 @@ static void pm_prepare_treeview(GtkWidget *tree, GtkListStore *store)
 
 	/* set model to tree view */
 	gtk_tree_view_set_model(GTK_TREE_VIEW(tree), filter_model);
-	g_object_unref(store);
 	g_object_unref(filter_model);
+
+	pm_populate(store);
 }
 
 
@@ -1533,9 +1902,11 @@ static void pm_show_dialog(GtkMenuItem *menuitem, gpointer user_data)
 
 	/* prepare treeview */
 	pm_widgets.tree = gtk_tree_view_new();
-	pm_widgets.store = gtk_list_store_new(
-		PLUGIN_N_COLUMNS, G_TYPE_BOOLEAN, G_TYPE_POINTER);
+	pm_widgets.store = gtk_tree_store_new(
+		PLUGIN_N_COLUMNS, G_TYPE_BOOLEAN, G_TYPE_BOOLEAN, G_TYPE_POINTER);
 	pm_prepare_treeview(pm_widgets.tree, pm_widgets.store);
+	gtk_tree_view_expand_all(GTK_TREE_VIEW(pm_widgets.tree));
+	g_object_unref(pm_widgets.store);
 
 	swin = gtk_scrolled_window_new(NULL, NULL);
 	gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(swin),
@@ -1584,4 +1955,64 @@ static void pm_show_dialog(GtkMenuItem *menuitem, gpointer user_data)
 }
 
 
+/** Register the plugin as a proxy for other plugins
+ *
+ * Proxy plugins register a list of file extensions and a set of callbacks that are called
+ * appropriately. A plugin can be a proxy for multiple types of sub-plugins by handling
+ * separate file extensions, however they must share the same set of hooks, because this
+ * function can only be called at most once per plugin.
+ *
+ * Each callback receives the plugin-defined data as parameter (see geany_plugin_register()). The
+ * callbacks must be set prior to calling this, by assigning to @a plugin->proxy_funcs.
+ * GeanyProxyFuncs::load and GeanyProxyFuncs::unload must be implemented.
+ *
+ * Nested proxies are unsupported at this point (TODO).
+ *
+ * @note It is entirely up to the proxy to provide access to Geany's plugin API. Native code
+ * can naturally call Geany's API directly, for interpreted languages the proxy has to
+ * implement some kind of bindings that the plugin can use.
+ *
+ * @see proxy for detailed documentation and an example.
+ *
+ * @param plugin The pointer to the plugin's GeanyPlugin instance
+ * @param extensions A @c NULL-terminated string array of file extensions, excluding the dot.
+ * @return @c TRUE if the proxy was successfully registered, otherwise @c FALSE
+ *
+ * @since 1.26 (API 226)
+ */
+GEANY_API_SYMBOL
+gboolean geany_plugin_register_proxy(GeanyPlugin *plugin, const gchar **extensions)
+{
+	Plugin *p;
+	const gchar **ext;
+	PluginProxy *proxy;
+	GList *node;
+
+	g_return_val_if_fail(plugin != NULL, FALSE);
+	g_return_val_if_fail(extensions != NULL, FALSE);
+	g_return_val_if_fail(*extensions != NULL, FALSE);
+	g_return_val_if_fail(plugin->proxy_funcs->load != NULL, FALSE);
+	g_return_val_if_fail(plugin->proxy_funcs->unload != NULL, FALSE);
+
+	p = plugin->priv;
+	/* Check if this was called aready. We want to reserve for the use case of calling
+	 * this again to set new supported extensions (for example, based on proxy configuration). */
+	foreach_list(node, active_proxies.head)
+	{
+		proxy = node->data;
+		g_return_if_fail(p != proxy->plugin);
+	}
+
+	foreach_strv(ext, extensions)
+	{
+		proxy = g_new(PluginProxy, 1);
+		g_strlcpy(proxy->extension, *ext, sizeof(proxy->extension));
+		proxy->plugin = p;
+		/* prepend, so that plugins automatically override core providers for a given extension */
+		g_queue_push_head(&active_proxies, proxy);
+	}
+
+	return TRUE;
+}
+
 #endif


Modified: src/pluginutils.c
11 lines changed, 2 insertions(+), 9 deletions(-)
===================================================================
@@ -96,8 +96,7 @@ GEANY_API_SYMBOL
 void plugin_module_make_resident(GeanyPlugin *plugin)
 {
 	g_return_if_fail(plugin);
-
-	g_module_make_resident(plugin->priv->module);
+	plugin_make_resident(plugin->priv);
 }
 
 
@@ -444,12 +443,7 @@ static void connect_plugin_signals(GtkBuilder *builder, GObject *object,
 	gpointer symbol = NULL;
 	struct BuilderConnectData *data = user_data;
 
-	if (!g_module_symbol(data->plugin->priv->module, handler_name, &symbol))
-	{
-		g_warning("Failed to locate signal handler for '%s': %s",
-			signal_name, g_module_error());
-		return;
-	}
+	symbol = plugin_get_module_symbol(data->plugin->priv, handler_name);
 
 	plugin_signal_connect(data->plugin, object, signal_name, FALSE,
 		G_CALLBACK(symbol) /*ub?*/, data->user_data);
@@ -503,7 +497,6 @@ void plugin_builder_connect_signals(GeanyPlugin *plugin,
 	struct BuilderConnectData data = { NULL };
 
 	g_return_if_fail(plugin != NULL && plugin->priv != NULL);
-	g_return_if_fail(plugin->priv->module != NULL);
 	g_return_if_fail(GTK_IS_BUILDER(builder));
 
 	data.user_data = user_data;


Modified: src/utils.h
8 lines changed, 8 insertions(+), 0 deletions(-)
===================================================================
@@ -116,6 +116,14 @@ G_BEGIN_DECLS
 #define foreach_slist(node, list) \
 	foreach_list(node, list)
 
+/* Iterates all the nodes in @a list. Safe against removal during iteration
+ * @param node should be a (@c GList*).
+ * @param list @c GList to traverse. */
+#define foreach_list_safe(node, list) \
+	for (GList *_node = (list), *_next = (list) ? (list)->next : NULL; \
+	     (node = _node) != NULL; \
+	     _node = _next, _next = _next ? _next->next : NULL)
+
 /** Iterates through each unsorted filename in a @c GDir.
  * @param filename (@c const @c gchar*) locale-encoded filename, without path. Do not modify or free.
  * @param dir @c GDir created with @c g_dir_open(). Call @c g_dir_close() afterwards.



--------------
This E-Mail was brought to you by github_commit_mail.py (Source: https://github.com/geany/infrastructure).


More information about the Commits mailing list