Tuesday, 13 July 2010

Understanding Python decorators

I've finally come to grasp with Python's decorators, and have managed to create somewhat nicer code. Here's what I had before:


def on_top(self, event):
index, item = self.find_shape()
self.shapes.pop(index)
self.shapes.append(item)
self.populate()
self.list.Select(0)


def on_bottom(self, event):
index, item = self.find_shape()
self.shapes.pop(index)
self.shapes.insert(0, item)
self.populate()
self.list.Select(len(self.shapes) - 1)


def on_up(self, event):
index, item = self.find_shape()
self.shapes.pop(index)
self.shapes.insert(index + 1, item)
x = self.list.GetFirstSelected() - 1
self.populate()
self.list.Select(x)


def on_down(self, event):
index, item = self.find_shape()
self.shapes.pop(index)
self.shapes.insert(index - 1, item)
x = self.list.GetFirstSelected() + 1
self.populate()
self.list.Select(x)



As you can see - a lot of repeated code, with only different things happening in the middle of each function. This is ideal for a decorator. I struggled for a while to get it working as a decorator function inside a class, before reading how you define a wrapper function for creating a closure over the decorated function.

My code now reads,


def move_shape(fn):
"""
Passes the selected shape and its index to the decorated function, which
handles moving the shape. function returns the list's index to select
"""
def wrapper(self, event, index=None, item=None):
index, item = self.find_shape()
self.shapes.pop(index)
x = fn(self, event, index, item)

self.populate()
self.list.Select(x)
return wrapper

@move_shape
def on_top(self, event, index=None, item=None):
self.shapes.append(item)
return 0

@move_shape
def on_bottom(self, event, index=None, item=None):
self.shapes.insert(0, item)
return len(self.shapes) - 1

@move_shape
def on_up(self, event, index=None, item=None):
self.shapes.insert(index + 1, item)
return self.list.GetFirstSelected() - 1

@move_shape
def on_down(self, event, index=None, item=None):
self.shapes.insert(index - 1, item)
return self.list.GetFirstSelected() + 1


Unfortunately the event parameter is there because these are also wxPython event listeners, and I'd have to create delegate functions just to remove the one parameter, which seems pointless.

Friday, 25 June 2010

More pubsub calls

