Răsfoiți Sursa

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 13 ani în urmă
părinte
comite
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 logger import Logger
+from drivers import select_driver
 
 
 
 
 class EventServer(Logger):
 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.
         Calculate the acceleration in pixels/second.
         """
         """
         return self.movement_distance() / self.movement_time()
         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
     Abstract class for gesture tracker definitions. Contains methods for
     changing the state of touch points.
     changing the state of touch points.
     """
     """
-    # Supported gesture types
+    # Supported gesture classes
+    supported_gestures = NotImplemented
+
+    # Supported gesture types (filled on factory registration)
     gesture_types = []
     gesture_types = []
 
 
     # Configurable properties (see configure() method)
     # Configurable properties (see configure() method)
     configurable = []
     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):
     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)
         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):
     def configure(self, **kwargs):
         for name, value in kwargs.iteritems():
         for name, value in kwargs.iteritems():
@@ -66,22 +42,35 @@ class GestureTracker(Logger):
 
 
             setattr(self, name, value)
             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):
 class Gesture(object):
     """
     """
     Abstract class that represents a triggered gesture.
     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
 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
 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):
 class TapTracker(GestureTracker):
-    gesture_types = ['tap', 'single_tap', 'double_tap']
+    supported_gestures = [TapGesture, SingleTapGesture, DoubleTapGesture]
 
 
     configurable = ['tap_distance', 'tap_time', 'double_tap_time',
     configurable = ['tap_distance', 'tap_time', 'double_tap_time',
                     'double_tap_distance', 'update_rate']
                     'double_tap_distance', 'update_rate']
@@ -15,7 +39,7 @@ class TapTracker(GestureTracker):
     def __init__(self, window=None):
     def __init__(self, window=None):
         super(TapTracker, self).__init__(window)
         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 = {}
         self.reg = {}
 
 
         # Maximum radius in which a touch point can move in order to be a tap
         # 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:
             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
                 # Last tap is too long ago to be a double tap, so trigger a
                 # single tap
                 # single tap
-                self.trigger(SingleTapGesture(self.last_tap))
+                self.trigger(SingleTapGesture(None, self.last_tap))
                 self.reset_last_tap()
                 self.reset_last_tap()
 
 
             time.sleep(1. / self.update_rate)
             time.sleep(1. / self.update_rate)
@@ -57,28 +81,32 @@ class TapTracker(GestureTracker):
         self.last_tap_time = 0
         self.last_tap_time = 0
         self.last_tap = None
         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
             return
 
 
         # If a stationary point moves beyond a threshold, delete it so that the
         # If a stationary point moves beyond a threshold, delete it so that the
         # 'up' event will not trigger a 'tap'
         # '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
         # 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
             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
         # Only trigger a tap event if the 'up' is triggered within a certain
         # time afer the 'down'
         # time afer the 'down'
@@ -87,43 +115,20 @@ class TapTracker(GestureTracker):
         if current_time - down_time > self.tap_time:
         if current_time - down_time > self.tap_time:
             return
             return
 
 
-        tap = TapGesture(point)
+        tap = TapGesture(event)
         self.trigger(tap)
         self.trigger(tap)
 
 
         # Trigger double tap if the threshold has not not expired yet
         # Trigger double tap if the threshold has not not expired yet
         if self.last_tap:
         if self.last_tap:
             if self.last_tap.distance_to(tap) <= self.double_tap_distance:
             if self.last_tap.distance_to(tap) <= self.double_tap_distance:
                 # Close enough to be a double tap
                 # Close enough to be a double tap
-                self.trigger(DoubleTapGesture(self.last_tap))
+                self.trigger(DoubleTapGesture(event, self.last_tap))
                 self.reset_last_tap()
                 self.reset_last_tap()
                 return
                 return
 
 
             # Generate a seperate single tap gesture for the last tap,
             # Generate a seperate single tap gesture for the last tap,
             # because the lat tap variable is overwritten now
             # 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_time = current_time
         self.last_tap = tap
         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
 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):
 class TransformationTracker(GestureTracker):
     """
     """
     Tracker for linear transformations. This implementation detects rotation,
     Tracker for linear transformations. This implementation detects rotation,
     scaling and translation using the centroid of all touch points.
     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
         # All touch points performing the transformation
         self.points = []
         self.points = []
