From b6c86986d953cbb0e646e0dce8448fe0c401ca34 Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Fri, 6 Nov 2015 23:52:15 +0100 Subject: [PATCH 1/4] geanypy: Fix build on some systems pkg-config exports PYTHON_LIBS and not PYTHON_LDFLAGS --- geanypy/src/Makefile.am | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geanypy/src/Makefile.am b/geanypy/src/Makefile.am index 303b07a81..3e4094384 100644 --- a/geanypy/src/Makefile.am +++ b/geanypy/src/Makefile.am @@ -8,7 +8,7 @@ geanypy_la_CPPFLAGS = @GEANY_CFLAGS@ @PYGTK_CFLAGS@ @PYTHON_CPPFLAGS@ \ -DGEANYPY_PYTHON_DIR="\"$(libdir)/geany/geanypy\"" \ -DGEANYPY_PLUGIN_DIR="\"$(datadir)/geany/geanypy/plugins\"" geanypy_la_CFLAGS = @GEANYPY_CFLAGS@ @GMODULE_CFLAGS@ -geanypy_la_LIBADD = @GEANY_LIBS@ @PYGTK_LIBS@ @PYTHON_LDFLAGS@ \ +geanypy_la_LIBADD = @GEANY_LIBS@ @PYGTK_LIBS@ @PYTHON_LIBS@ \ @PYTHON_EXTRA_LIBS@ @PYTHON_EXTRA_LDFLAGS@ \ @GMODULE_LIBS@ geanypy_la_SOURCES = geanypy-app.c \ From 0564deeb369166015c0621e88dea9b88b722c858 Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Fri, 6 Nov 2015 23:53:26 +0100 Subject: [PATCH 2/4] geanypy: convert to a proxy plugin This allows to drop the custom loader and plugin manager and instead make use of Geany's new proxy plugin feature, where python plugins are embedded into the standard plugin manager as first class citizen. Existing plugins continue to run with one exception: The help and configure methods have been renamed, and in case of configure the samantics have changed accordingly to Geany's unified configure dialog for all plugins. So existing scripts that used either show_help() or show_configure() need to to the following: 1) rename show_help() to help() 2a) rename show_configure to configure() 2b) change configure to just return the content widget and remove the creation of a custom dialog The plugin script dir is now $geanylibdir/geany, the same as for native plugns. Geany loooks only there and doesn't make a difference between native and sub-plugins. --- geanypy/geany/Makefile.am | 2 - geanypy/geany/__init__.py | 2 - geanypy/geany/loader.py | 172 -------------- geanypy/geany/manager.py | 179 --------------- geanypy/geany/plugin.py | 1 - geanypy/plugins/Makefile.am | 2 +- geanypy/plugins/console.py | 16 +- geanypy/src/Makefile.am | 2 +- geanypy/src/geanypy-plugin.c | 335 +++++++++++++++++----------- geanypy/src/geanypy-plugin.h | 5 +- geanypy/src/geanypy-signalmanager.c | 1 + 11 files changed, 216 insertions(+), 501 deletions(-) delete mode 100644 geanypy/geany/loader.py delete mode 100644 geanypy/geany/manager.py diff --git a/geanypy/geany/Makefile.am b/geanypy/geany/Makefile.am index 880c5f662..62bac20df 100644 --- a/geanypy/geany/Makefile.am +++ b/geanypy/geany/Makefile.am @@ -1,7 +1,5 @@ geanypy_sources = __init__.py \ console.py \ - manager.py \ - loader.py \ plugin.py \ signalmanager.py geanypy_objects = $(geanypy_sources:.py=.pyc) diff --git a/geanypy/geany/__init__.py b/geanypy/geany/__init__.py index 493dfa4d8..264d6eee1 100644 --- a/geanypy/geany/__init__.py +++ b/geanypy/geany/__init__.py @@ -15,9 +15,7 @@ import encoding import filetypes import highlighting -import loader import main -import manager import msgwindow import navqueue import prefs diff --git a/geanypy/geany/loader.py b/geanypy/geany/loader.py deleted file mode 100644 index 14b9b73d6..000000000 --- a/geanypy/geany/loader.py +++ /dev/null @@ -1,172 +0,0 @@ -import os -import imp -from collections import namedtuple -import geany - -PluginInfo = namedtuple('PluginInfo', 'filename, name, version, description, author, cls') - - -class PluginLoader(object): - - plugins = {} - - def __init__(self, plugin_dirs): - - self.plugin_dirs = plugin_dirs - - self.available_plugins = [] - for plugin in self.iter_plugin_info(): - self.available_plugins.append(plugin) - - self.restore_loaded_plugins() - - - def update_loaded_plugins_file(self): - for path in self.plugin_dirs: - if os.path.isdir(path): - try: - state_file = os.path.join(path, '.loaded_plugins') - with open(state_file, 'w') as f: - for plugfn in self.plugins: - f.write("%s\n" % plugfn) - except IOError as err: - if err.errno == 13: #perms - pass - else: - raise - - - def restore_loaded_plugins(self): - loaded_plugins = [] - for path in reversed(self.plugin_dirs): - state_file = os.path.join(path, ".loaded_plugins") - if os.path.exists(state_file): - for line in open(state_file): - line = line.strip() - if line not in loaded_plugins: - loaded_plugins.append(line) - for filename in loaded_plugins: - self.load_plugin(filename) - - - def load_all_plugins(self): - - for plugin_info in self.iter_plugin_info(): - if plugin_filename.endswith('test.py'): # hack for testing - continue - plug = self.load_plugin(plugin_info.filename) - if plug: - print("Loaded plugin: %s" % plugin_info.filename) - print(" Name: %s v%s" % (plug.name, plug.version)) - print(" Desc: %s" % plug.description) - print(" Author: %s" % plug.author) - - - def unload_all_plugins(self): - - for plugin in self.plugins: - self.unload_plugin(plugin) - - - def reload_all_plugins(self): - - self.unload_all_plugins() - self.load_all_plugins() - - - def iter_plugin_info(self): - - for d in self.plugin_dirs: - if os.path.isdir(d): - for current_file in os.listdir(d): - #check inside folders inside the plugins dir so we can load .py files here as plugins - current_path=os.path.abspath(os.path.join(d, current_file)) - if os.path.isdir(current_path): - for plugin_folder_file in os.listdir(current_path): - if plugin_folder_file.endswith('.py'): - #loop around results if its fails to load will never reach yield - for p in self.load_plugin_info(current_path,plugin_folder_file): - yield p - - #not a sub directory so if it ends with .py lets just attempt to load it as a plugin - if current_file.endswith('.py'): - #loop around results if its fails to load will never reach yield - for p in self.load_plugin_info(d,current_file): - yield p - - def load_plugin_info(self,d,f): - filename = os.path.abspath(os.path.join(d, f)) - if filename.endswith("test.py"): - pass - text = open(filename).read() - module_name = os.path.basename(filename)[:-3] - try: - module = imp.load_source(module_name, filename) - except ImportError as exc: - print "Error: failed to import settings module ({})".format(exc) - module=None - if module: - for k, v in module.__dict__.iteritems(): - if k == geany.Plugin.__name__: - continue - try: - if issubclass(v, geany.Plugin): - inf = PluginInfo( - filename, - getattr(v, '__plugin_name__'), - getattr(v, '__plugin_version__', ''), - getattr(v, '__plugin_description__', ''), - getattr(v, '__plugin_author__', ''), - v) - yield inf - - except TypeError: - continue - - - def load_plugin(self, filename): - - for avail in self.available_plugins: - if avail.filename == filename: - inst = avail.cls() - self.plugins[filename] = inst - self.update_loaded_plugins_file() - geany.ui_utils.set_statusbar('GeanyPy: plugin activated: %s' % - inst.name, True) - return inst - - - def unload_plugin(self, filename): - - try: - plugin = self.plugins[filename] - name = plugin.name - plugin.cleanup() - del self.plugins[filename] - self.update_loaded_plugins_file() - geany.ui_utils.set_statusbar('GeanyPy: plugin deactivated: %s' % - name, True) - except KeyError: - print("Unable to unload plugin '%s': it's not loaded" % filename) - - - def reload_plugin(self, filename): - - if filename in self.plugins: - self.unload_plugin(filename) - self.load_plugin(filename) - - - def plugin_has_help(self, filename): - - for plugin_info in self.iter_plugin_info(): - if plugin_info.filename == filename: - return hasattr(plugin_info.cls, 'show_help') - - - def plugin_has_configure(self, filename): - - try: - return hasattr(self.plugins[filename], 'show_configure') - except KeyError: - return None diff --git a/geanypy/geany/manager.py b/geanypy/geany/manager.py deleted file mode 100644 index 9c7e9a478..000000000 --- a/geanypy/geany/manager.py +++ /dev/null @@ -1,179 +0,0 @@ -import gtk -import gobject -import glib -from htmlentitydefs import name2codepoint -from loader import PluginLoader - - -class PluginManager(gtk.Dialog): - - def __init__(self, plugin_dirs=[]): - gtk.Dialog.__init__(self, title="Plugin Manager") - self.loader = PluginLoader(plugin_dirs) - - self.set_default_size(400, 450) - self.set_has_separator(True) - icon = self.render_icon(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_MENU) - self.set_icon(icon) - - self.connect("response", lambda w,d: self.hide()) - - vbox = gtk.VBox(False, 12) - vbox.set_border_width(12) - - lbl = gtk.Label("Choose plugins to load or unload:") - lbl.set_alignment(0.0, 0.5) - vbox.pack_start(lbl, False, False, 0) - - sw = gtk.ScrolledWindow() - sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) - sw.set_shadow_type(gtk.SHADOW_ETCHED_IN) - vbox.pack_start(sw, True, True, 0) - - self.treeview = gtk.TreeView() - sw.add(self.treeview) - - vbox.show_all() - - self.get_content_area().add(vbox) - - action_area = self.get_action_area() - action_area.set_spacing(0) - action_area.set_homogeneous(False) - - btn = gtk.Button(stock=gtk.STOCK_CLOSE) - btn.set_border_width(6) - btn.connect("clicked", lambda x: self.response(gtk.RESPONSE_CLOSE)) - action_area.pack_start(btn, False, True, 0) - btn.show() - - self.btn_help = gtk.Button(stock=gtk.STOCK_HELP) - self.btn_help.set_border_width(6) - self.btn_help.set_no_show_all(True) - action_area.pack_start(self.btn_help, False, True, 0) - action_area.set_child_secondary(self.btn_help, True) - - self.btn_prefs = gtk.Button(stock=gtk.STOCK_PREFERENCES) - self.btn_prefs.set_border_width(6) - self.btn_prefs.set_no_show_all(True) - action_area.pack_start(self.btn_prefs, False, True, 0) - action_area.set_child_secondary(self.btn_prefs, True) - - action_area.show() - - self.load_plugins_list() - - - def on_help_button_clicked(self, button, treeview, model): - path = treeview.get_cursor()[0] - iter = model.get_iter(path) - filename = model.get_value(iter, 2) - for plugin in self.loader.available_plugins: - if plugin.filename == filename: - plugin.cls.show_help() - break - else: - print("Plugin does not support help function") - - - def on_preferences_button_clicked(self, button, treeview, model): - path = treeview.get_cursor()[0] - iter = model.get_iter(path) - filename = model.get_value(iter, 2) - try: - self.loader.plugins[filename].show_configure() - except KeyError: - print("Plugin is not loaded, can't run configure function") - - - def activate_plugin(self, filename): - self.loader.load_plugin(filename) - - - def deactivate_plugin(self, filename): - self.loader.unload_plugin(filename) - - - def load_plugins_list(self): - liststore = gtk.ListStore(gobject.TYPE_BOOLEAN, str, str) - - self.btn_help.connect("clicked", - self.on_help_button_clicked, self.treeview, liststore) - - self.btn_prefs.connect("clicked", - self.on_preferences_button_clicked, self.treeview, liststore) - - self.treeview.set_model(liststore) - self.treeview.set_headers_visible(False) - self.treeview.set_grid_lines(True) - - check_renderer = gtk.CellRendererToggle() - check_renderer.set_radio(False) - check_renderer.connect('toggled', self.on_plugin_load_toggled, liststore) - text_renderer = gtk.CellRendererText() - - check_column = gtk.TreeViewColumn(None, check_renderer, active=0) - text_column = gtk.TreeViewColumn(None, text_renderer, markup=1) - - self.treeview.append_column(check_column) - self.treeview.append_column(text_column) - - self.treeview.connect('row-activated', - self.on_row_activated, check_renderer, liststore) - self.treeview.connect('cursor-changed', - self.on_selected_plugin_changed, liststore) - - self.load_sorted_plugins_info(liststore) - - - def load_sorted_plugins_info(self, list_store): - - plugin_info_list = list(self.loader.iter_plugin_info()) - #plugin_info_list.sort(key=lambda pi: pi[1]) - - for plugin_info in plugin_info_list: - - lbl = str('%s %s\n%s\n' + - 'Author: %s\n' + - 'Filename: %s') % ( - glib.markup_escape_text(plugin_info.name), - glib.markup_escape_text(plugin_info.version), - glib.markup_escape_text(plugin_info.description), - glib.markup_escape_text(plugin_info.author), - glib.markup_escape_text(plugin_info.filename)) - - loaded = plugin_info.filename in self.loader.plugins - - list_store.append([loaded, lbl, plugin_info.filename]) - - - def on_selected_plugin_changed(self, treeview, model): - - path = treeview.get_cursor()[0] - iter = model.get_iter(path) - filename = model.get_value(iter, 2) - active = model.get_value(iter, 0) - - if self.loader.plugin_has_configure(filename): - self.btn_prefs.set_visible(True) - else: - self.btn_prefs.set_visible(False) - - if self.loader.plugin_has_help(filename): - self.btn_help.set_visible(True) - else: - self.btn_help.set_visible(False) - - - def on_plugin_load_toggled(self, cell, path, model): - active = not cell.get_active() - iter = model.get_iter(path) - model.set_value(iter, 0, active) - if active: - self.activate_plugin(model.get_value(iter, 2)) - else: - self.deactivate_plugin(model.get_value(iter, 2)) - - - def on_row_activated(self, tvw, path, view_col, cell, model): - self.on_plugin_load_toggled(cell, path, model) diff --git a/geanypy/geany/plugin.py b/geanypy/geany/plugin.py index f8adef6be..8a9a2afce 100644 --- a/geanypy/geany/plugin.py +++ b/geanypy/geany/plugin.py @@ -49,7 +49,6 @@ class Plugin(object): #__plugin_version__ = None #__plugin_author__ = None - _events = { "document-open": [], # TODO: add more events here diff --git a/geanypy/plugins/Makefile.am b/geanypy/plugins/Makefile.am index 6aa07ff22..3cb662de8 100644 --- a/geanypy/plugins/Makefile.am +++ b/geanypy/plugins/Makefile.am @@ -1,4 +1,4 @@ geanypy_plugins = demo.py hello.py console.py -geanypydir = $(datadir)/geany/geanypy/plugins +geanypydir = $(libdir)/geany geanypy_DATA = $(geanypy_plugins) EXTRA_DIST = $(geanypy_plugins) diff --git a/geanypy/plugins/console.py b/geanypy/plugins/console.py index ee26ae35b..df5180e7e 100644 --- a/geanypy/plugins/console.py +++ b/geanypy/plugins/console.py @@ -191,16 +191,7 @@ def on_bg_color_changed(self, clr_btn, data=None): self.bg = clr_btn.get_color().to_string() - def show_configure(self): - dialog = gtk.Dialog("Configure Python Console", - geany.main_widgets.window, - gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, - (gtk.STOCK_CLOSE, gtk.RESPONSE_ACCEPT)) - - dialog.set_has_separator(True) - - content_area = dialog.get_content_area() - content_area.set_border_width(6) + def configure(self, dialog): vbox = gtk.VBox(spacing=6) vbox.set_border_width(6) @@ -306,8 +297,5 @@ def show_configure(self): vbox.pack_start(fra_general, True, True, 0) vbox.pack_start(fra_appearances, False, True, 0) - content_area.pack_start(vbox, True, True, 0) - content_area.show_all() - dialog.run() - dialog.destroy() + return vbox diff --git a/geanypy/src/Makefile.am b/geanypy/src/Makefile.am index 3e4094384..c81b0f51e 100644 --- a/geanypy/src/Makefile.am +++ b/geanypy/src/Makefile.am @@ -6,7 +6,7 @@ geanyplugindir = $(libdir)/geany geanypy_la_LDFLAGS = -module -avoid-version -Wl,--export-dynamic geanypy_la_CPPFLAGS = @GEANY_CFLAGS@ @PYGTK_CFLAGS@ @PYTHON_CPPFLAGS@ \ -DGEANYPY_PYTHON_DIR="\"$(libdir)/geany/geanypy\"" \ - -DGEANYPY_PLUGIN_DIR="\"$(datadir)/geany/geanypy/plugins\"" + -DGEANYPY_PLUGIN_DIR="\"$(libdir)/geany\"" geanypy_la_CFLAGS = @GEANYPY_CFLAGS@ @GMODULE_CFLAGS@ geanypy_la_LIBADD = @GEANY_LIBS@ @PYGTK_LIBS@ @PYTHON_LIBS@ \ @PYTHON_EXTRA_LIBS@ @PYTHON_EXTRA_LDFLAGS@ \ diff --git a/geanypy/src/geanypy-plugin.c b/geanypy/src/geanypy-plugin.c index 338067c6a..dfcf2d1a6 100644 --- a/geanypy/src/geanypy-plugin.c +++ b/geanypy/src/geanypy-plugin.c @@ -27,25 +27,10 @@ #include "geanypy.h" -G_MODULE_EXPORT GeanyPlugin *geany_plugin; -G_MODULE_EXPORT GeanyData *geany_data; -G_MODULE_EXPORT GeanyFunctions *geany_functions; - - -G_MODULE_EXPORT PLUGIN_VERSION_CHECK(211) - -G_MODULE_EXPORT PLUGIN_SET_INFO( - _("GeanyPy"), - _("Python plugins support"), - "1.0", - "Matthew Brush ") - - -static GtkWidget *loader_item = NULL; -static PyObject *manager = NULL; -static gchar *plugin_dir = NULL; -static SignalManager *signal_manager = NULL; +#include +#include +GeanyData *geany_data; /* Forward declarations to prevent compiler warnings. */ PyMODINIT_FUNC initapp(void); @@ -131,7 +116,9 @@ GeanyPy_start_interpreter(void) "import os, sys\n" "path = '%s'.replace('~', os.path.expanduser('~'))\n" "sys.path.append(path)\n" - "import geany\n", py_dir); + "path = '%s'.replace('~', os.path.expanduser('~'))\n" + "sys.path.append(path)\n" + "import geany\n", py_dir, GEANYPY_PLUGIN_DIR); g_free(py_dir); PyRun_SimpleString(init_code); @@ -146,141 +133,239 @@ GeanyPy_stop_interpreter(void) Py_Finalize(); } +typedef struct +{ + PyObject *base; + SignalManager *signal_manager; +} +GeanyPyData; -static void -GeanyPy_init_manager(const gchar *dir) +typedef struct { - PyObject *module, *man, *args; - gchar *sys_plugin_dir = NULL; + PyObject *class; + PyObject *module; + PyObject *instance; +} +GeanyPyPluginData; - g_return_if_fail(dir != NULL); +static gboolean has_error(void) +{ + if (PyErr_Occurred()) + { + PyErr_Print(); + return TRUE; + } + return FALSE; +} - module = PyImport_ImportModule("geany.manager"); - if (module == NULL) - { - g_warning(_("Failed to import manager module")); - return; - } +static gboolean geanypy_proxy_init(GeanyPlugin *plugin, gpointer pdata) +{ + GeanyPyPluginData *data = (GeanyPyPluginData *) pdata; - man = PyObject_GetAttrString(module, "PluginManager"); - Py_DECREF(module); + data->instance = PyObject_CallObject(data->class, NULL); + if (has_error()) + return FALSE; - if (man == NULL) - { - g_warning(_("Failed to retrieve PluginManager from manager module")); - return; - } + return TRUE; +} -#ifdef GEANYPY_WINDOWS - { /* Detect the system plugin's dir at runtime on Windows since we - * don't really know where Geany is installed. */ - gchar *geany_base_dir; - geany_base_dir = g_win32_get_package_installation_directory_of_module(NULL); - if (geany_base_dir) - { - sys_plugin_dir = g_build_filename(geany_base_dir, "lib", "geanypy", "plugins", NULL); - g_free(geany_base_dir); - } - if (!g_file_test(sys_plugin_dir, G_FILE_TEST_EXISTS)) - { - g_warning(_("System plugin directory not found.")); - g_free(sys_plugin_dir); - sys_plugin_dir = NULL; - } - } -#else - sys_plugin_dir = g_strdup(GEANYPY_PLUGIN_DIR); -#endif +static void geanypy_proxy_cleanup(GeanyPlugin *plugin, gpointer pdata) +{ + GeanyPyPluginData *data = (GeanyPyPluginData *) pdata; - g_log(G_LOG_DOMAIN, G_LOG_LEVEL_INFO, "User plugins: %s", dir); + PyObject_CallMethod(data->instance, "cleanup", NULL); + if (has_error()) + return; +} - if (sys_plugin_dir) + +static GtkWidget *geanypy_proxy_configure(GeanyPlugin *plugin, GtkDialog *parent, gpointer pdata) +{ + GeanyPyPluginData *data = (GeanyPyPluginData *) pdata; + PyObject *o, *oparent; + GObject *widget; + + oparent = pygobject_new(G_OBJECT(parent)); + o = PyObject_CallMethod(data->instance, "configure", "O", oparent, NULL); + Py_DECREF(oparent); + + if (!has_error() && o != NULL) { - g_log(G_LOG_DOMAIN, G_LOG_LEVEL_INFO, "System plugins: %s", sys_plugin_dir); - args = Py_BuildValue("([s, s])", sys_plugin_dir, dir); - g_free(sys_plugin_dir); + /* Geany wants only the underlying GtkWidget, we must only ref that + * and free the pygobject wrapper */ + widget = g_object_ref(pygobject_get(o)); + Py_DECREF(o); + return GTK_WIDGET(widget); } - else - args = Py_BuildValue("([s])", dir); + return NULL; +} - manager = PyObject_CallObject(man, args); - if (PyErr_Occurred()) - PyErr_Print(); - Py_DECREF(man); - Py_DECREF(args); - - if (manager == NULL) - { - g_warning(_("Unable to instantiate new PluginManager")); - return; - } +static void geanypy_proxy_help(GeanyPlugin *plugin, gpointer pdata) +{ + GeanyPyPluginData *data = (GeanyPyPluginData *) pdata; + + PyObject_CallMethod(data->instance, "help", NULL); + if (has_error()) + return; } +static gint +geanypy_probe(GeanyPlugin *proxy, const gchar *filename, gpointer pdata) +{ + gchar *file_plugin = g_strdup_printf("%.*s.plugin", + (int)(strrchr(filename, '.') - filename), filename); + gint ret = PROXY_IGNORED; -static void -GeanyPy_show_manager(void) + /* avoid clash with libpeas py plugins, those come with a corresponding .plugin file */ + if (!g_file_test(file_plugin, G_FILE_TEST_EXISTS)) + ret = PROXY_MATCHED; + + g_free(file_plugin); + return ret; +} + + +static const gchar *string_from_attr(PyObject *o, const gchar *attr) { - PyObject *show_method; - - g_return_if_fail(manager != NULL); - - show_method = PyObject_GetAttrString(manager, "show_all"); - if (show_method == NULL) - { - g_warning(_("Unable to get show_all() method on plugin manager")); - return; - } - PyObject_CallObject(show_method, NULL); - Py_DECREF(show_method); + PyObject *string = PyObject_GetAttrString(o, "__plugin_name__"); + const gchar *ret = PyString_AsString(string); + Py_DECREF(string); + + return ret; +} + + +static gpointer +geanypy_load(GeanyPlugin *proxy, GeanyPlugin *subplugin, const gchar *filename, gpointer pdata) +{ + GeanyPyData *data = pdata; + PyObject *fromlist, *module, *dict, *key, *val, *found = NULL; + Py_ssize_t pos = 0; + gchar *modulename, *dot; + gpointer ret = NULL; + + modulename = g_path_get_basename(filename); + /* We are guaranteed that filename has a .py extension + * because we did geany_plugin_register_proxy() for it */ + dot = strrchr(modulename, '.'); + *dot = '\0'; + /* we need a fromlist to be able to import modules with a '.' in the + * name. -- libpeas */ + fromlist = PyTuple_New (0); + + module = PyImport_ImportModuleEx(modulename, NULL, NULL, fromlist); + if (has_error() || !module) + goto err; + + dict = PyModule_GetDict(module); + + while (PyDict_Next (dict, &pos, &key, &val) && found == NULL) + { + if (PyType_Check(val) && PyObject_IsSubclass(val, data->base)) + found = val; + } + + if (found) + { + GeanyPyPluginData *pdata = g_slice_new(GeanyPyPluginData); + PluginInfo *info = subplugin->info; + GeanyPluginFuncs *funcs = subplugin->funcs; + Py_INCREF(found); + pdata->module = module; + pdata->class = found; + pdata->instance = NULL; + info->name = string_from_attr(pdata->class, "__plugin_name__"); + info->description = string_from_attr(pdata->class, "__plugin_description__"); + info->version = string_from_attr(pdata->class, "__plugin_version__"); + info->author = string_from_attr(pdata->class, "__plugin_author__"); + funcs->init = geanypy_proxy_init; + funcs->cleanup = geanypy_proxy_cleanup; + if (PyObject_HasAttrString(found, "configure")) + funcs->configure = geanypy_proxy_configure; + if (PyObject_HasAttrString(found, "help")) + funcs->help = geanypy_proxy_help; + if (GEANY_PLUGIN_REGISTER_FULL(subplugin, 224, pdata, NULL)) + ret = pdata; + } + +err: + g_free(modulename); + Py_DECREF(fromlist); + return ret; } static void -on_python_plugin_loader_activate(GtkMenuItem *item, gpointer user_data) +geanypy_unload(GeanyPlugin *plugin, GeanyPlugin *subplugin, gpointer load_data, gpointer pdata_) { - GeanyPy_show_manager(); + GeanyPyPluginData *pdata = load_data; + + Py_XDECREF(pdata->instance); + Py_DECREF(pdata->class); + Py_DECREF(pdata->module); + while (PyGC_Collect()); + g_slice_free(GeanyPyPluginData, pdata); } -G_MODULE_EXPORT void -plugin_init(GeanyData *data) +static gboolean geanypy_init(GeanyPlugin *plugin_, gpointer pdata) { - GeanyPy_start_interpreter(); - signal_manager = signal_manager_new(geany_plugin); - - plugin_dir = g_build_filename(geany->app->configdir, - "plugins", "geanypy", "plugins", NULL); - - if (!g_file_test(plugin_dir, G_FILE_TEST_IS_DIR)) - { - if (g_mkdir_with_parents(plugin_dir, 0755) == -1) - { - g_warning(_("Unable to create Python plugins directory: %s: %s"), - plugin_dir, - strerror(errno)); - g_free(plugin_dir); - plugin_dir = NULL; - } - } - - if (plugin_dir != NULL) - GeanyPy_init_manager(plugin_dir); - - loader_item = gtk_menu_item_new_with_label(_("Python Plugin Manager")); - gtk_widget_set_sensitive(loader_item, plugin_dir != NULL); - gtk_menu_append(GTK_MENU(geany->main_widgets->tools_menu), loader_item); - gtk_widget_show(loader_item); - g_signal_connect(loader_item, "activate", - G_CALLBACK(on_python_plugin_loader_activate), NULL); + const gchar *exts[] = { "py", NULL }; + GeanyPyData *state = pdata; + PyObject *module; + + plugin_->proxy_funcs->probe = geanypy_probe; + plugin_->proxy_funcs->load = geanypy_load; + plugin_->proxy_funcs->unload = geanypy_unload; + + geany_data = plugin_->geany_data; + + GeanyPy_start_interpreter(); + state->signal_manager = signal_manager_new(plugin_); + + module = PyImport_ImportModule("geany.plugin"); + if (has_error() || !module) + goto err; + + state->base = PyObject_GetAttrString(module, "Plugin"); + Py_DECREF(module); + if (has_error() || !state->base) + goto err; + + if (!geany_plugin_register_proxy(plugin_, exts)) { + Py_DECREF(state->base); + goto err; + } + + return TRUE; + +err: + signal_manager_free(state->signal_manager); + GeanyPy_stop_interpreter(); + return FALSE; } -G_MODULE_EXPORT void plugin_cleanup(void) +static void geanypy_cleanup(GeanyPlugin *plugin, gpointer pdata) { - signal_manager_free(signal_manager); - Py_XDECREF(manager); + GeanyPyData *state = pdata; + signal_manager_free(state->signal_manager); + Py_DECREF(state->base); GeanyPy_stop_interpreter(); - gtk_widget_destroy(loader_item); - g_free(plugin_dir); +} + +G_MODULE_EXPORT void +geany_load_module(GeanyPlugin *plugin) +{ + GeanyPyData *state = g_new0(GeanyPyData, 1); + + plugin->info->name = _("GeanyPy"); + plugin->info->description = _("Python plugins support"); + plugin->info->version = "1.0"; + plugin->info->author = "Matthew Brush "; + plugin->funcs->init = geanypy_init; + plugin->funcs->cleanup = geanypy_cleanup; + + GEANY_PLUGIN_REGISTER_FULL(plugin, 224, state, g_free); } diff --git a/geanypy/src/geanypy-plugin.h b/geanypy/src/geanypy-plugin.h index 5457404d8..c97bd9606 100644 --- a/geanypy/src/geanypy-plugin.h +++ b/geanypy/src/geanypy-plugin.h @@ -26,10 +26,7 @@ extern "C" { #endif - -extern GeanyPlugin *geany_plugin; -extern GeanyData *geany_data; -extern GeanyFunctions *geany_functions; +extern GeanyData *geany_data; #ifndef PyMODINIT_FUNC diff --git a/geanypy/src/geanypy-signalmanager.c b/geanypy/src/geanypy-signalmanager.c index 0f7fd5ee7..8682da816 100644 --- a/geanypy/src/geanypy-signalmanager.c +++ b/geanypy/src/geanypy-signalmanager.c @@ -88,6 +88,7 @@ GObject *signal_manager_get_gobject(SignalManager *signal_manager) static void signal_manager_connect_signals(SignalManager *man) { + GeanyPlugin *geany_plugin = man->geany_plugin; plugin_signal_connect(geany_plugin, NULL, "build-start", TRUE, G_CALLBACK(on_build_start), man); plugin_signal_connect(geany_plugin, NULL, "document-activate", TRUE, G_CALLBACK(on_document_activate), man); plugin_signal_connect(geany_plugin, NULL, "document-before-save", TRUE, G_CALLBACK(on_document_before_save), man); From 22b0f8171a9e821e957b7123a9ddac6e1df92389 Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Fri, 6 Nov 2015 23:55:51 +0100 Subject: [PATCH 3/4] geanypy: new type geany.PluginBase This is the base class of geany.Plugin and is implemented in C. The purpose is solely to store the per-plugin GeanyPlugin that we get from Geany, so that python plugins are enabled to call API functions that require it. This is a precondition for keybinding support. --- geanypy/geany/plugin.py | 8 ++-- geanypy/src/geanypy-plugin.c | 93 +++++++++++++++++++++++++++++++++++- geanypy/src/geanypy-plugin.h | 5 ++ 3 files changed, 102 insertions(+), 4 deletions(-) diff --git a/geanypy/geany/plugin.py b/geanypy/geany/plugin.py index 8a9a2afce..212fa7ca7 100644 --- a/geanypy/geany/plugin.py +++ b/geanypy/geany/plugin.py @@ -36,8 +36,9 @@ def cleanup(self): files with a `.py` extension will be loaded. """ - -class Plugin(object): +from geany.pluginbase import PluginBase + +class Plugin(PluginBase): """ Base class for all plugins. All plugins must inherit from this in order to be properly detected. @@ -55,7 +56,8 @@ class Plugin(object): } - def __init__(self): + def __init__(self, ctx=None): + PluginBase.__init__(self, ctx) """ When the plugin is loaded its __init__() function will be called so that's a good place to put plugin initialization code. diff --git a/geanypy/src/geanypy-plugin.c b/geanypy/src/geanypy-plugin.c index dfcf2d1a6..08ed26d41 100644 --- a/geanypy/src/geanypy-plugin.c +++ b/geanypy/src/geanypy-plugin.c @@ -49,6 +49,7 @@ PyMODINIT_FUNC initscintilla(void); PyMODINIT_FUNC initsearch(void); PyMODINIT_FUNC inittemplates(void); PyMODINIT_FUNC initui_utils(void); +PyMODINIT_FUNC initpluginbase(void); static void @@ -89,6 +90,7 @@ GeanyPy_start_interpreter(void) initsearch(); inittemplates(); initui_utils(); + initpluginbase(); #ifdef GEANYPY_WINDOWS { /* On windows, get path at runtime since we don't really know where @@ -158,14 +160,36 @@ static gboolean has_error(void) return FALSE; } +static PyTypeObject PluginBaseType; + static gboolean geanypy_proxy_init(GeanyPlugin *plugin, gpointer pdata) { GeanyPyPluginData *data = (GeanyPyPluginData *) pdata; + GeanyPyPluginBase *base; + PyObject *args; + + base = PyObject_New(GeanyPyPluginBase, &PluginBaseType); + base->plugin = plugin; + + /* The new-style constructor gets a context parameter, and the class must pass + * it to the geany.Plugin constructor. The new-style constructor is required + * to have certain APIs work in it (those that need the GeanyPlugin pointer) */ + args = Py_BuildValue("(O)", base); + data->instance = PyObject_CallObject(data->class, args); + Py_DECREF(args); + Py_DECREF(base); + + if (PyErr_Occurred()) { + PyErr_Clear(); + /* If the plugin still implements the old constructor we can catch this and try again */ + data->instance = PyObject_CallObject(data->class, NULL); + } - data->instance = PyObject_CallObject(data->class, NULL); if (has_error()) return FALSE; + ((GeanyPyPluginBase *)data->instance)->plugin = plugin; + return TRUE; } @@ -369,3 +393,70 @@ geany_load_module(GeanyPlugin *plugin) GEANY_PLUGIN_REGISTER_FULL(plugin, 224, state, g_free); } + + +static void PluginBase_dealloc(GeanyPyPluginBase *self) { } + +static PyMethodDef +PluginBase_methods[] = { + { NULL } +}; + +static PyMethodDef +PluginModule_methods[] = { + { NULL } +}; + + +static PyGetSetDef +PluginBase_getseters[] = { + { NULL }, +}; + + +static int +PluginBase_init(GeanyPyPluginBase *self, PyObject *args, PyObject *kwargs) +{ + GeanyPyPluginBase *py_context; + + if (PyArg_ParseTuple(args, "O", (PyObject **) &py_context) && (PyObject *)py_context != Py_None) + self->plugin = py_context->plugin; + + return 0; +} + + +static PyTypeObject PluginBaseType = { + PyObject_HEAD_INIT(NULL) + 0, /* ob_size */ + "geany.pluginbase,PluginBase", /* tp_name */ + sizeof(GeanyPyPluginBase), /* tp_basicsize */ + 0, /* tp_itemsize */ + (destructor) PluginBase_dealloc, /* tp_dealloc */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* tp_print - tp_as_buffer */ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */ + "Wrapper around a GeanyPlugin structure." ,/* tp_doc */ + 0, 0, 0, 0, 0, 0, /* tp_traverse - tp_iternext */ + PluginBase_methods, /* tp_methods */ + 0, /* tp_members */ + PluginBase_getseters, /* tp_getset */ + 0, 0, 0, 0, 0, /* tp_base - tp_dictoffset */ + (initproc) PluginBase_init, /* tp_init */ + 0, 0, /* tp_alloc - tp_new */ +}; + + +PyMODINIT_FUNC initpluginbase(void) +{ + PyObject *m; + + PluginBaseType.tp_new = PyType_GenericNew; + if (PyType_Ready(&PluginBaseType) < 0) + return; + + m = Py_InitModule3("geany.pluginbase", PluginModule_methods, + "Plugin management."); + + Py_INCREF(&PluginBaseType); + PyModule_AddObject(m, "PluginBase", (PyObject *)&PluginBaseType); +} diff --git a/geanypy/src/geanypy-plugin.h b/geanypy/src/geanypy-plugin.h index c97bd9606..dea777c45 100644 --- a/geanypy/src/geanypy-plugin.h +++ b/geanypy/src/geanypy-plugin.h @@ -33,6 +33,11 @@ extern GeanyData *geany_data; #define PyMODINIT_FUNC void #endif +typedef struct +{ + PyObject_HEAD + GeanyPlugin *plugin; +} GeanyPyPluginBase; #ifdef __cplusplus } /* extern "C" */ From 0b28be76ffe5791ca4a6b99f16ddb94724b5b76b Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Fri, 6 Nov 2015 23:56:45 +0100 Subject: [PATCH 4/4] geanypy: add support for keybindings. geany.Plugin gains two methods to create groups and items --- geanypy/src/geanypy-plugin.c | 82 ++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/geanypy/src/geanypy-plugin.c b/geanypy/src/geanypy-plugin.c index 08ed26d41..ef3df16aa 100644 --- a/geanypy/src/geanypy-plugin.c +++ b/geanypy/src/geanypy-plugin.c @@ -397,8 +397,90 @@ geany_load_module(GeanyPlugin *plugin) static void PluginBase_dealloc(GeanyPyPluginBase *self) { } + +static gboolean call_key(gpointer *unused, guint key_id, gpointer data) +{ + PyObject *callback = data; + PyObject *args; + + args = Py_BuildValue("(i)", key_id); + PyObject_CallObject(callback, args); + Py_DECREF(args); +} + + +static PyObject * +PluginBase_set_kb_group(GeanyPyPluginBase *self, PyObject *args, PyObject *kwargs) +{ + static gchar *kwlist[] = { "section_name", "count", "callback", NULL }; + int count = 0; + const gchar *section_name = NULL; + GeanyKeyGroup *group = NULL; + PyObject *py_callback = NULL; + if (PyArg_ParseTupleAndKeywords(args, kwargs, "si|O", kwlist, §ion_name, &count, &py_callback)) + { + if (PyCallable_Check(py_callback)) + { + Py_INCREF(py_callback); + group = plugin_set_key_group_full(self->plugin, section_name, count, + (GeanyKeyGroupFunc) call_key, py_callback, Py_DecRef); + } + else + group = plugin_set_key_group(self->plugin, section_name, count, NULL); + } + + if (group) + { + GObject *wrapper; + PyObject *ret; + wrapper = g_object_new(G_TYPE_OBJECT, NULL); + g_object_set_data(wrapper, "pointer", group); + ret = pygobject_new(wrapper); + g_object_unref(wrapper); + return ret; + } + Py_RETURN_NONE; +} + + +static PyObject * +PluginBase_set_kb_item(GeanyPyPluginBase *self, PyObject *args, PyObject *kwargs) +{ + static gchar *kwlist[] = { "key_group", "key_id", "key", "mod", "name", "label", "menu_item", "callback", NULL }; + int id = -1; + int key; + int mod; + PyObject *py_group; + const gchar *name = NULL, *label = NULL; + PyObject *py_menu_item = NULL; + PyObject *py_callback = NULL; + + if (PyArg_ParseTupleAndKeywords(args, kwargs, "Oiiiss|OO", kwlist, + &py_group, &id, &key, &mod, &name, &label, &py_menu_item, &py_callback) && id >= 0) + { + GObject *group_wrapper = G_OBJECT(pygobject_get(py_group)); + GtkWidget *menu_item = (py_menu_item == NULL || py_menu_item == Py_None) + ? NULL : GTK_WIDGET(pygobject_get(py_menu_item)); + GeanyKeyGroup *group = g_object_get_data(group_wrapper, "pointer"); + if (PyCallable_Check(py_callback)) + { + Py_INCREF(py_callback); + keybindings_set_item_full(group, id, (guint) key, (GdkModifierType) mod, name, label, + menu_item, (GeanyKeyBindingFunc) call_key, py_callback, Py_DecRef); + } + else + keybindings_set_item(group, id, NULL, (guint) key, (GdkModifierType) mod, name, label, + menu_item); + } + Py_RETURN_NONE; +} + static PyMethodDef PluginBase_methods[] = { + { "set_key_group", (PyCFunction)PluginBase_set_kb_group, METH_KEYWORDS, + "Sets up a GeanyKeybindingGroup for this plugin." }, + { "set_key_item", (PyCFunction)PluginBase_set_kb_item, METH_KEYWORDS, + "Adds an action to one of the plugin's key groups" }, { NULL } };