Friday, November 26, 2010

Testing Interactive Code

The preceding work on the Render and Project Settings dialogs in PiTiVi has led to some fairly complicated UI logic, and we haven't really got a good way to test it.

Imagine that we want to test the following trivial python UI
import gtk
import gobject
import random

def b_clicked_cb(button):
    clicked = True
    button.props.label = "Ouch!"

w = gtk.Window()
v = gtk.VBox()
b = gtk.Button("Click me")
b.connect("clicked", b_clicked_cb)
v.pack_start(b)
w.add(v)
w.show_all()

gtk.main()
We want to check that the button label is correctly changed after being clicked. Because of control inversion, we obviously can't code in a direct, sequential style. At the very least we must install a timeout or idle function into the main loop.

This is a naive approach:
def test_case():
    b.activate()
    assert b.props.label == "Ouch!"
    return False

gobject.timeout_add(1000, test_case)
gtk.main()

It doesn't work because of timing issues. The button's label won't change immedately after activate() is called, so you get the following error -- despite the fact that the the label clearly changed.

Traceback (most recent call last):
  File "figure_1.py", line 19, in test_case
    assert b.props.label == "Ouch!"
AssertionError

The obvious solution is to split the callback in twain:

def test_case():
    b.activate()
    gobject.timeout_add(1000, finish_test)
    return False

def finish_test():
    assert b.props.label == "Ouch!"
    gtk.main_quit()
    return False

gobject.timeout_add(1000, test_case)
gtk.main()

It's good enough for this trivial example, but it should be clear that as the complexity of the UI increases you'll end up with yet another maze of callbacks.

Last night I experimented with applying python generator functions to this problem. The idea is to (ab)use the yield keyword as a way of passing control between the test and the mainloop. It's similar to concepts presented in this paper, which describes how continuations can be used to solve the problem of control inversion in web programming.

Quick summary of python generator functions: the yield keyword in python is a limited form of either lazy evaluation or continuations, depending on your point of view. Usually we think of generators as sequences. For example this generator can be thought of as the sequence of positive integers from 1 to 10
def ints():
   i = 1
   while i <= 10:
      yield i
      i += 1
I can treat it as sequence and take its sum, or iterate over it:
print sum(ints())
squares = [x ** 2 for x in ints()]
What actually happens when squares is called is that an iterator is created and returned to the caller. Yield indicates to the interpreter that squares() is a generator, and that the state should be saved so it can be re-entered later. The fact that the state of the function is saved inside iterator allows us to think of the iterator as a continuation (generators are not as powerful as continuations). With this in mind, we re-write our test function:
def test_case():
    b.activate()
    yield
    assert b.props.label == "Ouch!"
Here the yield keyword is going to provide the same control flow boundary that splitting our code into separate functions did earlier. Now all we need is a bit of code to consume values from this iterator until it is exhausted. We do this using timeouts -- as we did in earlier examples -- which allows the mainloop to continue running in between the two halves of our test case.
def run_test_case(iterator):
    print "Tick"
    try:
        iterator.next()
    except StopIteration:
        print "Test Case Finished Successfully"
        gtk.main_quit()
        return False
    except Exception, e:
        print "An error occured"
        gtk.main_quit()
        return False
    return True

gobject.timeout_add(1000, run_test_case, test_case())
gtk.main()
This is already an improvement, but not yet flexible enough. Suppose we add a new widget:
def c_clicked_cb(button):
    def set_label_async():
        c.props.label = "Ouch!"

    gobject.timeout_add(random.randint(500, 5000),
        set_label_async)
c = gtk.Button("Async Operation")
c.connect("clicked", c_clicked_cb)
c.show()
v.pack_start(c)
Now we have a problem: we have no idea when the action triggered by clicking the second button will complete. Simply waiting for one second will not always work. At the very least we should be able to override the default sleep value. But it would be better still if we could wait until the label value itself is changed. That way if the action takes only a short time, we don't have to wait, while if the action takes longer than expected, the test can still finish successfully. In other words, we shouldn't just assume that after each step in the test we straight on to the next one. Let's factor out portion of the loop that does the scheduling:
class Sleep(object):

    def __init__(self, timeout=1000):
        self.timeout = timeout

    def schedule(self, iterator):
        gobject.timeout_add(self.timeout, run_test_case, iterator)

def run_test_case(iterator):
    print "Tick"
    try:
        scheduler = iterator.next()

    except StopIteration:
        print "Test Case Finished Successfully"
        gtk.main_quit()
        return False

    except Exception, e:
        print "An error occured"
        gtk.main_quit()
        return False

    scheduler.schedule(iterator)
    return False
Now we can easily customize the timeout for the second button specifying a Sleep scheduler with a different timeout.
def test_case():
    b.activate()
    yield Sleep()
    assert b.props.label == "Ouch!"

    c.activate()
    yield Sleep(6000)
    assert c.props.label == "Ouch!"
Actually we can go one better. We don't have to rely on timeouts for scheduling at all. For example, we can easily define a scheduler that will wait for a signal to fire:
class WaitForSignal(object):

    def __init__(self, obj, signame):
        self.obj = obj
        self.signame = signame
        self.iterator = None
        self.sigid = None

    def schedule(self, iterator):
        self.sigid = self.obj.connect(self.signame, self._handler)
        self.iterator = iterator

    def _handler(self, *args):
        run_test_case(self.iterator)
        self.obj.disconnect(self.sigid)
Adopting this is just a one line change to test_case():
def test_case():
    b.activate()
    yield Sleep()
    assert b.props.label == "Ouch!"

    c.activate()
    yield WaitForSignal(c, "notify::label")
    assert c.props.label == "Ouch!"
And we don't have to touch run_test_case() at all. I think this idea could be expanded into a framework for testing event-driven code. True, I would want much better error reporting. But with just that, it would be pretty straight-forward to cover every part of PiTiVi's interface except the timeline (for the Timeline, I need is the ability to synthesize raw input). If necessary, I can include other types of scheduling scenarios, such as waiting for a file or socket access. And, because I can work with the widgets directly, it's possible to verify conditions that would be impossible to check for under Dogtail or LDTP (which are both limited to what AT-SPI exposes, and run the test from a separate process). Full Source

And the winner is...

The overwhelming majority went for version B. I changed the layout a bit, and a cleaned up version is what's in my branch now. Thanks, everyone.

Thursday, November 18, 2010

Round 4

So it's down to two competing ideas. At this point my brain is shot and I can't really decide between them. So I've decided to put them up here and see what people think. The best way to compare the two is right-click the images into separate tabs and switch between them a few times. Take a good hard look at each one.

What I'm looking for here is clarity of ideas, not cosmetic considerations like alignment and balance. That is, which of these dialogs is easier to understand?

A


B













Now that you've done that, does anything seem to be missing from either one that would make it an improvement over the other?

Wednesday, November 17, 2010

Round 3

09:38 < bemasc> emdash: So, the true parameters here are the storage height,
storage width, PAR, and DAR.
09:39 < bemasc> that's 4 values in total, but only 3 degrees of freedom.
09:40 < bemasc> Personally, I would expose all 4, and update PAR after each change to DAR (and vice versa)

How about this, then?
Starting to look a bit better? I kept the notion of 'Display resolution' mainly as a source of feedback, but it occurs to me now that perhaps little rectangle that represents the display aspect ratio would be a better idea. Unfortunately that would mean increasing the size of the dialog again.

I'm actually not so sure that DAR and PAR should update each other, as I played around with it and it's somewhat frustrating. I think most of the time you're either targeting a specific format (for example, 480i) in which case you'd just choose a preset, or you'll be working with square pixels.

I think i need to add a 'constrain aspect ratio' check-button to make scaling a bit easier.

Project Settings Progress

Re-worked it a bit this afternoon. But i'm still not happy with it. I think it might be too confusing to expose pixel-aspect-ratio to the user (hell, it's confusing enough for the programmer).



Another idea: present display width / height, optionally allowing the user to constrain to an aspect ratio. Separately present content (storage) width / height, and finally show the pixel-aspect-ratio as a read-only label, as a form of feedback. In other words, PAR would be computed working backward from display and content resolutions.

Mockup: The diagram on the bottom shows how updates
would propagate between widgets

Tuesday, November 16, 2010

PiTiVi Dialogs

I am (finally) making progress on the two main dialogs in pitivi: "Project Settings" and "Render". I'll just get right to the point: Screenshots!

Render dialog showing video tab

Render dialog showing general tab
Project settings showing video tab

I spent most of my time today on project settings. I'm still not quite happy with it, as the current design is, well, huge. I think I could make it a little narrower and shorter. But that's more-or-less what I'm after.

The properties in the video tab are inter-dependent. One feature I wanted was for the widgets to ripple-update like a spreadsheet. To make this easier I wrote a class that manages a graph of widgets that you give it. Each widget can have an update function, and when the user changes one of the widgets, the the update functions are called starting from that widget in breadth-first order. During the traversal, updates are ignored to prevent infinite loops of signal emissions. This means I get my ripple updating behavior, but I only have to write a single, naive update function for each widget without worrying about keeping other widget values in sync.

Tomorrow I will implement presets, make sure the dialogs remember their state, and then test the hell out of them.

Monday, November 1, 2010

Side Project

