Tutorials

This section contains some tutorials for ROX application developers.

Ex-RISC OS users: Read the Python for BASIC programmers primer first.

Useful documentation

Creating a ROX application

This tutorial will show you how to create simple ROX applications using Python. Of course, you can write ROX applications in any language you choose, but python is probably the easiest. Before starting, you will need ROX-Filer, and ROX-Lib (2.0 or later).

http://rox.sourceforge.net/screens/tutor.png

If you find the python code confusing, you may want to read the httpPython Tutorial first.

Making the application directory

ROX applications should be supplied as self-contained Application Directories. This makes them very easy to install and manage, even for users without root access. Creating a basic application is simple:

  1. Choose a name for your application and create a directory with that name.
  2. Create a new file inside it called `AppRun' (not `apprun' or `APPRUN'!). This file will be run when a user clicks on the application directory. For this tutorial, it should initially contain the following:
    #!/usr/bin/env python
    import findrox; findrox.version(1, 9, 2)
    import rox
     
    rox.info('Hello World!')
  3. Make the AppRun file executable (using the filer's Permissions feature).
  4. Open ROX-Lib2's Help subdirectory (choose 'Help' from the filer's menu) and copy the findrox.py file into your new directory.

Now go up to the directory containing your application and refresh the display. Your directory should have a different icon -- click on it! If all went well, you'll get a nice message window with a pretty icon.

The code in detail

Before we go on, let's take a look at the program so far:

#!/usr/bin/env python
import findrox; findrox.version(1, 9, 2)
import rox

The first line tells the system that this is a Python program. The second loads the 'findrox.py' file you copied in, which locates ROX-Lib2 and adds it to the path. You only have to import findrox once in your program (not in every file). The third line loads the main module from ROX-Lib2 (this must be done after findrox is loaded).
rox.info('Hello World!')

This uses a function from the 'rox' module imported above to display a message in a box. There are several functions available for displaying dialogs, including alert (an error box), croak (a fatal error box), and confirm (allowing the user to cancel).

Windows

As well as dialog boxes, we can create normal windows too (replace the last line with these lines):

window = rox.Window()
window.show_all()
 
rox.mainloop()

The rox.Window class is like a regular GtkWindow, except that it increments a counter each time you create one and decrements it each time a window is destroyed. This allows your application to terminate automatically when all its windows are closed.

Initially the window is hidden. Widgets aren't actually displayed on the screen until you show them. `show_all' will also show any widgets which we put inside the window, which will be handy later...

The last line enters the main loop, causing our program to wait for events (such as the window being closed, buttons being pressed, etc) and handle these events by calling any signal handlers we may have set up (see below).

When the window is closed, it calls rox.toplevel_unref() to decrease the number-of-windows counter. Since it was 1 before, it will now reach zero and the rox.mainloop() function returns. Having reached the end of the code, the process will quit. If you want to continue running even when no windows are open, you can call rox.toplevel_ref and rox.toplevel_unref manually to prevent your program quitting too soon.

Extending the program

We'll need to use the gtk module now. You should import this via the rox module to make sure you get the correct version (not the gtk+-1.2 version). The module is simply called 'g', since you'll be typing it a lot!

We'll set the title for the window and give it a border. Then, we'll add a vertical packing box and put some things in it -- put this code just before the `window.show_all()':

from rox import g
 
window.set_title('My 1st App!')
window.set_border_width(10)
 
vbox = g.VBox(spacing = 4)
window.add(vbox)
 
button = g.Button('Click me!')
count = g.Label('0')
 
vbox.pack_start(g.Label('This is a button:'))
vbox.pack_start(button)
vbox.pack_start(count)

Structure of the window

Why do we need a packing box?

A window can only contain one widget directly. If you added two it wouldn't know how to lay them out, so we use a special VBox widget for that. A VBox stacks any widgets you put in it vertically (you could use an HBox instead to put them side-by-side).
By default, GTK tends to crowd widgets close together, so we use `set_border_width(10)' and `spacing = 4' to spread things out a bit.

Signals

Widgets (such as windows, buttons, etc) emit `signals' when things happen to them. For example, a button emits the `clicked' signal when the user clicks on it. By `connecting' some code to the signal, we can react to signals in any way we want.

Put this just after the previous code:

def inc_counter(b):
        number = int(count.get())
        count.set_text(str(number + 1))
 
button.connect('clicked', inc_counter)

Whenever the button is clicked, the `inc_counter' function is called. This function gets the text in the label (0 to begin with) and converts it to an integer. It then adds one to it, converts back to a string and puts the result back in the label.

Although the code we've looked at here is very simple, it's easy to extend it to more complicated problems. In particular, you can keep putting more widgets inside other widgets. For example, you could pack_start an HBox into the VBox and put buttons inside the HBox to get them along the bottom of the window.

Futher reading

Now we're going to look at other aspects of creating an application. If you want to know more about using GTK, you'll want to read the httpGTK Tutorial. The code is in C rather than python, but you can usually work out how to convert it.

Eg, this (first line is general, second is a specific example):

gtk_<sometype>_<some_method>(GTK_<SOMETYPE>(object), arg1, arg2, ...);
gtk_window_set_title(GTK_WINDOW(window), "The title");

becomes this:

object.some_method(arg1, arg2, ...)
window.set_title("The title")

The ROX applications Archive, Memo and Edit are written in python, if you want some more complicated examples. Feel free to copy bits for your own (GPL) programs.

Packaging and distributing

Before you can give your program to other people, you'll want to add a few things. For a start, you don't want the standard `Application Directory' icon! Use a graphics package (such as The GIMP) to create an icon up to about 42 by 42 pixels. Save it in PNG format as .DirIcon within the application directory. You'll probably have to refresh the filer's display to see it. You can also set the icon using the filer's 'Set Icon...' feature; make sure you select 'Copy image into directory'.

More importantly, you'll need to write some documentation. For this, create a subdirectory called Help inside the application. When you bring up a menu over the application in ROX-Filer and choose Help, this directory will be opened.

You can organise the files in here as you please, but some helpful things to include are Changes (What's new?), COPYING (What's the license?) and a README with some general information including:

  • The author and homepage of the application.
  • A summary of what the application does.
  • Enough instructions that someone can do something useful with it.
  • A place to report bugs.

A full manual would be nice too! Click on the Help button in ROX-Filer's toolbar for an example.

Finally, you should create an AppInfo.xml file with some details about your program. Here is a sample file:

<?xml version="1.0"?>
<AppInfo>
  <Summary>A little counter</Summary>
  <About>
    <Purpose>Counter</Purpose>
 
    <Version>2.0.0 (12-Jul-2002)</Version>
    <Authors>Thomas Leonard</Authors>
    <License>GNU General Public License</License>
    <Homepage>http://rox.sourceforge.net</Homepage>
 
  </About>
</AppInfo>

ROX-Filer will show the Summary text in a tooltip over your application, and the fields in the About section in the Info box. See the ROX-Filer Manual for more details about this file.

To create a package for distribution, drag the application directory to Archive to create a .tgz archive. Other people should be able to simply extract this archive anywhere and run the application.

If you've got extra python files in your program, you'll find that there are some '.pyc' files lying around -- delete them before archiving to make the archive smaller.

Drag-and-drop Loading

This tutorial shows how to handle files being dropped onto your application from the filer or from another application. We will make an application that counts the number of words in a file.

tutor

You should have already read the first tutorial, as this one assumes you already have the application directory and other files set up.

Simple drag-and-drop

The easiest way to handle drag-and-drop is to let the filer deal with it. The user drops data onto your application icon in a filer window, panel, etc. When this happens, the filer will run your application with a list of files dropped (sys.argv is the list of arguments passed to your program; argv[0] is the name of the program itself):

#!/usr/bin/env python
import findrox; findrox.version(1, 9, 8)
import rox
import sys, os
 
files = sys.argv[1:]
 
if not files:
        rox.croak('Drag a file onto the WordCount application to count it')
 
rox.info('The following files were dropped on me:\n' + '\n'.join(files))

