diff --git a/geanypy/README b/geanypy/README index 7a65ac619..e9ae6247b 100644 --- a/geanypy/README +++ b/geanypy/README @@ -33,14 +33,13 @@ module more "Pythonic". To write a plugin, inherit from the ``geany.Plugin`` class and implmenent the required members (see ``geany/plugin.py`` documentation comments). Then put the plugin in a searched plugin path. Currently two locations are search for -plugins. The first is ``PREFIX/share/geany/geanypy/plugins`` and the recommended +plugins. The first is ``PREFIX/lib/geany`` and the recommended location is under your personal Geany directory (usually -``~/.config/geany/plugins/geanypy/plugins``). To load or unload plugins, click -the Python Plugin Manager item under the Tools menu which will appear when you -activate GeanyPy through Geany's regular plugin manager. +``~/.config/geany/plugins``). To load or unload plugins, use Geany's regular Plugin ++Manager. Python plugins appear there once GeanyPy is activated. -When the GeanyPy plugin is loaded, it has an option to add a new tab to -the notebook in the message window area that contains an interactive +When the Python Console plugin is enabled, it will add a new tab to the notebook in +the message window area that contains an interactive Python shell with the `geany` Python shell with the ``geany`` module pre-imported. You can tinker around with API with this console, for example:: diff --git a/geanypy/doc/source/starting.rst b/geanypy/doc/source/starting.rst index 917dae262..238913e29 100644 --- a/geanypy/doc/source/starting.rst +++ b/geanypy/doc/source/starting.rst @@ -7,34 +7,30 @@ GeanyPy, it's important to note how it works and some features it provides. What the heck is GeanyPy, really? ================================= -GeanyPy is "just another Geany plugin", really. Geany sees GeanyPy as any -other `plugin `_, so -to activate GeanyPy, use Geany's +GeanyPy is a proxy plugin. Geany initially sees GeanyPy as any other +`plugin `_, but +GeanyPy registers some additional stuff that enables Geany to load python plugins +through GeanyPy. So to activate, use Geany's `Plugin Manager `_ under the Tools menu as you would for any other plugin. -Once the GeanyPy plugin has been activated, a few elements are added to Geany's -user interface as described below. +Once the GeanyPy plugin has been activated, Geany should rescan the plugin +directories and pick those up that are supported through GeanyPy. It'll integrate +the python plugins into the Plugin Manager in an additional hierarchy level below +GeanyPy. -Python Plugin Manager -===================== +* [ ] Geany plugin 1 +* [x] GeanyPy + * [ ] Python plugin 1 + * [x] Python plugin 2 + * [ ] Python plugin 3 +* [ ] Geany plugin 3 -Under the Tools menu, you will find the Python Plugin Manager, which is meant -to be similar to Geany's own Plugin Manager. This is where you will activate -any plugins written in Python. +Remember that Geany looks in three places for plugins: -The Python Plugin Manager looks in exactly two places for plugins: - -1. For system-wide plugins, it will search in PREFIX/share/geany/geanypy/plugins. -2. In Geany's config directory under your home directory, typically ~/.config/geany/plugins/geanypy/plugins. - -Where `PREFIX` is the prefix used at configure time with Geany/GeanyPy (see -the previous section, Installation). Both of these paths may vary depending on -your platform, but for most \*nix systems, the above paths should hold true. - -Any plugins which follow the proper interface found in either of those two -directories will be listed in the Python Plugin Manager and you will be able -to activate and deactivate them there. +1. For system-wide plugins, it will search in (usually) /usr/share/geany or /usr/local/share/geany. +2. In Geany's config directory under your home directory, typically ~/.config/geany/plugins. +3. A user-configurable plugin directory (useful during plugin development). Python Console ============== 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..ccada0cd4 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 @@ -26,6 +24,7 @@ import search import templates import ui_utils +import keybindings from app import App from prefs import Prefs, ToolPrefs @@ -43,6 +42,7 @@ "main_widgets", "interface_prefs", "app", + "keybindings", "general_prefs", "search_prefs", "template_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..c93e2c885 100644 --- a/geanypy/geany/plugin.py +++ b/geanypy/geany/plugin.py @@ -30,12 +30,12 @@ def cleanup(self): The guts of the API are exposed to plugins through the `geany` package and its modules. -Plugins should be placed in either the system plugin directory (something -like /usr/local/share/geany/geanypy/plugins) or in their personal plugin -directory (something like ~/.config/geany/plugins/geanypy/plugins). Only -files with a `.py` extension will be loaded. +Plugins should be placed in either the system plugin directory (something like +/usr/local/lib/geany) or in the user plugin directory (something like +~/.config/geany/plugins). Only files with a `.py` extension will be loaded. """ +import keybindings class Plugin(object): """ @@ -49,7 +49,6 @@ class Plugin(object): #__plugin_version__ = None #__plugin_author__ = None - _events = { "document-open": [], # TODO: add more events here @@ -121,3 +120,10 @@ def author(self): return self.__plugin_author__ else: return "" + + def set_key_group(self, section_name, count, callback = None): + """ + Sets up a GeanyKeyGroup for this plugin. You can use that group to add keybindings + with group.add_key_item(). + """ + return keybindings.set_key_group(self, section_name, count, callback) 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 e7135f2f0..1600b59b9 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\"" \ -DG_LOG_DOMAIN=\"GeanyPy\" geanypy_la_CFLAGS = @GEANYPY_CFLAGS@ @GMODULE_CFLAGS@ geanypy_la_LIBADD = @GEANY_LIBS@ @PYGTK_LIBS@ \ @@ -23,6 +23,7 @@ geanypy_la_SOURCES = geanypy-app.c \ geanypy-highlighting.c \ geanypy-indentprefs.c \ geanypy-interfaceprefs.c \ + geanypy-keybindings.c \ geanypy-main.c \ geanypy-mainwidgets.c \ geanypy-msgwindow.c \ diff --git a/geanypy/src/geanypy-keybindings.c b/geanypy/src/geanypy-keybindings.c new file mode 100644 index 000000000..1c6ea269f --- /dev/null +++ b/geanypy/src/geanypy-keybindings.c @@ -0,0 +1,213 @@ +/* + * plugin.c + * + * Copyright 2015 Thomas Martitz + * + * 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. + */ + + +#include "geanypy.h" +#include "geanypy-keybindings.h" + +#include + +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); +} + + +/* plugin.py provides an OOP-style wrapper around this so call it like: + * class Foo(geany.Plugin): + * def __init__(self): + * self.set_key_group(...) + */ +static PyObject * +Keybindings_set_key_group(PyObject *self, PyObject *args, PyObject *kwargs) +{ + static gchar *kwlist[] = { "plugin", "section_name", "count", "callback", NULL }; + int count = 0; + const gchar *section_name = NULL; + GeanyKeyGroup *group = NULL; + PyObject *py_callback = NULL; + PyObject *py_ret = Py_None; + PyObject *py_plugin; + gboolean has_cb = FALSE; + + Py_INCREF(Py_None); + + if (PyArg_ParseTupleAndKeywords(args, kwargs, "Osi|O", kwlist, + &py_plugin, §ion_name, &count, &py_callback)) + { + GeanyPlugin *plugin = plugin_get(py_plugin); + g_return_val_if_fail(plugin != NULL, Py_None); + + has_cb = PyCallable_Check(py_callback); + if (has_cb) + { + Py_INCREF(py_callback); + group = plugin_set_key_group_full(plugin, section_name, count, + (GeanyKeyGroupFunc) call_key, py_callback, + (GDestroyNotify) Py_DecRef); + } + else + group = plugin_set_key_group(plugin, section_name, count, NULL); + } + + if (group) + { + Py_DECREF(py_ret); + py_ret = KeyGroup_new_with_geany_key_group(group, has_cb); + } + + return py_ret; +} + + +static PyObject * +KeyGroup_add_key_item(KeyGroup *self, PyObject *args, PyObject *kwargs) +{ + static gchar *kwlist[] = { "name", "label", "callback", "key_id", "key", "mod" , "menu_item", NULL }; + int id = -1; + int key = 0, mod = 0; + const gchar *name = NULL, *label = NULL; + PyObject *py_menu_item = NULL; + PyObject *py_callback = NULL; + GeanyKeyBinding *item = NULL; + + if (PyArg_ParseTupleAndKeywords(args, kwargs, "ss|OiiiO", kwlist, + &name, &label, &py_callback, &id, &key, &mod, &py_menu_item)) + { + if (id == -1) + id = self->item_index; + + GtkWidget *menu_item = (py_menu_item == NULL || py_menu_item == Py_None) + ? NULL : GTK_WIDGET(pygobject_get(py_menu_item)); + if (PyCallable_Check(py_callback)) + { + Py_INCREF(py_callback); + item = keybindings_set_item_full(self->kb_group, id, (guint) key, + (GdkModifierType) mod, name, label, menu_item, + (GeanyKeyBindingFunc) call_key, py_callback, + (GDestroyNotify) Py_DecRef); + } + else + { + if (!self->has_cb) + g_warning("Either KeyGroup or the Keybinding must have a callback\n"); + else + item = keybindings_set_item(self->kb_group, id, NULL, (guint) key, + (GdkModifierType) mod, name, label, menu_item); + } + Py_XDECREF(py_menu_item); + + self->item_index = id + 1; + } + + if (item) + { + /* Return a tuple containing the key group and the opaque GeanyKeyBinding pointer. + * This is in preparation of allowing chained calls like + * set_kb_group(X, 3).add_key_item().add_key_item().add_key_item() + * without losing access to the keybinding pointer (might become necessary for newer + * Geany APIs). + * Note that the plain tuple doesn't support the above yet, we've got to subclass it, + * but we are prepared without breaking sub-plugins */ + PyObject *ret = PyTuple_Pack(2, self, PyCapsule_New(item, "GeanyKeyBinding", NULL)); + return ret; + } + Py_RETURN_NONE; +} + + +static PyMethodDef +KeyGroup_methods[] = { + { "add_key_item", (PyCFunction)KeyGroup_add_key_item, METH_KEYWORDS, + "Adds an action to the plugin's key group" }, + { NULL } +}; + +static PyMethodDef +Keybindings_methods[] = { + { "set_key_group", (PyCFunction)Keybindings_set_key_group, METH_KEYWORDS, + "Sets up a GeanyKeybindingGroup for this plugin." }, + { NULL } +}; + + +static PyGetSetDef +KeyGroup_getseters[] = { + { NULL }, +}; + + +static void +KeyGroup_dealloc(KeyGroup *self) +{ + g_return_if_fail(self != NULL); + self->ob_type->tp_free((PyObject *) self); +} + + +static PyTypeObject KeyGroupType = { + PyObject_HEAD_INIT(NULL) + 0, /* ob_size */ + "geany.keybindings.KeyGroup", /* tp_name */ + sizeof(KeyGroup), /* tp_basicsize */ + 0, /* tp_itemsize */ + (destructor) KeyGroup_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 GeanyKeyGroup structure." ,/* tp_doc */ + 0, 0, 0, 0, 0, 0, /* tp_traverse - tp_iternext */ + KeyGroup_methods, /* tp_methods */ + 0, /* tp_members */ + KeyGroup_getseters, /* tp_getset */ + 0, 0, 0, 0, 0, /* tp_base - tp_dictoffset */ + 0, 0, (newfunc) PyType_GenericNew, /* tp_init - tp_alloc, tp_new */ +}; + + +PyMODINIT_FUNC initkeybindings(void) +{ + PyObject *m; + + if (PyType_Ready(&KeyGroupType) < 0) + return; + + m = Py_InitModule3("keybindings", Keybindings_methods, "Keybindings support."); + + Py_INCREF(&KeyGroupType); + PyModule_AddObject(m, "KeyGroup", (PyObject *)&KeyGroupType); +} + +PyObject *KeyGroup_new_with_geany_key_group(GeanyKeyGroup *group, gboolean has_cb) +{ + KeyGroup *ret = PyObject_New(KeyGroup, &KeyGroupType); + + ret->kb_group = group; + ret->has_cb = has_cb; + ret->item_index = 0; + + return (PyObject *) ret; +} + diff --git a/geanypy/src/geanypy-keybindings.h b/geanypy/src/geanypy-keybindings.h new file mode 100644 index 000000000..27ecf651a --- /dev/null +++ b/geanypy/src/geanypy-keybindings.h @@ -0,0 +1,39 @@ +/* + * geanypy-keybindings.h + * + * Copyright 2015 Thomas Martitz + * + * 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. + */ + + +#ifndef GEANYPY_KEYBINDINGS_H +#define GEANYPY_KEYBINDINGS_H + +#include + +typedef struct +{ + PyObject_HEAD + GeanyKeyGroup *kb_group; + gboolean has_cb; + gint item_index; +} KeyGroup; + +extern PyObject * +KeyGroup_new_with_geany_key_group(GeanyKeyGroup *group, gboolean has_cb); + +#endif /* GEANYPY_KEYBINDINGS_H */ diff --git a/geanypy/src/geanypy-plugin.c b/geanypy/src/geanypy-plugin.c index 338067c6a..134808af9 100644 --- a/geanypy/src/geanypy-plugin.c +++ b/geanypy/src/geanypy-plugin.c @@ -26,26 +26,12 @@ #define INCLUDE_PYGOBJECT_ONCE_FULL #include "geanypy.h" +#include "geanypy-keybindings.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); @@ -64,6 +50,7 @@ PyMODINIT_FUNC initscintilla(void); PyMODINIT_FUNC initsearch(void); PyMODINIT_FUNC inittemplates(void); PyMODINIT_FUNC initui_utils(void); +PyMODINIT_FUNC initkeybindings(void); static void @@ -104,6 +91,7 @@ GeanyPy_start_interpreter(void) initsearch(); inittemplates(); initui_utils(); + initkeybindings(); #ifdef GEANYPY_WINDOWS { /* On windows, get path at runtime since we don't really know where @@ -131,7 +119,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 +136,284 @@ 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 != Py_None) { - 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); - 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; - } + Py_DECREF(o); /* Must unref even if it's Py_None */ + return NULL; } -static void -GeanyPy_show_manager(void) +static void do_show_configure(GtkWidget *button, gpointer pdata) +{ + GeanyPyPluginData *data = (GeanyPyPluginData *) pdata; + PyObject_CallMethod(data->instance, "show_configure", NULL); +} + + +static GtkWidget *geanypy_proxy_configure_legacy(GeanyPlugin *plugin, GtkDialog *parent, gpointer pdata) { - 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); + GeanyPyPluginData *data = (GeanyPyPluginData *) pdata; + PyObject *o, *oparent; + GtkWidget *box, *label, *button, *align; + gchar *text; + + /* This creates a simple page that has only one button to show the plugin's legacy configure + * dialog. It is for older plugins that implement show_configure(). It's not pretty but + * it provides basic backwards compatibility. */ + box = gtk_vbox_new(FALSE, 2); + + text = g_strdup_printf("The plugin \"%s\" is older and hasn't been updated\nto provide a configuration UI. However, it provides a dialog to\nallow you to change the plugin's preferences.", plugin->info->name); + label = gtk_label_new(text); + + align = gtk_alignment_new(0, 0, 1, 1); + gtk_container_add(GTK_CONTAINER(align), label); + gtk_alignment_set_padding(GTK_ALIGNMENT(align), 0, 6, 2, 2); + gtk_box_pack_start(GTK_BOX(box), align, FALSE, FALSE, 0); + + button = gtk_button_new_with_label("Open dialog"); + align = gtk_alignment_new(0.5, 0, 0.3f, 1); + gtk_container_add(GTK_CONTAINER(align), button); + g_signal_connect(button, "clicked", (GCallback) do_show_configure, pdata); + gtk_box_pack_start(GTK_BOX(box), align, FALSE, TRUE, 0); + + gtk_widget_show_all(box); + g_free(text); + return box; +} + +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; + + /* 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 *string = PyObject_GetAttrString(o, attr); + 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; + PyObject *caps = PyCapsule_New(subplugin, "GeanyPlugin", NULL); + Py_INCREF(found); + pdata->module = module; + pdata->class = found; + PyObject_SetAttrString(pdata->class, "__geany_plugin__", caps); + 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; + else if (PyObject_HasAttrString(found, "show_configure")) + funcs->configure = geanypy_proxy_configure_legacy; + 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, 226, state, g_free); } diff --git a/geanypy/src/geanypy-plugin.h b/geanypy/src/geanypy-plugin.h index 5457404d8..007309149 100644 --- a/geanypy/src/geanypy-plugin.h +++ b/geanypy/src/geanypy-plugin.h @@ -26,16 +26,20 @@ extern "C" { #endif - -extern GeanyPlugin *geany_plugin; -extern GeanyData *geany_data; -extern GeanyFunctions *geany_functions; +extern GeanyData *geany_data; #ifndef PyMODINIT_FUNC #define PyMODINIT_FUNC void #endif +static inline GeanyPlugin *plugin_get(PyObject *self) +{ + PyObject *caps = PyObject_GetAttrString(self, "__geany_plugin__"); + return PyCapsule_GetPointer(caps, "GeanyPlugin"); +} + + #ifdef __cplusplus } /* extern "C" */ 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);