Просмотр исходного кода

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 лет назад
Родитель
Сommit
73e05a5d14
7 измененных файлов с 129 добавлено и 47 удалено
  1. 15 4
      src/event.py
  2. 3 1
      src/event_server.py
  3. 32 15
      src/geometry.py
  4. 11 4
      src/touch_objects.py
  5. 11 5
      src/trackers/tap.py
  6. 52 16
      src/widget.py
  7. 5 2
      src/widgets.py

+ 15 - 4
src/event.py

@@ -1,7 +1,8 @@
+from geometry import Positionable
 from touch_objects import OBJECT_NAMES
 
 
-class Event(object):
+class Event(Positionable):
     """
     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
@@ -12,8 +13,10 @@ class Event(object):
     _type = NotImplemented
 
     def __init__(self, touch_object):
+        super(Event, self).__init__(*touch_object)
         self.touch_object = touch_object
         self.stopped = self.stopped_immidiate = False
+        self.offset = Positionable(0, 0)
 
     def __getattr__(self, name):
         if name in OBJECT_NAMES \
@@ -23,15 +26,23 @@ class Event(object):
         raise AttributeError("'%s' object has no attribute '%s'"
                              % (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):
         return self._type
 
     def get_touch_object(self):
         return self.touch_object
 
-    def get_position(self):
-        return self.touch_object.get_position()
-
     def stop_propagation(self):
         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
         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):
         """

+ 32 - 15
src/geometry.py

@@ -5,15 +5,39 @@ import time
 
 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):
         self.x = x
         self.y = y
 
-    def __str__(self):
+    def __repr__(self):
         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):
         self.x = x
         self.y = y
@@ -21,19 +45,12 @@ class Positionable(object):
     def get_position(self):
         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.
         """
-        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):
@@ -66,8 +83,8 @@ class MovingPositionable(Positionable):
         Calculate rotation of this positionable relative to a center
         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)
         current_angle = atan2(self.x - cx, self.y - cy)
         rotation = current_angle - prev_angle
@@ -85,7 +102,7 @@ class MovingPositionable(Positionable):
         Calculate the movement relative to the last position as a vector
         positionable.
         """
-        px, py = self.prev.get_position()
+        px, py = self.prev
         return Positionable(self.x - px, self.y - py)
 
     def movement_distance(self):

+ 11 - 4
src/touch_objects.py

@@ -1,15 +1,15 @@
 from geometry import AcceleratedPositionable
 
 
-class TouchPoint(AcceleratedPositionable):
+class TouchObject(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()
+        super(TouchObject, self).__init__(x, y)
+        self.object_id = self.create_object_id()
 
     def get_id(self):
         return self.object_id
@@ -22,8 +22,15 @@ class TouchPoint(AcceleratedPositionable):
         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.:
-#class TouchFiducial(TouchPoint): ...
+#class TouchFiducial(TouchObject): ...
 
 
 OBJECT_NAMES = {

+ 11 - 5
src/trackers/tap.py

@@ -34,7 +34,7 @@ class TapTracker(GestureTracker):
     supported_gestures = [TapGesture, SingleTapGesture, DoubleTapGesture]
 
     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):
         super(TapTracker, self).__init__(window)
@@ -57,6 +57,10 @@ class TapTracker(GestureTracker):
         # Times per second to detect single taps
         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.single_tap_thread = Thread(target=self.detect_single_tap)
         self.single_tap_thread.daemon = True
@@ -82,8 +86,7 @@ class TapTracker(GestureTracker):
         self.last_tap = None
 
     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):
         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
         # '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]
 
     def on_point_up(self, event):
@@ -115,6 +118,9 @@ class TapTracker(GestureTracker):
         if current_time - down_time > self.tap_time:
             return
 
+        if not self.propagate_up_event:
+            event.stop_propagation()
+
         tap = TapGesture(event)
         self.trigger(tap)
 

+ 52 - 16
src/widget.py

@@ -1,14 +1,19 @@
+from functools import partial
+
 from geometry import Positionable
 from logger import Logger
 from trackers import create_tracker
+from abc import ABCMeta, abstractmethod
 
 
 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):
         Positionable.__init__(self, x, y)
 
@@ -22,21 +27,41 @@ class Widget(Positionable, Logger):
         self.parent = None
         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 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
 
@@ -59,7 +84,7 @@ class Widget(Positionable, Logger):
         Set a new parent widget. If a parent widget has already been assigned,
         remove the widget from that parent first.
         """
-        if self.parent:
+        if widget and self.parent:
             self.parent.remove_widget(self)
 
         self.parent = widget
@@ -95,13 +120,16 @@ class Widget(Positionable, Logger):
         for gtype in self.trackers[gesture_type].gesture_types:
             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
-        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:
             tracker = create_tracker(gesture_type, self)
+            tracker.configure(**kwargs)
             self.trackers[gesture_type] = tracker
             self.handlers[gesture_type] = []
 
@@ -124,8 +152,9 @@ class Widget(Positionable, Logger):
             raise AttributeError("'%s' has no attribute '%s'"
                                  % (self.__class__.__name__, name))
 
-        return lambda handler: self.bind(name[3:], handler)
+        return partial(self.bind, name[3:])
 
+    @abstractmethod
     def contains_event(self, event):
         """
         Check if the coordinates of an event are contained within this widget.
@@ -140,13 +169,20 @@ class Widget(Positionable, Logger):
         if not self.children:
             self.propagate_event(event)
         else:
+            event.set_offset(self.get_offset())
+            child_found = False
+
             for child in self.children:
                 if child.contains_event(event):
+                    child_found = True
                     child.delegate_event(event)
 
                     if event.is_propagation_stopped():
                         break
 
+            if not child_found:
+                self.propagate_event(event)
+
     def propagate_event(self, event):
         for tracker in set(self.trackers.itervalues()):
             tracker.handle_event(event)

+ 5 - 2
src/widgets.py

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