Other resources:
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:
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.
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.
See Patches and feature requests for details.
Git has very good documentation, e.g.:
http://www.kernel.org/pub/software/scm/git/docs/tutorial.html
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:
$ 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.
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:
This is a guide for people wanting to translate ROX-Filer (or other ROX projects using GIT) into other languages.
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
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!
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:
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).
To update to the latest version of the program, do a pull:
$ git pull
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.
To actually create the archive that people will download:
$ svn co https://svn.sourceforge.net/svnroot/rox/trunk/Archive
$ 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.
$ 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
$ xxdiff archive-1.1 archive-1.2
$ LANG=fr ./Program/AppRun
$ gpg program-x.y.z.tgz.sig
There are various documents detailing how different bits of the desktop work:
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.
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 ;-)
For consistancy, the ROX documentation tries to use the following words with the meanings given here:
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:
As you can see, all these rules mean that you should never have to preform any actions at install time.
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.
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:
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:
See also:
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:
Using image icons in the buttons can also help here.
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.
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!)...
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.
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:
If you are using ROX-Lib, uncaught exceptions will be automatically displayed in a box like this:
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 ;-)
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.
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 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!
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.
This section has a few more technical hints for application authors:
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.
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=valin 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 "$@" ] ; thenwith
if [ -f "$@" ] ; then
Equality
Use only a single = symbol to test for equality, not ==
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.
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.
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.
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.
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 from subversion to git, so see the git instructions instead. The other components are still in subversion for now.
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.
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 .
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.
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.
This is a long-term plan for how to handle configuration in ROX.
Road map:
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.
See this 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: www.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:
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.
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.
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.
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 http://www.freedesktop.org/Standards/mime-actions-spec.
Every application running should use the same keys for Save, Quit, etc.
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:
- 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
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?
Suggestion: in options, when 'show iconified windows' is on, add:
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