As of version 0.1 Editra has support to handle extensions written in python. These extensions are in the form of plugins that interact with defined interfaces in the editor. This document is meant to give an overview of api available to write extensions with as well as the available interfaces that are currently available to be extended upon. During this overview we will also walk through how to write a basic plugin.
This discussion is broken into three sections that should be read in order. The first is a short overview of what a plugin is, second is how to package and build a plugin, and third is how to have a plugin implement on interface and be used by the editor.
A plugin in the sense of Editra is simply a class object that is a subclass of the Plugin class in the plugins.py module. This requirement is in place to ensure the creation of the object happens in a certain way but it does not place any real restrictions on the implementation of the plugin you are creating. Shown below is a snippet to create a very simple, but very useless plugin from which we will extend upon as we get further into this document.
import plugin class MyPlugin(plugin.Plugin) """This plugin does nothing""" pass
This is a valid plugin from Editra's point of view and can be loaded into the PluginMgr during startup, but it doesn't implement any Interface so it wont be called upon to do anything during runtime. The discussion on interfaces is later however, first we need to talk about how to package this new plugin so that it can be loaded by Editra.
Editra's plugins must be packaged as Python Eggs. This section will describe how to package the plugin we created in the above section as an egg. In order to create a Python Egg, setuptools is required. Python Eggs are basically a special zip file that emits an entry point that Editra can use to load your plugin object, they also make for an easy one file distribution and installation of your plugin. More can be read about python eggs over at Peak.
Described below is one possible way to layout your plugin package there are of course other ways to do this as well but this is an easy starting point. Directories are labeled with [..], subdirectories and files are indented a level and filenames are written plainly without any markers.
[myplugin]
|
setup.py
[myplugin]
|
__init__.py
The __init__.py file contains the code for the plugin we discussed in the first section. Next lets discuss the contents of the setup.py file that is used to build your plugin package. This template can be used and modified to fit any plugin package.
from setuptools import setup __author__ = "Joe Cool" __doc__ = """An example plugin""" __version__ = "0.0.1" setup( name = "MyPlugin", # Plugin Name version = __version__, # Plugin Version description = __doc__, # Short plugin description author = __author__, # Your Name author_email = "jc@somewhere.com", # Your contact license = "wxWindows", # Plugins licensing info packages = ['myplugin'], # Package directory name(s) entry_points = ''' [Editra.plugins] MyPlugin = myplugin:MyPlugin ''' )
All of the variables passed to setup are important but the most important one to take note of is the entry_points argument. Editra only uses one Entry Point, by the name of Editra.plugins so your entry_point must use [Editra.plugins] in order to be loaded. what follows is a list class objects that are to be loaded by the entry_point. Our example plugin only has one entry point object. The definition of these objects is structured as follows.
OBJ_NAME = [package/module]:CLASS_NAME
Since we now have a plugin package ready and a setup file to build it all thats left is to build the plugin. So open up a terminal if you don't have one open already and change to the directory that has your setup.py file in it. Then use the following command to build the egg.
python setup.py bdist_egg
You should now have an egg file (dist/MyPlugin-0.0.1-py2.5.egg). This can now be installed by either dragging and dropping the egg on the Installation Page of the PluginManager or by manually copying it to your runtime plugin directory ($HOME/.Editra/plugins).
Then start Editra and open the Plugin Manger and look at the Plugins page to see if it was loaded. You wont be able to do much else with this plugin other than see if it was loaded or not, so in the next section we will discuss the available interfaces and some of the available Api within Editra that will help to help you to write more powerful plugins that can add any number of features to the editor without the need to modify any of Editra's internal code.
Plugins are used to Extend defined interfaces within Editra, these interfaces are what define the contract your plugin is to be implemented under. In this section we will discuss the interfaces that Editra offers to be extended upon. The list of interfaces will increase in later releases but the methodology will be applicable to all future interfaces. Within the discussion of these two interface's we will expand the example used in the previous sections to implement both of these interfaces.
This interface is a very simple and very general. Its purpose is to allow for almost any object to install itself as a component of the MainWindow. The MainWindow is Editra's basic Frame/MenuBar/ToolBar/Notebook window. The interface is defined as follows.
class MainWindowI(plugin.Interface): """Provides simple one method interface into adding extra functionality to the main window. The method in this interface called at the end of the window's initialization. """ def PlugIt(self, window): """Do whatever is needed to integrate is plugin into the editor. """ pass def GetMenuHandlers(self): """Get menu event handlers/id pairs. This function should return a list of tuples containing menu ids and their handlers. The handlers should be not be a member of this class but a member of the ui component that they handler acts upon. @return: list [(ID_FOO, foo.OnFoo), (ID_BAR, bar.OnBar)] """ pass def GetUIHandlers(self): """Get update ui event handlers/id pairs. This function should return a list of tuples containing object ids and their handlers. The handlers should be not be a member of this class but a member of the ui component that they handler acts upon. @return: list [(ID_FOO, foo.OnFoo), (ID_BAR, bar.OnBar)] """ pass
The MainWindow calls upon the PlugIt method of this interface at the very end of the Frame's initialization. So now that we have an interface to implement lets modify our previous example to implement this interface and have it add a menu item to the "Edit" Menu that opens up a Hello World message dialog.
"""Adds A Hello Word entry to the Edit Menu""" # NEW __author__ = "Joe Cool" # NEW __version__ = "0.0.1" # NEW import wx # NEW import iface # NEW import plugin _ = wx.GetTranslation ID_HELLO_WORLD = wx.NewId() class MyPlugin(plugin.Plugin): """Adds a Hello World Item to the MainWindow Edit Menu""" plugin.Implements(iface.MainWindowI) # NEW def PlugIt(self, parent): """Implements MainWindowI's PlugIt Method""" mw = parent em = mw.GetMenuBar().GetMenuByName("edit") em.InsertAlpha(ID_HELLO_WORLD, _("Hello World"), _("Show a Hello World Message Box")) def GetMenuHandlers(self): """Returns the event handler for this plugins menu entry""" return [(ID_HELLO_WORLD, self.OnHello)] def OnHello(self, evt): """Handles the menu event generated by our new menu entry""" if evt.GetId() == ID_HELLO_WORLD: wx.MessageBox(_("Hello World from MyPlugin"), _("Hello Word"))
We did a number of things in this example so lets slow down and take a look at some of them.
So as a roundup of the introduction to this first interface you should use the setup.py file we defined in the earlier examples and use it to build a new egg, then copy this egg to the proper location and try it out to see it in action. Just remember that it will have to be activated in the Plugin Manager first.
This interface is used for defining a new type of document generator. The HTML and LaTeX generators are examples of this interface. The GeneratorI is more specific than the MainWindowI as it serves only a single purpose. It has three methods that need to be implemented in order to be fully functional, shown below is its definition, the doc strings should be enough to describe what each does.
class GeneratorI(plugin.Interface): """Plugins that are to be used for generating code/document need to implement this interface. """ def Generate(self, txt_ctrl): """Generates the code. The txt_ctrl parameter is a reference to an EdStc object (see ed_stc.py). The return value of this function needs to be a 2 item tuple with the first item being an associated file extension to use for setting highlighting if available and the second item is the string of the new document. """ pass def GetId(self): """Must return the Id used for the generator objects menu id. This is used to identify which Generator to call on a menu event. """ pass def GetMenuEntry(self, menu): """Returns the MenuItem entry for this generator""" pass
The code that calls upon this interface takes care of all the event handling and where the items get placed in the Generator Menu, so don't try to handle any of those items in this interface. The Generate method is the method that is called upon when your Generator is asked for, you but need to return the requested object and the core code will take care of what is done with it. We will take the previous example and extend it to implement this interface as well as the MainWindowI.
"""Adds A Hello Word entry to the Edit Menu""" __author__ = "Joe Cool" __version__ = "0.0.1" import wx import iface import generator # NEW import plugin _ = wx.GetTranslation ID_HELLO_WORLD = wx.NewId() ID_HELLO_GEN = wx.NewId() # NEW class MyPlugin(plugin.Plugin): """Adds a Hello World Item to the MainWindow Edit Window""" plugin.Implements(iface.MainWindowI, generator.GeneratorI) # NEW def PlugIt(self, parent): """Implements MainWindowI's PlugIt Method""" mw = parent em = mw.GetMenuBar().GetMenuByName("edit") em.InsertAlpha(ID_HELLO_WORLD, _("Hello World"), _("Show a Hello World Message Box")) def GetMenuHandlers(self): """Returns the event handler for this plugins menu entry""" return [(ID_HELLO_WORLD, self.OnHello)] def OnHello(self, evt): """Handles the menu event generated by our new menu entry""" if evt.GetId() == ID_HELLO_WORLD: wx.MessageBox(_("Hello World from MyPlugin"), _("Hello Word")) # New def Generate(self, txt_ctrl): """Transforms every instance of Hello in the txt_ctrl to HelloWorld""" txt = txt_ctrl.GetText() txt = txt.replace("Hello", "HelloWorld") return ("txt", txt) def GetId(self): return ID_HELLO_GEN def GetMenuEntry(self, menu): return wx.MenuItem(menu, ID_HELLO_GEN, _("Generate %s") % "HelloWorld", _("Generate HelloWord from Hello"))
This isn't a very practical generator but it does illustrates how to implement one. What will happen is that when a user clicks on the menu entry for "Generate HelloWorld", the Generate method will be passed reference to the current document. The generator then performs some transformations on the text of the given document which in this case all it will return is a new document where every instance of "Hello" in the current document will be transformed into "HelloWorld". The core code will then open this document in a new notebook page and apply any available highlighting that it can find for the document type depending upon the file extension that was returned with the document text.
The ShelfI is an interface into the shelf which is a floatable/dockable tabbed window that is docked to the bottom of Editra's editting pane by default. This interface allows for having multiple instances of a plugin pane open at any given time. I won't include any examples but how to implement it should be fairly obvious by applying the above examples to this interface.
class ShelfI(plugin.Interface): """Interface into the L{Shelf}. All plugins wanting to be placed on the L{Shelf} should implement this interface. """ def AllowMultiple(self): """This method is used to check if multiple instances of this item are allowed to be open at one time. @return: True/False @rtype: boolean """ return True def CreateItem(self, parent): """This is them method used to open the item in the L{Shelf} It should return an object that is a Panel or subclass of a Panel. @param parent: The would be parent window of this panel @return: wx.Panel """ raise NotImplementedError def GetBitmap(self): """Get the bitmap to show in the shelf for this item @return: wx.Bitmap @note: this method is optional """ return wx.NullBitmap def GetId(self): """Return the id that identifies this item (same as the menuid) @return: Item ID @rtype: int """ raise NotImplementedError def GetMenuEntry(self, menu): """Returns the menu entry associated with this item @param menu: The menu this entry will be added to @return: wx.MenuItem """ raise NotImplementedError def GetName(self): """Return the name of this shelf item. This should be the same as the MenuEntry's label. @return: name of item @rtype: string """ raise NotImplementedError def InstallComponents(self, mainw): """Called by the Shelf when the plugin is created to allow it to install any extra components that it may have that fall outside the normal interface. This method is optional and does not need to be implimented if it is not needed. @param mainw: MainWindow Instance """ pass def IsStockable(self): """Return whether this item type is stockable. The shelf saves what pages it had open the last time the program was run and then reloads the pages the next time the program starts. If this item can be reloaded between sessions return True otherwise return False. """ return True
As can be seen from these examples it is fairly easy to add any number of extensions to the editor through this plugin architecture.