Parcourir la source

Implemented widget tree, gesture propagation and some more stuff:

- New naming rule: an "event" is triggered by a driver, a "gesture" is
  triggered by a tracker and delegated to the application by a widget.
- point down, move and up have been generalized to "events".
- Windows have been replaced by widgets, chich also support a tree structure.
- Gestures are propagated to parent widgets, events are delegated to child
  widgets. both propagation and delegation can be stopped by a handler.
- Gestures now save their originating event, events save their originating
  touch points.
- Test programs have been adapted to the new gesture binding style.
Taddeus Kroes il y a 13 ans
Parent
commit
12ada91d45

+ 10 - 0
src/drivers/__init__.py

@@ -0,0 +1,10 @@
+from tuio import TuioDriver
+
+
+def select_driver(event_server):
+    """
+    Create an object of a driver implementation, based on hardware support.
+    Currently, only a TUIO driver implementation exists. In the future, this
+    method should select one driver from a set of implementations.
+    """
+    return TuioDriver(event_server)

+ 93 - 0
src/drivers/tuio.py

@@ -0,0 +1,93 @@
+from OSC import OSCServer
+# FIXME: don't print  tracebacks in final implementation?
+OSCServer.print_tracebacks = True
+
+from ..event_driver import EventDriver
+from ..events import PointDownEvent, PointMoveEvent, PointUpEvent
+from ..touch_objects import TouchPoint
+from ..screen import pixel_coords
+
+
+class TuioDriver(EventDriver):
+    tuio_address = 'localhost', 3333
+
+    def __init__(self, event_server):
+        super(TuioDriver, self).__init__(event_server)
+
+        # OSC server that listens to incoming TUIO events
+        self.server = OSCServer(self.tuio_address)
+        self.server.addDefaultHandlers()
+        self.server.addMsgHandler('/tuio/2Dobj', self.receive)
+        self.server.addMsgHandler('/tuio/2Dcur', self.receive)
+        self.server.addMsgHandler('/tuio/2Dblb', self.receive)
+
+        # List of alive session id's
+        self.alive = set()
+
+        # List of session id's of points that have generated a 'point_down'
+        # event
+        self.down = set()
+
+        # Map of session id to touch point
+        self.points = {}
+
+    def receive(self, addr, tags, data, source):
+        surface = addr[8:]
+        #self.debug('Received message <surface=%s tags="%s" '
+        #           'data=%s source=%s>' % (surface, tags, data, source))
+        msg_type = data[0]
+
+        # FIXME: obj/blb events are ignored (for now)
+        if surface != 'cur':
+            return
+
+        if msg_type == 'alive':
+            alive = set(data[1:])
+            released = self.alive - alive
+            self.alive = alive
+
+            if released:
+                self.debug('Released %s.' % ', '.join(map(str, released)))
+                self.down -= released
+
+            for sid in released:
+                point = self.points[sid]
+                del self.points[sid]
+                self.event_server.delegate_event(PointUpEvent(point))
+                #self.event_server.on_point_up(sid)
+        elif msg_type == 'set':
+            sid, x, y = data[1:4]
+
+            if sid not in self.alive:
+                raise ValueError('Point with sid %d is not alive.' % sid)
+
+            # Translate to pixel coordinates
+            px, py = pixel_coords(x, y)
+
+            # Check if 'point_down' has already been triggered. If so, trigger
+            # a 'point_move' event instead
+            if sid in self.down:
+                self.debug('Moved %d to (%.4f, %.4f), in pixels: (%d, %d).'
+                           % (sid, x, y, px, py))
+                point = self.points[sid]
+                point.set_position(px, py)
+                self.event_server.delegate_event(PointMoveEvent(point))
+                #self.event_server.on_point_move(sid, px, py)
+            else:
+                self.debug('Down %d at (%.4f, %.4f), in pixels: (%d, %d).'
+                           % (sid, x, y, px, py))
+                self.down.add(sid)
+                self.points[sid] = point = TouchPoint(px, py)
+                self.event_server.delegate_event(PointDownEvent(point))
+                #self.event_server.on_point_down(sid, px, py)
+
+    def run(self):
+        self.server.handle_request()
+
+    def start(self):
+        self.info('Starting OSC server')
+        self.server.serve_forever()
+
+    def stop(self):
+        self.info('Stopping OSC server')
+        self.server.close()

+ 39 - 0
src/event.py

@@ -0,0 +1,39 @@
+from touch_objects import OBJECT_NAMES
+
+
+class Event(object):
+    """
+    Abstract base class for events triggered by an event driver. These events
+    are delegated to gesture trackers, to be translated to gestures. To be able
+    to check whether an event is located within a widget, a position is
+    required. Therefore, the touch object that triggers the event is is linked
+    to the event object.
+    """
+    _type = NotImplemented
+
+    def __init__(self, touch_object):
+        self.touch_object = touch_object
+        self.stop = False
+
+    def __getattr__(self, name):
+        if name in OBJECT_NAMES \
+                and type(self.touch_object) == OBJECT_NAMES[name]:
+            return self.touch_object
+
+        raise AttributeError("'%s' object has no attribute '%s'"
+                             % (self.__class__.__name__, name))
+
+    def get_type(self):
+        return self._type
+
+    def get_touch_object(self):
+        return self.touch_object
+
+    def get_position(self):
+        return self.touch_object.get_position()
+
+    def stop_delegation(self):
+        self.stop = True
+
+    def is_delegation_stopped(self):
+        return self.stop

+ 32 - 0
src/event_driver.py

@@ -0,0 +1,32 @@
+from logger import Logger
+
+
+class EventDriver(Logger):
+    """
+    Abstract factory class for drivers. A driver translates driver-specific
+    messages to a common set of events. The minimal set is {point_down,
+    point_move, point_up}. A driver implementation should define the methods
+    'start' and 'stop', which starts/stops some event loop that triggers the
+    'delegate_event' method of a widget.
+    """
+    def __init__(self, event_server):
+        self.event_server = event_server
+
+    def start(self):
+        """
+        Start the event loop.
+        """
+        raise NotImplementedError
+
+    def stop(self):
+        """
+        Stop the event loop.
+        """
+        raise NotImplementedError
+
+    def run(self):
+        """
+        Execute a single loop iteration. If implemented, this method provides a
+        way to run the driver within another event loop.
+        """
+        raise NotImplementedError

+ 38 - 22
src/event_server.py

@@ -1,34 +1,50 @@
 from logger import Logger
+from drivers import select_driver
 
 
 class EventServer(Logger):
     """
-    Abstract class for event servers. An event server translates driver
-    events to point 'down', 'move' and 'up' events. An event server
-    implementation should define the methods 'start' and 'stop', which
-    starts/stops some event loop that triggers on_point_up, on_point_move and
-    on_point_down methods on the 'handler_obj' object.
+    The event server uses an event driver to receive events, which are
+    delegated to a widget tree (and eventually to gesture trackers).
     """
