Developers

Other resources:

Using GIT

Many ROX applications are developed using the GIT version control system. This is how we keep track of each change to the code (who made it, when and why), alternative lines of development (for trying out new features) and which versions correspond to official releases.

For example, you can click here to view ROX-Filer's repository in your browser, which will show you what we're up to at the moment:

To get the current developer version of a program, you'll need to install GIT itself. e.g. use something like this (on Ubuntu):

$ apt-get install git-core gitk

(gitk is a largish visualisation tool and is not strictly necessary, but highly recommended).

Start by cloning my repository, which will create a new directory called "rox-filer":

$ git clone git://repo.or.cz/rox-filer.git
$ cd rox-filer

The directory contains the latest version (as with a subversion checkout), ready to compile. There's also a hidden .git directory with all the git-related bits (just in the top-level directory, not one in each sub-directory like with .svn).

To see the log:

$ git-log

This doesn't require network access; your checkout has the whole history.

To view a visualisation of the history:

$ gitk --all

(--all shows the history of all branches and tags, not just the main trunk)

Here's an example:

gitk screenshot

Versions nearer the top are more recent. This is showing us that since version 2.8 (near the bottom) a "GIO" development branch split off to test using the GIO library for file access. Meanwhile, the main trunk of development (master) continued (splitting and rejoining as different developers worked in parallel). Finally, a "fam" branch holds an unreviewed patch which might get accepted onto "master".

You can click on any commit message to see the diff and/or the state of any file in that version. It has good (and fast!) searching too.

Fetching updates

To download the latest updates into your copy:

$ git pull

You can also full from other places. If someone posts to the mailing list, they will tell you where to pull from to try the feature out.

Making patches

See Patches and feature requests for details.

Further reading

Git has very good documentation, e.g.:

http://www.kernel.org/pub/software/scm/git/docs/tutorial.html

Patches and feature requests

Note: feature requests should be added to the feature request tracker for discussion! When requesting features, please check the list archives to ensure that it hasn't been proposed before. You can also propose features on the mailing lists.

If you've changed the code in some way and you want your changes in the main distribution then you should post your changes to the mailing list.
If the project uses GIT (see Using GIT for instructions on getting hold of the latest version of a program) then you should commit your change(s) first to your local repository, like this:

After making your changes, do:

git commit -a

Enter a log message. The first line should be a short summary (like the subject of an email). Then leave a blank line, then write a longer description. This only commits to your local machine, so you don't need any special permission for this.

To view your patch after committing:

git show

If you realised you made a mistake, correct it and then do:

git commit -a --amend

Then, you have two options:

  1. Push the changes to your public repository, then post to the mailing list telling us where your public repository is and which branch to pull from. If you don't have your own repository, you can click on 'fork' to create one (e.g. in the navigation bar at the top of http://repo.or.cz/w/rox-filer.git).
  2. Create a patch which you can send to the mailing list directly like this:
      $ git format-patch origin/master
    

If the project uses subversion, do svn diff to create a patch and send it to the list.

Hints on getting patches accepted:

Translators: see Internationalisation for instructions on creating a language translation.

Base platform

The ROX base platform is the minimum library requirements for running the base ROX applications. Third-party applications may require newer libraries, of course, but it should be possible to install a working desktop with these versions.
Currently, the base plaform is:

GIT for translators

This is a guide for people wanting to translate ROX-Filer (or other ROX projects using GIT) into other languages.

Getting the program to be translated

You'll need a copy of GIT before you start. e.g.

$ apt-get install git-core

Start by getting a copy of the ROX-Filer repository:

$ git clone git://repo.or.cz/rox-filer.git

This creates a directory called "rox-filer":

$ cd rox-filer

You can now build and run the filer as usual:

$ ./ROX-Filer/AppRun

Creating or updating a translation

