tuikit/events.py
author Radek Brich <radek.brich@devl.cz>
Fri, 18 Jan 2013 22:36:50 +0100
changeset 62 2f61931520c9
parent 55 1ab0edd5d784
child 63 2a0e04091898
permissions -rw-r--r--
Rework layouts: Layout is now normal Container which places its children upon resize event. Drop TopWindow, top is now any subclass of Container. Add floater concept: floaters are widgets drawn over normal widgets, not clipped by parent. Add HScrollbar and Scrollbar abstract base class.

# -*- coding: utf-8 -*-
"""Event emitter.

There are two sorts of events - those which are emitted by user or system
and others which are emitted by Widgets. We will not distinguish them.

This module implements universal mechanism for event emitting and handling.

"""

import logging
import inspect


class Event:
    def __init__(self):
        self.event_name = None
        self.originator = None

    def __getitem__(self, key):
        return self.__dict__[key]

    def __repr__(self):
        return 'Event()'


class DrawEvent(Event):
    def __init__(self, driver, x, y):
        Event.__init__(self)
        self.driver = driver
        self.x = x
        self.y = y

    def __repr__(self):
        return 'DrawEvent(x={0.x},y={0.y})'.format(self)


class FocusEvent(Event):
    def __init__(self, old=None, new=None):
        Event.__init__(self)
        #: Former focused widget.
        self.old = old
        #: Current focused widget.
        self.new = new

    def __repr__(self):
        return 'FocusEvent(old={0.old},new={0.new})'.format(self)


class KeyboardEvent(Event):
    def __init__(self, keyname, char):
        Event.__init__(self)
        self.keyname = keyname
        self.char = char

    def __repr__(self):
        return 'KeyboardEvent(keyname={0.keyname},char={0.char})'.format(self)


class MouseEvent(Event):
    def __init__(self, x=0, y=0, button=0):
        Event.__init__(self)
        self.x = x   # global coordinates
        self.y = y
        self.wx = x  # local widget coordinates
        self.wy = y
        self.px = 0  # parent coordinates
        self.py = 0
        #: Mouse button: left=1, middle=2, right=3, wheelup=4 wheeldown=5
        self.button = button

    def make_child_event(self, container, child):
        ev = MouseEvent(self.x, self.y, self.button)
        # original local coordinates are new parent coordinates
        ev.px = self.wx
        ev.py = self.wy
        # update local coordinates
        ev.wx = self.wx - child.x
        ev.wy = self.wy - child.y
        return ev

    def __repr__(self):
        return 'MouseEvent(x={0.x},y={0.y},button={0.button})'.format(self)


class GenericEvent(Event):
    """Generic event which carries an object with additional data."""
    def __init__(self, data):
        self.data = data


class Emitter:

    """Event emitter mixin class."""

    def add_events(self, *events):
        """Add events which may be emitted on instances of this class.

        This should be called only by subclasses.
        It also serves as initializer, other methods of Emitter
        will not work if add_events was not called.

        events -- Arguments must be given in pairs.

        Each pair consists of event_name, event_class:
            event_name -- a string used in add_handler(), emit() etc.
            event_class -- class of event payload

        It will also inspect base classes of instance on which add_events()
        is called, adding all methods meeting naming convention to handler list.
        The convention for method names is "on_event_name", "after_event_name".
        Prefix on/after determines whether the handler is added as first or last.

        """
        if len(events) % 2:
            raise ValueError('add_events(): Event names and classes must be passed in pairs.')
        if not hasattr(self, '_event_handlers'):
            self._event_handlers = dict()
            self._event_class = dict()
        for event_name, event_class in zip(events[::2], events[1::2]):
            if not isinstance(event_name, str):
                raise TypeError('add_events(): Event name must be a string: %r.' % event_name)
            if not issubclass(event_class, Event):
                raise TypeError('add_events(): Class %s does not inherit from Event.' % event_class)
            self._event_handlers[event_name] = []
            self._event_class[event_name] = event_class
            self._add_default_handlers(event_name)

    def add_handler(self, event_name, handler, last=False):
        """Add handler to event name.

        last=False -- Add handler as first in handler list.
        last=True -- Add handler to the end of handler list.

        """
        if event_name in self._event_handlers:
            if last:
                self._event_handlers[event_name].append(handler)
            else:
                self._event_handlers[event_name].insert(0, handler)
        else:
            raise KeyError('Unknown event: %s', event_name)

    def remove_handler(self, event_name, handler):
        """Remove event handler from the list."""
        if event_name in self._event_handlers:
            self._event_handlers[event_name].remove(handler)
        else:
            raise KeyError('Unknown event: %s', event_name)

    def remove_all_handlers(self, event_name):
        """Remove all handlers for event."""
        if event_name in self._event_handlers:
            self._event_handlers[event_name][:] = []
        else:
            raise KeyError('Unknown event: %s', event_name)

    def has_handlers(self, event_name):
        """Test if any handlers are attached to event name.

        Return True if event handler list is not empty,
        False otherwise.

        """
        if event_name in self._event_handlers:
            return bool(self._event_handlers[event_name])
        else:
            raise KeyError('Unknown event: %s', event_name)

    def emit(self, event_name, *args, **kwargs):
        """Emit the event.

        Call all handlers from event's handler list,
        starting from last added handler, going to those added
        before, ending with those added with last=True.

        Stop if any handler returns True.

        Return True when one of the handlers returns True,
        False otherwise.

        Creates new instance of event_class given to
        add_events() and passes all arguments after event_name
        to its __init__ method.

        Unless first of these arguments is Event instance
        in which case no object is created and the Event instance
        is passed to handlers.

        """
        # create event from specified event class, or use first argument
        if len(args) == 1 and isinstance(args[0], Event):
            event = args[0]
        else:
            event = self._event_class[event_name](*args, **kwargs)
        # set originator to instance on which emit() was called
        event.originator = self
        event.event_name = event_name
        # log
        logging.getLogger('tuikit').debug('Emit %r %r on %s %s',
            event_name, event,
            self.__class__.__name__,
            getattr(self, 'name', None) or id(self))
        # call handlers from first to last, stop if satisfied
        for handler in self._event_handlers[event_name]:
            handled = handler(event)
            if handled:
                return True
        return False

    def _add_default_handlers(self, event_name):
        """Add default handlers from the instance and its base classes.

        Handlers are looked up in methods by their names.
        Method name is composited from prefix and event_name.
        Prefix is one of "on_" or "after_".
        Depending on prefix the handler is added to top or bottom of handler stack.

        See _add_default_handler method for more information.

        """
        for cls in reversed(inspect.getmro(self.__class__)):
            self._add_default_handler(cls, 'on_', event_name, last=False)
            self._add_default_handler(cls, 'after_', event_name, last=True)

    def _add_default_handler(self, cls, prefix, event_name, last):
        """Add handler from one of base classes to handler list.

        cls -- one of self's base classes
        prefix -- method name prefix
        event_name -- event name
        last -- if True, add handler to end of handler list (will be called last)

        Look for method of name prefix + event_name in class 'cls',
        if exists, add it to handler list.

        """
        method_name = prefix + event_name
        if method_name in cls.__dict__:
            unbound_handler = cls.__dict__[method_name]
            if callable(unbound_handler):
                handler = unbound_handler.__get__(self, cls)
                self.add_handler(event_name, handler, last)