-    def __init__(self, handler_obj):
-        self.handler_obj = handler_obj
+    def __init__(self, root_widget=None):
+        # Root widget to which events are delegated
+        self.root_widget = root_widget
 
-    def start(self):
-        raise NotImplementedError
+        # Driver implementation that will be serving events
+        self.event_driver = select_driver(self)
 
-    def stop(self):
-        raise NotImplementedError
+    def get_root_widget(self):
+        return self.root_widget
 
+    def set_root_widget(self, widget):
+        self.root_widget = widget
 
-class EventServerHandler(Logger):
-    """
-    Interface for gesture server. Defines empty on_point_up, on_point_move and
-    on_point_down handlers.
-    """
-    def on_point_down(self, sid, x, y):
-        return NotImplemented
+    def delegate_event(self, event):
+        """
+        Delegate an event that has been triggered by the event driver to the
+        widget tree.
+        """
+        self.root_widget.handle_event(event)
 
-    def on_point_move(self, sid, x, y):
-        return NotImplemented
+    def start(self):
+        """
+        Start the event loop. A root widget is needed to be able to delegate
+        events, so check if it exists first.
+        """
+        if not self.root_widget:
+            raise ValueError('Cannot start event server without root widget.')
+
+        self.event_driver.start()
 
-    def on_point_up(self, sid):
-        return NotImplemented
+    def stop(self):
+        """
+        Stop the event loop.
+        """
+        self.event_driver.stop()
+
+    def run(self):
+        """
+        Execute a single loop iteration of the event driver.
+        """
+        self.event_driver.run()

+ 22 - 0
src/events.py

@@ -0,0 +1,22 @@
+from event import Event
+
+
+class PointDownEvent(Event):
+    """
+    Addition of a simple touch point to the screen.
+    """
+    _type = 'point_down'
+
+
+class PointMoveEvent(Event):
+    """
+    Movement of a simple touch point from the screen.
+    """
+    _type = 'point_move'
+
+
+class PointUpEvent(Event):
+    """
+    Removal of a simple touch point from the screen.
+    """
+    _type = 'point_up'

+ 0 - 59
src/geometry.py

@@ -120,62 +120,3 @@ class AcceleratedPositionable(MovingPositionable):
         Calculate the acceleration in pixels/second.
         """
         return self.movement_distance() / self.movement_time()
-
-
-class Surface(Positionable):
-    """
-    Interface class for surfaces with a position. Defines a function
-    'contains', which calculates whether a position is located in the surface.
-    """
-    def contains(self, point):
-        raise NotImplementedError
-
-
-class RectangularSurface(Surface):
-    """
-    Rectangle, represented by a left-top position (x, y) and a size (width,
-    height).
-    """
-    def __init__(self, x, y, width, height):
-        super(RectangularSurface, self).__init__(x, y)
-        self.set_size(width, height)
-
-    def __str__(self):
-        return '<%s at (%s, %s) size=(%s, %s)>' \
-               % (self.__class__.__name__, self.x, self.y, self.width,
-                  self.height)
-
-    def set_size(self, width, height):
-        self.width = width
-        self.height = height
-
-    def get_size(self):
-        return self.width, self.height
-
-    def contains(self, point):
-        x, y = point.get_position()
-        return self.x <= x <= self.x + self.width \
-               and self.y <= y <= self.y + self.height
-
-
-class CircularSurface(Surface):
-    """
-    Circle, represented by a center position (x, y) and a radius.
-    """
-    def __init__(self, x, y, radius):
-        super(CircularSurface, self).__init__(x, y)
-        self.set_radius(radius)
-
-    def __str__(self):
-        return '<%s at (%s, %s) size=(%s, %s)>' \
-               % (self.__class__.__name__, self.x, self.y, self.width,
-                  self.height)
-
-    def set_radius(self, radius):
-        self.radius = radius
-
-    def get_radius(self):
-        return self.radius
-
-    def contains(self, point):
-        return self.distance_to(point) <= self.radius

+ 0 - 75
src/gesture_server.py

@@ -1,75 +0,0 @@
-from event_server import EventServerHandler
-from tuio_server import TuioServer2D
-from window import FullscreenWindow
-from point import TouchPoint
-
-
-class GestureServer(EventServerHandler):
-    """
-    Multi-touch gesture server. This uses a TUIO server to receive basic touch
-    events, which are translated to gestures using gesture trackers. Trackers
-    are assigned to a Window object, and gesture handlers are bound to a
-    tracker.
-    """
-    def __init__(self):
-        # List of all connected windows
-        self.windows = []
-
-        # Map of point sid to TouchPoint object
-        self.points = {}
-
-        # Create TUIO server, to be started later
-        self.tuio_server = TuioServer2D(self)
-
-    def add_window(self, window):
-        self.windows.append(window)
-
-    def remove_window(self, window):
-        self.windows.remove(window)
-        # TODO: Remove window from touch points
-
-    def on_point_down(self, sid, x, y):
-        if sid in self.points:
-            raise ValueError('Point with sid %d already exists.' % sid)
-
-        # Create a new touch point
-        self.points[sid] = point = TouchPoint(x, y, sid)
-
-        # Save the windows containing the point in a dictionary, and update
-        # their trackers
-        for window in self.windows:
-            if window.contains(point):
-                point.add_window(window)
-                window.update_trackers('down', point)
-
-    def on_point_move(self, sid, x, y):
-        if sid not in self.points:
-            raise KeyError('No point with sid %d exists.' % sid)
-
-        # Update point position and corresponding window trackers
-        point = self.points[sid]
-        point.set_position(x, y)
-        point.update_window_trackers('move')
-
-    def on_point_up(self, sid):
-        if sid not in self.points:
-            raise KeyError('No point with sid %d exists.' % sid)
-
-        # Clear the list of windows containing the point, and update their
-        # trackers
-        self.points[sid].update_window_trackers('up')
-        del self.points[sid]
-
-    def start(self):
-        # Assert that at least one window exists, by adding a fullscreen window
-        # if none have been added yet
-        if not self.windows:
-            self.add_window(FullscreenWindow())
-
-        self.tuio_server.start()
-
-    def stop(self):
-        self.tuio_server.stop()
-
-    def run(self):
-        self.tuio_server.run()

+ 0 - 33
src/point.py

@@ -1,33 +0,0 @@
-from geometry import AcceleratedPositionable
-
-
-class TouchPoint(AcceleratedPositionable):
-    """
-    Representation of an object touching the screen. The simplest form of a
-    touch object is a 'point', represented by an (x, y) position and a TUIO
-    session id (sid).  All dimensions are in pixels.
-    """
-    def __init__(self, x, y, sid):
-        super(TouchPoint, self).__init__(x, y)
-        self.sid = sid
-
-        # List of all windows this point is located in
-        self.windows = []
-
-    def __str__(self):
-        return '<%s at (%s, %s) sid=%d>' \
-               % (self.__class__.__name__, self.x, self.y, self.sid)
-
-    def add_window(self, window):
-        self.windows.append(window)
-
-    def remove_window(self, window):
-        self.windows.remove(window)
-
-    def update_window_trackers(self, event_type):
-        for window in self.windows:
-            window.update_trackers(event_type, self)
-
-
-# TODO: Extend with more complex touch object, e.g.:
-#class TouchFiducial(TouchPoint): ...

+ 32 - 0
src/touch_objects.py

@@ -0,0 +1,32 @@
+from geometry import AcceleratedPositionable
+
+
+class TouchPoint(AcceleratedPositionable):
+    """
+    Representation of an object touching the screen. The simplest form of a
+    touch object is a 'point', represented by an (x, y) position and a unique
+    object id. All dimensions are in pixels.
+    """
+    def __init__(self, x, y):
+        super(TouchPoint, self).__init__(x, y)
+        self.object_id = self.__class__.create_object_id()
+
+    def get_id(self):
+        return self.object_id
+
+    object_id_count = 0
+
+    @classmethod
+    def create_object_id(cls):
+        cls.object_id_count += 1
+        return cls.object_id_count
+
+
+# TODO: Extend with more complex touch object, e.g.:
+#class TouchFiducial(TouchPoint): ...
+
+
+OBJECT_NAMES = {
+    'point': TouchPoint,
+    #'fiducial': TouchFiducial,
+}

+ 42 - 53
src/tracker.py

@@ -6,57 +6,33 @@ class GestureTracker(Logger):
     Abstract class for gesture tracker definitions. Contains methods for
     changing the state of touch points.
     """
