Using Serna API

Using Serna API

Serna API

Building Serna C++ API Examples

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: Help > Examples > C++ API Examples Demo .

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.

  • Building

    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.

  • Installation

    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 C++ API Examples Demo ( Help > Examples ) 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.

  • Example

    To build and install the 'indexterm' example do the following:

    1. Go to the indexterm directory

    2. Type gmake (Linux) or nmake ( Windows)

    3. Make sure that the file indexterm20.dll (Windows), indexterm20.so (Linux) or indexterm20 .dylib (Mac OS) was created

    4. Copy indexterm.spd to the directory sernaInstallationPath/sapi-examples.

    5. 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.

    6. Restart Serna to make the example completely functional within the Docbook documents.

Tutorial: Using Serna Python API

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.

A 'Hello, World!' Plugin

Let's create a simplest plugin that creates " Hello " menu in Serna with a single " Hello, World! " menu item. Selecting this menu item creates a message box with inscription " Hello, World! " 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:

  1. Create a directory where plugin will be residing

    The directory must be a subdirectory of sernaInstallationPath/plugins directory. (You can keep several plugins in one directory).

    Note:

    Sometimes you may need to keep your plugins separate from Serna installation. You can add additional plugins directory in Tools > Preferences > Search Path > Path to additional plugins directory .

  2. Create Serna Plugin Description file ( SPD file).

    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.

  3. Create the plugin executable module.

    This is the actual program code which should implement the plugin functionality.

Note:

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.

Step 1: Plugin Directory

The first step is simple. We create the sernaInstallationPath/plugins/pyplugin-tutorial directory.

Step 2: Creating SPD File

Now let's create SPD file with Serna: go to Document > New Document > Syntext > SPD . 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:

Important:

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 Hello .

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>: The unique plugin ID (required)

    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).

  • <shortdesc>: The human-readable description of the plugin (optional)

    This is the human-readable annotation that you will see in Tools > Preferences > Plugins tab, when plugin is loaded. If you do not use shortdesc, then name will appear in the plugins tab instead.

  • <dll>: Plugin dll (required)

    Specifies plugin executable (dynamic library). For Python plugins it is usually $SERNA_PLUGINS_BIN/pyplugin/pyplugin21. Just write it as is.

  • <load-for>: For what GUI mode we create the plugin?

    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.

  • <data>: The plugin-specific properties

    Python plugins must always have the following properties:

    • <python-dll>: The Python interpreter DLL

      Just write $SERNA_PYTHON_DLL.

    • <instance-module>: Name of plugin Python module

      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.

    • <instance-class>: The main class of the Python plugin

      Serna will instantiate this class when it launches the plugin. We'll name this class HelloWorld. Please see the file helloWorld.py.

  • <ui>: The description of User Interface controls of the plugin

    In this section we describe which GUI actions and controls must be created for the plugin.

    • <uiActions>: The list of plugin actions callable from the user interface

      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.

      • <uiAction>: "callHelloWorldMsg"

        We create an action for our plugin.

        • <name>: A unique for the plugin action name

          The unique action ID that is used for binding to the GUI controls. In our case it is callHelloWorldMsg.

        • <commandEvent>: Unique command event name

          This is the name of command event which will be emitted when the action is activated.

        • <inscription>: A human-readable action name

          This name will show up on the menu item, for example. In our case it is " Hello, World!"

    • <uiItems>: The user interface controls that call the plugin actions

      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.

      Note:

      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.

      • <PopupMenu>/<properties>

        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.

        Tip:

        You can learn the names and hierarchy of all GUI items from Interface Customizer: Tools > Customize... . GUI items and actions originated by the plugin are always prefixed with the plugin name in the Customizer.

      • <MenuItem>/<properties>

        Again, we specify the unique name of the item ( helloWorldMenuItem), and we bind the menuItem to corresponding uiAction, by supplying the action name: callHelloWorldMsg.

SUMMARY

We do the following steps when writing SPD file:

  1. Provide plugin description properties

  2. Provide list of UI actions that trigger the plugin

  3. Provide the description of the UI items that are bound to the UI actions.

Step 3: Write the Plugin Python Module

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.

