PycaWM plugin tutorial
----------------------

1) Some general concepts

This small document describes how to develop a plugin for PycaWM. It includes an example plugin which plays some sounds when the user triggers specific actions (when s/he closes or maximizes a window, ...)

For PycaWM, a plugin is a python object which inherits from a specific class (simply named `Plugin') and sets some hooks in order to be called when the window manager executes its internal functions.

Default plugins for PycaWM are stored in the directory `plugins' of your PycaWM's installation path. (The installation path is `/usr/lib/python$(version)/site-packages/pycawm' if you have not modified the Makefile.)

The most important file in this directory is the `plugin.py' file which contains the basic object `Plugin' and the basic exception `PycaPluginError'. The other files are the other plugins bundled with PycaWM.

To create a new plugin, you must import the `Plugin' class located in the `plugins' directory:

"from pycawm.plugins import Plugin"

Your new plugin will subclass this base class:

"class ExamplePlugin(Plugin):"

Your plugin can have some default attributes:
* runnable (boolean, default set to False): indicates that the plugin can be executed. If you set `runnable' to False, this means that the plugin is an abstract class for other plugins.
* conflicts (list of strings, default to an empty list): indicates that the plugin cannot be executed when other plugins are already loaded. These other plugins are stored in the `conflicts' attribute by their class names. If your new plugin must only be executed one, set the `conflicts' attribute to a list containing the current plugin name. (e.g: conflicts = ['ExamplePlugin'])
* depends (dict of plugins to load with their values, default to an empty dict): indicates that the window manager must load other plugins before loading the current one. The keys of the dict are either the plugin class, or a tuple (plugin class, plugin name (as a string)). The values of the dict are dicts too, they are the configuration values of the plugin. (They store the same values as in the second parameter of the `add_plugin' method of the window manager. See below for further details.)

The plugin must have two methods: `__init__' and `destroy', called at initialization (when the window manager starts or when the user explicitly calls the `add_plugin' method of the window manager) and destruction (when the window manager quits or when the user explicitly calls the `remove_plugin' method of the window manager).

Note: when a plugin with dependencies is added, its dependencies are loaded before itself. If the plugin fails to load, the already loaded dependencies will be destroyed (the `destroy' method of each of the dependencies will be called).

The `__init__' method takes the instance (self) as the first parameter, and the instance of the window manager as the parameters. The remaining parameters are keyword arguments. These keyword arguments are the configuration values for the plugin. When the user adds a plugin, these values are inside the dict in the second parameter of the `add_plugin' method:

"wm.add_plugin(ExamplePlugin, dict(val1=1, val2=2))"

In PycaWM, the purpose of the `__init__' method of your plugin is to set some instance attributes and to hook some functions of the window manager. 

When the plugin fails to load, it must raise the `PycaPluginError' exception.

The purpose of the `destroy' method is to remove the hooks and to do some cleaning (i.e Xlib cleaning).

The `destroy' method only takes one parameter: the plugin instance (self).

2) A small example

To show a working example, we will write a small plugin which plays some sounds when its hooks are being called.

The sounds will be played throuth Pygame's mixer.

The plugin takes one configuration parameter: a dict with the hooked functions and the sounds to play.

---8<---

import pygame

from pycawm import Client
from pycawm.hookmanager import add_pre_hook, remove_pre_hook
from pycawm.plugins import Plugin, PycaPluginError

class SoundPlugin(Plugin):
    conflicts = ['SoundPlugin'] # only one instance of SoundPlugin
    runnable = True

    def __init__(self, wm, sounds=None):
        pygame.init()
        if not pygame.mixer:
            raise PycaPluginError('no sound support')

        if not sounds:
            raise PycaPluginError('no sounds given')
        self.sounds = dict((hooked,
                            lambda *args, **kwargs: self.play_sound(sound))
                           for hooked, sound in sounds.iteritems())
        for hooked, sound_fun in self.sounds.iteritems():
            add_pre_hook(hooked, sound_fun)

    @staticmethod
    def play_sound(sound):
        try:
            sound_ = pygame.mixer.Sound(sound)
            sound_.play()
        except:
            print 'cannot create a sound to play'

    def destroy(self):
        for hooked, sound_fun in self.sounds.iteritems():
            remove_pre_hook(hooked, sound_fun)

---8<---

You can load this plugin by adding this line to the configuration file:

"wm.add_plugin(SoundPlugin, dict(sounds={Client.close: '/path/to/sound.ogg'})"

Note: the `SoundPlugin' class must be imported before using it, the `Client' class too.

3) Further information

PycaWM has commented unit tests which describe how the plugin system works.
These unit tests are in the `tests/plugins.test' file.
