332 lines
11 KiB
Python
332 lines
11 KiB
Python
'''
|
|
Stack Layout
|
|
============
|
|
|
|
.. only:: html
|
|
|
|
.. image:: images/stacklayout.gif
|
|
:align: right
|
|
|
|
.. only:: latex
|
|
|
|
.. image:: images/stacklayout.png
|
|
:align: right
|
|
|
|
.. versionadded:: 1.0.5
|
|
|
|
The :class:`StackLayout` arranges children vertically or horizontally, as many
|
|
as the layout can fit. The size of the individual children widgets do not
|
|
have to be uniform.
|
|
|
|
For example, to display widgets that get progressively larger in width::
|
|
|
|
root = StackLayout()
|
|
for i in range(25):
|
|
btn = Button(text=str(i), width=40 + i * 5, size_hint=(None, 0.15))
|
|
root.add_widget(btn)
|
|
|
|
.. image:: images/stacklayout_sizing.png
|
|
:align: left
|
|
'''
|
|
|
|
__all__ = ('StackLayout', )
|
|
|
|
from kivy.uix.layout import Layout
|
|
from kivy.properties import NumericProperty, OptionProperty, \
|
|
ReferenceListProperty, VariableListProperty
|
|
|
|
|
|
def _compute_size(c, available_size, idx):
|
|
sh_min = c.size_hint_min[idx]
|
|
sh_max = c.size_hint_max[idx]
|
|
val = c.size_hint[idx] * available_size
|
|
|
|
if sh_min is not None:
|
|
if sh_max is not None:
|
|
return max(min(sh_max, val), sh_min)
|
|
return max(val, sh_min)
|
|
if sh_max is not None:
|
|
return min(sh_max, val)
|
|
return val
|
|
|
|
|
|
class StackLayout(Layout):
|
|
'''Stack layout class. See module documentation for more information.
|
|
'''
|
|
|
|
spacing = VariableListProperty([0, 0], length=2)
|
|
'''Spacing between children: [spacing_horizontal, spacing_vertical].
|
|
|
|
spacing also accepts a single argument form [spacing].
|
|
|
|
:attr:`spacing` is a
|
|
:class:`~kivy.properties.VariableListProperty` and defaults to [0, 0].
|
|
|
|
'''
|
|
|
|
padding = VariableListProperty([0, 0, 0, 0])
|
|
'''Padding between the layout box and it's children: [padding_left,
|
|
padding_top, padding_right, padding_bottom].
|
|
|
|
padding also accepts a two argument form [padding_horizontal,
|
|
padding_vertical] and a single argument form [padding].
|
|
|
|
.. versionchanged:: 1.7.0
|
|
Replaced the NumericProperty with a VariableListProperty.
|
|
|
|
:attr:`padding` is a
|
|
:class:`~kivy.properties.VariableListProperty` and defaults to
|
|
[0, 0, 0, 0].
|
|
|
|
'''
|
|
|
|
orientation = OptionProperty('lr-tb', options=(
|
|
'lr-tb', 'tb-lr', 'rl-tb', 'tb-rl', 'lr-bt', 'bt-lr', 'rl-bt',
|
|
'bt-rl'))
|
|
'''Orientation of the layout.
|
|
|
|
:attr:`orientation` is an :class:`~kivy.properties.OptionProperty` and
|
|
defaults to 'lr-tb'.
|
|
|
|
Valid orientations are 'lr-tb', 'tb-lr', 'rl-tb', 'tb-rl', 'lr-bt',
|
|
'bt-lr', 'rl-bt' and 'bt-rl'.
|
|
|
|
.. versionchanged:: 1.5.0
|
|
:attr:`orientation` now correctly handles all valid combinations of
|
|
'lr','rl','tb','bt'. Before this version only 'lr-tb' and
|
|
'tb-lr' were supported, and 'tb-lr' was misnamed and placed
|
|
widgets from bottom to top and from right to left (reversed compared
|
|
to what was expected).
|
|
|
|
.. note::
|
|
|
|
'lr' means Left to Right.
|
|
'rl' means Right to Left.
|
|
'tb' means Top to Bottom.
|
|
'bt' means Bottom to Top.
|
|
'''
|
|
|
|
minimum_width = NumericProperty(0)
|
|
'''Minimum width needed to contain all children. It is automatically set
|
|
by the layout.
|
|
|
|
.. versionadded:: 1.0.8
|
|
|
|
:attr:`minimum_width` is a :class:`kivy.properties.NumericProperty` and
|
|
defaults to 0.
|
|
'''
|
|
|
|
minimum_height = NumericProperty(0)
|
|
'''Minimum height needed to contain all children. It is automatically set
|
|
by the layout.
|
|
|
|
.. versionadded:: 1.0.8
|
|
|
|
:attr:`minimum_height` is a :class:`kivy.properties.NumericProperty` and
|
|
defaults to 0.
|
|
'''
|
|
|
|
minimum_size = ReferenceListProperty(minimum_width, minimum_height)
|
|
'''Minimum size needed to contain all children. It is automatically set
|
|
by the layout.
|
|
|
|
.. versionadded:: 1.0.8
|
|
|
|
:attr:`minimum_size` is a
|
|
:class:`~kivy.properties.ReferenceListProperty` of
|
|
(:attr:`minimum_width`, :attr:`minimum_height`) properties.
|
|
'''
|
|
|
|
def __init__(self, **kwargs):
|
|
super(StackLayout, self).__init__(**kwargs)
|
|
trigger = self._trigger_layout
|
|
fbind = self.fbind
|
|
fbind('padding', trigger)
|
|
fbind('spacing', trigger)
|
|
fbind('children', trigger)
|
|
fbind('orientation', trigger)
|
|
fbind('size', trigger)
|
|
fbind('pos', trigger)
|
|
|
|
def do_layout(self, *largs):
|
|
if not self.children:
|
|
self.minimum_size = (0., 0.)
|
|
return
|
|
|
|
# optimize layout by preventing looking at the same attribute in a loop
|
|
selfpos = self.pos
|
|
selfsize = self.size
|
|
orientation = self.orientation.split('-')
|
|
padding_left = self.padding[0]
|
|
padding_top = self.padding[1]
|
|
padding_right = self.padding[2]
|
|
padding_bottom = self.padding[3]
|
|
|
|
padding_x = padding_left + padding_right
|
|
padding_y = padding_top + padding_bottom
|
|
spacing_x, spacing_y = self.spacing
|
|
|
|
# Determine which direction and in what order to place the widgets
|
|
posattr = [0] * 2
|
|
posdelta = [0] * 2
|
|
posstart = [0] * 2
|
|
for i in (0, 1):
|
|
posattr[i] = 1 * (orientation[i] in ('tb', 'bt'))
|
|
k = posattr[i]
|
|
if orientation[i] == 'lr':
|
|
# left to right
|
|
posdelta[i] = 1
|
|
posstart[i] = selfpos[k] + padding_left
|
|
elif orientation[i] == 'bt':
|
|
# bottom to top
|
|
posdelta[i] = 1
|
|
posstart[i] = selfpos[k] + padding_bottom
|
|
elif orientation[i] == 'rl':
|
|
# right to left
|
|
posdelta[i] = -1
|
|
posstart[i] = selfpos[k] + selfsize[k] - padding_right
|
|
else:
|
|
# top to bottom
|
|
posdelta[i] = -1
|
|
posstart[i] = selfpos[k] + selfsize[k] - padding_top
|
|
|
|
innerattr, outerattr = posattr
|
|
ustart, vstart = posstart
|
|
deltau, deltav = posdelta
|
|
del posattr, posdelta, posstart
|
|
|
|
u = ustart # inner loop position variable
|
|
v = vstart # outer loop position variable
|
|
|
|
# space calculation, used for determining when a row or column is full
|
|
|
|
if orientation[0] in ('lr', 'rl'):
|
|
sv = padding_y # size in v-direction, for minimum_size property
|
|
su = padding_x # size in h-direction
|
|
spacing_u = spacing_x
|
|
spacing_v = spacing_y
|
|
padding_u = padding_x
|
|
padding_v = padding_y
|
|
else:
|
|
sv = padding_x # size in v-direction, for minimum_size property
|
|
su = padding_y # size in h-direction
|
|
spacing_u = spacing_y
|
|
spacing_v = spacing_x
|
|
padding_u = padding_y
|
|
padding_v = padding_x
|
|
|
|
# space calculation, row height or column width, for arranging widgets
|
|
lv = 0
|
|
|
|
urev = (deltau < 0)
|
|
vrev = (deltav < 0)
|
|
firstchild = self.children[0]
|
|
sizes = []
|
|
lc = []
|
|
for c in reversed(self.children):
|
|
if c.size_hint[outerattr] is not None:
|
|
c.size[outerattr] = max(
|
|
1, _compute_size(c, selfsize[outerattr] - padding_v,
|
|
outerattr))
|
|
|
|
# does the widget fit in the row/column?
|
|
ccount = len(lc)
|
|
totalsize = availsize = max(
|
|
0, selfsize[innerattr] - padding_u - spacing_u * ccount)
|
|
if not lc:
|
|
if c.size_hint[innerattr] is not None:
|
|
childsize = max(1, _compute_size(c, totalsize, innerattr))
|
|
else:
|
|
childsize = max(0, c.size[innerattr])
|
|
availsize = selfsize[innerattr] - padding_u - childsize
|
|
testsizes = [childsize]
|
|
else:
|
|
testsizes = [0] * (ccount + 1)
|
|
for i, child in enumerate(lc):
|
|
if availsize <= 0:
|
|
# no space left but we're trying to add another widget.
|
|
availsize = -1
|
|
break
|
|
if child.size_hint[innerattr] is not None:
|
|
testsizes[i] = childsize = max(
|
|
1, _compute_size(child, totalsize, innerattr))
|
|
else:
|
|
childsize = max(0, child.size[innerattr])
|
|
testsizes[i] = childsize
|
|
availsize -= childsize
|
|
if c.size_hint[innerattr] is not None:
|
|
testsizes[-1] = max(
|
|
1, _compute_size(c, totalsize, innerattr))
|
|
else:
|
|
testsizes[-1] = max(0, c.size[innerattr])
|
|
availsize -= testsizes[-1]
|
|
|
|
# Tiny value added in order to avoid issues with float precision
|
|
# causing unexpected children reordering when parent resizes.
|
|
# e.g. if size is 101 and children size_hint_x is 1./5
|
|
# 5 children would not fit in one line because 101*(1./5) > 101/5
|
|
if (availsize + 1e-10) >= 0 or not lc:
|
|
# even if there's no space, we always add one widget to a row
|
|
lc.append(c)
|
|
sizes = testsizes
|
|
lv = max(lv, c.size[outerattr])
|
|
continue
|
|
|
|
# apply the sizes
|
|
for i, child in enumerate(lc):
|
|
if child.size_hint[innerattr] is not None:
|
|
child.size[innerattr] = sizes[i]
|
|
|
|
# push the line
|
|
sv += lv + spacing_v
|
|
for c2 in lc:
|
|
if urev:
|
|
u -= c2.size[innerattr]
|
|
c2.pos[innerattr] = u
|
|
pos_outer = v
|
|
if vrev:
|
|
# v position is actually the top/right side of the widget
|
|
# when going from high to low coordinate values,
|
|
# we need to subtract the height/width from the position.
|
|
pos_outer -= c2.size[outerattr]
|
|
c2.pos[outerattr] = pos_outer
|
|
if urev:
|
|
u -= spacing_u
|
|
else:
|
|
u += c2.size[innerattr] + spacing_u
|
|
|
|
v += deltav * lv
|
|
v += deltav * spacing_v
|
|
lc = [c]
|
|
lv = c.size[outerattr]
|
|
if c.size_hint[innerattr] is not None:
|
|
sizes = [
|
|
max(1, _compute_size(c, selfsize[innerattr] - padding_u,
|
|
innerattr))]
|
|
else:
|
|
sizes = [max(0, c.size[innerattr])]
|
|
u = ustart
|
|
|
|
if lc:
|
|
# apply the sizes
|
|
for i, child in enumerate(lc):
|
|
if child.size_hint[innerattr] is not None:
|
|
child.size[innerattr] = sizes[i]
|
|
|
|
# push the last (incomplete) line
|
|
sv += lv + spacing_v
|
|
for c2 in lc:
|
|
if urev:
|
|
u -= c2.size[innerattr]
|
|
c2.pos[innerattr] = u
|
|
pos_outer = v
|
|
if vrev:
|
|
pos_outer -= c2.size[outerattr]
|
|
c2.pos[outerattr] = pos_outer
|
|
if urev:
|
|
u -= spacing_u
|
|
else:
|
|
u += c2.size[innerattr] + spacing_u
|
|
|
|
self.minimum_size[outerattr] = sv
|