pdnew/util.py

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)