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
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
......
......@@ -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):
"""
......
......@@ -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):
......
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 = {
......
......@@ -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)
......
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)
......
......@@ -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):
......
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