More unit tests
[codimension.git] / doc / plugins.md
blob59311559b547ccc7eb48e2892f42a7d998aa041d
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
14 Python.
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
25 may look as follows:
27 ~~~
28 /usr/share/codimension-plugins/plugin1/...
29                                plugin2/...
30                                plugin3/...
32 /home/mike/.codimension/plugins/plugin4/...
33                                 plugin5/...
34                                 plugin6/...
35 ~~~
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
45 plugin.
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
57     plugin wins.
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
60     version wins.
61 *   If names, vaersions and locations of two plugins match then an arbitrary one
62     wins.
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
66 interface.
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
82 follows.
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.
96 Plugin Files
97 ------------
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
101 follows.
104 /home/mike/.codimension/plugins/pdfexporter/pdfexporter.cdmp
105                                             __init__.py
106                                             util_functions.py
107                                             config_dialog.py
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.
115 [Core]
116 Name = PDF exporter
117 Module = .
119 [Documentation]
120 Author = Mike Slartibartfast <mike.slartibartfast@some.com>
121 Version = 1.0.0
122 Website = http://mike.slartibartfast.homelinux.com/pdfexporter
123 Description = Codimension PDF exporter plugin
124 License = GPL v.3
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:
144 ~~~{.python}
145 # The plugin modules do not require relative import
146 import config_dialog
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:
153 ~~~{.python}
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
178 systems.
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:
186 ~~~{.python}
187 if self.ide.project.isLoaded():
188     # The ide has a project loaded
189     ...
190 else:
191     # There is no project, the user edits individual files
192     ...
195 Access to all the IDE objects should start with:
197 ~~~{.python}
198 self.ide. ...
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:
216 ~~~{.python}
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:
229 *   a tab is closed
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:
238 *   in the log tab
239 *   on the status bar
240 *   do not show anything
241 The selected option should be memorized and restored the next time Codimension
242 starts.
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.
247 ~~~{.shell}
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:
256 [Core]
257 Name = Garbage collector
258 Module = .
260 [Documentation]
261 Author = Sergey Satskiy <sergey.satskiy@gmail.com>
262 Version = 1.0.0
263 Website = http://satsky.spb.ru/codimension
264 Description = Codimension garbage collector plugin
265 License = GPL v.3
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
270 `__init__.py` file:
273 ~~~{.python}
274 from plugins.categories.wizardiface import WizardInterface
276 class GCPlugin( WizardInterface ):
277     def __init__( self ):
278         WizardInterface.__init__( self )
279         return
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
284 initializations.
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:
291 ~~~{.python}
292     @staticmethod
293     def isIDEVersionCompatible( ideVersion ):
294         return True
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
308 last thing to do.
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.
313 ~~~{.python}
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 )
321         return
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 )
330         return
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
346 enabled.
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:
352 ~~~{.python}
353 from configdlg import GCPluginConfigDialog
355 # ...
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
364             pass
365         return
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".
378 ~~~{.python}
379 import ConfigParser
381 class GCPlugin( WizardInterface ):
383     def __init__( self ):
384         WizardInterface.__init__( self )
385         self.__where = GCPluginConfigDialog.SILENT
386         return
388     def __getConfigFile( self ):
389         return self.ide.settingsDir + "gc.plugin.conf"
391     def __getConfiguredWhere( self ):
392         try:
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
399             return value
400         except:
401             return GCPluginConfigDialog.SILENT
403     def __saveConfiguredWhere( self ):
404         try:
405             f = open( self.__getConfigFile(), "w" )
406             f.write( "# Autogenerated GC plugin config file\n"
407                      "[general]\n"
408                      "where=" + str( self.__where ) + "\n" )
409             f.close()
410         except:
411             pass
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)
416 the following:
418 ~~~{.python}
419         self.__where = self.__getConfiguredWhere()
422 Now we can complete implementation of the configuration function:
425 ~~~{.python}
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()
433         return
436 Having the destination of the information message at hand we can implement
437 the `__collectGarbage` method:
439 ~~~{.python}
440 import logging
442 # ...
444     def __collectGarbage( self, ignored = None ):
445         iterCount = 0
446         collected = 0
448         currentCollected = gc.collect()
449         while currentCollected > 0:
450             iterCount += 1
451             collected += currentCollected
452             currentCollected = gc.collect()
454         if self.__where == GCPluginConfigDialog.SILENT:
455             return
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 )
462         else:
463             logging.info( message )
464         return
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
488 dialog:
490 ~~~{.python}
491     def populateMainMenu( self, parentMenu ):
492         parentMenu.addAction( "Configure", self.configure )
493         parentMenu.addAction( "Collect garbage", self.__collectGarbage )
494         return
496     def populateFileContextMenu( self, parentMenu ):
497         # No file context menu is required
498         return
500     def populateDirectoryContextMenu( self, parentMenu ):
501         # No directory context menu is required
502         return
504     def populateBufferContextMenu( self, parentMenu ):
505         return
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)
524 Miscellaneous
525 -------------
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**:
532 ~~~{.python}
533 print "Hi from plugin"
536 then the message will appear in the log tab in black. If a plugin prints on
537 **stderr**:
539 ~~~{.python}
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:
548 ~~~{.python}
549 import logging
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.
557 ~~~{.python}
558 import logging
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>.