This section contains some tutorials for ROX application developers.
Ex-RISC OS users: Read the Python for BASIC programmers primer first.
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).
If you find the python code confusing, you may want to read the Python Tutorial first.
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:
#!/usr/bin/env python import findrox; findrox.version(1, 9, 2) import rox rox.info('Hello World!')
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.
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
rox.info('Hello World!')
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.
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)
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.
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.
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 GTK 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.
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:
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.
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.
You should have already read the first tutorial, as this one assumes you already have the application directory and other files set up.
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.
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))
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.
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 ;-)
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:
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.
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) ...
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.
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
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.
People often want to make applets that scale to the current size of the panel. The problems people often have with this are:
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:
* 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()
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.
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
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).
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).
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).
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:
<options> <section title='Display'> <frame label='Time format'> <radio-group name='format'> <radio label='Short format' value='short'>Just show the time</radio> <radio label='Long format' value='long'>Show the time and date</radio> </radio-group> </frame> </section> </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...
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.
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.
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:
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.
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.
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.
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.
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.
For more information, see the tasks API documentation.
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?
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.
Here's the familiar hello world program in BASIC and in Python:
PRINT "Hello World"
becomes
print "Hello World"
a = 4.5 b% = 3 c$ = "Hello World"
becomes
a = 4.5 b = 3 c = "Hello World"
d = None
number = input("Enter a number:")
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).
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
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 < 10: ... print x ... x = x + 1 ... 1 2 3 [etc]
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 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 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 < 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" ...
>>> def double(x): return 2 * x ... >>> double(4) 8
>>> def hello(): ... print "Hello World" ... >>> hello() Hello World
>>> a = hello() Hello World >>> print a None
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"
>>> a[1] = 4 >>> a ["red", 4, "blue"]
>>> a[-1] "blue" >>> a[-2] 4
>>> a[1:3] [4, "blue"] >>> a[1:] [4, "blue"] >>> a[:1] ["red"] >>> a[:-1] ["red", 4]
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'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"
In fact, python provides a better way to count, as we shall see later.
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"
try: answer = 1 / number except DivisionByZero: print "Can't devide by zero!" except: print "Something wierd happened!"
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).
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] <---- 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 <---- 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!
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
a ----> [1, 2, 3]
[1, 2, 3] a ----> None
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
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.
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()
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...).
> stream% = OPENIN("MyFile") > PRINT stream% 254
becomes
>>> stream = file("MyFile") >>> stream <open file 'MyFile', mode 'r' at 0x8151290>
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
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")
>>> import math >>> math.cos(0) 1.0
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.
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
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.
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.
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:
>>> "Pi is %.3f (to three decimal places)" % math.pi 'Pi is 3.142 (to three decimal places)'
>>> 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
>>> def double(x): return 2 * x ... >>> map(double, [1,2,3]) [2,4,6]
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)
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.
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
<?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
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):
Note that my repository has two branches: "master" contains modifications that upstream may want to pull back, while "rox" contains the ROX-specific modifications.
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:
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.
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.
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):
To pull any new features in from upstream:
$ git-pull origin
Then run make-release to upload it as version 0.2.