Python Hulahop
XULrunner the application is stand-alone, and is responsible for
bringing up a window, in which the web application is displayed.
pyxpcomext provides the linkage between XULrunner and python.
Python Hulahop does something similar, but starts from a different
angle. Hulahop joins a standard GTK window to the technology
behind XULrunner, thus requiring the python developer to use
"import hulahop", to create a hulahop WebView and to add it
to a GTK app just as you would any other GTK widget, and to
use "import xpcom" to then gain access to the WebView's
interfaces.
This is a completely different perspective, which allows the developer
to embed XULrunner into GTK applications as "just another widget".
This article will therefore first cover the "startup" phase, and then
go on to describe how to interact with the resultant widget, including
being able to respond to DOM events (with python callbacks). Future
articles will cover more complex topics such as how to use XMLHttpRequest
from python.
Diving in
First, two files must be created - hula.py and progresslistener.py.
hula.py is adapted from the OLPC "web browser", and progresslistener.py
is copied verbatim from the same project.
Here is the hula.py file:
# Copyright (C) 2006, Red Hat, Inc.
# Copyright (C) 2007, One Laptop Per Child
# Copyright (C) 2009, Luke Kenneth Casson Leighton <lkcl@lkcl.net>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import os
import hulahop
#from sugar import env
#hulahop.startup(os.path.join(env.get_profile_path(), 'gecko'))
# this is equivalent to ~/.firefox profiles subdirectory
hulahop.startup('/home/lkcl/test')
from hulahop.webview import WebView
import gtk
import gobject
import xpcom
from xpcom.nsError import *
from xpcom import components
from xpcom.components import interfaces
from progresslistener import ProgressListener
class ContentInvoker:
_com_interfaces_ = interfaces.nsIDOMEventListener
def __init__(self, browser):
self._browser = browser
def handleEvent(self, event):
print event.type, event.button
target = event.target
print "target:", target.tagName.lower()
class Browser(WebView):
def __init__(self):
WebView.__init__(self)
self.progress = ProgressListener()
io_service_class = components.classes[ \
"@mozilla.org/network/io-service;1"]
io_service = io_service_class.getService(interfaces.nsIIOService)
# Use xpcom to turn off "offline mode" detection, which disables
# access to localhost for no good reason. (Trac #6250.)
io_service2 = io_service_class.getService(interfaces.nsIIOService2)
io_service2.manageOfflineStatus = False
self.progress.connect('loading-stop', self._loaded)
self.progress.connect('loading-progress', self._loading)
def do_setup(self):
WebView.do_setup(self)
self.progress.setup(self)
listener = xpcom.server.WrapObject(ContentInvoker(self),
interfaces.nsIDOMEventListener)
self.window_root.addEventListener('click', listener, False)
def _loaded(self, progress_listener):
""" once document is loaded, we can now "do" stuff.
until the document is "loaded", it's unsafe to go
messing with the DOM, because stuff either isn't
initialised properly or, more importantly, the
loaded document will overwrite anything that's
been done up to that point!
"""
doc = self.get_dom_window().document
body = doc.createElement("body")
n = doc.createTextNode("hello")
doc.body = body
body.appendChild(n)
r = body.getBoundingClientRect()
print "body rect", r.bottom, r.top, r.left, r.right
body.style.setProperty('background-color', '#00ff00', None)
def _loading(self, progress_listener, progress):
print "loading", progress
win = gtk.Window(gtk.WINDOW_TOPLEVEL)
win.set_size_request(800,600)
win.connect('destroy', gtk.main_quit)
wv = Browser()
wv.show()
win.add(wv)
win.show()
# you have to create a blank document, to trigger a loading callback,
# unfortunately.
f = open("/tmp/blank.html", "w")
f.write("")
f.close()
# load the blank document.
wv.load_uri('file:///tmp/blank.html')
# start gtk
gtk.main()
Briefly, then: the equivalent of firefox profile directory is set up;
a ContentListener class is declared, which is used to wrap event
handling; the main "Browser" class is declared (in which the key
point of interest is the _loaded function); the rest is GTK and
startup code.
Here is the progresslistener.py file:
# Copyright (C) 2006, Red Hat, Inc.
# Copyright (C) 2007, One Laptop Per Child
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import gobject
import xpcom
from xpcom.components import interfaces
class ProgressListener(gobject.GObject):
_com_interfaces_ = interfaces.nsIWebProgressListener
__gsignals__ = {
'location-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
([object])),
'loading-start': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
([])),
'loading-stop': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
([])),
'loading-progress': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
([float]))
}
def __init__(self):
gobject.GObject.__init__(self)
self.total_requests = 0
self.completed_requests = 0
self._wrapped_self = xpcom.server.WrapObject( \
self, interfaces.nsIWebProgressListener)
weak_ref = xpcom.client.WeakReference(self._wrapped_self)
self._reset_requests_count()
def setup(self, browser):
mask = interfaces.nsIWebProgress.NOTIFY_STATE_NETWORK | \
interfaces.nsIWebProgress.NOTIFY_STATE_REQUEST | \
interfaces.nsIWebProgress.NOTIFY_LOCATION
browser.web_progress.addProgressListener(self._wrapped_self, mask)
def _reset_requests_count(self):
self.total_requests = 0
self.completed_requests = 0
def onLocationChange(self, webProgress, request, location):
self.emit('location-changed', location)
def onProgressChange(self, webProgress, request, curSelfProgress,
maxSelfProgress, curTotalProgress,
maxTotalProgress):
pass
def onSecurityChange(self, webProgress, request, state):
pass
def onStateChange(self, webProgress, request, stateFlags, status):
if stateFlags &
interfaces.nsIWebProgressListener.STATE_IS_REQUEST:
if stateFlags &
interfaces.nsIWebProgressListener.STATE_START:
self.total_requests += 1
elif stateFlags &
interfaces.nsIWebProgressListener.STATE_STOP:
self.completed_requests += 1
if stateFlags &
interfaces.nsIWebProgressListener.STATE_IS_NETWORK:
if stateFlags &
interfaces.nsIWebProgressListener.STATE_START:
self.emit('loading-start')
self._reset_requests_count()
elif stateFlags &
interfaces.nsIWebProgressListener.STATE_STOP:
self.emit('loading-stop')
if self.total_requests < self.completed_requests:
self.emit('loading-progress', 1.0)
elif self.total_requests > 0:
self.emit('loading-progress', float(self.completed_requests) /
float(self.total_requests))
else:
self.emit('loading-progress', 0.0)
def onStatusChange(self, webProgress, request, status, message):
pass
Briefly: the OLPC team have linked python-gobject and the xpcom
"nsIWebProgressListener" interface together, so that gobject
callbacks to python functions can occur when xpcom progress
events occur.
What happens?
Firstly, and crucially, the equivalent of the firefox profiles
subdirectory is set up, if it doesn't exist:
hulahop.startup('/home/lkcl/test')
In this subdirectory, you will find that prefs.js, xpti.dat, pluginreg.dat,
cert8.db and many other XULrunner / Gecko essential files are created,
along with a Cache subdirectory.
Next, a GTK window is created, of size 800 by 600, and a Browser instance
is created (Browser is derived from hulahop.WebView). Note also that
a progress listener instance is created:
self.progress = ProgressListener()
self.progress.connect('loading-stop', self._loaded)
self.progress.connect('loading-progress', self._loading)
It's thanks to ProgressListener, which wraps the nsiWebProgressListener
interface, that the Browser._loaded and Browser._loading functions will
be called (through gobject signals) as the page load progresses and
finishes. Of course, it's necessary to load a page, but that
will be covered shortly. Before then, it's important to note the
Browser.do_setup function:
WebView.do_setup(self)
self.progress.setup(self)
listener = xpcom.server.WrapObject(ContentInvoker(self),
interfaces.nsIDOMEventListener)
self.window_root.addEventListener('click', listener, False)
The OLPC team created a convention whereby WebView.do_setup will be
called at an appropriate time (presumably after XULrunner interfaces
are initialised properly, which is something that has to be taken
into consideration). We take advantage of this to set up the WebView
itself, and the progress listener, and then also to add an Event
Listener to window. As can be seen, this is the voodoo-magic
incantation that is equivalent to "window.onclick = function()
{...}"
in javascript.
It is absolutely crucial to note the use of the ContentInvoker class,
here. There are several conventions which the various XULrunner interfaces
expect. In the case of nsIDOMEventListener, the class instance
that is wrapped using xpcom.server.WrapObject is expected to have a
method "handleEvent". In the case of nsIWebProgressListener, the
class instance that is wrapped is expected to have functions
onLocationChange, onProgressChange, onStateChange etc. even if those
functions are not used (hence, in progresslistener.py, the use of
"pass" to ensure that runtime exceptions do not occur when the XULrunner
engine cannot find the callbacks it expects).
Now we get to trigger the Browser._loading function, by creating
a dummy web page containing no content:
f = open("/tmp/blank.html", "w")
f.write("")
f.close()
# load the blank document.
wv.load_uri('file:///tmp/blank.html')
It's worth noting here that the use of a blank document is entirely
deliberate for demonstration purposes, but that literally any content,
and any valid URL, could be given here. Absolutely anything, including
a remote HTML page, another XUL application (with a chrome: URI), is
perfectly acceptable. All we care about, however, is that the page
successfully loads, and Browser._loaded gets called.
Finally, we get to do something!
After all that voodoo incantation magic, we can get at the browser
document instance, confident that it will not "disappear" out from
under us, and confident that all the XULrunner interfaces have been
initialised and will be valid. Now it's possible, in Browser._loaded,
to do something:
doc = self.get_dom_window().document
body = doc.createElement("body")
n = doc.createTextNode("hello")
doc.body = body
body.appendChild(n)
r = body.getBoundingClientRect()
print "body rect", r.bottom, r.top, r.left, r.right
body.style.setProperty('background-color', '#00ff00', None)
None of these functions and properties should come as a surprise: they
should in fact look eerily familar to anyone who has done javascript
programming. However, we're no longer in Kansas: this is python!
Now it can be seen clearly why a blank document was loaded, so as to
be able to demonstrate the use of nsIDOMDocument's createElement
function to create a <body> element, and place it into the
document. A text node is also created, with the word "hello" in it,
and this is added to the body element using appendChild. For
demonstration purposes, getBoundingRectClient is called, in order
to print (on the standard console) the outer bounds of the body element.
Finally: instead of having a separate CSS stylesheet, the body's
"background-color" property is modified to an eye-glaring green.
Not a single bit of javascript is involved.
Window Event Handling
Lastly - there's just one more trick that's easy to miss: the nsIDOMWindow
click handler that was created in Browser.do_setup(). When running
the application, click anywhere in the window. On the standard console,
there should be some output: "click 0" and "html" should typically be
observed, indicating that the left button was used and that the main
content was clicked. Clicking on the word "hello" should show the word
"body" instead. Examine ContentInvoker.handleEvent to find out why:
def handleEvent(self, event):
print event.type, event.button
target = event.target
print "target:", target.tagName.lower()
So, it can be seen that the actual event can be accessed, just as it
can as if this was javascript, not python. Note also that because
of the ContentInvoker class instance, it's possible to gain access
to the Browser window (through self._browser), so there is absolutely
no excuse for creating global variables: it's completely unnecessary
to do that. If more state information is required in the event handler,
adapt ContentInvoker and add more arguments to its constructor, passing
them in when the listener xpcom wrapper is created.
Conclusion
Through a little bit of voodoo magic, thanks to the OLPC Sugar team's
efforts, a declarative style of python web programming is now possible.
As Hulahop is also a GTK Widget as well as a XULrunner / Gecko Engine,
it's possible to embed Gecko into standard python gtk2 applications
and still manipulate and interact with the DOM model. Adding <embed<>>
nodes to the DOM, on-demand, to fire up NPAPI plugins such as Flash and Java
plugins is perfectly within the realms of possibility.
Taking DOM model manipulation to its fullest logical and inevitable
conclusion,
an entire new python GUI Widget Set API can be created, utilising absolutely
every single feature that is available in the DOM of web browser engine
technology. The result is Pyjamas Desktop
So, for the curious: here in
Pyjamas
Desktop
is a more sophisticated version of hula.py (progresslistener.py is still
the same), and its cousin, pywebkitgtk.py. These two, hula.py and
pywebkitgtk.py, are slowly being made to be identical, so that declarative
applications can be created which will have exactly the same underlying
python API as far as developers are concerned, and the developer or even
the user can choose, at runtime, whether to pick XULrunner or WebKit,
with the application needing absolutely no modifications whatsoever.