I started working in a calendar app. Right now you can think of it mainly as a prototype, but it's already got some interesting features. It's implemented in python, and while it relies on goocanvas at the moment, the calendar view is actually one big canvas item and could easily be ported to a stand-alone widget.



Interface

At first glance it looks like the week view used in most timelines. However, the view is an infinite scrolling area. There is no horizontal scroll bar, as this is a ranged widget. You move in time by clicking and dragging on the day heading. If you flick the heading, the calendar will continute to 'coast', slowing down gradually until it stops (or immediately if click on it).


I've found that I rarely look at the month view in a calendar application, so I sought to eliminate it. My calendars tend to have a lot of daily repeating events, making the month view seem cluttered. I mainly use it for skipping forward or backward to a specific week without loosing context, or to see a group of days that span a weekend.

With infinite scrolling I found I really don't need the month view. I don't loose context skipping around because of the smoothness of the animation. Moreover, it's no trouble to position the calendar so that Friday is in the middle of the window (the coloration on the heading serves as a reference). Expanding the window will reveal more days, not make the days larger. If you want to look two weeks ahead, you can -- even on an eee 901. I do plan to add a zoom feature, however.

Selections

Another feature I've added as a reaction against current calendars: When you click and drag on the calendar itself, it doesn't create a new event automatically. I've always found this to be annoying. When I make a selection, I am usually in some kind of tentative state of mind. Moreover, in thunderbird and evolution, it's possible to accidentally create events that are too small to be selected, forcing you to go into another view to delete them. Again, really annoying. I don't want the application taking some action based on my half-baked ideas. I much prefer it to wait until I'm sure of myself.

Undo / Redo

This applet is the first time I've implemented the Command Pattern in a program, and I'm curious to see how it pans out. The idea is that rather than employing low-level tricks to capture state for undo, you explicitly code the do and undo procedure for each action that your application supports. So far, New, delete, move, resize, and select can all be undone.

I think that selection is too often left out as an undo action. Doubly annoying because selections are ephemeral, disappearing the minute you focus on something else. Often I select things without thinking. I've put a lot of work into creating selections, only to have them evaporate due to an errant click.

On the implementation side, I distinguish between MenuCommands, from which menu and toolbar items can be created automatically, and MouseCommands (invoked repeatedly with new mouse coordinates before being committed to the UndoStack in their final state). Both types support a class-methods indicating whether they are currently available. This allows toolbar state to be managed, and for the controller to distinguish between one of several commands it might support.

One thing I found is that with this approach, the Controller becomes a general-purpose object which mostly creates and commits commands to an UndoStack in response to low-level events. The controller implements the logic for detecting drag, click, double-click detection. The commands implement more concrete actions, such as moving an event in the calendar based on given mouse coordinates. The Controller only needs to be specialized to add new input events. For example, flicking the calendar is currently implemented with a custom controller.

In the future I plan to make navigation undoable. I'm debating whether navigation commands should be posted to the main UndoStack, or into a separate stack that would be exposed as "Forward / Back".  I'll have to try and see.

Development Status

If I follow this out to its conclusion, it would need at least the following features before I'd call it "Done".

  • Ability to edit the names of events
    • tricky because the whole thing is done in raw cairo
  • Some system for handling repeating events
  • convenience navigation buttons, such as "Today", "Last {Week, Month, Year}", "Next {Week, Month, Year}" etc.
  • Alarm notification system, and ability to handle alarms
  • Integration with google calendar (probably using libgdata)
  • some cosmetic improvements
The implementation of the alarm notification system worries me most. I have only a vague idea of how I would implement that, and it would need to be absolutely reliable before I'd consider using this program myself. I do have some interesting ideas for how to create alarms and repeating events in the UI, however.

Saturday, October 30, 2010

Shopping

Coming home Saturday night I was delighted to see that "The Co-Operative" grocery store nearest my house was still open. I went in to buy some things for next morning's breakfast.


There were two choices for bacon: one claiming Britain as its origin, and another from "the family farms of the Netherlands". At this point I must confess that my first reaction was "Eek! Why would I want Dutch bacon? I'm in England! The British bacon is for me." It seems I have begun to adopt a British point of view after only a few short weeks. That is until I looked more closely at the package and read that it consisted of "87% pork."

I was perplexed. How in he world can 13% of a slice of bacon consist of something other than pork? Some minutes passed, during which time I surveyed the contents of the package. Perhaps closer inspection would reveal a portion of the of the slice where some miscreant had inserted turkey or beef. At least that would be a reasonable explanation. Alas, I found no signs of obvious adulteration.

Unsettled, I chose the Dutch variety.

Scene from a small boat punting up the Cam (the river in Cambridge).

Saturday, October 9, 2010

Some More Pics From Spain, Continued...




I ate at this really neat cafe in Figueres. 
The bathroom was entirely stainless steel, and used car door handles.



