Creating an add-on using the add-on SDK

0

Introduction

In this article, I’m going to talk about my experience creating a Firefox add-on using the add-on SDK. Although I’ve been a member of the add-on SDK team for some time now, I’ve never actually used the SDK to create an add-on. Dogfooding is extremely important if you want to deliver a high quality product, so it was time to remedy that situation.

Every add-on begins with an idea. Coming up with a good idea for an add-on is not as easy as I thought though. Almost every idea I came up with could be implemented as a webpage as well, so implementing it as an add-on would not have provided any added value.

So, in what cases does an add-on provide added value over a webpage? Well, for one thing, unlike a webpage, the lifetime of an add-on is not restricted to that of a single site. This makes an add-on the implementation of choice if you want to keep track of information over multiple sites. With that in the back of my head, I started thinking if there was some kind of information I would like to keep track of.

A big problem for me is that I’m easily distracted by websites such as Twitter. I probably spend way more time on such sites than I should, but I’ve never been able to quantify just how much. Wouldn’t it be neat if I could create an add-on that gave me an overview of what fraction of my time was spent on which websites? An idea was born.

Using the canvas API to draw pie charts

Before thinking about how to get the data I need, I wanted to have a way to visualise it. For this, I decided to use the canvas API. Although not particularly fast, canvas is great for making simple vector based drawings. The API is simple enough that even beginning programmers can create something awesome looking with it.

The code I wrote takes a table of key/value pairs, creates a histogram from it, and then draws the sectors of a pie chart based on that histogram. It also sorts the pie sectors from large to small, and groups the last sectors together so that there are never more sectors than available colors (I couldn’t come up with a good algorithm for picking colors, so I decided to hardcode those instead).

For those of you that are interested, the canvas code can be found here below:

function Pie(table) {
    this.table_ = table;
};
 
Pie.colors = [
    "#FF8080",
    "#80FF80",
    "#8080FF",
    "#FFFF80",
    "#FF80FF",
    "#80FFFF",
    "#808080"
];
 
Pie.prototype.draw = function (context, x, y, radius) {
    var total = 0;
    for (var key in this.table_) 
        total += this.table_[key];
 
    var sectors = [];
    for (var key in this.table_)
        sectors.push({
            name: key,
            frac: this.table_[key] / total
        });
 
    sectors = sectors.sort(function (a, b) {
        return b.frac - a. frac;
    });
 
    sectors = sectors.slice(0, Pie.colors.length - 1).concat([{
        name: 'Other',
        frac: sectors.slice(Pie.colors.length - 1)
                     .reduce(function (frac, sector) {
            return frac + sector.frac;
        }, 0)
    }]);
 
    var startAngle = -0.5 * Math.PI;
    context.strokeStyle = "#FFFFFF";
    sectors.forEach(function (sector, index) {
        var endAngle = startAngle + 2 * Math.PI * sector.frac;
        context.fillStyle = Pie.colors[index];
 
        context.beginPath();
        context.moveTo(x, y);
        context.arc(x, y, radius, startAngle, endAngle, false);
        context.lineTo(x, y);
        context.fill();
 
        context.beginPath();
        context.moveTo(x, y);
        context.arc(x, y, radius, startAngle, endAngle, false);
        context.lineTo(x, y);
        context.stroke();
 
        var angle = startAngle + (endAngle - startAngle) / 2;
        context.fillStyle = "#FFFFFF";
 
        context.beginPath();
        context.moveTo(x + Math.cos(angle) * radius,
                       y + Math.sin(angle) * radius);
        context.lineTo(x + Math.cos(angle) * (radius + 8),
                       y + Math.sin(angle) * (radius + 8));
        context.stroke();
 
        context.textAlign = angle < 0.5 * Math.PI ? "left" : "right";
        context.fillText(sector.name, x + Math.cos(angle) * (radius + 16),
                                      y + Math.sin(angle) * (radius + 16));
 
        startAngle = endAngle;
    });
};

Using the above code, and given a table of key/value pairs, a canvas context, and the desired x,y position and radius, drawing a pie chart is now as simple as writing the following two lines:

var pie = new Pie(table);
pie.draw(context, x, y, radius);

With the preliminary work now in place, we can start creating the actual add-on. My goal was to create an add-on that keeps track of how much time I spend on a particular webpage, and provides some interface that shows a pie chart from this data.

Using the timers API to periodically update a table

The first surprise I ran into is that setTimeout is not available as a global function in the main script of your add-on. It turns out that this is because add-ons do not run within the same environment as ordinary websites. That means the Window object isn’t there, and since it lives on the Window object, neither is the setTimeout function.

Luckily, the SDK developers were smart enough to recognise the importance of the setTimeout API, so they provided the same API as part of the SDK itself. Using this timers API, we can write some code that periodically updates a table based on the name of the current website (curName) and the time recorded during the previous update (oldTime):

var table = {};
var curName, oldTime;
 
function updateTable() {
    var newTime = new Date().getTime();
    table[curName] += newTime - oldTime;
    oldTime = newTime;
    require('timers').setTimeout(updateTable, 1000);
};
 
require('timers').setTimeout(updateTable, 1000);

Note the use of the require function above. This function is predefined, and is used to load external modules in your add-on script. The SDK provides a set of built-in modules, such as the timers module, that provides the timers API we discussed earlier. It is also possible to write your own modules and load those, allowing you to better structure your add-on.

Using the tabs API to obtain information about the active tab

