Plugin Overview

Preface:


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.



Plugin's in Editra:


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.




Packaging Plugins


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.



Interfaces


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.

Interface 1:

MainWindowI

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 GetUIHandlers(self):
        return list() # not needed by this plugin
 
    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.

  1. The first thing to notice are the three lines a the top of the file, these lines are meta data that is used by the configuration page of the Plugin Manager to describe your plugin. The first is a docstring that should describe your plugins purpose, the second is your name, and the last line is the version of this plugin.

  2. The next thing to notice is the import statements, the imports take place inside of Editra's namespace at runtime so there is no need to preface the imports with src.MODULE or Editra.src.MODULE, ect. Just import the module as if your working path was inside Editra's main src package directory. This will let you have access to any of the convenience functions available inside of Editra's Api as well as the various interfaces.

  3. Next is the line plugin.Implements, this is used to tell the plugin manager what interfaces to associate your plugin with. Your plugin can implement any number of interfaces than can be specified in this call each being separated by a comma.

  4. Third is the parent argument to PlugIt. This is a reference to the MainWindow that can be used to access any of its children objects. In this case we wanted a certain menu. The module ed_menu is what all menu's and menubars in Editra use, these objects include a number of convenience functions to make your and my life easier. In this one we used the InsertAlpha function which will try to insert the given menu entry in alphabetical order based on the label.

  5. Note that the binding of the menu event is not handled in our plugin but is instead done by the main window when it calls GetMenuHandlers.

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.


Interface 2:

GeneratorI

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.


Interface 3:

ShelfI

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


Final Notes:

As can be seen from these examples it is fairly easy to add any number of extensions to the editor through this plugin architecture.