-    # Supported gesture types
+    # Supported gesture classes
+    supported_gestures = NotImplemented
+
+    # Supported gesture types (filled on factory registration)
     gesture_types = []
 
     # Configurable properties (see configure() method)
     configurable = []
 
-    def __init__(self, window=None):
-        # Hashmap of gesture types
-        self.handlers = {}
-
-        if window:
-            window.add_tracker(self)
+    def __init__(self, widget):
+        self.widget = widget
 
-    def bind(self, gesture_type, handler, *args, **kwargs):
+    def handle_event(self, event):
         """
-        Bind a handler to a gesture type. Multiple handlers can be bound to a
-        single gesture type. Optionally, (keyword) arguments that will be
-        passed to the handler along with a Gesture object can be specified.
+        Handle an event that was delegated by a widget. The tracker
+        implementation should define a handler function for the event.
+        Otherwise, the event will be ignored.
         """
-        if gesture_type not in self.gesture_types:
-            raise ValueError('Unsupported gesture type "%s".' % gesture_type)
-
-        h = handler, args, kwargs
+        handler_name = 'on_' + event.get_type()
 
-        if gesture_type not in self.handlers:
-            self.handlers[gesture_type] = [h]
-        else:
-            self.handlers[gesture_type].append(h)
+        if hasattr(self, handler_name):
+            getattr(self, handler_name)(event)
 
     def trigger(self, gesture):
-        if gesture._type not in self.handlers:
-            self.debug('Triggered "%s", but no handlers are bound.'
-                       % gesture._type)
-            return
-
+        gesture.set_tracker(self)
         self.info('Triggered %s.' % gesture)
-
-        for handler, args, kwargs in self.handlers[gesture._type]:
-            handler(gesture, *args, **kwargs)
-
-    def is_type_bound(self, gesture_type):
-        return gesture_type in self.handlers
-
-    def on_point_down(self, point):
-        pass
-
-    def on_point_move(self, point):
-        pass
-
-    def on_point_up(self, point):
-        pass
+        self.widget.handle_gesture(gesture)
 
     def configure(self, **kwargs):
         for name, value in kwargs.iteritems():
@@ -66,22 +42,35 @@ class GestureTracker(Logger):
 
             setattr(self, name, value)
 
-    def __getattr__(self, name):
-        """
-        Allow calls like:
-        tracker.gesture(...)
-        instead of:
-        tracker.bind('gesture', ...)
-        """
-        if name not in self.gesture_types:
-            raise AttributeError("'%s' has no attribute '%s'"
-                                 % (self.__class__.__name__, name))
-
-        return lambda handler: self.bind(name, handler)
-
 
 class Gesture(object):
     """
     Abstract class that represents a triggered gesture.
     """
-    pass
+    _type = NotImplemented
+
+    def __init__(self, originating_event=None):
+        self.stop = False
+        self.tracker = None
+        self.originating_event = originating_event
+
+    def get_type(self):
+        return self._type
+
+    def get_tracker(self):
+        return self.tracker
+
+    def get_event(self):
+        return self.originating_event
+
+    def has_event(self):
+        return bool(self.originating_event)
+
+    def set_tracker(self, tracker):
+        self.tracker = tracker
+
+    def stop_propagation(self):
+        self.stop = True
+
+    def is_propagation_stopped(self):
+        return self.stop

+ 33 - 0
src/trackers/__init__.py

@@ -0,0 +1,33 @@
+from basic import BasicEventTracker
+from tap import TapTracker
+from transform import TransformationTracker
+
+
+# Map of gesture type to tracker type
+_tracker_types = {}
+
+
+def _register_tracker(tracker_type):
+    tracker_type.gesture_types = \
+            [gesture._type for gesture in tracker_type.supported_gestures]
+
+    for gesture_type in tracker_type.gesture_types:
+        if gesture_type in _tracker_types:
+            raise ValueError('Gesture type "%s" is already registered to '
+                                'tracker type "%s".' % (gesture_type,
+                                _tracker_types[gesture_type].__name__))
+
+        _tracker_types[gesture_type] = tracker_type
+
+
+def create_tracker(gesture_type, widget):
+    if gesture_type not in _tracker_types:
+        raise KeyError('No tracker type registered for gesture type "%s".'
+                        % gesture_type)
+
+    return _tracker_types[gesture_type](widget)
+
+
+_register_tracker(BasicEventTracker)
+_register_tracker(TapTracker)
+_register_tracker(TransformationTracker)

+ 18 - 18
src/trackers/basic.py

@@ -2,30 +2,30 @@ from ..tracker import GestureTracker
 from utils import PointGesture
 
 
