Writing a plugin

From MegaZine3
Jump to: navigation, search
See also: Writing a plugin II

This tutorial will give a short overview for writing a new plugin from scratch. The goal of this plugin will be to add a new button to the existing navigation bar and add custom functionality to it. This functionality will implement jumping to a certain anchor in the book. The name of the anchor will be changeable via a setting the plugin defines.

There are multiple ways of achieving custom GUI elements. The two preferred methods are explained here.

Contents

Prerequisites

To compile a plugin you will need the sources from the Subversion repository (pick the latest tag). Download them to a working directory. If using windows, give TortoiseSVN a try, it nicely integrates in the shell.

This way you'll also get the "dummy plugin" class, mentioned in the next paragraph, which is located under src/de/mightypirates/megazine/plugins/dummy.

Step 1: Basic skeleton

First off, create a directory for your plugin, and copy the dummy plugin class into it. Rename it accordingly and adjust the name in the constructor, too. You will see two other functions already defined.

register(...)

This function can be used to register things the plugin needs. This includes registering the plugin as attribute handler for book chapters, pages and elements, as well as registering new settings that should be parsed from the attributes set in the <book> tag.

initialize(...)

This function can be used for further initialization of the plugin, and will be called after the book has finished initializing, i.e. all basic definitions from the book's definition XML have been parsed, pages have been created and content loading has been triggered. Create GUI components here.

Plugin order

In either case, the register() and the initialize() function, the code will be executed after that of plugins your plugin depends on. For example, if you plugin depends on the NavigationBar plugin (as this example will), you can safely assume the NavigationBar plugin has completed it's initialization step when the initialize() function in your plugin is called, meaning the GUI of the NavigationBar has been created and can be accessed.

Code

