-
-
Save s0h3ck/da79c4b7ff0913fce78d4f22f2edf051 to your computer and use it in GitHub Desktop.
LockerLayout - adding kivy widgets to predefined boxes in desired sequence
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import kivy | |
from kivy.app import App | |
from kivy.uix.label import Label | |
from kivy.uix.layout import Layout | |
from kivy.properties import NumericProperty, VariableListProperty, ListProperty, OptionProperty, DictProperty | |
from kivy.uix.button import Button | |
class LockerLayout(Layout): | |
"""Spacing between children, in pixels.""" | |
spacing = NumericProperty(0) | |
"""Padding between layout box and children: [padding_left, padding_top, padding_right, padding_bottom]""" | |
padding = VariableListProperty() | |
"""size of lockers for attaching widgets, layout space divided among lockers proportionally to locker_size""" | |
locker_size = ListProperty() | |
"""Orientation of the layout.""" | |
orientation = OptionProperty('horizontal', options=('horizontal', 'vertical')) | |
"""locker box size ad position""" | |
lockerdata = DictProperty() | |
"""child to lockerid map""" | |
child2locker = DictProperty() | |
def __init__(self, **kwargs): | |
super(LockerLayout, self).__init__(**kwargs) | |
update = self._trigger_layout | |
fbind = self.fbind | |
fbind('spacing', update) | |
fbind('padding', update) | |
fbind('children', update) | |
fbind('orientation', update) | |
fbind('parent', update) | |
fbind('size', update) | |
fbind('pos', update) | |
fbind('locker_size', update) | |
fbind('resize_children', update) | |
fbind('children2locker', update) | |
fbind('lockerdata', update) | |
def do_layout(self, *args): | |
children = self.children | |
self.update_lockerdata() | |
for c in children: | |
# if locker data for child are not ready get_lockerdata(c) returns None | |
ld = self.get_lockerdata(c) | |
if ld is None: | |
continue | |
w, h = ld['width'], ld['height'] | |
# size | |
shw, shh = c.size_hint | |
shw_min, shh_min = c.size_hint_min | |
shw_max, shh_max = c.size_hint_max | |
if shw is not None and shh is not None: | |
c_w = shw * w | |
c_h = shh * h | |
if shw_min is not None and c_w < shw_min: | |
c_w = shw_min | |
elif shw_max is not None and c_w > shw_max: | |
c_w = shw_max | |
if shh_min is not None and c_h < shh_min: | |
c_h = shh_min | |
elif shh_max is not None and c_h > shh_max: | |
c_h = shh_max | |
c.size = c_w, c_h | |
elif shw is not None: | |
c_w = shw * w | |
if shw_min is not None and c_w < shw_min: | |
c_w = shw_min | |
elif shw_max is not None and c_w > shw_max: | |
c_w = shw_max | |
c.width = c_w | |
elif shh is not None: | |
c_h = shh * h | |
if shh_min is not None and c_h < shh_min: | |
c_h = shh_min | |
elif shh_max is not None and c_h > shh_max: | |
c_h = shh_max | |
c.height = c_h | |
# pos | |
# ld = self.get_lockerdata(c) | |
x, y, w, h = ld['x'], ld['y'], ld['width'], ld['height'] | |
c.x, c.y = x, y | |
for key, value in c.pos_hint.items(): | |
if key == 'x': | |
c.x = x + value * w | |
elif key == 'right': | |
c.right = x + value * w | |
elif key == 'pos': | |
c.pos = x + value[0] * w, y + value[1] * h | |
elif key == 'y': | |
c.y = y + value * h | |
elif key == 'top': | |
c.top = y + value * h | |
elif key == 'center': | |
c.center = x + value[0] * w, y + value[1] * h | |
elif key == 'center_x': | |
c.center_x = x + value * w | |
elif key == 'center_y': | |
c.center_y = y + value * h | |
def update_lockerdata(self, *args): | |
locker_size = self.locker_size | |
if min(locker_size) <= 0: | |
oborra('some locker bins are <= 0 : ', | |
'{}'.format({str(i): x for i, x in enumerate(locker_size) if x <=0})) | |
padding_left, padding_top, padding_right, padding_bottom = self.padding | |
spacing = self.spacing | |
padding_x = padding_left + padding_right | |
padding_y = padding_top + padding_bottom | |
orientation = self.orientation | |
locker_total = float(sum(locker_size)) | |
x = self.x + padding_left | |
y = self.y + padding_bottom | |
if orientation == 'horizontal': | |
chilren_space = self.width - padding_x - (len(locker_size) - 1) * spacing | |
height = self.height - padding_y | |
for lid, ls in enumerate(locker_size): | |
# width is a fraction of available children_space | |
width = round(ls / locker_total * chilren_space) | |
# update locker bin data | |
self.set_lockerdata(lid, x, y, width, height) | |
# prepare for next bin | |
x += width + spacing | |
else: | |
# orientation is an option property vertical orientation here | |
chilren_space = self.height - padding_y - (len(locker_size) - 1) * spacing | |
width = self.width - padding_x | |
for lid, ls in enumerate(locker_size): | |
# height is a fraction of available children_space | |
height = round(ls / locker_total * chilren_space) | |
self.set_lockerdata(lid, x, y, width, height) | |
y += height + spacing | |
def set_lockerdata(self, lid, x, y, width, height): | |
# update locker bin data, calculates all key values form x, y, width, height | |
lockerdata = self.lockerdata | |
if lid not in lockerdata: | |
lockerdata[lid] = {} | |
lockerdata[lid]['x'] = x | |
lockerdata[lid]['y'] = y | |
lockerdata[lid]['pos'] = (x, y) | |
lockerdata[lid]['width'] = width | |
lockerdata[lid]['height'] = height | |
lockerdata[lid]['size'] = (width, height) | |
center_x = x + width / 2. | |
lockerdata[lid]['center_x'] = center_x | |
center_y = y + height / 2. | |
lockerdata[lid]['center_y'] = center_y | |
lockerdata[lid]['center'] = (center_x, center_y) | |
lockerdata[lid]['top'] = y + height | |
lockerdata[lid]['right'] = x + width | |
def get_lockerdata(self, child): | |
# covers widgets added with lockerid passed to add_widget and no lockerid attribute | |
lid = self.get_lockerid(child) | |
# always consider child lockerid | |
if hasattr(child, 'lockerid'): | |
lid = self.update_child2locker(child, child.lockerid) | |
if lid is None: | |
return None | |
lockerdata = self.lockerdata | |
ld = {} | |
try: | |
x = lockerdata[lid[0]]['x'] | |
y = lockerdata[lid[0]]['y'] | |
width = lockerdata[lid[1]]['right'] - lockerdata[lid[0]]['x'] | |
height = lockerdata[lid[1]]['top'] - lockerdata[lid[0]]['y'] | |
ld['x'] = x | |
ld['y'] = y | |
ld['pos'] = (x, y) | |
ld['width'] = width | |
ld['height'] = height | |
ld['size'] = (width, height) | |
center_x = x + width / 2. | |
ld['center_x'] = center_x | |
center_y = y + height / 2. | |
ld['center_y'] = center_y | |
ld['center'] = (center_x, center_y) | |
ld['top'] = y + height | |
ld['right'] = x + width | |
return ld | |
except KeyError: | |
return None | |
def get_lockerid(self, child): | |
return self.child2locker.get(child, None) | |
def add_widget(self, widget, index=0, lockerid=None): | |
# widget.lockerid has piority over lockerid | |
super(LockerLayout, self).add_widget(widget, index) | |
try: | |
lid = widget.lockerid if lockerid is None else lockerid | |
except AttributeError: | |
return | |
self.update_child2locker(widget, lid) | |
def update_child2locker(self, child, lid): | |
# lid can be either int or n-m string representing span over lockers | |
lid = str(lid) | |
# get locker id range | |
try: | |
lid = map(float, lid.split('-'))[0:2] | |
except ValueError: | |
lid = [0] | |
if len(lid) == 1: lid.append(lid[0]) | |
if lid[0] > lid[1]: lid.reverse() | |
# lid must be a valid locker_size index list | |
# lid = [max(0, lid[0]), min(lid[1], len(self.locker_size)-1)] | |
self.child2locker[child] = lid | |
return lid | |
def remove_widget(self, widget): | |
super(LockerLayout, self).remove_widget(widget) | |
try: | |
self.child2locker.pop(widget) | |
except KeyError: | |
pass | |
class MyApp(App): | |
title='LockerLayout' | |
def build(self): | |
locker = LockerLayout(locker_size=[.05, .8, .05], orientation='horizontal') | |
for i in range(10): | |
locker.add_widget(Button(text=str(i)), index=i) | |
return locker | |
if __name__ == '__main__': | |
MyApp().run() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment