1. Introduction
  2. Your First Extension
  3. Using Two or More Python Scripts
  4. Using Resources From Inside the Bundle
  5. Using Gconf to Store Preferences
  6. Setting up a Context Menu
  7. The GNOME Extension Bundle Builder
  8. Debugging

Introduction

GNOME panel extensions are a new type of object for the GNOME panel. Much like the current panel applets they are designed to be easy to write. Unlike any current applets, however, extensions are contained in a single file in the filesystem, and can be distributed without relying on package managers or build tools.

All panel extensions must be written in Python. This is not to slight any other language, in fact, it is entirely possible that support will be added for other languages in the future. Python, however, is an ideal language for this for several reasons:

Because of their single-file nature, extensions, unlike panel applets, can be distributed without a packaging system like apt or yum, and without running any kind of build utilities. Users simply point their web browsers at the panel extensions web site ( http://www.gnome.org/~tvachon at the time of this writing) and download extensions.

At the moment, extensions can only be run through a special container applet. Users add the container applet to the panel like any other applet, and can use the container's interface to choose an extension. Eventually extensions should be integrated into the panel's C-coded interface, but the current system will allow for easy working out of bugs.

Enough boring chatter, let's get to the meat and potatoes: your first panel extension!

Table of Contents

Your First Extension

In the grand tradition of first programs, we present to you the easiest of easy extensions, Hello World:


import pygtk
pygtk.require('2.0')

import gtk
import gobject

import panel_extension


def return_extension():
    return HelloExtension()
        
class HelloExtension(panel_extension.PanelExtension):
    def __init__(self):
        self.__gobject_init__()
        panel_extension.PanelExtension.__init__(self)

    def __extension_init__(self):

        self.label = gtk.Label("Hello, World!")

        self.add(self.label)

        self.show_all()
        


gobject.type_register(HelloExtension)


Nothing to it! Let's go through that slowly to figure it out:

import pygtk
pygtk.require('2.0')

import gtk
import gobject

import panel_extension

This section imports all of the Python modules we'll be needing later in the program. The import pygtk stuff ensures we have the correct version of PyGTK running, which really shouldn't be a problem with a modern desktop distribution of GNOME. Next we get gtk, which we'll soon see is very important, and gobject, which just makes sure the extensions fit nicely into GNOME. The last import is very important, as we'll be basing all of our applets on a class contained in the panel_extension module, panel_extension.PanelExtension.

Where's the she-bang!?
Some of you experienced Pythonista's out there may be wondering why we don't need a she-bang line. Well, to be frank, this script ain't made for runnin'. Instead, it will eventually be imported, simplifying the whole "different locations for Python installations" nastiness.

def return_extension():
    return HelloExtension()
ALL EXTENSIONS MUST IMPLEMENT THIS FUNCTION!

Got it? Good. It is very important that extensions implement a return_extension() function which simply returns an initialized copy of the class you are about to define. Easy as pie.

class HelloExtension(panel_extension.PanelExtension):
    def __init__(self):
        self.__gobject_init__()
        panel_extension.PanelExtension.__init__(self)

For anyone unfamiliar with Python, this is simply the beginning of a Python class. Like any good class, HelloExtension initializes its parent, and like any good GObject, HelloExtension calls __gobject_init__. You should make sure your extension includes these two functions in its __init__ function.

Unlike most classes, however, you should not include anything more in the __init__ function! While technically ok, a number of the functions provided by panel_extension.Panelextension will not work correctly if included here. To ensure everything works correctly...

    def __extension_init__(self):

        self.label = gtk.Label("Hello, World!")

        self.add(self.label)

        self.show_all()

... just include all of your code in a second function, __extension_init__. __extension_init__ should contain the meat and potatoes of the extension. In this case, we simply create a new label, add it to ourself ("we" in this case, are actually a type of GTK container called a "bin"), and then call self.show_all() to display ourself to the world! Yee ha!

gobject.type_register(HelloExtension)
With all that excitement, it would be easy to forget to register ourself. This is an important thing to do, however, as otherwise GTK won't quite work correctly. Just tack this on to the end of your script and you have yourself an extension!

Now that we're done with that, the only thing left to do is use the GNOME Extension Bundle Builder (gebb) to package things up. Before we get into that, however, let's revisit our Hello World Extension and look at a few more features of the panel extension system.

Table of Contents

Using Two or More Python Scripts

In our first example, we used only one script. This is the script we want to be loaded by the extension container applet and contains all of the necessary instructions to create the appropriate object on the GNOME panel.

It is often the case, however, that we want to seperate our script into more than one piece. Often this is done to create a logical seperation between utility functions and the main script, and often it is simply done to improve the readability and clarity of the script.

To do this, you as the extension writer have two options:

To illustrate this, let us assume that we have created a script hello_globals.py containing the following function:

def hello_text():
    return "This was imported!"

We can now import use this function in our main script using import hello_globals just like we would in any other Python script:

import pygtk
pygtk.require('2.0')

import gtk
import gobject

import panel_extension

import hello_globals

def return_extension():
    return ImportHelloExtension()
        
class ImportHelloExtension(panel_extension.PanelExtension):
    def __init__(self):
        self.__gobject_init__()
        panel_extension.PanelExtension.__init__(self)

    def __extension_init__(self):

        self.label = gtk.Label(hello_globals.hello_text())

        self.add(self.label)

        self.show_all()

gobject.type_register(ImportHelloExtension)

Great! All we have left to do is package everything up and ship it out. For details on that, check out the section on gebb.

Table of Contents

Using Resources From Inside the Bundle

Seperating data is good, but we could get away with including everything in one script. It would be much more difficult, however, to include things like images and non-Python text files in a single script file. It would be much easier if we could just include these resources in the bundle file and extract them when we want to use them.

The good news is that this is entirely possible, and quite easy. We will, however, need to use a couple features provided to us by the PanelExtension class, as illustrated in the following example:

import pygtk
pygtk.require('2.0')

import gtk
import gobject

import panel_extension

import hello_globals



def return_extension():
    return ImportHelloExtension()
        
class ImportHelloExtension(panel_extension.PanelExtension):
    def __init__(self):
        self.__gobject_init__()
        panel_extension.PanelExtension.__init__(self)

    def __extension_init__(self):

        bundle = self.get_bundle()

        image = bundle.open_gtk_image("hello.png")

        self.add(image)

        self.show_all()

gobject.type_register(ImportHelloExtension)

Let's look at that slowly:

We first use PanelExtension.get_bundle() to fetch a Python Bundle object representing the bundle file:

bundle = self.get_bundle()

Now that we have a Bundle object, we use its open_gtk_image() function to get a gtk.Image object which we can plug in just like a gtk.Label:

image = bundle.open_gtk_image("hello.png")

self.add(image)

In addition to open_gtk_image, Bundle provides two other methods for fetching items:

Bundle.open def open(filename) filename : name of file to extract This function returns a file-like object created with the StringIO module. Note that this means the object is only file-like, and does not provide all of the functions of a normal file object. See the StringIO documentation for more information.
Bundle.open_gtk_image def open_gtk_image(filename) filename : name of file to extract This returns a gtk.Image() object that can be used like any other gtk.Widget in an interface.
Bundle.get_icon def get_icon() No parameters This function returns the gtk.Image specified in the bundle manifest as the icon file for the bundle. There can only be one icon per bundle, and this file can be accessed with the Bundle.open_gtk_image function as well.

Future versions will probably support more functions, but for most cases, Bundle.open() should do the trick.

Table of Contents

Using Gconf to Store Preferences

At some point during your programming you'll probably want to store preferences in a way that will persist even after you exit GNOME or the extension is removed from the panel. panel_extension.PanelExtension provides a method to simplify this process:

PanelExtension.get_preferences_key def get_preferences_key(key) key : a unique key to store preferences under This function returns a Gconf path to store preferences in. The user should take care to use a key that will not be used by other extensions, as there is no method for ensuring the key does not overlap with a key used by another extension.

This function can be used as demonstrated in the following example:

import pygtk
pygtk.require('2.0')

import gtk
import gobject
import gconf

import panel_extension


def return_extension():
    return ImportHelloExtension()
        
class ImportHelloExtension(panel_extension.PanelExtension):
    def __init__(self):
        self.__gobject_init__()
        panel_extension.PanelExtension.__init__(self)

    def __extension_init__(self):


        self.client = gconf.client_get_default()
        
        self.prefs_key = self.get_preferences_key("/gconf_hello_extension")

        use_image = self.client.get_bool(self.prefs_key+"/useimage")

        if use_image:
            
            bundle = self.get_bundle()
            
            image = bundle.open_gtk_image("hello.png")
            
            self.add(image)

            self.client.set_bool(self.prefs_key+"/useimage", False)
            
        else:
            label = gtk.Label("I'm text!")

            self.add(label)

            self.client.set_bool(self.prefs_key+"/useimage", True)

        self.show_all()

gobject.type_register(ImportHelloExtension)

Again, let's look at the juicy bits.

We first need to import gconf so we can use it to interact with GNOME's Gconf database. For more information on the Python interface to Gconf, see the PyGTK documentation:

import gconf

The first thing we do is get a Gconf client object, which provides methods to get and set values in the Gconf database.

Next, we use the PanelExtension.get_preferences_key function to get an appropriate preferences key. Note that we must provide a key which we believe will be reasonably unique, as other extensions will be using the same directory in the database. In this case, gconf_hello_extension seems like it should do the trick:

        self.client = gconf.client_get_default()
        
        self.prefs_key = self.get_preferences_key("/gconf_hello_extension")

Next, we get a key named useimage from the Gconf database, which we are assuming is a boolean (True or False) type key. The Gconf client provides similar access methods for any type of key that can be stored in the Gconf database:

        use_image = self.client.get_bool(self.prefs_key+"/useimage")

Now, if the key we got from the Gconf database was True, we'll display an image and set the key to False. If the key was False, we'll use text and set the key to True. Thus, the extension will alternate between displaying an image and text each time it is loaded!

        if use_image:
            
            bundle = self.get_bundle()
            
            image = bundle.open_gtk_image("hello.png")
            
            self.add(image)

            self.client.set_bool(self.prefs_key+"/useimage", False)
            
        else:
            label = gtk.Label(hello_globals.hello_text())

            self.add(label)

            self.client.set_bool(self.prefs_key+"/useimage", True)

Using Gconf is easy and fun, but note this key difference from panel applets: Extension preferences will be saved even when they are removed from the panel. This also means that multiple instances of the same extension will share preferences, unless the extension writer deliberately avoids this.

Table of Contents

Setting up a Context Menu

panel_extension.PanelExtension provides two functions to create context menus. Because extensions are displayed using a container applet, this function is tied into the context menu creation functions provided by the library libapplet, which contains functions used to create panel applets.

Thus, because libapplet's context menu creation functions use Bonobo user interface XML, panel_extension.PanelExtension's context menu creation functions use Bonobo user interface XML.

This justification aside, the next example will attempt to provide a rudimentary introduction to adding custom menu items to an extension's context menu. The first thing to do is create an XML file defining the context menu:

<Root>
  <popups>
    <popup name="button3">
      <menuitem name="Sweet"     verb="Sweet" _label="_Sweet..."     pixtype="stock" pixname="gnome-stock-about"/>
    </popup>
  </popups>
</Root>

Next, we can use one of two functions defined in panel_extension.PanelExtension to set up our menu:

PanelExtension.setup_extension_menu def setup_extension_menu(xml,verbs) xml : a string containing the menu XML
verbs : a list of tuples of the form (verb, function)
This function creates a context menu from a string containing the menu XML and a list of (verb, function) tuples.
PanelExtension.setup_extension_menu_from_file def setup_extension_menu_from_file(file,verbs) file : file object containing menu XML verbs : a list of tuples of the form (verb, function) This function creates a context menu from a file containing the menu XML, and a list of (verb, function) tuples.

These functions can be used as demonstrated in the following example:

import pygtk
pygtk.require('2.0')

import gtk
import gobject

import panel_extension


def return_extension():
    return HelloExtension()
        
class HelloExtension(panel_extension.PanelExtension):
    def __init__(self):
        self.__gobject_init__()
        panel_extension.PanelExtension.__init__(self)

    def __extension_init__(self):
        self.bundle = self.get_bundle()
        
        menu_file = self.bundle.open("hello.xml")

        self.setup_extension_menu_from_file (menu_file,
                                            [("Sweet", self._sweet),])

        self.label = gtk.Label("Hello, World!")
        
        self.add(self.label)

        self.show_all()
        
    def _sweet(self, uicomponent, verb):
        self.label.set_text("Sweet!")



gobject.type_register(HelloExtension)

Let's look at that briefly:

The important function is the setup_extension_menu_from_file function from panel_extension.PanelExtension. The first parameter is simply a file-like object containing the menu XML. The second is a bit trickier: it is a list of tuples of the form (verb, function), where verb is the verb specified in the menu XML corresponding to the button you would like to associate with the specified function. Since in the menu XML we gave the "Sweet" button a verb of "Sweet", and we would like to associate this button with the function self._sweet, we make the following function call:

        self.setup_extension_menu_from_file (menu_file,
                                            [("Sweet", self._sweet),])

Now our function self._sweet can do almost anything it likes, in this case, simply change the text of the label.

    def _sweet(self, uicomponent, verb):
        self.label.set_text("Sweet!")

For more information on Bonobo UI XML, please consult the Bonobo UI documentation.

Table of Contents

The GNOME Extension Bundle Builder (gebb)

You've created your extension script and assembled your resource files, but it's all for nothing until you bundle them all up for the container applet to use.

Fortunately, gebb is your man. gebb not only builds an extension bundle, it is also used to build the XML file that specifies the various files to pack in and how they should be used. To build a bundle, simply go to the directory containing the files you would like to bundle up, add each file to a "manifest.xml" file in that directory using gebb, and run gebb build <bundle name> where <bundle name> is the name of the bundle file you want to build. One important thing to remember is that all bundle files must have a ".gpe" ending. If they do not, they simply will not be recognized by the container applet, and cannot be loaded.

Before we present a short example of using gebb, let us first run through gebb's command structure.

gebb has the following syntax:

gebb [options] <command> [command-options-and-arguments]

Where <command> can be any of the following:

add <file> [<files>...] add adds an entry to the manifest.xml file for each specified file. If the file does not exist, gebb will not add it.
add-main <file> add-main adds the named file to the manifest as the main file for the bundle. gebb will make sure there is only one such file, and will overwrite any currently set main script files when add-main is called.
add-icon <file> add-icon adds the named file to the manifest as the icon file for the bundle. gebb will make sure there is only one such file, and will overwrite any currently set icon files when add-main is called. The bundle icon is used by the container applet if a live preview cannot be created, and by any other programs that would like to display an icon for a bundle.
set-name <name> set-name sets a name for the bundle file. This name is used by the container applet and any other programs that would like to display a name for a bundle.
set-desc <description> set-desc sets a short description for the bundle file. This description is used by the container applet and any other programs that would like to display a description for a bundle.
build <bundle name> build builds a bundle according to the instructions contained in the "manifest.xml" file. <bundle name> must end with ".gpe"

Example

We have created an extension using a script named finalextension.py as follows:

import pygtk
pygtk.require('2.0')

import gtk
import gobject
import gconf

import panel_extension
import hello_globals

def return_extension():
    return ImportHelloExtension()
        
class ImportHelloExtension(panel_extension.PanelExtension):
    def __init__(self):
        self.__gobject_init__()
        panel_extension.PanelExtension.__init__(self)

    def __extension_init__(self):
        self.bundle = self.get_bundle()

        menu_file = self.bundle.open("hello.xml")

        self.setup_extension_menu_from_file (menu_file,
                                            [("Sweet", self._sweet),])

        
        self.client = gconf.client_get_default()
        
        self.prefs_key = self.get_preferences_key("/final_hello_extension")

        use_image = self.client.get_bool(self.prefs_key+"/useimage")

        if use_image:
            

            
            image = self.bundle.open_gtk_image("hello.png")
            
            self.add(image)

            self.client.set_bool(self.prefs_key+"/useimage", False)
            
        else:
            self.label = gtk.Label(hello_globals.hello_text())

            self.add(self.label)

            self.client.set_bool(self.prefs_key+"/useimage", True)

        self.show_all()


    def _sweet(self, uicomponent, verb):
        try:
            self.label.set_text("Sweet!")
        except:
            pass


gobject.type_register(ImportHelloExtension)

First, we move to the directory containing the files we would like to include in the bundle:

 $ ls
finalhello.py  hello_globals.py  hello.png  hello.xml  icon.png

Next, we add the our files to the manifest:

 $ gebb add-main finalhello.py
 $ gebb add hello_globals.py hello.xml hello.png
 $ gebb add-icon icon.png

Now we set a name and description:

 $ gebb set-name Final Hello Extension
 $ gebb set-desc Final Hello Extension Rocks!

Finally, we build everything into one big bundle:

 $ gebb build finalextension.gpe
Built finalextension.gpe.

We can also run gebb with the -v flag to get more information:

 $ gebb -v build finalextension.gpe
init file:
name = "Final Hello Extension"
description = "Final Hello Extension Rocks!"
main_script = "finalhello"
icon = "icon.png"

Built finalextension.gpe.
finalextension.gpe contains ['finalhello.py', 'hello_globals.py', 
'hello.xml', 'hello.png', 'icon.png', '__bundle_init__.py', 'manifest.xml']

Voila! We now have a panel extension bundle. We can move it into the ~/.panelextensions directory and use the container applet to add it to the panel:

 $ cp finalextension.gpe ~/.panelextensions/

Success!

Table of Contents

Debugging

Unfortunately, debugging panel extensions is a bit of a black art. Probably the best idea is to create test functions to make sure each of the pieces work correctly.

There will still be situations in which is is necessary to get some sort of output when the extension is added to the panel. In these situations you have two options:

First, you can define a log file somewhere on the file system and have all output (including error message) go to it. If, for example, your log file is at /tmp/log.txt the following function should do the trick:

import sys
out = open('/tmp/log.txt', 'w+')
sys.stdout = out
sys.stderr = out

Second, if you really need to get arcane, you can actually run the extension container applet from the command line before you add it to the panel. For example, if extension_container_applet.py is at /usr/lib/gnome-panel/extension_container_applet.py, run

/usr/lib/gnome-panel/extension_container_applet.py

before you add the container applet to the panel. Note that this will not work if there are any copies of the container applet currently on the panel.