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): """ 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) # List of all active trackers self.trackers = [] # Map of gesture types to a list of handlers for that type self.handlers = {} # Widget tree references self.parent = None self.children = [] 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 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: offset_parent = self.get_root_widget() if not self.parent: if offset_parent is self: return 0, 0 ox, oy = offset_paret x = y = 0 else: ox, oy = offset_parent x = self.x y = self.y return x - ox, y - oy def add_widget(self, widget): """ Add a new child widget. """ self.children.append(widget) widget.set_parent(self) def remove_widget(self, widget): """ Remove a child widget. """ self.children.remove(widget) widget.set_parent(None) def set_parent(self, widget): """ Set a new parent widget. If a parent widget has already been assigned, remove the widget from that parent first. """ if widget and self.parent: self.parent.remove_widget(self) self.parent = widget 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: widget.on_gesture(...) instead of: widget.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 widget. """ raise NotImplementedError def delegate_event(self, event): """ Delegate a triggered event to all child widgets. If a child stops propagation, return so that its siblings and the parent widget will not delegate the event to their trackers. """ child_found = False if self.children: event.set_offset(self.get_offset()) # Delegate to children in reverse order because widgets that are # added later, should be placed over previously added siblings for child in reversed(self.children): if child.contains_event(event): child_found = True child.delegate_event(event) if event.is_propagation_stopped(): return if not child_found: self.propagate_event(event) def propagate_event(self, event): for tracker in self.trackers: tracker.handle_event(event) if event.is_immediate_propagation_stopped(): break if self.parent and not event.is_propagation_stopped(): self.parent.propagate_event(event) 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. Second, if the gesture's propagation has not been stopped by a handler, the gesture is propagated to the parent widget. """ for handler in self.handlers.get(gesture.get_type(), ()): handler(gesture) #if self.parent and not gesture.is_propagation_stopped(): # self.parent.handle_gesture(gesture)