-class BasicTracker(GestureTracker):
-    """
-    The main goal of this class is to provide a triggering mechanism for the
-    low-level point-down, point-move and point-up events.
-    """
-    gesture_types = ['down', 'move', 'up']
+class DownGesture(PointGesture):
+    _type = 'point_down'
 
-    def on_point_down(self, point):
-        self.trigger(DownGesture(point))
 
-    def on_point_move(self, point):
-        self.trigger(MoveGesture(point))
+class MoveGesture(PointGesture):
+    _type = 'point_move'
 
-    def on_point_up(self, point):
-        self.trigger(UpGesture(point))
 
+class UpGesture(PointGesture):
+    _type = 'point_up'
 
-class DownGesture(PointGesture):
-    _type = 'down'
 
+class BasicEventTracker(GestureTracker):
+    """
+    The main goal of this class is to provide a triggering mechanism for the
+    low-level point-down, point-move and point-up events.
+    """
+    supported_gestures = [DownGesture, MoveGesture, UpGesture]
 
-class MoveGesture(PointGesture):
-    _type = 'move'
+    def on_point_down(self, event):
+        self.trigger(DownGesture(event))
 
+    def on_point_move(self, event):
+        self.trigger(MoveGesture(event))
 
-class UpGesture(PointGesture):
-    _type = 'up'
+    def on_point_up(self, event):
+        self.trigger(UpGesture(event))

+ 46 - 41
src/trackers/tap.py

@@ -6,8 +6,32 @@ from ..geometry import Positionable
 from utils import PointGesture
 
 
+class TapGesture(PointGesture):
+    """
+    A tap gesture is triggered when a touch point releases from the screen
+    within a certain time and distance from its 'point_down' event.
+    """
+    _type = 'tap'
+
+
+class SingleTapGesture(TapGesture):
+    """
+    A single tap gesture is triggered after a regular tap gesture, if no double
+    tap is triggered for that gesture.
+    """
+    _type = 'single_tap'
+
+
+class DoubleTapGesture(TapGesture):
+    """
+    A double tap gesture is triggered if two sequential taps are triggered
+    within a certain time and distance of eachother.
+    """
+    _type = 'double_tap'
+
+
 class TapTracker(GestureTracker):
-    gesture_types = ['tap', 'single_tap', 'double_tap']
+    supported_gestures = [TapGesture, SingleTapGesture, DoubleTapGesture]
 
     configurable = ['tap_distance', 'tap_time', 'double_tap_time',
                     'double_tap_distance', 'update_rate']
@@ -15,7 +39,7 @@ class TapTracker(GestureTracker):
     def __init__(self, window=None):
         super(TapTracker, self).__init__(window)
 
-        # Map of TUIO session id to tuple (timestamp, position) of point down
+        # Map of touch object id to tuple (timestamp, position) of point down
         self.reg = {}
 
         # Maximum radius in which a touch point can move in order to be a tap
@@ -48,7 +72,7 @@ class TapTracker(GestureTracker):
             if self.last_tap and time_diff > self.double_tap_time:
                 # Last tap is too long ago to be a double tap, so trigger a
                 # single tap
-                self.trigger(SingleTapGesture(self.last_tap))
+                self.trigger(SingleTapGesture(None, self.last_tap))
                 self.reset_last_tap()
 
             time.sleep(1. / self.update_rate)
@@ -57,28 +81,32 @@ class TapTracker(GestureTracker):
         self.last_tap_time = 0
         self.last_tap = None
 
-    def on_point_down(self, point):
-        x, y = point.get_position()
-        self.reg[point.sid] = time.time(), Positionable(x, y)
+    def on_point_down(self, event):
+        x, y = event.get_position()
+        self.reg[event.point.get_id()] = time.time(), Positionable(x, y)
+
+    def on_point_move(self, event):
+        oid = event.point.get_id()
 
-    def on_point_move(self, point):
-        if point.sid not in self.reg:
+        if oid not in self.reg:
             return
 
         # If a stationary point moves beyond a threshold, delete it so that the
         # 'up' event will not trigger a 'tap'
-        t, initial_position = self.reg[point.sid]
+        t, initial_position = self.reg[oid]
 
-        if point.distance_to(initial_position) > self.tap_distance:
-            del self.reg[point.sid]
+        if event.point.distance_to(initial_position) > self.tap_distance:
+            del self.reg[oid]
+
+    def on_point_up(self, event):
+        oid = event.point.get_id()
 
-    def on_point_up(self, point):
         # Assert that the point has not been deleted by a 'move' event yet
-        if point.sid not in self.reg:
+        if oid not in self.reg:
             return
 
-        down_time = self.reg[point.sid][0]
-        del self.reg[point.sid]
+        down_time = self.reg[oid][0]
+        del self.reg[oid]
 
         # Only trigger a tap event if the 'up' is triggered within a certain
         # time afer the 'down'
@@ -87,43 +115,20 @@ class TapTracker(GestureTracker):
         if current_time - down_time > self.tap_time:
             return
 
-        tap = TapGesture(point)
+        tap = TapGesture(event)
         self.trigger(tap)
 
         # Trigger double tap if the threshold has not not expired yet
         if self.last_tap:
             if self.last_tap.distance_to(tap) <= self.double_tap_distance:
                 # Close enough to be a double tap
-                self.trigger(DoubleTapGesture(self.last_tap))
+                self.trigger(DoubleTapGesture(event, self.last_tap))
                 self.reset_last_tap()
                 return
 
             # Generate a seperate single tap gesture for the last tap,
             # because the lat tap variable is overwritten now
-            self.trigger(SingleTapGesture(self.last_tap))
+            self.trigger(SingleTapGesture(event, self.last_tap))
 
         self.last_tap_time = current_time
         self.last_tap = tap
-
-
-class TapGesture(PointGesture):
-    """
-    A tap gesture is triggered
-    """
-    _type = 'tap'
-
-
-class SingleTapGesture(TapGesture):
-    """
-    A single tap gesture is triggered after a regular tap gesture, if no double
-    tap is triggered for that gesture.
-    """
-    _type = 'single_tap'
-
-
-class DoubleTapGesture(TapGesture):
-    """
-    A double tap gesture is triggered if two sequential taps are triggered
-    within a certain time and distance of eachother.
-    """
-    _type = 'double_tap'

+ 74 - 72
src/trackers/transform.py

@@ -4,15 +4,71 @@ from ..tracker import GestureTracker, Gesture
 from ..geometry import Positionable, MovingPositionable
 
 
