Using Serna API
Serna is shipped with three example plugins that demonstrate basics of the Serna C++ API. The source code of the plugins is located in the directory: sernaInstallationPath/sapi-examples.
The distribution package provides the pre-build and working examples. To look at them and find out how they work choose:
.You can also build and install them manually as described below. Every example represents a Serna plugin consisting of a dynamic library and an *.spd description file. Description files provide plugin configurations and are used by Serna to instantiate the plugin.
To build an example type gmake (Linux, Mac OS)or nmake (Windows) in the corresponding example directory. To build all examples type the above command in the directory sernaInstallationPath/sapi-examples. On success, a dynamic library file with the .dll (Windows), .so (Linux), or .dylib (Mac OS) extension is created.
Copy the corresponding *.spd file into the sernaInstallationPath/sapi-examples directory.
The newly-built plugins will be immediately visible in Serna but only in the
( ) document because it contains the PI telling Serna where the plugin library is located.You can switch to Text Mode in the example document and see the PI:<?syntext-serna load-plugins="Indexterm UpdateOnSave LinkVoyager"?>
In order to make the plugins available for all Docbook V4.3 documents you must correct the load-plugins element of the Docbook V4.3 document template (sernaInstallationPath/plugins/docbook/dbk43.sdt). The element lists the names of the plugins that are activated for this template. These names are declared in the name element of the corresponding *.spd files. Add the plugin names (separated by spaces) to the template and restart Serna.
To build and install the 'indexterm' example do the following:
Go to the indexterm directory
Type gmake (Linux) or nmake ( Windows)
Make sure that the file indexterm20.dll (Windows), indexterm20.so (Linux) or indexterm20 .dylib (Mac OS) was created
Copy indexterm.spd to the directory sernaInstallationPath/sapi-examples.
Add the plugin to the Docbook templates by correcting the files: sernaInstallationPath/plugins/docbook/dbk42.sdt, sernaInstallationPath/plugins/docbook/dbk43.sdt, sernaInstallationPath/plugins/docbook/dblite05.sdt.
so that the entry t:load-plugins will contain the word Indexterm separated by spaces.
Restart Serna to make the example completely functional within the Docbook documents.
This tutorial provides you with step by step introduction about how to create Python plugins for Serna. When you are familiar with this tutorial you will be able to create Python plugins for Serna with fairly complex functionality.
Let's create a simplest plugin that creates "
" menu in Serna with a single " " menu item. Selecting this menu item creates a message box with inscription " " and OK button.Figure 1. "Hello, World!" Plugin Working
To create a plugin one should make the following major steps, which will be described in detail later:
The directory must be a subdirectory of sernaInstallationPath/plugins directory. (You can keep several plugins in one directory).
Sometimes you may need to keep your plugins separate from Serna installation. You can add additional plugins directory in
.This file keeps all the properties of the plugin, so that Serna knows where to find its executable, how it is named, when it should be loaded, etc. The file must have *.spd suffix and must reside in the plugin subdirectory.
This is the actual program code which should implement the plugin functionality.
Do not forget to put __init__.py file into the directory which contains your plugin, which is required by Python to load modules correctly. Usually this file can be empty unless you need some specific initialization code.
The first step is simple. We create the sernaInstallationPath/plugins/pyplugin-tutorial directory.
Now let's create SPD file with Serna: go to
. Serna will provide you with the list of the possible elements to insert.You'd want to create the file like the one which you can see in sernaInstallationPath/plugins/pyplugin-tutorial, that is named hello_world.spd:
This plugin is disabled by default to not interfere with the regular work with Serna. To see how this plugin works, uncomment load-for element and restart Serna. When you start Serna, you will see the new menu
.Figure 2. "Hello, World!" SPD File
<?xml version='1.0' encoding='UTF-8'?> <serna-plugin> <name>HelloWorld</name> <shortdesc>Hello, World Example Plugin</shortdesc> <dll>$SERNA_PLUGINS_BIN/pyplugin/pyplugin21</dll> <!-- <load-for>no-doc</load-for> --> <data> <python-dll>$SERNA_PYTHON_DLL</python-dll> <instance-module>helloWorld</instance-module> <instance-class>HelloWorld</instance-class> </data> <ui> <uiActions> <uiAction> <name>callHelloWorldMsg</name> <commandEvent>helloWorldMsgEvent</commandEvent> <inscription>Hello, World!</inscription> </uiAction> </uiActions> <uiItems> <MainWindow> <MainMenu> <PopupMenu> <properties> <name>helloWorldMenu</name> <inscription>Hello</inscription> <before>helpSubmenu</before> </properties> <MenuItem> <properties> <name>helloWorldMenuItem</name> <action>callHelloWorldMsg</action> </properties> </MenuItem> </PopupMenu> </MainMenu> </MainWindow> </uiItems> </ui> </serna-plugin>
Now let's see what is within this file:
Name uniquely identifies the plugin, and allows it to be bound to the documents, document templates or GUI mode (see below). Plugin name must be a valid XML name (it must be alphanumeric and must contain no spaces).
This is the human-readable annotation that you will see in
tab, when plugin is loaded. If you do not use shortdesc, then name will appear in the plugins tab instead.Specifies plugin executable (dynamic library). For Python plugins it is usually $SERNA_PLUGINS_BIN/pyplugin/pyplugin21. Just write it as is.
You should already noticed that Serna creates a GUI layout (buttons, commands, menus, etc) specific for document types and editing modes. There are three major GUI modes in Serna:
when no documents are opened yet ( no-doc)
when the current document is open in WYSIWYG mode ( wysiwyg-mode)
when the current document is open in text-mode ( text-mode).
Note that when load-for is specified, plugins will be loaded for all document types.
We'll create our plugin for no document GUI mode, when no documents are open.
Python plugins must always have the following properties:
Just write $SERNA_PYTHON_DLL.
In our case we'll create module helloWorld.py and put it to pyplugin-tutorial directory. That is why the value of this element now is helloWorld.
Serna will instantiate this class when it launches the plugin. We'll name this class HelloWorld. Please see the file helloWorld.py.
In this section we describe which GUI actions and controls must be created for the plugin.
To call a plugin method we must create UI Action. The action may be bound to one or many GUI controls that can call this action. When user activates the GUI control (e.g. selects menu item), the action is called, and it emits a command event. A command event triggers the plugin, which executes the required functionality.
We create an action for our plugin.
The unique action ID that is used for binding to the GUI controls. In our case it is callHelloWorldMsg.
This is the name of command event which will be emitted when the action is activated.
This name will show up on the menu item, for example. In our case it is " Hello, World!"
Here we describe the uiItems that should be in the user interface, and where they should be in the interface.
We want to create a new popup menu "Hello" with a menu item that is bound to "callHelloWorldMsg" action (and therefore will have inscription "Hello, World!"). We also specify where the GUI controls will be placed by making the describing elements the children of the following hierarchy: MainWindow->MainMenu.
Therefore, we prescribe the location for the UI items by specifying the hierarchy from the root element: MainWindow.
When you create SPD document in Serna, the SPD XML validating schema provides you with the list of available UI items and their properties.
TODO: Create a detailed reference about what UI items are available in Serna, how they nest,and what elements make them up.
We provide the following properties for the popup menu: its name, which is unique within the SPD file, the human-readable inscription, and element before, that describes the location of the popup menu within MainWindow. In this case it is the popup menu " Help", with name helpSubmenu.
You can learn the names and hierarchy of all GUI items from Interface Customizer:
. GUI items and actions originated by the plugin are always prefixed with the plugin name in the Customizer.Again, we specify the unique name of the item ( helloWorldMenuItem), and we bind the menuItem to corresponding uiAction, by supplying the action name: callHelloWorldMsg.
We do the following steps when writing SPD file:
Provide plugin description properties
Provide list of UI actions that trigger the plugin
Provide the description of the UI items that are bound to the UI actions.
The simple task of showing a message box requires a simple Python module. We name it helloWorld.py, as specified by SPD's instance_module element.
Figure 3. "Hello, World!" Plugin Programming Module
from SernaApi import * class HelloWorld(DocumentPlugin): """This is a "Hello World" Serna python plugin example.""" def __init__(self, a1, a2): DocumentPlugin.__init__(self, a1, a2) self.buildPluginExecutors(True) def executeUiEvent(self, evName, cmd): if evName == "helloWorldMsgEvent": self.sernaDoc().showMessageBox(self.sernaDoc().MB_INFO, "Title", "Hello, World!", "OK")
The first line imports the Serna API module.
Then we define the HelloWorld class, derived from DocumentPlugin class. This class is the plugin hook point for Serna, because we mentioned its name in instance_class. When Serna gets into the no-doc mode it creates the instance of Python class specified in instance_class.
Note the magic names of __init__ method arguments. The two arguments of the plugin class constructor are required and must be passed as-is to the DocumentPlugin class constructor. Be sure you always do this. See more detailed description on the plugin lifetime in Plugin Loading and Phases of Plugin Initialization.
The buildPluginExecutors method instantiates the GUI items we described in the SPD file. It makes that all the SPDs UI actions will be passed to executeUiEvent method that actually reacts on the events.
So, finally, the method that does all the job is executeUiEvent. When user activates the GUI control (selects menu item helloWorldMenuItem in our case), the UI action ( callHelloWorldMsg) is executed, emitting the helloWorldMsgEvent.
The event is passed to the executeUiEvent method. Note that only events of actions defined in the SPD will be passed to executeUiEvent.
Finally, the message box is shown.
SUMMARYWe do the following major steps when writing a plugin module:
Import Serna API functionality.
Create plugin instance class derived from DocumentPlugin with proper __init__ method.
Create the implementation of the executeUiEvent method.
With this example plugin we learned quite a lot:
What Serna plugin consist from (SPD and Python module), and where they are located.
What SPD is needed for, and how to create it.
From what major parts Python module plugin usually consists from.
What is UI action, UI item, command event and what they are for.
How to create menu and menu-item, and execute a menu-item action.
How to call a Message Box.
In this example we'll create a plugin that demonstrates how to examine the elements of the current document. We'll also see how to make Serna run a certain command just before user saves the document.
To be exact, we will work with " Simple Letter" document, and will create a command that help user to check if he somehow left an empty paragraph. User may call this command from the menu, and this command is also called (automatically) when user saves the document.
Serna distribution includes a very simple DTD for letters. The letter may have title, date, must have one or more paragraphs, and a signature element at the end of the letter. Now:
Create simple letter document, selecting:
.Pay attention to the new menu
right before in the main menu. The menu contains menu item. You can select this menu: it will do nothing (because we have no empty paras).Now create an empty para element, and select the
again.Serna will complain in the Message Box that an empty para exists. Click this message, and Serna will put a cursor into the empty para.
Now, right click the Message Box, and select " Clear Messages" (to clear the message box).
Now try to save the document. Again, Serna will complain that there are empty paragraphs.
Figure 4. Plugin Found an Empty <para> Element
Let's find out how this plugin works. See the SPD file for the plugin in sernaInstallationPath/plugins/pyplugin-tutorial/check_empty_para.spd.
The only major difference from the previous " Hello, World!" plugin, is that there is no load-for element in this SPD. This is because we want the plugin to be instantiated only for the Simple Letter documents. Therefore, we add Letter_CheckEmptyPara into the <load-plugins> element of the Simple Letter document template: sernaInstallationPath/plugins/syntext/simple_letter.sdt.
Now let's see the actual functionality that does the job:
Figure 5. ExecuteUiEvent for "Check Empty Para" Plugin
def aboutToSave(self): self.checkEmptyParas() def executeUiEvent(self, evName, cmd): """Execute the plugin's events. """ # Call this method on any evName, because we have # only one event in this example anyway. self.checkEmptyParas() def checkEmptyParas(self): document = self.sernaDoc().structEditor().sourceGrove().document() node_set = XpathExpr("//para[not(node())]\ [not(self::processing-instruction('se:choice'))]").\ eval(document).getNodeSet() if node_set.size(): self.sernaDoc().messageView().clearMessages() for n in node_set: self.sernaDoc().messageView().\ emitMessage("Empty <para> element found!", n)
For clarity we show only the relevant code fragments.
Because we define only one command event in the plugin, the method immediately calls checkEmptyPara method.
In the first line of the checkEmptyPara we create the document variable for easier access to the document grove. Let's see in what hierarchy this instance resides:
The DocumentPlugin instance has the access to SernaDoc instance which basically keeps all the objects that allow to work with the currently opened document (UI controls, Document Source Information, etc.).
Among other objects SernaDoc holds StructEditor instance which has the functionality that allows to correctly operate on the currently opened document (execute commands with undo/redo history).
Finally, the StuctEditor instance keeps the instance of the parsed XML document tree ( Grove). Grove structure closely resembles the Document Object Model (DOM).
The simplest way to find the empty paras is to evaluate proper XPath expression. The expression itself is clear enough, but there is the specificity with se:choice element. Let's see the expression in more detail:
//para[not(node())][not(self::processing-instruction('se:choice'))]
Find all para elements, that have no children, and which are not so called "choice-elements". The trick is that when Serna's validator creates element according to the schema, it may generate " choice-elements", that stand on the place where alternative elements are required but not yet entered by the user.
For technical reasons the " choice-elements" in Serna have ambiguous nature: they are represented as special processing-instructions in Serna grove, but in XPath expressions and XSLT patterns they will match to any element name that may be inserted in place of choice element according to the schema.
Now, if the evaluated XPath expression returned us non-empty node set, this means we do have empty paras, and we show the message in the message box:
emitMessage("Empty <para> element found!", n)
Note that we emit as many messages as empty nodes in the node set, and we provide node context to the message. With provided context information message box will be able to set Serna cursor into the element in question when user clicks on the message.
Finally, let's pay attention to method aboutToSave redefined in our plugin class CheckEmptyPara. This method is always called when user saves document, just before the save process.
With this example we learned:
How to associate plugin with specific document types (document that are opened with specific template)
How to access document tree (grove) and the document elements
How to find elements in the document using XPath
How to show messages in Serna Message Box
How to do some action just before document save.
In this section we'll learn how to create simple dialogs in Serna GUI, and how to modify the document with the input from the dialogs.
Again, we'll work with " Simple Letter" document, and create a command that will bring up a dialog that asks for the address fields. If the address already exists in the document, the dialog fields will be filled with the address. When user clicks OK, the new address is inserted into the letter.
Figure 6. Inserting an Address
To see how the plugin works do the following:
Create Simple Letter document, selecting:
.Pay attention to the new menu
right before in the main menu. This menu contains menu item.Select this menu. Serna will bring up
.Fill the edit-boxes, and click OK. Serna will create address element with the appropriate child elements.
If you select
again Serna will bring up the with the filled edit-boxes.Let's examine the SPD file for the plugin in sernaInstallationPath/plugins/pyplugin-tutorial/insert_address.spd. In this example the ui section is the most interesting for us. It defines the following UI actions:
Figure 7. Insert Address Plugin UI Actions
<uiActions> <uiAction> <name>insertAddress</name> <commandEvent>InsertAddress</commandEvent> <inscription>Insert Address</inscription> </uiAction> <uiAction> <name>okAddress</name> <commandEvent>OkAddress</commandEvent> <inscription>&OK</inscription> </uiAction> <uiAction> <commandEvent>CancelAddress</commandEvent> <name>cancelAddress</name> <inscription>&Cancel</inscription> </uiAction> </uiActions>
The first UI action insertAddress calls the
dialog. The other two are for executing the events when user clicks OK and Cancel buttons on the dialog.Besides the menu item insertAddressMenuItem, the uiItems section defines the layout of the dialog:
Figure 8. Insert Address Dialog Plugin Definition
<Dialog> <properties> <name>insertAddressDialog</name> <is-modal>true</is-modal> <is-visible>true</is-visible> <caption>Insert Address</caption> <width>200</width> </properties> <Layout> <GridLayout> <properties> <row-num>6</row-num> <col-num>2</col-num> <margin>0</margin> </properties> <GridWidget> <properties> <row>0</row> <col>0</col> </properties> <Label> <properties> <inscription>Street:</inscription> </properties> </Label> </GridWidget> <GridWidget> <properties> <row>0</row> <col>1</col> </properties> <LineEdit> <properties> <name>streetLineEdit</name> <editable>true</editable> </properties> </LineEdit> </GridWidget> ..... [skipping for the sake of clarity] ... <GridWidget> <properties> <row>5</row> <col>0</col> <col-span>2</col-span> </properties> <Layout> <properties> <orientation>horizontal</orientation> <margin>0</margin> </properties> <PushButton> <properties> <name>okButton</name> <action>okAddress</action> </properties> </PushButton> <Stretch/> <PushButton> <properties> <name>cancelButton</name> <action>cancelAddress</action> </properties> </PushButton> </Layout> </GridWidget> </GridLayout> </Layout> </Dialog>
We define properties of the dialog itself in the properties child of the Dialog element (the element names are self-explanatory). Note the name property, which uniquely identifies the dialog in the plugin.
It is more interesting how we define the dialog layout, that goes within the Layout element. The first and the only direct child of the dialog is GridLayout. This widget allows to place GridWidgets, within the grid of GridLayout. The GridLayout properties are therefore row-num, and col-num, that describe how many rows and columns the grid has, and also margin, that describes the width of external margins of the widget.
After that we define the GridWidgets for the grid cells. The most important properties of those are of course row, col, and col-span. They describe how GridWidgets occupy the grid cells.
Finally, inside the GridWidgets we put the terminal widgets, that you can see in the dialog:
The widget that simply shows an inscription
The edit box where user may type text
Button with an inscription. Usually used in the dialogs.
An invisible widget that is usually inserted between the widgets. It is used as a "spring" between widgets. When the layout they are inserted into changes its size, the stretch does not allow adjacent widgets to stick to each other.
We also used Layout widget, which is simpler than GridWidget. It allows to place widgets in a row either horizontally or vertically.
Now let's move on to the plugin program module. We'll examine method by method what it does.
def postInit(self): self.se = self.sernaDoc().structEditor() self.doc = self.se.sourceGrove().document() # Build the dialog from its .spd file description self.__dialog = self.buildUiItem("insertAddressDialog")
In this method we create a couple of "shortcut" class members for handy access to structEditor and the document node of the current document. We create these members in the special overloaded (virtual) postInit method, because access to these objects are not possible from DocumentPlugin class at the time of __init__ method execution (they are not created by that time). The postInit method is called by Serna to finalize initialization of the plugin after the document has been loaded and parsed.
The more detailed description of initialization and termination stages of the plugins are described in a separate section (Phases of Plugin Initialization).
The last line of postInit() constructs our . When created, it simply resides as an instance in memory, and is not shown because it is not attached to the GUI object tree.
Let's see the executeUiEvent method:
def executeUiEvent(self, evName, uiAction): if "InsertAddress" == evName: self.showDialog() return if "OkAddress" == evName: self.acceptAddress() # In both cases of OK and Cancel close the dialog self.__dialog.remove() # Set input focus back to the document edit window self.se.grabFocus()
It handles the three events the following way:
User clicked
menu item. Show the dialog.User clicked remove method), set focus back to the editing window.
button in the . Insert the new address he just typed in the dialog to the document, close dialog (by detaching the dialog object from the GUI tree withPay attention that the event was bound to the dialog
button, when we defined dialog in the SPD file.User clicked
button in the . Simply close the dialog and set focus back to the editing window.Now let's see how we construct a dialog and fill its fields with the existing values:
def showDialog(self): """Execute event that shows up a dialog""" # If the <address> element already exists, fill dialog with # its values. node_set = XpathExpr("//address\ [not(self::processing-instruction('se:choice'))]").eval(self.doc).getNodeSet() if node_set.firstNode(): for i in node_set.firstNode().children(): # Put the string value from node <xxx> to xxxLineEdit # control's property called "text" # # We get the text value of the node by evaluating its # XPath string value. Note, that the next expression # is evaluated with the current node as context node. text = XpathExpr("string()").eval(i).getString() line_edit = self.__dialog.findItemByName(i.nodeName() + "LineEdit") if line_edit: line_edit.set("text", text) self.sernaDoc().appendChild(self.__dialog) self.__dialog.setVisible(True)
This method basically does the two things:
if address element already exists in the document, then insert its value to the address edit-box.
shows the dialog.
First, we find the address node. We use XPath expression, making sure that se:choice pseudo-element did not match instead of the address. Then we examine each child of the address, taking its content by means of XPath function string(), that is evaluated in the context of this child. Then we insert the value of the child to the corresponding dialog edit-box using that fact that for the address' child named xxx we should have the edit-box named xxxLineEdit. If we find such an edit-box (by using findItemByName method of the dialog), then we set its property text to the value of the corresponding element.
After that we simply attach the prepared dialog into the GUI tree, which makes this dialog visible.
What happens when user types new values, and hits OK? This is handled by acceptAddress method:
def acceptAddress(self): """Execute event that inserts the values from dialog when user presses OK button. """ # Firstly, remove the old <address> if exists node_set = XpathExpr("//address\ [not(self::processing-instruction('se:choice'))]").eval(self.doc).getNodeSet() if node_set.firstNode(): self.se.executeAndUpdate( self.se.groveEditor().removeNode(node_set.firstNode())); # Build element tree, taking text from the dialog, # and insert to the document. address = ["street", "city", "state", "zip", "country"] fragment = GroveDocumentFragment() address_element = GroveElement("address") fragment.appendChild(address_element) for i in address: text = self.__dialog.findItemByName(i + "LineEdit").get("text") address_child = GroveElement(i) if len(text): address_child.appendChild(GroveText(text)) address_element.appendChild(address_child) # Find the position for the new <address> node. # This is either right after <title> and <date> elements if # they exist, or this is the first child of the document. node_set = XpathExpr("/*/title|/*/date").eval(self.doc).getNodeSet() if node_set.size(): position_node = node_set.list()[-1] else: position_node = None if position_node: grove_pos = GrovePos(position_node.parent(), position_node.nextSibling()) else: grove_pos = GrovePos(self.doc.documentElement(), self.doc.documentElement().firstChild()) self.se.executeAndUpdate(self.se.groveEditor().paste(fragment, grove_pos))
We do the following steps:
We use the familiar XPath expression in order to find the address node. If such node exists (result node set is not empty), then we should remove the node from the grove.
Two actors are involved in removing the node. One is GroveEditor, that performs actions on the document XML grove and manages the undo/redo history. And the second is StructEditor, which is the document editor front-end. It paints the document view, calls validator, etc. It is usually forbidden to do any direct modifications of the document tree (such as adding or removing nodes) bypassing the GroveEditor, because it will cause inconsistency with the undo/redo framework and eventually will crash the application..
We perform the address node removal in the grove with the following line:
self.se.groveEditor().removeNode(node_set.firstNode())
GroveEditor removes the node and returns the Command object, that should be passed to StructEditor which will perform validation and update the document view:
self.se.executeAndUpdate(...)
We have to construct the new fragment of document grove, that has the address node and all its children. Note that we use GroveDocumentFragment instance for creating this fragment, because we are going to merge the "standalone" document fragment with the main document. From the code snippet below you can see that we use familiar DOM operations.
Again, when getting the text values for the address children's text nodes we use the fact that for the child node named xxx the dialog has corresponding line-edits: xxxLineEdit.
address = ["street", "city", "state", "zip", "country"] fragment = GroveDocumentFragment() address_element = GroveElement("address") fragment.appendChild(address_element) for i in address: text = self.__dialog.findItemByName(i + "LineEdit").get("text") address_child = GroveElement(i) if len(text): address_child.appendChild(GroveText(text)) address_element.appendChild(address_child)
The next code portion locates the insertion position for the new address element in the document.
According to the schema the correct position for address is either right after title and date, or it is the first element, if they do not exist. That is why we define the position node as the last node of the "/*/title|/*/date" node set:
position_node = node_set.list()[-1]
Now we construct the GrovePos object, which will be used as a reference point where to insert the new node:
if position_node: grove_pos = GrovePos(position_node.parent(), position_node.nextSibling()) else: grove_pos = GrovePos(self.doc.documentElement(), self.doc.documentElement().firstChild())
The GrovePos consists of (parent-node, before-node) pair which defines the exact position in the document tree.
Again, using the GroveEditor and StructEditor we insert the new element:
self.se.executeAndUpdate(self.se.groveEditor().paste(fragment, grove_pos))
Here we used "paste" command which inserts the document fragment into the document.
With this example we learned:
How to define a simple dialog in SPD file, how to show it, and how to operate with its graphical components.
Simple widgets that typically constitute dialogs: GridLayout, GridWidget, Label, LineEdit, PushButton, Stretch.
The postInit method.
How to define the position in the grove with GrovePos.
StructEditor, GroveEditor, and how to modify the document.
This chapter provides lower-level and more detailed view to the Serna API. It is illustrated in the terms of C++ API, because Python API rules are almost same with some exceptions which are shown later on.
Plugin objects (which are dynamic libraries) are loaded only on-demand as specified in the corresponding SPD ( Serna Plugin Description) file(s), document templates and/or instances. Serna processes SPD files and loads the plugins as follows:
At the Serna start-up time it reads plugin descriptions from all SPD files (*.spd) in all immediate subdirectories in $SERNA_INSTALL_DIR/plugins directory, and also in the all immediate subdirectories in the "Additional plugins path" if it is specified in the Preferences.
Serna searches all plug-in descriptions for preload-dll properties and pre-loads these DLL's. This is an advanced feature that is used only for certain 3rd party libraries which have problems with initialization of static objects. Never use preload-dll if you are not sure what you are doing.
After that Serna either opens a document or goes into "No-Document" mode (when no documents are opened yet). In any case, Serna searches for all plugin descriptions that have load-for element which specifies load mode for the plugins. The load-for can have the following values: no-doc, wysiwyg-mode, text-mode. If Serna finds the plugin(s) with the appropriate load mode it loads DLL specified by the dll property. The DLL load process is shown in more details later.
load-for can also be used for loading this particular plugin for some document template; in this case it must have no text, but template children element(s) with category (and, optionally, name) children which must contain template category and name, respectively. Example:
<load-for> <template> <category>TEI P4</category> <name>Lite</name> </template> </load-for>In this example, plugin will be loaded for TEI P4/Lite template only. If we omit name from template, then plugin will be loaded for all templates in this category.
When the document is being opened, Serna looks for the load-plugins DSI property (see section "DSI Resolution Rules" in Serna Developer's Guide). It then finds the plugin descriptions with names listed in load-plugins, and loads the appropriate DLL's.
For all Python plugins the DLL is always the same pyplugin DLL, which in turn loads the Python interpreter.
After loading the DLL Serna calls the init_serna_plugin function of the DLL. This function should return either the instance of SernaApiBase object (later on called plugin instance object) or 0, which means plugin initialization failure.
Since most plugins must have the interface to the Serna functionality, their init_serna_plugin must return instance of subclass of SernaApi::DocumentPlugin (which is in turn a subclass of SernaApiBase). There is a convenient interface: just use SAPI_DEFINE_PLUGIN_CLASS macro, which defines right init_serna_plugin function for you. Note that SernaApi::DocumentPlugin class can only be used for the plugins which should work in WYSIWYG mode (not in the no-doc or text-mode).
class MyPlugin : public SernaApi::DocumentPlugin { ... }; SAPI_DEFINE_PLUGIN_CLASS(MyPlugin)
This operation is done automatically for Python plugins. Python plugin must only define plugin class which is inherited from SernaApi.DocumentPlugin and mention the name of this class in the instance-class property of SPD file.
The plugin instance object is deleted when the document associated with it has been closed. The DLL itself is never unloaded.
When the document is being opened it goes through multiple phases of initialization, and plugins may do some custom actions during each phase. For this a corresponding virtual method must be implemented in the user plugin class (which must be inherited from SernaApi::DocumentPlugin).
Basic initialization is done, and UI command executors must be registered (with REGISTER_PLUGIN_EXECUTOR macro (C++ only) and buildPluginExecutors method. The document itself and user interface are not available at this point.
This method is called when the new document has just been created. At this point it is possible to modify the document tree directly (bypassing the GroveEditor). Typically this method is used to insert some data into newly created document via custom dialog.
This method is called when the user interface is being built. At this point plugin may add its own user interface controls, and usually must also call the buildPluginInterface function in the base class. By default (when not redefined by user) this method builds user interface items specified in the ui section of SPD file.
This method is called when the document has been opened. Plugins can do any post-initialization here. Serna API is fully available at this point.
This method is called when user asks to close the document, but immediately before actual close. Now plugin have a chance to save its persistent data. If this method returns false, then the user will not be allowed to close the document (dialog about unsaved data will appear). By default this method should return true.
This method is called when user asked to save the document, but just before the actual save occurs. This can be used e.g. for adding modification time stamps into the saved document.
Called when the document instance is being deleted. UI and document are not available at this point.
Direct modification of the document tree (bypassing GroveEditor) is allowed only within the newDocumentGrove(). If you will try to do so in any other case, this will inevitably corrupt your document and crash Serna. Navigation and read-only access is safe in all cases.
Serna API provides a wrapper layer to the native (internal) Serna API, which is not accessible to the end users. Wrapped interfaces tend to use Java-like access semantics (copy by value, no pointers) because it is easier to use and bind to the scripting languages such as Python. There are different kinds of wrappers which affect lifetime of the wrapped objects. Each API class is derived from one of the following wrapper types:
This wrapper holds raw pointer to the internal object. Therefore it is safe to create circular references with these objects; however one must make sure that referenced objects will be alive at the time when it is referenced. Usually this wrapper type is used for objects whose lifetime is not controlled by the plug-in (i.e. XsltEngine).
This is the most common wrapper: it uses reference counting for the underlying object. One should be careful for not introducing circular references with these objects, because they may cause memory leaks or even application crash in some cases.
Usually it is safe to access uninitialized object - in such case pointer to the underlying object will be zero and all class methods will just do nothing.
Many Serna API classes are tree nodes and include generic tree API. Tree API functions are described in the XTREENODE_WRAP_DECL macro. The functions are the same for all classes which have tree API, only their return type differs.
One of the most common tree-based structures is PropertyTree, which consists of PropertyNodes. Each PropertyNode has a name, and may contain string/numeric value OR children (other PropertyNodes). This data structure is used for Serna configuration, GUI properties, etc.
It is important to realize that Serna Python API provides the interface to the underlying C++ objects which aren't written in Python themselves. This difference is often subtle, therefore one should keep it in mind when writing Python plugins for Serna. Several most common problems which are encountered with improper use of Serna Python API are demonstrated below.
If you forget to put __init__.py file into your plugin directory, Python will not be able to load your plugin. Presence of this file tells Python that the current directory can be treated as a Python module. For the most purposes you may leave this file empty.
Serna API uses its own string type ( SString) in all API methods. In many cases conversion is done automatically, e.g. when you construct instance of Serna API object:
node = GroveElement("my-element") # OK - automatic conversion
However, the code below will cause mysterious error messages from the inside of the "re" module:
import re s = SString("my_pattern") r = re.compile(s) # mysterious errors will appear
This happens because re.compile is also written in C, and it expects Python string object. Before passing Serna API strings to the methods which need Python strings only, you can use explicit conversion function str():
r = re.compile(str(s))
Do not mix different string types in multi-operand string operations such as concatenation (" + "). Both Serna and Python strings support concatenation, but they cannot be concatenated directly to each other. Use str() function on all operands to bring all string types to Python string.
s1 = SString("abc") s2 = "def" s = s1 + s2 # error - different string types s = s1 + "def" # error - "def" is a Python string s = str(s1) + s2 # OK - both are Python strings s = s1 + String("def") # OK - both are Serna API strings
Note that if you also use PyQt you will have three different kinds of string objects: Python string, Serna API String and QString. Note that QString cannot be initialized directly from SString (and vice versa): you must use str() conversion in this case, too.
The mysterious "Runtime Error" will happen if you forget to initialize base class in your subclass:
class MyWatcher(SimpleWatcher): def __init__(self, blah): self.blah = blah watcher = MyWatcher(blah) # OOPS - Runtime Error
Should be:
class MyWatcher(SimpleWatcher): def __init__(self, blah): SimpleWatcher.__init__(self) # need this one! self.blah = blah watcher = MyWatcher(blah) # OK
Python uses reference counting behind the scenes, so it is very easy to create circular references with Python. This may cause memory leaks, undesired behaviour or even application crash. The typical mistake is shown below:
class MyWatcher(SimpleWatcher): def __init__(self, plugin): self.__plugin = plugin self.__plugin.do_something() class MyPlugin(DocumentPlugin): def postInit(self): self.__w = MyWatcher(self) self.sernaDoc().structEditor().setDoubleClickWatcher(self.__w)
The above code creates circular reference between the plugin object and the watcher, so neither of them will ever be deleted. In such cases, weak references must always be used:
import weakref class MyWatcher(SimpleWatcher): def __init__(self, plugin): self.__plugin = weakref.ref(plugin) self.__plugin().do_something() # note __plugin()
Sometimes users may want to create their custom dialogs with PyQt. Such dialogs will need a proper parent widget. To get the parent widget, use the following:
qw = ui_item_widget(self.sernaDoc())
ui_item_widget() function takes UI item as its only argument and returns corresponding QWidget.
Since wrapped Serna API objects aren't really Python objects, you cannot treat them as proper Python class objects. For example, you cannot change the dictionary of methods of the Serna API object.
Serna GUI system has several significant architectural concepts. The most important ones are the following:
The whole UI (User Interface) is represented as a completely uniform tree with UI objects (hereinafter UI Items) with properties ( UI Item Properties), each of which may be associated with one or more UI Actions. Custom applications (plugins) can traverse this tree, modify its properties, insert/remove objects, etc. All these changes are immediately reflected in the GUI view.
When several UI items reference to the single UI action, this means that they reuse properties of such action (e.g inscription), and their state becomes synchronized (e.g. check-box can be synchronized with the toggle button and the toggleable menu item). When user triggers some UI control, then Serna (or its plugin who registered this action) receives event with corresponding UI action (see executeUiEvent method in the DocumentPlugin class).
Serna GUI can support several completely different views in a single window (these views can be switched e.g. with tabs). Therefore, each document type (or document instance) can have its own set of controls that do not interfere with other documents opened at the same time in the same window.
The state of the GUI (or its parts) can be saved (in XML format) and restored. Serna uses this ability for saving the customizations made by the user. Users can explicitly save or restore GUI states using
and commands.The easiest way to explore Serna GUI is to use
To avoid naming conflicts, all UI items and actions created by the plugin using buildPluginInterface and registerPluginExecutors methods are automatically prefixed with the plugin name followed by the colon. Therefore, if you define MyAction in the MyPlugin via the .spd file, the real action name (visible in the Customizer) will be MyPlugin:MyAction.
Some UI items may change their properties (e.g. button can be toggled, or item may be selected from the list box). Such changes can be tracked using PropertyWatcher's (a generic mechanism for tracking changes on the value of the property tree nodes).
See the full list of available UI items and their properties in the SAPI reference guide.
Serna comes with a set of compiled-in icons, which can be referenced by name (icon ID), usually from icon element in UI item descriptions in SPD files. Besides, users can add icons (globally or for particular plugins) or override existing icons.
Naming conventions for icons (as they are referenced in UI items and user-visible pull-down lists) are as follows:
<icon-name> (just name without extension) means icon or pixmap defined in Serna (compiled-in or in dist/icons).
<plugin-name>:<icon-name> means icons which were registered from the plugin.
In .spd file, if icon element contains the names starting with " %: " (percent sign followed by the colon), then the percent sign is replaced with the plugin name.
Icon file(s) must be placed to the $SERNA_INSTALL_DIR/icons directory.
Icon file(s) must be placed to the subdirectory icons within the plugin directory.
If the icon with the same name already exists (built-in into Serna), then it will be overridden with the new icon.
Icon file naming conventions are as follows:
Icon file(s) must have names icon-file.EXT and icon-file_disabled.EXT (for disabled icons). EXT stands for graphic format extension, such as . gif or . png.
If the icon file names in the icons subdirectory of the plugin start with '_', then plugin prefix will not be added to the icon name.
The following graphic formats are supported: JPG, GIF, BMP, PNG, XBM, XPM, PNM.
CommandEvents are internal Serna events which implement some of its functionality. Some CommandEvents can be useful for the plugin developer, and DocumentPlugin class includes a method executeCommandEvent which allows direct calling of named command events. Many command events are bound to the UI actions by default and thereby can be called by triggering appropriate UI Action. For example, button is bound to the saveDocument UI Action which in turn calls SaveStructDocument command event.
Some command events can take input data and return a value. In such case, both the data and return value are the property trees. In the table below if command events takes input data it has an " I " mark in the Args column. If command event produces output data, it has " O ". Note that when calling command event one must obey these rules and supply input and output property trees as required, otherwise such call will be ignored.
Name | Args | Description |
---|---|---|
ShowFileDialog |
I, O |
Calls native Open File dialog or Save dialog. Input property tree:
Output property tree:
|
ShowUrlDialog |
I, O |
Calls WebDAV Open File dialog or Save dialog. Input property tree:
Output property tree:
|
SetXsltParams |
I |
Changes top-level parameters of applied XSLT stylesheet and updates document look Input property tree:
Arbitrary number of XXX properties can be passed in input property tree. |
GetXsltParams |
O |
Returns the list of top-level parameters in applied XSLT stylesheet Output property tree:
Arbitrary number of XXX properties can be returned in property tree. |
ShowElementAttribute |
I |
Opens dialog (if not visible) and ensures that certain (given) attribute is visible in it.Input property tree:
|
EditCommentOrPiDialog |
I, O |
Calls or dialog.Input property tree:
Output property tree:
|
SetElementAttributes |
Calls for editing (passed) attributes. Attributes cannot be added or deleted using this command event.Input property tree:
Output property tree:
|
|
SetAttributes |
Calls and returns attributes with their values, set (or/and added) by user.Input property tree:
Output property tree:
|