Merge pull request #629 from kugel-/pluxy

Add support for plugins acting as proxies for foreign plugins,
promoting foreign plugins to first-class citizen.
This commit is contained in:
Colomban Wendling 2015-10-06 15:53:14 +02:00
commit c6952c7599
10 changed files with 1264 additions and 151 deletions

View File

@ -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
@page proxy Proxy Plugin HowTo
@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.
@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.
@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.
@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
@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.
@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.
@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:
@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
@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().
@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);
}
@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.
@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);
}
@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.
@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;
}
@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.
@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;
}
@endcode
demoproxy_unload() simply releases all resources aquired in demoproxy_load(). It does not have to
do anything else in for unloading.
@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);
}
@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().
@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);
}
@endcode
*/

View File

@ -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

202
plugins/demoproxy.c Normal file
View File

@ -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);
}

15
plugins/demoproxytest.px Normal file
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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;

View File

@ -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.