+class RotationGesture(Gesture, Positionable):
+    """
+    A rotation gesture has a angle in radians and a rotational centroid.
+    """
+    _type = 'rotate'
+
+    def __init__(self, event, centroid, angle):
+        Gesture.__init__(self, event)
+        Positionable.__init__(self, *centroid.get_position())
+        self.angle = angle
+
+    def __str__(self):
+        return '<RotationGesture at (%s, %s) angle=%s>' \
+               % (self.x, self.y, self.angle)
+
+    def get_angle(self):
+        return self.angle
+
+
+class PinchGesture(Gesture, Positionable):
+    """
+    A pinch gesture has a scale (1.0 means no scaling) and a centroid from
+    which the scaling originates.
+    """
+    _type = 'pinch'
+
+    def __init__(self, event, centroid, scale):
+        Gesture.__init__(self, event)
+        Positionable.__init__(self, *centroid.get_position())
+        self.scale = scale
+
+    def __str__(self):
+        return '<PinchGesture at (%s, %s) scale=%s>' \
+               % (self.x, self.y, self.scale)
+
+    def get_scale(self):
+        return self.scale
+
+
+class DragGesture(Gesture, Positionable):
+    """
+    A momevent gesture has an initial position, and a translation from that
+    position.
+    """
+    _type = 'drag'
+
+    def __init__(self, event, initial_position, translation):
+        Gesture.__init__(self, event)
+        Positionable.__init__(self, *initial_position.get_position())
+        self.translation = translation
+
+    def __str__(self):
+        return '<DragGesture at (%s, %s) translation=(%s, %s)>' \
+               % (self.get_position() + self.translation.get_position())
+
+
 class TransformationTracker(GestureTracker):
     """
     Tracker for linear transformations. This implementation detects rotation,
     scaling and translation using the centroid of all touch points.
     """
-    gesture_types = ['rotate', 'pinch', 'move']
+    supported_gestures = [RotationGesture, PinchGesture, DragGesture]
 
-    def __init__(self, window=None):
-        super(TransformationTracker, self).__init__(window)
+    def __init__(self, widget=None):
+        super(TransformationTracker, self).__init__(widget)
 
         # All touch points performing the transformation
         self.points = []
@@ -40,87 +96,33 @@ class TransformationTracker(GestureTracker):
         else:
             self.centroid = MovingPositionable(x, y)
 
-    def on_point_down(self, point):
-        self.points.append(point)
+    def on_point_down(self, event):
+        self.points.append(event.point)
         self.update_centroid()
 
-    def on_point_move(self, point):
+    def on_point_move(self, event):
+        point = event.point
         l = len(self.points)
 
         if l > 1:
             # Rotation (around the previous centroid)
-            if self.is_type_bound('rotate'):
-                rotation = point.rotation_around(self.centroid) / l
-                self.trigger(RotationGesture(self.centroid, rotation))
+            rotation = point.rotation_around(self.centroid) / l
+            self.trigger(RotationGesture(event, self.centroid, rotation))
 
             # Scale
-            if self.is_type_bound('pinch'):
-                prev = point.get_previous_position().distance_to(self.centroid)
-                dist = point.distance_to(self.centroid)
-                dist = prev + (dist - prev) / l
-                scale = dist / prev
-                self.trigger(PinchGesture(self.centroid, scale))
+            prev = point.get_previous_position().distance_to(self.centroid)
+            dist = point.distance_to(self.centroid)
+            dist = prev + (dist - prev) / l
+            scale = dist / prev
+            self.trigger(PinchGesture(event, self.centroid, scale))
 
         # Update centroid before movement can be detected
         self.update_centroid()
 
         # Movement
-        self.trigger(MovementGesture(self.centroid,
-                                     self.centroid.translation()))
+        self.trigger(DragGesture(event, self.centroid,
+                                 self.centroid.translation()))
 
-    def on_point_up(self, point):
-        self.points.remove(point)
+    def on_point_up(self, event):
+        self.points.remove(event.point)
         self.update_centroid()
-
-
-class RotationGesture(Positionable, Gesture):
-    """
-    A rotation gesture has a angle in radians and a rotational centroid.
-    """
-    _type = 'rotate'
-
-    def __init__(self, centroid, angle):
-        Positionable.__init__(self, *centroid.get_position())
-        self.angle = angle
-
-    def __str__(self):
-        return '<RotationGesture at (%s, %s) angle=%s>' \
-               % (self.x, self.y, self.angle)
-
-    def get_angle(self):
-        return self.angle
-
-
-class PinchGesture(Positionable, Gesture):
-    """
-    A pinch gesture has a scale (1.0 means no scaling) and a centroid from
-    which the scaling originates.
-    """
-    _type = 'pinch'
-
-    def __init__(self, centroid, scale):
-        Positionable.__init__(self, *centroid.get_position())
-        self.scale = scale
-
-    def __str__(self):
-        return '<PinchGesture at (%s, %s) scale=%s>' \
-               % (self.x, self.y, self.scale)
-
-    def get_scale(self):
-        return self.scale
-
-
-class MovementGesture(Positionable, Gesture):
-    """
-    A momevent gesture has an initial position, and a translation from that
-    position.
-    """
-    _type = 'move'
-
-    def __init__(self, initial_position, translation):
-        Positionable.__init__(self, *initial_position.get_position())
-        self.translation = translation
-
-    def __str__(self):
-        return '<MovementGesture at (%s, %s) translation=(%s, %s)>' \
-               % (self.get_position() + self.translation.get_position())

+ 9 - 5
src/trackers/utils.py

@@ -2,11 +2,15 @@ from ..tracker import Gesture
 from ..geometry import Positionable
 
 
-class PointGesture(Positionable, Gesture):
+class PointGesture(Gesture, Positionable):
     """
-    Abstract base class for positionable gestures that have the same
-    coordinated as a touch point.
+    Abstract base class for positionable gestures that have an assigned touch
+    point.
     """
-    def __init__(self, point):
-        # Use the coordinates of the touch point
+    def __init__(self, event, point=None):
+        Gesture.__init__(self, event)
+
+        if not point:
+            point = event.get_touch_object()
+
         Positionable.__init__(self, *point.get_position())

+ 0 - 120
src/tuio_server.py

