Skip to content

Latest commit

 

History

History
259 lines (189 loc) · 11.1 KB

PLUGIN_WRITING.md

File metadata and controls

259 lines (189 loc) · 11.1 KB

Plugins

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.

Defining your plugin

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.

Adding functionality to your plugin

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 data
  • PageViewExtension: 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 page
  • NotebookExtension: similar to a PageViewExtension, 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 notebook
  • InsertedObjectTypeExtension: special extension for plugins that want to define an object type (e.g. equation) that can be rendered inline in the text -- see also to zim.plugins.base.imagegenerator module
  • MainWindowExtension: for any other changes to the mainwindow that are not handled by a PageViewExtension or a NotebookExtension

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 and properties

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.

Actions

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.

Accessing functions of other plugins

The functions zim.plugins.find_extension() and find_action() can be used to access extensions and actions defined by other plugins.

Signals

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 ConnectorMixin 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 SignalEmitterclass 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 ConnectorMixin class keeps track of the connections it made, which helps in cleaning up.

Coding Style & Conventions

See the python style guide for best practices. Some additional remarks:

  • In contradiction to the style guide, zim uses TABs (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)

Test suite

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.

Merge request checklist

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.

How to ...

If your answer is not in this list, see if any of the default plugins do something similar and inspect the code.

Let a plugin handle a specific URL scheme

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.

Define a new inline object type

With an "inline object" we mean objects that are part of the wiki text, like equations, diagrams, tables.

These can be defined via the InsertedObjectTypeExtension. The object model requires a "model-view" architecture: the wiki text is parsed and stored in an "model" object. This model is than presented in a "view" (the actual Gtk widget). Keep in mind that a page can be shown in multiple windows at the same time, so there may be multiple views connected to a single model.

See also to zim.plugins.base.imagegenerator module for a special class of objects which generate an image based on text input - like the equation and diagram editor plugins.

Add a widget in the side pane of the window

To add widgets to the side pane of the window, you should either extend the PageView using a PageViewExtension or extend the NotebookView using a NotebookViewExtension. Both have a method add_sidepane_widget() which takes care of the placement in the window. The widget itself should be derived of a Gtk widget and also inherit from the WindowSidePaneWidget class.

The window layout works best if widgets are placed in the left and right side panes. Use of the top and bottom panes is supported, but discouraged.

Widgets are hidden when the pane is collapsed. Therefore the visible property of the Gtk widget can be used to determine whether a widget is currently being shown or not.

Maintain state of a plugin

Besides the preferences and properties, there is an uistate dict which can be used to store the state of a plugin - e.g. window size, visible selections etc. This dict is intended to store the state which one would wnat to restore after closign the application and restarting. It will never capture the exact state of the live Gtk widgets.