340 lines
11 KiB
Python
340 lines
11 KiB
Python
# builtins
|
|
import warnings
|
|
import random
|
|
import datetime
|
|
import colorsys
|
|
|
|
def random_string_light(length=6):
|
|
|
|
ranges = ((65, 90), (97, 122)) # A-Z, a-z
|
|
rand = random.SystemRandom() #os.urandom, cryptographically secure
|
|
string = ''
|
|
|
|
for i in range(0, length):
|
|
|
|
r = ranges[rand.randint(0, len(ranges)-1)]
|
|
string += chr(rand.randint(r[0], r[1]))
|
|
|
|
return string
|
|
|
|
class class_or_instance_method(classmethod):
|
|
|
|
def __get__(self, instance, owner=None):
|
|
|
|
if instance is None: # call via class
|
|
return super().__get__(instance, owner) # invoke classmethod.__get__
|
|
return self.__func__.__get__(instance, owner) # invoke standard implementation
|
|
|
|
def prettydate(value):
|
|
|
|
if isinstance(value, datetime.datetime):
|
|
return value.strftime('%a %b %d %Y - %H:%M:%S')
|
|
|
|
return 'Lost in Time'
|
|
|
|
class ChildAwareMeta(type):
|
|
|
|
def __init__(cls, *args, **kwargs):
|
|
|
|
super().__init__(*args, **kwargs)
|
|
#super().__init_subclass__(**kwargs)
|
|
|
|
cls.__class_children__ = {}
|
|
cls.__class_descendants__ = {}
|
|
cls.__class_children_lowercase__ = {}
|
|
cls.__class_descendants_lowercase__ = {}
|
|
cls._lowerclass = cls.__name__.lower()
|
|
|
|
for base in cls.__bases__:
|
|
|
|
if hasattr(base, '__class_children__'): # in lieu of isinstance check with as-yet undefined ChildAware
|
|
|
|
if cls.__name__ in base.__class_children__:
|
|
|
|
cls_old = base.__class_children__[cls.__name__] # get existing class from base
|
|
|
|
# same qualname means redefinition of class in local scope,
|
|
# which is a-ok and can freely overwrite the old reference
|
|
if cls.__full_qualname__ != cls_old.__full_qualname__:
|
|
warnings.warn(f"ChildAware subclasses should have unique names, '{cls.__full_qualname__}' overwrites '{cls_old.__full_qualname__}' in '{base.__full_qualname__}.__class_children__'.")
|
|
|
|
base.__class_children__[cls.__name__] = cls
|
|
|
|
if cls._lowerclass in base.__class_children_lowercase__:
|
|
|
|
cls_old = base.__class_children_lowercase__[cls._lowerclass]
|
|
|
|
# same qualname yadda yadda
|
|
if cls.__full_qualname__ != cls_old.__full_qualname__:
|
|
warnings.warn(f"ChildAware subclasses should have unique lowercase names, '{cls.__full_qualname__}' overwrites '{cls_old.__full_qualname__}' in '{base.__full_qualname__}.__class_children_lowercase__'.")
|
|
|
|
base.__class_children_lowercase__[cls._lowerclass] = cls
|
|
|
|
for ancestor in cls.__mro__[1:]:
|
|
|
|
if hasattr(ancestor, '__class_descendants__'): # in lieu of isinstance check with as-yet undefined ChildAware
|
|
|
|
if cls.__name__ in ancestor.__class_descendants__:
|
|
|
|
cls_old = ancestor.__class_descendants__[cls.__name__] # get existing class from ancestor
|
|
|
|
# same qualname means redefinition of class in local scope,
|
|
# which is a-ok and can freely overwrite the old reference
|
|
if cls.__full_qualname__ != cls_old.__full_qualname__:
|
|
|
|
warnings.warn(f"ChildAware subclasses should have unique names, '{cls.__full_qualname__}' overwrites '{cls_old.__full_qualname__}' in '{ancestor.__full_qualname__}.__class_descendants__' .")
|
|
|
|
ancestor.__class_descendants__[cls.__name__] = cls
|
|
|
|
if cls.__name__ in ancestor.__class_descendants_lowercase__:
|
|
|
|
cls_old = ancestor.__class_descendants_lowercase__[cls._lowerclass]
|
|
|
|
if cls.__full_qualname__ != cls_old.__full_qualname__:
|
|
warnings.warn(f"ChildAware subclasses should have unique lowercase names, '{cls.__full_qualname__}' overwrites '{cls_old.__full_qualname__}' in '{base.__full_qualname__}.__class_descendants_lowercase__'.")
|
|
|
|
ancestor.__class_descendants_lowercase__[cls._lowerclass] = cls
|
|
|
|
|
|
@property # @property works to make this a *class* attribute because we're in the metaclass
|
|
def __full_qualname__(cls):
|
|
return f'{cls.__module__}.{cls.__qualname__}'
|
|
|
|
class ChildAware(metaclass=ChildAwareMeta):
|
|
pass
|
|
|
|
class Color(object):
|
|
|
|
"""
|
|
Magic color class implementing and supplying on-the-fly manipulation of
|
|
RGB and HSV (and alpha) attributes. Taken from gulik.
|
|
"""
|
|
|
|
def __init__(self, red=None, green=None, blue=None, alpha=None, hue=None, saturation=None, value=None):
|
|
|
|
rgb_passed = bool(red)|bool(green)|bool(blue)
|
|
hsv_passed = bool(hue)|bool(saturation)|bool(value)
|
|
|
|
if not alpha:
|
|
alpha = 1.0
|
|
|
|
if rgb_passed and hsv_passed:
|
|
raise ValueError("Color can't be initialized with RGB and HSV at the same time.")
|
|
|
|
elif hsv_passed:
|
|
|
|
if not hue:
|
|
hue = 0.0
|
|
if not saturation:
|
|
saturation = 0.0
|
|
if not value:
|
|
value = 0.0
|
|
|
|
super(Color, self).__setattr__('hue', hue)
|
|
super(Color, self).__setattr__('saturation', saturation)
|
|
super(Color, self).__setattr__('value', value)
|
|
self._update_rgb()
|
|
|
|
else:
|
|
|
|
if not red:
|
|
red = 0
|
|
if not green:
|
|
green = 0
|
|
if not blue:
|
|
blue = 0
|
|
|
|
super(Color, self).__setattr__('red', red)
|
|
super(Color, self).__setattr__('green', green)
|
|
super(Color, self).__setattr__('blue', blue)
|
|
self._update_hsv()
|
|
|
|
super(Color, self).__setattr__('alpha', alpha)
|
|
|
|
def __setattr__(self, key, value):
|
|
|
|
if key in ('red', 'green', 'blue'):
|
|
if value > 1.0:
|
|
value = 1.0
|
|
super(Color, self).__setattr__(key, value)
|
|
self._update_hsv()
|
|
|
|
elif key in ('hue', 'saturation', 'value'):
|
|
if key == 'hue' and (value >= 360.0 or value < 0):
|
|
value = value % 360.0
|
|
elif key != 'hue' and value > 1.0:
|
|
value = 1.0
|
|
super(Color, self).__setattr__(key, value)
|
|
self._update_rgb()
|
|
|
|
else:
|
|
if key == 'alpha' and value > 1.0: # TODO: Might this be more fitting in another place?
|
|
value = 1.0
|
|
|
|
super(Color, self).__setattr__(key, value)
|
|
|
|
def __repr__(self):
|
|
|
|
return '<%s: red %f, green %f, blue %f, hue %f, saturation %f, value %f, alpha %f>' % (
|
|
self.__class__.__name__,
|
|
self.red,
|
|
self.green,
|
|
self.blue,
|
|
self.hue,
|
|
self.saturation,
|
|
self.value,
|
|
self.alpha
|
|
)
|
|
|
|
def css(self):
|
|
"""
|
|
newstyle rgb(), NOT rgba().
|
|
"""
|
|
return f'rgb({round(self.red * 255)} {round(self.green * 255)} {round(self.blue * 255)} / {self.alpha * 100}%)'
|
|
|
|
def clone(self):
|
|
return Color(red=self.red, green=self.green, blue=self.blue, alpha=self.alpha)
|
|
|
|
def blend(self, other, mode='normal'):
|
|
|
|
clone = self.clone()
|
|
if clone.alpha != 1.0: # no clue how to blend with a translucent bottom layer
|
|
clone.red = clone.red * clone.alpha
|
|
clone.green = clone.green * clone.alpha
|
|
clone.blue = clone.blue * clone.alpha
|
|
|
|
clone.alpha = 1.0
|
|
|
|
if mode == 'normal':
|
|
own_influence = 1.0 - other.alpha
|
|
clone.red = (clone.red * own_influence) + (other.red * other.alpha)
|
|
clone.green = (clone.green * own_influence) + (other.green * other.alpha)
|
|
clone.blue = (clone.blue * own_influence) + (other.blue * other.alpha)
|
|
|
|
return clone
|
|
|
|
def lighten(self, other):
|
|
|
|
if isinstance(other, int) or isinstance(other, float):
|
|
other = Color(red=other, green=other, blue=other, alpha=1.0)
|
|
|
|
if self.alpha != 1.0:
|
|
self.red = self.red * self.alpha
|
|
self.green = self.green * self.alpha
|
|
self.blue = self.blue * self.alpha
|
|
|
|
self.alpha = 1.0
|
|
|
|
red = self.red + (other.red * other.alpha)
|
|
green = self.green + (other.green * other.alpha)
|
|
blue = self.blue + (other.blue * other.alpha)
|
|
|
|
if red > 1.0:
|
|
red = 1.0
|
|
|
|
if green > 1.0:
|
|
green = 1.0
|
|
|
|
if blue > 1.0:
|
|
blue = 1.0
|
|
|
|
self.red = red
|
|
self.green = green
|
|
self.blue = blue
|
|
|
|
def darken(self, other):
|
|
|
|
if isinstance(other, int) or isinstance(other, float):
|
|
other = Color(red=other, green=other, blue=other, alpha=1.0)
|
|
|
|
red = self.red - other.red
|
|
green = self.green - other.green
|
|
blue = self.blue - other.blue
|
|
|
|
if red < 0:
|
|
red = 0
|
|
|
|
if green < 0:
|
|
green = 0
|
|
|
|
if blue < 0:
|
|
blue = 0
|
|
|
|
self.red = red
|
|
self.green = green
|
|
self.blue = blue
|
|
|
|
def tuple_rgb(self):
|
|
""" return color (without alpha) as tuple, channels being float 0.0-1.0 """
|
|
return (self.red, self.green, self.blue)
|
|
|
|
def tuple_rgba(self):
|
|
""" return color (*with* alpha) as tuple, channels being float 0.0-1.0 """
|
|
return (self.red, self.green, self.blue, self.alpha)
|
|
|
|
def hex_rgb(self):
|
|
|
|
red, green, blue = map(lambda channel: round(channel * 255), self.tuple_rgb())
|
|
return f'#{red:02x}{green:02x}{blue:02x}'
|
|
|
|
def hex_rgba(self):
|
|
|
|
red, green, blue, alpha = map(lambda channel: round(channel * 255), self.tuple_rgba())
|
|
return f'#{red:02x}{green:02x}{blue:02x}{alpha:02x}'
|
|
|
|
@classmethod
|
|
def from_hex(cls, hexstring):
|
|
|
|
hexstring = hexstring.lower()
|
|
|
|
if hexstring.startswith('#'):
|
|
hexstring = hexstring[1:]
|
|
|
|
length = len(hexstring)
|
|
|
|
if length % 2 != 0:
|
|
raise ValueError(f"All hex channels MUST be to characters long, got uneven length in '{hexstring}'!")
|
|
|
|
if length == 3:
|
|
raise NotImplemented("Place code to parse short hex format for colors here.")
|
|
# TODO: Is 4 chars valid shortform for rgba?
|
|
|
|
if length > 8:
|
|
raise ValueError(f"More than 4 8-bit channels in hex string: '{hexstring}'!")
|
|
|
|
if length < 6:
|
|
raise ValueError(f"Less than 3 8-bit channels in hex string: '{hexstring}'!")
|
|
|
|
channel_names = {
|
|
0: 'red',
|
|
2: 'green',
|
|
4: 'blue',
|
|
6: 'alpha',
|
|
}
|
|
|
|
params = {}
|
|
|
|
for i in range(0, len(hexstring), 2):
|
|
|
|
hexbyte = hexstring[i:i+2]
|
|
intbyte = int(hexbyte, 16)
|
|
|
|
channel_name = channel_names[i]
|
|
params[channel_name] = intbyte / 255
|
|
|
|
return cls(**params)
|
|
|
|
def _update_hsv(self):
|
|
|
|
hue, saturation, value = colorsys.rgb_to_hsv(self.red, self.green, self.blue)
|
|
super(Color, self).__setattr__('hue', hue * 360.0)
|
|
super(Color, self).__setattr__('saturation', saturation)
|
|
super(Color, self).__setattr__('value', value)
|
|
|
|
def _update_rgb(self):
|
|
|
|
red, green, blue = colorsys.hsv_to_rgb(self.hue / 360.0, self.saturation, self.value)
|
|
super(Color, self).__setattr__('red', red)
|
|
super(Color, self).__setattr__('green', green)
|
|
super(Color, self).__setattr__('blue', blue)
|