SUMMARY

We do the following major steps when writing a plugin module:

  1. Import Serna API functionality.

  2. Create plugin instance class derived from DocumentPlugin with proper __init__ method.

  3. Create the implementation of the executeUiEvent method.

Example Summary

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.

A Plugin Examining the Document

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: Document > New Document > Syntext > Simple Letter .

  • Pay attention to the new menu Letter right before Help in the main menu. The menu contains Check Empty Para 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 Letter > Check Empty Para 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:

  • SernaDoc

    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.).

    • StructEditor

      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).

      • Grove

        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.

Example Summary

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.

A Plugin Modifying the Document

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: Document > New Document > Syntext > Simple Letter .

  • Pay attention to the new menu Letter right before Help in the main menu. This menu contains Insert Address menu item.

  • Select this menu. Serna will bring up Insert Address Dialog .

  • Fill the edit-boxes, and click OK. Serna will create address element with the appropriate child elements.

  • If you select Insert Address again Serna will bring up the Insert Address Dialog with the filled edit-boxes.

Defining Custom Dialog Layout

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>&amp;OK</inscription>
  </uiAction>
  <uiAction>
    <commandEvent>CancelAddress</commandEvent>
    <name>cancelAddress</name>
    <inscription>&amp;Cancel</inscription>
  </uiAction>
</uiActions>

The first UI action insertAddress calls the Insert Address 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:

  • Label

    The widget that simply shows an inscription

  • LineEdit

    The edit box where user may type text

  • PushButton

    Button with an inscription. Usually used in the dialogs.

  • Stretch

    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.

The PostInit Method

    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.

Note:

The more detailed description of initialization and termination stages of the plugins are described in a separate section (Phases of Plugin Initialization).

Showing Dialog with Text from Document

The last line of postInit() constructs our Insert Address Dialog . 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:

  • InsertAddress

    User clicked Insert Address menu item. Show the dialog.

  • OkAddress

    User clicked OK button in the Insert Address Dialog . Insert the new address he just typed in the dialog to the document, close dialog (by detaching the dialog object from the GUI tree with remove method), set focus back to the editing window.

    Note:

    Pay attention that the event was bound to the dialog OK button, when we defined dialog in the SPD file.

  • CancelAddress

    User clicked Cancel button in the Insert Address Dialog . 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.

Modifying the Document Grove

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:

  1. Remove the address element if it already exists in the document, because it will be replaced with the new one with the new values.

    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(...)
  2. Create the address element

    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)
  3. Find the position for new address element

    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.

  4. Insert the ready address element

    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.

Example Summary

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.

Serna API Design Notes

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 Loading

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:

  1. 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.

  2. 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.

  3. 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.
  4. 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.

Note:

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.

Phases of Plugin Initialization

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).

  1. Plugin Class Constructor

    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.

  2. void newDocumentGrove()

    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.

  3. void buildPluginInterface()

    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.

  4. void postInit()

    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.

  5. bool preClose()

    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.

  6. void aboutToSave()

    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.

  7. Plugin Class Destructor

    Called when the document instance is being deleted. UI and document are not available at this point.

Note:

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.

Wrapped API Objects

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:

  • SimpleWrappedObject

    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).

  • RefCountedWrappedObject

    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.

Important Data Structures

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.

Python API Notes

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.

  • Missing __init__.py file

    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.

  • String Conversion Problems

    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.

  • Uninitialized Base

    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
  • Lifetime Issues

    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()
  • Qt Binding

    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())
    Note:

    ui_item_widget() function takes UI item as its only argument and returns corresponding QWidget.

  • Not a Native Python Object

    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.

GUI System Notes

Serna GUI system has several significant architectural concepts. The most important ones are the following:

  • Uniformity

    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).

  • Modality

    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.

  • Persistent State

    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 View > Save View... and View > Restore View... commands.

The easiest way to explore Serna GUI is to use Tools > Customize... dialog. It shows the tree of the UI items for the current document, their associated actions, and the list of all UI items which can be created and inserted into the UI items tree.