Here I am, trying to be artistic





There's a lot of really good street art in Figueres...


...as well as the more typical variety.




More scenes from Figueres.







Waiting for the train back to Barcelona.


In front of the Dali museum, with a dopey grin.
It's worth the trip. Thanks for the tip, Kat!


A closeup of the the dots on the exterior of the
building. Each one appears to be slightly
different.

Some More Pics From Spain

I've been out of Spain for a couple of months, but I still have plenty of photos I never posted. Truth be told I found it a bit painful to manually upload them. I don't know why it never occurred to me to use Picasa until just now. Enjoy.








Tarragona







Scenes from a public concert in Montjuic



A warm waffle, covered in chocolate sauce and sweetened condensed milk. Purchased from the above stand.

Monday, October 4, 2010

So, I'm in Cambridge, UK now. I've been meaning to post up for days but haven't quite found the time. I'm going to try to get some longer posts out in the next couple of weeks, but for now I'll settle for something short and simple.

Quote of the Week

The other day I was walking through Midsummer Common, which is a big green field near the center of town that normally contains quite a few cows. Instead there was some kind of bicycle race event and the place was packed with people and cycles. I smelled the smell of good, greasy food -- like the kind you get at a county fair or a race track -- so I sauntered on in. As I was munching on a tasty roast pork sandwich (with stuffing and apple sauce) I overheard the following snippet of conversation:

Boy (in a whiny English kid voice): So, can I mum, can I?
Parent: No Roger, you've been an absolute muppet today!

That's the first time I have ever heard the word "muppet" used this way. I wasn't sure whether it was a fluke or not, but while house-hunting today, my would-be roomate used the term in the exact same way to describe someone who forgets to lock up the house when they're the last to leave.

Nerd Section


Some time ago I got distracted by a silly idea that just wouldn't leave me alone. It kept gnawing at me to follow it up, so eventually I gave in and started hacking on it. Have a look at the following code. I'll give you three guesses to tell me what language it's written in.


     DEF (id(outer) FUNCTION(id(x)
       FUNCTION(id(ignored) VAL(x))))

     DEF (id(one_ret) APPLY(VAL(outer) _(1)))
     DEF (id(two_ret) APPLY(VAL(outer) _(2)))

     PRINT (APPLY (VAL(one_ret) _()))
     PRINT (APPLY (VAL(two_ret) _()))

     DEF (id(count_backward) FUNCTION(id(x)
           IF(NOT(VAL(x))
              PRINT (_(done))
              BLOCK(
                PRINT (VAL(x))
                APPLY(VAL(count_backward) MINUS(VAL(x) I(1))
)))))



No, it's not LISP. The parenthesis go on the outside of the expression.

No, it's not python. Notice how there's no commas separating arguments.

Did I hear someone say haskell? Now you're just being silly.

It's actually C. Through abuse of variadic functions and preprocessor macros, I implemented a functional, dynamic language directly in C. For the curious, you can see the full example at my git repository, http://github.com/emdash/goof/blob/no_commas/simple.c:

I did it for fun, but it does work. Bonus points for explaining how it works in a comment.

Monday, August 16, 2010

Returning Home

I'm a bad blogger, I know. I didn't even post before leaving spain :( In the last couple of weeks I managed to do some traveling around Catalonia, seeing Taragona and Figueres. Dali Museum is really quite awesome in Figueres. Taragona has roman ruins and a good beach. Have pictures, but not on this computer. Will try and post them soon.

I'm currently in Boston visiting some friends on my way back home. My friends live between Harvard and MIT. Walked around the campus and then my friend took me down town to see boston common, and the harbor. We also passed by the grave yard where Paul Revere, John Hancock, and the victims of the Boston Massacre are buried. John Hancock's tomb stone is the largest of the bunch. While walking through downtown Boston we ran into Richard Stallman. That's all I've got for the moment.

Today I am thinking about busing to New York city and back for a bagel, a hot dog, and a pizza. But we'll see. Jet lag and my friend's cat have combined to give me a bit of a jumpstart on the day. I'm looking at the schedules right now.

Friday, July 23, 2010

Getting Ready for GUADEC

The last few days have been a welcome reprieve from the oppressive summer heat. Thank god for that. Of course it cools down just before my departure to the Netherlands for GUADEC.

Going to GUADEC next week. There's going to be a big PiTiVi hackfest involving the majority of the current PiTiVi contributors. I'm really looking forward to some serious progress.

Also hoping to give a lightning talk about some hair-brained ideas I have for UI development.
Last week, my friend Aaron came to visit and we rented scooters and rode around the city. It was hot, but fun. Enjoy the photos.

This is us up at Parc Guell.




Did you ever think you'd see Aaron on a scooter?