1 Codimension Plugins Tutorial
2 ============================
4 In this tutorial two major topics are discussed. The first part discusses how
5 plugin support is implemented in Codimension. The second part discusses
6 implementation of a simple plugin and what is available for plugins.
8 Plugin Support Implementation
9 -----------------------------
11 The shortest answer on the question "what is a Codimension plugin?" is as
12 follows: a Codimension plugin is a Python class implemented in a certain way.
13 Codimension is written in Python and thus its plugins should also be written in
16 Before the implementation details are discussed it makes sense to introduce a
17 few terms Codimension uses while it works with plugins.
19 At the start time Codimension looks for plugins in two places. The first one is
20 `/usr/share/codimension-plugins/`. The second one is the directory called
21 `.codimension/plugins` located in the user home directory. So on Linux system
22 the latter most probably will be `~/.codimension/plugins`. It is highly
23 recommended that each plugin occupies a designated directory where it keeps all
24 the required files. So on a certain system the related directories structure
28 /usr/share/codimension-plugins/plugin1/...
32 /home/mike/.codimension/plugins/plugin4/...
37 Depending on a plugin location Codimension splits all the found plugins into two
38 groups: system wide plugins and user plugins. So in the example above `plugin1`,
39 `plugin2` and `plugin3` are **system wide** plugins while `plugin4`, `plugin5` and
40 `plugin6` are **user** plugins.
42 The next pieces which are important for Codimension are a plugin name and a
43 plugin version. A name and a version are stored in a plugin description file
44 (it will be discussed later). That description file is what triggers loading a
47 The next important piece is that each plugin can be enabled or disabled (a pair
48 of other terms activated/deactivated is also used further with the same
49 meaning). While loading plugins Codimension initially treats all plugins enabled
50 so a newly installed plugin will be automatically enabled next time Codimension
51 starts. It is possible however that a plugin conflicts with another plugin.
52 Certain types of conflicts can be detected by Codimension automatically and
53 Codimension can disable some plugins to resolve a conflict. The following rules
54 are used for automatic conflict resolution:
56 * If there is a user and a system wide plugin with the same name then the user
58 * If there are two plugins with the same name and both of them are either user
59 or system wide then their versions are taken into consideration. The higher
61 * If names, vaersions and locations of two plugins match then an arbitrary one
64 There are a few other cases when Codimension disables a plugin automatically. A
65 good example of such a case is when a plugin does not implement the required
68 An important detail on the plugin initialization stage is that regardless whether
69 a plugin is enabled or disabled it is instantiated. The plugin class instance will
70 stay in memory till Codimension is closed.
72 The user is always able to enable or disable plugins manually and in particular
73 resolve detected conflicts the required way if automatic resolution is not what
74 is needed. The manual control of plugin states is done in the plugin manager.
75 The manager user interface is available via main menu **Options->Plugin
76 Manager** menu item as shown below.
78 ![Plugin Manager](PluginManager.png "Plugin Manager")
80 Each plugin can move between the enabled and disabled state an arbitrary number
81 of times within a single Codimension session. This could be illustrated as
84 ![Plugin States](PluginStates.png "Plugin States")
86 The last term Codimension introduces for plugins is a plugin category. Plugins
87 could require different support on the IDE side and a plugin category is the way
88 how to distinguish the required support. For example, a spell checker plugin
89 might need certain support targeted to text editing while a plugin which
90 implements a regular expression visual testing facility does not need text
91 editing support at all. A plugin category defines an interface variation between
92 Codimension and a plugin. The categories come in a form of predefined base
93 classes and each plugin must derive from one of them.
99 As it was mentioned above it is highly recommended that a plugin occupies a
100 designated directory. For example, directory content for a plugin may look as
104 /home/mike/.codimension/plugins/pdfexporter/pdfexporter.cdmp
110 The `pdfexporter.cdmp` file contains a textual plugin description. The name of the
111 file does not matter, Codimension looks for the .cdmp file extensions. A
112 content of the pdfexporter.cdmp file may be similar to the following.
120 Author = Mike Slartibartfast <mike.slartibartfast@some.com>
122 Website = http://mike.slartibartfast.homelinux.com/pdfexporter
123 Description = Codimension PDF exporter plugin
127 The `[Core].Name` value is an arbitrary string however it is better to keep it
128 relatively short. The `[Core].Module` value is a directory path where
129 Codimension plugin resides. It is recommended that all the plugin files are
130 sitting in a designated directory including the plugin description file and
131 therefore the `[Core].Module` value refers to the very directory it is sitting
132 in. The '.' value is the recommended value for all the Codimension plugins.
134 The `[Documentation]` section has self explanatory values. A plugin can add any
135 values to this section and all of them will be displayed in the **Detailed
136 information** box in the plugin manager dialog when a plugin is selected.
138 The `__init__.py` file is the one where a plugin class definition must reside.
139 In the example above the plugin also has some utility functions in the
140 `util_functions.py` and a configuration dialog in separate files. To import
141 `util_functions` and `config_dialog` modules in `__init__.py` there is no need
142 to use relative imports. The `__init__.py` can simply use:
145 # The plugin modules do not require relative import
147 from util_functions import designCoastline
150 The Codimension modules are also available for the plugin code. So a plugin can
151 use statements similar to the following:
154 # Importing pixmaps cache from a Codimension module
155 from utils.pixmapcache import PixmapCache
156 codimensionLogo = PixmapCache().getPixmap( 'logo.png' )
159 It was mentioned in the previous section that a plugin class must derive from
160 one of the predefined plugin category base class. So a part of the PDF exporter
161 plugin class hierarchy may look as follows:
163 ![Plugin Base Classes](PluginBases.png "Plugin Base Classes")
165 The `PDFExporterPlugin` class must reside in the `__init__.py` file. This is the
166 class which implements the plugin interface. The plugin developer does not need
167 and should not make any changes in any other classes shown on the diagram.
169 The `WizardInterface` class is a Codimension provided plugin category base
170 class. The class is defined in `codimension/src/`
171 `plugins/categories/wizardiface.py`. The class has a set of member functions
172 some of which have to be implemented by the plugin of this category. The member
173 function documentation strings describe in details what is expected by
174 Codimension. At the time of writing (Codimension v.2.1.0) the `WizardInterface`
175 is the only supported plugin category. When a new plugin category is introduced
176 its base class will appear in the `codimension/src/plugins/categories/`
177 directory. The next anticipated plugin category will serve version control
180 The `CDMPluginBase` class is a Codimension provided convenience class which
181 simplifies access to the major IDE objects. The class definition resides in the
182 `codimension/src/plugins/categories/cdmpluginbase.py` file. Having
183 `CDMPluginBase` class in the hierarchy makes it possible for a plugin class to
184 use simple to read statements similar to the following:
187 if self.ide.project.isLoaded():
188 # The ide has a project loaded
191 # There is no project, the user edits individual files
195 Access to all the IDE objects should start with:
201 See the `IDEAccess` class in the
202 `codimension/src/plugins/categories/cdmpluginbase.py` file for a full list of
203 provided IDE objects.
205 Codimension uses thirdparty library called
206 [yapsy](http://yapsy.sourceforge.net/) to build plugin support on top of it.
207 Yapsy needs to have IPlugin in the plugin class hierarchy and so it is here.
208 A plugin developer should not need to deal with this class directly however.
210 The `QObject` class is a PyQt provided class. The class is included into the
211 hierarchy for convenience. Codimension uses QT library for the user interface
212 and therefore QT signals are used quite often. Having `QObject` in the base
213 gives a convenient way to subscribe for signals and to emit them, e.g. a plugin
214 may have the following code:
217 self.connect( self.ide.project, SIGNAL( 'projectChanged' ),
218 self.__onProjectChanged )
222 Plugin Example: Garbage Collector Plugin
223 ----------------------------------------
225 The idea of an example plugin is quite simple. The Python garbage collector
226 triggers objects collection at pretty much unknown moments and the plugin will
227 make it more predictable. The garbage collector plugin (GC plugin) will call
228 the `collect()` method of the `gc` Python module when:
230 * a project is changed
231 * new files appeared in a project
232 * some files are deleted from a project
234 The `gc.collect()` call provides an information of how many objects were
235 collected and this could be interesting to see. So a
236 message should be shown somewhere. To make it more user friendly the GC plugin
237 should provide a configuration dialog with options where to show the message:
240 * do not show anything
241 The selected option should be memorized and restored the next time Codimension
244 Having the requirements at hand let's start implementing the GC plugin with
245 creating a directory where all the plugin files will be located.
248 > mkdir garbagecollector
249 > cd garbagecollector
252 First, we need the plugin description file, let's call it
253 `garbagecollector.cdmp`. The content of the file will be as follows:
257 Name = Garbage collector
261 Author = Sergey Satskiy <sergey.satskiy@gmail.com>
263 Website = http://satsky.spb.ru/codimension
264 Description = Codimension garbage collector plugin
268 The GC plugin will belong to the wizard plugin category so it must derive from
269 the `WizardInterface` class. The definition of the class must be in the
274 from plugins.categories.wizardiface import WizardInterface
276 class GCPlugin( WizardInterface ):
277 def __init__( self ):
278 WizardInterface.__init__( self )
282 Codimension instantiates all the found plugins regardless whether they are activated
283 or not. So the GC plugin `__init__` does not do any significant resource consuming
286 One of the first things Codimension does before a plugin is acivated, it asks
287 the plugin if the current IDE version is supported by the plugin. Codimension
288 passes the current version as a string, e.g. `"2.1.0"`. The method must be implemented
289 by the plugin and for the GC plugin it is trivial, all the versions are supported:
293 def isIDEVersionCompatible( ideVersion ):
298 The next pair of methods which must be implemented in a plugin is `activate` and
299 `deactivate`. Obviously, the `activate` method will be called when a plugin is activated. It
300 may happened at the start time automatically or when a user activates previously
301 deactivated plugin. Therefore it is generally a good idea to have plugin data
302 allocated and deallocated in these two methods respectively.
304 The first thing to be done in the `activate` method is to call `activate` of the
305 interface base class. If this call is forgotten then `self.ide.` statements will
306 not work at all and the plugin management system may also be confused. Respectively,
307 the `deactivate` method should call `deactivate` of the interface base class as the
310 The GC plugin initialization is basically connecting a few IDE signals with the
311 plugin member functions. When the plugin is deactivated the signals should be disconnected.
314 def activate( self, ideSettings, ideGlobalData ):
315 WizardInterface.activate( self, ideSettings, ideGlobalData )
317 self.connect( self.ide.editorsManager, SIGNAL( 'tabClosed' ),
318 self.__collectGarbage )
319 self.connect( self.ide.project, SIGNAL( 'projectChanged' ),
320 self.__collectGarbage )
323 def deactivate( self ):
324 self.disconnect( self.ide.project, SIGNAL( 'projectChanged' ),
325 self.__collectGarbage )
326 self.disconnect( self.ide.editorsManager, SIGNAL( 'tabClosed' ),
327 self.__collectGarbage )
329 WizardInterface.deactivate( self )
333 The GC plugin needs some configuring. The user should be able to instruct the
334 plugin where the garbage collection information should be displayed. It is going
335 to be a modal graphics dialog with three radio buttons:
337 ![GC Plugin Configuration Dialog](GCConfigDialog.png "GC Plugin Configuration Dialog")
339 Let's place the dialog code into a separate file `configdlg.py`. Three integer
340 constants will be defined in the file as well. These constants identify the
341 GC plugin information message destination.
343 Codimension provides a unified way to call plugin configuration dialogs. Look
344 at the plugin manager screenshot above. There is a button with a wrench on it
345 for each found plugin. If a plugin needs configuring then its button will be
348 In order to tell Codimension if a plugin needs configuring there is an interface
349 method which should return a python callable or None if no configuring is required.
350 The GC plugin needs configuring so the implementation will look as follows:
353 from configdlg import GCPluginConfigDialog
357 def getConfigFunction( self ):
358 return self.configure
360 def configure( self ):
361 dlg = GCPluginConfigDialog( ... ) # Will be discussed below
362 if dlg.exec_() == QDialog.Accepted:
363 # Will be discussed below
368 The user choice should be stored to be used next time Codimension starts. There are many
369 options how to do it and only one of them is considered here. The user choice
370 will be stored in file `gc.plugin.conf` which uses an industry standard ini files format.
371 Where to keep this file? The plugin message destination choice does not depend on a
372 project so it does not make sense to store `gc.plugin.conf` in a project specific data
373 directory. It makes sense to store the file where IDE stores its settings.
374 We'll need a few member functions to deal with the user
375 choice and one member variable. The member variable will be initialized in the plugin
376 class constructor with "do not show anything".
381 class GCPlugin( WizardInterface ):
383 def __init__( self ):
384 WizardInterface.__init__( self )
385 self.__where = GCPluginConfigDialog.SILENT
388 def __getConfigFile( self ):
389 return self.ide.settingsDir + "gc.plugin.conf"
391 def __getConfiguredWhere( self ):
393 config = ConfigParser.ConfigParser()
394 config.read( [ self.__getConfigFile() ] )
395 value = int( config.get( "general", "where" ) )
396 if value < GCPluginConfigDialog.SILENT or \
397 value > GCPluginConfigDialog.LOG:
398 return GCPluginConfigDialog.SILENT
401 return GCPluginConfigDialog.SILENT
403 def __saveConfiguredWhere( self ):
405 f = open( self.__getConfigFile(), "w" )
406 f.write( "# Autogenerated GC plugin config file\n"
408 "where=" + str( self.__where ) + "\n" )
414 At the time of the plugin activation the saved value should be restored so we
415 need to insert into the `activate` method (after initializing the plugin base class)
419 self.__where = self.__getConfiguredWhere()
422 Now we can complete implementation of the configuration function:
426 def configure( self ):
427 dlg = GCPluginConfigDialog( self.__where )
428 if dlg.exec_() == QDialog.Accepted:
429 newWhere = dlg.getCheckedOption()
430 if newWhere != self.__where:
431 self.__where = newWhere
432 self.__saveConfiguredWhere()
436 Having the destination of the information message at hand we can implement
437 the `__collectGarbage` method:
444 def __collectGarbage( self, ignored = None ):
448 currentCollected = gc.collect()
449 while currentCollected > 0:
451 collected += currentCollected
452 currentCollected = gc.collect()
454 if self.__where == GCPluginConfigDialog.SILENT:
457 message = "Collected " + str( collected ) + " objects in " + \
458 str( iterCount ) + " iteration(s)"
459 if self.__where == GCPluginConfigDialog.STATUS_BAR:
460 # Display it for 5 seconds
461 self.ide.showStatusBarMessage( message, 0, 5000 )
463 logging.info( message )
467 The last piece we need to discuss is menus. Codimension provides
468 four convenient places where a plugin can inject its menu items:
470 * main menu. If a plugin provides a main menu item then it is shown in the
471 Codimension main menu under the `plugin manager` menu item. The name of the
472 plugin menu item is set by default to the plugin name from the description file
473 however the plugin can change it.
474 * editing buffer context menu. If a plugin provides an editing buffer context
475 menu then it is shown at the bottom of the standard context menu. The plugin menu
476 item name policy is the same as for the main menu.
477 * project / file system context menu appeared for a file. It works similar to the
478 editing buffer context menu.
479 * project / file system context menu appeared for a directory. It works similar to
480 the editing buffer context menu.
482 In all the cases Codimension provides an already created parent menu item in which
483 a plugin can populate its menu items. If nothing is populated then Codimension
484 will not display the plugin menu.
486 The GC plugin will have only the main menu. The entries will be for collecting
487 garbage immediately and for an alternative way to run the plugin configuration
491 def populateMainMenu( self, parentMenu ):
492 parentMenu.addAction( "Configure", self.configure )
493 parentMenu.addAction( "Collect garbage", self.__collectGarbage )
496 def populateFileContextMenu( self, parentMenu ):
497 # No file context menu is required
500 def populateDirectoryContextMenu( self, parentMenu ):
501 # No directory context menu is required
504 def populateBufferContextMenu( self, parentMenu ):
508 The methods above is a convenient way to deal with context menus for most of the
509 cases. Generally speaking plugins are not limited with what they can do because
510 all the IDE objects are available via the global data and settings objects passed
511 in the `activate` method.
513 The configuration dialog code is not discussed here because it is pure PyQt code and
514 is not specific to the Codimension plugin subsystem.
516 Full plugin source code is available here:
518 * [`garbagecollector.cdmp`](http://code.google.com/p/codimension/source/browse/trunk/plugins/garbagecollector/garbagecollector.cdmp)
519 * [`__init__.py`](http://code.google.com/p/codimension/source/browse/trunk/plugins/garbagecollector/__init__.py)
520 * [`configdlg.py`](http://code.google.com/p/codimension/source/browse/trunk/plugins/garbagecollector/configdlg.py)
527 ###Printing and Logging
528 Plugins are running in Codimension context so everything what is done in
529 Codimension for the IDE is applicable to plugins. In particular Codimension
530 intercepts printing to **stdout** and to **stderr**. If a plugin prints on **stdout**:
533 print "Hi from plugin"
536 then the message will appear in the log tab in black. If a plugin prints on
540 print >> sys.stderr, "ATTENTION"
543 then the message will appear in the log tab in red.
545 Codimension also defines a logging handler so that the messages will be
546 redirected to the log tab, for example:
550 logging.info( "Message" )
553 will lead to a message in the log tab. Codimension can be started with `--debug`
554 option and in this case debug log level will be switched on, otherwise debug
555 log messages are suppressed. E.g.
559 logging.error( "Error message" ) # Will be shown regardless of the startup options
560 logging.debug( "Debug message" ) # Will be shown only if Codimension started as:
561 # > codimension --debug
565 ###Globals and Settings
566 When a plugin is activated references to the IDE global data singleton and to
567 the IDE settings singleton are passed to the plugin. Using these singletons a
568 plugin can get access to pretty much everything in the IDE. It is also possible
569 to cause Codimension crash if important data are improperly modified.
571 The `CDMPluginBase` class provides syntactic shugar to simplify access to the
572 most important IDE objects. The other IDE objects could be accessible using
573 direct access to the globals and settings members. If you feel more syntactic
574 shugar should be added to `CDMPluginBase` (or something is not accessible)
575 please feel free to contact Sergey Satskiy at <sergey.satskiy@gmail.com>.