Wednesday, June 17, 2009

usb-creator-kde - adventures in gobject land

I'll first start out with a disclaimer: I am not fluent in Qt nor Gtk programming. What I know or have learned is through the internet, various books and reading others code. So, what I describe below may not be the best way to achieve the end result, but it appears to work and allow for minimal interference with an existing application backend.

Now that that's out of the way, I'll proceed with the discussion: porting gobject calls to Qt so that I can get a usable KDE/Qt frontend for usb-creator-kde.

For the gtk client (usb-creator), the backend currently implements timers, callbacks, and process watchers via gobject. Similar mechanisms exist for Qt. The current backend is unsuitable for general use by both a gtk and Qt frontend as gobject and Qt seem to tromp all over one another, and cause the frontend to crash. TO get around this, we need to move the gobect calls to the gtk frontend, and implement wrappers that the backend can call from the frontend. Once we have wrappers in place, we can then re-implement the wrappers in our desired frontend (e.g. PyKDE).

Here's an example of some code from the backend:

self.timeouts[udi] = gobject.timeout_add(UPDATE_FREE_INTERVAL, self.update_free, udi)
And from the PyGtk manual:

timeout_add
int timeout_add(int interval, callback callback [, mixed user_data1, ... ]);

Registers a function to be called periodically. The function will be called repeatedly after interval milliseconds until it returns false (or nothing) at which point the timeout is destroyed and will not be called again.

So to keep the timeout alive, your callback function needs to return true;
Unfortunately, there is no single call in Qt that provides this mechanism (none that I know of). So, to implement this, I needed to write a couple of functions, using unnamed arguments lists, lambda notation, etc.

First off, we need a generic timer callback function that will call our passed function, test the return value, and stop the timer if the return value is not True. I also want this function to be private to my frontend class. Here what it looks like:

def __timeout_callback(self, func, *args):
'''Private callback wrapper used by add_timeout'''

timer = self.sender()
active = func(*args)
if not active:
timer.stop()
So, func is the passed calback function to execute, followed by a list of optional arguments *args. The sender will be a timer object, which we get from self.sender, assuming that the parent is some QObject or derivation thereof (in my case, the frontend class KdeFrontend is derived from QObject).

Ok, that allows us to have an arbitrary function with any number of arguments, and have it stop a timer when appropriate. We now need to implement the public wrapper that will use this private callback. Here is the code for that:

def add_timeout(self, interval, func, *args):
'''Add a new timer for function 'func' with optional arguments. Mirrors a
similar gobject call timeout_add.'''

timer = QTimer()
QObject.connect(timer,
SIGNAL("timeout()"),
lambda: self.__timeout_callback(func, *args))
timer.start(interval)

return timer
The add_timeout function takes the same parameters as the gobject.timeout_add function. Inside, we setup a new timer, connect it to our private callback, start the timer and return a reference to it. The magic is in how we connect the passed function func. Notice that we use lambda to call our private callback, passing along the func and *args. Normally, you do not pass a function with variable parameters to Qt connect statements, but in our case, we absolutely are required to do so. This is where using lambda comes in handy.

So, there you have it. A way to implement gobject.timeout_add using Qt. While everyday use of this is not likely, it will certainly help in porting applications from PyGtk to PyQt. I hope someone out there finds this useful. I know I searched for an easy way to do this, and never found anything. After lots of trial and error and asking lots of questions to my fellow developers, I was able to come up with the above.

Cheers.