Plugins may create their own UI items and modify existing UI items tree or UI Actions. Usually all UI items and actions created by the plugin should be listed in the <ui> section in *.spd file.
Note:

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.

Note that UI items created by the plugins are also saved (since they become the part of the main UI tree). This allows users to customize plugin-originated UI controls as if they are Serna's own controls, and save/restore their state. In case when the plugin is switched off or removed but there are items in the saved UI tree created by such plugin, these UI items just disappear from the user interface and appear again (re-created in the original place) when the plugin is loaded again.

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.

Icons

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.

  • Adding global icons

    Icon file(s) must be placed to the $SERNA_INSTALL_DIR/icons directory.

  • Adding plugin-specific icons

    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.

Command Events

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, Save 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:

  • save-url

    If present, will show Save dialog instead of Open dialog.

  • filter

    File filter pattern. Default filter is "All files (*)"

  • caption

    Caption of the dialog.

Output property tree:

  • url

    Resulting absolute URL.

ShowUrlDialog

I, O

Calls WebDAV Open File dialog or Save dialog.

Input property tree:

  • save-url

    If present, will show Save dialog instead of Open dialog.

  • caption

    Caption of the dialog.

  • choose-encoding

    If present, combo-box for choosing document encoding will be shown.

  • url-list

    List of recently visited URLs, will be shown in Collection Url combo-box.

    • url

      Recently visited url. Several occurrences allowed.

  • filename

    Initial value for the Filename field.

Output property tree:

  • url

    Resulting absolute URL.

  • encoding

    Chosen encoding if save-url and choose-encoding are present.

SetXsltParams

I

Changes top-level parameters of applied XSLT stylesheet and updates document look

Input property tree:

  • XXX, actual name equals to the name of parameter.

    • value

      Parameter`s value.

    • type

      Parameter`s type. Acceptable values: string, bool, numeric.

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:

  • XXX, actual name equals to the name of parameter.

    • value

      Parameter`s value.

    • guessed-type

      Parameter`s type. Acceptable values: string, bool, numeric.

Arbitrary number of XXX properties can be returned in property tree.

ShowElementAttribute

I

Opens Element Attributes dialog (if not visible) and ensures that certain (given) attribute is visible in it.

Input property tree:

  • XXX, name of the attribute to show.

EditCommentOrPiDialog

I, O

Calls Edit Comment or Edit PI dialog.

Input property tree:

  • caption

    Caption of the dialog.

  • data

    Text of the comment (or PI data if Edit PI dialog is called).

  • target

    PI target. If present Edit PI dialog is called otherwise Edit Comment is called.

Output property tree:

  • data

    Text of the comment (or PI data if Edit PI dialog was called).

  • target

    Returned If Edit PI dialog was called.

SetElementAttributes

Calls Element Attributes Dialog for editing (passed) attributes. Attributes cannot be added or deleted using this command event.

Input property tree:

  • existing-attrs

    List of existing attributes.

    • XXX

      Existing attribute and its value.

  • attr-spec-list

    List of attribute specifications.

    • type

      Attribute type (schema defined)

    • default-value

      This value will be set if S et Default Value button is pressed.

    • enum

      List of acceptable values if attribute value is enumeration.

      • XXX

        One of the acceptable values. Name of this property is a value of attribute.

  • element-name

    Name of the element which attributes is edited.

  • ns-map

    List of namespace mappings.

Output property tree:

  • existing-attrs

    List of existing attributes.

    • XXX

      Existing attribute and its value.

SetAttributes

Calls Element Attributes Dialog and returns attributes with their values, set (or/and added) by user.

Input property tree:

  • existing-attrs

    List of existing attributes.

    • XXX

      Existing attribute and its value.

  • attr-spec-list

    List of attribute specifications.

    • type

      Attribute type (schema defined)

    • default-value

      This value will be set if S et Default Value button is pressed.

    • enum

      List of acceptable values if attribute value is enumeration.

      • XXX

        One of the acceptable values. Name of this property is a value of attribute.

  • element-name

    Name of the element which attributes is edited.

  • ns-map

    List of namespace mappings.

Output property tree:

  • existing-attrs

    List of existing attributes.

    • XXX

      Existing attribute and its value.