Эх сурвалжийг харах

Fully implemented event delegation and propagation:

- Widget positions are now relative to their parent.
- Event positions are relative to the root widget.
- Added functions to calculate relative positions between widgets.
- Events delegation/propagation was buggy and incomplete, not anymore.
Taddeus Kroes 13 жил өмнө
parent
commit
73e05a5d14

+ 15 - 4
src/event.py

@@ -1,7 +1,8 @@
+from geometry import Positionable
 from touch_objects import OBJECT_NAMES
 from touch_objects import OBJECT_NAMES
 
 
 
 
-class Event(object):
+class Event(Positionable):
     """
     """
     Abstract base class for events triggered by an event driver. These events
     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
     are delegated to gesture trackers, to be translated to gestures. To be able
@@ -12,8 +13,10 @@ class Event(object):
     _type = NotImplemented
     _type = NotImplemented
 
 
     def __init__(self, touch_object):
     def __init__(self, touch_object):
+        super(Event, self).__init__(*touch_object)
         self.touch_object = touch_object
         self.touch_object = touch_object
         self.stopped = self.stopped_immidiate = False
         self.stopped = self.stopped_immidiate = False
+        self.offset = Positionable(0, 0)
 
 
     def __getattr__(self, name):
     def __getattr__(self, name):
         if name in OBJECT_NAMES \
         if name in OBJECT_NAMES \
@@ -23,15 +26,23 @@ class Event(object):
         raise AttributeError("'%s' object has no attribute '%s'"
         raise AttributeError("'%s' object has no attribute '%s'"
                              % (self.__class__.__name__, name))
                              % (self.__class__.__name__, name))
 
 
+    def get_offset(self):
+        return self - self.offset
+
+    def set_offset(self, offset):
+        self.offset.set_position(*offset)
+
+    def set_root_widget(self, widget):
+        x, y = widget
+        self.x -= x
+        self.y -= y
+
     def get_type(self):
     def get_type(self):
         return self._type
         return self._type
 
 
     def get_touch_object(self):
     def get_touch_object(self):
         return self.touch_object
         return self.touch_object
 
 
-    def get_position(self):
-        return self.touch_object.get_position()
-
     def stop_propagation(self):
     def stop_propagation(self):
         self.stopped = True
         self.stopped = True
 
 

+ 3 - 1
src/event_server.py

@@ -25,7 +25,9 @@ class EventServer(Logger):
         Delegate an event that has been triggered by the event driver to the
         Delegate an event that has been triggered by the event driver to the
         widget tree.
         widget tree.
         """
         """
-        self.root_widget.delegate_event(event)
+        if self.root_widget.contains_event(event):
+            event.set_root_widget(self.root_widget)
+            self.root_widget.delegate_event(event)
 
 
     def start(self):
     def start(self):
         """
         """

+ 32 - 15
src/geometry.py

@@ -5,15 +5,39 @@ import time
 
 
 class Positionable(object):
 class Positionable(object):
     """
     """
-    Parent class for any object with a position.
+    Parent class for any object with a position. Defines operators {+, -, *, /,
+    **} for its position with another positionable or (x, y) iterable.
     """
     """
     def __init__(self, x=None, y=None):
     def __init__(self, x=None, y=None):
         self.x = x
         self.x = x
         self.y = y
         self.y = y
 
 
-    def __str__(self):
+    def __repr__(self):
         return '<%s at (%s, %s)>' % (self.__class__.__name__, self.x, self.y)
         return '<%s at (%s, %s)>' % (self.__class__.__name__, self.x, self.y)
 
 
+    def __str__(self):
+        return repr(self)
+
+    def __iter__(self):
+        return iter((self.x, self.y))
+
+    def __add__(self, other):
+        ox, oy = other
+        return Positionable(self.x + ox, self.y + oy)
+
+    def __sub__(self, other):
+        ox, oy = other
+        return Positionable(self.x - ox, self.y - oy)
+
+    def __mul__(self, amt):
+        return Positionable(self.x * amt, self.y * amt)
+
+    def __div__(self, amt):
+        return Positionable(self.x / amt, self.y / amt)
+
+    def __pow__(self, exp):
+        return Positionable(self.x ** exp, self.y ** exp)
+
     def set_position(self, x, y):
     def set_position(self, x, y):
         self.x = x
         self.x = x
         self.y = y
         self.y = y
