CursorEvent Bubbling and Capture

This page describes how cursor events are routed to individual nodes.

Event Bubbling

Determining which node an event is sent to follows a process called bubbling. Initially, libavg looks for the target node by recursively going down the node tree, always choosing the first child node that contains the cursor, until it reaches the lowest node that contains the cursor. Then, libavg examines the nodes from leaf to root (starting from the last node found) and delivers the event to the nodes that have appropriate event handlers in that order. The first step goes down the hierarchy in a geometrical fashion, and the second one goes up the node hierarchy by following the chain of parent nodes.

Note that DivNodes react to events directly as well as by bubbling - if the cursor of an event is inside a DivNode, that node will get the event.

Sound complicated? It really isn't and corresponds to the javascript event bubbling model. Usually, the effect is just what you would expect: Put an event handler in a leaf node and a surrounding div, and both will get called. The leaf node handler gets called first. But: if a node is covered by another node - even if it's something transparent or a DivNode -, it will never get events. Here is an example:

event.py

 1#!/usr/bin/env python
 2# -*- coding: utf-8 -*-
 3
 4from libavg import app, avg
 5
 6class MainDiv(app.MainDiv):
 7    def onInit(self):
 8        self.node = avg.WordsNode(pos=(10,10), 
 9                text="Should I stay or should I go?", parent=self)
10        div = avg.DivNode(pos=(100,0), size=(80,200), parent=self)
11        self.node.subscribe(avg.Node.CURSOR_MOTION, self.onWords)
12        div.subscribe(div.CURSOR_MOTION, self.onDiv)
13
14    def onDiv(self, event):
15        print "div" 
16        self.node.color = "FF8000" 
17
18    def onWords(self, event):
19        print "words" 
20        self.node.color = "00FF00" 
21
22app.App().run(MainDiv())

Now, whenever the cursor is above the left part of the text, the text will turn green. When the cursor is over the right part - which is covered by the div - the text will turn orange. If you switch the order of the div and words nodes in the avg file, this won't happen anymore: The text will always turn green whenever the cursor is over it (since there's no cursorout handler, it will never turn white again, though).

Any node can be completely cut off from event handling by setting it's sensitive attribute. If this attribute is False, the node is ignored by the event handling code. Try this with the DivNode in the example and see what happens. The sensitive attribute extends to the children of a node: if you set it to False in a DivNode, all it's children will stop receiving events as well.

DivNodes and Event Handling

A DivNode with an explicit size attribute reacts to events as described above: Any events inside of its area are routed to the div node. A DivNode without an explicit size attribute, however, does not react to events directly. Bubbling still works: if a child of the DivNode is underneath a click, the event handlers of the parent will be called as well as described above.

Event Capture

When implementing things like dragging, scrollbars or even simple buttons, it helps a lot to be able to route all events from a pointer to one specific node. One way to do this is by using the event capture mechanisms of libavg (alternative that is often easier is to use Contacts, as described under TouchInput). Event capture allows you to route all events from a specific cursor to a single node temporarily. (As an aside, this is something completely different than what javascript calls event capture. It's the same thing that MFC calls event capture, though.)

Capture is set up by calling Node.setEventCapture() and released using Node.releaseEventCapture(). Here's an example drag-and-drop:

drag.py

 1#!/usr/bin/env python
 2# -*- coding: utf-8 -*-
 3
 4from libavg import avg
 5
 6offset = None
 7
 8def onMouseDown(event):
 9    global offset
10    node = event.node
11    offset = node.getRelPos((event.x, event.y))
12    node.setEventCapture()
13
14def onMouseMove(event):
15    global offset
16    node = event.node
17    if offset != None:
18        node.x = event.x-offset[0]
19        node.y = event.y-offset[1]
20
21def onMouseUp(event):
22    global offset
23    node = event.node
24    if offset != None:
25        node.releaseEventCapture()
26        offset = None;
27
28player = avg.Player.get()
29
30canvas = player.createMainCanvas(size=(640,480))
31rootNode = canvas.getRootNode()
32node = avg.WordsNode(pos=(10,10), font="arial", 
33        text="Hello World", parent=rootNode)
34node.subscribe(node.CURSOR_DOWN, onMouseDown)
35node.subscribe(node.CURSOR_MOTION, onMouseMove)
36node.subscribe(node.CURSOR_UP, onMouseUp)
37player.play()

If you remove the set/releaseEventCapture() calls, dragging will only work as long as you move slowly and don't leave the area of the node with the cursor. The code to duplicate this functionality without event capture is messy, especially if you need to support multitouch and several elements can be dragged at once. For multitouch devices, set/releaseEventCapture() take a cursor id as parameter so you can specify which finger corresponds to which user interface element.