@@ -1,120 +0,0 @@
-from OSC import OSCServer
-OSCServer.print_tracebacks = True
-
-from event_server import EventServer
-from screen import pixel_coords
-
-
-class TuioServer2D(EventServer):
-    tuio_address = 'localhost', 3333
-
-    def __init__(self, handler_obj):
-        super(TuioServer2D, self).__init__(handler_obj)
-
-        # OSC server that listens to incoming TUIO events
-        self.server = OSCServer(self.tuio_address)
-        self.server.addDefaultHandlers()
-        self.server.addMsgHandler('/tuio/2Dobj', self._receive)
-        self.server.addMsgHandler('/tuio/2Dcur', self._receive)
-        self.server.addMsgHandler('/tuio/2Dblb', self._receive)
-
-        # List of alive seddion id's
-        self.alive = set()
-
-        # List of session id's of points that have generated a 'point_down'
-        # event
-        self.down = set()
-
-    def _receive(self, addr, tags, data, source):
-        surface = addr[8:]
-        #self.debug('Received message <surface=%s tags="%s" '
-        #           'data=%s source=%s>' % (surface, tags, data, source))
-        msg_type = data[0]
-
-        # FIXME: Ignore obj/blb events?
-        if surface != 'cur':
-            return
-
-        if msg_type == 'alive':
-            alive = set(data[1:])
-            released = self.alive - alive
-            self.alive = alive
-
-            if released:
-                self.debug('Released %s.' % ', '.join(map(str, released)))
-                self.down -= released
-
-            for sid in released:
-                self.handler_obj.on_point_up(sid)
-        elif msg_type == 'set':
-            sid, x, y = data[1:4]
-
-            if sid not in self.alive:
-                raise ValueError('Point with sid "%d" is not alive.' % sid)
-
-            # Translate to pixel coordinates
-            px, py = pixel_coords(x, y)
-
-            # Check if 'point_down' has already been triggered. If so, trigger
-            # a 'point_move' event instead
-            if sid in self.down:
-                self.debug('Moved %d to (%.4f, %.4f) or (%d, %d).'
-                           % (sid, x, y, px, py))
-                self.handler_obj.on_point_move(sid, px, py)
-            else:
-                self.debug('Down %d at (%.4f, %.4f) or (%d, %d).'
-                           % (sid, x, y, px, py))
-                self.down.add(sid)
-                self.handler_obj.on_point_down(sid, px, py)
-
-    def run(self):
-        self.server.handle_request()
-
-    def start(self):
-        self.info('Starting OSC server')
-        self.server.serve_forever()
-
-    def stop(self):
-        self.info('Stopping OSC server')
-        self.server.close()
-
-
-if __name__ == '__main__':
-    import argparse
-    import logging
-
-    from logger import Logger
-    from event_server import EventServerHandler
-
-    parser = argparse.ArgumentParser(description='TUIO server test.')
-    parser.add_argument('--log', metavar='LOG_LEVEL', default='INFO',
-            choices=['DEBUG', 'INFO', 'WARNING'], help='Global log level.')
-    parser.add_argument('--logfile', metavar='FILENAME', help='Filename for '
-            'the log file (the log is printed to stdout by default).')
-    args = parser.parse_args()
-
-    # Configure logger
-    log_config = dict(level=getattr(logging, args.log))
-
-    if args.logfile:
-        log_config['filename'] = args.logfile
-
-    Logger.configure(**log_config)
-
-    # Define handlers
-    class Handler(EventServerHandler, Logger):
-        def on_point_down(self, sid, x, y):
-            self.info('Point down: sid=%d (%.4f, %.4f)' % (sid, x, y))
-
-        def on_point_up(self, sid):
-            self.info('Point up: sid=%d' % sid)
-
-        def on_point_move(self, sid, x, y):
-            self.info('Point move: sid=%d (%.4f, %.4f)' % (sid, x, y))
-
-    server = TuioServer2D(Handler())
-
-    try:
-        server.start()
-    except KeyboardInterrupt:
-        server.stop()

+ 142 - 0
src/widget.py

@@ -0,0 +1,142 @@
+from geometry import Positionable
+from logger import Logger
+from trackers import create_tracker
+
+
+class Widget(Positionable, Logger):
+    """
+    A widget represents a 2D object on the screen in which gestures can occur.
+    Handlers for a specific gesture type can be bound to a widget. The widget
+    will
+    """
+    def __init__(self, x=None, y=None):
+        Positionable.__init__(self, x, y)
+
+        # Map of gesture types to gesture trackers
+        self.trackers = {}
+
+        # Map of gesture types to a list of handlers for that type
+        self.handlers = {}
+
+        # Widget tree references
+        self.parent = None
+        self.children = []
+
+    def add_widget(self, widget):
+        """
+        Add a new child widget.
+        """
+        self.children.append(widget)
+        widget.set_parent(self)
+
+    def remove_widget(self, widget):
+        """
+        Remove a child widget.
+        """
+        self.children.remove(widget)
+        widget.set_parent(None)
+
+    def set_parent(self, widget):
+        """
+        Set a new parent widget. If a parent widget has already been assigned,
+        remove the widget from that parent first.
+        """
+        if self.parent:
+            self.parent.remove_widget(self)
+
+        self.parent = widget
+
+    def unbind(self, gesture_type, handler=None):
+        """
+        Unbind a single handler, or all handlers bound to a gesture type.
+        Remove the corresponding tracker if it is no longer needed.
+        """
+        if gesture_type not in self.handlers:
+            raise KeyError('Gesture type "%s" is not bound.')
+
+        if handler:
+            # Remove a specific handler
+            self.handlers[gesture_type].remove(handler)
+
+            # Only remove the handler list and optionally the tracker if no
+            # other handlers exist for the gesture type
+            if self.handlers[gesture_type]:
+                return
+        else:
+            # Remove handler list
+            del self.handlers[gesture_type][:]
+
+        # Check if any other handlers need the tracker
+        for gtype in self.trackers[gesture_type].gesture_types:
+            if gtype in self.handlers:
+                return
+
+        # No more handlers are bound, remove unused tracker and handlers
+        del self.trackers[gesture_type]
+
+        for gtype in self.trackers[gesture_type].gesture_types:
+            del self.handlers[gtype]
+
+    def bind(self, gesture_type, handler, remove_existing=False):
+        """
+        Bind a handler to the specified type of gesture. Create a tracker for
+        the gesture type if it does not exists yet.
+        """
+        if gesture_type not in self.handlers:
+            tracker = create_tracker(gesture_type, self)
+            self.trackers[gesture_type] = tracker
+            self.handlers[gesture_type] = []
+
+            # Create empty tracker lists for all supported gestures
+            for gtype in tracker.gesture_types:
+                self.handlers[gtype] = []
+        elif remove_existing:
+            del self.handlers[gesture_type][:]
+
+        self.handlers[gesture_type].append(handler)
+
+    def __getattr__(self, name):
+        """
+        Allow calls like:
+        widget.on_gesture(...)
+        instead of:
+        widget.bind('gesture', ...)
+        """
+        if len(name) < 4 or name[:3] != 'on_':
+            raise AttributeError("'%s' has no attribute '%s'"
+                                 % (self.__class__.__name__, name))
+
+        return lambda handler: self.bind(name[3:], handler)
+
+    def contains_event(self, event):
+        """
+        Check if the coordinates of an event are contained within this widget.
+        """
+        raise NotImplementedError
+
+    def handle_event(self, event):
+        """
+        Delegate a triggered event to gesture trackers and child widgets.  A
+        handler can stop the delegation of the event, preventing it from being
+        delegated to child widgets.
+        """
+        for tracker in set(self.trackers.itervalues()):
+            tracker.handle_event(event)
+
+        if not event.is_delegation_stopped():
+            for child in self.children:
+                if child.contains_event(event):
+                    child.handle_event(event)
+
+    def handle_gesture(self, gesture):
+        """
+        Handle a gesture that is triggered by a gesture tracker. First, all
+        handlers bound to the gesture type are called. Second, if the gesture's
+        propagation has not been stopped by a handler, the gesture is
+        propagated to the parent widget.
+        """
+        for handler in self.handlers.get(gesture.get_type(), ()):
+            handler(gesture)
+
+        if self.parent and not gesture.is_propagation_stopped():
+            self.parent.handle_gesture(gesture)

