Extending Field in Field — Custom Overrides

The 'developer level' documentation — DelegationAndVisualElements — describes the secret core of Field — visual elements, or boxes, while they look flat on Field's canvas are actually arranged in a directed graph) structure. What you'd call 'methods' in Java or Python — to paint boxes, interact with the mouse, handle things like option-clicking — actually delegate 'upwards' along the edges of this graph until it gets to the sheet itself (the sheet counts as a box) and beyond, carrying on 'upwards' into the land of plugins. While in Java methods and instances have only one super, in Field, boxes can have many and method delegation continues as a breadth-first traversal over the graph.

Properties work like this, but this is also the primary extension mechanism of Field — plugins attach themselves near the top of this graph to change, for example, what menu items appear when you right click on a box (see the TopologyAndControlFlow), or just exactly how things get executed (in the case of the ProcessingPlugin). By attaching to places near the top of the graph (typically just above the sheet itself) a plugin can act on any element in the sheet.

You can, of course, write plugins yourself &mdash in Java. But much more fun is to write things that extend the behavior of Field directly inside the environment. This is a direct way of extending Field into your own domains of interest.

Bar graphs everywhere

A silly example will demonstrate how to add additional painting logic to elements in a sheet.

First, let's make two elements, A and a (this duplicates some of the documentation here:

Now we need to add A as a parent of a. We can use the special mouse-modal tool:

We can either select it with the mouse or just press and hold 'D'. Then drag with the mouse a connection between the two boxes:

Now, just as the label says, a will delegate to A. Any base-functionality that we add to A will also get applied to a. Now we have an arrow connecting these two boxes, just so that we know that they are in a hierarchy:

This arrow will still be drawn (but much more feightly) when neither of the boxes are selected.

A custom paint 'method'

Here's the method that will draw a bar chart:

def customPaint(source, bounds, visible):
    hArray = flatten(source.height_ or [])
    offset = 5
    for height in hArray:
        line = PLine().rect(Rect(bounds.x+offset, bounds.y+bounds.h-5-height, 10, height))
        line(filled=1, fillColor=Color4(0.25,0,0,0.25), thickness=0.5)  
        paintFLineNow(line)
        offset += 12

Breaking this down line-by-line:

  • customPaint(source, bounds, visible) — this is the signature that all custom paint methods must have. They take three arguments: the visual element that's being painted, that elements' bounding Rect and whether the element is actually visible or not.
  • hArray = flatten(source.height_ or []) — this line contains two tricks. Firstly it uses the idiom element.property or defaultValue (see SelfAndProperties) to access a property inside the element source or supply a default. Secondly it uses Field's useful library function flatten to take lists-of-lists and flatten their contents. This means that height_ can be a single number (a single bar) or a list of numbers (or a bar 'chart').
  • line = PLine() ... — this is just straightforward Field drawing code, see DrawingFLines.
  • paintFLineNow(line) — but, unlike the normal way of drawing PLine where you add the material to _self.lines to get drawn, we are actually writing a draw method. paintFLineNow() actually goes and puts this line on the screen right this very instant. You can only call this function inside draw methods. In fact, you are actually now in a position to write your own version of the convenience code that looks for properties like _self.lines and hands them to paintFLineNow() for drawing.

We are almost done. The last step is to tell Field that this function should be incorporated into the dispatch graph at this point. We do this by adding an decorator to the function:

@overridePaint
def customPaint(source, bounds, visible):
    hArray = flatten(source.height_ or [])
    offset = 5
    for height in hArray:
        line = PLine().rect(Rect(bounds.x+offset, bounds.y+bounds.h-5-height, 10, height))
        line(filled=1, fillColor=Color4(0.25,0,0,0.25), thickness=0.5)  
        paintPLineNow(line)
        offset += 12

You might have encountered this @somethingOrOther idiom in Field before. We've used it for Swing callbacks and for Processing Plugin callbacks. The key thing to remember is this: these kinds of extension points maintain a list of functions that ought to be called, the functions are, in this case, keyed off of the name of the function. You can only have one function called customPaint acting as a custom paint function. If you execute the code above twice, you overwrite your previous customPaint function.

This is almost always exactly what you want. Field encourages you to write code incrementally and with this approach you can keep modifying your customPaint method, and keep redefining it, without having to detatch your previous definition. This is exactly how I came up with the above method — in stages. Of course you can have as many paint functions at a particular point in the dispatch graph as you like, they just have to have different names. Just one thing to note: whatever you do do in a custom paint function better be pretty quick — it will get called once per child element per window update.

You should also note that these overrides are not persisted to disk (persisting code is complex to the point of almost being impossible to do correctly) — you have to apply them (once) every time that the sheet loads. So the code above is an ideal candidate for the autoExec property (use the menu in the top left hand corner of the text editor).

You should also note that, for the purposes of speed, things _self isn't set up correctly for the function call above. You are probably much more interested in the source argument to customPaint.

One last thing and we're done. We now have a custom paint function that draws a bar chart (or just a bar) based on the contents of the height_ property. To actually see anything we need to actually set this property somewhere. Again, in element A:

_self.height_=[30, 50, 20]

Will give us:

A bar chart! But wait, why two? Well, from SelfAndProperties you'll know that properties also delegate. So, given the absence of a height_ set on a it grabs the closest on in the graph — the one of A. We can override this locally, inside a:

_self.height_ = [30,-30]

Going a little further, we can make a new element, again parented to A with:

_self.height_=0

def go():
    _self.height_+=1
    _self.dirty=1

_r = go

Which gives us a height that grows over time while it's running and is reset at the beginning:

This begins (but only really begins) to hint at the power of the approach here. Code outside a group of elements, in one location, can modify how many elements are drawn (and are interacted with, and what properties they appear to have, and so on). You can choose, from one central place, what parts of the inner workings of distributed code get visualized. There's also now an obvious place to draw connections between elements or analysis of multiple elements that needs to be updated every time the canvas is redrawn.

Further extensions

There are all kinds of overrides, in addition to painting:

# called when an element is deleted
@overrideDeleted
def customDeleter(source):
    print "it's gone %s " % source

# called when an element is about to start executing (that is, about to turn green)
@overrideBegin
def customBegin(source):
    print "it's starting %s " % source

# called when an element is about to stop executing
@overrideEnd
def customEnd(source):
    print "it's stopping %s " % source

# called to get all properties
@overrideGetProperty
def customGet(source, prop, result):
    print "getP %s:%s @ %s " % (source, prop, result)

# called to set all properties
@overrideSetProperty
def customSet(source, prop, to):
    print "setP %s:%s @ %s " % (source, prop, to)

All of these decorators start with override so you can use auto-complete on override to get a documented list.

A few notes:

  • print can go somewhere unexpected / _self is not set up — unlike in every other instance, Field, for the purposes of efficiency, doesn't set up the Python environment as you might have come to expect before calling these extension points. This means that you can't use _self, nor can you rely on print going anywhere other than the console (or the terminal window if you choose the 'hijack' option from the file menu or run Field from the command line).
  • getProperty / setProperty are called a lot — you'll get many, many calls to these functions (many more than even painting functions). You'll see properties being set even when you type things in the text editor — after all, by typing in the text editor you are setting the source code properties of elements.
  • calls to getProperty / setProperty chain — result and to in the above examples are of type Ref which is a class with methods get and set. You can use an override in getProperty to simply monitor what's going on, you don't actually have to figure out how to get the property itself.