To make a translation, edit the ROX-Filer/src/po/*.po file; see Internationalisation for details.

To see what you changed:

$ git diff

To save your changes (just to your local computer):

$ git commit -a

Enter a log message, e.g. "Updated Chinese translation". You don't need to add your name to the message, because it's recorded automatically.

To see your new commit:

$ git log

Check that the 'Author' field is correct!

Publishing your updates

To create a patch to email:

$ git format-patch origin/master
0001-Updated-Chinese-translation.patch

You can send this file to the mailing list.

To publish your changes directly, you'll need write access to a repository. You can either create your own, or ask for access to the main one. To use ours, go here:

http://repo.or.cz/

Register yourself as a user and then write to the ROX mailing list to tell us your username. You'll be added as a user of the rox-filer/translations project.

Tell GIT about the translations project (replace user with the name you registered):

$ git remote add translations git+ssh://user@repo.or.cz/srv/git/rox-filer/translations.git

You can then push your changes directly to this project, like this (replace chinese with your language):

$ git push -f translations master:refs/heads/chinese

(-f means "force". You only need it the first time, to create the new branch.)

Then post to the rox-devel mailing list telling us to pull from this branch (tell us the name you used).

Updating to a new version

To update to the latest version of the program, do a pull:

$ git pull

Release process

You can speed up the ROX release process by volunteering to be a release manager!

Making a release is fairly easy, but time-consuming. Often, developers don't have the time to make regular releases, so ordinary users can take the lead in managing a release, with the help of the developers.

Before the release

Becoming a release manager

  1. Announce your intention to be a release manager to the rox-devel mailing list, stating which program(s) you want to manage.
  2. Make a release...

Preparing for the release

  1. A few days before the release, announce the release date on the Mailing Lists.
  2. Developers commit any outstanding changes they want in the new release.
  3. Ensure the manual is up-to-date.
  4. Announce code freeze (except for bug fixes).
  5. People Using Subversion versions test the code.
  6. Translators will also check in their changes during this time. Some translators don't commit directly to CVS; they will send their patches for you to apply manually.

Creating the release archive

To actually create the archive that people will download:

  1. Check the program out from subversion (or svn update your copy if you already have it). E.g.:
    $ svn co https://svn.sourceforge.net/svnroot/rox/trunk/Archive
  2. Fetch the svn changelog and summarise the changes in the Help/Changes file.
    $ svn log https://svn.sourceforge.net/svnroot/rox/trunk/Archive

    Note that this lists all changes ever, so remember to stop when you've gone back as far as the previous release! You can also combine multiple commits into one entry (e.g. "Added new feature", "Oops - forgot to commit a file", "Fixed bug in new feature", etc can just be "Added new feature"). If you need more information for a message like "Updated", svn log -v will tell you which files were changed.

  3. Update the version number and date in AppInfo.xml, the injector feed XML file (e.g. Archive.xml), and the Help/Changes file.
  4. Commit your changes.
  5. Run the rox-release script from the directory where you want the release files to be created. The argument is the path to the injector feed in the SVN checkout of the program you are releasing:
    $ svn co https://svn.sourceforge.net/svnroot/rox/trunk/rox-release
    $ 0alias rox-release rox-release/ROX-Release.xml
    $ cd .../releases
    $ rox-release .../Archive/Archive.xml
    
  6. Diff against the previous release (xxdiff is good). Look out for missing files, or debugging statements that slipped in:
    $ xxdiff archive-1.1 archive-1.2
  7. Check the program runs. Try with a translation too:
    $ LANG=fr ./Program/AppRun
  8. Upload the tgz file somewhere and request sign-off from the program's maintainer.

Making the release

Uploading the archive

  1. The maintainer will send you a detached GPG signature for the archive.
  2. Create some release notes (this is basically just this version's section of the changelog, but sorted by function rather than by date).
  3. Go to Source Forge's file release system and:
    • Create a new release. New release name is just the version number part, not the whole program name.
    • Upload the archive and signature.
    • Set changelog, release notes, etc.
    • Set the file formats. The format for the signature is Other.
  4. Download archive and signature from file release system. Check signature matches:
    $ gpg program-x.y.z.tgz.sig
  5. Edit the program's web page (you can get to it from the software index page). Set the Current testing version to the new release's version number. The program will now automatically show up in the Testers wanted box on the news page.

Announcing the release

  1. Add the release to the Zero Install (AddApp) feed (if any), marked as testing.
  2. Click on the announce this release link at the bottom of the program's web page (it's next to the add new comment link) and enter a short paragraph about what changed. If you keep to plain text and write in the 3rd person, you can reuse this text for the freshmeat announcement later!
  3. Do the Send Email Notification on the file releases page.
  4. Send a release annoucement to the mailing lists (your new web announcement includes an automatically-generated message suitable for emailing to the list).
  5. Make a http://freshmeat.net release announcement.
  6. Update any other places that seem interesting (ROX-Filer announcements also go to the news:comp.sys.acorn.announce newsgroup and http://gnomefiles.org).

Marking as stable

  1. After a week or so with no major problems, update the page by moving the version from the testing field to the stable field.
  2. Update the Zero Install interface file in the same way.

Standards

There are various documents detailing how different bits of the desktop work:

Configuration system
Describes where to load and save user preferences and allows layers of preferences (such that user preferences override system-wide preferences and so on).
Application directories
A system for distributing applications in a form that can be run without the need to install anything. The application program remains completely self-contained.
httpFreedesktop.org
The X Desktop Group has links to various standards that are common to several desktops, and develops new ones. Lots of interesting work is going on here.
httpThe X Direct Save protocol
This protocol allows the user to save by dragging data from a program to a filer.
There is a test application here: httphttp://rox.sf.net/xds.html

Style Guide

Introduction

This page contains a somewhat random collection of hints about writing ROX applications. Some of them are general, some are specific to ROX. Of course, you don't have to follow these; they're just guides. But you should have a good reason before deviating from them.

Futher reading

Since ROX is very much about a particular user interface style, ROX applications don't follow all the normal rules.
But we do follow most of them, and they're worth knowing anyway so you know when you're breaking them on purpose,
and when you're just being stupid ;-)

Terminology

For consistancy, the ROX documentation tries to use the following words with the meanings given here:

Select
Cause something to become selected, such as an icon or a piece of text.
Choose
Choose an item from a menu.

Software installation and startup

Installation

ROX applications should be distributed as application directories. An application directory contains all the resources that a program needs (source code, binary, images, help, etc) in a form that can be used directly without the need for any installation.
They must be relocatable at run-time, so don't put any absolute paths into your programs (ie, don't assume the application lives in /usr/local/apps, etc). Don't assume the user installing the software has root access.
ROX applications should not normally be supplied with any kind of installer. This makes software installation simpler and easier for everyone, and completely automatic for Zero Install users.

There are a few cases where you might want to install shared resources. Some of these are examined here:

Icons for filetypes
It is the user's choice what icon to use to represent each type of file. You should only set the icon for a file type if there isn't one set up already, and this should be done when the program is run, not when it is installed.
When your program is run, if <Choices>/MIME-icons/type.png is missing (or a broken symlink) then you may symlink it to an icon inside your application.
Note that we support freedesktop.org's icon theme spec in the CVS version of the filer. In that case, install your icon as mime-application:foo into the default theme (hicolor). This will make it visible to other desktops, too (support for this may take a while to catch up).
Packagers for distributions may choose to set a system-wide icon when the package is installed; this must be coordinated over the whole distribution to avoid conflicts.
MIME-info rules (guessing a file's MIME type from its name)
Like icons, MIME-info rules should be installed at run-time. Drop an XML file with your program's name into the shared MIME package directory (see freedesktop.org) and
run update-mime-database.
MIME-types (what application handles each type of file)
ROX-Filer provides a very easy way for users to specify which application should handle a particular type of file. Do not set this yourself.
Services
Programs which should be run automatically on startup should also not set themselves up at install time. You may ask the user if it's OK to be run automatically when they run the program. This allows users to install software without worrying about opening security holes.

As you can see, all these rules mean that you should never have to preform any actions at install time.

Start-up

A ROX application is started by clicking on its application directory, which runs the file AppRun inside. Users will not normally be able to pass flags to your program (although they can give arguments by dragging files onto it).
When run, your program should give the user visual feedback that it is loading. Try to open the main window as soon as possible, even if it appears blank at first. This allows the user to move their eyes to the area where the content is going to appear. It also means they can reposition the window while the program is loading to get it where they want it.
Splash screens may be placed inside the main window -- do not use separate windows or set override-redirect because the user may want to continue working while the program loads.
A better solution than a splash screen is to simply load quickly in the first place! If your program takes a long time to load, see if you can defer some of the initialisation until later.
For example, ROX-Filer only builds the Options window when it is opened, not when the filer is loaded, and it will open a directory viewer before it has finished scanning the directory.

Loading and saving

ROX uses drag-and-drop for file operations. Users load files by dragging to your application directory. They import files by dragging into a document window. They save by dragging back to a filer window. See Edit for an example of this.
The traditional method of using a 'mini-filer' window is bad because:

  • The user must learn two interfaces (the application and the filer) which do the same thing -- navigate the filesystem.
  • Mini-filers either have too little functionality (eg, can't create a new directory, can't search for files) or have too much and are confusing.
  • It should be the user's choice which filer to use for all applications, not the application author's choice on a per-application basis.
  • Mini-filers invariably default to the wrong directory. In ROX, the user can have multiple filer windows open, drag to pinboard and panels and so on.
  • Drag-and-drop saving allows the user to save directly from one application into another.

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

When some data (not in a file) is dropped onto your application directory, ROX-Filer will ask you to load '-' and passes the data in on standard input. Make sure you support this!
Menu items names:

  • Save As... opens a savebox. Most programs only need to provide this. It is called Save As rather than Save to avoid confusion with Window's Save behaviour.
  • Quick Save saves directly using the current filename without opening a savebox. This is what many systems call Save, but to avoid confusing with
    RISC OS we use a different name.

See also:

Dialog boxes

Do not over-use dialog boxes. If a dialog box pops up, then it should usually be worth the user's while to read it.

Confirmation boxes that appear too frequently are useless; the confirming click becomes part of the action and the net effect is simply slowing the user down without gaining any safety. Whereever possible, operations should be undoable. This allows a fast interface without any confirmations, but also protects the user from all mistakes. Generally, therefore, it is only worth confirming an operation which would take a reasonable amount of work to recover from.

Do not use button labels like Yes and OK if there is more than one button. Instead, use a verb (such as Delete or Send). If the user knows what their options are (eg, whether the data is to be saved) then they should be able to work out which button to click without reading the text. This also prevents mistaking one dialog for another, eg:

  • Do you want to save? : Yes
  • Really quit without saving? : Yes

Using image icons in the buttons can also help here.

Buttons

The GNOME developers have decided on some guidelines for button ordering which seem pretty sensible. The basic idea is that the 'Yes, do it' button goes in the bottom right corner. This is not necessarily the safest choice. We have moved across to this system too.

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

Note: we're not following all of GNOME's advice on dialog boxes, so don't do anything stupid like this (note the two negatives!)...

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

Instead, just open the normal save box, but with an extra 'Discard' button. The user already knows how the save box works, so there shouldn't be any confusion.

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

Errors

When reporting errors, always say why the error occured. Sometimes it's difficult to know the root cause of a problem, but you must have had some reason to have shown the box in the first place... tell the user whatever you know. Don't do this:

GNOME error box

If you are using ROX-Lib, uncaught exceptions will be automatically displayed in a box like this:

ROX-Lib error box

You can catch specific errors and make them more user-friendly (the above screenshot, created by saving from Edit to root's home directory, isn't ideal). In general, I prefer to err on the side of providing too much information than too little (which path was read-only may be obvious in some cases, but completely non-obvious in others). I'd rather a user sees the stack-trace box with full details when they don't need it, than not get the information when they do.

The ROX-Lib error dialog also has a Details button which provides access to the stack trace, local variables and an interactive Python prompt for more advanced users. This is open source; encourage users to explore!

Never catch all errors and just assume what the problem was. MS Word does something like this:

try:
   doc.save()
except Exception:
   alert('Disk full. Try deleting some files.')

Users can delete a lot of important files before someone less trusting of error messages helps them out ;-)

Toolbars, menubars, etc

Screen space is precious. Don't waste it with clutter! You either reduce the area available for showing the user's data, or you cover other programs. Programs should use drag-and-drop to work together and small windows make this easier.
In general, you should start off with the most minimal interface possible (ie, just showing the data). Add extra user interface features only if they are needed. Do NOT start a new application by creating toolbars filled with non-functional buttons all around your window!
Toolbars:

Toolbars are generally OK if they're small. Don't put too many tools on the bar though -- searching for the tool you want may waste more time than just using the popup menu. Users are more likely to investigate five tools than thirty.

Menu-bar:

Try to avoid these. A menu bar is slower to access than a popup menu, and wastes screen space. Only use one if you have a very large main window and lots of menus.
The preferred method for giving commands in ROX applications is the popup menu. Popups have the following advantages over other means:

  • They appear instantly, right under the cursor. No time wasted moving the mouse.
  • They are arranged in a hierarchical structure. All the functions of the program are shown in one place.
  • GTK+ makes it very easy to bind keys to menu items (by pressing the key while pointing at it).
  • Popups don't waste any screen space when not in use.

All toolbar functions should also appear on the menu so that keys may be bound to them.

Status-bar, Preview panes, etc:

These are usually a bad idea, but it depends on the application. Put them in if they become necessary.

Wizards

A wizard takes the user through an operation step-by-step, because the operation is too difficult for the user to perform on their own. The need for a wizard usually indicates a problem with your user interface. It is often better to fix the problem than to write the wizard.
For example, consider the task of making a chart in a spreadsheet when the user clicks on the 'Chart' tool:
A wizard-based approach would bring up a long series of dialog boxes asking the user for the chart type (bar/line/pie/scatter), title, axis labels, grid lines and so on.
A non-wizard approach would display a chart with suitable defaults immediately and let the user change anything that was wrong using the normal editing tools.

The second approach has a number of advantages:

  • It's usually faster, because the user only changes things the program got wrong.
  • If the user is trying to make a sketch graph quickly, they don't have to mess around with things like the grid type which they don't care about.
  • The user feels comfortable making changes to the graph later. A wizard-user will often have to delete the chart and start again every time they wish to change something.

It's often better to tell the user how to do something than to do it for them. A good example is setting the program to handle a particular type of file when no default has been set. ROX-Filer tells the user to use the `Set Run Action...' menu item; they'll know how to change it again later. Explorer brings up a dialog for them; most Windows users have no idea how to change a file's run action a second time!

Scripting

If you want to add scripting facilities to your program, make sure you use an existing language. Python, PERL and scheme are all easy to embed in your applications and are sure to be faster, more powerful and less buggy than any language you write. There's also a good chance that users will already know the language.
Look into this early. It starts with a #include in the app's config files and before you know it you've been suckered into adding support for lambda expressions, nested modules and garbage collection!
Which language you choose is up to you, and depends on the application. However, many ROX applications are written in python and this is surely the best language for beginners (and possibly experts too ;-).
In fact, it's often a good idea to write the whole application in a high level language as you avoid the whole problem of compiling (many users don't have a compiler, don't have the header files, run on multiple architectures, have wierd compilers, etc). Usually only very small parts of your program will benefit from being written in C; most languages allow you to embed small C functions easily.

Applets

  • Please make sure the menu appears in the right place (the same distance from the edge of the screen as for other items on the panel).
    See ROX-Lib's applet class for an example implementation.
  • Make sure that your applet can expand and shrink as the panel size changes (use Options -> Panels -> Panel width to change it).

Tips

Tips

This section has a few more technical hints for application authors:

Portability

The ROX desktop works on many different systems, almost all of them some variant of Unix. However that are many small differences between operating systems, between distributions of an operating system and between versions of a distribution. Some of these you will need to know about when writing ROX applications.

The Bourne Shell and the Bourne Again Shell

One thing that makes Unix a powerful operating system is its shell, the program that lets you type in commands and execute them. The default shell, the Bourne Shell, is quite a powerful scripting language (although its user inteface features may be lacking). Some commands on a Unix like system are not compiled programs, but instead are Bourne shell scripts. Under ROX the AppRun files of wrapper applications and compiled C programs are normally Bourne shell scripts.
Although the Bourne shell (normally located at /bin/sh) is a good scripting language it is not so good as a user interface. What's more it is proprietry. Thus a free open source replacement is required for Linux and other free OSes. One such (and there are many others) is the Bourne Again shell (/bin/bash). Unlike some of the other shells the Bourne Again shell is fully compatible with the Bourne shell and adds many useful features. Under Linux /bin/sh is actually a symbolic link to /bin/bash. This causes problems that Linux users and programmers may be unaware of.

When acting as /bin/sh, the Bourne Again shell still supports all its extended features. Thus a programmer on Linux can write a Bourne shell script using Bourne Again features and not know it, until it is run on a system with a genuine Bourne shell such as the commercial Unix's.
Here are a few of the more common problems.
export of variables

This works in bash, but not sh

export VAR=val

in sh it must be

VAR=val export VAR

test

The -e argument to test to test for the existance of a file does not exist in sh, use -f. e.g. replace

if [ -e "$@" ] ; then

with

if [ -f "$@" ] ; then

Equality

Use only a single = symbol to test for equality, not ==

pygtk

The pygtk 2.0.0 module is fairly recent, some versions of distributions are still using the development series 1.99.x. Most of the diffences are minor, but one that has caused trouble is the creation of GtkLabel's. In pygtk 2.0.0 you can omit the string argument to get a blank label but in some of the 1.99.x releases you cannot. Simply replace

lbl=rox.g.Label()

with

lbl=rox.g.Label('')

and it should be fine.

which

The which program is used to locate executable programs. Except that under Solaris it works differently than under Linux. It is explicity tied to the C shell, not sh, and produces more output than you may expect. Best to avoid it.

killall

Under Linux killall kills all the processes with the program name given as arguments. Under Solaris killall kills all user processes prior to system shutdown and pkill kills processes by name. Best to avoid it.

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.

    Using Subversion

    If you're too impatient to wait for even developer releases, you can get the current version of the filer and other programs directly from subversion.

    This is the version that the developers are actually working on at the time.

    ROX-Filer has moved!

    ROX-Filer has moved from subversion to git, so see the git instructions instead. The other components are still in subversion for now.

    Fetching the latest version of a program

    You can view the repository contents online here.

    To checkout a module (e.g., Edit) using subversion, use this command:

    svn co https://rox.svn.sourceforge.net/svnroot/rox/trunk/Edit

    For more information on using subversion, see the SF.net instructions.

    Snapshots

    If you don't have a subversion client, you can download the daily snapshots instead. These are generated at around 0710 each day (UK time) from https://rox.svn.sourceforge.net/svnroot/rox/trunk/ for each application, except for ROX-Filer which is now generated from the primary git repostitory .

    Why are some files missing from my checked-out version of ROX-Filer?

    To get a complete system, you also need to create Manual.html from Manual.xml (by running make in the src/Docs directory) as well as recreate the various language translations. These files are not in subversion because they are generated from other files.

    However, the daily snapshots do have these files included.

    The Future

    Current and future work

    Trackers

    Better AppDir support

    Better core App Dir support

    Currently ROX-Filer uses the function rox_spawn() to start other programs. However it does not directly support App Dirs, each caller to rox_spawn() must do that itself and not all do. This means only partial support for our own format!
    We should re-write rox_spawn() to directly support App Dirs itself, removing that burden from its callers. The various libraries should also support it.

    Configuration

    This is a long-term plan for how to handle configuration in ROX.

    • Use the httpfreedesktop.org config system when it's ready.
      This stores settings in a similar way to ROX, but using XDG_CONFIG_DIRS in place of CHOICESPATH (default is
      ~/.config rather than Choices, but you can use anything). D-BUS is used to send notification of changes,
      and to send settings to remote applications.
    • Move options user interfaces out of ROX-Filer and ROX-Session and have them run small Python applications
      when the user chooses Options from the menu. The change notification means that changes to the settings will
      still be instant effect.
    • Move non-session-related options out of ROX-Session's Options box and into a number of separate applications
      (DisplaySettings, MouseSettings, etc). They're only in there at the moment because the XSettings system requires
      a single application to set all XSettings.

    Road map:

    1. Make ROX-Session start a D-BUS session on login (done)
    2. Migrate to storing choices using basedir spec (done)
    3. Help with fd.org config system.
    4. Get ROX-Lib to use new config system.
    5. Get ROX-Filer and ROX-Session to use ROX-Lib for options. The filer's options box should also be split (so that you only see pinboard options when opened from the pinboard menu, etc).

    Note: Just to be clear on this: the idea is not to centralise the user interface (regedit, etc), but only to rationalise the storage of the options (they already get saved outside the applications). Whenever this comes up, someone suggests having a 'control centre' application where all the options for all applications go. This might be a fun programming exercise, but it's terrible UI, rather akin to moving all the light switches in your house to a single room (neat, but annoying).

    So, if you're looking to configure ROX-Filer, you'll still get to the options through the ROX-Filer application directory (the same object you use to run it, read the documentation, etc). Separate configuration applications should be created only where there isn't already an application obviously associated with those options (eg, the default font or screen resolution).

    2004-04-13 All the XSettings options have been moved out of ROX-Session and into a new python applications, which communicate with ROX-Session using D-BUS (which ROX-Session now starts on login). Find them in /uri/0install/rox.sourceforge.net/apps/Configure.

    Drag-and-drop saving in all GTK applications

    Can I get drag-and-drop saving in all my Gtk+ applications?

    See httpthis feature request. Feel free to add a comment if you want this. Type the comment into
    the box marked `Additional Comments' and click on the `Save Changes' button.
    In the mean-time, Stephen Watson has created some tools to make converting existing programs easier: httpwww.kerofin.demon.co.uk. This was for
    GTK+ 1.2, not the current GTK+2.2. In any case GTK+ 2.4 will have a different load/save interface.
    The Gimp now (version 2.3.x in CVS) does drag-and-drop saving from the toolbar, if the preview widget is enabled there. They can't do it using the save box, due to GTK limitations:

    httphttp://thread.gmane.org/gmane.comp.video.gimp.user/6799

    Freedesktop.org autostart

    A draft of the freedesktop.org Autostart Specification has appeared. Possibly we should support it.

    On the other hand, it requires .desktop files and doesn't support application directories, so it might not be useful for us.

    Icon themes

    ROX-Filer lets you choose an icon theme from a menu in Types section of the Options box, allowing you to theme all your icons at once. There is a freedesktop.org standard for icon naming, which should allow us to share themes:

    However, we still cannot share themes (although this has improved with recent Filer versions) because the icon names are inconsistent (we implemented one version of the spec, but it changed again).

    It seems that there is now a (conflicting) freedesktop.org icon naming specification too.

    Restrict read/write access to dragged files

    Is there a way of restricting read/write access to files to drag and drop?

    The idea is that my application is effectively put into a chroot-like prison (perhaps, with the exception of a configuration directory with quota, say), so that I have full control over which files can be actually accessed. After a second thought, this is probably an old idea floating around in this community, but I have not found any hints, yet.

    This is a good idea, but the problem is always with the X server. Any X application can listen for events (key logger) or create them. So, a sandboxed program can just searh for an xterm and start typing commands! Apart from that, there's nothing stopping you doing it now (you can even run the program on a different machine; it can still get the dragged data, provided host names in DnD are turned on in ROX-Filer's config box).

    See Plash: the Principle of Least Authority shell for an experimental implementation of this.

    Shared MIME run actions

    This is about offering the user a list of applications which can open a particular file. Ideally, applications should be able to add themselves to this list in a way that can be picked up by all desktops. Also, the user's preferred handler for opening a particular type should affect all applications.
    See httphttp://www.freedesktop.org/Standards/mime-actions-spec.

    Shared default shortcuts

    Every application running should use the same keys for Save, Quit, etc.

    The File menu

    The file menu is really getting too long now. We should probably hide items that don't apply and think about removing unneeded items (eg, 'Count', now that Info does it). -- Thomas Leonard
    It does need pruning, but I'd prefer not to hide items. It's better if the menu layout doesn't change each time it appears. -- Stephen Watson
    Right, although some things change anyway (eg, Shift Open is displayed as 'Open as Text', 'Mount', etc). There's also a new menu API going into GTK at the moment, but I haven't looked into it. -- Thomas Leonard

    Thoughts:

    • Look Inside and Open AVFS really do the same thing, but one for files and one for apps.

    - Send To is really just to help people find it if they didn't read the manual. Noone actually uses it (apart from binding a key to it).
    I beg to differ! :-) That "bind to key" aspect is important because, without an explicit menu entry, there's no easy way to bind the key. In any case, the File menu is the obvious place for Send To (see the FAQ at the end of this page, for example) -- John Pettigrew

    • Count and Permissions should be moved to Info (which becomes Properties). Need a way to report errors while counting. Have a Permissions button for directories for the full interface?
    • Need extra items for multiple-selections (bulk rename, permissions, etc).
    • Move Find somewhere else, so it works on everything by default?
    • The panel and pinboard File menus should match the main one.

    Trash / recycle bin / undelete feature

    Instead of asking users whether they really want to delete something irreversably, we should simply move it to a trash directory and offer the user the chance to undo the action if it was a mistake.

    Perhaps we can use The FreeDesktop.org Trash specification?

    Virtual File System

    • Since 1. AVFS is outdated, 2. gnome-vfs restricts the opening of files to programs supporting it and 3. FUSE is Linux-only yet, I would like to suggest some kind of "open" vfs layer that enables the user to use whatever VFS he could lay his hands on. -- Alex Kloss
    • Freedesktop are planning a new cross-desktop VFS: see httphttp://thread.gmane.org/gmane.comp.freedesktop.xdg/5511

    When 'show iconified windows' is on, show thumb

    Suggestion: in options, when 'show iconified windows' is on, add:

    1. make a snapshot (thumb) of the minimized window and use it as icon (toggle)
    2. show only iconified windows of current workspace (toggle) (done in ROX-Filer 2.3)

    I like that idea. They would help the user have a better idea of what the minimized folder was, and it would help disinguish more clearly the fact that the minimized program in question is, in fact, a minimized program and not a shortcut. --James Gecko

    Rejected ideas

    • Keep the path entry minibuffer open at all times. We had a patch for this, but it turned out it wasn't useful (even the original author requested it be removed). Pressing / is pretty easy.
    • Back and Forward browser-type buttons. Again, we had a patch for this and again it was decided to remove it. The extra buttons make the interface much more confusing for very little gain. See the 'Recently Visited' submenu from the bookmarks button for an easier way to backtrack.
    • Have a script (App Dir/AppBoot) that gets run when you open a filer window showing the application (like RISC OS did). This is a massive security risk. Some people suggest reducing what the script can do (eg, only setting environment variables). It doesn't matter; any change is a security risk (setting PATH is an obvious example, but even something 'harmless' like BROWSER will allow anyone to hijack your machine the next time you try to view a URL).