from time import sleep
from functools import wraps
from traceback import print_exc
import PIL
from helpers import setup_logger
from number_input import IntegerAdjustInput
from utils import to_be_foreground
from base_ui import BaseUIElement, internal_callback_in_background
logger = setup_logger(__name__, "info")
class RefresherExitException(Exception):
pass
[docs]class Refresher(BaseUIElement):
"""
A Refresher allows you to update the screen on a regular interval.
All you need is to provide a function that'll return the text/image you want to display;
that function will then be called with the desired frequency and the display
will be updated with whatever it returns.
"""
[docs] def __init__(self, refresh_function, i, o, refresh_interval=1, keymap=None, name="Refresher", **kwargs):
"""Initialises the Refresher object.
Args:
* ``refresh_function``: a function which returns data to be displayed on the screen upon being called, in the format accepted by ``screen.display_data()`` or ``screen.display_image()``. To be exact, supported return values are:
* Tuples and lists - are converted to lists and passed to ``display_data()``
* Strings - are converted to a single-element list and passed to ``display_data()``
* `PIL.Image` objects - are passed to ``display_image()``
* ``i``, ``o``: input&output device objects
Kwargs:
* ``refresh_interval``: Time between display refreshes (and, accordingly, ``refresh_function`` calls).
* ``keymap``: Keymap entries you want to set while Refresher is active.
* By default, KEY_LEFT deactivates the Refresher, if you want to override it, make sure that user can still exit the Refresher.
* ``name``: Refresher name which can be used internally and for debugging.
"""
self.custom_keymap = keymap if keymap else {}
BaseUIElement.__init__(self, i, o, name, input_necessary=False, **kwargs)
self.set_refresh_interval(refresh_interval)
self.set_refresh_function(refresh_function)
self.calculate_intervals()
@property
def is_active(self):
return self.in_background
def pause(self):
"""
Pauses the refresher, not allowing it to print anything on the screen
while it's paused.
"""
self.in_foreground = False
def resume(self):
"""
Resumes the refresher after it's been paused, allowing it to continue
printing things on the screen. Refreshes the screen when it's called.
"""
if not self.in_foreground:
self.to_foreground()
def background_if_inactive(self):
"""
If the UI element hasn't been launched yet, launches it in background
and waits until it's fully running. Otherwise, resumes the UI element.
"""
if not self.is_active:
self.run_in_background()
self.wait_for_active()
else:
self.resume()
def wait_for_active(self, timeout=100):
"""
If the UI element hasn't been launched yet, launches it in background
and waits until it's fully running.
"""
counter = 0
while not self.is_active:
sleep(0.1)
counter += 1
if counter == timeout:
raise ValueError("Waiting for {} to be active - never became active!".format(self.name))
def set_refresh_interval(self, new_interval):
"""Allows setting Refresher's refresh intervals after it's been initialized"""
#interval for checking the in_background property in the activate()
#when refresh_interval is small enough, is the same as refresh_interval
if new_interval == 0:
raise ValueError("Refresher refresh_interval can't be 0 ({})".format(self.name))
self.refresh_interval = new_interval
self.sleep_time = 0.1 if new_interval > 0.1 else new_interval
self.calculate_intervals()
def set_refresh_function(self, refresh_function):
if isinstance(refresh_function, RefresherView):
refresh_function.init(self.o)
self.refresh_function = refresh_function
def calculate_intervals(self):
"""Calculates the sleep intervals of the refresher, so that no matter the
``refresh_interval``, the refresher is responsive. Also, sets the counter to zero."""
#in_background of the refresher needs to be checked approx. each 0.1 second,
#since users expect the refresher to exit almost instantly
iterations_before_refresh = self.refresh_interval/self.sleep_time
if iterations_before_refresh < 1:
logger.warning("{}: self.refresh_interval is smaller than self.sleep_time!".format(self.name))
#Failsafe
self.iterations_before_refresh = 1
else:
self.iterations_before_refresh = int(iterations_before_refresh)
self._counter = 0
def idle_loop(self):
if self.in_foreground:
if self._counter == self.iterations_before_refresh:
self._counter = 0
if self._counter == 0:
self.refresh()
self._counter += 1
sleep(self.sleep_time)
@internal_callback_in_background
def change_interval(self):
"""
A helper function to adjust the Refresher's refresh interval while it's running
"""
new_interval = IntegerAdjustInput(self.refresh_interval, self.i, self.o, message="Refresh interval:").activate()
if new_interval is not None:
self.set_refresh_interval(new_interval)
def set_keymap(self, keymap):
keymap.update(self.custom_keymap)
BaseUIElement.set_keymap(self, keymap)
def generate_keymap(self):
return {}
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.
"""
# This function is copied from base_ui.py - the only difference is
# the RefresherExitException handling. TODO: think of a prettier way
# to make it work.
@wraps(func)
def wrapper(*args, **kwargs):
self.to_background()
self.to_background()
e = None
try:
func(*args, **kwargs)
except RefresherExitException:
self.deactivate()
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
@to_be_foreground
def refresh(self):
logger.debug("{}: refreshed data on display".format(self.name))
try:
data_to_display = self.refresh_function()
except RefresherExitException:
logger.info("{}: received exit exception, deactivating".format(self.name))
self.deactivate()
return
if isinstance(data_to_display, basestring):
#Passed a string, not a list.
#Let's be user-friendly and wrap it in a list!
data_to_display = [data_to_display]
elif isinstance(data_to_display, tuple):
#Passed a tuple. Let's convert it into a list!
data_to_display = list(data_to_display)
elif isinstance(data_to_display, PIL.Image.Image):
if "b&w" not in self.o.type:
raise ValueError("The screen doesn't support showing images!")
self.o.display_image(data_to_display)
return
elif not isinstance(data_to_display, list):
raise ValueError("refresh_function returned an unsupported type: {}!".format(type(data_to_display)))
self.o.display_data(*data_to_display)
class RefresherView(object):
def __init__(self, text_callback, monochrome_callback, color_callback=None):
self.text_callback = text_callback
self.monochrome_callback = monochrome_callback
self.color_callback = color_callback
def init(self, o):
if "color" in o.type:
self.callback = self.color_callback if self.color_callback else self.monochrome_callback
elif "b&w" in o.type:
self.callback = self.monochrome_callback
else:
self.callback = self.text_callback
def __call__(self, *args, **kwargs):
return self.callback(*args, **kwargs)