Source code for ui.base_ui

import os
from time import sleep
from functools import wraps
from threading import Event
from traceback import print_exc
from inspect import getframeinfo, stack

import PIL

from helpers import setup_logger
from ui.utils import to_be_foreground

logger = setup_logger(__name__, "info")

def internal_callback_in_background(method):
    """
      A decorator for UI elements' internal methods that need to bring the UI element
      into the background during execution.
    """
    @wraps(method)
    def wrapper(self, *args, **kwargs):
        self.to_background()
        method(self, *args, **kwargs)
        logger.debug("{}: executed wrapped function: {}".format(self.name, method.__name__))
        if self.in_background:
            self.to_foreground()
    return wrapper


class BaseUIElement(object):

    def __init__(self, i, o, name=None, override_left=True, input_necessary=True):
        """
        Sets the most basic variables and checks whether the input object
        is supplied in case it's necessary. To be called by a child object.
        """
        self.i = i
        self.o = o
        if name is not None:
            self.name = name
        else:
            self.generate_name_if_not_supplied()
        self.overlays = []
        self._in_foreground = Event()
        self._in_background = Event()
        self._input_necessary = input_necessary
        self._override_left = override_left
        if not i and input_necessary:
            raise ValueError("UI element {} needs an input object supplied!".format(self.name))
        self.check_properties()
        self.set_default_keymap()

    def check_properties(self):
        properties = ["in_foreground", "in_background", "is_active"]
        for p in properties:
            assert isinstance(getattr(self, p), bool), "{} needs to be a property".format(p)

    @property
    def in_foreground(self):
        return self._in_foreground.isSet()

    @in_foreground.setter
    def in_foreground(self, value):
        self._in_foreground.set() if value else self._in_foreground.clear()

    @property
    def in_background(self):
        return self._in_background.isSet()

    @in_background.setter
    def in_background(self, value):
        self._in_background.set() if value else self._in_background.clear()

    def to_foreground(self):
        """
        A method that is called once in ``activate`` to set all the variables,
        activates the input device and draw the first image on the display.
        """
        logger.debug("{} in foreground".format(self.name))
        self.before_foreground()
        self.in_background = True
        self.in_foreground = True
        self.activate_input()
        self.refresh()

    def to_background(self):
        """ Signals ``activate`` to finish executing. """
        self.in_foreground = False
        logger.debug("{} in background".format(self.name))

    def before_foreground(self):
        """Hook for child UI elements. Is called each time to_foreground is called."""
        pass

    def before_activate(self):
        """
        Hook for child UI elements, is called each time activate() is called.
        Is the perfect place to clear any flags that you don't want to persist
        between multiple activations of a single instance of an UI element.
        """
        pass

    def after_activate(self):
        """
        Hook for child UI elements, is called each time before activate() returns -
        before ``get_return_value`` is called. Is the perfect place to, say, remove
        input streaming callbacks.
        """
        pass

    def activate(self):
        """
        A method which is called when the UI element needs to start operating.
        Is blocking, sets up input&output devices, refreshes the UI element,
        then calls the ``idle_loop`` method while the UI element is active.
        ``self.in_foreground`` is True, while callbacks are executed from the
        input device thread.
        """
        self.before_activate()
        logger.debug("{} activated".format(self.name))
        self.to_foreground()
        while self.is_active:
            self.idle_loop()
        self.after_activate()
        return_value = self.get_return_value()
        logger.debug("{} exited".format(self.name))
        return return_value

    def get_return_value(self):
        """
        Can be overridden by child UI elements. Return value will be returned when
        UI element's ``activate()`` exits.
        """
        return None  # Default value to indicate that no meaningful result was returned

    @property
    def is_active(self):
        """
        A condition to be checked by ``activate`` to see when the UI element
        is active. Can be overridden by child elements if necessary.
        By default returns ``self.in_background``, so if you only have a
        single UI element without external callback processing, it might make
        sense to check ``in_foreground`` instead.
        """
        return self.in_background

    def idle_loop(self):
        """
        A method that will be called all the time while the UI element is in background
        (regardless of whether it's in foreground or not).
        To be implemented by a child object.
        """
        raise NotImplementedError

    def deactivate(self):
        """ Deactivates the UI element, exiting it. """
        self.in_foreground = False
        self.in_background = False
        logger.debug("{} deactivated".format(self.name))

    def print_name(self):
        """ A debug method. Useful for hooking up to an input event so that
        you can see which UI element is currently active. """
        logger.debug("{} is currently active".format(self.name))

    def print_keymap(self):
        """ A debug method. Useful for hooking up to an input event so that
        you can see what's the keymap of a currently active UI element. """
        logger.debug("{} has keymap: {}".format(self.name, self.keymap))

    def process_callback(self, func):
        """
        Decorates a function so that during its execution the UI element stops
        being in foreground. Is typically used as a wrapper for a callback from
        input event processing thread. After callback's execution is finished,
        sets the keymap again and refreshes the UI element.
        """
        @wraps(func)
        def wrapper(*args, **kwargs):
            self.to_background()
            e = None
            try:
                func(*args, **kwargs)
            except Exception as e:
                print_exc()
            logger.debug("{}: executed wrapped function: {}".format(self.name, func.__name__))
            if self.in_background:
                self.to_foreground()
            if e:
                raise e
        return wrapper

    def process_keymap(self, keymap):
        """
        Processes the keymap, wrapping all callbacks using the ``process_callback`` method.
        If a string is supplied instead of a callable, it looks it up from methods -
        if a method is not found, raises ``ValueError``.
        Also, sets KEY_LEFT to ``deactivate`` unless ``self.override_left``
        is set to ``False`` (override with caution).
        """
        logger.debug("{}: processing keymap - {}".format(self.name, keymap))
        for key in keymap:
            callback_or_name = keymap[key]
            if isinstance(callback_or_name, basestring):
                if hasattr(self, callback_or_name):
                    callback = getattr(self, callback_or_name)
                    keymap[key] = callback
                else:
                    raise ValueError("{}: {} not an attribute/method or callable".format(self.name, callback_or_name))
            elif callable(callback_or_name):
                callback = self.process_callback(keymap[key])
                keymap[key] = callback
            else:
                raise ValueError("{}: {} is not a callable".format(self.name, callback_or_name))
        if self._override_left:
            if not "KEY_LEFT" in keymap:
                keymap["KEY_LEFT"] = self.deactivate
        return keymap

    def set_keymap(self, keymap):
        """
        Receives and processes UI element's keymap (filtered using ``process_keymap``
        before setting).
        """
        self.keymap = self.process_keymap(keymap)

    def set_default_keymap(self):
        """
        Sets the default keymap, getting it straight from the ``generate_keymap``.
        """
        self.set_keymap(self.generate_keymap())

    def generate_keymap(self):
        """
        Returns the default keymap for the UI element.
        To be implemented by a child object.
        """
        raise NotImplementedError

    def update_keymap(self, new_keymap):
        """
        Updates the UI element's keymap with a new one (filtered using
        ``process_keymap`` before updating).
        """
        self._override_left = False
        processed_keymap = self.process_keymap(new_keymap)
        self._override_left = True
        self.keymap.update(processed_keymap)

    def configure_input(self):
        """
        Configures the input device - you can set your keymap, streaming callbacks
        or both - abstracting away ``stop_listen`` and ``listen``. Can be overridden
        by child elements.
        """
        self.i.clear_keymap()
        self.i.set_keymap(self.keymap)

    @to_be_foreground
    def activate_input(self):
        """
        Sets up the input device if one was passed to the UI element - calling
        ``i.stop_listen``, ``self.configure_input`` and ``i.listen``. If an input
        element wasn't passed, checks if one is expected.
        """
        if self.i:
            self.i.stop_listen()
            self.configure_input()
            self.i.listen()
        else:
            if self._input_necessary:
                raise ValueError("UI element {} needs an input object supplied!")
            else:
                logger.warning("{}: no input device object supplied, not setting the keymap".format(self.name))

    @to_be_foreground
    def refresh(self):
        """
        Is called when the screen contents have to be updated.
        To be implemented by a child object.
        """
        raise NotImplementedError

    def generate_name_if_not_supplied(self):
        """ Generating a random yet descriptive UI element name if one was not supplied.
        The name hasa to be random enough so that overlays can be applied properly.
        The name generated will include the directory where the app is called from."""
        cwd = os.getcwd()
        st = stack()
        last_filename = None
        filename = None
        lineno = None
        for i, frame in enumerate(st):
            caller = getframeinfo(frame[0])
            filename = caller.filename
            lineno = caller.lineno
            if filename.startswith(cwd):
                filename = filename[len(cwd):].lstrip("/")
            if filename.startswith("apps/"):
                # First frame that starts with 'apps' - likely is a frame from the app file
                break
            elif not filename.startswith('ui') and last_filename and last_filename.startswith('ui'):
                # First frame that does not start with 'ui' - likely is a useful frame
                break
            last_filename = filename
        else:
            # No suitable frame found? 0_0
            logger.warning("No suitable frame found for UI element {} at {} while generating a name, overlays might not apply correctly!".format(self.__class__.__name__, id(self)))
        self.name = "{} from {}:{}".format(self.__class__.__name__, filename, lineno)
        logger.warning("No name supplied for an UI element {} at {}, generated a new name: {}".format(self.__class__.__name__, id(self), repr(self.name)))