Plugins are one of the two extension mechanisms supported by zim. Plugins are loaded as part of the application code and can modify key components of the interface. One of our design goals is to keep the core functionality limited and ship default plugins for anything a user would want to be able to disable.
( The other extension mechanism being "custom tools". The latter allows defining tools in the zim menu that call an external script or application. This can be used to add some simple functions that only act on the files in the notebook. )
Plugins are written in Python3 and consists of a class defining the plugin and one or more extension classes.
See the module zim.plugins
for API documentation of the plugin framework.
NOTE: Under the GPL license used for distributing this program all plugins should also be licensed under the GPL. A closed source plugin extension is not allowed. A plugin is allowed to call any non-GPL program as long as the plugin itself is under GPL and the non-GPL program runs as a separate process with a clearly defined inter process communication interface.
Plugins are simply sub-modules of the zim.plugins
python package. However
only core plugins should be placed directly in the module folder.
To allow plugins to be installed locally, the $XDG_DATA_HOME/zim/plugins
folder and all $XDG_DATA_DIRS/zim/plugins
folders are added to the search
path for zim.plugins
.
By default, the home folder would be ~/.local/share/zim/plugins
.
The best for packaging is then is to organize your plugin as a folder with a
__init__.py
containing the main plugin class. Users can then directly unpack
this folder to ~/.local/share/zim/plugins
or directly branch your git
repository to this location.
The main plugin class should provide information for Zim to recognize the plugin and add it to the preferences dialog.
Example plugin class:
from zim.plugins import PluginClass
class MyPlugin(PluginClass):
plugin_info = {
'name': _('My Plugin'),
'description': _('My first plugin'),
'author': 'Your Name',
}
NOTE: when testing your plugin you will have to quit zim and restart after changing the plugin code because once a plugin is loaded, it keeps using the copy from memory.
The plugin class itself does not do much other than providing information about the plugin and its preferences.
To add functionality to your plugin you need to define one or more classes that do the actual work. These will be "extension" classes and must derive from an extension base-class.
You can find all extension points in the application by searching for the
@extendable
class decorator. Classes that are extendable have this decorator
and thus declare what extension base class they support.
For example, to add functionality to the PageView
class, you must define a
class derived from PageViewExtension
.
At the time of writing these extension base-classes are defined:
NotebookExtension
: for functions that act on signals of the notebook. Use this for extensions that work directly with the notebook data, or the index database. Typically a separate extension would be defined within the same plugin to add a UI that shows that dataPageViewExtension
: for functions that add functionality to the editor window, or want to add side panes next to the editor. Use this for e.g. for a side pane showing additional information or content for a specific pageNotebookExtension
: similar to aPageViewExtension
, but these extensions only load for the main window where the notebook can be navigated, not for windows with a single page that cannot be changed. Use this for e.g. for a side pane showing an index of a the notebookInsertedObjectTypeExtension
: special extension for plugins that want to define an object type (e.g. equation) that can be rendered inline in the text -- see also tozim.plugins.base.imagegenerator
moduleMainWindowExtension
: for any other changes to the mainwindow that are not handled by aPageViewExtension
or aNotebookExtension
When you define a subclass of such an extension class, it will be loaded automatically by the plugin for each instance of the target component.
Key interfaces for extensions are: adding actions, adding widgets, connecting to signals and calling methods on the extended object.
Apart from extensions, there is one other class that will also be used
automatically: Classes derived from the Command
class are used to handle
commandline arguments in the form "zim --plugin yourplugin
".
The class diagram below may help finding your way around the code and to identify where you want to extend the application.
Preferences are maintained by the plugin object and are global for the application. That means that they apply in the same way for all notebooks. If your plugin has a behavior that should be different per notebook, you need to use the notebook properties instead.
Preferences can be defined by adding a list of plugin_preferences
to your
plugin class. Within the plugin these are accessible through the preferences
dict and in the user interface, they will show up in the plugin configuration
in the preferences dialog.
Notebook properties can be defined similarly by adding a list of
plugin_notebook_properties
to the plugin class. To obtain the properties
for a specific notebook as a dict you need to call the plugin method
notebook_properties()
. In the user interface, they will show up in the
properties dialog.
Some extension classes use actions to define menu items. These can be defined
using the @action
or @toggle_action
decorators from the zim.actions
module.
The functions zim.plugins.find_extension()
and find_action()
can be used to access
extensions and actions defined by other plugins.
Zim is build using the Gtk / GObject toolkit. This toolkit relies heavily on the concept of "signals" to collaborate between classes.
In short, this means that any class can define a number of signals that are
emitted for various events. Objects that are interested in these events can
register an event handler using the connect()
method.
Zim defines a SignalConnectorMixin
class in zim.signals
with some
convenience methods for classes that want to connect to signals.
For classes that do not derive from GObject classes but do want to emit their
own signals zim has its own SignalEmitter
class in zim.signals
.
Keep in mind that with each connect()
an object reference is created to the
handler method. The reference is kept by the object that is being connected to
and is only broken when that object is being destroyed. So if you do
object_A.connect('some-signal', object_B.some_method)
then object_B will not be destroyed as long as object_A is alive. On the other hand, if object_A is destroyed, object_B simply doesn't get any signals anymore.
The SignalConnectorMixin
class keeps track of the connections it made, which
helps in cleaning up.
See the python style guide for best practices. Some additional remarks:
- In contradiction to the style guide, zim uses
TAB
s (not spaces) with a tabstop set to the equivalent of 4 spaces - Only use "assert" for checks that could be removed when code is stable, these statements could be optimized away
- Writing test cases is good, full test coverage is better.
Run
./test.py --cover
to get a coverage report. - Use signals for collaboration between classes where possible
- Signal handlers have a method name starting with "do_" (for signals of the same class) or "on_" (for signals of collaborating classes)
Zim comes with a full test suite, it can be executed using the test.py
script. See test.py --help
for its commandline options.
This test suite will at least test all plugins that can be loaded and contain the proper information.
To test the specific functions of your plugin, you need to write your own test case. Have a look at test cases of existing plugins for ideas how to do that.
If you think your plugin is a good fit for the list of default plugins in Zim you can create your own branch of the zim source code with your plugin added and open a merge request.
Some things to consider:
- Make sure the plugin not only solves your own problem, but is also applicable for a more generic use case that many people may have
- Each plugin should have its own page in the user manual that explains what it does and how to use it.
- Each plugin should come with its own test cases for the test suite. Other developers may not use your plugin, so if it breaks later on it may go undetected unless there is a test case for it.
Also see CONTRIBUTING.md for more guidelines on getting new features accepted for the main repository.
If your answer is not in this list, see if any of the default plugins do something similar and inspect the code.
The PageView object defines a signal activate-link
. An extension object
can connect to this signal and check the link that is being opened. If it
matches the URL scheme of interest you can handle it and return True
to let
the PageView know it should not try to open this link itself.