Commit 73e05a5d authored by Taddeüs Kroes's avatar Taddeüs Kroes

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.
parent 73a026f9
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): ...@@ -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): ...@@ -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
......
...@@ -25,6 +25,8 @@ class EventServer(Logger): ...@@ -25,6 +25,8 @@ 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.
""" """
if self.root_widget.contains_event(event):
event.set_root_widget(self.root_widget)
self.root_widget.delegate_event(event) self.root_widget.delegate_event(event)
def start(self): def start(self):
......
...@@ -5,15 +5,39 @@ import time ...@@ -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): ...@@ -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 distance_to(self, other):
def xy(self):
"""
Shortcut getter for (x, y) position.
"""
return self.x, self.y
def distance_to(self, positionable):
""" """
Calculate the Pythagorian distance from this positionable to another. Calculate the Pythagorian distance from this positionable to another.
""" """
x, y = positionable.get_position() ox, oy = other
return ((x - self.x) ** 2 + (y - self.y) ** 2) ** .5 return ((ox - self.x) ** 2 + (oy - self.y) ** 2) ** .5
class MovingPositionable(Positionable): class MovingPositionable(Positionable):
...@@ -66,8 +83,8 @@ 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() cx, cy = center
px, py = self.prev.get_position() 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): ...@@ -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):
......
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) super(TouchObject, self).__init__(x, y)
self.object_id = self.__class__.create_object_id() 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): ...@@ -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 = {
......
...@@ -34,7 +34,7 @@ class TapTracker(GestureTracker): ...@@ -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): ...@@ -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): ...@@ -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(), event
self.reg[event.point.get_id()] = time.time(), Positionable(x, y)
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): ...@@ -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): ...@@ -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)
......
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. Abstract class for widget implementations. A widget represents a 2D object
Handlers for a specific gesture type can be bound to a widget. The widget on the screen in which gestures can occur. Handlers for a specific gesture
will 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): ...@@ -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):
""" """
Get the offset position relative to an offset parent. If no offset Traverse up in the widget tree to find the root widget.
parent is specified, the parent widget is used. If no parent widget is
assigned, return absolute coordinates.
""" """
x, y = self.get_position() if self.parent:
return self.parent.get_root_widget()
return self
def get_screen_offset(self):
"""
Get the position relative to the screen.
"""
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: offset_parent = self.get_root_widget()
return x, y
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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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)
......
...@@ -2,6 +2,9 @@ from widget import Widget ...@@ -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): ...@@ -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): ...@@ -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):
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment