630 lines
22 KiB
Python
630 lines
22 KiB
Python
'''
|
|
Grid Layout
|
|
===========
|
|
|
|
.. only:: html
|
|
|
|
.. image:: images/gridlayout.gif
|
|
:align: right
|
|
|
|
.. only:: latex
|
|
|
|
.. image:: images/gridlayout.png
|
|
:align: right
|
|
|
|
.. versionadded:: 1.0.4
|
|
|
|
The :class:`GridLayout` arranges children in a matrix. It takes the available
|
|
space and divides it into columns and rows, then adds widgets to the resulting
|
|
"cells".
|
|
|
|
.. versionchanged:: 1.0.7
|
|
The implementation has changed to use the widget size_hint for calculating
|
|
column/row sizes. `uniform_width` and `uniform_height` have been removed
|
|
and other properties have added to give you more control.
|
|
|
|
Background
|
|
----------
|
|
|
|
Unlike many other toolkits, you cannot explicitly place a widget in a specific
|
|
column/row. Each child is automatically assigned a position determined by the
|
|
layout configuration and the child's index in the children list.
|
|
|
|
A GridLayout must always have at least one input constraint:
|
|
:attr:`GridLayout.cols` or :attr:`GridLayout.rows`. If you do not specify cols
|
|
or rows, the Layout will throw an exception.
|
|
|
|
Column Width and Row Height
|
|
---------------------------
|
|
|
|
The column width/row height are determined in 3 steps:
|
|
|
|
- The initial size is given by the :attr:`col_default_width` and
|
|
:attr:`row_default_height` properties. To customize the size of a single
|
|
column or row, use :attr:`cols_minimum` or :attr:`rows_minimum`.
|
|
- The `size_hint_x`/`size_hint_y` of the children are taken into account.
|
|
If no widgets have a size hint, the maximum size is used for all
|
|
children.
|
|
- You can force the default size by setting the :attr:`col_force_default`
|
|
or :attr:`row_force_default` property. This will force the layout to
|
|
ignore the `width` and `size_hint` properties of children and use the
|
|
default size.
|
|
|
|
Using a GridLayout
|
|
------------------
|
|
|
|
In the example below, all widgets will have an equal size. By default, the
|
|
`size_hint` is (1, 1), so a Widget will take the full size of the parent::
|
|
|
|
layout = GridLayout(cols=2)
|
|
layout.add_widget(Button(text='Hello 1'))
|
|
layout.add_widget(Button(text='World 1'))
|
|
layout.add_widget(Button(text='Hello 2'))
|
|
layout.add_widget(Button(text='World 2'))
|
|
|
|
.. image:: images/gridlayout_1.jpg
|
|
|
|
Now, let's fix the size of Hello buttons to 100px instead of using
|
|
size_hint_x=1::
|
|
|
|
layout = GridLayout(cols=2)
|
|
layout.add_widget(Button(text='Hello 1', size_hint_x=None, width=100))
|
|
layout.add_widget(Button(text='World 1'))
|
|
layout.add_widget(Button(text='Hello 2', size_hint_x=None, width=100))
|
|
layout.add_widget(Button(text='World 2'))
|
|
|
|
.. image:: images/gridlayout_2.jpg
|
|
|
|
Next, let's fix the row height to a specific size::
|
|
|
|
layout = GridLayout(cols=2, row_force_default=True, row_default_height=40)
|
|
layout.add_widget(Button(text='Hello 1', size_hint_x=None, width=100))
|
|
layout.add_widget(Button(text='World 1'))
|
|
layout.add_widget(Button(text='Hello 2', size_hint_x=None, width=100))
|
|
layout.add_widget(Button(text='World 2'))
|
|
|
|
.. image:: images/gridlayout_3.jpg
|
|
|
|
'''
|
|
|
|
__all__ = ('GridLayout', 'GridLayoutException')
|
|
|
|
from kivy.logger import Logger
|
|
from kivy.uix.layout import Layout
|
|
from kivy.properties import NumericProperty, BooleanProperty, DictProperty, \
|
|
BoundedNumericProperty, ReferenceListProperty, VariableListProperty, \
|
|
ObjectProperty, StringProperty, OptionProperty
|
|
from math import ceil
|
|
from itertools import accumulate, product, chain, islice
|
|
from operator import sub
|
|
|
|
|
|
def nmax(*args):
|
|
# merge into one list
|
|
args = [x for x in args if x is not None]
|
|
return max(args)
|
|
|
|
|
|
def nmin(*args):
|
|
# merge into one list
|
|
args = [x for x in args if x is not None]
|
|
return min(args)
|
|
|
|
|
|
class GridLayoutException(Exception):
|
|
'''Exception for errors if the grid layout manipulation fails.
|
|
'''
|
|
pass
|
|
|
|
|
|
class GridLayout(Layout):
|
|
'''Grid 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 one 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 its children: [padding_left,
|
|
padding_top, padding_right, padding_bottom].
|
|
|
|
padding also accepts a two argument form [padding_horizontal,
|
|
padding_vertical] and a one argument form [padding].
|
|
|
|
.. versionchanged:: 1.7.0
|
|
Replaced NumericProperty with VariableListProperty.
|
|
|
|
:attr:`padding` is a :class:`~kivy.properties.VariableListProperty` and
|
|
defaults to [0, 0, 0, 0].
|
|
'''
|
|
|
|
cols = BoundedNumericProperty(None, min=0, allownone=True)
|
|
'''Number of columns in the grid.
|
|
|
|
.. versionchanged:: 1.0.8
|
|
Changed from a NumericProperty to BoundedNumericProperty. You can no
|
|
longer set this to a negative value.
|
|
|
|
:attr:`cols` is a :class:`~kivy.properties.NumericProperty` and defaults to
|
|
None.
|
|
'''
|
|
|
|
rows = BoundedNumericProperty(None, min=0, allownone=True)
|
|
'''Number of rows in the grid.
|
|
|
|
.. versionchanged:: 1.0.8
|
|
Changed from a NumericProperty to a BoundedNumericProperty. You can no
|
|
longer set this to a negative value.
|
|
|
|
:attr:`rows` is a :class:`~kivy.properties.NumericProperty` and defaults to
|
|
None.
|
|
'''
|
|
|
|
col_default_width = NumericProperty(0)
|
|
'''Default minimum size to use for a column.
|
|
|
|
.. versionadded:: 1.0.7
|
|
|
|
:attr:`col_default_width` is a :class:`~kivy.properties.NumericProperty`
|
|
and defaults to 0.
|
|
'''
|
|
|
|
row_default_height = NumericProperty(0)
|
|
'''Default minimum size to use for row.
|
|
|
|
.. versionadded:: 1.0.7
|
|
|
|
:attr:`row_default_height` is a :class:`~kivy.properties.NumericProperty`
|
|
and defaults to 0.
|
|
'''
|
|
|
|
col_force_default = BooleanProperty(False)
|
|
'''If True, ignore the width and size_hint_x of the child and use the
|
|
default column width.
|
|
|
|
.. versionadded:: 1.0.7
|
|
|
|
:attr:`col_force_default` is a :class:`~kivy.properties.BooleanProperty`
|
|
and defaults to False.
|
|
'''
|
|
|
|
row_force_default = BooleanProperty(False)
|
|
'''If True, ignore the height and size_hint_y of the child and use the
|
|
default row height.
|
|
|
|
.. versionadded:: 1.0.7
|
|
|
|
:attr:`row_force_default` is a :class:`~kivy.properties.BooleanProperty`
|
|
and defaults to False.
|
|
'''
|
|
|
|
cols_minimum = DictProperty({})
|
|
'''Dict of minimum width for each column. The dictionary keys are the
|
|
column numbers, e.g. 0, 1, 2...
|
|
|
|
.. versionadded:: 1.0.7
|
|
|
|
:attr:`cols_minimum` is a :class:`~kivy.properties.DictProperty` and
|
|
defaults to {}.
|
|
'''
|
|
|
|
rows_minimum = DictProperty({})
|
|
'''Dict of minimum height for each row. The dictionary keys are the
|
|
row numbers, e.g. 0, 1, 2...
|
|
|
|
.. versionadded:: 1.0.7
|
|
|
|
:attr:`rows_minimum` is a :class:`~kivy.properties.DictProperty` and
|
|
defaults to {}.
|
|
'''
|
|
|
|
minimum_width = NumericProperty(0)
|
|
'''Automatically computed minimum width needed to contain all children.
|
|
|
|
.. versionadded:: 1.0.8
|
|
|
|
:attr:`minimum_width` is a :class:`~kivy.properties.NumericProperty` and
|
|
defaults to 0. It is read only.
|
|
'''
|
|
|
|
minimum_height = NumericProperty(0)
|
|
'''Automatically computed minimum height needed to contain all children.
|
|
|
|
.. versionadded:: 1.0.8
|
|
|
|
:attr:`minimum_height` is a :class:`~kivy.properties.NumericProperty` and
|
|
defaults to 0. It is read only.
|
|
'''
|
|
|
|
minimum_size = ReferenceListProperty(minimum_width, minimum_height)
|
|
'''Automatically computed minimum size needed to contain all children.
|
|
|
|
.. versionadded:: 1.0.8
|
|
|
|
:attr:`minimum_size` is a
|
|
:class:`~kivy.properties.ReferenceListProperty` of
|
|
(:attr:`minimum_width`, :attr:`minimum_height`) properties. It is read
|
|
only.
|
|
'''
|
|
|
|
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'.
|
|
|
|
.. versionadded:: 2.0.0
|
|
|
|
.. note::
|
|
|
|
'lr' means Left to Right.
|
|
'rl' means Right to Left.
|
|
'tb' means Top to Bottom.
|
|
'bt' means Bottom to Top.
|
|
'''
|
|
|
|
def __init__(self, **kwargs):
|
|
self._cols = self._rows = None
|
|
super(GridLayout, self).__init__(**kwargs)
|
|
fbind = self.fbind
|
|
update = self._trigger_layout
|
|
fbind('col_default_width', update)
|
|
fbind('row_default_height', update)
|
|
fbind('col_force_default', update)
|
|
fbind('row_force_default', update)
|
|
fbind('cols', update)
|
|
fbind('rows', update)
|
|
fbind('parent', update)
|
|
fbind('spacing', update)
|
|
fbind('padding', update)
|
|
fbind('children', update)
|
|
fbind('size', update)
|
|
fbind('pos', update)
|
|
fbind('orientation', update)
|
|
|
|
def get_max_widgets(self):
|
|
if self.cols and self.rows:
|
|
return self.rows * self.cols
|
|
else:
|
|
return None
|
|
|
|
def on_children(self, instance, value):
|
|
# if that makes impossible to construct things with deferred method,
|
|
# migrate this test in do_layout, and/or issue a warning.
|
|
smax = self.get_max_widgets()
|
|
if smax and len(value) > smax:
|
|
raise GridLayoutException(
|
|
'Too many children in GridLayout. Increase rows/cols!')
|
|
|
|
@property
|
|
def _fills_row_first(self):
|
|
return self.orientation[0] in 'lr'
|
|
|
|
@property
|
|
def _fills_from_left_to_right(self):
|
|
return 'lr' in self.orientation
|
|
|
|
@property
|
|
def _fills_from_top_to_bottom(self):
|
|
return 'tb' in self.orientation
|
|
|
|
def _init_rows_cols_sizes(self, count):
|
|
# the goal here is to calculate the minimum size of every cols/rows
|
|
# and determine if they have stretch or not
|
|
current_cols = self.cols
|
|
current_rows = self.rows
|
|
|
|
# if no cols or rows are set, we can't calculate minimum size.
|
|
# the grid must be constrained at least on one side
|
|
if not current_cols and not current_rows:
|
|
Logger.warning('%r have no cols or rows set, '
|
|
'layout is not triggered.' % self)
|
|
return
|
|
|
|
if current_cols is None:
|
|
current_cols = int(ceil(count / float(current_rows)))
|
|
elif current_rows is None:
|
|
current_rows = int(ceil(count / float(current_cols)))
|
|
|
|
current_cols = max(1, current_cols)
|
|
current_rows = max(1, current_rows)
|
|
|
|
self._has_hint_bound_x = False
|
|
self._has_hint_bound_y = False
|
|
self._cols_min_size_none = 0. # min size from all the None hint
|
|
self._rows_min_size_none = 0. # min size from all the None hint
|
|
self._cols = cols = [self.col_default_width] * current_cols
|
|
self._cols_sh = [None] * current_cols
|
|
self._cols_sh_min = [None] * current_cols
|
|
self._cols_sh_max = [None] * current_cols
|
|
self._rows = rows = [self.row_default_height] * current_rows
|
|
self._rows_sh = [None] * current_rows
|
|
self._rows_sh_min = [None] * current_rows
|
|
self._rows_sh_max = [None] * current_rows
|
|
|
|
# update minimum size from the dicts
|
|
items = (i for i in self.cols_minimum.items() if i[0] < len(cols))
|
|
for index, value in items:
|
|
cols[index] = max(value, cols[index])
|
|
|
|
items = (i for i in self.rows_minimum.items() if i[0] < len(rows))
|
|
for index, value in items:
|
|
rows[index] = max(value, rows[index])
|
|
return True
|
|
|
|
def _fill_rows_cols_sizes(self):
|
|
cols, rows = self._cols, self._rows
|
|
cols_sh, rows_sh = self._cols_sh, self._rows_sh
|
|
cols_sh_min, rows_sh_min = self._cols_sh_min, self._rows_sh_min
|
|
cols_sh_max, rows_sh_max = self._cols_sh_max, self._rows_sh_max
|
|
|
|
# calculate minimum size for each columns and rows
|
|
has_bound_y = has_bound_x = False
|
|
idx_iter = self._create_idx_iter(len(cols), len(rows))
|
|
for child, (col, row) in zip(reversed(self.children), idx_iter):
|
|
(shw, shh), (w, h) = child.size_hint, child.size
|
|
shw_min, shh_min = child.size_hint_min
|
|
shw_max, shh_max = child.size_hint_max
|
|
|
|
# compute minimum size / maximum stretch needed
|
|
if shw is None:
|
|
cols[col] = nmax(cols[col], w)
|
|
else:
|
|
cols_sh[col] = nmax(cols_sh[col], shw)
|
|
if shw_min is not None:
|
|
has_bound_x = True
|
|
cols_sh_min[col] = nmax(cols_sh_min[col], shw_min)
|
|
if shw_max is not None:
|
|
has_bound_x = True
|
|
cols_sh_max[col] = nmin(cols_sh_max[col], shw_max)
|
|
|
|
if shh is None:
|
|
rows[row] = nmax(rows[row], h)
|
|
else:
|
|
rows_sh[row] = nmax(rows_sh[row], shh)
|
|
if shh_min is not None:
|
|
has_bound_y = True
|
|
rows_sh_min[row] = nmax(rows_sh_min[row], shh_min)
|
|
if shh_max is not None:
|
|
has_bound_y = True
|
|
rows_sh_max[row] = nmin(rows_sh_max[row], shh_max)
|
|
self._has_hint_bound_x = has_bound_x
|
|
self._has_hint_bound_y = has_bound_y
|
|
|
|
def _update_minimum_size(self):
|
|
# calculate minimum width/height needed, starting from padding +
|
|
# spacing
|
|
l, t, r, b = self.padding
|
|
spacing_x, spacing_y = self.spacing
|
|
cols, rows = self._cols, self._rows
|
|
|
|
width = l + r + spacing_x * (len(cols) - 1)
|
|
self._cols_min_size_none = sum(cols) + width
|
|
# we need to subtract for the sh_max/min the already guaranteed size
|
|
# due to having a None in the col. So sh_min gets smaller by that size
|
|
# since it's already covered. Similarly for sh_max, because if we
|
|
# already exceeded the max, the subtracted max will be zero, so
|
|
# it won't get larger
|
|
if self._has_hint_bound_x:
|
|
cols_sh_min = self._cols_sh_min
|
|
cols_sh_max = self._cols_sh_max
|
|
|
|
for i, (c, sh_min, sh_max) in enumerate(
|
|
zip(cols, cols_sh_min, cols_sh_max)):
|
|
if sh_min is not None:
|
|
width += max(c, sh_min)
|
|
cols_sh_min[i] = max(0., sh_min - c)
|
|
else:
|
|
width += c
|
|
|
|
if sh_max is not None:
|
|
cols_sh_max[i] = max(0., sh_max - c)
|
|
else:
|
|
width = self._cols_min_size_none
|
|
|
|
height = t + b + spacing_y * (len(rows) - 1)
|
|
self._rows_min_size_none = sum(rows) + height
|
|
if self._has_hint_bound_y:
|
|
rows_sh_min = self._rows_sh_min
|
|
rows_sh_max = self._rows_sh_max
|
|
|
|
for i, (r, sh_min, sh_max) in enumerate(
|
|
zip(rows, rows_sh_min, rows_sh_max)):
|
|
if sh_min is not None:
|
|
height += max(r, sh_min)
|
|
rows_sh_min[i] = max(0., sh_min - r)
|
|
else:
|
|
height += r
|
|
|
|
if sh_max is not None:
|
|
rows_sh_max[i] = max(0., sh_max - r)
|
|
else:
|
|
height = self._rows_min_size_none
|
|
|
|
# finally, set the minimum size
|
|
self.minimum_size = (width, height)
|
|
|
|
def _finalize_rows_cols_sizes(self):
|
|
selfw = self.width
|
|
selfh = self.height
|
|
|
|
# resolve size for each column
|
|
if self.col_force_default:
|
|
cols = [self.col_default_width] * len(self._cols)
|
|
for index, value in self.cols_minimum.items():
|
|
cols[index] = value
|
|
self._cols = cols
|
|
else:
|
|
cols = self._cols
|
|
cols_sh = self._cols_sh
|
|
cols_sh_min = self._cols_sh_min
|
|
cols_weight = float(sum((x for x in cols_sh if x is not None)))
|
|
stretch_w = max(0., selfw - self._cols_min_size_none)
|
|
|
|
if stretch_w > 1e-9:
|
|
if self._has_hint_bound_x:
|
|
# fix the hints to be within bounds
|
|
self.layout_hint_with_bounds(
|
|
cols_weight, stretch_w,
|
|
sum((c for c in cols_sh_min if c is not None)),
|
|
cols_sh_min, self._cols_sh_max, cols_sh)
|
|
|
|
for index, col_stretch in enumerate(cols_sh):
|
|
# if the col don't have stretch information, nothing to do
|
|
if not col_stretch:
|
|
continue
|
|
# add to the min width whatever remains from size_hint
|
|
cols[index] += stretch_w * col_stretch / cols_weight
|
|
|
|
# same algo for rows
|
|
if self.row_force_default:
|
|
rows = [self.row_default_height] * len(self._rows)
|
|
for index, value in self.rows_minimum.items():
|
|
rows[index] = value
|
|
self._rows = rows
|
|
else:
|
|
rows = self._rows
|
|
rows_sh = self._rows_sh
|
|
rows_sh_min = self._rows_sh_min
|
|
rows_weight = float(sum((x for x in rows_sh if x is not None)))
|
|
stretch_h = max(0., selfh - self._rows_min_size_none)
|
|
|
|
if stretch_h > 1e-9:
|
|
if self._has_hint_bound_y:
|
|
# fix the hints to be within bounds
|
|
self.layout_hint_with_bounds(
|
|
rows_weight, stretch_h,
|
|
sum((r for r in rows_sh_min if r is not None)),
|
|
rows_sh_min, self._rows_sh_max, rows_sh)
|
|
|
|
for index, row_stretch in enumerate(rows_sh):
|
|
# if the row don't have stretch information, nothing to do
|
|
if not row_stretch:
|
|
continue
|
|
# add to the min height whatever remains from size_hint
|
|
rows[index] += stretch_h * row_stretch / rows_weight
|
|
|
|
def _iterate_layout(self, count):
|
|
orientation = self.orientation
|
|
padding = self.padding
|
|
spacing_x, spacing_y = self.spacing
|
|
|
|
cols = self._cols
|
|
if self._fills_from_left_to_right:
|
|
x_iter = accumulate(chain(
|
|
(self.x + padding[0], ),
|
|
(
|
|
col_width + spacing_x
|
|
for col_width in islice(cols, len(cols) - 1)
|
|
),
|
|
))
|
|
else:
|
|
x_iter = accumulate(chain(
|
|
(self.right - padding[2] - cols[-1], ),
|
|
(
|
|
col_width + spacing_x
|
|
for col_width in islice(reversed(cols), 1, None)
|
|
),
|
|
), sub)
|
|
cols = reversed(cols)
|
|
|
|
rows = self._rows
|
|
if self._fills_from_top_to_bottom:
|
|
y_iter = accumulate(chain(
|
|
(self.top - padding[1] - rows[0], ),
|
|
(
|
|
row_height + spacing_y
|
|
for row_height in islice(rows, 1, None)
|
|
),
|
|
), sub)
|
|
else:
|
|
y_iter = accumulate(chain(
|
|
(self.y + padding[3], ),
|
|
(
|
|
row_height + spacing_y
|
|
for row_height in islice(reversed(rows), len(rows) - 1)
|
|
),
|
|
))
|
|
rows = reversed(rows)
|
|
|
|
if self._fills_row_first:
|
|
for i, (y, x), (row_height, col_width) in zip(
|
|
reversed(range(count)),
|
|
product(y_iter, x_iter),
|
|
product(rows, cols)):
|
|
yield i, x, y, col_width, row_height
|
|
else:
|
|
for i, (x, y), (col_width, row_height) in zip(
|
|
reversed(range(count)),
|
|
product(x_iter, y_iter),
|
|
product(cols, rows)):
|
|
yield i, x, y, col_width, row_height
|
|
|
|
def do_layout(self, *largs):
|
|
children = self.children
|
|
if not children or not self._init_rows_cols_sizes(len(children)):
|
|
l, t, r, b = self.padding
|
|
self.minimum_size = l + r, t + b
|
|
return
|
|
self._fill_rows_cols_sizes()
|
|
self._update_minimum_size()
|
|
self._finalize_rows_cols_sizes()
|
|
|
|
for i, x, y, w, h in self._iterate_layout(len(children)):
|
|
c = children[i]
|
|
c.pos = x, y
|
|
shw, shh = c.size_hint
|
|
shw_min, shh_min = c.size_hint_min
|
|
shw_max, shh_max = c.size_hint_max
|
|
|
|
if shw_min is not None:
|
|
if shw_max is not None:
|
|
w = max(min(w, shw_max), shw_min)
|
|
else:
|
|
w = max(w, shw_min)
|
|
else:
|
|
if shw_max is not None:
|
|
w = min(w, shw_max)
|
|
|
|
if shh_min is not None:
|
|
if shh_max is not None:
|
|
h = max(min(h, shh_max), shh_min)
|
|
else:
|
|
h = max(h, shh_min)
|
|
else:
|
|
if shh_max is not None:
|
|
h = min(h, shh_max)
|
|
|
|
if shw is None:
|
|
if shh is not None:
|
|
c.height = h
|
|
else:
|
|
if shh is None:
|
|
c.width = w
|
|
else:
|
|
c.size = (w, h)
|
|
|
|
def _create_idx_iter(self, n_cols, n_rows):
|
|
col_indices = range(n_cols) if self._fills_from_left_to_right \
|
|
else range(n_cols - 1, -1, -1)
|
|
row_indices = range(n_rows) if self._fills_from_top_to_bottom \
|
|
else range(n_rows - 1, -1, -1)
|
|
|
|
if self._fills_row_first:
|
|
return (
|
|
(col_index, row_index)
|
|
for row_index, col_index in product(row_indices, col_indices))
|
|
else:
|
|
return product(col_indices, row_indices)
|