PubSub is a great Python package allowing you to decouple your objects from one another. I've changed Whyteboard to use more pubsub message passing instead of direct method invocation; it's allowed me to test the application easier and to also chain multiple actions from a single message (e.g. "change tab" updates the thumbnail, bolds its text and changes the notes' tree control selection

Eventually none of my objects will know about any of its interaction objects, except for the GUI, which will mediate method calls. Perhaps I could even put this mediator outside of the GUI so that the GUI only creates dialogs and other GUI components and handles menus' events by delegating it to another object


My main problem is the models sometimes need to know the state of the GUI or its sub-components: e.g. the canvas may wish to know if the drawing mode is set to transparent - which is stored in gui.util.

Eventually the canvas will have no reference to the GUI at all, so it needs to know a way to query the current state of the program. Hmm...

Tuesday, 18 May 2010

Note: test before release

Embarrassing...released version 0.40 with about 6 bugs that I found within a 10 minute play around with the program. For shame!

Anyway, that takes care of that, I hope. Version 0.40.1 released Had another bug report about damn unicode errors - thought I'd fixed those! oh dear

Sunday, 16 May 2010

Whyteboard 0.4 released

New whyteboard released, bringing a whole new bunch of features.

New features:
- Highlighter tool
- Improvements to selection tool
- UI changes
- Better shape viewer
- PDF Cache viewer
- bug fixes and misc. changes/improvements - see changelog for further details

Enjoy. As always, report any bugs/issues/suggestions.

Monday, 3 May 2010

The Law of Demeter

I've been learning a lot about refactoring over the past few days, and have made an effort to put these practices into effect. One interesting concept is the Law of Demeter - classes should know as little about their interacting classes as possible. This means restricting access on instance variable, and method arguments.

Suppose we have an instance variable of type B inside A. Class B contains a reference to class C. The Law of Demeter states we should not access properties of C through B; instead we should tell B on how to interact with C.

Here's a before/after example of some code from Whyteboard I've modified, involving selecting a shape using the Select tool:



class Select:
def check_for_hit(self, shape, x, y):
"""
Sees if a shape is underneath the mouse coords, and allows the shape to
be re-dragged to place
"""
found = False
handle = shape.handle_hit_test(x, y) # test handle before area

if handle:
self.handle = handle
found = True
elif shape.hit_test(x, y):
found = True

if found:
self.canvas.overlay = wx.Overlay()
self.shape = shape
self.dragging = True
self.offset = self.shape.offset(x, y)

if self.canvas.selected:
self.canvas.deselect()
self.canvas.selected = shape
shape.selected = True

pub.sendMessage('shape.selected', shape=shape)
return found


class Gui:
# bind a handler for shape.selected
pub.subscribe(self.shape_selected, 'shape.selected')

def shape_selected(self, shape):
"""
Shape getting selected (by Select tool)
"""
x = self.canvas.shapes.index(shape)
self.canvas.shapes.pop(x)
self.canvas.redraw_all() # hide 'original'
self.canvas.shapes.insert(x, shape)
shape.draw(self.canvas.get_dc(), False) # draw 'new'

ctrl, menu = True, True
if not shape.background == wx.TRANSPARENT:
ctrl, menu = False, False

self.control.transparent.SetValue(ctrl)
self.menu.Check(ID_TRANSPARENT, menu)


In this code, we see the Select tool (a "model" is manipulating the canvas directly, when it really shouldn't be doing so. We can delegate this to the canvas itself, through the GUI.



class Select:
def check_for_hit(self, shape, x, y):
"""
Sees if a shape is underneath the mouse coords, and allows the shape to
be re-dragged to place
"""
found = False
handle = shape.handle_hit_test(x, y) # test handle before area

if handle:
self.handle = handle
found = True
elif shape.hit_test(x, y):
found = True

if found:
self.shape = shape
self.dragging = True
self.offset = self.shape.offset(x, y)
pub.sendMessage('shape.selected', shape=shape)
return found


class Gui:
# bind a handler for shape.selected
pub.subscribe(self.shape_selected, 'shape.selected')

def shape_selected(self, shape):
"""
Shape getting selected (by Select tool)
"""
self.canvas.select_shape(shape)

ctrl, menu = True, True
if not shape.background == wx.TRANSPARENT:
ctrl, menu = False, False

self.control.transparent.SetValue(ctrl)
self.menu.Check(ID_TRANSPARENT, menu)


class Canvas:
def select_shape(self, shape):
"""Selects the selected shape"""
self.overlay = wx.Overlay()
if self.selected:
self.deselect_shape()

self.selected = shape
shape.selected = True
x = self.shapes.index(shape)
self.shapes.pop(x)
self.redraw_all() # hide 'original'
self.shapes.insert(x, shape)
shape.draw(self.get_dc(), False) # draw 'new'



We can now see that the appropriate classes are performing their correct responsibilities. The model sends out a message to indicate something's changed, without caring how the event is handled, and the GUI is updating its controls and menus. The GUI delegates a method call to the canvas which updates itself.

There are no longer many "access levels" (this.that.other.do_something) as all operations are on variables within a small scope. This is better code that's easier to test.

Wednesday, 21 April 2010

New release almost ready

Since the last post, development has steadily resumed again. Over 30 commits have been made, fixing bugs and adding in a bunch of new features. One of the new things I've addded is when you are moving a shape using the Select tool, moving the shape close to the canvas' edge will scroll the canvas in that direction, so that you don't have to stop dragging the shape, scroll the canvas manually and resume dragging.

There's also a new feature that allows you to move selected shapes using the keyboard arrow keys. This integrates nicely with the above "canvas edge" auto-scroll. The shape viewer has gained a "delete" button to remove un-selectable shapes such as a pen/eraser.

The shape viewer has gained several improvements too, such as better synchronisation with the program as a whole. Undo/redo/deleting shapes/renaming a sheet all trigger the viewer to update itself and shapes report their properties more accurately.

A PDF Cache viewer allows you to see cached pdf/svg/ps files, and remove them from the cache, forcing Whyteboard to re-convert the item. The date a file is converted is also stored into the cache so you know when it was last converted.

Recently Closed Sheets is implemented as a sub-menu - as well as being able to re-open the last closed sheet with Ctrl+Shift+T, you can browse through the sub-menu and choose which sheet to close.

and various other small tweaks/improvements.
Stay tuned for a release - there's only a few bugs to iron out now. There's been over 80 commits between the last release and the current one - a lot has changed, making the program much better, overall.

Here's 2 HD videos showing some new stuff:

Improved Shape Viewer




Shapes Scrolling the canvas

Saturday, 27 March 2010

Arrested Development

Development has practically stopped lately. Been busy with work - when I get home I really don't want to spend time programming.

I plan on getting a bit of work done this weekend - it's been too long since I coded!