from collections import namedtuple, Sequence
from functools import wraps
from time import time, sleep
from PIL import ImageOps, Image
from helpers import setup_logger
logger = setup_logger(__name__, "info")
to_be_foreground_warnings = []
def to_be_foreground(func):
""" A safety check wrapper so that certain functions can't possibly be called
if UI element is not the one active"""
@wraps(func)
def wrapper(self, *args, **kwargs):
bypass = False
if "bypass_to_be_foreground" in kwargs:
if kwargs.pop("bypass_to_be_foreground"):
bypass = True
if bypass or self.in_foreground:
return func(self, *args, **kwargs)
else:
data = (self.__class__.__name__, func.__name__, getattr(self, "name", None))
if data not in to_be_foreground_warnings:
to_be_foreground_warnings.append(data)
logger.warning("{}.{} (UI el {}) was prevented from being executed " \
"by to_be_foreground!".format(*data) )
return False
return wrapper
def clamp(value, _min, _max):
"""
Returns a value clamped between two bounds (inclusive)
>>> clamp(17, 0, 100)
17
>>> clamp(-89, 0, 100)
0
>>> clamp(65635, 0, 100)
100
"""
return max(_min, min(value, _max))
def is_sequence_not_string(value):
"""
Checks if the value passed is a sequence, like a list or tuple - except strings.
"""
return isinstance(value, Sequence) and not isinstance(value, basestring)
def modulo_list_index(value, _list):
"""
Returns an always valid list index. Repeats the list circularly.
>>> robots=['R2D2', 'C3PO', 'HAL9000']
>>> robots[modulo_list_index(0, robots)]
'R2D2'
>>> robots[modulo_list_index(3, robots)]
'R2D2'
>>> [robots[modulo_list_index(i, robots)] for i in range(10)]
['R2D2', 'C3PO', 'HAL9000', 'R2D2', 'C3PO', 'HAL9000', 'R2D2', 'C3PO', 'HAL9000', 'R2D2']
"""
return value % len(_list)
def clamp_list_index(value, _list):
"""
Returns a list index clamped to the bounds of the list.
Useful to prevent iterating out of bounds, repeats the bounds values.
>>> astronauts = ['Collins', 'Armstrong', 'Aldrin']
>>> astronauts[clamp_list_index(0, astronauts)]
'Collins'
>>> astronauts[clamp_list_index(2, astronauts)]
'Aldrin'
>>> astronauts[clamp_list_index(9000, astronauts)]
'Aldrin'
>>> astronauts[clamp_list_index(-666, astronauts)]
'Collins'
"""
return clamp(value, 0, len(_list) - 1)
def check_value_lock(func):
""" A safety check wrapper so that there's no race conditions
between functions that are able to change position/value"""
@wraps(func)
def wrapper(self, *args, **kwargs):
assert self.value_lock, "Class has no member self.value_lock" # todo:maybe we should create it here ?
# Value-changing code is likely to run in concurrent thread and therefore we need a lock
if self.__locked_name__ is not None:
logger.warning(
"Another function already working with the value! Name is {}, current is {}".format(
self.__locked_name__,
func.__name__
)
)
with self.value_lock:
self.__locked_name__ = func.__name__
logger.debug("Locked function {}".format(func.__name__))
result = func(self, *args, **kwargs)
logger.debug("Unlocked function {}".format(func.__name__))
self.__locked_name__ = None
return result
return wrapper
class Chronometer(object):
"""
This object measures time.
>>> cron = Chronometer()
>>> cron.active
False
>>> cron.start()
>>> cron.active
True
>>> sleep(1)
>>> cron.update()
>>> round(cron.elapsed)
1.0
>>> cron.pause()
>>> sleep(1)
>>> round(cron.elapsed)
1.0
>>> cron.toggle() # or cron.resume()
>>> sleep(1)
>>> cron.update()
>>> round(cron.elapsed)
2.0
>>> cron.restart()
>>> sleep(1)
>>> cron.update()
>>> round(cron.elapsed)
1.0
"""
def __init__(self):
self.__active = False
self.__cron = Ticker()
self.__elapsed = 0
@property
def active(self):
# type: () -> bool
"""whether the Chronometer is counting time"""
return self.__active
@property
def elapsed(self):
# type: () -> float
"""returns the elapsed time"""
return self.__elapsed
def update(self):
# type: () -> None
"""Updates the chronometer with the current time"""
if not self.__active:
return
self.__elapsed += self.__cron.tick()
def stop(self):
# type: () -> None
"""Stop and resets the Chronometer"""
self.__cron.tick()
self.__elapsed = 0
self.__active = False
def pause(self):
# type: () -> None
"""Pauses the Chronometer, but keeps the measured time so far"""
self.__active = False
def resume(self):
# type: () -> None
"""Resumes measuring time after a pause"""
self.__cron.tick()
self.__active = True
def start(self):
# type: () -> None
"""Starts measuring time"""
self.stop()
self.resume()
def toggle(self):
# type: () -> None
"""Toggles between pause and resume"""
self.pause() if self.active else self.resume()
def restart(self):
# type: () -> None
"""Resets the Chronometer and starts a new measure immediatly"""
self.start()
class Ticker(object):
"""
This object returns the time elapsed between two calls to it's `tick()` function
>>> ticker = Ticker()
>>> sleep(1)
>>> elapsed = ticker.tick()
>>> round(elapsed) #rounded because time.sleep() is not that precise
1.0
"""
def __init__(self):
self.__active = False
self.__last_call = time()
def tick(self):
"""
:rtype: int
:return: the time elapsed since the previous tick
"""
now = time()
elapsed = now - self.__last_call
self.__last_call = now
return elapsed
Rect = namedtuple('Rect', ['left', 'top', 'right', 'bottom'])
[docs]def fit_image_to_screen(image, o):
"""Fits a given image to fit on any sized screen whilst maintaining the aspect ratio.
Any remaining space is filled with borders. The resized image is returned as ``image``.
Args:
* ``image``: A PIL image to be resized.
* ``o``: output device object. Used to find the width and height of the screen. """
image_width, image_height = image.size
if o.height > image_height and o.width > image.width: # Checks if the screen dimensions are equal to the image size
logger.info("Using resize script")
if o.height/image_height < o.width/image_width: # Checks which side is bigger in proportion to the image size
logger.info("Using height as multiplier")
bigger_side = o.height
bigger_image_side = image_height
smaller_image_side = image_width
else:
logger.info("Using width as multiplier")
bigger_side = o.width
bigger_image_side = image_width
smaller_image_side = image_height
bigger_side_percent = (bigger_side/float(bigger_image_side))
other_size = int((float(smaller_image_side)*float(bigger_side_percent))) # Working out smaller side length
image = image.resize((bigger_side,other_size), Image.ANTIALIAS) # Resizes the image to the calculated dimensions to fit the screen and stick to the aspect ratio using a $
elif (o.width, o.height) == image.size: # Checks if screen dimensions and exactly the same as image dimensions
logger.info("Exact same size - no changes needed")
elif (o.width == image_width and o.height > image_height) or (o.height == image_height and o.width > image_width):
logger.info("One side is the same, the other is bigger - borders needed")
else: # This should happen if the screen is smaller on one or both sides than the image
logger.info("Using thumbnail script")
size = o.width, o.height
image.thumbnail(size, Image.ANTIALIAS) # Resizes the image sticking to the aspect ratio using
if (o.width, o.height) != image.size:
logger.info("Adding borders")
left = top = right = bottom = 0
width, height = image.size
if o.width > width:
logger.info("Adding borders for width")
delta = o.width - width
left = delta // 2
right = delta - left
if o.height > height:
logger.info("Adding borders for height")
delta = o.height - height
top = delta // 2
bottom = delta - top
image = ImageOps.expand(image, border=(left, top, right, bottom), fill="black")
logger.info("Borders added are: top - {}, bottom - {}, left - {} and right - {}".format(top, bottom, left, right))
logger.info("All resizing finished")
return image