@@ -21,19 +45,12 @@ class Positionable(object):
     def get_position(self):
     def get_position(self):
         return self.x, self.y
         return self.x, self.y
 
 
-    @property
-    def xy(self):
-        """
-        Shortcut getter for (x, y) position.
-        """
-        return self.x, self.y
-
-    def distance_to(self, positionable):
+    def distance_to(self, other):
         """
         """
         Calculate the Pythagorian distance from this positionable to another.
         Calculate the Pythagorian distance from this positionable to another.
         """
         """
-        x, y = positionable.get_position()
-        return ((x - self.x) ** 2 + (y - self.y) ** 2) ** .5
+        ox, oy = other
+        return ((ox - self.x) ** 2 + (oy - self.y) ** 2) ** .5
 
 
 
 
 class MovingPositionable(Positionable):
 class MovingPositionable(Positionable):
@@ -66,8 +83,8 @@ class MovingPositionable(Positionable):
         Calculate rotation of this positionable relative to a center
         Calculate rotation of this positionable relative to a center
         positionable.
         positionable.
         """
         """
-        cx, cy = center.get_position()
-        px, py = self.prev.get_position()
+        cx, cy = center
+        px, py = self.prev
         prev_angle = atan2(px - cx, py - cy)
         prev_angle = atan2(px - cx, py - cy)
         current_angle = atan2(self.x - cx, self.y - cy)
         current_angle = atan2(self.x - cx, self.y - cy)
         rotation = current_angle - prev_angle
         rotation = current_angle - prev_angle
@@ -85,7 +102,7 @@ class MovingPositionable(Positionable):
         Calculate the movement relative to the last position as a vector
         Calculate the movement relative to the last position as a vector
         positionable.
         positionable.
         """
         """
-        px, py = self.prev.get_position()
+        px, py = self.prev
         return Positionable(self.x - px, self.y - py)
         return Positionable(self.x - px, self.y - py)
 
 
     def movement_distance(self):
     def movement_distance(self):

+ 11 - 4
src/touch_objects.py

@@ -1,15 +1,15 @@
 from geometry import AcceleratedPositionable
 from geometry import AcceleratedPositionable
 
 
 
 
-class TouchPoint(AcceleratedPositionable):
+class TouchObject(AcceleratedPositionable):
     """
     """
     Representation of an object touching the screen. The simplest form of a
     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
     touch object is a 'point', represented by an (x, y) position and a unique
     object id. All dimensions are in pixels.
     object id. All dimensions are in pixels.
     """
     """
     def __init__(self, x, y):
     def __init__(self, x, y):
-        super(TouchPoint, self).__init__(x, y)
-        self.object_id = self.__class__.create_object_id()
+        super(TouchObject, self).__init__(x, y)
+        self.object_id = self.create_object_id()
 
 
     def get_id(self):
     def get_id(self):
         return self.object_id
         return self.object_id
@@ -22,8 +22,15 @@ class TouchPoint(AcceleratedPositionable):
         return cls.object_id_count
         return cls.object_id_count
 
 
 
 
+class TouchPoint(TouchObject):
+    """
+    Simple point touchin the scree, consisting only of an (x, y) position.
+    """
+    pass
+
+
 # TODO: Extend with more complex touch object, e.g.:
 # TODO: Extend with more complex touch object, e.g.:
-#class TouchFiducial(TouchPoint): ...
+#class TouchFiducial(TouchObject): ...
 
 
 
 
 OBJECT_NAMES = {
 OBJECT_NAMES = {

+ 11 - 5
src/trackers/tap.py

@@ -34,7 +34,7 @@ class TapTracker(GestureTracker):
     supported_gestures = [TapGesture, SingleTapGesture, DoubleTapGesture]
     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', 'propagate_up_event']
 
 
     def __init__(self, window=None):
     def __init__(self, window=None):
         super(TapTracker, self).__init__(window)
         super(TapTracker, self).__init__(window)
@@ -57,6 +57,10 @@ class TapTracker(GestureTracker):
         # Times per second to detect single taps
         # Times per second to detect single taps
         self.update_rate = 30
         self.update_rate = 30
 
 
+        # Whether to stop propagation of the 'point_up' event to parent widgets
+        # If False, this reserves tap events to child widgets
+        self.propagate_up_event = True
+
         self.reset_last_tap()
         self.reset_last_tap()
         self.single_tap_thread = Thread(target=self.detect_single_tap)
         self.single_tap_thread = Thread(target=self.detect_single_tap)
         self.single_tap_thread.daemon = True
         self.single_tap_thread.daemon = True
@@ -82,8 +86,7 @@ class TapTracker(GestureTracker):
         self.last_tap = None
         self.last_tap = None
 
 
     def on_point_down(self, event):
     def on_point_down(self, event):
-        x, y = event.get_position()
-        self.reg[event.point.get_id()] = time.time(), Positionable(x, y)
+        self.reg[event.point.get_id()] = time.time(), event
 
 
     def on_point_move(self, event):
     def on_point_move(self, event):
         oid = event.point.get_id()
         oid = event.point.get_id()
@@ -93,9 +96,9 @@ class TapTracker(GestureTracker):
 
 
         # 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[oid]
+        t, down_event = self.reg[oid]
 
 
-        if event.point.distance_to(initial_position) > self.tap_distance:
+        if event.distance_to(down_event) > self.tap_distance:
             del self.reg[oid]
             del self.reg[oid]
 
 
     def on_point_up(self, event):
     def on_point_up(self, event):
@@ -115,6 +118,9 @@ class TapTracker(GestureTracker):
         if current_time - down_time > self.tap_time:
         if current_time - down_time > self.tap_time:
             return
             return
 
 
+        if not self.propagate_up_event:
+            event.stop_propagation()
+
         tap = TapGesture(event)
         tap = TapGesture(event)
         self.trigger(tap)
         self.trigger(tap)
 
 

+ 52 - 16
src/widget.py

@@ -1,14 +1,19 @@
+from functools import partial
+
 from geometry import Positionable
 from geometry import Positionable
 from logger import Logger
 from logger import Logger
 from trackers import create_tracker
 from trackers import create_tracker
+from abc import ABCMeta, abstractmethod
 
 
 
 
 class Widget(Positionable, Logger):
 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
+    Abstract class for widget implementations. 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.
     """
     """
+    __metaclass__ = ABCMeta
+
     def __init__(self, x=None, y=None):
     def __init__(self, x=None, y=None):
         Positionable.__init__(self, x, y)
         Positionable.__init__(self, x, y)
 
 
@@ -22,21 +27,41 @@ class Widget(Positionable, Logger):
         self.parent = None
         self.parent = None
         self.children = []
         self.children = []
 
 
-    def get_offset(self, offset_parent=None):
+    def get_root_widget(self):
+        """
+        Traverse up in the widget tree to find the root widget.
+        """
+        if self.parent:
+            return self.parent.get_root_widget()
+
+        return self
+
+    def get_screen_offset(self):
         """
         """
-        Get the offset position relative to an offset parent. If no offset
-        parent is specified, the parent widget is used. If no parent widget is
-        assigned, return absolute coordinates.
+        Get the position relative to the screen.
         """
         """
-        x, y = self.get_position()
+        root = self.get_root_widget()
+        return root + self.get_offset(root)
 
 
+    def get_offset(self, offset_parent=None):
+        """
+        Get the position relative to an offset parent. If no offset parent is
+        specified, the position relative to the root widget is returned. The
+        position of the root widget itself is (0, 0).
+        """
         if not offset_parent:
         if not offset_parent:
-            if not self.parent:
-                return x, y
+            offset_parent = self.get_root_widget()
 
 
-            offset_parent = self.parent
+        if not self.parent:
+            if offset_parent is self:
+                return 0, 0
 
 
-        ox, oy = offset_parent.get_position()
+            ox, oy = offset_paret
+            x = y = 0
+        else:
+            ox, oy = offset_parent
+            x = self.x
+            y = self.y
 
 
         return x - ox, y - oy
         return x - ox, y - oy
 
 
@@ -59,7 +84,7 @@ class Widget(Positionable, Logger):
         Set a new parent widget. If a parent widget has already been assigned,
         Set a new parent widget. If a parent widget has already been assigned,
         remove the widget from that parent first.
         remove the widget from that parent first.
         """
         """
-        if self.parent:
+        if widget and self.parent:
             self.parent.remove_widget(self)
             self.parent.remove_widget(self)
 
 
         self.parent = widget
         self.parent = widget
@@ -95,13 +120,16 @@ class Widget(Positionable, Logger):
         for gtype in self.trackers[gesture_type].gesture_types:
         for gtype in self.trackers[gesture_type].gesture_types:
             del self.handlers[gtype]
             del self.handlers[gtype]
 
 
-    def bind(self, gesture_type, handler, remove_existing=False):
+    def bind(self, gesture_type, handler, remove_existing=False, **kwargs):
         """
         """
         Bind a handler to the specified type of gesture. Create a tracker for
         Bind a handler to the specified type of gesture. Create a tracker for
-        the gesture type if it does not exists yet.
+        the gesture type if it does not exists yet. Ik a new tracker is
+        created, configure is with any keyword arguments that have been
+        specified.
         """
         """
         if gesture_type not in self.handlers:
         if gesture_type not in self.handlers:
             tracker = create_tracker(gesture_type, self)
             tracker = create_tracker(gesture_type, self)
+            tracker.configure(**kwargs)
             self.trackers[gesture_type] = tracker
             self.trackers[gesture_type] = tracker
             self.handlers[gesture_type] = []
             self.handlers[gesture_type] = []
 
 
@@ -124,8 +152,9 @@ class Widget(Positionable, Logger):
             raise AttributeError("'%s' has no attribute '%s'"
             raise AttributeError("'%s' has no attribute '%s'"
                                  % (self.__class__.__name__, name))
                                  % (self.__class__.__name__, name))
 
 
-        return lambda handler: self.bind(name[3:], handler)
+        return partial(self.bind, name[3:])
 
 
+    @abstractmethod
     def contains_event(self, event):
     def contains_event(self, event):
         """
         """
         Check if the coordinates of an event are contained within this widget.
         Check if the coordinates of an event are contained within this widget.
@@ -140,13 +169,20 @@ class Widget(Positionable, Logger):
         if not self.children:
         if not self.children:
             self.propagate_event(event)
             self.propagate_event(event)
         else:
         else:
+            event.set_offset(self.get_offset())
+            child_found = False
+
             for child in self.children:
             for child in self.children:
                 if child.contains_event(event):
                 if child.contains_event(event):
+                    child_found = True
                     child.delegate_event(event)
                     child.delegate_event(event)
 
 
                     if event.is_propagation_stopped():
                     if event.is_propagation_stopped():
                         break
                         break
 
 
+            if not child_found:
+                self.propagate_event(event)
+
     def propagate_event(self, event):
     def propagate_event(self, event):
         for tracker in set(self.trackers.itervalues()):
         for tracker in set(self.trackers.itervalues()):
             tracker.handle_event(event)
             tracker.handle_event(event)

+ 5 - 2
src/widgets.py

@@ -2,6 +2,9 @@ from widget import Widget
 from screen import screen_size
 from screen import screen_size
 
 
 
 
+__all__ = ['RectangularWidget', 'CircularWidget', 'FullscreenWidget']
+
+
 class RectangularWidget(Widget):
 class RectangularWidget(Widget):
     """
     """
     Rectangular widget, has a position and a size.
     Rectangular widget, has a position and a size.
@@ -23,7 +26,7 @@ class RectangularWidget(Widget):
         return self.width, self.height
         return self.width, self.height
 
 
     def contains_event(self, event):
     def contains_event(self, event):
-        x, y = event.get_position()
+        x, y = event.get_offset()
         return self.x <= x <= self.x + self.width \
         return self.x <= x <= self.x + self.width \
                and self.y <= y <= self.y + self.height
                and self.y <= y <= self.y + self.height
 
 
@@ -48,7 +51,7 @@ class CircularWidget(Widget):
         return self.radius
         return self.radius
 
 
     def contains_event(self, event):
     def contains_event(self, event):
-        return event.get_touch_object().distance_to(self) <= self.radius
+        return event.distance_to(self) <= self.radius
 
 
 
 
 class FullscreenWidget(RectangularWidget):
 class FullscreenWidget(RectangularWidget):