@@ -40,87 +96,33 @@ class TransformationTracker(GestureTracker):
         else:
         else:
             self.centroid = MovingPositionable(x, y)
             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()
         self.update_centroid()
 
 
-    def on_point_move(self, point):
+    def on_point_move(self, event):
+        point = event.point
         l = len(self.points)
         l = len(self.points)
 
 
         if l > 1:
         if l > 1:
             # Rotation (around the previous centroid)
             # 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
             # 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
         # Update centroid before movement can be detected
         self.update_centroid()
         self.update_centroid()
 
 
         # Movement
         # 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()
         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
 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())
         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
 from tests.parse_arguments import create_parser, parse_args
+
 parse_args(create_parser())
 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
 # 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
 # Start listening to TUIO events
 try:
 try:

+ 30 - 22
tests/draw.py

@@ -4,19 +4,17 @@ import pygame
 from threading import Thread
 from threading import Thread
 from math import degrees
 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 src.screen import screen_size
 
 
-from tests.parse_arguments import create_parser, parse_args
+# Parse arguments
 parser = create_parser()
 parser = create_parser()
 parser.add_argument('-f', '--fullscreen', action='store_true', default=False,
 parser.add_argument('-f', '--fullscreen', action='store_true', default=False,
                     help='run in fullscreen')
                     help='run in fullscreen')
 args = parse_args(parser)
 args = parse_args(parser)
 
 
-
 pygame.init()
 pygame.init()
 
 
 # Config
 # Config
@@ -56,6 +54,7 @@ angle = 0
 scale = 1
 scale = 1
 taps = []
 taps = []
 dtaps = []
 dtaps = []
+points = []
 
 
 
 
 def update():
 def update():
@@ -73,14 +72,14 @@ def update():
     screen.blit(transformed, rect)
     screen.blit(transformed, rect)
 
 
     # Draw touch points
     # 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)
         xy = coord(p.x, p.y)
 
 
         # Draw line to centroid
         # Draw line to centroid
-        if transform.centroid:
+        if transform and transform.centroid:
             pygame.draw.line(screen, LINE_COLOR, xy, c, 1)
             pygame.draw.line(screen, LINE_COLOR, xy, c, 1)
 
 
         # Draw outlined circle around touch point
         # Draw outlined circle around touch point
@@ -90,7 +89,7 @@ def update():
         pygame.draw.circle(screen, BG_COLOR, xy, FINGER_RADIUS - 1, 0)
         pygame.draw.circle(screen, BG_COLOR, xy, FINGER_RADIUS - 1, 0)
 
 
     # Draw filled circle around centroid
     # Draw filled circle around centroid
-    if transform.centroid:
+    if transform and transform.centroid:
         pygame.draw.circle(screen, CIRCLE_COLOR, c, CENTROID_RADIUS)
         pygame.draw.circle(screen, CIRCLE_COLOR, c, CENTROID_RADIUS)
 
 
     # Draw an expanding circle around each tap event
     # Draw an expanding circle around each tap event
@@ -125,30 +124,39 @@ def update():
     pygame.display.flip()
     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):
 def rotate(gesture):
     global angle
     global angle
     angle += gesture.get_angle()
     angle += gesture.get_angle()
+    save_tracker(gesture)
 
 
 
 
 def pinch(gesture):
 def pinch(gesture):
     global scale
     global scale
     scale = min(scale * gesture.get_scale(), MAX_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:
 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
 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
 # Start listening to TUIO events
 try:
 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
 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
 # Start listening to TUIO events
 try:
 try:

+ 7 - 12
tests/vtk_interactor.py

@@ -2,25 +2,20 @@ import vtk
 from threading import Thread
 from threading import Thread
 from math import degrees
 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():
 class vtkMultitouchInteractor():
     def __init__(self):
     def __init__(self):
         self.iren = vtk.vtkRenderWindowInteractor()
         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):
     def SetRenderWindow(self, window):
         self.iren.SetRenderWindow(window)
         self.iren.SetRenderWindow(window)