from functools import partial from geometry import Positionable from logger import Logger from trackers import create_tracker from abc import ABCMeta, abstractmethod class Area(Positionable, Logger): """ Abstract class for area implementations. A area represents a 2D object on the screen in which gestures can occur. Handlers for specific gesture types can be bound to a area. """ __metaclass__ = ABCMeta def __init__(self, x=None, y=None): Positionable.__init__(self, x, y) # List of all active trackers self.trackers = [] # Map of gesture types to a list of handlers for that type self.handlers = {} # Area tree references self.parent = None self.children = [] self.delegate_queue = {} self.update_handlers = [] def get_root_area(self): """ Traverse up in the area tree to find the root area. """ if self.parent: return self.parent.get_root_area() return self def get_screen_offset(self): """ Get the position relative to the screen. """ if not self.parent: return self.get_position() ox, oy = self.parent.get_screen_offset() return ox + self.x, oy + self.y def get_root_offset(self): """ Get the position relative to the root area. """ if not self.parent: return 0, 0 ox, oy = self.parent.get_root_offset() return ox + self.x, oy + self.y def get_offset(self, offset_area): """ Get the position relative to an ancestor area. """ if self == offset_area: return 0, 0 ox, oy = self.parent.get_offset(offset_area) return ox + self.x, oy + self.y def add_area(self, area): """ Add a new child area. """ self.children.append(area) area.set_parent(self) def remove_area(self, area): """ Remove a child area. """ self.children.remove(area) area.set_parent(None) def set_parent(self, area): """ Set a new parent area. If a parent area has already been assigned, remove the area from that parent first. """ if area and self.parent: self.parent.remove_area(self) self.parent = area 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][:] tracker = self.find_tracker(gesture_type) # Check if any other handlers need the tracker for gtype in tracker.gesture_types: if gtype in self.handlers: return # No more handlers are bound, remove unused tracker and handlers self.trackers.remove(gesture_type) for gtype in tracker.gesture_types: del self.handlers[gtype] def find_tracker(self, gesture_type): """ Find a tracker that is tracking some gesture type. """ for tracker in self.trackers: if gesture_type in tracker.gesture_types: return tracker 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. 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.append(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: area.on_gesture(...) instead of: area.bind('gesture', ...) """ if len(name) < 4 or name[:3] != 'on_': raise AttributeError("'%s' has no attribute '%s'" % (self.__class__.__name__, name)) return partial(self.bind, name[3:]) @abstractmethod def contains_event(self, event): """ Check if the coordinates of an event are contained within this area. """ raise NotImplementedError def delegate_event(self, event): """ Delegate a triggered event to all child areas. If a child stops propagation, return so that its siblings and the parent area will not delegate the event to their trackers. """ event.add_offset(*self.get_position()) #print 'delegated in %s: %s' % (self, event) if self.children: # Delegate to children in reverse order because areas that are # added later, should be placed over previously added siblings delegate_to = [c for c in self.children if c.contains_event(event)] if delegate_to: self.delegate_queue[event] = delegate_to self.propagate_event(event) def propagate_event(self, event): """ Delagate an event to all gesture trackers (immediate propagation), then propagate it to the parent area (if any). Propagation can be stopped by a tracker. """ if event in self.delegate_queue: child = self.delegate_queue[event].pop() if not self.delegate_queue[event]: del self.delegate_queue[event] child.delegate_event(event) return self.handle_event(event) if self.parent and not event.is_propagation_stopped(): event.add_offset(-self.x, -self.y) #print 'propagated to %s: %s' % (self.parent, event) self.parent.propagate_event(event) def handle_event(self, event): for tracker in self.trackers: tracker.handle_event(event) if event.is_immediate_propagation_stopped(): return 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. """ for handler in self.handlers.get(gesture.get_type(), ()): handler(gesture) def on_update(self, handler): self.update_handlers.append(handler) def update(self): """ Call update handlers of this area and all children, so that the underlying subtree knows about changes in geometry and can redraw. """ for handler in self.update_handlers: if handler(): return True for child in self.children: if child.update(): return True def set_position(self, x, y): Positionable.set_position(self, x, y) self.update()