If you drag some files onto this program, it will list their names. Try dragging from Edit's savebox onto the WordCount application. You should see the file is named '-', meaning that you should read the data from the standard input stream (it doesn't exist as a file).

Replace the last line with some code to actually count the files:

message = 'WordCount results:\n'
 
for filename in files:
        if filename == '-': stream = sys.stdin
        else: stream = file(filename)
 
        words = 0
        for line in stream:
                words += len(line.split())
 
        message += '\n%s: %d' % (os.path.basename(filename), words)
 
rox.info(message)

And that's it! Coupled with the next tutorial (drag-and-drop saving), you can write lots of useful 'filter' type applications, such as Archive.

Drag-and-drop loading within an application

You can make any widget in your application sensitive to data being dropped on it by using the rox.loading module's XDSLoader mix-in class:

#!/usr/bin/env python
import findrox; findrox.version(1, 9, 8)
import rox
from rox import g, loading
 
class DropBox(rox.Window, loading.XDSLoader):
        def __init__(self):
                rox.Window.__init__(self)
                loading.XDSLoader.__init__(self, ['text/plain'])
                self.set_border_width(20)
 
                label = g.Label('Drop some data here')
                self.add(label)
                label.show()
 
 
window = DropBox()
window.show()
 
rox.mainloop()

Notice how our new DropBox class is both a Window and an XDSLoader, so we have to initialise both base classes in the __init__ constructor. XDSLoader is passed a list of MIME types that we're interested in.

If you try running the code above, you'll get a window which you can drop files onto, although doing so just shows an error message because we haven't set a handler yet. Try dragging in a file from ROX-Filer, and some text from Edit's savebox.

There are various methods in XDSLoader we can override to handle the data, but the most useful is xds_load_from_stream():

        def xds_load_from_stream(self, name, type, stream):
                if not name: name = 'Untitled data'
 
                words = 0
                for line in stream:
                        words += len(line.split())
 
                rox.info('%s contains %d words' % (name, words))

Drag-and-drop saving tutorial

This tutorial shows how to write a simple Lorem Ipsum generator, with drag-and-drop saving. It also shows how to add extra widgets to the savebox.

You should have already read the first tutorial, as this one assumes you already have the application directory and other files set up.

ROX style saveboxes are unusual, because you can drag the icon in the savebox to a file manager or to another application.

Aim

Lorem Ipsum is nonsense text used in DTP packages to check layouts. If you've just set out some frames and you need some dummy text, you'll be able to drag from our generator into the frame (well, if you've got a decent DTP package, anyway ;-)

The code

Python is an object-oriented language, and you should have an object representing the data that you want to save. We will create a SaveBox widget, and tell it to use this object to get the actual data. We'll start with a complete program, as it's rather simple:

#!/usr/bin/env python
import findrox; findrox.version(1, 9, 8)
import rox
from rox import saving
 
text = """Lorem ipsum dolor sit amet, consectetaur adipisicing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris
nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
culpa qui officia deserunt mollit anim id est laborum."""
 
class Lipsum(saving.Saveable):
        def save_to_stream(self, stream):
                print >>stream, text
 
box = saving.SaveBox(Lipsum(), 'Lipsum', 'text/plain')
box.show()
 
rox.mainloop()

Well, that was pretty short -- most of it's the text to save! The three arguments to SaveBox's constructor are:

  • The object to save (we create an instance of the Lipsum class)
  • The default filename.
  • The MIME type.

Note that the SaveBox widget is automatically reference-counted by ROX-Lib, so the program will terminate correctly.
For more complicated tasks, you might want to look at the full saving API.

Adding widgets to the savebox

You may have spotted that our program is pretty pointless as it stands. We could have just put that text into a file and dragged the file in! So, now we're going to let the user specify how many repetitions they want. To do that, we'll create a subclass of the
SaveBox widget:

from rox import g, TRUE, FALSE
 
class LipsumBox(saving.SaveBox):
        def build_main_area(self):
                self.vbox.pack_start(self.save_area, 
                                expand = TRUE, fill = TRUE, padding = 0)
 
                hbox = g.HBox(FALSE, 0)
                hbox.pack_start(g.Label('Repetitions:'), FALSE, TRUE, 0)
 
                self.repetitions = g.Adjustment(1, lower = 1, upper = 100,
                                                step_incr = 1)
                spinner = g.SpinButton(self.repetitions, 1, 0)
                hbox.pack_start(spinner)
 
                self.vbox.pack_start(hbox, FALSE, TRUE, 0)
                hbox.show_all()

The build_main_area method is used to place the save_area (the bit with the icon and the text field) into the savebox. We can override this method to place it somewhere else or, as in this case, put it in the same place but add other widgets at the same time.

The HBox contains a label ('Repetitions:') and a SpinButton (a widget for entering numbers). We add this to the dialog's vbox area. Don't forget to show the new widgets! The Adjustment object stores the current number, the allowed limits and how much it increases by when you click on the arrows.

Now, we'll use this widget instead of the normal SaveBox:

box = LipsumBox(Lipsum(), 'Lipsum', 'text/plain')

In the Lipsum object (the object which actually generates the text), we can read the value of the adjustment to decide how many
times to write the text:

      def save_to_stream(self, stream):
                for x in range(box.repetitions.value):
                        print >>stream, text

And we're done! One handy Lorem Ipsum generator! Of course, there are better ways of generating additional text than by just repeating, so feel free to improve it!

Note: Normally, the Saveable will also be a Window and will store the SaveBox widget in an attribute, eg in a text editor. You can inherit from multiple classes easily in python, like this:

class Lipsum(rox.Window, saving.Saveable):
        def __init__(self):
                rox.Window.__init__(self)
                # (Saveable has no __init__ method)
                ...

Creating a ROX applet

This tutorial will show you how to create a simple ROX applet (a program that runs inside the panel). The applet will be a clock, so we'll also look at timed events.

You should have read the first tutorial.

Start with a normal application

It's good to be able to run applets in their own window too, and its very similar, so we'll start by creating a stand-alone clock application.
Create the application directory as before, and start with this code:

#!/usr/bin/env python
import findrox; findrox.version(1, 9, 8)
import rox
from rox import g
import time
 
time_display = g.Label('')
def update_time():
        time_display.set_text(time.ctime())
update_time()
 
main = rox.Window()
main.add(time_display)
main.show_all()
 
rox.mainloop()

OK, that's pretty straighforward. A label in a window. To get the time to update we need to add a timeout. This takes the minimum time between calls (in ms) and a function to call. Add this just before entering the mainloop:

g.timeout_add(1000, update_time)

If you run this, you'll see that the time only updates once. update_time() needs to return True to get called again:

def update_time():
        time_display.set_text(time.ctime())
        return True

Making it an applet

Copy AppRun as AppletRun. Instead of creating a rox.Window, create a rox.applet.Applet, passing the first command-line argument:

import sys
from rox import applet
main = applet.Applet(sys.argv[1])

Make sure AppletRun is executable. And that's it!

When you click on the program, it runs it in a window using AppRun, but if you drag it to a panel then ROX-Filer runs AppletRun with the ID of a socket on the panel.

For a more complicated task you would put most of the code in a separate file (eg main.py) and get AppRun and AppletRun to import that, to save duplicating code.

Applet sizing

People often want to make applets that scale to the current size of the panel. The problems people often have with this are:

  • The panel grows and grows...
  • ...or shrinks and shrinks...
  • ...or the user can't change the size anymore from the Options box.

These are caused by the applet trying to tell the filer what size the applet wants to be. The filer then sets/limits the panel size as requested, which often causes the applet to try to change its size for the new panel, etc.

Here's how to do it correctly. This explanation assumes a horizontal panel. For vertical panels, reverse width and height...

The trick is: set your vertical size request to a small and fixed value. 8 is good. Set the horizontal width to the desired size.

The sequence then looks like this:

  1. The panel asks the applet for its desired size.
  2. The applet responds 50×8 (want to be 50 pixels wide and as high as the panel).
  3. The panel sends the applet a size-allocate with its new size of 50×100 (the requested width × the actual height).

* Do not request the applet's actual height. This value is the minimum height for the applet. Stick to 8 (or whatever the actual minimum is).
* Do not request a height of -1. GTK will then use the widget's default natural size, which for images is the same situation as the previous point.

Here is a Demo Applet that both scales with the Panel and also adjusts to the orientation of the Panel (Horizontal or Vertical).

#!/usr/bin/env python
 
import findrox
findrox.version(1,9,0)
import rox, sys
from rox import applet, g
 
# This XPM ripped EVILly from the GTK tutorials:
xpm_data = [
        "16 13 3 1",
        "       c None",
        ".      c #000000000000",
        "X      c #FFFFFFFFFFFF",
        "   ......       ",
        "   .XXX.X.      ",
        "   .XXX.XX.     ",
        "   .XXX.XXX.    ",
        "   .XXX.....    ",
        "   .XXXXXXX.    ",
        "   .XXXXXXX.    ",
        "   .XXXXXXX.    ",
        "   .XXXXXXX.    ",
        "   .XXXXXXX.    ",
        "   .XXXXXXX.    ",
        "   .XXXXXXX.    ",
        "   .........    "]
 
class DemoApplet(applet.Applet):
    """
    A Demo Applet that displays a GtkImage and scales along with the Panel
    and adjusts according to the orientation of the Panel.
    """
    def __init__(self):
        applet.Applet.__init__(self, sys.argv[1])
 
        self.vertical = self.get_panel_orientation() in ('Left', 'Right')
        if self.vertical:
            self.set_size_request(8, -1)
        else:
            self.set_size_request(-1, 8)
 
        self.image = g.Image()
        self.add(self.image)
 
        self.pixbuf = g.gdk.pixbuf_new_from_xpm_data(xpm_data)
        self.image.set_from_pixbuf(self.pixbuf)
        self.size = 0
 
        self.connect('size-allocate', self.event_callback)
 
    def event_callback(self, widget, rectangle):
        """
        Get the new size and resize the pixbuf,
        but only if the size is different and is valid.
        This also assumes you want square widgets.
        """
        side = self.get_panel_orientation()
        if self.vertical:
            size = rectangle[2]
        else:
            size = rectangle[3]
 
        if size != self.size:
            self.resize_image(size)
 
    def resize_image(self, size):
        """Create a scaled version of the pixmap, and set image to that."""
        scaled_pixbuf = self.pixbuf.scale_simple(size, size,
                                                 g.gdk.INTERP_BILINEAR)
        self.image.set_from_pixbuf(scaled_pixbuf)
        self.size = size
 
    def get_panel_orientation(self):
        "Return the panel orientation ('Top', 'Bottom', 'Left', 'Right')"
        pos = self.socket.property_get('_ROX_PANEL_MENU_POS', 'STRING', False)
        if pos:
            return pos[2].split(',')[0]
        return 'Bottom'
 
main = DemoApplet()
main.show_all()
rox.mainloop()

Menus

Here we give our clock applet/application a popup menu.

If you have trouble understanding how the clock part works, read the (very short) applet tutorial.

menus

The application so far

Here's our old clock application, but written using a more object-oriented style. ClockWindow is defined as a special type of Window (ie, one which displays a clock, by adding a Clock widget when it's created). The functionality is the same as before. AppRun now looks like this:

#!/usr/bin/env python
 
import findrox; findrox.version(1, 9, 8)
import rox
import clock
 
class ClockWindow(rox.Window):
        def __init__(self):
                rox.Window.__init__(self)
                self.add(clock.Clock())
 
window = ClockWindow()
window.show_all()
rox.mainloop()

A Clock widget is a special type of Label (one which displays the current time). It's defined in a new clock.py file:

import time
import rox
from rox import g
 
class Clock(g.Label):
        def __init__(self):
                g.Label.__init__(self, '')
                self.update_time()
                timeout = g.timeout_add(1000, self.update_time)
                def destroyed(self):
                        g.timeout_remove(timeout)
                self.connect('destroy', destroyed)
 
        def update_time(self):
                self.set_text(time.ctime())
                return True

Adding a menu

Because keyboard short-cuts can be bound to menu items, we need to say where these settings should be loaded from and saved to. We do this using set_save_name() (in this case, <Choices>/MiniClock/menus will be used).
Then, we create the menu. Each menu we define has to be given a unique name (here main). This used to identify it in the shortcuts save file. Each item in the menu has a path (such as /Options) and the name of a method to call to handle it. The third argument is the type of the item (used for submenus and other special items). A default key can be given with the optional fourth argument.

#!/usr/bin/env python
 
import findrox; findrox.version(1, 9, 8)
import rox
from rox import g, Menu
import clock
Menu.set_save_name('MiniClock')
 
menu = Menu.Menu('main', [
        ('/Options',            'show_options', ''),
        ('/Quit',               'quit',         ''),
        ])

We need to detect button-3 clicks on the window, and open the menu when we get one. By default, windows aren't sensitive to button events, so we need to use add_events to change that. Now, any button press over the window will cause a button-press-event signal to be emitted.

We then connect to the button-press-event signal, check that button-3 was used, and open the menu if so. We pass self to the popup function; this is the object which will actually implement the various menu functions (in our case, this is the ClockWindow).

Finally, we 'attach' the menu to the window. This means that keypress events on the window will be checked against shortcuts defined in the menu. We pass both the widget to listen for events on (window) and the object which implements the menu functions (window again).

class ClockWindow(rox.Window):
        def __init__(self):
                rox.Window.__init__(self)
                self.add(clock.Clock())
                self.add_events(g.gdk.BUTTON_PRESS_MASK)
                self.connect('button-press-event', self.button_press)
                menu.attach(self, self)
 
        def button_press(self, window, event):
                if event.button == 3:
                        menu.popup(self, event)
 
window = ClockWindow()
window.show_all()
rox.mainloop()

Notice that we only create the menu once, but must do the signal and attaching code for every window created (in a multi-window application).

Implementing the menu functions

You should now be able to open the menu and choose something. When you do, an error box appears complaining that the function isn't implemented.

Unless you've got user-definable shortcuts disabled, you should also be able to trigger the error by setting a shortcut (open the menu, point at Quit and press Ctrl-Q, for example) and then pressing the shortcut when the window has the focus. Shortcuts will be automatically loaded and saved.

The error box is rather fun; if you click on the Details button then you get a complete stack-trace of where the error happened. When you select a stack frame, you are shown all the local variables for that frame and you can use the box at the bottom to evaluate expressions in the context of that frame. This
can be very useful for debugging.

In our case, however, it's obvious what the problem is (we haven't written the code yet ;-). We just need to define functions with the names given when we created the menu, eg:

        def quit(self):
                self.destroy()
 
        def show_options(self):
                rox.edit_options()

We'll see how to add the options dialog box in the next tutorial.

Note: you should also connect to the popup-menu signal. This is called when the user presses the open-menu key (some keyboards have a special key for this, or Shift+F10 is the default). This is mainly for disabled users (without mouse access). However, you still have to connect to button-press-event as well, because when the mouse is used you need to pass the event to popup (CVS ROX-Lib allows it to be None for the keyboard case).

Options tutorial

This tutorial shows how to add an options box to your application. We will be extending the example code created in the menus tutorial (since the menu is needed to access the options box).

options

Adding the options

The options menu item defined in the last tutorial doesn't work yet, because we haven't defined any options. As with the menus, the first thing to do is tell ROX-Lib where we're loading and saving our options. Options live in groups, but normally we just use setup_app_options() to create a single default group:

from rox import g, Menu
rox.setup_app_options('MiniClock')
 
import clock
 
rox.app_options.notify()

Notice that we initialise the options before importing the clock module. This is because the clock module is going to add options to the default group. After all options have been created (eg, by importing all the modules that define options), you must call notify() to let everyone know about changes from the default values (app_options is the default group created by setup_app_options).

Now create a single option in clock.py:

from rox import g
from rox.options import Option
 
format = Option('format', 'short')

The first argument to the Option constructor is the name of the option (used in the save file), and the second is the default value.

Now we need to define the layout of the options box. ROX-Lib makes this very easy. Create a new file called Options.xml:

&lt;options>
  &lt;section title='Display'>
    &lt;frame label='Time format'>
 
      &lt;radio-group name='format'>
        &lt;radio label='Short format' value='short'>Just show the time&lt;/radio>
        &lt;radio label='Long format' value='long'>Show the time and date&lt;/radio>
      &lt;/radio-group>
    &lt;/frame>
 
  &lt;/section>
&lt;/options>

'Display' is the section label, and is only shown if you define more than one section. Frames are used to group options visually under a heading. The element creates a list of radio-buttons for the option named 'format'. Each of the elements inside it corresponds to one choice, giving the label for the button, the value and the tooltip with a longer explaination of the option.

See the OptionsBox module documentation for a full description of each option type.

If you try it now, you'll find you have a working options box, complete with a Revert button, which will save your settings between runs. Now we just need to actually change the display as the option is changed...

Making the option do something

Getting the value of an option is as simple as option.value. Values are always strings, but you can use option.int_value for integer options to save converting them (if value isn't a valid number then int_value is -1).

        def update_time(self):
                if format.value == 'short':
                        self.set_text(time.strftime('%H:%M.%S'))
                else:
                        self.set_text(time.ctime())
                return 1

However, this doesn't update instantly when the option is changed (the user has to wait a full second for the next update). This isn't too bad, but in other cases the wait could be longer. We can get notified when an option is changed like this:

        def __init__(self):
                g.Label.__init__(self, '')
                self.update_time()
                # Add notify callback
                rox.app_options.add_notify(self.options_changed)
                timeout = g.timeout_add(1000, self.update_time)
                def destroyed(self):
                        g.timeout_remove(timeout)
                        # Remove callback
                        rox.app_options.remove_notify(self.options_changed)
                self.connect('destroy', destroyed)

When any option is changed, the options_changed method is called. You can use the has_changed flag to see if any particular option has changed. If several options are changed at once (eg, when the user clicks Revert) then options_changed will only be called once, at the end.

        def options_changed(self):
                if format.has_changed:
                        self.update_time()

You should now find that changing the option updates the display instantly.

Tasks tutorial

The tasks module provides a simple alternative to writing multi-threaded applications, using Python's generator functions. In this tutorial, we create a simple network service that can handle connections from several clients simultaneously.

tasks

Introduction

You will often find that your application must perform several tasks at once. For example, when the user is download something from the network, they will still want to be able to use your application (if only to click the Cancel button).

There are four ways to do this:

  • Use a new thread for each task.
  • Use callbacks.
  • Use a recursive mainloop.
  • Use this tasks module.

Using threads causes a number of problems. Some builds of PyGTK/Python don't support them; they can easily introduce race conditions, often leading to many subtle bugs; and they require lots of resources (you probably wouldn't want 10,000 threads running at once). In particular, two threads can run at exactly the same time (perhaps on different processors), so you have to be really careful that they don't both try to update the same variable at the same time. This requires lots of messy locking, which is hard to get right.

Callbacks work within a single thread. For example, you open a dialog box and then tell the system to call one function if it's closed, and another if the user clicks OK, etc. The function that opened the box then returns, and the system calls one of the given callback functions later. Callbacks only execute one at a time, so you don't have to worry about race conditions. However, they are often very awkward to program with, because you have to save state somewhere and then pass it to the functions when they're called.

A recursive mainloop only works with nested tasks (you can create a sub-task, but the main task can't continue until the sub-task has finished). We use these for, eg, rox.alert() boxes since you don't normally want to do anything else until the box is closed, but it is not appropriate for long-running jobs.

Tasks use Python's generator feature to provide a more pleasant interface to callbacks.

Getting started

We'll begin with this simple program that displays a log window:

#!/usr/bin/env python
from __future__ import generators
 
import socket
 
import findrox; findrox.version(1, 9, 17)
import rox
from rox import tasks, g
 
class LogWindow(rox.Window):
        def __init__(self):
                rox.Window.__init__(self)
                self.set_title('Network logger')
 
                swin = g.ScrolledWindow()
                swin.set_policy(g.POLICY_AUTOMATIC, g.POLICY_ALWAYS)
                swin.set_shadow_type(g.SHADOW_IN)
                swin.set_border_width(4)
                self.add(swin)
 
                self.text = g.TextView()
                self.text.set_editable(False)
                self.text.set_cursor_visible(False)
                swin.add(self.text)
 
                self.set_default_size(400, 300)
                swin.show_all()
 
        def append(self, message):
                self.text.get_buffer().insert_at_cursor(message)
 
log = LogWindow()
log.show()
log.append('Started\n')
 
rox.mainloop()

There's nothing surprising here, it's just so we can see what's going on. When something interesting happens, we use log.append() to log the message. If you run the program now, it will just open a window with a single message saying that it has started.

The main server task

We're going to allow other programs to connect to our logger and log messages with it. There can be any number of these clients sending us messages at the same time. We'll start by creating a Task whose job is to accept new connections. Put this just before rox.mainloop():

tasks.Task(server_task())

Define server_task somewhere above this, as follows:

def server_task():
        server_port = 8123
 
        server_socket = socket.socket()
        server_socket.bind(('', server_port))
        server_socket.listen(5)
        log.append('Listening on port %d...\n' % server_port)
        while True:
                blocker = tasks.InputBlocker(server_socket)
                yield blocker
                client_socket, client_addr = server_socket.accept()
                log.append("Got connection from %s\n" % str(client_addr))
                client_socket.close()

This function contains the yield keyword, which makes it special: when the function is called (with server_task()) it doesn't actually execute any of the code. Instead, it immediately returns a Python iterator. Calling this iterator's next method actually runs the function. The tasks module calls this later (once the mainloop has started) to actually start the task running.

If you're not familiar with sockets code, read the python sockets module documentation. Basically, we create a new socket and set it to listen for incoming connections on port 8123 (any highish number will do). Then, we loop forever (while True). Inside the loop, we want to wait for something to happen on our new socket. We create an InputBlocker for the socket object. A Blocker waits until something happens (in this case, until the socket needs our attention).
We then yield the Blocker object back to the tasks module. This suspends our function mid-execution. The tasks module then allows the rest of the program to continue running as normal. When a client attempts to connect to our server, blocker is triggered. The tasks module then resumes the suspended function, which handles the connection request (logging the event and then, rather rudely, closing the new client's socket).

You can test the new server using telnet:

$ telnet localhost 8123
Trying 127.0.0.1...
Connected to localhost (127.0.0.1).
Escape character is '^]'.
Connection closed by foreign host.

Each time you do this, a message should be logged in the window.

Handling the clients

Rather than closing the new client's socket immediately, we will create a new Task for each one. Replace the close line with this:

tasks.Task(client_task(client_socket, client_addr))

Again, client_task is a generator function (a function containing a yield statement):

def client_task(client_socket, client_addr):
        while True:
                blocker = tasks.InputBlocker(client_socket)
                yield blocker
                data = client_socket.recv(1000)
                if data:
                        log.append(data)
                else:
                        log.append('Lost connection from %s\n' % str(client_addr))
                        client_socket.close()
                        break

This is similar to before. We create a new InputBlocker for the client's socket and yield it, causing the task to be suspended until the client sends us some data. Then, we read whatever we got (up to 1000 bytes at a time) and add it to the log. Getting nothing back from the recv is the kernel's way of telling us the client closed the connection. In that case, we close our end too and finish the task, logging the event.
You should now find that you can have any number of simultaneous telnet connections to the server without problems.

Points to note

  • If we'd done this using threads, we would have had to be very careful about locking, otherwise if two clients sent us data at the same time (or if we were handing a redraw event from the X server while one client sent us data) we would likely crash. Here, only one piece of python code can run at a time. The code has complete control of the program until it uses yield.
  • There wasn't too much state to store here, but if we wanted to have a dialogue with each client (eg, asking for a user name and password) then a callback-based solution would have quickly become complicated.
  • Each Blocker can only be triggered once. This is why we create a new InputBlocker each time round the loop.
    Should the blocker be given a name in the example, then? The name doesn't enable anything useful to be done, and it could be a slight attractive nuisance, in that the blocker is still around for another attempt at triggering. (Naming could be useful in more advanced cases, but this example need not be bound by those.)

    Correct: in this case, the name isn't needed (you could yield InputBlocker(stream) directly). However, you will often want to use the objects after the yield to find out which one was triggered (when yielding multiple blockers) or to extract some information about the event.

  • We could have used a single task, and had it yield multiple blockers at once. However, using several tasks is easier.

For more information, see the tasks API documentation.

Glade

  1. Create the application directory as normal.
  2. Save the glade file as interface.glade inside it.
  3. Call rox.toplevel_ref() and _unref() manually.

A sample starting file might look like this:

#!/usr/bin/env python
import findrox; findrox.version(1, 9, 9)
import rox, os
import gtk.glade
 
gladefile = os.path.join(rox.app_dir, 'interface.glade')
wTree = gtk.glade.XML(gladefile, 'window1')
 
wTree.get_widget('window1').connect('destroy', rox.toplevel_unref)
 
rox.toplevel_ref()
rox.mainloop()

Perhaps someone who uses glade could extend this tutorial?

BASIC to Python

This is a quick primer, mainly for ex-BBC BASIC programmers moving to Python, although others may find it useful. Python is a clean, simple and elegant language which provides fairly direct equivalents to most BASIC statements, but for more advanced users also goes far beyond that.

Python makes an excellent language for writing ROX applications, and most of the ROX applications are written in it. However, for this primer we only consider general programming constructs without any GUI stuff. See the other tutorials for that. You'll also want to read the official python tutorial after this.

Getting started

Here's the familiar hello world program in BASIC and in Python:

PRINT "Hello World"

becomes

print "Hello World"

  • Python keywords are lowercase.

a = 4.5
b% = 3
c$ = "Hello World"

becomes

a = 4.5
b = 3
c = "Hello World"

  • Any python variable can hold a value of any type. There are no special characters in the name indicating type.

d = None

  • There is a special value, None, different from zero or false or the empty string. This is useful in many situations. Consider, for example, a function which gets a number from the user and returns it. How should it indicate that the user has finished the list of numbers? Traditionally, a BASIC programmer might return zero or -1 to indicate this, but using None will prevent confusion if the user really does enter the reserved number.

number = input("Enter a number:")

  • INPUT is a function, not a statement, in python.

Note: use raw_input() to get the exact string the user entered. The input() function tries to evaluate whatever you enter as an expression (eg, entering "1+1" will return 2).

Interactive mode

Like BASIC, python can be run interactively (type 'python' at the command prompt). Unlike BASIC, you don't need to write PRINT to evaluate things; in interactive mode, the result of every expression is printed automatically. >>> indicates the python prompt.

>>> a = 4
>>> a
4
 
>>> a + 1
5

WHILE...ENDWHILE

You can also enter multi-line statements, such as loops, at the prompt. The '...' prompt indicates that it's waiting for more input. Enter a blank line to complete the statement.

>>> x = 1
>>> while x &lt; 10:
...     print x
...     x = x + 1
...
1
2
3
[etc]

  • BASIC programmers normally indent their code to show loops clearly. However, BASIC itself ignores the indentation and uses the ENDWHILE marker to detect the end of the loop. Python uses the indentation directly, so no end markers are needed.

Python has a 'break' statement that can be used to exit from a loop at any point:

10 Finished% = FALSE
20 WHILE NOT Finished%
30   REM Stuff...
40   IF FNerror THEN Finished% = TRUE:ENDWHILE:GOTO 70
50   REM More stuff...
60 ENDWHILE
70 PRINT "Done"

becomes

finished = False
while not finished:
        # Stuff...
        if error(): break
        # More stuff...
print "Done"

  • Python uses # instead of REM for comments
  • A function with no arguments has empty brackets ()
  • Python doesn't have GOTO; more flexible programming structures make this unnecessary

REPEAT...UNTIL

Python doesn't have REPEAT loops. These can normally be done directly as while loops, but if not, use break for the end
condition. 'while True' is an infinite loop, as in BASIC.

REPEAT
  REM Stuff
UNTIL FNfinished

becomes

while True:
        # Stuff
        if finished(): break

IF...THEN...ELSE...ENDIF

If statements are very similar. The use of indentation means that there is no difference between single line and multiline statements. Note the `:'; this starts all nested blocks. The 'else' must go at the start of a new line (this prevents any confusion as to which IF the ELSE goes with, as is possible in BASIC).

if x > 0 and x &lt; 10: print "OK"
else: print "Out of range!"

Like BASIC, python considers 0 to be false, and any other integer to be true. Unlike BASIC, python also allows non-integers:

>>> if "hello": print "yes"
...
yes
 
>>> if None: print "yes"
...
  • An empty string is false, a non-empty one true.
  • None is also considered to be false.

DEF FN

>>> def double(x): return 2 * x
...
>>> double(4)
8

  • There is no need for the FN prefix.

DEF PROC

>>> def hello():
...     print "Hello World"
...
>>> hello()
Hello World

  • There is no need for the PROC prefix.
  • There is no difference in python between a function and a procedure. A procedure is just a function that doesn't return anything.

>>> a = hello()
Hello World
>>> print a
None

  • To be more precise, a proceduce is a function that returns the special value `None'.
  • You need the 'print' statement to print None; the interpreter skips it to save space.

DIM ()

Python uses lists rather than arrays. They are rather more flexible (they can change size, for example).

>>> a = ["red", "green", "blue"]
>>> a
["red", "green", "blue"]
 
>>> a[0]
"red"
>>> a[1]
"green"
>>> a[2]
"blue"
>>> a[1] = "yellow"
>>> a[1]
"yellow"
  • You can use them much like BASIC arrays.

>>> a[1] = 4
>>> a
["red", 4, "blue"]

  • Just as you can store any type in any variable, you can mix types in lists.

>>> a[-1]
"blue"
>>> a[-2]
4

  • Negative indices count backwards from the end. This is very useful!

>>> a[1:3]
[4, "blue"]
>>> a[1:]
[4, "blue"]
>>> a[:1]
["red"]
>>> a[:-1]
["red", 4]

  • You can specify a start (inclusive) and end (exclusive) index to create a 'slice' of the original list. If you leave off one limit, it goes from the beginning or end.
  • [:-1] means 'all but the last element', while [1:] means 'all but the first'

LEFT$, RIGHT$, MID$

Python has none of these. Instead, you can treat a string as a list of characters and use the indexing and slicing notation as for lists:

>>> b = "Hello World"
>>> b[0]
'H'
>>> b[1]
'e'
>>> b[-1]
'd'
>>> b[-2]
'l'
>>> b[1:]
'ello World'
>>> b[:-1]
'Hello Worl'
 
>>> b[2:4]
'll'
  • Python is neat!

FOR...NEXT

Python's for loops are completly different to BASIC's. Instead of providing start, end and step values, you provide a list to loop over.

>>> for x in ["red", "green", "blue"]:
...     print "The next colour is", x
...
The next colour is red
The next colour is green
The next colour is blue

You can get the BASIC behaviour by using the range() function to create a list of numbers to loop over, but you'll find you almost never need to use this.

a$ = "Hello,Bye,Fred"
count% = 0
FOR c% = 1 TO LEN(a$)
  IF MID$(a$, c%, 1) = "," THEN count% += 1
NEXT c%
PRINT "String contains "; count%; " commas"

becomes

a = "Hello,Bye,Fred"
count = 0
for c in a:
  if c == ",": count += 1
print "String contains", count, "commas"

  • No need to loop over indices and then extract the element at that index. Just loop over the list instead.
  • Use '==' for comparisons instead of '='. Python won't let you use '=' in an IF statement, so no risk of C-style errors.
  • += notation is the same

In fact, python provides a better way to count, as we shall see later.

ON ERROR LOCAL...RESTORE ERROR

Python error handling is much simpler, and doesn't require you to create a new function for each error trapping point.

 10 INPUT "Enter a number:", number%
 20 PROCrecip(number%)
 30 PRINT "Done!"
 40 END
 50 
 60 DEF PROCrecip(number%)
 70  ON ERROR LOCAL RESTORE ERROR: PRINT "Can't do that, sorry!": ENDPROC
 80  answer = 1 / number%
 90  PRINT "Result is", answer
100 ENDPROC

becomes:

number = input("Enter a number:")
try:
        answer = 1 / number
        print "Result is", answer
except:
        print "Can't do that, sorry!"
print "Done"
  • Any error in a 'try' block jumps to the 'except' clause.

try:
        answer = 1 / number
except DivisionByZero:
        print "Can't devide by zero!"
except:
        print "Something wierd happened!"
  • You can provide multiple except clauses for different types of
    error.

If you don't provide a default handler ('except:') then the error is passed up to the enclosing 'try' block (possibly in a calling function) until it is either handled or there are no more try blocks. In that case, python prints out the error, giving not only the line number of the problem, but showing the call in each function leading to it.

In fact, you should almost never use except on its own since unexpected errors should be reported with all their details. Only try to catch errors you want to handle specially (such as division by zero above).

Objects

Time to come clean... python is an object oriented language. This is a good thing, but requires a little adjustment to appreciate fully. Now, consider variables in BASIC. We think of a variable as a box with a label (the variable's name) and a value inside it. In an OO (Object Oriented) language, the values exist outside of any boxes on their own. Variables merely 'point to' values. The difference is, two variables can point to the same value. Consider:

>>> a = [1,2,3]
 
>>> b = [1,2,3]
>>> c = b

The final result can be visualised like this:

a ----> [1,2,3]
b ----> [1,2,3] &lt;---- c

That is, we have created two lists. 'a' points to the first, while 'b' and 'c' point to the second. The '==' test in python checks if two arrays contain equal values, while the 'is' test checks if 'two' values are actually one:

>>> a == b
True
>>> b == c
True
>>> a is b
False
>>> b is c
True

The difference is important when you change something:

>>> b[1] = 42
>>> a
[1,2,3]
 
>>> b
[1,42,3]
>>> c
[1,42,3]

On the other hand, if you assign something to 'b' (rather than modifying the thing 'b' points to), you make 'b' point at that instead:

>>> b = [4,5,6]
a ----> [1,2,3]
c ----> [1,42,3]
b ----> [4,5,6]

You might be a bit worried about this:

>>> a = 1
>>> b = a
>>> a += 1
>>> b
[ what goes here? ]

Don't worry. After assigning to 'b', 'a' and 'b' do indeed point at the same object (the number 1):

a ----> 1 &lt;---- b

However, incrementing 'a' has the effect of creating a new number, and making 'a' point at that:

a ----> 2
b ----> 1

You cannot change the number which 'a' points to in-place (numbers are thus said to be 'immutable'). Strings are also immutable. This means you can pass lists (arrays) to functions very quickly (because you're just making another pointer to the same array). If you want to copy a list, use the slicing notation to copy the whole thing:

>>> a = [1,2,3]
>>> b = a[:]
a ----> [1,2,3]
b ----> [1,2,3]

MAKE SURE YOU UNDERSTOOD THE ABOVE!

Garbage collection

OK, so we can create lots of new objects. How do we get rid of them again? The answer is simple: when it's no longer possible to refer to an object, python will free it for you:

>>> a = [1, 2, 3]
>>> a = None
  • The first line creates three number objects and a list, and makes a point to the list:

a ----> [1, 2, 3]
  • The second line makes a point to the None object:
    [1, 2, 3]
    a ----> None
  • Since there's no way to get to the list or the objects it contains, they are all freed.
  • Objects and methods

    Because BASIC has only a few types, it tends to represent everything else with numbers. File handles are an example; window handles another. Python tends to create new types for everything.

    We have already seen a few types. You can use a type like a function, to create a new object of that type. Eg:

    >>> a = int("4")
    >>> a
    4
    >>> b = float(4)
    >>> b
    4.0
    >>> c = str(4)
    >>> c
    "4"
    >>> d = list()
    >>> d
    []

    BASIC has a global selection of functions, and typically prefixes the names with types to avoid clashes. Consider:

    account% = FNaccount_new()
    PROCaccount_credit(account%, 100)
    PRINT "Now has", FNaccount_check(account%)
     
    door% = FNdoor_new()
     
    PROCdoor_close(door%)
    PROCaccount_close(account%)

    Because python keeps track of types (and allows new ones to be created), it can automatically work out which of several functions to use, by keeping the functions 'inside the type'. Such functions are called 'methods':

    account = Account()
    account.credit(100)
    print "Now has", account.check()
     
    door = Door()
     
    door.close()
    account.close()

    Here 'Door' and 'Account' are types (like 'int' or 'str'), while 'account' and 'door' are values of that type (like 4 or "Hello"). Notice how we use the type to create new objects ('instances') of that type.

    Python knows that 'door' is a Door, so 'door.close()' calls the close function for doors, whereas 'account.close()' calls the close function for accounts.

    Most objects in python have many methods. Here are a few for strings:

    >>> a = "Hello"
    >>> a.upper()
    "HELLO"
     
    >>> a.lower()
    "hello"
    >>> "   spaces    ".strip()
    "spaces"
    >>> a = "Hello,Bye,Fred"
    >>> print "String contains", a.count(","), "commas"
    String contains 2 commas

    • Strings are immutable, so the above methods return a new string, leaving the original alone. In mutable objects, such as lists, some methods will modify the object in place.

    While we're on the subject of strings, I should point out that you can use either single or double quotes, as desired:

    >>> a = "Hello"
    >>> a = 'Hello'
     
    >>> a = '"Hello", she said.'
    >>> a = '''In python, " and ' can be used to quote strings''' 

    Tripple quotes of either type can also be used, and these can span multiple lines. Strings can be any length (unlike in BASIC); you can read an entire file into a string if you want.

    CASE ... OF

    There is no CASE statement. You can use IF for a direct BASIC translation:

    if x == 1:
            print "One"
    elif x == 2:
            print "Two"
    elif x == 3:
            print "Three"
    else:
            print "Many"

    However, as with FOR loops, you'll generally find that python makes such things unnecessary. Consider:

    FOR creature_number% = 0 TO number_of_creatures% - 1
      CASE type_of%[creature_number%] OF
        WHEN 1: PROCmove_elf(creature_number%)
        WHEN 2: PROCmove_goblin(creature_number%)
        WHEN 3: PROCmove_dragon(creature_number%)
        ...
      ENDCASE
    NEXT creature_number%

    would likely be written in python as:

    for creature in creatures: creature.move()
    • Create a new type for each kind of creature. Python will call
      the correct 'move' function for each creature automatically!

    ERROR

    ERROR 0, "User error!"

    becomes

    raise Exception("User error!")

    Exception is a type, so this creates a new Exception object and raises it as an error. It can be caught in a try block, as shown above. When you know how to create your own types, you can use that to create different types of error (any object can be raised as an error, even a string, but there are conventions...).

    OPENIN, OPENOUT

    > stream% = OPENIN("MyFile")
    > PRINT stream%
    254

    becomes

    >>> stream = file("MyFile")
    >>> stream
    &lt;open file 'MyFile', mode 'r' at 0x8151290>

    • 'file' is a type, like 'str' or 'int'.
    • 'r' means 'for reading'
    • The number is a unique ID for the python object, so you can see
      at a glace whether it's the same as some other object.

    Note that 'stream' is not a string; that's just python's way of
    representing a 'file' object when you try to print it.
    You can use the file type's methods to read from it:

    >>> stream.readline()
    'The first line\n'
    >>> stream.readline()
    'The second line\n'
    >>> stream.readline()
    'The end\n'
    >>> stream.readline()
    ''

    Each line read includes the newline character, so you can tell the
    difference between a blank line ('\n') and the end of the file
    ('').
    More commonly, you'll use a file object as the sequence in a
    list. In this case, the loop iterates over the lines:

    my_data = file('MyFile')
    for line in my_data: print "Next line is", line
    my_data.close()

    Or, indeed:

    for line in file('MyFile'): print "Next line is", line
    • When the loop finishes, the file object is unreachable (since we never assigned it to a variable). Therefore, the garbage collector frees it, closing the file for us automatically.
    • Earlier, I said that for loops take a list to work on. In fact, a for loop takes an iterator (anything that can generate a sequence of values). Therefore, lists, files and even user-defined types can be used in for loops.

    For writing, pass 'w' to the constructor (the type used to create a new object):

    stream% = OPENOUT("Output")
    PRINT#stream%, "Hello World"
    CLOSE#stream% 

    becomes

    stream = file('Output', 'w')
    print >>stream, "Hello World"
    stream.close()

    Note that print's syntax is slightly unusual for python, as it's a statement rather than a function. I've shown this system as it's the most BASIC-like. You can also use the write method of the stream object if you prefer:

    stream.write("Hello World")

    LIBRARY

    >>> import math
    >>> math.cos(0)
    1.0
    • import loads routines from another file (called a 'module')
    • You access a module's functions using the same notation as an object's methods. This avoids name conflicts.

    For example, most math functions exist in both normal and 'complex' forms:

    >>> import cmath, math
    >>> math.cos(0)
    1.0
    >>> cmath.cos(0)
    (1-0j)

    You can also pull in some names directly:

    >>> from math import cos, sin
    >>> cos(0)
    1.0
     
    >>> sin(0)
    0.0

    You can use import to split your own programs into several files. Name the files with a '.py' extension, and use import without the extension to load the file.

    HELP

    Use the help function to find out about anything in python. You can get help on functions, types and modules!

    >>> import math
    >>> help(math)
    >>> help(math.cos)
    >>> help(str)

    Notice how we are passing apparently abstract ideas as function arguments! In fact, all these things are real python values, and can be assigned just like anything else:

    >>> import math
    >>> print math.cos(0)
    1.0
    >>> print math.cos
    <built-in function cos>
    >>> print math
    <module 'math' from '/usr/lib/python2.2/lib-dynload/math.so'>
    >>> a = math.cos
     
    >>> a(0)
    1.0

    AND, OR, EOR, ^

    Most python operators are the same as in BASIC. The bit-wise operators are different, however. EOR is written ^ in python, AND as & and OR as | (and ^ becomes **):

    > PRINT 6 AND 3
    2
    > PRINT 6 OR 3
    7
    > PRINT 6 EOR 3
    5
    > PRINT 6 ^ 3
    216

    becomes

    >>> 6 & 3
    2
    >>> 6 | 3
    7
    >>> 6 ^ 3
    5
    >>> 6 ** 3
    216

    BASIC also uses the bitwise operators AND and OR for boolean
    operations. Use the normal python 'and' and 'or' operators for
    this. These are 'short-circuit' operators; they only evaluate as
    far as they need to to get the result:

    >>> 0 and 1/0
    0

    There is no error, because python only evaluates as far as the 0
    before realising the result is going to be false.

    LOCAL

    Python doesn't have LOCAL; any variable assigned to in a function
    is automatically considered to be a local variable. Instead, the
    keyword 'global' must be used to indicate that a variable is NOT
    local:

    >>> def inc_a():
    ...     global a
    ...     a += 1
    ...
    >>> a = 1
    >>> inc_a()
    >>> print a
    2

    Also, the scoping rules are different. This isn't much of a
    problem, because python does what you'd expect, and BASIC doesn't.
    BASIC fakes local variables by storing the current global value on
    the stack and then restoring it when the function exits. Python
    does it properly. Consider:

     10 a% = 1
     20 PROCfoo
     30 END
     40
     50 DEF PROCfoo
     60 LOCAL a%
     70 a% = 2
     80 PROCbar
     90 ENDPROC
    100
    110 DEF PROCbar
    120 PRINT a%
    130 ENDPROC

    This BASIC program prints 2, perhaps rather surprisingly.

    def foo():
            a = 2   # Local variable
            bar()
     
    def bar(): print a
     
    a = 1
    foo()

    The python version prints 1, because the 'a' assigned in foo() is
    only accessible from within foo().
    To be picky, when you use a variable without assigning to it,
    python uses the variable assigned to in the closest enclosing
    lexical scope. Which only matters if you're doing un-BASIC things,
    like defining functions inside other functions. And it still does
    what you'd expect:

    def foo():
            a = 2   # Local variable
            def bar():
                    print a
            bar()
    a = 1
    foo()

    This prints 2, as you probably guessed.

    Future reading

    That's enough for now. Now play for a bit, and then read the
    offical python
    tutorial
    !
    A few other interesting bits to look out for in python:

    • The % operator can be used with a string to insert formatted
      values. Eg:
      >>> "Pi is %.3f (to three decimal places)" % math.pi
      'Pi is 3.142 (to three decimal places)'
    • The 'class' keyword lets you create your own types. Vital to
      take advantage of python properly!
      >>> class Point:
      ...     def __init__(self, x, y):
      ...             self.x = x
      ...             self.y = y
      ... 
      >>> p1 = Point(3, 4)
      >>> p2 = Point(7, 8)
      >>> p1.x
      3
      >>> p2.y
      8
    • There is a very useful `dict' type. It holds a set of elements,
      each with a `key' and a `value' and can look up a value from a key
      in constant time (no matter how big the dictionary gets). Very
      useful.
    • The 'assert' statement checks that something is true, and
      raises an exception if not. Sprinkle generously around your code to
      check for programming errors.
    • Everything is an object! Functions, modules, classes can all be
      assigned to variables just like other values. You can pass a
      function to another function, or get one back. Consider this:
      >>> def double(x): return 2 * x
      ...
      >>> map(double, [1,2,3])
      [2,4,6]

    Desktop Notifications (using libnotify)

    Desktop Notifications is a system for consolidating an API, UI and mechanism to allow applications, applets, services, etc. to notify the user when something interesting happens.

    The software for this is currently hosted under the Galago project, which is listed on freedesktop.org.

    This software consists of a notification-daemon, a client API implemented by the libnotify library, the DBus system for tying things together, and finally various language bindings (so far only python and of course C)

    The simplest way to add notifications to your app is to call the notify-send utility. But this is quite limited, and so not very interesting.

    To use libnotify in a python application do the following:

    try:
        import pynotify
        if pynotify.init("My Application Name"):
            n = pynotify.Notification("Title", "message")
            n.show()
        else:
            print "there was a problem initializing the pynotify module"
    except:
        print "you don't seem to have pynotify installed"

    You can set the urgency level to one of three values using the following:

            n.set_urgency(pynotify.URGENCY_LOW)
            n.set_urgency(pynotify.URGENCY_NORMAL)
            n.set_urgency(pynotify.URGENCY_CRITICAL)

    One option to the Notification() call is to add an icon. This can use one of three methods:
    1. a URI specifying the icon file name (e.g. file://path/to/my-icon.png)
    2. a 'stock' icon name. One that would succeed in a call to gtk_icontheme_lookup() (e.g. 'stock-delete') Note: these are not necessarily normal GTK stock icons - any theme icon will work.
    3. a pixbuf

    For the first two methods, just specify the icon name or URI as the 3rd parameter to the Notification() call.

            n = pynotify.Notification("Title", "message", "icon-name")

    For the pixbuf method use: (where 'icon' is a pixbuf)

            n.set_icon_from_pixbuf(icon)

    To set the timeout value for the displayed message:

            n.set_timeout(seconds)

    To position the message (e.g. to associate it with a toolbar applet)

            n.attach_to_widget(widget) # 'self' often works
          # or position it explicitly
          #  n.set_hint("x", x-coordinate)
          #  n.set_hint("y", y-coordinate)

    You can close the notification before it has timed out using

            n.close()

    There's lots more you can do, such as putting buttons on the messages and getting callbacks when they are clicked, but that is beyond this tutorial. See the examples that come with the pynotify package to learn more.

    Reinteract-ROX: a re-packaging tutorial

    Reinteract thumbnail

    Earlier this month, Owen Taylor announced Reinteract ("a system for interactive experimentation with python"). I've been wanting something like this for a while now.

    In this article, we're going to turn it into a ROX application. In the past, this has meant taking a copy of a program and renaming and changing things to fit the ROX application structure. The trouble is, the ROX version and the original upstream version diverge over time. So, we're going to look at how improvements in tooling can make things easier for us.

    The first bit of good news is that Reinteract uses a GIT, an excellent decentralised version control system (which we also use for ROX-Filer). GIT allows us to clone Owen's repository, make any changes we want, and resynchronise at any time with a single command. This should be a big improvement over the situation a few years ago when people were using systems like CVS and Subversion. It's hard work synchronising two subversion repositories.

    So, we'll start by cloning the original repository:

    $ git clone git://git.fishsoup.net/reinteract
    

    Metadata

    We could start by moving things around to create a ROX application directory, but that will cause conflicts when we try to merge in new versions. It's also very unlikely that a desktop-neutral project is going to apply such changes, forcing us to maintain them indefinitely. Instead, we're just going to add some metadata saying where things are. The 0publish command can create a template for you and validate what you enter, or you can just type it in manually. Here's what I added, in a file called Reinteract.xml:
    <?xml version="1.0" ?>
    <interface xmlns="http://zero-install.sourceforge.net/2004/injector/interface">
      <name>Reinteract-ROX</name>
      <summary>a system for interactive experimentation with python</summary>
      <description>
    Reinteract is a system for interactive experimentation with python.
    You enter Python code and expressions and immediately see the results.
    What distinguishes Reinteract from a shell (such as IPython or the builtin
    interactive mode) is that you can go back and edit expressions you entered
    earlier and the results will flow through the part of the worksheet after
    the changed portion.</description>
    
      <homepage>http://www.reinteract.org/</homepage>
    
      <icon href="http://www.reinteract.org/images/reinteract-logo.png" type="image/png"/>
    
      <feed-for interface="http://rox.sourceforge.net/2007/interfaces/Reinteract.xml"/>
    
      <group license="OSI Approved :: GNU General Public License (GPL)" main="bin/uninst.py">
        <implementation id="." version="0.1-pre"/>
      </group>
    </interface>
    

    In writing this file, we're just restating the information on the Reinteract web-site: that this is a pre-release version (0.1-pre), that you run it using the command bin/uninst.py, etc. But, crucially, we're doing it using a well-defined structure that we can process automatically using our existing tools. The only ROX-specific changes were to rename the program to "Reinteract-ROX", so that people don't get confused about which version they're using, and to specify where I will publish it (in the feed-for element; essentially this is the web-page I will put up to describe my modified version).

    Commit the changes to your local GIT repository (no network access is required for this):

    $ git add Reinteract.xml
    $ git commit -a
    

    An easy way to test that the file is roughly right is to try running it with 0launch (which is probably in a package called "zeroinstall-injector" in your distribution). This command should run it as if you'd run the bin/uninst.py script directly:

    $ 0launch ./Reinteract.xml
    

    Finally, you should publish your GIT repository somewhere. I registered one at http://repo.or.cz/w/reinteract/rox.git, and added that to my local repository under the name "main":

    $ git remote add main git+ssh://repo.or.cz/srv/git/reinteract/rox.git
    

    Adding ROX-style saving

    Next, we make our changes. I added drag-and-drop saving, which was rather straight-forward (see the changes on the rox branch of my repository if you're interested):

    Reinteract ROX

    Note that my repository has two branches: "master" contains modifications that upstream may want to pull back, while "rox" contains the ROX-specific modifications.

    Creating a release

    Reinteract hasn't had an official release yet, so we'll make one now. Making releases is tedious and involves many steps if you want to do it properly (such as tagging the version in GIT and making sure that the archive you release can be generated exactly from that version). I like to create a script to make releases; that way I always follow the same process for each version. Luckily, we have a (new) tool for this too. Create a new directory (not under version control) for the release files and set it up, like this:

    $ mkdir releases
    $ cd releases
    $ 0launch http://0install.net/2007/interfaces/0release.xml ../Reinteract.xml
    

    Here, we use the 0release tool to create a release script. The argument is the path to the XML metadata file we created above. 0release uses this information automate some parts of the release. Have a look in the generated make-release script, which contains settings that are specific to you (such as the command to upload the release archive to your download server).

    You can leave all of the settings blank except for the first one, which says where you're going to host the files. For myself, I like to automate as much as possible, so I used the following settings for sourceforge ("update-rox-site" is a custom command I use to push changes to the live site):

    ARCHIVE_DIR_PUBLIC_URL=http://osdn.dl.sourceforge.net/sourceforge/rox
    MASTER_FEED_FILE="$HOME/rox/htdocs/2007/interfaces/Reinteract.xml"
    ARCHIVE_UPLOAD_COMMAND='ftp upload.sf.net'
    MASTER_FEED_UPLOAD_COMMAND='svn ci -m "New release" "$@" && update-rox-site'
    PUBLIC_SCM_REPOSITORY=main
    

    Finally, run the script to create the release:

    $ ./make-release
    Releasing Reinteract-ROX
    Snapshot version is 0.1-pre
    Version number for new release [0.1]:
    

    Notice how 0release uses the metadata file we created above to suggest the next version number for us (accept the default offered).

    Next, 0release prepares a tarball for us. It will also generate a changelog from the GIT history automatically, as a starting point for a release announcement:

    Wrote changelog from start to here as changelog-0.1
    
    Candidate release archive: reinteract-rox-0.1.tar.bz2
    (extracted to /home/talex/src/reinteract/releases/reinteract-rox-0.1 for inspection)
    
    Please check candidate and select an action:
    P) Publish candidate (accept)
    F) Fail candidate (untag)
    (you can also hit CTRL-C and resume this script when done)
    Publish/Fail:
    

    You can test that the archive works if you like, and then enter P<return> to publish it. This:

    • Uploads the archive to sourceforge.net (which, annoyingly, requires some further interaction using your web-browser).
    • Tags the version it released in GIT (as v0.1) and pushes the changes to your public GIT repository.
    • Uploads a signed XML file (which is similar to the metadata file above, except that it gives the final download location and digest of the release archive, rather than a local directory). In my case, this is done by committing the XML to subversion and then running a custom script to check it out on the server, but you may use some other system, of course. This XML file is called a feed; like an RSS feed, people can subscribe to it to be notified of new releases.

    Using the release

    So, what have we achieved? So far, we've just performed a rather normal software release. In particular, the archive doesn't contain a ROX application directory.

    But we can turn it into one automatically! Like so:

    $ 0launch http://rox.sourceforge.net/2006/interfaces/Zero2Bundle.xml \
          http://rox.sourceforge.net/2007/interfaces/Reinteract.xml
    

    The Zero2Bundle program takes an XML file produced by 0release and creates a ROX application directory from it. After confirming that you trust the GPG key that signed the XML feed, a ROX savebox appears letting you save your new ROX application. This has the usual ROX application layout, with an AppRun file, a .DirIcon, etc. You can run it, add it to your panel, drag files to the icon to load them just like a normal ROX program.

    Reinteract ROX app dir

    Because there's nothing ROX-specific about all this, it's quite possible that upstream authors will provide suitable XML files themselves: instant ROX applications for free! And, indeed, some of the programs in the ROX-All package were produced like this, by non-ROX developers. All we did was add the feed's URL to the script that generates the ROX-All release!

    Of course, standard ROX application directories are a bit limiting. We don't get notified of new versions for one thing, and if there are multiple users of the computer they won't automatically share the code. No problem! The XML feed file can be used by other programs too. In fact, ROX users are more likely to drag the feed to AddApp to create a launcher.

    The again, the feed file is desktop neutral. There are programs for GNOME, KDE and Xfce which will add the program to those desktops. Maybe people will use the feed to create Debs or RPMs too, although I don't know of any tools to do that yet.

    Getting updates

    The advantage of this setup is the ease of releasing updates. GIT can track both my local changes and upstream ones. The history displayed below shows that Owen has made a change since I branched, and also that I thought of something to add to both the master and ROX branches (I added it to master and then merged it to ROX):

    Reinteract history

    To pull any new features in from upstream:

    $ git-pull origin
    

    Then run make-release to upload it as version 0.2.