The code so far looks like this (I've stripped all comments away to keep it shorter). I will name this plugin "Example".

package de.mightypirates.megazine.plugins.example {
import de.mightypirates.megazine.interfaces.IMegaZine;
import de.mightypirates.megazine.interfaces.IPluginManager;
import de.mightypirates.megazine.plugins.AbstractPlugin;
import de.mightypirates.megazine.plugins.PluginData;
import de.mightypirates.utils.interfaces.ISettings;
 
public class Example extends AbstractPlugin {
 
    public function Example(data:PluginData = null) {
        super(data || new PluginData("example", "1.0.0"));
    }
 
    override protected function register():void {
    }
 
    override protected function initialize():void {
    }
}
}

Step 2: Adding your GUI definitions

See also: ASUL Document and Editing the GUI.

Possibility 1, extra ASUL definition file

This is route one. Here the GUI definitions will be added to an ASUL file for the new plugin. To let the engine know we want some ASUL definitions, we need to call the super constructor accordingly. Change the super call to the following:

super(data || new PluginData("example", "1.0.0", [], true));

Notice the empty array? That's where we can define dependencies. Now, because we want to add a button to the navigation bar, we want to be sure it's loaded before we get going, so we add a dependency. This is achieved by creating a new instance of the Dependency class and put it into the array passed on:

super(data || new PluginData("example", "1.0.0", [new Dependency("navigationbar", true)], true));

The second parameter, true, is optional and defaults to false. What it does is define whether the dependency is a strong or a weak one. Weak dependencies are only used to enforce a proper loading order. Strong dependencies mean that the plugin cannot work until the plugin it depends on is available. Because we definitely need the navigation bar, we have a strong dependency.

Now that the engine knows we want ASUL definitions, we'll have to prepare a file for it to load. This file must be named like the plugin, with the extension "asul", and must be located in the GUI folder (see book@guipath). So, create a new text file and save it. In this case it'll be called example.asul, because the plugin's name is example. The basic ASUL file should look like this.

<?xml version="1.0" encoding="utf-8"?>
<asul>
</asul>

What we want is a button we can create and then add to the navigation bar. To do that, we'll create a Button definition, and because we want it to look nice, we define different behaviour for its three states using a style block. The style definitions look and behave a lot like CSS, but are only a very rudimentary implementation. E.g. paths / chains of element types will always mean direct children, no gaps will be "bridged". What does that mean? Well, say you have a box inside a button inside a box. The path box button box will be applied to the inner box, the path box box won't. Another difference to CSS is, that what is defined in styles are actually attributes with their values. E.g. you can have a style which defines a Text element's default display value.

If this concept is new to you, I'd strongly recommend you to learn a bit about CSS, as this will definitely help you understand how ASUL styles work, and there are enough tutorials on CSS out there.

Button definition

Now, the button only needs to be a basic skeleton, and it will look like this:

<button id="btn_example" style="example">
    <box name="$up$"/>
    <box name="$over$" style="over"/>
    <box name="$down$" style="down"/>
</button>

A lot of attributes there all of a sudden. Let me explain:

Style defintiion

So, now we have a basic definition. To define the styles, we'll add a style block the the ASUL file:

<style>
<![CDATA[
]]>
</style>

Inside it, add the following definitions:

button.example {
    minwidth: 24;
    minheight: 24;
    maxwidth: 24;
    maxheight: 24;
}
button.example box {
    width: pw;
    height: ph;
    background: image(gui/example/btn_example_up.png);
}
button.example box.over {
    background: image(gui/example/btn_example_over.png);
}
button.example box.down {
    background: image(gui/example/btn_example_down.png);
}

The four blocks do the following, top to bottom:

To keep the look of the GUI homogenous, it is recommended to use the common style for buttons, as defined in the megazine.asul file. I will not use it hear to make it easier to understand what's going on. Just bear in mind that normally it is recommended to use something like this: <button ... style="common example"> instead, and leave the size definitions to the common style.

Note: style definitions from the megazine.asul file are also valid for every plugin's ASUL file, but may be overridden in the plugin's ASUL file's style block.

Small technical note: normally you'd define the size via width and height, but because the navigation bar uses a layouting element we'll have to explicitly set the maximum and minimum sizes so the button's size won't be messed up.

See the paths in the definitions? The engine now expects to find three images in the gui/example/ directory ('gui' is the default value of book@guipath, adjust this if you use another one). The images are:

Final defintion file

The final file looks like this:

<?xml version="1.0" encoding="utf-8"?>
<asul>
    <style>
    <![CDATA[
    button.example {
        minwidth: 24;
        minheight: 24;
        maxwidth: 24;
        maxheight: 24;
    }
    button.example box {
        width: pw;
        height: ph;
        background: image(gui/example/btn_example_up.png);
    }
    button.example box.over {
        background: image(gui/example/btn_example_over.png);
    }
    button.example box.down {
        background: image(gui/example/btn_example_down.png);
    }
    ]]>
    </style>
 
    <button id="btn_example" style="example">
        <box name="$up$"/>
        <box name="$over$" style="over"/>
        <box name="$down$" style="down"/>
    </button>
</asul>

Possibility 2, add it to the NavigationBar plugin's ASUL definition file

An alternative is to add your GUI definition to the one of the navigation bar itself. This obvioulsy leads to a less "portable" implementation (necessary to change existing files instead of just adding new ones), but allows more freedom regarding the actual position of the button.

First off, as with the first possibility, define the dependency on the NavigationBar plugin in the constructor, but without the need for own ASUL definitions:

super(data || new PluginData("example", "1.0.0", [new Dependency("navigationbar", true)]));

Now to add an element to the NavigationBar plugin's definitions, open the navigationbar.asul file in the gui folder.

Look for a spot where you want your button to appear, and insert the following:

<button name="btn_example" title="Example" style="common example">
    <box name="$up$"/>
    <box name="$over$" style="over"/>
    <box name="$down$" style="down"/>
</button>

In the style block of the file, insert the following:

button.example box { background: image(gui/navigationbar/btn_example.png); }

If you want, change the path to the image. This image is the graphic used for the button, over and down states use the same image, but behave as defined in the common style in the megazine.asul.

Step 2: Accessing the GUI definition and adding interaction

Possibility 1, continued

All of the following goes into the initialize() function. First, we'll have to create a new instance of the button we defined in the ASUL file. To do this, use the createAsulObject() function:

var btn:DisplayObject = createAsulObject("btn_example");

Note that btn_example is the id of the element we wish to create.

Now, to add it to the navigation bar, we use the function given in it's interface.

// First, we get a reference to the NavigationBar plugin.
var navbar:INavigationBar = pluginManager_.getPlugin("navigationbar") as INavigationBar;
// On success, add the created button to it.
if (navbar) {
    // The addButton function returns a boolean value, telling us if the adding succeeded.
    if (navbar.addButton(btn)) {
        // Allright, button is in the bar. Now we add an event handler to handle clicks on the button.
        btn.addEventListener(MouseEvent.CLICK, handleButtonClick);
    }
}

Possibility 2, continued

As in the first route, the following code goes into the initialzie() function. To get access to the button we just search for it by name, starting at the very top of the plugin layer (which is used to display all plugin GUI).

var btn:DisplayObject = IAsulObject(megazine.pluginLayer).deepGetChildByName("btn_example");
// If the button was found, add an event listener.
if (btn) {
    btn.addEventListener(MouseEvent.CLICK, handleButtonClick);
}

Common code

Now, all that's left is add a function that acts as the event handler. As used in the registration process of the event listeners, we'll name it handleButtonClick:

private function handleButtonClick(e:MouseEvent):void {
    // Do something. For now, let's just jump to the first page to see if it works.
    megazine_.gotoFirstPage();
}

Code thus far

So, at this point we have the following code in our plugin (possibility 2 in comments):

package de.mightypirates.megazine.plugins.example {
/*
// Possibility 2
import de.mightypirates.asul.interfaces.IAsulObject;
*/
import de.mightypirates.megazine.interfaces.IMegaZine;
import de.mightypirates.megazine.interfaces.IPluginManager;
import de.mightypirates.megazine.plugins.AbstractPlugin;
import de.mightypirates.megazine.plugins.Dependency;
import de.mightypirates.megazine.plugins.navigationbar.INavigationBar;
import de.mightypirates.utils.interfaces.ISettings;
 
import flash.display.DisplayObject;
import flash.events.MouseEvent;
 
public class Example extends AbstractPlugin {
 
    public function Example(data:PluginData = null) {
        // Possibility 1
        super(data || new PluginData("example", "1.0.0", [new Dependency("navigationbar", true)], true));
        /*
        // Possibility 2
        super(data || new PluginData("example", "1.0.0", [new Dependency("navigationbar", true)]));
        */
    }
 
    override protected function register():void {
    }
 
    override protected function initialize():void {
        // Possibility 1
        var btn:DisplayObject = createAsulObject("btn_example");
        // First, we get a reference to the NavigationBar plugin.
        var navbar:INavigationBar = pluginManager_.getPlugin("navigationbar") as INavigationBar;
        // On success, add the created button to it.
        if (navbar) {
            // The addButton function returns a boolean value, telling us if the adding succeeded.
            if (navbar.addButton(btn)) {
                // Allright, button is in the bar. Now we add an event handler to handle clicks on the button.
                btn.addEventListener(MouseEvent.CLICK, handleButtonClick);
            }
        }
        /*
        // Possibility 2
        var btn:DisplayObject = IAsulObject(megazine.pluginLayer).deepGetChildByName("btn_example");
        // If the button was found, add an event listener.
        if (btn) {
            btn.addEventListener(MouseEvent.CLICK, handleButtonClick);
        }
        */
    }
 
    private function handleButtonClick(e:MouseEvent):void {
        // Do something. For now, let's just jump to the first page to see if it works.
        megazine_.gotoFirstPage();
    }
}
}

Using settings

So far so good. But assuming you want to make whatever happens inside the handler a bit more dynamic? Say you want it to jump to a certain anchor and you don't want to hardcode the anchor name. That's where settings, and the register() function come into play.

Note: if you do not use the register() function at all, and it just calls super.register() you can safely remove it. Note that you only need to call super.register() if your plugin extends another plugin.

So, let's say we want to jump to an anchor, we register a setting name "exampleanchor" in the register() function:

settings.register("exampleanchor", "first");

Validation

This paragraph is a short excursion into the realm of input validation.

For some basic types, namely String, Boolean and Number, the type will be recognized by the Settings.register() function, and you won't have to worry about validation and so on. For int and uint values, there's a shortcut by passing the type as the third parameter (e.g. settings.register("examplejumptopage", 0, uint);).

If you want more influence over the values you get, you can pass validation functions as a third parameter. There are some validation functions defined in the Validation class in the de.mightypirates.utils package, so you'll get the idea: a validation function takes at least two arguments, the first one is the value to parse, the second one a default value to use if the input value is invalid. There's also a utility function in the Validation class which allows binding of minimum and maximum values of validation functions. For example, if you only wish to allow integer values between 10 and 20, you could define the setting this way:

settings.register("exampleinteger", 15, Validation.validateBoundMinMax(Validation.validateInt, 10, 20));

Using the setting in our event handler

Now that we have the setting registered, we can rewrite our event handler to jump to the specified anchor. To do that, we'll first need to import the IAnchors interface from the Anchors plugin. We should also define a dependency, because if the anchors plugin isn't loaded our plugin won't be able to do anything. So, change the call to the super constructor:

super("example", "1.0.0", [new Dependency("navigationbar", true), new Dependency("anchors", true)], true);

For route two remove the true defining ASUL definition use.

In our event handler we can now do this:

var anchors:IAnchors = pluginManager_.getPlugin("anchors") as IAnchors;
if (anchors) {
    anchors.gotoAnchor(settings.getSetting("exampleanchor"));
}

Final code

So, with everything implemented, the final code looks like this. Again, possibility two in comments.


package de.mightypirates.megazine.plugins.example {
/*
// Possibility 2
import de.mightypirates.asul.interfaces.IAsulObject;
*/
import de.mightypirates.megazine.interfaces.IMegaZine;
import de.mightypirates.megazine.interfaces.IPluginManager;
import de.mightypirates.megazine.plugins.AbstractPlugin;
import de.mightypirates.megazine.plugins.Dependency;
import de.mightypirates.megazine.plugins.anchors.IAnchors;
import de.mightypirates.megazine.plugins.navigationbar.INavigationBar;
import de.mightypirates.utils.interfaces.ISettings;
 
import flash.display.DisplayObject;
import flash.events.MouseEvent;
 
public class Example extends AbstractPlugin {
 
    public function Example() {
        // Possibility 1
        super("example", "1.0.0", [new Dependency("navigationbar", true), new Dependency("anchors", true)], true);
        /*
        // Possibility 2
        super("example", "1.0.0", [new Dependency("navigationbar", true), new Dependency("anchors", true)]);
        */
    }
 
    override protected function register():void {
        settings.register("exampleanchor", "first");
    }
 
    override protected function initialize():void {
        // Possibility 1
        var btn:DisplayObject = createAsulObject("btn_example");
        // First, we get a reference to the NavigationBar plugin.
        var navbar:INavigationBar = pluginManager_.getPlugin("navigationbar") as INavigationBar;
        // On success, add the created button to it.
        if (navbar) {
            // The addButton function returns a boolean value, telling us if the adding succeeded.
            if (navbar.addButton(btn)) {
                // Allright, button is in the bar. Now we add an event handler to handle clicks on the button.
                btn.addEventListener(MouseEvent.CLICK, handleButtonClick);
            }
        }
        /*
        // Possibility 2
        var btn:DisplayObject = IAsulObject(megazine.pluginLayer).deepGetChildByName("btn_example");
        // If the button was found, add an event listener.
        if (btn) {
            btn.addEventListener(MouseEvent.CLICK, handleButtonClick);
        }
        */
    }
 
    private function handleButtonClick(e:MouseEvent):void {
        var anchors:IAnchors = pluginManager_.getPlugin("anchors") as IAnchors;
        if (anchors) {
            anchors.gotoAnchor(settings.getSetting("exampleanchor"));
        }
    }
}
}

Compiling

For help on compiling your plugin, please read the Compiling page, then copy one of the plugin targets in the build.plugins.xml file and adjust it for your plugin.

MegaZine3 Plugin-related articles
Plugins Anchors · Background Sounds · Batchpages · Bookmarks · Console · ElementIDs · Gallery · Google Analytics · Help · JavaScript · Keyboard Navigation · Links · NavigationBar · Options · Overlays · Password · PDFLinks · Print · PrintPDF · SWFAddress· Search · Sidebar · Slideshow · Titles
Plugin Articles Writing a plugin · Writing a plugin II · Plugin development
Personal tools
Namespaces
Variants
Actions
Navigation
documentation
miscellaneous
Toolbox