+ 66 - 0
src/widgets.py

@@ -0,0 +1,66 @@
+from widget import Widget
+from screen import screen_size
+
+
+class RectangularWidget(Widget):
+    """
+    Rectangular widget, has a position and a size.
+    """
+    def __init__(self, x, y, width, height):
+        super(RectangularWidget, self).__init__(x, y)
+        self.set_size(width, height)
+
+    def __str__(self):
+        return '<%s at (%s, %s) size=(%s, %s)>' \
+               % (self.__class__.__name__, self.x, self.y, self.width,
+                  self.height)
+
+    def set_size(self, width, height):
+        self.width = width
+        self.height = height
+
+    def get_size(self):
+        return self.width, self.height
+
+    def contains_event(self, event):
+        x, y = event.get_position()
+        return self.x <= x <= self.x + self.width \
+               and self.y <= y <= self.y + self.height
+
+
+class CircularWidget(Widget):
+    """
+    Circular widget, has a position and a radius.
+    """
+    def __init__(self, x, y, radius):
+        super(CircularWidget, self).__init__(x, y)
+        self.set_radius(radius)
+
+    def __str__(self):
+        return '<%s at (%s, %s) size=(%s, %s)>' \
+               % (self.__class__.__name__, self.x, self.y, self.width,
+                  self.height)
+
+    def set_radius(self, radius):
+        self.radius = radius
+
+    def get_radius(self):
+        return self.radius
+
+    def contains_event(self, event):
+        return self.distance_to(event.get_touch_object()) <= self.radius
+
+
+class FullscreenWidget(RectangularWidget):
+    """
+    Widget representation for the entire screen. This class provides an easy
+    way to create a single rectangular widget that catches all gestures.
+    """
+    def __init__(self):
+        super(FullscreenWidget, self).__init__(0, 0, *screen_size)
+
+    def __str__(self):
+        return '<FullscreenWidget size=(%d, %d)>' % self.get_size()
+
+    def contains_event(self, event):
+        return True

+ 0 - 85
src/window.py

@@ -1,85 +0,0 @@
-from geometry import Surface, RectangularSurface, CircularSurface
-from screen import screen_size
-
-
-class Window(Surface):
-    """
-    Abstract class that represents a 2D object on the screen to which gesture
-    trackers can be added. Implementations of this class should define a method
-    that can detect wether a touch point is located within the window.
-
-    Because any type of window on the screen has (at least) an (x, y) position,
-    it extends the Positionable object.
-
-    Note: Dimensions used in implementations of this class should be in pixels.
-    """
-    def __init__(self, **kwargs):
-        # All trackers that are currently bound to this window
-        self.trackers = []
-
-        # List of points that originated in this window
-        self.points = []
-
-        if 'server' in kwargs:
-            kwargs['server'].add_window(self)
-
-    def add_tracker(self, tracker):
-        self.trackers.append(tracker)
-
-    def remove_tracker(self, tracker):
-        self.trackers.remove(tracker)
-
-    def on_point_down(self, point):
-        self.points.append(point)
-        self.update_trackers('down', point)
-
-    def on_point_move(self, point):
-        self.update_trackers('move', point)
-
-    def on_point_up(self, point):
-        self.points.remove(point)
-        self.update_trackers('up', point)
-
-    def update_trackers(self, event_type, point):
-        """
-        Update all gesture trackers that are bound to this window with an
-        added/moved/removed touch point.
-        """
-        handler_name = 'on_point_' + event_type
-
-        for tracker in self.trackers:
-            if hasattr(tracker, handler_name):
-                getattr(tracker, handler_name)(point)
-
-
-class RectangularWindow(Window, RectangularSurface):
-    """
-    Rectangular window.
-    """
-    def __init__(self, x, y, width, height, **kwargs):
-        Window.__init__(self, **kwargs)
-        RectangularSurface.__init__(self, x, y, width, height)
-
-
-class CircularWindow(Window, CircularSurface):
-    """
-    Circular window.
-    """
-    def __init__(self, x, y, radius, **kwargs):
-        Window.__init__(self, **kwargs)
-        CircularSurface.__init__(self, x, y, radius)
-
-
-class FullscreenWindow(RectangularWindow):
-    """
-    Window representation for the entire screen. This class provides an easy
-    way to create a single rectangular window that catches all gestures.
-    """
-    def __init__(self, **kwargs):
-        super(FullscreenWindow, self).__init__(0, 0, *screen_size, **kwargs)
-
-    def __str__(self):
-        return '<FullscreenWindow size=(%d, %d)>' % (self.width, self.height)
-
-    def contains(self, point):
-        return True

+ 10 - 12
tests/basic.py

@@ -1,21 +1,19 @@
-from src.gesture_server import GestureServer
-from src.window import FullscreenWindow
-from src.trackers.basic import BasicTracker
-
+from src.event_server import EventServer
+from src.widgets import FullscreenWidget
 from tests.parse_arguments import create_parser, parse_args
+
 parse_args(create_parser())
 
-# Create server
-server = GestureServer()
+# Create server and fullscreen widget
+screen = FullscreenWidget()
+server = EventServer(screen)
 
-# Create a window to add trackers to
-win = FullscreenWindow(server=server)
+# Bind handlers
 
 # Add tracker and handlers
-tracker = BasicTracker(win)
-tracker.down(lambda g: 0)
-tracker.move(lambda g: 0)
-tracker.up(lambda g: 0)
+screen.on_point_down(lambda g: 0)
+screen.on_point_move(lambda g: 0)
+screen.on_point_up(lambda g: 0)
 
 # Start listening to TUIO events
 try:

+ 30 - 22
tests/draw.py

@@ -4,19 +4,17 @@ import pygame
 from threading import Thread
 from math import degrees
 
