Anda di halaman 1dari 61

Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

"Copyright (C) 2009 Luke Kenneth Casson Leighton


(lkcl@lkcl.net) and Erik Westra"

Introduction
Computer Applications help us with the presentation, creation and manipulation of information and
knowledge. Deployment of applications on the Internet has seen an explosive rise in complexity.
Programming languages and frameworks have struggled to keep up, imposing legacy infrastructure and
ways of working that were never designed to cope with the ways in which they are now being used.
Attempts are being made to provide better infrastructure that suits the increasing Internet-led demand for
access to information and knowledge, yet at the same time provide an easier way to reuse the same
source code to write applications that run stand-alone on the Desktop. Adobe AIR, Silverlight and its
free-software counterpart, Moonlight, are proprietary-driven frameworks that blur the distinction between
"Desktop" and "Internet". Google's Web Toolkit (GWT) is a toolkit exclusive to Java that allows you to
write applications that look like they are Java Desktop applications, but actually they are being run in a
web browser (as AJAX). You cannot, however, take the same GWT Java application and actually run it
as a desktop application (unless it's under Adobe AIR, as AJAX).
In 2006, GWT was ported to python, by James Tauber, in a successful experiment called Pyjamas. The
80,000 or so lines of source code that made up GWT 1.2 were dramatically reduced to only 8,000, for
Pyjamas. Thus, in exactly the same way that GWT is a java-to-javascript compiler and AJAX Widget set,
Pyjamas is a python-to-javascript compiler and AJAX Widget set (with an identical API to that of GWT's
Widget API). In 2008, Pyjamas was ported to the Desktop, bypassing all of the javascript completely. The
underlying technology used was Webkit, which is the same technology behind Adobe AIR, Google
Chrome, Safari, Midori and the iPhone browser. Thanks to Webkit, Pyjamas, by keeping such exulted
company, becomes the world's first free-software Applications framework that is both cross-browser and
cross-platform. Also, by using Webkit, the Pyjamas-Desktop port is cross-widget-set as well.
The strategic significance of the independence of the Pyjamas User-Interface API is easily
underestimated. A free software python-based application can be written - once - and can run on any
web browser, including IE6, IE7, Opera, Mozilla, Firefox and Safari, and also it can be run on embedded
platforms such as smartphones and PDAs, as well as Desktop OSes such as Linux, Windows, FreeBSD
and MacOSX.
Also, the Pyjamas API is effectively a new type of Widget set, and is not only functionally superior in its
scope and features to its competitors, Qt4 and Gtk2, but is also easier to use, extend and understand.
The reason for this is quite straightforward: Pyjamas is based on DOM model - browser - manipulation,
and browser technology has had to mature much, much faster than Desktop technology. Browsers have
to be far more tolerant, forgiving and flexible when it comes to layout and presentation, whereas the
Desktop application APIs are really quite rigid; advise developers not to fix the width and height of
widgets, for fear of interfering with the framework's limited ability to cope with resizing and layouts;
extension of either of the competitor frameworks is far from straightforward; Rich text (such as HTML)
support is limited or severely limited, and is broken by design. By contrast, the features that Pyjamas
comes with by default, even in Pyjamas Desktop, includes not only full and complete CSS and HTML
support, but also includes support for things like SVG 2D canvas and NPAPI plugins such as Adobe
Flash.
The future of Pyjamas on Desktops includes, using Webkit, the tantalising possibility of supporting other
languages, such as c++ (Pyjamas++) and even Java. It would be very interesting to see Google's Web
Tookit ported to the Desktop, for example. And it would be as equally interesting to see Pyjamas ported
to Ruby, by using rb2js (http://rb2js.rubyforge.org/), as well as other languages. So, although this book
covers the Python programming language, it's definitely worthwhile raising these future language
possibilities, as it would make the Pyjamas API a truly cross-language, cross-platform, cross-desktop
and cross-widget-set Free Software Applications Development API.

Scope of this book

1 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

The purpose of this book is to assist Python developers who are either Web developers, PyQt4 or
PyGtk2 Desktop developers to write applications that will run both on the Desktop and in a Web browser,
depending on whether they choose to run the application using Pyjamas-Desktop or to compile it to
AJAX, using the Pyjamas Javascript compiler.
The book will introduce the beginner to the alien concept of "easy web application development"; it will
serve as a reference for both beginners and experienced developers; also covered will be how to create
your own widgets, and how to port a Javascript widget library (such as extjs, prototype or YUI - Yahoo
User Interface) to Pyjamas. Creation of Pyjamas widgets is something that is infinitely easier than it is
for Gtk2 and Qt4, thanks to the simple concept of manipulating a browser-based DOM model.
In only thirty minutes, even the average Python developer should be up and running, creating ubiquitous
rich media applications, and be dreaming one day of writing their own version of a gmail client...

Dive Straight in
Not only for the impatient, but also getting right to the point, start by downloading the Pyjamas source
code, from either http://pyjs.org or http://code.google.com/p/pyjamas and, if you already have the
standard Python 2.5 interpreter installed, you can begin immediately. Pyjamas is self-contained and does
not even require to be "installed".
Unpack the downloaded archive, and browse to the examples directory. You will find a script called
"buildall.sh". If you just want to compile the "helloworld" example, browse to the examples/helloworld
directory, and you will see a "build.sh" file and a README. Follow the instructions, there, which instruct
you to run the following command:

python ../../builder/build.py Hello.py

You will then be able to open your web browser, browsing to the examples/helloworld/output/Hello.html file,
and, assuming that you have Javascript enabled, you should see a single button, "Click me!". Click it, and
you should get an alert "Hello Ajax!".
Congratulations, you have compiled and tested your first Pyjamas application.

If you accidentally opened examples/helloworld/Hello.html in your


web browser, this is a "stub loader" that requires the pygwt.js
and the compiled application to be in the same subdirectory.
Double-check that you have opened examples/helloworld/output/Hello.html,
not examples/helloworld/Hello.html

Debugging
If you didn't get anything displayed, then it is of vital importance now and forever that you examine the
Javascript Console of your browser of choice - right now. Get used to doing that, even at this early
stage: your development cycle depends critically on you understanding the link between the Python code
you are writing in and the Javascript that it becomes.
Also of critical importance will be to install a Javascript Debugger tool. "Venkman" is the codename of the
Firefox plugin that you will need; for Internet Explorer you can get away with the stand-alone Script
Debugger, although you can also use the Script Editor that comes with Office or Visual Studio if you
prefer. The key tool that is absolutely essential is to be able to see a stack trace (Opera will display a
stack trace, by default).
The alternative is to run your application under Pyjamas-Desktop, and you will then get a standard python
stack trace error printed on the console output. So, if you get absolutely stuck, running the code in a
browser, you can always run it under Pyjamas-Desktop.
More advice on debugging applications will be given later.

Editing your first application


Lovely as it is to run, it's more useful to be able to add things, so start by modifying Hello.py so that it
looks like this:

2 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

from pyjamas.ui import Button, RootPanel, HTML


from pyjamas import Window

def greet(sender):
Window.alert("Hello, AJAX!")

class Hello:
def onModuleLoad(self):
b = Button("Click me", greet)
RootPanel().add(b)
hw = HTML("Hello <b>World</b>")
RootPanel().add(hw)

The changes made are the addition of the import of HTML, from pyjamas.ui; the declaration of a
variable, hw, and its addition to the RootPanel. Re-run the build command, refresh the browser window,
and, underneath the Button there should now be the words "Hello World", with "World" being in bold.
Again, if it doesn't appear, double-check the Javascript Console for error messages - for example, if you
see "HTML is undefined", it'll be because you missed out the line "from pyjamas.ui import HTML".

if you are unfamiliar with Python, and you get a build error, read up on
Python application editing: double-check that you have used the same
"indent" level of whitespace, when you added the two extra lines in the
onModuleLoad() function.

Horizontal Layout
Next, let's try editing the application so that the Button and the HTML are laid out horizontally, rather than
vertically. Try this:

from pyjamas.ui import Button, RootPanel, HTML


from pyjamas.ui import HorizontalPanel
from pyjamas import Window

def greet(sender):
Window.alert("Hello, AJAX!")

class Hello:
def onModuleLoad(self):
b = Button("Click me", greet)
hw = HTML("Hello <b>World</b>")

p = HorizontalPanel()
p.add(b)
p.add(hw)

RootPanel().add(p)

Here, we have imported HorizontalPanel, then we have declared an instance (p) to which we have added
both the Button and the HTML. Then, we have added the HorizontalPanel, p, into the RootPanel.
Remember to recompile, and remember to refresh the browser.
It's worth emphasising that Pyjamas applications really are this straightforward, and that quite complex
layouts can be built up very quickly and in a straightforward manner, with the simple deployment of
HorizontalPanel, VerticalPanel, Grid and HTMLTable. However, at this early stage, although our
application is "functional", it doesn't look too pretty. We should probably add in some CSS styling, next.

CSS Styling
Pyjamas being an HTML-capable development platform, you have complete access to CSS. Start off by
creating a subdirectory called "public", in the examples/helloworld directory, and create a file, Hello.css,
as follows:

.panel {
margin: 20px;

3 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

padding: 20px;
background-color: #80ff80;
width: 50%;
height: 200px;
}
.helloworldwords {
font-size: 200px;
border: 1px solid #aaaaff;
margin: 10px;
padding: 10px;
}

Next, create a matching Hello.html file, again in the subdirectory called "public", If there is already one in
the examples/helloworld directory, copy it into "public" to save yourself some time, and edit it to add a link
to the stylesheet:

<html>
<head>
<meta name="pygwt:module" content="Hello">
<link rel='stylesheet' href='Hello.css'>
<title>Hello</title>
</head>
<body bgcolor="white">
<script language="javascript" src="pygwt.js"></script>
</body>
</html>

All we've done, here, is add a link to our stylesheet, Hello.css - there's no "voodoo magic" going on, at
this stage. Our next step, however, is to tell the HorizontalPanel instance and the HTML instance that
they should be using the two CSS style classes we've created. Add the two lines, shown, to the Hello.py
file:

p.add(b)
p.add(hw)

p.setStyleName("panel") # same name as in Hello.css


hw.setStyleName("helloworldwords") # style declared in Hello.css

RootPanel().add(p)

The HorizontalPanel gets a CSS style class name of "panel", and the HTML instance gets one of
"helloworldwords". Again, recompile, and refresh the browser, and you should find that the
HorizontalPanel now takes up half the screen width, is an ugly light green colour; that the size of the
words "Hello World" have doubled in size and are surrounded by a pretty light-blue border.
You're now well on your way to causing much grief and eye-burn to all but the most colour-blind of
computer users.

Random things to try


If you're feeling adventurous, you might like to add in two buttons, or perhaps add in a CheckBox or
RadioButton (remember to import them!). Also, it's highly illustrative to add things to the RootPanel from
the greet() function, as this will definitely emphasise that this is not "static HTML".

from pyjamas.ui import CheckBox


def greet(sender):
RootPanel().add(HTML("Not in Kansas no more"))
y = CheckBox("yellow road")
r = CheckBox("red road")
RootPanel().add(y)
RootPanel().add(r)

Remember to click the button several times, and you should find that the text and two checkboxes are
added on each click. You might wish to create a global variable that counts up by one - x += 1 - that you
tack on to the end of the string - "No of times not in Kansas: %d" % x as this will make it

4 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

clearer:

x = 0

def greet(sender):
global x
x += 1
RootPanel().add(HTML("No of times not in Kansas: %d" % x))

What we have done


To recap what's been achieved, in this section, we have:
Downloaded and unpacked the Pyjamas Compiler.
Compiled and run the "Hello World" application (yippee!)
Optionally encountered our first Javascript Console error
Added an HTML widget to display "Hello World" under the button
Optionally discovered that widgets need to be imported
Changed the layout from vertical to horizontal
Added some eye-burning colour using a CSS Stylesheet.
Learned in a few minutes that Pyjamas apps are dead-easy.
Discovered that we're definitely not in Kansas.
So we have gained the confidence that Pyjamas applications can be written in very few lines of code, and
also now have a feel for what the development cycle will be like. The only thing is that the "Hello World"
application is, after all, very much like a "Static HTML" page, despite it being in Javascript. The reason is
because we're not exactly interacting with the rest of the world.
Quite sophisticated single-user applications can still be written either using SVG 2D Canvas (see the
examples/addonsgallery Canvas Tab) for games and entertainment purposes, but it's only when we start
using AJAX, to communicate with a Web Server, that we can really begin to create powerful "rich media"
interactive - and useful - applications.

AJAX - Gateway to the World


Javascript, when run in browsers, is restricted - for example, access to user's files is off-limits. So, even
when running Pyjamas-Desktop, your gateway to the rest of the world in Pyjamas apps should be with
AJAX - Asynchronous Javascript and XML. The implications are that applications need to be divided
down into a front-end and a back-end, with the back-end running on a Web Server. Even for stand-alone
applications, applications still need to talk to a Web Server, which will need to be installed locally.
Pyjamas includes a library - HTTPRequest - that provides convenient access to AJAX. The differences
between Internet Explorer and other browsers when using AJAX, that you'd normally be faced with, is
taken care of by the library, so that, on all platforms, the HTTPRequest library is functionally identical.
The simplest use of HTTPRequest is to obtain text files or HTML content from the server, whilst more
involved ones will use HTTPRequest to provide a Remote Procedure Call framework, such as
JSONRPC or XMLRPC. Here are some Pyjamas examples, included with the Pyjamas distribution, that
use AJAX / HTTPRequest:
The slideshow example loads its slides in text format, and then applies some simple Wiki-like
markup rules to convert the text file into HTML.
The TemplatePanel addons module can be used as the basis of a Content Manager, as it
downloads HTML content, allows it to be edited (with FSCK Rich Text Editor) and uploaded - all
using AJAX.
The JSONRPCService example shows how powerful AJAX can be, as the JSONRPC
infrastructure transparently makes function calls look like they were being made on the server.
This latter example - the JSONRPCService one - is one which is going to be explored in more detail,
later, but first it's worth illustrating with the simplest example, the Slideshow Loader.

Simple use of HTTPRequest


Here is the basis of the Slideshow example, which you should save as Slideshow.py:

5 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

from pyjamas.ui import HTML, RootPanel


from pyjamas.HTTPRequest import HTTPRequest

class SlideLoader:
def __init__(self, panel):
self.panel = panel

def onCompletion(self, text):


self.panel.setSlide(text)

def onError(self, text, code):


self.panel.onError(text, code)

def onTimeout(self, text):


self.panel.onTimeout(text)

class Slideshow:

def onModuleLoad(self):
self.slide = HTML()
RootPanel().add(self.slide)

HTTPRequest().asyncPost("test_slide.txt", "", SlideLoader(self))

def setSlide(self, content):


self.slide.setHTML("<pre>%s</pre>" % content)

def onError(self, text, code):


self.slide.setHTML(text + "<br />" + code)

Create a matching Slideshow.html, saving it in a folder called public:

<html>
<head>
<meta name="pygwt:module" content="Slideshow">
<title>Test Slideshow!</title>
</head>
<body bgcolor="white">
<script language="javascript" src="pygwt.js"></script>

<iframe id='__pygwt_historyFrame' style='width:0;height:0;border:0'></iframe>


</body>
</html>

Now create a test slide in the public folder, and call it test_slide.txt:

Yippee, we get to see how AJAX works.


We put the content here, wrapped with <pre> </pre>
so it looks pretty.
You could put anything you wanted in here.

You should now have three files on your computer like this:

Slideshow.py
public/Slideshow.html
public/test_slide.txt

Compile this example to javascript, by running the following command:

python ../../builder/build.py Slideshow.py

You will now need to use a Web browser to access the output, but it is important that you load the output
from a Web Server, not from your local filesystem. There are several free web servers and web
frameworks available, and for the purposes of this exercise, one that serves static web pages is
perfectly adequate.

6 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

AJAX has security restrictions on where the content can be downloaded from,
due to malicious misuse. As there is no visual indication, with AJAX,
when content is being downloaded, AJAX was used to obtain and run malicious
scripts from web sites other than the one being presented to the user.
So, now, when you browse a web site, if AJAX is used, the AJAX content can
only be downloaded from the same web site. Local file system URLs are
definitely "out".

Configuring Apache2
A recommended way to serve web pages is to install and use Apache2. You should, after installation, set
up a location where your compiled output is made visible. Here is an Apache Config snippet that you can
adapt to your needs, by changing the directory location:

Alias /examples/ "/home/lkcl/src/pyjamas/examples/"


<Directory "/home/lkcl/src/pyjamas/examples/">
Options Indexes FollowSymLinks MultiViews
AllowOverride None
Order deny,allow
allow from all
</Directory>

The above is pretty standard fare for an Apache configuration: remember however that you must ensure
that the AJAX application matches up with the locations that you set, here, in the Apache2 configuration,
and that the directories you ask the scripts and static content to be loaded from must actually exist!

Remember to change the location to one where your output is compiled!


Also, make sure that you place the trailing slashes on the end of the
directory paths. Also, remember to enable mod_cgi if you want CGI
scripts to run. Search on google for advice on how to set up Apache
CGI scripts, for your specific system, if you run into difficulties.

With the above example, once you have started (or restarted) Apache2, you will be able to open up a
web browser on http://localhost/examples/ to run your application. If successful, you should see the
contents of the test slide in the browser.

Remember that if you don't see anything, check the Javascript console,
and enable script debugging. Also, double-check that you have loaded
output/Slideshow.html, not public/Slideshow.html, in your browser.

Using Python's SimpleHTTPServer.py


TODO

python -c "import CGIHTTPServer; CGIHTTPServer.test()"

Using Lighttpd
TODO

server_modules = (
"mod_access",
"mod_accesslog",
"mod_dirlisting"
)

server.port = 3000
server.document-root = "/home/Administrator/prg/pyjamas-book"

server.errorlog = "/tmp/errorlog"

accesslog.filename = "/tmp/accesslog"

7 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

dir-listing.activate = "enable"

mimetype.assign = (
".html" => "text/html",
".txt" => "text/plain",
".jpg" => "image/jpeg",
".png" => "image/png"
)

Simple enhancements
In the Pyjamas slideshow example, setSlide() munges the AJAX-downloaded content before it gets
displayed, and you could prove to yourself that this is definitely not "static HTML" by modifying the
content in setSlide, by converting the content to uppercase:

def setSlide(self, content):


content = content.upper()
self.slide.setHTML("<pre>%s</pre>") % content

The extra line is the second one - modifying the content before it is displayed.

Remember to recompile the application, and refresh the browser.

So, in this simple example, we've shown how to load content, dynamically. Which is nice, because loading
the Web page first, and the content separately, is what AJAX is supposed to be all about: saving time
and speeding up the user experience. Of course, loading only the one page isn't that much of an
improvement, but it's the thought that counts. definitely.
However, with a small leap in imagination, it should be clear that by specifying a dynamic URL in the
HTTPRequest, instead of a static page, this simple example could be adapted to load user's individual
email messages, or turned into a wiki. The next step is to make the process of loading those email
messages, wiki pages or stock prices that much easier, and that much more like a standard desktop
application.

JSONRPC: a better AJAX experience


JSONRPC stands for "JavaScript Object Notation / Remote Procedure Calls". To make sense of that
banale sentence, it helps to look up the two anacronyms separately. JSON is simply a way to encode
objects - lists, dictionaries, integers, strings and floating point numbers - in a way that web browsers will
find easy to encode and decode. RPC is simply a way to make function calls on a server. In other words,
whilst the function is being called on the client, the function is actually executed on a (remote) server,
and the parameters and the return result are shipped back-and-forth without the client or the server
having to do any particularly hard, or obtuse, work.
The best RPC systems allow the function's parameters to be shipped back to the client, if they were
modified, as well as the return result of the function. However, JSONRPC isn't the brightest bunny on the
block, and so we have to make do with just a return result. Fortunately, JSON is capable of returning
reasonably complex results - lists of dictionaries, dictionaries of lists of strings etc. - so the limitations of
JSONRPC aren't the end of the world.

The crucial difference between running an application that makes


"real" function calls rather than using a client-server arrangement
to proxy the function parameters and return result back-and-forth is
that, as a developer, you must never forget that the function
call can fail! The server can, at any time, drop off the network,
or fail to respond in a timely fashion. It's absolutely essential
that you design your application with this in mind. RPC frameworks
help you out here, and the JSONRPC infrastructure in Pyjamas is no
exception.

So, to get started with JSONRPC, the following is needed:


A JSONRPC client (there's one built in to Pyjamas)

8 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

A JSONRPC-aware Web Server Framework


A Web Server (even when running on the desktop)
Discussed first will be the frameworks: some of the Python-based options, such as Twisted Matrix,
Django, Web.py and cgi-bin will be covered. Next, once a framework is set up, connecting to it with the
JSONRPC example client will be covered.

JSONRPC Web Server Frameworks


There are dozens of JSONRPC frameworks available, as a quick google search shows. Any one of
them will suffice, however the scope of this book is limited to Python, so the following Python-based
JSONRPC frameworks will be covered:
Pimentech's libcommonDjango - JSON-RPC for Django. libcommonDjango can be downloaded
from http://lkcl.net/libcommonDjango.tgz and the Django Framework from http://djangoproject.com
Web.py, a minimalist powerful web framework, can also be used. Download the JSONRPC
example here: http://lkcl.net/webpy.jsonrpc.tgz and the Web.py Framework from http://webpy.org
CGI-based JSONRPC - there is an example already included in the Pyjamas distribution,
examples/jsonrpc/public/services, called EchoService.py
txJSONRPC - JSON-RPC for Twisted. txJSONRPC can be downloaded from
https://launchpad.net/txjsonrpc and the Twisted Framework from http://twistedmatrix.com
So it is entirely up to you which one you use: the Pyjamas examples even include a PHP-based
JSONRPC service, using a php library called phpolait.

CGI-based JSONRPC
If you have a suitable web server installed, one of the simplest methods to get started has to be CGI
(Common Gateway Interface). It's also one of the slowest, causing the server to load an entire
application just to serve up one and only one web page - even if the response is a couple of lines, which
will often be the case on JSONRPC responses.
Ideally, you should ensure that CGI is configured correctly, and working. Here is an example Apache2
configuration for making the jsonrpc example services directory executable:

ScriptAlias /services/ /home/lkcl/src/pyjamas/examples/jsonrpc/output/services/


<Directory "home/lkcl/src/pyjamas/examples/jsonrpc/output/services/">
AllowOverride None
#AddHandler cgi-script .py
Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch
Order allow,deny
Allow from all
</Directory>

With the above example, once you have started (or restarted) Apache2, you will be able to open up a
web browser on http://localhost/examples/ to run your application. If successful, you should see the
contents of the test slide in the browser.
You can test the CGI example, after restarting or reloading Apache2, by browsing to
http://localhost/services/EchoService.py. If you are offered an opportunity to download
EchoService.py, then you have not correctly set up cgi-bin execution. If, however, you get presented with
a blank screen, then congratulations, the EchoService.py is correctly running: you are ready to connect
to it with a JSONRPC client, which will be covered later.
Once you have CGI correctly configured, examine the jsonrpc/public/services/EchoService.py source
code - it's pretty straightfoward:

#!/usr/bin/env python

class Service:
def echo(self, msg):
return msg

def reverse(self, msg):


return msg[::-1]

def uppercase(self, msg):

9 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

return msg.upper()

def lowercase(self, msg):


return msg.lower()

from jsonrpc.cgihandler import handleCGIRequest

handleCGIRequest(Service())

The first line tells a unix-based system that when this script is executed, it must be executed as a python
script. Every individual HTTP Request will result in a new process being started, to handle the Request,
and that process will terminate when the Response is sent to the client.

If running Windows, and Apache2 has been chosen as the web server, adding
"AddHandler cgi-script .py" to the Apache2 configuration section
will make all .py scripts in that section executable. Only add this to
the sections that you need - don't add this Directive globally!

The class "Service" contains our four functions that this JSONRPC service will support. Any JSONRPC
client that has EchoService.py as its HTTP "POST" url will be able to run these four functions.
The next two lines import and use handleCGIRequest to process the JSONRPC query. If you are
interested, take a look in the public/services/jsonrpc directory, at cgihandler.py - it's pretty
straightforward, but it's just a library. The reason why it's mentioned is that you may be wondering where
jsonrpc.cgihandler comes from, when you haven't installed a library called "jsonrpc" on your system: it's
being imported from the local path, by the python application, from the examples/public/services
directory.
That's really all there is to it. The dynamic capabilities of Python allow the jsonrpc cgihandler library to
match up the function call names in the incoming HTTP POST with the function names in the Service()
class instance. Adding extra functions is a trivial matter of adding new methods to the Service class.

JSONRPC in Django
This technique is a little more advanced, and relies on python "decorators", so you will need to ensure
that you at least have Python 2.4 or greater (but not Python 3). First, you should download and install both
Django and libcommonDjango, which are available from http://djangoproject.com and http://lkcl.net
/libcommonDjango.tgz respectively.
If you are unfamiliar with Django, then first, the standard Django Tutorial should be followed. By stage
three, enough of a polls/view.py will have been created such that the following lines can be added:

from django.pimentech.network import JSONRPCService, jsonremote

testservice = JSONRPCService()

@jsonremote(testservice)
def echo(response, msg):
try:
poll_id = int(msg)
except ValueError:
return "You must type an integer, you typed: %s" % msg
p = Poll.objects.get(pk=poll_id)
return p.question

@jsonremote(testservice)
def reverse(response, msg):
return msg[::-1]

@jsonremote(testservice)
def uppercase(response, msg):
return msg.upper()

@jsonremote(testservice)
def lowercase(response, msg):
return msg.lower()

10 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

As can be seen, the libcommonDjango library from http://pimentech.com is the key. The
@jsonremote() decorator turns the four functions into JSONRPC server functions. Note that, unlike
the CGI-based example, you have access to a response instance variable - just like in any other
Django HTTP Response processing function.
The echo function in this example reads from the Poll table, just to demonstrate how interaction between
JSONRPC and a database works. If preferred, or the Django Tutorial is not being used, simply return
msg instead, to maintain consistency with the CGI-based example.
In Django, response-processing functions and classes don't magically get called: it is necessary to map
them to incoming HTTP urls using urls.py. Edit the Django application's urls.py file, and add the line noted,
below:

urlpatterns = patterns('',
# this is the JSONRPC service line that needs to be added:
(r'^test-service/$', 'mysite.polls.views.testservice'),

(r'^admin/(.*)', admin.site.root),
(r'^polls/$', 'mysite.polls.views.index'),

As can be seen, each line in urlpatterns maps a url to a class or function, and in this example, if your web
server is accessible at http://localhost then any references to http://localhost/test-service/ will be mapped
to the JSONRPCService instance, testservice, in mysite/polls/views.py.

Make sure that the urlpatterns match the application - in other words,
the example above is only valid if application is called "mysite". Also,
take note of the URL, above, because it will be needed, later, when it comes
to connecting the JSONRPC-aware Pyjamas client to the Django server.

When working with Django, any modifications that you make to the application will be picked up
automatically when running Django in "test" mode (python manage.py 8080). If running the
application using mod_python under Apache2, Apache2 will need to either be restarted or reloaded.

Apache2 is quite happy with a reload, if the application has no errors.


However, if an error occurs in the application, the thread will bomb out
and "stick". As threads are chosen at random, to respond to requests,
what happens is that at some point in the future, the "stuck" thread will
be called on again to respond to a request... and it will respond with
the same error message, regardless of the request and regardless
of the requester. So it is unfortunate but vital that you restart
Apache2 if an error occurs, when using mod_python.

So, again - there's very little involved in setting up a JSONRPC service under Django. There's actually so
little "hard work" that it's almost a let-down to know that it is possible to do Web Development where the
technology doesn't get in the way.

Web.py JSONRPC Server


Web.py is a very simple and straightforward web framework that is used for some very substantial web
sites. The key significance of Web.py is that it does not need to be run from a Web server, it is a Web
Server.
It's very simple to integrate JSONRPC into web.py. First, download and install web.py from
http://webpy.org and also download a JSONRPC library from http://lkcl.net/webpy.jsonrpc.tgz which
contains a modified version of Pimentech's JSONRPC service. The modifications are tiny but significant,
allowing the raw data from the HTTP POST to be passed to the JSONRPC Service.
Start by unpacking the archive, and creating a file with the following content. The file should be saved in
the directory where you have unpacked the archive, so that the network library can be imported by it.

#!/usr/bin/env python

from network import JSONRPCService, jsonremote


import web
import os

11 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

urls = (
'/chat/', 'chatcls',
'/chat', 'chatcls',
)

# a "wrapper" class around the jsonrpc service "chatservice"


class chatcls:
def POST(self):
print chatservice(web.webapi.data())

chatservice = JSONRPCService()

# the two demo functions

@jsonremote(chatservice)
def echo(request, msg):
web.debug(repr(request))
return "hello world %s" % msg

@jsonremote(chatservice)
def reverse(request, msg):
return "hello world %s" % msg[::-1]

@jsonremote(chatservice)
def uppercase(request, msg):
return "hello world %s" % msg.upper()

@jsonremote(chatservice)
def lowercase(request, msg):
return "hello world %s" % msg.lower()

# start me!
if __name__ == "__main__":
web.run(urls, globals())

Once again, we can see that it's really quite straightforward to set up a JSONRPC Service. In Web.py,
the mapping between URLs and the handlers is done in a similar fashion to Django (the urls variable); a
key difference between Django and Web.py is that classes must be used, and the classes must have a
GET (or in this case, a POST) function. In this example, a wrapper class has been used, that links the
URL /chat/ to the JSONRPC service.
The last three lines run the built-in web server, which, by default, will run on port 8000. If the above code
is saved as code.py, it can be started with the command python code.py. Starting the application will
allow the JSONRPC service to be accessed on http://localhost:8000/chat/ with a
JSONRPC-aware client. You will need to remember this URL when it comes to connecting the client with
the web.py server.

Twisted Matrix txJSONRPC


Twisted Matrix is a comprehensive framework for networking and networked applications, written in
Python. There is a JSONRPC library, which can be used to create both clients and servers, called
txJSONRPC: it is available from https://launchpad.net/txjsonrpc.
First, you should download and install the Twisted Matrix framework, from http://twistedmatrix.com.
Second, you should install txJSONRPC. To create a Twisted Matrix JSONRPC Service, start by creating
a file, to be saved as server.tac, with the following contents:

from twisted.web2 import server, static


from twisted.web2.channel import http
from twisted.application import service, internet

from txjsonrpc.web2 import jsonrpc

class Example(jsonrpc.JSONRPC):
"""An example object to be published."""

addSlash = True

12 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

def jsonrpc_echo(self, request, x):


"""Return all passed args."""
return x

def jsonrpc_add(self, request, a, b):


"""Return sum of arguments."""
return a + b

root = static.File("/var/www/")
root.putChild("doc", static.File("/usr/share/doc"))
root.putChild("test", Example())
site = server.Site(root)

chan = http.HTTPFactory(site)
application = service.Application("Example JSON-RPC Server")
jsonrpcServer = internet.TCPServer(7080, chan)
jsonrpcServer.setServiceParent(application)

Once saved as server.tac, the above can be run with this command:

twistd -noy server.tac

The example is an adaptation of the server example that can be found on https://wiki.ubuntu.com
/txJSON-RPC/TwistedWeb2, and it is instructive to compare the two. As it proved quite challenging to
find easy-to-understand documentation on how to set up Twisted Matrix servers, some time will be spent
explaining the above. The key to a JSONRPC service, here, is in creating a class that derives from
txjsonrpc.web2.jsonrpc.JSONRPC. However, the example on the txJSONRPC Wiki serves pages in a
way which dominates port 7080:

site = server.Site(Example())
chan = http.HTTPFactory(site)
application = service.Application("Example JSON-RPC Server")
jsonrpcServer = internet.TCPServer(7080, chan)
jsonrpcServer.setServiceParent(application)

In this example, taken from the txJSONRPC Wiki, the only thing that can be accessed is the JSONRPC
Service, via the following url: http://localhost:7080/. Clearly, this is of no use, in a real
environment, as it will not be possible to serve anything else, such as the index.html file of the site! These
lines, therefore, are key:

from twisted.web2 import static

root = static.File("/var/www/")
root.putChild("doc", static.File("/usr/share/doc"))
root.putChild("test", Example())
site = server.Site(root)

Static HTML can, in this case, be served from /var/www/, which will be the "root" - accessible as
http://localhost:7080/. The contents of /usr/share/doc will be served from
http://localhost:7080/doc/, and, crucially, our JSONRPC Example service will be accessible
via http://localhost:7080/test/.
In this way, an AJAX application's index.html and other content, such as images, can be placed in
/var/www, and the AJAX code in the Pyjamas JSONRPC client code, once downloaded from the Web
Server's root and executed in the user's browser, can be instructed to access /test/.
Once again, it is essential to remember the URI where the JSONRPC service will be accessible, as the
Pyjamas JSONRPC client code must be told where, on the Web Server, that service can be found.
Lastly: the Wiki example https://wiki.ubuntu.com/txJSON-RPC/TwistedWeb2 has been copied near
verbatim, and so it is missing the functions reverse, uppercase and lowercase. It is left as a simple
exercise to the reader to add these missing functions.

Conclusion on Python JSONRPC Services

13 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

The list covered here, of available Python Frameworks that can be used to run JSONRPC Services, is
by no means complete, but it should be clear that it's not exactly rocket science. If your preferred
framework does not support JSONRPC (yet), then start by examining the difference between the Django
and the Web.py ports of pimentech's network library: very little change to the library was required, to
adapt it to Web.py.
Additional resources can easily be found using a google query of "Python JSONRPC Server" and
this brings up two additional options not covered here, as well as some other useful links:
http://www.freenet.org.nz/dojo/pyjson/
http://pypi.python.org/pypi/z3c.jsonrpc/
http://json-rpc.org
What has been covered in this section is as follows:
How incredibly simple it is to make a Python JSONRPC Service
How to do CGI-based, Django, Web.py and Twisted Matrix JSONRPC
How to specify, in each case, where the service is to be accessed, by web clients.
It's often the case - bizarrely - that there will be more lines of python code to create the front-end
Pyjamas application than there will be in the Web Server back-end that manages the information in the
database! This is a testament to the power of the available Python frameworks as much as it is to the
level of complexity of Desktop-like applications development.
When such amazingly powerful technology is available, exactly how and why the development of
comprehensive web applications are still done using server-side HTML-based technology is somewhat
puzzling.

Testing the JSONRPC services, using Python-based JSONRPC


clients
Before progressing to doing JSONRPC in a browser, it's probably a good idea to double-check that the
example Server of choice actually works. Python-based JSONRPC clients are surprisingly difficult to
find, perhaps because they are so simple to write that people forgot to do it. So here are some links to
the clients that do exist (and are easy to find):
http://developer.spikesource.com/wiki/index.php/Article:Accessing_JSON-RPC_with_Python
https://wiki.ubuntu.com/txJSON-RPC/TwistedWeb2
http://pypi.python.org/pypi/z3c.jsonrpc/

Simple JSONRPC client


First, download a very simple client, written by Matt Harrison. Copies are available here:
http://files.blog-city.com/files/F05/96843/b/jsonrpclib.tar.gz
http://lkcl.net/jsonrpclib.tgz
Then, create the following file, saving it as jsonrpctest.py:

import jsonrpclib

s = jsonrpclib.ServerProxy("http://localhost:7080/test/")
reply = s.echo("foo bar")
print reply

As can be seen, it really couldn't get any easier, and the code is good enough to help prove that there's
actually something there.

Remember to adapt the URL to point to the JSONRPC Server that you want to test!
The example shows how to test the txJSONRPC server: if you want to test the
Django example, you will need to specify a URL of http://localhost/test-service/
or wherever it was that you edited your Django app's urls.py to answer from.

Run the script and test it:

$ python jsonrpc_test.py
{u'error': None, u'id': 2, u'result': u'foo bar'}

14 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

Whilst this isn't the most user-friendly of answers, it's definitely the right one, and it's better than this:

NameError: global name 'ProtocolError' is not defined

which is what you will encounter if you used the wrong URL. Double-check that you have set the URL
correctly, in whatever framework you have chosen. In the case of connecting to an Apache2-driven CGI
script, first make sure that the script is executable, by typing the URL directly into your browser, for
example, if you have followed verbatim the advised setup, then the Apache2 Directive that makes
"/services/" part of your Apache2 server's CGI-based infrastructure, you can type
http://localhost/services/EchoService.py into your browser, and you should get a blank
screen (rather than downloading EchoService.py as a static file...)
Once you have confirmed that the cgi-bin script is executed, modify jsonrpc_test.py to make the
argument passed to ServerProxy point to the cgi script:

s = jsonrpclib.ServerProxy("http://localhost/services/EchoService.py")

Again, u'foo bar' should be printed out, thus confirming that everything is hunky-dory.

Simple txJSONRPC client


Create a file, called client.py, with the following contents, remembering to edit it to suit the JSONRPC
service to be tested:

from twisted.internet import reactor

from txjsonrpc.web.jsonrpc import Proxy

def printValue(value):
print repr(value)
reactor.stop()

def printError(error):
print 'error', error
reactor.stop()

proxy = Proxy('http://127.0.0.1:7080/test/')
proxy.callRemote('add', 3, 5).addCallbacks(printValue, printError)
reactor.run()

Again, as with the previous example, note that the URL is set in this instance to point to the txJSONRPC
server.tac Service, and, if testing against Django, for example, the URL should be set to
http://127.0.0.1/test-service/.
When run, using python client.py, the number 8 should be printed. If you would like to test the echo
function, use this, instead:

proxy.callRemote('echo', 'hello').addCallbacks(printValue, printError)

This will print out "hello".


It has to be pointed out that, when compared to the best RPC systems, this example's syntax is pretty
user-hostile - obtuse at best. However, that's not what matters, here: the point is that it can be used to
prove that the servers actually work - to provide an independent test mechanism in the future, should it
prove necessary to track down the source of errors in the development of Pyjamas JSONRPC
applications.

Purpose of using independent JSONRPC clients


So there are at least two, possibly even four independent JSONRPC clients available, all written in native
Python, that can be used, and two of them are demonstrated here. The purpose of performing
independent testing is firstly to confirm that the JSONRPC server works, but also, during the
development of your application, errors may be encountered that would otherwise be difficult to track
down.

15 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

Eliminating one source of errors from the equation is always a good idea. Using a Web browser, which
will be difficult to control and perform automated test validations with, is not something that you should
consider. Whereas, although obtuse, the use of the above python clients will allow you at least to run
automated tests on your JSONRPC service from the command-line.

Pyjamas JSONRPCExample
Finally - after all that fussing around - there's a service (at least one) against which we can demonstrate
that the Pyjamas jsonrpc example actually works. Perhaps importantly, it stands a chance of working
against the Web Framework of choice. If the php JSONRPC example is sufficient, download, install and
configure php5, and configure Apache2 or other Web Server to use it. There is plenty of information on
how to set up Apache2 and php5, by searching online for "LAMP HOWTO" advice.
First, compile the jsonrpc pyjamas example, by changing directory to examples/jsonrpc/ and either
running build.sh or executing the following command:

python ../../builder/build.py JSONRPCExample.py

Then, point a browser at http://localhost/examples/jsonrpc/output


/JSONRPCExample.html and you should have some instructions to follow, a text box, a drop-down
and two buttons. If you have correctly configured php, then clicking on the "Send to PHP Service" button
should result in the words {'Test'} [\"String\"] appearing below the button. If not, an obtuse
error message will be displayed.

If you get an obtuse error message, then it is with very little regret,
and quite a bit of glee, to mention that the author cares not one bit
about PHP, and is not inclined to spend time encouraging readers, who
are likely to be Python Programmers, to use it. So, if an obtuse
error message is encountered, great!. Ignore it, and move on...

Once the php test is confirmed as working, or if php causes much boredom or irritation beyond belief, it's
worthwhile moving swiftly on to testing the python Echo Service. For this to work, the
JSONRPCExample.py must be pointing to the correct URL, as has been emphasised in the sections on
setting up a JSONRPC Service. Edit the file JSONRPCExample.py in the examples/jsonrpc/ directory,
and change the last few lines:

class EchoServicePHP(JSONProxy):
def __init__(self):
# ignoring the fact that it says PHP, not Twisted Matrix,
# this example works against the txJSONRPC Example server:
JSONProxy.__init__(self, "/test/",
["echo",
"reverse",
"uppercase",
"lowercase"])

class EchoServicePython(JSONProxy):
def __init__(self):
# this example works against the Django JSONRPC Example:
JSONProxy.__init__(self, "/test-service/",
["echo",
"reverse",
"uppercase",
"lowercase"])

Remember to recompile, after saving, using build.sh. As can be seen, the critical key is the
JSONProxy's first parameter (yes, it's the first parameter - ignoring "self") which has to be changed to
match the location where the JSONRPC service will answer function calls from. However - there's a
catch, which needs explaining, and the reason why the txJSONRPC Server example was modified will
become clear.

Getting Pyjamas AJAX clients and txJSONRPC Services "lined up"

16 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

Recall that AJAX is restricted, for security reasons, to allowing access only to the same server, as
described at the beginning of this chapter. The implications are that the Web Framework must be
configured to serve both the AJAX application's HTML and Javascript from the same root URL that the
JSONRPC server - and all other AJAX responses - come from.
This is the reason why this was done, in the txJSONRPC example:

root = static.File("/var/www/")
root.putChild("doc", static.File("/usr/share/doc"))
root.putChild("test", Example())
site = server.Site(root)

In this instance, the compiled output would have to be moved into either /var/www/ or into
/usr/share/doc/, neither of which are really acceptable, so it is better to modify the server.tac
example to this:

root = static.File("home/lkcl/src/pyjamas/examples/jsonrpc/output/")
root.putChild("test", Example())
site = server.Site(root)

Remember to substitute the appropriate location where the jsonrpc compiled output can be accessed. By
making the static files be the root of the server.tac example, the compiled output of the
JSONRPCExample can be accessed by browsing to http://localhost:7080/. And, as can be
seen, there is also a /test/ URL which points to the Example txJSONRPC Service that now matches
up with the modifications made in the Pyjamas client source code.

Getting Pyjamas AJAX clients and Django JSONRPC Services "lined up"
In the Django case, it's probably best to not bother to modify the Django example app to load static HTML
pages, but to go straight to running Django as a mod_python Apache2 module. Install and configure
mod_python, if it isn't already included as part of the standard Apache2 distribution, and add the following
lines to the configuration:

<Location "/test-service/">
SetHandler python-program
PythonHandler django.core.handlers.modpython
# change mysite to point to the correct Django app settings
SetEnv DJANGO_SETTINGS_MODULE mysite.settings
# change the path to point to the directory with mysite in it.
PythonPath "['/home/lkcl/src/django_pyjamas/'] + sys.path"
PythonDebug On
</Location>

Edit the directory of the PythonPath as appropriate, to point to the directory where the Django application
was created. Restart (or better, just reload) Apache2, and this is all that should be needed, in
combination with prior edits to the Apache2 configuration (the /examples/ Directive), to get the
JSONRPCExample to work.

Do not add the path of the Django application itself to the PythonPath!
If the Django application is called "mysite", do not add "/path/src/mysite",
just add "/path/src". Also, remember to modify the DJANGO_SETTINGS_MODULE
to point to "mysite" only if the Django application is called "mysite",
and to modify it, otherwise.

When using mod_python, it's important to remember that any errors in the Django application will cause
the Apache2 thread that served the page to "stick", and Apache2 will need to be restarted, to get rid of it.
Also, when using mod_python, running the built-in Django Test server (python ./manage.py
runserver 8000) is no longer necessary.

Pyjamas AJAX clients talking to Web.py


Whilst web.py automatically supports access to static content in subdirectories, if slightly more strict

17 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

control over the content is required, the following class can be used. It's based on the Images class that
can be found at http://webpy.org/images, with the additional mime types of HTML, Javascript, Text and
CSS being added. Add this to the code.py example, created earlier:

urls = (
'/chat/', 'chatcls',
'/chat', 'chatcls',
'/(.*)', 'static' # static content
)

class static:
def GET(self, name):

if name == '':
name = 'JSONRPCExample.html'

ext = name.split(".")[-1]

cType = {
"js":"application/x-javascript",
"txt":"text/plain",
"css":"text/css",
"html":"text/html",
"png":"images/png",
"jpg":"image/jpeg",
"gif":"image/gif",
"ico":"image/x-icon" }

static_location = '/home/lkcl/src/pyjamas/examples/jsonrpc/output'
if name in os.listdir(static_location):
web.header("Content-Type", cType[ext])
print open('%s/%s' % (static_location, name), "rb").read()
else:
web.notfound()

Note that the "static" class has been added to urls, right at the top, to deal with all URLs (except
/chat/). Also, it will be necessary to modify the location from where the static content is loaded from.
Also, for convenience, if no name is given, the content from JSONRPCExample.html is served. This
allows users to type in http://localhost:8080 once the Web.py application is running, instead of
http://localhost:8080/JSONRPCExample.html.
Also, either modify the urls to match up with the existing locations that the Pyjamas JSONRPCService.py
has been modified to support (/test/ or /test-service/), or edit JSONRPCService.py and
change the URI that JSONProxy talks to:

class EchoServicePython(JSONProxy):
def __init__(self):
# this example works against the Web.py Example:
JSONProxy.__init__(self, "/chat/",
["echo",
"reverse",
"uppercase",
"lowercase"])

Remember to save and recompile the Pyjamas application. Also, once the modifications to code.py
above have been saved, re-run the Web.py app, with the command python code.py, and open
http://localhost:8080 in a Web browser.

What's been covered


We've gone over how to use AJAX, in both static (straight loading of text files) and dynamic (using
JSONRPC) fashions. The dynamic examples show how to use four different kinds of Web frameworks,
all of which are very similar to one another. We've shown how to use python-based JSONRPC clients,
which can be used as the basis of test frameworks for JSONRPC servers.
In each case, with each of the Web Frameworks, the following has been emphasised and covered:

18 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

The importance of matching up the URL in the Pyjamas application's JSONRPC Proxy with the
location where the chosen Framework serves its JSONRPC Service from.
The importance of pandering to the restrictions on AJAX, for security reasons
The ways in which the Framework serves the static HTML and Javascript content from one
location and the JSONRPC Service responses from another, but they must both come from the
same Root URL (due to AJAX security restrictions).
It's important to get this bit right, and well-understood, as it's the fundamental basis of the interaction
between Pyjamas applications and the rest of the world - the Web Server.

Merging Javascript into Python Pyjamas applications


Pyjamas is compiler technology: it converts python source code into javascript source code. In a way,
pyjs.py - the compiler - is very much like gcc, the Gnu C Compiler. gcc turns perfectly good (or bad) c
code into a human-unreadable mess, using an intermediate step: assembler. The Pyjamas compiler
effectively turns python source code into "assembly-like" javascript. Another way to put this is that it is
pure coincidence (it's not, really!) that the "assembly output" of pyjamas happens to be reasonably-
human-readable javascript.
There is quite a lot of javascript functionality which needs to be accessible by Pyjamas applications, in
order for Pyjamas to be useful (especially in Web Browsers), and so the design of the Pyjamas compiler
has had to include a way to understand javascript libraries, and to be able to include javascript code
fragments into the output, unmodified. The reasons are simple: not only would it be too much to expect
developers to rewrite perfectly good javascript libraries in python, but also there are built-in functions - in
both the Javascript language and in Web browsers - that simply cannot be rewritten. A way to interact
with javascript is therefore essential.
Being able to insert javascript fragments is conceptually identical to the gcc method of doing "inline
assembler". In gcc, you do horrible things like this:

int a=10, b;
asm ("movl %1, %%eax;
movl %%eax, %0;"
:"=r"(b) /* output */
:"r"(a) /* input */
:"%eax" /* clobbered register */
);

And, in Pyjamas, it's not that different:

def foo(b): # this is python-land (to be translated to javascript)


a = 10; # this is still python-land (to be translated to javascript)
JS("""
b = a; /* this is javascript-land (and will be outputted, verbatim) */
""")

So, crucially, this chapter will cover what it is that needs careful attention, when wrapping a javascript
library in Python code; how Python functions should be called from inside wrapped javascript (and the
problems associated with doing that); and the importance of double-checking the compiled output against
the original Python code, to make sure that everything's reasonable.
At this point, it must be said that this is not for the faint-hearted: assembly-style programming never is.
Not only is it necessary to know two programming languages; not only is it necessary to know how the
compiler interfaces the two together, but also, it's necessary to work at "second hand" with one of the
languages, when debugging the application! That having been said, it is quite straightforward.

Simple Rectangle Test Class


The purpose of this exercise is to show, using as small a useful javascript library as possible, how to
wrap Javascript with Pyjamas. Start by saving the following javascript code as a file called jsrecttest.js as
the javascript "library":

function rectobj() {

19 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

function rect_init(x, y) {
this.x = x;
this.y = y;
}

function rect_area() {
return this.x * this.y;
}

function rect_add(rect) {
this.x += rect.x;
this.y += rect.y;
}

rectobj.prototype.area = rect_area;
rectobj.prototype.add = rect_add;
rectobj.prototype.init = rect_init;

If javascript is unfamiliar, then right now take a minute to look up prototyping, and how javascript objects
work. An object instance can be created in javascript, like this:

var obj = new rectobj();

It's quite obtuse, but easy to follow: the function rectobj() is being called, but the qualifier "new" tells
javascript to actually make an object (using the function rectobj), and thus, any occurrences of the word
"this" can be used in the function - just like "self" in python and "this" in c++ - to refer to member
variables, functions and objects. So, the function rect_init() adds two variables to a rectobj;
rect_area can return those two variables, multiplied together.
Crucially, however, the last three lines are what makes javascript incredibly powerful. Notice the use of
"prototype" - that's telling the javascript engine that all future declarations of "new" rectobjs must have
those three functions added to them.
Exactly the same thing can be done in Python - it's just that it's not good sane practice to modify Python
classes on-the-fly, as anyone reading the Python source code is going to get a real headache,
wondering where on earth the extra functions are coming from. However, javascript is a pure prototyping
language; there are no "classes"; there is only prototype. Given that there isn't any choice in the matter
(javascript programmers being used to getting headaches), everybody's happy.

It's worth noting that the reason why Python and Javascript work so well
together is because of the dynamic run-time similarity - this ability to
create classes in Python, by modifying the object, and adding functions
at runtime, being so similar to javascript's prototype capabilities.
By contrast, The PyPy team (PyPy is an E.U-funded compiler project)
are having a very difficult time with the Java Bytecode back-end,
because Java, as a language, is simply not equipped with such
dynamism as Python and Javascript. Consequently, Java Bytecode -
and the JVM it runs on - does not react favourably to such maltreatment.

Slight Detour, illustrating python-javascript equivalence


Whilst it would be unkind to create an exact replica of what's going on with jsrecttest.js, an
"equivalent" program in Python is here, for illustrative purposes only:

class rectobj:
def __init__(self, x, y):
self.x = x
self.y = y
def area(self):
return self.x * self.y
def add(self, rect):
self.x += rect.x
self.y += rect.y

20 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

The reason why this isn't exactly the same is because, in jsrecttest.js the javascript "class" (not
that there is such a concept of classes in javascript) is being created dynamically. So it's more like this:

class rectobj:
pass
def rect_area(self):
return self.x * self.y
def rect_add(self, rect):
self.x += rect.x
self.y += rect.y
def rect_init(self, x, y):
self.x = x
self.y = y
rectobj.__init__ = rect_init
rectobj.add = rect_add
rectobj.area = rect_area

Python purists should either be screaming or quaking in their boots at the incorrectness of this approach.
The particularly well-informed ones will likely scream loudest about the lack of decorators. All of which is
irrelevant, because only the most insane or the most confident of Python programmers would dare do
anything like this. But - it's here for illustrative purposes.

Consider compiling the above examples to javascript, using this command:


python pyjs.py horribletest.py
Compare the resultant output with the original jsrecttest.js, to get
a feel for how pyjs.py works and also to gain confidence in its
capabilities. If the output from pyjs.py looks close to what would
be written in "pure" javascript, it makes life a lot easier when it
comes to understanding and writing Python-Javascript hybrids.

Wrapping javascript, inline, with JS()


Moving swiftly on, create a file, TestRect.py, with the following contents:

import jsrecttest.js # YUK!!!

class Rect:
def __init__(self, x, y):
JS("""
this.rect = new rectobj();
this.rect.init(x, y);
""")

def add(self, r):


JS("""
this.rect.add(r.rect);
""")
def area(self):
JS("""
return this.rect.area();
""")

def get_x(self):
JS("""
return this.rect.x;
""")

def get_y(self):
JS("""
return this.rect.y;
""")

Straight away, notice two critical things: firstly, importing jsrecttest.js with an extension ".js" is definitely
not standard python - but then again, the whole concept of compiling python into javascript throws
spanners in the traditional works, anyway. Secondly: in this strange-looking Python class, whilst every
function uses "self", the javascript snippets use "this".

21 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

"self" in python is just a convention. If it feels more comfortable,


substitute all occurences of the word "self" with the word "this" -
consistently, in a python class member function - and the feeling of
panic and unease should subside, only to be replaced by a different form
of unease at breaking a python convention: the disappearance of the word
"self" from the python code...

The inclusion of jsrecttest.js as if it was a python module is definitely non-pythonic, but due to the way
that pyjamas code is generated, and to ensure that there are no surprises, there really isn't much choice.
Browsers have a habit of executing code when it is loaded - but only when the code is included from the
initial web page. If javascript is used, to modify the URL of the browser page, to load up a second HTML
page, things start to go horribly wrong. So, the only way to guarantee that no limitations are imposed is to
actually include the script wholesale and inline, in the Pyjamas compiled output!
Examining the class Rect, it can be seen that the initialisation appears - somehow - to magically go into
javascript-land and create a rectobj() as a member variable, and that the rectobj instance gets
initialised with the same x and y parameters that the Python Rect class gets initialised with! It's the same
story, throughout all of the functions, and at this point, it's a very very good idea to run the pyjs.py
compiler, to find out what's going on. Run the following command (assuming that you saved the files in a
subdirectory of examples, or are executing the command from the pyjamas/examples/jsobject directory):

python ../../pyjs/pyjs.py TestRect.py

The output from pyjs.py is printed on-screen, and, an abbreviated version is shown, here:

function __Rect() {
}
function Rect(x, y) {
var instance = new __Rect();
if(instance.__init__) instance.__init__.apply(instance, arguments);
return instance;
}

function __Rect_initialize() {
if(__Rect.__was_initialized__) return;
__Rect.__was_initialized__ = true;
pyjs_extend(__Rect, __pyjslib_Object);
__Rect.prototype.__class__.__new__ = Rect;
__Rect.prototype.__init__ = function(x, y) {

this.rect = new rectobj();


this.rect.init(x, y);

};
__Rect.prototype.get_x = function() {

return this.rect.x;

};

/* ...
functions cut, for clarity and brevity, are area, get_y and add.
as well as a boat-load of non-intuitive crud.
...
*/
}
__Rect_initialize();

To explain the morass of extra code: it's to create some semblance and imitation of pythonic classes, in
a language which doesn't have classes. Note in particular the use of the function pyjs_extend, which
emulates class inheritance; the technique is prevalent in javascript libraries, to "extend" the functionality
of an object by copying its methods into the child "class".
One thing that deserves particular attention is this line:

22 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

__Rect.prototype.__init__ = function(x, y) {

This line sets up a "class" initialiser function, called __init__, which has two parameters, x and y. Just like
in c++, the concept of "this" is implicit in javascript: you always unavoidably get one, and so it need not be
passed around as a parameter. Note also that the contents of the __init__ function are - literally
verbatim - what was placed inside the JS(""" """) inlining wrapper.
Regarding the remaining functions, and to illustrate in a clearer manner the relationship between Python
and its resultant javascript output when compiled, change the get_x() function, as follows:

def get_x(self):
return self.rect.x

Re-run pyjs,py and look for the following lines, in the output:

__Rect.prototype.get_x = function() {
return this.rect.x;
};

That's... that's the same, isn't it? Yes and no: there's more white-space in the previous example, and the
reason for that is because, inside the original JS(""" return this.rect.x; """) block there was... a lot of
white-space, all of which was substituted, verbatim, into the compiled output (go back to the previous
code, and either remove all white-space, add in javascript-style comments, or remove the indentation,
and re-run pyjs.py, to help emphasise this point).

Note! Javascript separates statements with semi-colons!


Python does actually support semi-colons to separate statements,
but typical well-written python applications don't try to cram
vast numbers of statements onto one line, so the semi-colons are
unnecessary. semi-colons are usually used in python line-cramming
competitions, such as writing a fully-functional wiki server in
11 lines of code...

In other words, what's happened is that the auto-generated code happens to be identical to the code that
we inlined. pyjs.py happens to "understand" how to turn python "self" into javascript "this", and the syntax
of javascript happens to be similar to python, in this simple example. So, purely for the purposes of this
exercise, unnecessary work was done by putting "inline" the very javascript that Pyjamas would have
generated, if the compiler was trusted to do its job. Try the same thing with the area() function:

def area(self):
return self.rect.area()

Re-run pyjs.py TestRect.py, and, in the output, you will see this:

__Rect.prototype.area = function() {
return this.rect.area();
};

Gosh. again, that's identical to the javascript substituted inline. how funny.

Running the example in a Web Browser


Whilst it's left to the reader as an experimental exercise to adapt and run this code in, for example,
Mozilla's spidermonkey javascript interpreter, it's quicker to test this code from inside a web browser.
Add the following lines to TestRect.py:

from pyjamas.ui import RootPanel, TextBox, HTML, Button

class TestRect:

23 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

def onModuleLoad(self):

self.r = Rect(0.0, 0.0)

self.xbox = TextBox()
self.ybox = TextBox()
self.addbutton = Button("Click to add x and y to Rectangle")
self.addbutton.addClickListener(self)

self.xbox.setText("2")
self.ybox.setText("5")

RootPanel().add(HTML("X Value:"))
RootPanel().add(self.xbox)
RootPanel().add(HTML("Y Value:"))
RootPanel().add(self.ybox)
RootPanel().add(self.addbutton)

RootPanel().add(HTML("Current value: %d %d" % ( self.r.get_x(), self.r.get_y(

def onClick(self, sender):

x = int(self.xbox.getText())
y = int(self.ybox.getText())

r = Rect(x, y)

self.r.add(r)

RootPanel().add(HTML("New value: %d %d" % ( self.r.get_x(), self.r.get_y())))


RootPanel().add(HTML("New Area: %d" % self.r.area()))

The application isn't designed to be pretty, it's designed to be functional. Create a file,
public/TestRect.html, with the following contents:

<html>
<head>
<meta name="pygwt:module" content="TestRect">
<title>Test Rectangle Javascript Object</title>
</head>
<body bgcolor="white">
<script language="javascript" src="pygwt.js"></script>
</body>
</html>

Compile the application, as follows:

python ../../builder/build.py TestRect.py

Browse to the examples/jsobject/output/ directory (or wherever the application is being built) and click on
output/TestRect.html

If using Pyjamas 0.3, the sprintf routine is limited to one % modifier


per specifier string. So, whilst "New Area: %d" % 20 is acceptable,
"New value: %d %d" % (5, 4) is not. Adapt the application as follows:
RootPanel().add(HTML("New X value: %d" % self.r.get_x()))
RootPanel().add(HTML("New Y value: %d" % self.r.get_y()))
and likewise for the display of "Current value". Alternatively,
upgrade to a new version of Pyjamas.

As can be seen, the application sets up the Button to call an onClick function, which reads the
text-input boxes, creates a Rect() and adds it to the instance self.r. self.r, another instance of Rect(),
accumulates values added; the new x and y values are printed out, as is the area of the rectangle.
Remember, also: look at the output - for example TestRect.Mozilla.cache.html - and look for both the
rectobj and the use of the Rect object, comparing it to the original javascript and the original Python code.

24 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

Recap
What's been illustrated in this simple example is as follows:
How to import javascript as if it was python (import jsfile.js)
How to use the inline-assembly-like function JS() to put fragments of javascript directly into the
compiler output, unmodified.
How to call javascript functions and access javascript objects as if they were Python - and how
simple it is.
The importance of examining the compiler's output, counting blessings that javascript is human-
readable and that pyjs.py generates human-readable output
The fundamental similarity between python and javascript - their ability to modify objects at run-time
- which makes the languages a much better match (than java, for example).
So, whilst it's necessary to have an understanding of both Javascript and Python, as well as how the
Pyjamas compiler works, the key to successfully wrapping javascript is definitely to keep an eye on the
output from the compiler, at every step of the way. One thing not covered, so far, however, is how to call
Python functions from inside pure javascript. Correction: how to make it look like python functions are
being called, given that the output is entirely javascript, in the end...

Calling Python from Javascript


It's a lie! What's actually needed is to understand how the Pyjamas compiler translates python functions,
especially those in classes and modules, into javascript. Understanding the translation is important,
because when no longer relying on pyjs.py to do the job automatically, it's clearly necessary to write,
inside the JS() block, exactly the same code that would have been generated.
So it's not like python is actually being called, from javascript - it just looks that way.

If the thought of how not in Kansas this all is, take a little time
for a Zen three-heels-clicking moment until the dizziness passes.

Once calm and zen-like peace has been achieved, create a file called jsdicttest.js, as follows:

function dictobj() {
}

function dict_init(d) {
this.d = d;
}

function dict_get_value(key) {
return this.d.__getitem__(key)
}

dictobj.prototype.get_value = dict_get_value;
dictobj.prototype.init = dict_init;

Next, create a file called TestDict.py with the following contents:

import jsdicttest.js # YUK!!!

class WrapperDict:
def __init__(self):
d = {'hello': 'world',
'goodbye': 2}
JS("""
this.dict = new dictobj();
this.dict.init(d);
""")

def python_get_value(self, key):


return self.dict.d[key]

def javascript_get_value(self, key):

25 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

JS("""
return this.dict.get_value(key);
""")

Notice that the jsdicttest.js file is being imported, verbatim, in that all-important and completely
non-pythonic fashion; notice also that, in the class initialisation, the local variable, d, is being passed in to
the inline-assembler-like JS() function.
Run pyjs.py, as follows, to compile the code to javascript

python ../../pyjs/pyjs.py TestDict.py

Examine the output. and look for this particular section:

__WrapperDict.prototype.__init__ = function() {
var d = new pyjslib_Dict([['hello', 'world'], ['goodbye', 2]]);

this.dict = new dictobj();


this.dict.init(d);

};

Comparing this carefully against the original javascript, notice how the dictionary has been declared, and
how the input data has been turned into a javascript array. pyjslib_Dict is the name of the function
that is created when the Dict class, in the pyjamas library called "pyjslib.py", is compiled to javascript.

When you run build.py, the location of the Pyjamas library is automatically
added to the module import path, for convenience of building Pyjamas Web
applications. Take a moment to look at builder/build.py's source code,
looking for the word "library". build.py uses pyjs.py but does quite a
bit more work - specifically - in preparing a web application - whereas
pyjs.py is definitely just a straight compiler.

Understanding how pyjs.py treats builtins such as Dict, List, Tuple and String is crucial, if wrapping
javascript, as is understanding how modules are imported. Take a look at library/pyjslib.py, looking for the
class Dict. In particular, it may be illustrative to compare the "builtin" pyjamas implementation of Dict to
UserDict.py which comes as standard as part of the Python 2 distribution. Note that, apart from
getObject(), which has been added for convenience, the function names in pyjslib.py's Dict class,
and those in UserDict.py, are identical.
The implementation of Dict, in pyjslib.py, however, can be seen to be using standard javascript arrays to
"emulate" the expected functionality and behaviour that Python programmers would expect a Dict object
to have. On the basis that it's unreasonable to expect python programmers to have to"get involved" with
javascript builtin objects, pyjslib.py takes care of the differences, making javascript builtin objects look like
Python builtins. So, looking further at the output from compiling TestDict.py, the following code fragment
should come as no surprise:

__WrapperDict.prototype.python_get_value = function(key) {
return this.dict.d.__getitem__(key);
};

Accessing the dictionary's items, by key, in the python_get_value() function in the original python code,
TestDict.py, has been compiled into a call to __getitem__. This is exactly what happens in standard
Python, as can be clearly seen with a simple test, using the UserDict.py module that comes with the
standard python distribution, so it should come as no surprise that Pyjamas is emulating this behaviour,
for convenience.

Illustrating how to "call" Python functions from javascript


Take the line which was generated by the compiler, above, in declaring the variable d in the WrapperDict
constructor, and adapt it to make changes to jstestdict.js, as follows:

function dict_init(d) {

26 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

var u = new pyjslib_Dict([['goodbye', 'cruel world']]);


this.d = d;
d.update(u);
}

What is expected to happen, here, is that the value referenced by the key 'goodbye' will overwritten, from
its expected value, before - the number '2' - with the string 'cruel world'. However, it was done inside the
javascript, by manually declaring a built-in Dict object, using the auto-generated pyjslib_Dict()
function.

Take special note of this "trick". If looking to achieve something in


a Javascript snippet, first write the code that you want, in Python.
Then, run the Python code through pyjs.py (or build.py, as appropriate),
and literally cut-and-paste - and adapt - the resultant output.
There's no point in making life difficult, when there is an automated
tool that accurately does the job, already.

To illustrate this, again, add another function to WrapperDict, which gets the length of the dictionary:

def python_dict_length(self):
return len(self.dict)

Again, len() is a built-in, that is implemented in pyjslib.py, and so it should come as no surprise to find,
when compiling TestDict.py by running pyjs.py TestDict.py, that the output contains this:

__WrapperDict.prototype.python_dict_length = function() {
return pyjslib_len(this.dict);
};

So, again, it has been illustrated that if it is required to find the length of a pyjslib.py-implemented Python
Dict object from inside Javascript, pyjslib_len must be used. All that's happening here is that the
name of the module is separated from the function name by an underscore. This same trick applies to
absolutely any python modules that are imported into an application. It's just that, for recognised built-in
functions such as len, map, filter and getattr, pyjs.py automatically recognises those and prepends the
module name "pyjslib". Any other functions, including not only in the modules that come with Pyjamas but
also developer-written code, must be imported as would normally be done, in Python, and the javascript
generated will be of the form "importedmodulename_functionname" or
"importedmodulename_classname".

Warning! This trick - of separating the module name from the function name -
is something that was chosen for the convenience of the Pyjamas compiler,
in order to emulate in javascript, as best as possible, the class and
variable scope rules of python. At some point, a better mechanism may be
deployed, and, as a result, it's possible that all use of this trick may
be utterly broken - at least, definitely version-dependent.
In fact, in the llpamies branch of Pyjamas, it already is broken!
The module-function calling conventions in the llpamies branch have
been completely changed, but the work was an experiment that has not
yet been completed.

Running TestDict in a Browser


To run TestDict in a Web browser, add the following lines to TestDict.py:

from pyjamas.ui import RootPanel, TextBox, HTML, Button

class TestDict:

def onModuleLoad(self):

self.r = WrapperDict()

27 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

self.kbox = TextBox()
self.addbutton = Button("Click to look up key value (hello or goodbye)")
self.addbutton.addClickListener(self)

self.kbox.setText("hello") # default to make life easier

RootPanel().add(HTML("Key:"))
RootPanel().add(self.kbox)
RootPanel().add(self.addbutton)

def display_value(self):

key = self.kbox.getText()

RootPanel().add(HTML("Value using python:" ))


RootPanel().add(HTML(self.r.python_get_value(key)))
RootPanel().add(HTML("Value using javascript:" ))
RootPanel().add(HTML(self.r.javascript_get_value(key)))

def onClick(self, sender):

self.display_value()

Next, create a file, public/TestDict.html, with the following contents:

<html>
<head>
<meta name="pygwt:module" content="TestDict">
<title>Test Dictionary Javascript Object</title>
</head>
<body bgcolor="white">
<script language="javascript" src="pygwt.js"></script>
</body>
</html>

After saving the modifications to TestDict.py, and saving public/TestDict.html, compile the application to
javascript, using build.py:

python ../../builder/build.py TestDict.py

Browse to the examples/jsobject/output/ directory (or wherever the application is being built) and click on
output/TestDict.html. The application will, when the button is pressed, look up the key "hello" and should
output "world". If the value in the input box is changed to "goodbye", then, in the first example, the value
"2" is outputted - from both the python_get_value() and the javascript_get_value() functions. However, if
the latter part of the exercise was followed, by calling update() from inside jstestdict.js, then the resultant
output will instead be "cruel world".
Again, just as with the TestRect.py example, remember to look at the compiled output,
TestDict.Mozilla.cache.html for example, looking for WrapperDict and its use, and looking for dictobj, and
how that's used.

If an error appears, "WrapperDict is undefined" in the Javascript console of


the browser, check that the contents of TestDict.py haven't been overwritten:
"import jstestdict.js", and the declaration of the WrapperDict class, still
need to be included in TestDict.py!

What's been covered


This section, on pretending to call Python from inside Javascript, covered the following:
How it's a lie that python is being "called" from javascript, and how it's actually the other way round:
that the python is being translated so that the inline javascript, in JS(), can be made to match up
with the compiled python code.
How to "trick"-compile code into inline javascript, by writing the code first in Python, compiling it to
javascript and then copying that into the JS() block, to save time and effort.
How a class or function in a Python module is turned into a javascript function by the compiler

28 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

(separating the two with an underscore is the current convention, as of Pyjamas 0.3).
How the builtin classes and functions automatically have the module name "pyjslib_" prepended, in
the compiled javascript.
Where to look for pyjslib.py (in the libraries/ directory);
How pyjamas implements Python built-ins, by wrapping and mapping to javascript built-in classes
and functions;
The importance - once again - of carefully examining the compiled output from pyjs.py and build.py,
to assist in the development of javascript wrappers.
Remembering that python doesn't typically use semi-colons (although it's supported) to separate
statements but that in javascript, the use of semi-colons is essential.

Conclusion
This quite comprehensive chapter emphasises that, whilst it's necessary to not only know both Python
and Javascript but also to know how the pyjs.py compiler turns python into javascript, it's not rocket
science to integrate javascript into Pyjamas applications. The same type of techniques and care that are
required, when writing inline assembler in a c or c++ program for example, are also required, here.
Yet, compared to writing inline assembler in c or c++, the task of integrating javascript inline into Pyjamas
applications is definitely a lot easier. Not least in making the task easier is the fact that javascript itself is
a high-level language (albeit an obtuse but very well-understood one), and asm programming is definitely
a black art.

User-Interface Applications
The ui module is the most comprehensive of the Pyjamas libraries and, in many ways, is the raison d'etre
for the entire Pyjamas project. The ui module's class structure and API is near-identical to the Google
Web Toolkit ui module, and is also incredibly similar to Python-Qt4 and Python-Gtk2. As such, pyjamas.ui
is almost minimalist in what it provides, but is definitely comprehensive enough to build decent and
functional applications.
Covered in this reference chapter will be each of the main classes in pyjamas.ui, which, for convenience,
are broken down into two sections: Panels and Widgets. Each of the two sections' classes are listed in
alphabetical order. Each class description contains the following:
A short description of each class
A source code example
Where one is needed, a CSS stylesheet is given
The key functions are described which make each class useful
The chapter covers existing widgets in the pyjamas.ui library. A separate chapter covers how to write
widgets from scratch.

Panels
A Panel is a name given to a container, which other widgets and panels can be added to. For example,
the SimplePanel can only have one widget added to it (which isn't as daft as it sounds: some widgets
have different interaction rules with other widgets that can be mitigated by placing the widget in a
"wrapper" panel). Other more complex panels control the layout and placement of their child widgets in
interesting and wonderful ways.
The set of available Panel widgets is quite basic, yet functional. The widgets are most definitely not as
pretty or as comprehensive as, for example, those that are available at http://extjs.com, but certainly the
ui Panel widgets are the "building blocks" - the foundation on which such lovelier widgets could be built.

ui.AbsolutePanel
ui.AbsolutePanel is a panel that positions its children using absolute pixel positions. This allows the
panel's children to overlap.
Note that the AbsolutePanel does not automatically resize itself to fit its children. There is no
straightforward way of doing this unless all the children are explicitly sized; the easiest workaround is just
to call panel.setWidth(width) and panel.setHeight(height) explicitly after adding the children (or specifying
the width and height in a CSS stylesheet). Choose an appropriate width and height, based on the sizes

29 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

and locations of children that are added to the AbsolutePanel.


Source Code:

from pyjamas.ui import SimplePanel, AbsolutePanel, VerticalPanel, HTML


from pyjamas import DOM

class AbsolutePanelDemo(SimplePanel):

def __init__(self):
SimplePanel.__init__(self)

panel = AbsolutePanel()

panel.add(self.makeBox("Child 1"), 20, 10)


panel.add(self.makeBox("Child 2"), 30, 30)

panel.setWidth("100%")
panel.setHeight("100px")

self.add(panel)

def makeBox(self, label):


wrapper = VerticalPanel()
wrapper.setBorderWidth(1)
wrapper.add(HTML(label))
DOM.setIntAttribute(wrapper.getTable(), "cellPadding", 10)
DOM.setAttribute(wrapper.getTable(), "bgColor", "#C3D9FF")

return wrapper

ui.DialogBox
The ui.DialogBox class implements a panel that behaves like a dialog box.
A dialog box has an optional caption, and a widget which is displayed as the main part of the dialog box.
The user can drag the dialog box around by clicking on the caption.
The DialogBox class makes use of stylesheet definitions; if these are not supplied, the dialog box will
look very strange. The following stylesheet definitions are used by the example shown below:

.gwt-DialogBox {
border: 2px outset;
background-color: white;
}

.gwt-DialogBox .Caption {
background-color: #C3D9FF;
padding: 3px;
margin: 2px;
font-weight: bold;
cursor: default;
}

.gwt-DialogBox .Contents {
padding: 10px;
}

As the DialogBox class is derived from PopupPanel, the user should be able to click outside the dialog
box to close it. However, because of a problem with Firefox 3, this does not work. To get around this, the
example shown below implements a "Close" button the user can click on.
Source Code:

from pyjamas.ui import SimplePanel, DialogBox, VerticalPanel, HTML, Button


from pyjamas import Window

class DialogBoxDemo(SimplePanel):

30 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

def __init__(self):
SimplePanel.__init__(self)

self.add(Button("Show Dialog", getattr(self, "showDialog")))

def showDialog(self):
contents = VerticalPanel()
contents.setSpacing(4)
contents.add(HTML('You can place anything in a dialog box. Even drivel.'))
contents.add(Button("Close", getattr(self, "onClose")))
contents.setStyleName("Contents")

self._dialog = DialogBox()
self._dialog.setHTML('<b>Welcome to the dialog box</b>')
self._dialog.setWidget(contents)

left = (Window.getClientWidth() - 200) / 2


top = (Window.getClientHeight() - 100) / 2
self._dialog.setPopupPosition(left, top)
self._dialog.show()

def onClose(self):
self._dialog.hide()

ui.DockPanel
The ui.DockPanel class divides the panel into five pieces, arranged into North, South, East, West and
center pieces. In general the outer pieces are smaller, with the centre holding the main part of the panel's
contents, as shown below.
The alignment and size can be set for each widget within the DockPanel, by calling
setCellHorizontalAlignment(widget, alignment), setCellVerticalAlignment(widget, alignment),
setCellHeight(widget, height) and setCellWidth(widget, width). The default horizontal and vertical
alignment to use for new widgets can be set by calling setHorizontalAlignment() and
setVerticalAlignment() before the widget is added.
Source Code:

from pyjamas.ui import SimplePanel, DockPanel, Label, HasAlignment

class DockPanelDemo(SimplePanel):

def __init__(self):
SimplePanel.__init__(self)

panel = DockPanel()
panel.setBorderWidth(1)

north = Label("North")
west = Label("West")
center = Label("Center")
east = Label("East")
south = Label("South")

panel.setHorizontalAlignment(HasAlignment.ALIGN_CENTER)
panel.setVerticalAlignment(HasAlignment.ALIGN_MIDDLE)

panel.add(north, DockPanel.NORTH)
panel.add(west, DockPanel.WEST)
panel.add(center, DockPanel.CENTER)
panel.add(east, DockPanel.EAST)
panel.add(south, DockPanel.SOUTH)

panel.setCellHeight(center, "200px")
panel.setCellWidth(center, "400px")

self.add(panel)

31 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

ui.FlexTable
The ui.FlexTable class implements a table that can have different numbers of cells in each row, and
single cells can span multiple rows and columns.
Each FlexTable has a FlexCellFormatter which can be used to format the cells in the table. The
FlexCellFormatter has methods to set the row or column spans for a cell, as well as change the cell
alignment, as shown below.
Note that if row or column spanning is used, the cells on the rest of that row or column will be moved
over. This can cause some surprising results. Imagine a table with a 3x2 layout, like this:

+---+---+---+
| A | B | C |
+---+---+---+
| D | E | F |
+---+---+---+

If Cell is set up 0,0 to span two columns, like this:

flexTable.getFlexCellFormatter().setColSpan(0, 0, 2)

then the table, when displayed, ends up looking like this:

+-------+---+---+
| A | B | C |
+---+---+---+---+
| D | E | F |
+---+---+---+

It might be expected that cell B would be above cell E, but to make this happen it would be necessary to
place cell E at (1, 2) rather than (1, 1), as Cell A at (0, 0) has specifically been set to span the first two
columns.
Each FlexTable also has a RowFormatter which can be used to change style names, attributes, and the
visibility of rows in the table.

Please note that, whilst FlexTable is useful for laying out complex
patterns, some browsers are not perfect in determining the layout.
If the patterns are particularly jagged, Firefox 2.0, for example,
can get horribly confused. There isn't anything that can be done
about this, other than to raise a bugreport with Mozilla, and redesign
the application to work around the problem. Welcome to an unfortunate
instance where browser incompatibility "hell" still applies, although
it's necessary to push Firefox pretty hard, with an exceptionally
complex layout, to encounter this particular bug.

Source Code:

from pyjamas.ui import SimplePanel, FlexTable, HasAlignment, Button

class FlexTableDemo(SimplePanel):

def __init__(self):
SimplePanel.__init__(self)

self._table = FlexTable()
self._table.setBorderWidth(1)
self._table.setWidth("100%")

cellFormatter = self._table.getFlexCellFormatter()
rowFormatter = self._table.getRowFormatter()

self._table.setHTML(0, 0, "<b>Mammals</b>")
self._table.setText(1, 0, "Cow")
self._table.setText(1, 1, "Rat")

32 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

self._table.setText(1, 2, "Dog")

cellFormatter.setColSpan(0, 0, 3)
cellFormatter.setHorizontalAlignment(0, 0, HasAlignment.ALIGN_CENTER)

self._table.setWidget(2, 0, Button("Hide", getattr(self, "hideRows")))


self._table.setText(2, 1, "1,1")
self._table.setText(2, 2, "2,1")
self._table.setText(3, 0, "1,2")
self._table.setText(3, 1, "2,2")

cellFormatter.setRowSpan(2, 0, 2)
cellFormatter.setVerticalAlignment(2, 0, HasAlignment.ALIGN_MIDDLE)

self._table.setWidget(4, 0, Button("Show", getattr(self, "showRows")))

cellFormatter.setColSpan(4, 0, 3)

rowFormatter.setVisible(4, False)

self.add(self._table)

def hideRows(self):
rowFormatter = self._table.getRowFormatter()
rowFormatter.setVisible(2, False)
rowFormatter.setVisible(3, False)
rowFormatter.setVisible(4, True)

def showRows(self):
rowFormatter = self._table.getRowFormatter()
rowFormatter.setVisible(2, True)
rowFormatter.setVisible(3, True)
rowFormatter.setVisible(4, False)

ui.FlowPanel
The ui.FlowPanel is a panel that allows its contents to "flow" from left to right, and then from top to bottom,
like words on a page.
Due to the way it works, only the width of a FlowPanel needs to be specified: it will automatically take up
as much height as is needed to fit the panel's contents.
Unfortunately, circumstances where FlowPanel is useful is actually quite limited, because of the way
other widgets are typically implemented. Many widgets are wrapped up in a element, which is a
block-level element that overrules the element used by the FlowPanel, which is an inline element. As a
result, if a ui.Label is added to a FlowPanel, for example, it will still appear on a line by itself rather than
flowing with the other elements.
So it may be desirable to avoid using FlowPanel unless it is certain that the items being added will flow
correctly. Also, it's worth experimenting by adding float: left; to the CSS style of the item being
added. If that doesn't work, try adding the widget to a SimplePanel, and adding the float-left style to the
SimplePanel.
Source Code:

from pyjamas.ui import SimplePanel, FlowPanel, Button

class FlowPanelDemo(SimplePanel):

def __init__(self):
SimplePanel.__init__(self)

flow = FlowPanel()

flow.add(Button("Item 1"))
flow.add(Button("Item 2"))
flow.add(Button("Item 3"))
flow.add(Button("Item 4"))
flow.add(Button("Item 5"))
flow.add(Button("Item 6"))

33 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

flow.add(Button("Item 7"))
flow.add(Button("Item 8"))
flow.add(Button("Item 9"))
flow.add(Button("Item 10"))
flow.setWidth("400px")
self.add(flow)

ui.FormPanel
The ui.FormPanel class implements a traditional HTML form.
Any TextBox, PasswordTextBox, TextArea, RadioButton, CheckBox, ListBox, FileUpload and Hidden
fields contained within the form panel will be sent to the server when the form is submitted.
The example below calls Google to perform a search using the query entered by the user into the text
field. The results are shown in a separate Frame. Alternatively, Form.addFormHandler(handler) can be
called, to manually process the results of posting the form. When this is done, handler.onSubmit(event)
will be called when the user is about to submit the form; call event.setCancelled(True) to cancel the event
within this method. Also, handler.onSubmitComplete(event) will be called when the results of submitting
the form are returned back to the browser. Call event.getResults() to retrieve the (plain-text) value
returned by the server.
Note that if a ui.FileUpload widget is used in a form, the form encoding and method must be set, as
follows:

self.form.setEncoding(FormPanel.ENCODING_MULTIPART)
self.form.setMethod(FormPanel.METHOD_POST)

Setting Multipart encoding and POST method will ensure that the form is submitted in a way that Web
servers expect files to be uploaded.
Source Code:

from pyjamas.ui import SimplePanel, FormPanel, VerticalPanel, HorizontalPanel


from pyjamas.ui import TextBox, Label, Button

class FormPanelDemo(SimplePanel):

def __init__(self):
SimplePanel.__init__(self)

self.form = FormPanel()
self.form.setAction("http://google.com/search")
self.form.setTarget("results")

vPanel = VerticalPanel()

hPanel = HorizontalPanel()
hPanel.add(Label("Search for:"))

self.field = TextBox()
self.field.setName("q")
hPanel.add(self.field)

hPanel.add(Button("Submit", getattr(self, "onBtnClick")))

vPanel.add(hPanel)

results = NamedFrame("results")
vPanel.add(results)

self.form.add(vPanel)
self.add(self.form)

def onBtnClick(self):
self.form.submit()

ui.Grid

34 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

The ui.Grid class implements a panel which lays its contents out in a grid-like fashion, very like an HTML
table.
The setHTML(row, col, html) method can be used to set the HTML-formatted text to be displayed at the
given row and column within the grid. Similarly, setText(row, col, text) can be called to display plain
(unformatted) text at the given row and column, and setWidget(row, col, widget) can be called to add any
widget at the given row and column within the grid.
Source Code:

from pyjamas.ui import SimplePanel, Grid, Button

class GridDemo(SimplePanel):

def __init__(self):
SimplePanel.__init__(self)

grid = Grid(5, 5)
grid.setHTML(0, 0, '<b>Hello, World!</b>')
grid.setBorderWidth(2)
grid.setCellPadding(4)
grid.setCellSpacing(1)

for row in range(1, 5):


for col in range(1, 5):
grid.setText(row, col, "%d*%d=%d" % (row, col row*col))

grid.setWidget(4, 4, Button("Peekaboo")

self.add(grid)

Please note that there is a bug in the sprintf code, in versions of Pyjamas
up to 0.3, that will stop "%d*%d=%d" from working. Either use this:
str(row) + "*" + str(col) + " = " + str(row*col)
or upgrade Pyjamas to greater than version 0.3

ui.HorizontalPanel
The ui.HorizontalPanel class is a panel that lays out its contents from left to right.
It is often useful to call setSpacing(spacing) to add space between each of the panel's widgets.
setHorizontalAlignment(alignment) and setVerticalAlignment(alignment) can be called, before adding
widgets, to control how those widgets are aligned within the available space. Alternatively,
setCellHorizontalAlignment(widget, alignment) and setCellVerticalAlignment(widget, alignment) can be
called, to change the alignment of a single widget after it has been added.

Note that if it is desired that a specific widget within the panel takes
up different amounts of space, do not call widget.setWidth(width)
or widget.setHeight(height), as these are ignored by the
panel. Instead, call panel.setCellWidth(widget, width) and
panel.setCellHeight(widget, height).

Source Code:

from pyjamas.ui import SimplePanel, HorizontalPanel, Label, HasAlignment

class HorizontalPanelDemo(SimplePanel):

def __init__(self):
SimplePanel.__init__(self)

panel = HorizontalPanel()
panel.setBorderWidth(1)

panel.setHorizontalAlignment(HasAlignment.ALIGN_CENTER)
panel.setVerticalAlignment(HasAlignment.ALIGN_MIDDLE)

35 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

part1 = Label("Part 1")


part2 = Label("Part 2")
part3 = Label("Part 3")
part4 = Label("Part 4")

panel.add(part1)
panel.add(part2)
panel.add(part3)
panel.add(part4)

panel.setCellWidth(part1, "10%")
panel.setCellWidth(part2, "70%")
panel.setCellWidth(part3, "10%")
panel.setCellWidth(part4, "10%")

panel.setCellVerticalAlignment(part3, HasAlignment.ALIGN_BOTTOM)

panel.setWidth("100%")
panel.setHeight("200px")

self.add(panel)

ui.HtmlPanel
The ui.HTMLPanel class allows HTML to be included within an application, and other widgets embedded
inside the panel's contents, by wrapping them inside a tag.
Source Code:

from pyjamas.ui import SimplePanel, HTMLPanel, Button, Label

class HtmlPanelDemo(SimplePanel):

def __init__(self):
SimplePanel.__init__(self)

id1 = HTMLPanel.createUniqueId()
id2 = HTMLPanel.createUniqueId()

panel = HTMLPanel('<b>This is some HTML</b><br>' +


'First widget:<span id="' + id1 + '"></span><br>' +
'Second widget:<span id="' + id2 + '"></span><br>' +
'More <i>HTML</i>')

panel.add(Button("Hi there"), id1)


panel.add(Label("This label intentionally left blank"), id2)

self.add(panel)

ui.PopupPanel
The ui.PopupPanel class implements a panel that pops up in the browser window to display some
contents. When the user clicks outside the popup, it disappears again.
The PopupPanel requires stylesheet definitions in order to work properly. The following stylesheet
definitions were used in the example below:

.showcase-popup {
background-color: white;
border: 1px solid #87B3FF;
padding: 4px;
}

Note that the popup panel is supposed to close when the user clicks outside of it. However, this doesn't
work under Firefox 3, so the code below adds a click handler so the user can click on the popup itself to
close it.
Source Code:

36 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

from pyjamas.ui import SimplePanel, VerticalPanel, HTML, Button, PopupPanel

class PopupPanelDemo(SimplePanel):

def __init__(self):
SimplePanel.__init__(self)

vPanel = VerticalPanel()
vPanel.setSpacing(4)

self._btn = Button("Click Me", getattr(self, "showPopup"))

vPanel.add(HTML("Click on the button below to display the popup."))


vPanel.add(self._btn)

self.add(vPanel)

def showPopup(self):
contents = HTML("Hello, World!")
contents.addClickListener(getattr(self, "onClick"))

self._popup = PopupPanel(autoHide=True)
self._popup.add(contents)
self._popup.setStyleName("showcase-popup")

left = self._btn.getAbsoluteLeft() + 10
top = self._btn.getAbsoluteTop() + 10
self._popup.setPopupPosition(left, top)
self._popup.show()

def onClick(self):
self._popup.hide()

ui.ScrollPanel
The ui.ScrollPanel class implements a panel that scrolls its contents.
If scroll bars are required to be always visible, call setAlwaysShowScrollBars(True). It's also possible to
change the current scrolling position programmatically, by calling setScrollPosition(vPos) and
setScrollHorizontalPosition(hPos) to change the horizontal and vertical scrolling position, respectively.
Source Code:

from pyjamas.ui import SimplePanel, ScrollPanel, HTML

class ScrollPanelDemo(SimplePanel):

def __init__(self):
SimplePanel.__init__(self)

panel = ScrollPanel()

contents = HTML("<b>Tao Te Ching, Chapter One</b><p>" +


"The Way that can be told of is not an unvarying " +
"way;<p>The names that can be named are not " +
"unvarying names.<p>It was from the Nameless that " +
"Heaven and Earth sprang;<p>The named is but the " +
"mother that rears the ten thousand creatures, " +
"each after its kind.")

panel.add(contents)
panel.setSize("300px", "100px")
self.add(panel)

ui.StackPanel
The ui.StackPanel class displays a "stack" of sub-panels, where only one sub-panel is open at a time.
The StackPanel relies heavily on stylesheet definitions to make it look good: the default look of a

37 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

StackPanel without any styles defined is almost unusable. The following eye-burning stylesheet
definitions were used for the example given below:

.gwt-StackPanel {
border: 5px solid #999999;
background-color: #CCCCCC;
border-collapse: collapse;
}

.gwt-StackPanel .gwt-StackPanelItem {
border: 2px solid #000099;
background-color: #FFFFCC;
cursor: pointer;
font-weight: normal;
}

.gwt-StackPanel .gwt-StackPanelItem-selected {
border: 3px solid #FF0000;
background-color: #FFFF66;
cursor: default;
font-weight: bold;
}

The currently-open panel can be programatically changed, by calling the setStackVisible(index, visible)
method. To find out which panel is currently open, call getSelectedIndex(). To retrieve the widget at a
given index, call getWidget(index). Finally, the label for a stack item can be changed, by calling
setStackText(index, text).
Source Code:

from pyjamas.ui import SimplePanel, StackPanel, HTML

class StackPanelDemo(SimplePanel):

def __init__(self):
SimplePanel.__init__(self)

stack = StackPanel()

stack.add(HTML('The quick<br>brown fox<br>jumps over the<br>lazy dog.'),


"Stack 1")
stack.add(HTML('The<br>early<br>bird<br>catches<br>the<br>worm.'),
"Stack 2")
stack.add(HTML('The smart money<br>is on the<br>black horse.'),
"Stack 3")

stack.setWidth("100%")
self.add(stack)

ui.TabPanel
The ui.TabPanel class implements a tabbed window, where clicking on a tab causes the associated
contents to be displayed.
The TabPanel relies heavily on cascading stylesheet definitions to operate correctly. The following
stylesheet definitions are used by the example shown below:

.gwt-TabPanel {
}

.gwt-TabPanelBottom {
border: 1px solid #87B3FF;
}

.gwt-TabBar {
background-color: #C3D9FF;
}

38 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

.gwt-TabBar .gwt-TabBarFirst {
height: 100%;
padding-left: 3px;
}

.gwt-TabBar .gwt-TabBarRest {
padding-right: 3px;
}

.gwt-TabBar .gwt-TabBarItem {
border-top: 1px solid #C3D9FF;
border-bottom: 1px solid #C3D9FF;
padding: 2px;
cursor: pointer;
}

.gwt-TabBar .gwt-TabBarItem-selected {
font-weight: bold;
background-color: #E8EEF7;
border-top: 1px solid #87B3FF;
border-left: 1px solid #87B3FF;
border-right: 1px solid #87B3FF;
border-bottom: 1px solid #E8EEF7;
padding: 2px;
cursor: default;
}

Source Code:

from pyjamas.ui import SimplePanel, TabPanel, HTML

class TabPanelDemo(SimplePanel):

def __init__(self):
SimplePanel.__init__(self)

tabs = TabPanel()
tabs.add(HTML("The quick brown fox jumps over the lazy dog."), "Tab 1")
tabs.add(HTML("The early bird catches the worm."), "Tab 2")
tabs.add(HTML("The smart money is on the black horse."), "Tab 3")

tabs.selectTab(0)
tabs.setWidth("100%")
tabs.setHeight("250px")

self.add(tabs)

ui.VerticalPanel
The ui.VerticalPanel class is a panel that lays out its contents from top to bottom.
It is often useful to call setSpacing(spacing) to add space between each of the panel's widgets.
setHorizontalAlignment(alignment) and setVerticalAlignment(alignment) can also be called before adding
widgets to control how those widgets are aligned within the available space. Alternatively,
setCellHorizontalAlignment(widget, alignment) and setCellVerticalAlignment(widget, alignment) can be
called, to change the alignment of a single widget after it has been added.

Note that if it is desired that a specific widget within the panel takes
up different amounts of space, do not call widget.setWidth(width)
or widget.setHeight(height), as these are ignored by the
panel. Instead, call panel.setCellWidth(widget, width) and
panel.setCellHeight(widget, height).

Source Code:

from pyjamas.ui import SimplePanel, VerticalPanel, Label, HasAlignment

39 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

class VerticalPanelDemo(SimplePanel):

def __init__(self):
SimplePanel.__init__(self)

panel = VerticalPanel()
panel.setBorderWidth(1)

panel.setHorizontalAlignment(HasAlignment.ALIGN_CENTER)
panel.setVerticalAlignment(HasAlignment.ALIGN_MIDDLE)

part1 = Label("Part 1")


part2 = Label("Part 2")
part3 = Label("Part 3")
part4 = Label("Part 4")

panel.add(part1)
panel.add(part2)
panel.add(part3)
panel.add(part4)

panel.setCellHeight(part1, "10%")
panel.setCellHeight(part2, "70%")
panel.setCellHeight(part3, "10%")
panel.setCellHeight(part4, "10%")

panel.setCellHorizontalAlignment(part3, HasAlignment.ALIGN_RIGHT)

panel.setWidth("50%")
panel.setHeight("300px")

self.add(panel)

Widgets
The definition of a Widget is "an element of a graphical user interface that displays information or allows
the user to interact with an application". It should therefore come as no surprise that pyjamas.ui contains
the basic widgets - buttons, input boxes etc. - that can be expected of a Widget set.
Also included are the kinds of widgets that are expected of an HTML-based widget set, such as those
that deal with Frames, Forms and Hyperlinks. These concepts, whilst unique to the HTML world, still need
to be "widgetified", if Pyjamas applications are to get at them.

These HTML-based widgets, and the HTMLPanel, made it difficult to port


Pyjamas to the desktop-based widget sets, PyQt4 and PyGtk2. Consequently,
Webkit, an HTML Web Engine, had to be used as the basis for the Pyjamas
Desktop port.

It's worth reiterating that, whilst the Pyjamas Widgets are in some ways minimalist yet comprehensive, it
is still possible, thanks to manipulation of the Browser DOM model, to write custom widgets very easily.
pyjamas.ui is stuffed full of widgets that manipulate the DOM model (such as Tree and Menubar), so it's
not like there's a lack of examples to follow, or anything.

ui.Button
The ui.Button class is used to show a button. When the user clicks on the button, the given listener
function is called.
Note that the getattr() function can be used to specify which method will be called when the button is
clicked. This is the best way to write button click handlers if there is more than one button on the panel. If
there is only one button, btn = Button("...", self) can be used, instead. Under these
circumstances, a method called onClick() must be provided in the class, which will respond to the button
click.
Another useful method is Button.setEnabled(enabled), which enables or disables the button, depending
on the value of its parameter.

40 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

It's particularly useful to call setEnabled(False) when some data has been
sent to a server, using AJAX. Normally in a web-based application, the
browser stops responding whilst the page is reloaded after a form is submitted.
However, in an AJAX application, pressing a button usually causes an AJAX
submit - and there is nothing to stop the user from pressing the button
more than once. So it is a really good idea to disable the button, explicitly,
until the AJAX response is received, and re-enable the button, if appropriate
for the application, after the response has been processed.

Source Code:

from pyjamas.ui import SimplePanel, Button


from pyjamas import Window

class ButtonDemo(SimplePanel):

def __init__(self):
SimplePanel.__init__(self)

btn = Button("Click Me", getattr(self, "onButtonClick"))


self.add(btn)

self.btn2 = Button("Tickle me!", self)


self.add(btn2)

def onButtonClick(self):
Window.alert("Ouch!")

def onClick(self, sender, event):


if sender == self.btn2:
Window.alert("That tickles!")
else:
Window.alert("Who sent that???")

ui.CheckBox
The ui.CheckBox class is used to show a standard checkbox. When the user clicks on the checkbox, the
checkbox's state is toggled.
The setChecked(checked) method checks or unchecks the checkbox depending on the value of the
parameter. To get the current value of the checkbox, call isChecked().
A checkbox can be enabled or disables using setEnabled(enabled). Also, addClickListener() can be
called to add a function that responds when the user clicks on the checkbox, as shown below. This
listener feature can be useful when building complicated input screens, where checking a checkbox
causes other input fields to be enabled or disabled.

Note: the onClick method will be called irrespective of whether


the checkbox is checked or unchecked, which is very useful. If it
is necessary to only take action when the checkbox is checked,
call isChecked() to determine its value at the time.

Source Code:

from pyjamas.ui import SimplePanel, CheckBox


from pyjamas import Window

class CheckBoxDemo(SimplePanel):

def __init__(self):
SimplePanel.__init__(self)

self.box = CheckBox("Print Results?")


self.box.addClickListener(getattr(self, "onClick"))

self.add(self.box)

41 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

def onClick(self):
Window.alert("checkbox status: " + self.box.isChecked())

ui.FileUpload
The ui.FileUpload class implements a file uploader widget.
The FileUpload widget must be inside a ui.FormPanel which is used to submit the HTML form to the
server. Note that it is necessary to set the form encoding and method as follows: {
self.form.setEncoding(FormPanel.ENCODING_MULTIPART)
self.form.setMethod(FormPanel.METHOD_POST) } Setting Multipart encoding and POST method will
ensure that the form is submitted in a way that Web servers expect files to be uploaded.
The example below doesn't really work, as there is no suitable server at nonexistent.com. However, it
does show how a file upload widget could be used within a FormPanel.
Source Code:

from pyjamas.ui import SimplePanel, FormPanel, VerticalPanel, HorizontalPanel


from pyjamas.ui import FileUpload, Label, Button

class FileUploadDemo(SimplePanel):

def __init__(self):
SimplePanel.__init__(self)

self.form = FormPanel()
self.form.setEncoding(FormPanel.ENCODING_MULTIPART)
self.form.setMethod(FormPanel.METHOD_POST)
self.form.setAction("http://nonexistent.com") # set this as appropriate
self.form.setTarget("results")

vPanel = VerticalPanel()

hPanel = HorizontalPanel()
hPanel.setSpacing(5)
hPanel.add(Label("Upload file:"))

self.field = FileUpload()
self.field.setName("file")
hPanel.add(self.field)

hPanel.add(Button("Submit", getattr(self, "onBtnClick")))

vPanel.add(hPanel)

results = NamedFrame("results")
vPanel.add(results)

self.form.add(vPanel)
self.add(self.form)

def onBtnClick(self):
self.form.submit()

ui.Frame
The ui.Frame class is used to embed a separate HTML page within an application.
If a URL is passed when the Frame is created, that URL will be used immediately. Alternatively, the
Frame.setUrl() method can be called to change the URL at any time. Frame.getUrl() can also be called
to retrieve the current URL for the frame.
Source Code:

from pyjamas.ui import SimplePanel, Frame

class FrameDemo(SimplePanel):

42 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

def __init__(self):
SimplePanel.__init__(self)

frame = Frame("http://google.com")
frame.setWidth("100%")
frame.setHeight("200px")
self.add(frame)

ui.Hidden
The ui.Hidden class represents a hidden form field.
ui.Hidden is really only useful when the hidden field is part of a ui.FormPanel.
Source Code:

from pyjamas.ui import SimplePanel, FormPanel, VerticalPanel


from pyjamas.ui import Hidden, Button, NamedFrame

class HiddenDemo(SimplePanel):

def __init__(self):
SimplePanel.__init__(self)

self.form = FormPanel()
self.form.setAction("http://google.com/search")
self.form.setTarget("results")

panel = VerticalPanel()
panel.add(Hidden("q", "python pyjamas"))
panel.add(Button("Search", getattr(self, "onBtnClick")))

results = NamedFrame("results")
panel.add(results)

self.form.add(panel)
self.add(self.form)

def onBtnClick(self):
self.form.submit()

ui.Html
The ui.HTML class displays HTML-formatted text. To display unformatted text, use ui.Label.
ui.HTML's content can be set and retrieved, using setHTML and getHTML. If False is passed as the
second parameter when creating an HTML instance, word wrapping will be disabled, forcing all the text to
be on one line.
Source Code:

from pyjamas.ui import SimplePanel, HTML

class HtmlDemo(SimplePanel):

def __init__(self):
SimplePanel.__init__(self)

html = HTML("Hello, <b><i>World!</i></b>")


self.add(html)

ui.Hyperlink
The ui.Hyperlink class acts as an "internal" hyperlink to a particular state of the application. These states
are stored in the application's history, allowing for the use of the Back and Next buttons in the browser to
move between application states.
The ui.Hyperlink class only makes sense in an application which keeps track of state using the History

43 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

module. When the user clicks on a hyperlink, the application changes state by calling
History.newItem(newState). The application then uses a history listener function to respond to the
change in state in whatever way makes sense.

Critical to ensuring that the History module works correctly is the


addition of a hidden "history" frame to the HTML loader file of the
application.

Here is the pyjamas "Kitchen Sink" html file, as an example:

<html>
<head>
<meta name="pygwt:module" content="KitchenSink">
<link rel='stylesheet' href='KitchenSink.css'>
<title>KitchenSink</title>
</head>
<body bgcolor="white">
<script language="javascript" src="pygwt.js"></script>

<iframe id='__pygwt_historyFrame' style='width:0;height:0;border:0'></iframe>


</body>
</html>

Source Code:

from pyjamas.ui import SimplePanel, VerticalPanel, HorizontalPanel


from pyjamas.ui import Hyperlink, Label
from History import History

class HyperlinkDemo(SimplePanel):

def __init__(self):
SimplePanel.__init__(self)

History().addHistoryListener(self)

vPanel = VerticalPanel()

self.stateDisplay = Label()
vPanel.add(self.stateDisplay)

hPanel = HorizontalPanel()
hPanel.setSpacing(5)
hPanel.add(Hyperlink("State 1", False, "state number 1"))
hPanel.add(Hyperlink("State 2", False, "state number 2"))

vPanel.add(hPanel)
self.add(vPanel)

def onHistoryChanged(self, state):


self.stateDisplay.setText(state)

ui.Image
The ui.Image class is used to display an image.
The Image class can display any image that is specified by a URL. The image can be stored somewhere
on the internet, or on any Web Server, anywhere. In the example below, the "public" directory within the
application's source folder is used, and the image is then accessed using a relative URL.
The example image file named "myImage.jpg" must be stored in "images" sub-directory, which must be in
the "public" directory of the application's main source directory.
As well as passing the image URL to the initialiser, setURL() can be called to change the image being
displayed, at any time. addClickListener() can also be used to add a listener function callback that will be
called when the user clicks on the image.

44 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

Source Code:

from pyjamas.ui import SimplePanel, Image


from pyjamas import Window

class ImageDemo(SimplePanel):

def __init__(self):
SimplePanel.__init__(self)

# We display the "myImage.jpg" file, stored in the "public/images"


# directory, where "public" is in the application's source directory.

img = Image("images/myImage.jpg")
img.addClickListener(getattr(self, "onImageClicked"))
self.add(img)

def onImageClicked(self):
Window.alert("Stop that!")

ui.Label
The ui.Label class is used to display unformatted text. Unlike the ui.HTML class, it does not interpret
HTML format codes. If False is passed as the second parameter when creating a label, word wrapping
will be disabled, forcing all the text to be on one line.
Source Code:

from pyjamas.ui import SimplePanel, Label

class LabelDemo(SimplePanel):

def __init__(self):
SimplePanel.__init__(self)

label = Label("This is a label", wordWrap=False)


self.add(label)

ui.ListBox
The ui.ListBox class allows the user to select one or more items from a list. There are two variations of
the ListBox: a normal list of items the user can click on, and a dropdown menu of items. Both variations
are shown in the example below.
Add items to a list by calling ListBox.addItem(). addItem can take the label to display, and also an optional
value to associate with that item in the list. ListBox.getSelectedIndex() returns the index of the currently
selected item, or -1 if nothing is selected. ListBox.getItemText(n) returns the text for the given item in the
list, while ListBox.getValue(n) returns the value associated with the given list item. To detect when the
user selects something from a ListBox, set up a listener by calling addChangeListener(). Finally,
ListBox.clear() clears the current contents of the ListBox.
Source Code:

from pyjamas.ui import SimplePanel, HorizontalPanel, ListBox


from pyjamas import Window

class ListBoxDemo(SimplePanel):

def __init__(self):
SimplePanel.__init__(self)

hPanel = HorizontalPanel()
hPanel.setSpacing(10)

self.list1 = ListBox()
self.list1.setVisibleItemCount(10)
self.list1.addItem("Item 1")

45 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

self.list1.addItem("Item 2")
self.list1.addItem("Item 3")
self.list1.addChangeListener(getattr(self, "onList1ItemSelected"))

self.list2 = ListBox()
self.list2.setVisibleItemCount(0)
self.list2.addItem("Item A")
self.list2.addItem("Item B")
self.list2.addItem("Item C")
self.list2.addChangeListener(getattr(self, "onList2ItemSelected"))

hPanel.add(self.list1)
hPanel.add(self.list2)
self.add(hPanel)

def onList1ItemSelected(self):
item = self.list1.getItemText(self.list1.getSelectedIndex())
Window.alert("You selected " + item + " from list 1")

def onList2ItemSelected(self):
item = self.list2.getItemText(self.list2.getSelectedIndex())
Window.alert("You selected " + item + " from list 2")

ui.Menubar
The ui.MenuBar and ui.MenuItem classes allow menu bars to be created in an application.
There are several important things to be aware of when adding menus to an application:
A stylesheet is absolutely essential, to define the look of the menu. The default style is terrible, and
makes the menu unusable. The following stylesheet entries were used for the example code below:

.gwt-MenuBar {
background-color: #C3D9FF;
border: 1px solid #87B3FF;
cursor: default;
}

.gwt-MenuBar .gwt-MenuItem {
padding: 1px 4px 1px 4px;
font-size: smaller;
cursor: default;
}

.gwt-MenuBar .gwt-MenuItem-selected {
background-color: #E8EEF7;
}

By default, each menu item can be associated with a class, whose execute method will be called
when that item is selected. Note that a helper class, MenuCmd, is defined below to allow more than
one menu item handler method to be defined within a single class.
Menu items are added directly, passing the item label and the associated command to
MenuBar.addItem(). For adding sub-menus, the sub-menu must be wrapped up in a MenuItem, as
shown below.
The HTML codes in a menu item's label can be used, by calling MenuBar.addItem(label, True,
cmd) instead of MenuBar.addItem(label, cmd). Similarly, HTML styling can be used in a menu's title
by calling MenuItem(label, True, submenu): see the second-to-last line of MenubarDemo.__init__,
below.
Source Code:

from pyjamas.ui import SimplePanel, MenuBar, MenuItem


from pyjamas import Window

class MenubarDemo(SimplePanel):

def __init__(self):
SimplePanel.__init__(self)

46 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

menu1 = MenuBar(vertical=True)
menu1.addItem("Item 1", MenuCmd(self, "onMenu1Item1"))
menu1.addItem("Item 2", MenuCmd(self, "onMenu1Item2"))

menu2 = MenuBar(vertical=True)
menu2.addItem("Apples", MenuCmd(self, "onMenu2Apples"))
menu2.addItem("Oranges", MenuCmd(self, "onMenu2Oranges"))

menubar = MenuBar(vertical=False)
menubar.addItem(MenuItem("Menu 1", menu1))
menubar.addItem(MenuItem("<i>Menu 2</i>", True, menu2))
self.add(menubar)

def onMenu1Item1(self):
Window.alert("Item 1 selected")

def onMenu1Item2(self):
Window.alert("Item 2 selected")

def onMenu2Apples(self):
Window.alert("Apples selected")

def onMenu2Oranges(self):
Window.alert("Oranges selected")

class MenuCmd:

def __init__(self, object, handler):


self._object = object
self._handler = handler

def execute(self):
handler = getattr(self._object, self._handler)
handler()

ui.NamedFrame
The ui.NamedFrame class is a variation of the ui.Frame which lets a name be assigned to the frame.
Naming a frame makes it possible to refer to that frame by name in both Javascript code and as the
target for a hyperlink.
Source Code:

from pyjamas.ui import SimplePanel, VerticalPanel, NamedFrame, HTML

class NamedFrameDemo(SimplePanel):

def __init__(self):
SimplePanel.__init__(self)

vPanel = VerticalPanel()
vPanel.setSpacing(5)

frame = NamedFrame("myFrame") # note name of frame


frame.setWidth("100%")
frame.setHeight("200px")

vPanel.add(frame)

# note targets are set to "myFrame"


vPanel.add(HTML('<a href="http://google.com" target="myFrame">Google</a>'))
vPanel.add(HTML('<a href="http://yahoo.com" target="myFrame">Yahoo</a>'))

self.add(vPanel)

ui.PasswordTextBox

47 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

The ui.PasswordTextBox class implements a standard password input field.


Like its cousins the ui.TextBox and ui.TextArea classes, ui.PasswordTextBox defines many methods
which may prove useful.
The most important methods are going to be setText() and getText(), which set and retrieve the contents
of the input field, and setMaxLength() to specify how many characters the user can type into the field.
Note that for some reason, the setVisibleLength() method is not defined for a password field. This
means that it is necessary to specify the width of the field in pixels, as shown below.
Source Code:

from pyjamas.ui import SimplePanel, PasswordTextBox

class PasswordTextBoxDemo(SimplePanel):

def __init__(self):
SimplePanel.__init__(self)

field = PasswordTextBox()
field.setWidth("100px")
self.add(field)

ui.RadioButton
The ui.RadioButton class is used to show a radio button. Each radio button is given a "group" value;
when the user clicks on a radio button, the other radio buttons in that group are deselected, and the
clicked on radio button is selected.
The setChecked(checked) method can be used to select or deselect a radio button, and isChecked()
can be called to see if a radio button is currently selected. Also a checkbox can be enabled or disabled
using setEnabled(enabled). Finally, addClickListener() can be used to set a function that will be called
when the user clicks on the radio button.
Source Code:

from pyjamas.ui import SimplePanel, HorizontalPanel, VerticalPanel, RadioButton

class RadioButtonDemo(SimplePanel):

def __init__(self):
SimplePanel.__init__(self)

panel1 = VerticalPanel()

# First group, called "group1"


panel1.add(RadioButton("group1", "Red"))
panel1.add(RadioButton("group1", "Green"))
panel1.add(RadioButton("group1", "Blue"))

panel2 = VerticalPanel()

# Second group, called "group2"


panel2.add(RadioButton("group2", "Solid"))
panel2.add(RadioButton("group2", "Liquid"))
panel2.add(RadioButton("group2", "Gas"))

hPanel = HorizontalPanel()
hPanel.add(panel1)
hPanel.add(panel2)

self.add(hPanel)

ui.TextArea
The ui.TextArea class implements a standard multi-line input field.
The setCharacterWidth() method sets the width of the input field, in characters, while setVisibleLines()
sets the height of the field, in lines.

48 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

Use the getText() method to retrieve the field's current text, and setText() to set it. There are many other
useful methods defined by ui.TextArea and its parent classes; see the module documentation for more
details.
Source Code:

from pyjamas.ui import SimplePanel, TextArea

class TextAreaDemo(SimplePanel):

def __init__(self):
SimplePanel.__init__(self)

field = TextArea()
field.setCharacterWidth(20)
field.setVisibleLines(4)
self.add(field)

ui.TextBox
The ui.TextBox class implements a standard one-line input field.
There are many useful methods defined by ui.TextBox and its parent classes. For example, getText()
returns the current contents of the input field, and setText() lets the field's contents be set to a given
string.
setVisibleLength() sets the width of the field, in characters. Similarly, setMaxLength() sets the maximum
number of characters the user can enter into the field.
Source Code:

from pyjamas.ui import SimplePanel, TextBox

class TextBoxDemo(SimplePanel):

def __init__(self):
SimplePanel.__init__(self)

field = TextBox()
field.setVisibleLength(20)
field.setMaxLength(10)

self.add(field)

ui.Tree
The ui.Tree class adds a tree control to an application.
Call Tree.addTreeListener() to add a tree listener object to a tree. That listener object's
onTreeItemSelected() method will be called when the user clicks on that item in the tree control with which
the listener has been associated. Similarly, the listener object's onTreeItemStateChanged() method will
be called whenever the user opens or closes a branch of the tree. The listener object must have both
these methods defined, even if neither of them is used.
To open a branch of the tree, call the TreeItem.setState() method. If the state parameter is True, the
branch of the tree will be opened; if it is False, the branch of the tree will be closed.
Source Code:

from pyjamas.ui import SimplePanel, Tree, TreeItem


from pyjamas import DOM
from pyjamas import Window

class TreeDemo(SimplePanel):

def __init__(self):
SimplePanel.__init__(self)

tree = Tree()

49 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

tree.addTreeListener(self)

s1 = self.createItem("Section 1")
s1.addItem(self.createItem("Item 1.1", value=11))
s1.addItem(self.createItem("Item 1.2", value=12))

s2 = self.createItem("Section 2")
s2.addItem(self.createItem("Item 2.1", value=21))
s2.addItem(self.createItem("Item 2.2", value=22))

s1.setState(True, fireEvents=False)
s2.setState(True, fireEvents=False)

tree.addItem(s1)
tree.addItem(s2)
self.add(tree)

def createItem(self, label, value=None):


item = TreeItem(label)
DOM.setStyleAttribute(item.getElement(), "cursor", "pointer")
if value != None:
item.setUserObject(value)
return item

def onTreeItemSelected(self, item):


value = item.getUserObject()
Window.alert("You clicked on " + value)

def onTreeItemStateChanged(self, item):


pass # We ignore this.

An important part of a widget toolkit is being able to write custom widgets. In many widget sets,
developers are confronted immediately with a quite complex set of unusual-looking functions - paint,
draw, refresh and other manipulations. The reason for this complexity is because the fundamental
underlying design of Desktop widget set frameworks is invariably too simplistic. Changes in width or
height of a single widget can have a knock-on effect that dramatically alters the layout of the entire
application. Rather than attempting to solve this really quite difficult problem, Desktop widgets simply
avoid it entirely, and impose on the developer to sort out the mess for themselves, and are given stern
warnings about how hard-coding widths and heights could mess things up.
The only technology that has tackled the issue of complex layouts, with any degree of success, is Web
Browsers. Web browsers simply have to tackle layouts properly, due to the overwhelming user-driven
diversity and the sheer numbers of people that use Web browsers. Consequently, Pyjamas and
Pyjamas-Desktop, both being based on web technology, benefit from the underlying layout issues having
been made a lot easier.
Both Pyjamas and Pyjamas-Desktop manipulate the DOM model - an HTML page - as if it was an XML
document. Better than that: unlike with manipulating an HTML page in traditional web browser
development, it's not necessary to get involved with Javascript - unless desired. Pyjamas provides a
module which makes the job of controlling the underlying DOM model that much easier, and this chapter
shows step-by-step how to go about creating a custom widget, through DOM model manipulation and
DOM event interaction.
Missing from the HTML specification, but present in Adobe Flash, are widgets such as sliders and dials.
Many Desktop widget sets have control widgets, so it makes a lot of sense to create one. So this chapter
starts with a simple Vertical scroller which receives "mouse click" to change the position, goes on later to
add "drag" and then adds keyboard interaction, almost as an afterthought (just like most badly-designed
applications do. hoorary!)

Vertical Slider
Start off by importing the DOM model and, because the slider will receive mouse (and later keyboard)
events, it is based on FocusWidget. FocusWidget has the means to add keyboard and event listeners,
set a "tab order" index, and to set and clear focus. Create a file called Controls.py, starting off with the
following lines:

from pyjamas import DOM

50 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

from pyjamas.ui import FocusWidget

The class will be derived from FocusWidget. Width and height parameters will not be added to the
constructor, because Pyjamas Widgets are based on HTML principles: DOM models. So, a developer
can add a CSS "Class", with setStyleName(), and thus set the width and/or height set as desired in the
CSS stylesheet. Alternatively, the functions setWidth() and setHeight() can be used. It is however
necessary to pass in the slider's minimum, maximum and default values.

This is an important point to emphasise: widgets should not impose "look"


onto users - that should, ideally, be defined through CSS. A Widget Class
API should be about "function" rather than "form". So, the constructor
for the widget has minimum, maximum and default values, not width, height
or colour.

Add the following lines to the newly-created Controls.py:

class VerticalDemoSlider(FocusWidget):

def __init__(self, min_value, max_value, start_value=None):

element = DOM.createDiv()
FocusWidget.__init__(self, element)

self.min_value = min_value
self.max_value = max_value
if start_value is None:
start_value = min_value
self.value = start_value
self.valuechange_listeners = []

Here also is the first actual bit of underlying HTML / DOM model showing through: the widget is based on
a "div" tag, hence we call DOM.createDiv() and set that as the FocusWidget's element. Immediately,
therefore, it is clear that the Pyjamas Widgets are effectively "guardian" classes that look after and
manipulate bits of the underlying DOM model, making the whole process of creating and maintaining an
application just that little bit easier to understand.
Next, it is necessary to make the slider "handle" and to make it possible for the handle to move freely. In
other words, it is necessary to be able to set the coordinates, exactly, of the slider "handle". There is a
Pyjamas ui widget which has exactly this functionality, already: AbsolutePanel. Take a look at
pyjamas/ui.py and search for "class AbsolutePanel". Examining what AbsolutePanel.__init__() does,
it can be seen that by setting its "div" container tag to be position "relative", child elements can be
positioned accurately within it. Also, a second hard-coded "div" will be added, to represent the actual
slider handle:

DOM.setStyleAttribute(element, "position", "relative")


DOM.setStyleAttribute(element, "overflow", "hidden")

self.handle = DOM.createDiv()
DOM.appendChild(element, self.handle)

Note that the second "handle" div is appended to the container "div" as a child element. As this is just a
demonstration, we're going to hand-code the slider handle with some attributes, making it 10 pixels high,
with a border of 1 pixel, fixing it to be the same width as the Widget, and making it a grey colour.

A much better way to do this would be to set a CSS stylesheet where


users could over-ride all these settings. the "handle" element
would have a CSS style added to it, using setStyleName.

Note also that DOM.setAttribute() is not used to set the border, width and height. W3C HTML
specifications need to be consulted, here: observe that "border" is a CSS attribute reserved for DOM
components such as "table". So, if DOM.setAttribute("border", "1px") is called on a "div" tag, it silently
fails to do anything, in the browser. If the Javascript Console is examined, there might be a warning - but
it is likely swamped by all the other CSS errors...

51 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

If the same thing is tried under Pyjamas-Desktop, a much more useful


run-time error will occur, as the attributes and the CSS style attributes
of an element are directly accessed as static variables, in entirely
different data structures. So, if the variable being accessed don't exist,
Python can do nothing but complain.

The upshot is: pay attention to the underlying DOM model, and remember to simultaneously develop an
application using both Pyjamas and Pyjamas-Desktop, to save a great deal of time. If a border on a "div"
tag is required, it must be set as a CSS Style attribute with DOM.setStyleAttribute, not as an
attribute (with DOM.setAttribute):

DOM.setStyleAttribute(self.handle, "border", "1px")


DOM.setStyleAttribute(self.handle, "width", "100%")
DOM.setStyleAttribute(self.handle, "height", "10px")
DOM.setStyleAttribute(self.handle, "backgroundColor", "#808080")

These lines make the slider "handle" have a border of 1 pixel; set the handle width to always be the same
as its container; set its height to 10 pixels, and set its colour to a nice boring shade of grey.

Testing
With these basic beginnings, it's enough to test the application, to see if it's working. If all that was wanted
was a little grey box in a widget, the task would be complete. However, it's good practice to confirm what's
already working, before proceeding. In the same directory that the Controls.py module has been saved,
create an HTML Loader file, ControlDemo.html, with the following contents:

<html>
<head>
<meta name="pygwt:module" content="ControlDemo">
<title>Hello</title>
</head>
<body bgcolor="white">
<script language="javascript" src="pygwt.js"></script>
</body>
</html>

Next, create a file, ControlDemo.py, again in the same directory, with the following contents:

""" testing our demo slider


"""
from pyjamas.ui import RootPanel
from Controls import VerticalDemoSlider

class ControlDemo:
def onModuleLoad(self):
b = VerticalDemoSlider(0, 100)
RootPanel().add(b)
b.setWidth("20px")
b.setHeight("100px")

Note that the Demo slider has been imported from Controls.py, which is why it was necessary to place
ControlDemo.py in the same directory. Also, note that the width and height are set, hard-coded, to 20 by
100 pixels. b.setStyleName("demoslider") could have been used, instead, but that would require the
creation of a CSS stylesheet along with referencing it in ControlDemo.html.
One thing that's great about Pyjamas: these eight lines are enough code to do exactly what is desired,
with clear and consise purpose: create a slider, add it to the root panel, set its width to 20 pixels and the
height to 100. It really couldn't get any easier. Compile the example with the following command:

python ../../builder/build.py ControlDemo.py

Open output/ControlDemo.html in a web browser, and it should result in a delightful 20x10 grey box being
displayed, which is very exciting. Next on the list is to make it move, and for that, a "click listener" is

52 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

needed.

Making it move
To receive a click event, FocusWidget.addClickListener() is to be used. The widget itself will receive the
mouse click event. Open pyjamas/ui.py and search for the function in FocusWidget called
onBrowserEvent(). For convenience, the relevant portion is shown here:

def onBrowserEvent(self, event):


type = DOM.eventGetType(event)
if type == "click":
for listener in self.clickListeners:
if hasattr(listener, "onClick"): listener.onClick(self, event)
else: listener(self, event)

Examining this function, it can clearly be seen that, in order to receive "click" events, a function called
onClick() must be added to the VerticalDemoSlider class. As we want to know where the mouse was
clicked, it is necessary to add two arguments to the onClick() function, in order to receive the mouse
event object as the second. Add these additional lines to the ControlDemo class:

def onClick(self, sender, event):

# work out the relative position of cursor


mouse_y = DOM.eventGetClientY(event) - \
DOM.getAbsoluteTop(sender.getElement())
self.moveSlider(mouse_y)

def moveSlider(self, mouse_y):

relative_y = mouse_y - DOM.getAbsoluteTop(self.getElement())


widget_height = self.getOffsetHeight()

# limit the position to be in the widget!


if relative_y < 0:
relative_y = 0
height_range = widget_height - 10 # handle height is hard-coded
if relative_y >= height_range:
relative_y = height_range

# move the handle


DOM.setStyleAttribute(self.handle, "top", "%dpx" % relative_y)
DOM.setStyleAttribute(self.handle, "position", "absolute")

As can be seen, the mouse event y position is retrieved from the incoming event, with
DOM.eventGetClientY; the absolute location of the container is subtracted, and the resultant relative
value is passed to moveSlider(). In moveSlider, the "offset height" of the widget is the total height of
the widget, giving the travel range. With a little math, the new position of the "handle" can be calculated, in
pixels. Again, learning from how AbsolutePanel sets the coordinates of its child widgets, copying some
lines of code from the function AbsolutePanel.setWidgetPosition allows the location of the slider handle
to be set.
Save the changes to ControlDemo.py, re-build and refresh the browser. Click on the slider, and... nothing
should happen. A very important line was missed out! Go back to VerticalDemoSlider.__init__ and add
this, at the end of the function, and try again:

self.addClickListener(self)

The significance of this line can be seen from how FocusPanel's onBrowserEvent function works: unless
the instance of the slider class is listening to click events, the onClick function will not be called. Once
added, and the application is recompiled, then amazing things happen! the slider widget works! A
single-click moves the slider handle to where mouse is clicked. Notice how the slider centre moves to
where the mouse pointer actually points to: this is entirely a fluke, and is probably due to browser bugs in
the CSS style implementation.
Notice also that the value of the "slider" - the control - hasn't actually been made use of, but there's

53 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

enough maths to calculate the value that it should be set to. Add these extra lines on to the end of
moveSlider():

val_diff = self.max_value - self.min_value


new_value = ((val_diff * relative_y) / height_range) + self.min_value
self.setValue(new_value)

Then, also add a setValue() function, which not only records the new value but also notifies any listeners.
Copying the style of Label and other widgets' addClickListener() and removeClickListener() functions,
add the functions addControlValueListener() and removeControlValueListener() to match:

def setValue(self, new_value):

old_value = self.value
self.value = new_value
for listener in self.valuechange_listeners:
listener.onControlValueChanged(self, old_value, new_value)

def addControlValueListener(self, listener):


self.valuechange_listeners.append(listener)

def removeControlValueListener(self, listener):


self.valuechange_listeners.remove(listener)

Notice how, right at the beginning, in the constructor, an empty list was added:
self.valuechange_listeners was initialised, and that list can now be used, as shown above, by
storing listeners that will receive notification of when the value of the slider changes. Also, notice how
we're defining the interface, here - the interaction with other widgets - by expecting classes to have a
function called onControlValueChanged() that will take as parameters the sender, the old value and
the new value of the slider control. The reason for this is that a listener will find it useful to know not only
which widget changed, and also the new value, but also it may be useful to a listener to know by how
much a value changed.
Save the file, as the next step is to check that this code works. In the "test code", ControlDemo.py, add
these extra lines to ControlDemo.onModuleLoad() and also add the additional function
onControlValueChanged:

b.addControlValueListener(self)
self.label = Label("Not set yet")
RootPanel().add(self.label)

def onControlValueChanged(self, slider, old_value, new_value):


self.label.setText("Value: %d" % int(new_value))

Note how the ControlDemo class is set as a listener for change notification: this is why an
onControlValueChanged function was added. In it, the label's text will be changed.

(Also, remember to add an import of Label to the top of the


file - the line "from pyjamas.ui import Label" will suffice).

Save the changes to ControlDemo.py, recompile the application with build.py, and refresh the browser
window (or reopen output/ControlDemo.html). There should be a very boring Text Label 200 pixels
underneath a grey box, with the words "Not set yet" displayed. Clicking anywhere between the box and
the words should not only move the slider, but also changes the text to say "Value: 83", or something to
that effect. Amazing.
Congratulations, a slider has been created, in 70 lines of python code, and a demonstration of its use in
20. It can be clicked on it. Try to resist the urge to press the up and down arrows and to click and hold the
mouse: it won't work.

What's been covered


In this section, the following issues have been covered:

54 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

How to create a widget that can react to user-input, by deriving from FocusWidget
How to use manipulation of the DOM model to change screen-content, by adding HTML Elements
to it
How to use the DOM module to change attributes and CSS Style attributes
The importance of checking the W3C HTML Specifications when it comes to distinguishing the
difference between HTML Element attributes and HTML Element CSS attributes
How a Pyjamas ui widget is effectively a "guardian" of HTML elements - parts of the DOM model.
How a widget could be made to react to user-input and other events, by using the features
provided by an onBrowserEvent function of a base class (in this example, FocusWidget).
The importance of looking at the existing pyjamas.ui source code to work out what is going on, and
the importance of identifying, copying and adapting sections of existing code.
This latter advice is perhaps just general good advice for programming. In the Pyjamas case, given that
the pyjamas.ui module is really quite small, it's a lot easier advice to follow than if confronted with tens of
thousands of lines of code.
Also, it's worth reiterating at this point that at no time, anywhere in the code, was there any mention of a
"repaint" function, a "redraw", "refresh" or other function that otherwise interferes with the simple, simple
job of doing exactly what is wanted: putting stuff on-screen. Although many people bitch like hell about
how "difficult" web browsers are, the bitching is invariably down to the requirement to get involved with
Javascript. Web browser technology actually does a stunningly good job, and is clearly a better basis for
a Desktop widget framework than most Desktop widget frameworks! And, given that the programming is
being done in Python, one of the key reasons for all the complaining goes away.

The reason why "repaint" isn't needed is because whenever a part of the DOM
model is changed, the underlying browser technology gets an automatic
notification to take care of redrawing the screen. It does have to be said
that in certain circumstances, certain browsers (in particular IE6 and IE7)
don't actually get it right every time - and even Firefox can be made to screw
up, if pushed particularly hard in the right places. However, with care and
attention, and a little studying of HTML and CSS behaviour, the automatic
redrawing will be perfect every time.

Improvements to the Slider: Event Handling and Capture


The simple slider receives a basic "click" to move the slider. Of much more interest is to be able to drag
and move the slider, as this is the kind of behaviour that is more intuitive to users. As there already is
demo slider to work from, that will be used as a base class. The primary focus of this section will
therefore to add mouse event handling - up, down, and move.
Add the following lines to Controls.py:

class VerticalDemoSlider2(VerticalDemoSlider):

def __init__(self, min_value, max_value, start_value=None):

VerticalDemoSlider.__init__(self, min_value, max_value, start_value)


self.mouseListeners = []
self.addMouseListener(self)
self.sinkEvents(Event.MOUSEEVENTS)
self.dragging = False

def addMouseListener(self, listener):


self.mouseListeners.append(listener)

def removeMouseListener(self, listener):


self.mouseListeners.remove(listener)

Here, VerticalDemoSlider2 is derived from VerticalDemoSlider. The class adds itself as a "mouse
listener" - and indicates to the DOM model that notifications about all Mouse Events must be received.

Examine pyjamas/ui.py and look for "class FocusWidget".


Note how FocusWidget sinks the events ONCLICK, FOCUSEVENTS and KEYEVENTS.
This is why VerticalDemoSlider, which derives from FocusWidgets, was

55 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

capable of receiving mouse click events (through FocusWidget's


onBrowserEvent function).

By asking VerticalDemoSlider2 to receive MOUSEEVENTS, when the mouse moves into the widget, a
"mouse enter" event is received; also, all move, down and up events are also received for as long as the
mouse is over the element and, when the mouse moves out, a mouse "leave" event is received. All of
these events will result in onBrowserEvent being called, which is the next key function needed:

def onBrowserEvent(self, event):


type = DOM.eventGetType(event)
if type == "mousedown" or type == "mouseup" or \
type == "mousemove" or type == "mouseover" or type == "mouseout":
MouseListener().fireMouseEvent(self.mouseListeners, self, event)
else:
VerticalDemoSlider.onBrowserEvent(self, event)

Here, as can be seen, it's very straightforward: mouse events are passed to a convenience function
(fireMouseEvent) which will call the appropriate handler functions on each of the listeners. These
lines of code were copied verbatim from other pyjamas/ui.py widgets that deal with mouse events:
Label for example. Also, note how, if the type of the event is not found to be a mouse event,
VerticalSliderDemo (the base class) is asked to handle the event. In this way, mouse click, focus and
keyboard events will still be dealt with. Without this explicit call through to the base class function (which is
actually in FocusWidget, as VerticalSliderDemo doesn't have its own onBrowserEvent function), it would
be extra and unnecessary work to handle these events.
Search for "class MouseListener" in pyjamas/ui.py. Examining fireMouseEvent, it can be seen that
there are five possible functions that could be called (on every one of the listeners, in turn):
onMouseDown, onMouseUp, onMouseMove, onMouseLeave and onMouseEnter. Therefore, it is
necessary to add each of these four functions - even if all of them are not used.
onMouseEnter and onMouseLeave, are going to be left blank. However, if it was desirable to make the
slider visually "respond" to the mouse hovering over the slider, it is these two functions that would take
care of that - perhaps by adding and removing a CSS stlye, using addStyleName().

def onMouseEnter(self, sender):


#self.addStyleName("sliderhighlighted")
pass
def onMouseLeave(self, sender):
#self.removeStyleName("sliderhighlighted")
pass

The core of the work is done in the remaining three functions. Firstly, onMouseMove, which takes care
of translating the mouse pointer's absolute y position into actual movement of the slider:

def onMouseMove(self, sender, x, y):


if not self.dragging:
return
self.moveSlider(y)

Notice how slider movement is only allowed when "dragging" is set. If this wasn't done, then the slider
would immediately start to jump and move when the mouse cursor was placed over the widget, as mouse
"move" events occur immediately that the mouse enters the DOM model Element that the widget is
responsible for. Clearly this is undesirable: the expected behaviour is for the slider to move only when a
mouse button is held down:

def onMouseDown(self, sender, x, y):


self.dragging = True
DOM.setCapture(self.getElement())
self.moveSlider(y)

So, when a mouse down event occurs, the "dragging" flag is set to True. An indication is made, using
DOM.setCapture that all future events will be sent to this widget; the slider is also moved. The reason
for moving the slider is because it is a separate event from "mouse move", and people will get rather

56 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

annoyed if the the "mouse button" is clicked and the slider stays where it is. The reason for capturing
mouse events from now on is because users are quite likely to move the mouse out of the widget whilst
the button is still held down, and will expect the slider to still move. Only when the user lets go of the
mouse button will it be expected that the slider will stop moving about:

def onMouseUp(self, sender, x, y):


self.dragging = False
DOM.releaseCapture(self.getElement())

As can be seen, all that is necessary is to simply indicate, by setting dragging to False, that the slider
should no longer move about when "mouse move" events occur, and for event capturing to be cancelled.
This is all that is required to turn the "click-only" slider into a dragging-capable one. The majority of the
user-interface hard work was done in the previous section - all that's being done here is that
VerticalDemoSlider's moveSlider() function is called at the appropriate times.
Save the changes to Controls.py, then either copy the ControlDemo.py and associated HTML file, to
ControlDemo2, or simply change the existing ControlDemo.py to use VerticalDemoSlider2 instead of
VerticalDemoSlider. Recompile the code with this command:

python ../../builder/build.py ControlDemo.py

Open output/ControlDemo.html with a web browser, click-and-hold the mouse anywhere on the
slider and then move the mouse up or down, within its 200 pixel range. The value will change and be
displayed in the Label, below the slider.

Observe, occasionally, that mouse-drag results in selecting text!


This Is Bad. It's to do with event handling, and there's a way to
stop it.

Mouse events have a default behaviour, in browsers, and a "mouse down" and "drag" results in text
selection. To terminate this behaviour, it is necessary to get at the event "early" - before it gets handed
on to the browser's underlying sub-system. Take a look at pyjamas/ui.py and search for "class
DialogBox" and look for the function onEventPreview. Copy those lines, and add them to
VerticalDemoSlider2:

def onEventPreview(self, event):


# preventDefault on mousedown events, outside of the
# control, to stop text-selection on dragging
type = DOM.eventGetType(event)
if type == 'mousedown':
target = DOM.eventGetTarget(event)
event_targets_control = target and \
DOM.isOrHasChild(self.getElement(), target)
if event_targets_control:
DOM.eventPreventDefault(event)
return VerticalDemoSlider.onEventPreview(self, event)

Also, add this line to VerticalDemoSlider2's constructor:

DOM.addEventPreview(self)

It can be seen that, when a "mouse down" event occurs, the "default" behaviour - in this case, text
selection - is prevented, through the use of DOM.eventPreventDefault. However, previewing of
events consumes CPU resources, especially given how many mouse move events there can be. So,
sending every single browser event through a widget should only be done if it is absolutely necessary.

What's been covered


This section added a more user-friendly behaviour to the slider: dragging. Quite specific sections of
code were cut-and-paste lifted from pyjamas/ui.py's DialogBox and PopupPanel to provide this
behaviour, and the following issues were covered:

57 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

Calling DOM.sinkEvents() to register event types, with the DOM subsystem, that the widget
should be notified of.
Adding an onBrowserEvent() function to receive and handle mouse events, and the
importance of passing on any other events to the base class.
How fireMouseEvent() provides some standard behaviour that makes mouse event handling
easier to write.
How to implement dragging, by noting when the mouse is down, and only reacting to mouse move
events when the mouse is down.
The importance of calling DOM.setCapture() to ensure that all events go to the widget whilst the
mouse is down.
The importance of calling DOM.releaseCapture() on mouse-up, to give control back to the
browser!
How and why an onEventPreview() should be used.
In dealing with the issues, from the user requirements of providing mouse "drag" capability, there's
something near-fatalistic about the way the code emerged. The code almost wrote itself, according to the
requirements; the rules of the underlying DOM model; the existing Pyjamas API and the way in which
sections of code were borrowed near-verbatim from other widgets.

TODO
Keyboard Events and Keyboard Focus

module ui
NAME
ui

FILE
/home/lkcl/src/pyjamas-desktop/pyjamas-webkit/pyjamas/ui.py

DESCRIPTION
This module implements the Pyjamas user-interface widget set for Pyjamas-Desktop.

Classes
CellFormatter

FlexCellFormatter

DockPanelTmpRow

Event

EventObject

FormSubmitCompleteEvent

FormSubmitEvent

Focus

FocusListener

HasAlignment

HasHorizontalAlignment

58 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

HasVerticalAlignment

KeyboardListener

LayoutData

MouseListener

RowFormatter

UIObject

MenuItem

TreeItem

RootTreeItem

Widget

Composite

ClickDelegatePanel

TabBar

TabPanel

FileUpload

FocusWidget

ButtonBase

Button

CheckBox

RadioButton

ListBox

TextBoxBase

PasswordTextBox

TextArea

TextBox

Frame

NamedFrame

59 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

Hidden

Hyperlink

Image

Label

HTML

MenuBar

Panel

ComplexPanel

AbsolutePanel

RootPanelCls

CellPanel

DockPanel

HorizontalPanel

VerticalPanel

DeckPanel

FlowPanel

HTMLPanel

StackPanel

HTMLTable

FlexTable

Grid

SimplePanel

FocusPanel

FormPanel

60 of 61 08-03-2009 18:11
Pyjamas Book file:///C:/Documents%20and%20Settings/Administrator/My%20Docum...

PopupPanel

DialogBox

MenuBarPopupPanel

ScrollPanel

TreeContentPanel

Tree

Functions
HTMLPanel_createUniqueId()
RootPanel(element=None)
The function which is used to obtain a Root Panel, to which
all widgets must ultimately be added.

@param element: element can be None, in which case the default


root panel is returned.
element can be a string, in which case the
widget in the underlying DOM model with the
'id' given by element is returned.
element can also be a node in the underlying DOM model.
A good way to think of this is in terms of an
HTML document. if element is None, the HTML
"body" is returned. if element is a string,
getElementById('element') is returned.

Data
CheckBoxUnique = 0
FormPanel_formId = 0
HTMLPanel_sUid = 0
prefetchImages = {}
rootPanels = {}

61 of 61 08-03-2009 18:11

Anda mungkin juga menyukai