323 lines
13 KiB
Python
323 lines
13 KiB
Python
'''
|
|
Layout
|
|
======
|
|
|
|
Layouts are used to calculate and assign widget positions.
|
|
|
|
The :class:`Layout` class itself cannot be used directly.
|
|
You should use one of the following layout classes:
|
|
|
|
- Anchor layout: :class:`kivy.uix.anchorlayout.AnchorLayout`
|
|
- Box layout: :class:`kivy.uix.boxlayout.BoxLayout`
|
|
- Float layout: :class:`kivy.uix.floatlayout.FloatLayout`
|
|
- Grid layout: :class:`kivy.uix.gridlayout.GridLayout`
|
|
- Page Layout: :class:`kivy.uix.pagelayout.PageLayout`
|
|
- Relative layout: :class:`kivy.uix.relativelayout.RelativeLayout`
|
|
- Scatter layout: :class:`kivy.uix.scatterlayout.ScatterLayout`
|
|
- Stack layout: :class:`kivy.uix.stacklayout.StackLayout`
|
|
|
|
|
|
Understanding the `size_hint` Property in `Widget`
|
|
--------------------------------------------------
|
|
|
|
The :attr:`~kivy.uix.Widget.size_hint` is a tuple of values used by
|
|
layouts to manage the sizes of their children. It indicates the size
|
|
relative to the layout's size instead of an absolute size (in
|
|
pixels/points/cm/etc). The format is::
|
|
|
|
widget.size_hint = (width_proportion, height_proportion)
|
|
|
|
The proportions are specified as floating point numbers in the range 0-1. For
|
|
example, 0.5 represents 50%, 1 represents 100%.
|
|
|
|
If you want a widget's width to be half of the parent's width and the
|
|
height to be identical to the parent's height, you would do::
|
|
|
|
widget.size_hint = (0.5, 1.0)
|
|
|
|
If you don't want to use a size_hint for either the width or height, set the
|
|
value to None. For example, to make a widget that is 250px wide and 30%
|
|
of the parent's height, do::
|
|
|
|
widget.size_hint = (None, 0.3)
|
|
widget.width = 250
|
|
|
|
Being :class:`Kivy properties <kivy.properties>`, these can also be set via
|
|
constructor arguments::
|
|
|
|
widget = Widget(size_hint=(None, 0.3), width=250)
|
|
|
|
.. versionchanged:: 1.4.1
|
|
The `reposition_child` internal method (made public by mistake) has
|
|
been removed.
|
|
|
|
'''
|
|
|
|
__all__ = ('Layout', )
|
|
|
|
from kivy.clock import Clock
|
|
from kivy.uix.widget import Widget
|
|
from kivy.compat import isclose
|
|
|
|
|
|
class Layout(Widget):
|
|
'''Layout interface class, used to implement every layout. See module
|
|
documentation for more information.
|
|
'''
|
|
|
|
_trigger_layout = None
|
|
|
|
def __init__(self, **kwargs):
|
|
if self.__class__ == Layout:
|
|
raise Exception('The Layout class is abstract and \
|
|
cannot be used directly.')
|
|
if self._trigger_layout is None:
|
|
self._trigger_layout = Clock.create_trigger(self.do_layout, -1)
|
|
super(Layout, self).__init__(**kwargs)
|
|
|
|
def do_layout(self, *largs):
|
|
'''This function is called when a layout is called by a trigger.
|
|
If you are writing a new Layout subclass, don't call this function
|
|
directly but use :meth:`_trigger_layout` instead.
|
|
|
|
The function is by default called *before* the next frame, therefore
|
|
the layout isn't updated immediately. Anything depending on the
|
|
positions of e.g. children should be scheduled for the next frame.
|
|
|
|
.. versionadded:: 1.0.8
|
|
'''
|
|
raise NotImplementedError('Must be implemented in subclasses.')
|
|
|
|
def add_widget(self, widget, *args, **kwargs):
|
|
fbind = widget.fbind
|
|
fbind('size', self._trigger_layout)
|
|
fbind('size_hint', self._trigger_layout)
|
|
fbind('size_hint_max', self._trigger_layout)
|
|
fbind('size_hint_min', self._trigger_layout)
|
|
super(Layout, self).add_widget(widget, *args, **kwargs)
|
|
|
|
def remove_widget(self, widget, *args, **kwargs):
|
|
funbind = widget.funbind
|
|
funbind('size', self._trigger_layout)
|
|
funbind('size_hint', self._trigger_layout)
|
|
funbind('size_hint_max', self._trigger_layout)
|
|
funbind('size_hint_min', self._trigger_layout)
|
|
super(Layout, self).remove_widget(widget, *args, **kwargs)
|
|
|
|
def layout_hint_with_bounds(
|
|
self, sh_sum, available_space, min_bounded_size, sh_min_vals,
|
|
sh_max_vals, hint):
|
|
'''(internal) Computes the appropriate (size) hint for all the
|
|
widgets given (potential) min or max bounds on the widgets' size.
|
|
The ``hint`` list is updated with appropriate sizes.
|
|
|
|
It walks through the hints and for any widgets whose hint will result
|
|
in violating min or max constraints, it fixes the hint. Any remaining
|
|
or missing space after all the widgets are fixed get distributed
|
|
to the widgets making them smaller or larger according to their
|
|
size hint.
|
|
|
|
This algorithms knows nothing about the widgets other than what is
|
|
passed through the input params, so it's fairly generic for laying
|
|
things out according to constraints using size hints.
|
|
|
|
:Parameters:
|
|
|
|
`sh_sum`: float
|
|
The sum of the size hints (basically ``sum(size_hint)``).
|
|
`available_space`: float
|
|
The amount of pixels available for all the widgets
|
|
whose size hint is not None. Cannot be zero.
|
|
`min_bounded_size`: float
|
|
The minimum amount of space required according to the
|
|
`size_hint_min` of the widgets (basically
|
|
``sum(size_hint_min)``).
|
|
`sh_min_vals`: list or iterable
|
|
Items in the iterable are the size_hint_min for each widget.
|
|
Can be None. The length should be the same as ``hint``
|
|
`sh_max_vals`: list or iterable
|
|
Items in the iterable are the size_hint_max for each widget.
|
|
Can be None. The length should be the same as ``hint``
|
|
`hint`: list
|
|
A list whose size is the same as the length of ``sh_min_vals``
|
|
and ``sh_min_vals`` whose each element is the corresponding
|
|
size hint value of that element. This list is updated in place
|
|
with correct size hints that ensure the constraints are not
|
|
violated.
|
|
|
|
:returns:
|
|
Nothing. ``hint`` is updated in place.
|
|
'''
|
|
if not sh_sum:
|
|
return
|
|
# TODO: test when children have size_hint, max/min of zero
|
|
|
|
# all divs are float denominator ;)
|
|
stretch_ratio = sh_sum / float(available_space)
|
|
if available_space <= min_bounded_size or \
|
|
isclose(available_space, min_bounded_size):
|
|
# too small, just set to min
|
|
for i, (sh, sh_min) in enumerate(zip(hint, sh_min_vals)):
|
|
if sh is None:
|
|
continue
|
|
|
|
if sh_min is not None:
|
|
hint[i] = sh_min * stretch_ratio # set to min size
|
|
else:
|
|
hint[i] = 0. # everything else is zero
|
|
return
|
|
|
|
# these dicts take i (widget child) as key
|
|
not_mined_contrib = {} # all who's sh > min_sh or had no min_sh
|
|
not_maxed_contrib = {} # all who's sh < max_sh or had no max_sh
|
|
sh_mins_avail = {} # the sh amt removable until we hit sh_min
|
|
sh_maxs_avail = {} # the sh amt addable until we hit sh_max
|
|
oversize_amt = undersize_amt = 0
|
|
hint_orig = hint[:]
|
|
|
|
# first, for all the items, set them to be within their max/min
|
|
# size_hint bound, also find how much their size_hint can be reduced
|
|
# or increased
|
|
for i, (sh, sh_min, sh_max) in enumerate(
|
|
zip(hint, sh_min_vals, sh_max_vals)):
|
|
if sh is None:
|
|
continue
|
|
|
|
diff = 0
|
|
|
|
if sh_min is not None:
|
|
sh_min *= stretch_ratio
|
|
diff = sh_min - sh # how much we are under the min
|
|
|
|
if diff > 0:
|
|
hint[i] = sh_min
|
|
undersize_amt += diff
|
|
else:
|
|
not_mined_contrib[i] = None
|
|
|
|
sh_mins_avail[i] = hint[i] - sh_min
|
|
else:
|
|
not_mined_contrib[i] = None
|
|
sh_mins_avail[i] = hint[i]
|
|
|
|
if sh_max is not None:
|
|
sh_max *= stretch_ratio
|
|
diff = sh - sh_max
|
|
|
|
if diff > 0:
|
|
hint[i] = sh_max # how much we are over the max
|
|
oversize_amt += diff
|
|
else:
|
|
not_maxed_contrib[i] = None
|
|
|
|
sh_maxs_avail[i] = sh_max - hint[i]
|
|
else:
|
|
not_maxed_contrib[i] = None
|
|
sh_maxs_avail[i] = sh_sum - hint[i]
|
|
|
|
if i in not_mined_contrib:
|
|
not_mined_contrib[i] = max(0., diff) # how much got removed
|
|
if i in not_maxed_contrib:
|
|
not_maxed_contrib[i] = max(0., diff) # how much got added
|
|
|
|
# if margin is zero, the amount of the widgets that were made smaller
|
|
# magically equals the amount of the widgets that were made larger
|
|
# so we're all good
|
|
margin = oversize_amt - undersize_amt
|
|
if isclose(oversize_amt, undersize_amt, abs_tol=1e-15):
|
|
return
|
|
|
|
# we need to redistribute the margin among all widgets
|
|
# if margin is positive, then we have extra space because the widgets
|
|
# that were larger and were reduced contributed more, so increase
|
|
# the size hint for those that are allowed to be larger by the
|
|
# most allowed, proportionately to their size (or inverse size hint).
|
|
# similarly for the opposite case
|
|
if margin > 1e-15:
|
|
contrib_amt = not_maxed_contrib
|
|
sh_available = sh_maxs_avail
|
|
mult = 1.
|
|
contrib_proportion = hint_orig
|
|
elif margin < -1e-15:
|
|
margin *= -1.
|
|
contrib_amt = not_mined_contrib
|
|
sh_available = sh_mins_avail
|
|
mult = -1.
|
|
|
|
# when reducing the size of widgets proportionately, those with
|
|
# larger sh get reduced less, and those with smaller, more.
|
|
mn = min((h for h in hint_orig if h))
|
|
mx = max((h for h in hint_orig if h is not None))
|
|
hint_top = (2. * mn if mn else 1.) if mn == mx else mn + mx
|
|
contrib_proportion = [None if h is None else hint_top - h for
|
|
h in hint_orig]
|
|
|
|
# contrib_amt is all the widgets that are not their max/min and
|
|
# can afford to be made bigger/smaller
|
|
# We only use the contrib_amt indices from now on
|
|
contrib_prop_sum = float(
|
|
sum((contrib_proportion[i] for i in contrib_amt)))
|
|
|
|
if contrib_prop_sum < 1e-9:
|
|
assert mult == 1. # should only happen when all sh are zero
|
|
return
|
|
|
|
contrib_height = {
|
|
i: val / (contrib_proportion[i] / contrib_prop_sum) for
|
|
i, val in contrib_amt.items()}
|
|
items = sorted(
|
|
(i for i in contrib_amt),
|
|
key=lambda x: contrib_height[x])
|
|
|
|
j = items[0]
|
|
sum_i_contributed = contrib_amt[j]
|
|
last_height = contrib_height[j]
|
|
sh_available_i = {j: sh_available[j]}
|
|
contrib_prop_sum_i = contrib_proportion[j]
|
|
|
|
n = len(items) # check when n <= 1
|
|
i = 1
|
|
if 1 < n:
|
|
j = items[1]
|
|
curr_height = contrib_height[j]
|
|
|
|
done = False
|
|
while not done and i < n:
|
|
while i < n and last_height == curr_height:
|
|
j = items[i]
|
|
sum_i_contributed += contrib_amt[j]
|
|
contrib_prop_sum_i += contrib_proportion[j]
|
|
sh_available_i[j] = sh_available[j]
|
|
curr_height = contrib_height[j]
|
|
i += 1
|
|
last_height = curr_height
|
|
|
|
while not done:
|
|
margin_height = ((margin + sum_i_contributed) /
|
|
(contrib_prop_sum_i / contrib_prop_sum))
|
|
if margin_height - curr_height > 1e-9 and i < n:
|
|
break
|
|
|
|
done = True
|
|
for k, available_sh in list(sh_available_i.items()):
|
|
if margin_height - available_sh / (
|
|
contrib_proportion[k] / contrib_prop_sum) > 1e-9:
|
|
del sh_available_i[k]
|
|
sum_i_contributed -= contrib_amt[k]
|
|
contrib_prop_sum_i -= contrib_proportion[k]
|
|
margin -= available_sh
|
|
hint[k] += mult * available_sh
|
|
done = False
|
|
|
|
if not sh_available_i: # all were under the margin
|
|
break
|
|
|
|
if sh_available_i:
|
|
assert contrib_prop_sum_i and margin
|
|
margin_height = ((margin + sum_i_contributed) /
|
|
(contrib_prop_sum_i / contrib_prop_sum))
|
|
for i in sh_available_i:
|
|
hint[i] += mult * (
|
|
margin_height * contrib_proportion[i] / contrib_prop_sum -
|
|
contrib_amt[i])
|