The above code isn’t particularly useful yet. We still need a way to provide curName and oldTime with an initial value, and to update the value of curName when the user visits another page. I decided to write another function, setUrl, that is used for both purposes. It is called once on initialisation, and once every time the user switches to another page:

function setUrl(url) {
    var match = url.match(/\/\/([^\/]*)\//);
    curName = match && match[1] ? match[1] : url;
    if (!(curName in table))
        table[curName] = 0;
    oldTime = new Date().getTime();
}

Presumably, whenever setUrl is called, the value of url is the URL of the website the user is currently visiting. For simplicity, we are only interested in the domain part of the URL, so we use a regular expression to extract it. The only remaining problem now is how to tell when setUrl needs to be called, and how to obtain the value of the url parameter.

As it turns out, the SDK provides an API, called the tabs API, that allows us to manipulate and obtain information about the tabs bar in the browser. In particular, the tabs API allows us to obtain the currently active tab, and query it for the URL of the page it is currently displaying, which is what we’re interested in.

We could have used a polling scheme, querying the currently active page every second or so. Since we already use a timer to periodically update a table, this would fit nicely into our current design. In general, however, polling is inefficient, and the SDK provides a better way, in the form of event listeners.

The events that we are interested in are called activate, deactivate, and ready. The first two events are not associated with any particular tab, and are triggered when a tab becomes active/unactive. The latter is associated with a particular tab, and is triggered when a page load in that tab has completed. What we want to do is add a listener for the ready event when to a tab when it becomes active, and then remove it when it becomes unactive again, like so:

function onready(tab) {
    setUrl(tab.url);
}
 
function onactivate(tab) {
    setUrl(tab.url);
    tab.on('ready', onready);
}
 
function ondeactivate(tab) {
    tab.removeListener('ready', onready);
}
 
var tabs = require('tabs');
tabs.on('activate', onactivate);
tabs.on('deactivate', ondeactivate);

One gotcha I ran into with the above code is that when the add-on is loaded, the currently active tab is already active, so the activate event won’t be triggered for it. This is easily solved, of course, by calling the handler for the activate event explicitly:

onactivate(tabs.activeTab);

Using the panel API to display a HTML page in a panel

At this point, we have an add-on that keeps track of how much time the user spends on each website. All we need now is some interface that allows us to show a pie chart from this information. Since we used the canvas API, this interface needs to display a simple HTML page with a canvas element in it. It turns out that this is easy, since the SDK provides an API, called the panels API, that allows you to create separate windows (or panels) displaying a HTML page.

Here is the HTML page I created:

<html>
    <head>
    </head>
    <body>
        <canvas width='492' height='492'></canvas>
    </body>
</html>

And here’s the code that creates a panel for it:

var panel = new require('panel').Panel({
    width: 512,
    height: 512,
    contentURL: require('self').data.url('index.html'),
    contentScriptFile: require('self').data.url('panel.js')
});

The contentScriptFile property needs some explanation. As I explained earlier, add-ons run in a different environment than actual webpages. That includes webpages displayed in a panel. Therefore, if you want to run some script on this page, you need to provide that separately from the script for the add-on itself. You can’t just embed the script in the HTML page either, because you usually want some way for both scripts to communicate.

The SDK provides a solution for this problem, called content scripts. A content script is a script that you provide to your page using the SDK, and that can communicate with the add-on script using a simple message passing API. This is probably best illustrated by an example. Here is the content script that I used for the panel (it also contains the pie chart code I showed earlier, but thats not displayed here):

self.port.on('draw', function (table) {
    var canvas = document.getElementsByTagName('canvas')[0];
    var context = canvas.getContext('2d');
    context.clearRect(0, 0, canvas.width, canvas.height);
    var pie = new Pie(table);
    var x = canvas.width / 2;
    var y = canvas.height / 2;
    var radius = 0.5 * Math.min(x, y);
    pie.draw(context, x, y, radius);
});

This code draws a pie chart in response to some event. The self.port object is provided to the content script by the SDK, and allows it to listen to the arrival of events sent by the add-on script. All we need now is for the add-on script to send an event to the content script whenever the table is updated. I did this by adding a single line to the updateTable function I showed earlier):

function updateTable() { 
    var newTime = new Date().getTime();
    table[curName] += newTime - oldTime;
    oldTime = newTime;
    panel.port.emit('draw', table);
    require('timers').setTimeout(updateTable, 1000);
};

Using the widget API to show a panel

We’re almost done! The only remaining problem is this: when a panel is created, it does not automatically appear. The last thing we need is therefore some interface that allows users to show/hide the panel at any given time. This is where the widget API comes in. The widget API provides a very simple means of adding an icon to the add-on bar in Firefox, and associating some behaviour with it. In fact, widgets and panels are so closely associated that there’s even some special property to associate a panel with a widget directly:

var widget = new require("widget").Widget({
    id: 'widget',
    label: 'My Addon',
    contentURL: "http://www.mozilla.org/favicon.ico",
    panel: panel
});

Conclusion

That’s it. Were’ done! The picture here below shows a screenshot of the final result:

We’ve used the canvas API to draw a pie charts, the timers API to periodically update a table, the tabs API to obtain information about the active tab, the panel API to display a HTML page in a panel, and the widget API to show the panel.

Hopefully, this little post-mortem has given you some idea on what it takes to create add-ons using the SDK. The API’s that it provides are straightforward, and powerful enough for almost every basic task you can come up with. The concept of content scripts takes a bit of time getting used to, but the SDK developers have gone through great lengths to make sure that they are easy to use.

Categories: Uncategorized

No responses {+}

Post Your Comment