-from src.gesture_server import GestureServer
-from src.window import FullscreenWindow
-from src.trackers.transform import TransformationTracker
-from src.trackers.tap import TapTracker
+from src.event_server import EventServer
+from src.widgets import FullscreenWidget
+from tests.parse_arguments import create_parser, parse_args
 from src.screen import screen_size
 
-from tests.parse_arguments import create_parser, parse_args
+# Parse arguments
 parser = create_parser()
 parser.add_argument('-f', '--fullscreen', action='store_true', default=False,
                     help='run in fullscreen')
 args = parse_args(parser)
 
-
 pygame.init()
 
 # Config
@@ -56,6 +54,7 @@ angle = 0
 scale = 1
 taps = []
 dtaps = []
+points = []
 
 
 def update():
@@ -73,14 +72,14 @@ def update():
     screen.blit(transformed, rect)
 
     # Draw touch points
-    if transform.centroid:
-        c = coord(*transform.centroid.xy)
+    if transform and  transform.centroid:
+            c = coord(*transform.centroid.xy)
 
-    for p in server.points.itervalues():
+    for p in points:
         xy = coord(p.x, p.y)
 
         # Draw line to centroid
-        if transform.centroid:
+        if transform and transform.centroid:
             pygame.draw.line(screen, LINE_COLOR, xy, c, 1)
 
         # Draw outlined circle around touch point
@@ -90,7 +89,7 @@ def update():
         pygame.draw.circle(screen, BG_COLOR, xy, FINGER_RADIUS - 1, 0)
 
     # Draw filled circle around centroid
-    if transform.centroid:
+    if transform and transform.centroid:
         pygame.draw.circle(screen, CIRCLE_COLOR, c, CENTROID_RADIUS)
 
     # Draw an expanding circle around each tap event
@@ -125,30 +124,39 @@ def update():
     pygame.display.flip()
 
 
-# Create server and fullscreen window
-server = GestureServer()
-win = FullscreenWindow(server=server)
+transform = None
+
+
+def save_tracker(gesture):
+    global transform
+
+    if not transform:
+        transform = gesture.get_tracker()
 
 
-# Bind trackers
 def rotate(gesture):
     global angle
     angle += gesture.get_angle()
+    save_tracker(gesture)
 
 
 def pinch(gesture):
     global scale
     scale = min(scale * gesture.get_scale(), MAX_SCALE)
+    save_tracker(gesture)
+
 
+widget = FullscreenWidget()
+server = EventServer(widget)
+widget.on_rotate(rotate)
+widget.on_pinch(pinch)
 
-transform = TransformationTracker(win)
-transform.rotate(rotate)
-transform.pinch(pinch)
+widget.on_tap(lambda g: taps.append([coord(*g.xy), FINGER_RADIUS]))
+widget.on_single_tap(lambda g: dtaps.append(list(coord(*g.xy)) + [1]))
+widget.on_double_tap(lambda g: dtaps.append(list(coord(*g.xy)) + [0]))
 
-tap = TapTracker(win)
-tap.tap(lambda g: taps.append([coord(*g.xy), FINGER_RADIUS]))
-tap.single_tap(lambda g: dtaps.append(list(coord(*g.xy)) + [1]))
-tap.double_tap(lambda g: dtaps.append(list(coord(*g.xy)) + [0]))
+widget.on_point_down(lambda g: points.append(g.get_event().point))
+widget.on_point_up(lambda g: points.remove(g.get_event().point))
 
 
 try:

+ 10 - 24
tests/tap.py

@@ -1,31 +1,17 @@
-from src.gesture_server import GestureServer
-from src.window import FullscreenWindow
-from src.trackers.tap import TapTracker
-
+from src.event_server import EventServer
+from src.widgets import FullscreenWidget
 from tests.parse_arguments import create_parser, parse_args
-parse_args(create_parser())
-
-# Create server
-server = GestureServer()
-
-# Create a window to add trackers to
-win = FullscreenWindow(server=server)
 
-# Above is short for:
-#win = FullscreenWindow()
-#server.add_window(win)
-
-# Add tracker and handlers
-tracker = TapTracker(win)
-
-
-def handler(gesture):
-    pass
+parse_args(create_parser())
 
+# Create server and fullscreen widget
+screen = FullscreenWidget()
+server = EventServer(screen)
 
-tracker.tap(handler)
-tracker.single_tap(handler)
-tracker.double_tap(handler)
+# Bind handlers
+screen.on_tap(lambda g: 0)
+screen.on_single_tap(lambda g: 0)
+screen.on_double_tap(lambda g: 0)
 
 # Start listening to TUIO events
 try:

+ 10 - 14
tests/transform.py

@@ -1,21 +1,17 @@
-from src.gesture_server import GestureServer
-from src.window import FullscreenWindow
-from src.trackers.transform import TransformationTracker
-
+from src.event_server import EventServer
+from src.widgets import FullscreenWidget
 from tests.parse_arguments import create_parser, parse_args
-parse_args(create_parser())
 
-# Create server
-server = GestureServer()
+parse_args(create_parser())
 
-# Create a window to add trackers to
-win = FullscreenWindow(server=server)
+# Create server and fullscreen widget
+screen = FullscreenWidget()
+server = EventServer(screen)
 
-# Add tracker and handlers
-tracker = TransformationTracker(win)
-tracker.rotate(lambda g: 0)
-tracker.pinch(lambda g: 0)
-tracker.move(lambda g: 0)
+# Bind handlers
+screen.on_rotate(lambda g: 0)
+screen.on_pinch(lambda g: 0)
+screen.on_drag(lambda g: 0)
 
 # Start listening to TUIO events
 try:

+ 7 - 12
tests/vtk_interactor.py

@@ -2,25 +2,20 @@ import vtk
 from threading import Thread
 from math import degrees
 
-from src.gesture_server import GestureServer
-from src.trackers.transform import TransformationTracker
-from src.trackers.tap import TapTracker
-from src.window import FullscreenWindow
+from src.event_server import EventServer
+from src.widgets import FullscreenWidget
 
 
 class vtkMultitouchInteractor():
     def __init__(self):
         self.iren = vtk.vtkRenderWindowInteractor()
 
-        self.server = GestureServer()
-        self.window = FullscreenWindow(server=self.server)
+        self.widget = FullscreenWidget()
+        self.server = EventServer(screen)
 
-        transform = TransformationTracker(window=self.window)
-        transform.rotate(self.on_rotate)
-        transform.pinch(self.on_pinch)
-
-        tap = TapTracker(window=self.window)
-        tap.tap(self.on_tap)
+        widget.on_rotate(self.on_rotate)
+        widget.on_pinch(self.on_pinch)
+        widget.on_tap(self.on_tap)
 
     def SetRenderWindow(self, window):
         self.iren.SetRenderWindow(window)