mirror of https://github.com/kivy/kivy.git
Merge pull request #6741 from gottadiveintopython/add_orientation_to_GridLayout
add 'orientation'property to GridLayout
This commit is contained in:
commit
e7f232501d
|
@ -4,6 +4,7 @@ uix.gridlayout tests
|
|||
'''
|
||||
|
||||
import unittest
|
||||
import pytest
|
||||
from kivy.tests.common import GraphicUnitTest
|
||||
|
||||
from kivy.uix.gridlayout import GridLayout
|
||||
|
@ -52,5 +53,59 @@ class UixGridLayoutTest(GraphicUnitTest):
|
|||
self.render(gl)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"n_cols, n_rows, orientation, expectation", [
|
||||
(2, 3, 'lr-tb', [(0, 0), (1, 0), (0, 1), (1, 1), (0, 2), (1, 2)]),
|
||||
(2, 3, 'lr-bt', [(0, 2), (1, 2), (0, 1), (1, 1), (0, 0), (1, 0)]),
|
||||
(2, 3, 'rl-tb', [(1, 0), (0, 0), (1, 1), (0, 1), (1, 2), (0, 2)]),
|
||||
(2, 3, 'rl-bt', [(1, 2), (0, 2), (1, 1), (0, 1), (1, 0), (0, 0)]),
|
||||
(2, 3, 'tb-lr', [(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2)]),
|
||||
(2, 3, 'tb-rl', [(1, 0), (1, 1), (1, 2), (0, 0), (0, 1), (0, 2)]),
|
||||
(2, 3, 'bt-lr', [(0, 2), (0, 1), (0, 0), (1, 2), (1, 1), (1, 0)]),
|
||||
(2, 3, 'bt-rl', [(1, 2), (1, 1), (1, 0), (0, 2), (0, 1), (0, 0)]),
|
||||
]
|
||||
)
|
||||
def test_create_col_and_row_index_iter(
|
||||
n_cols, n_rows, orientation, expectation):
|
||||
from kivy.uix.gridlayout import _create_col_and_row_index_iter
|
||||
index_iter = _create_col_and_row_index_iter(n_cols, n_rows, orientation)
|
||||
assert expectation == list(index_iter)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("orientation", [
|
||||
'lr-tb', 'lr-bt', 'rl-tb', 'rl-bt',
|
||||
'tb-lr', 'tb-rl', 'bt-lr', 'bt-rl',
|
||||
])
|
||||
def test_create_col_and_row_index_iter2(orientation):
|
||||
from kivy.uix.gridlayout import _create_col_and_row_index_iter
|
||||
index_iter = _create_col_and_row_index_iter(1, 1, orientation)
|
||||
assert [(0, 0)] == list(index_iter)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"n_cols, n_rows, orientation, n_children, expectation", [
|
||||
(3, None, 'lr-tb', 4, [(0, 15), (10, 15), (20, 15), (0, 0)]),
|
||||
(3, None, 'lr-bt', 4, [(0, 0), (10, 0), (20, 0), (0, 15)]),
|
||||
(3, None, 'rl-tb', 4, [(20, 15), (10, 15), (0, 15), (20, 0)]),
|
||||
(3, None, 'rl-bt', 4, [(20, 0), (10, 0), (0, 0), (20, 15)]),
|
||||
(None, 3, 'tb-lr', 4, [(0, 20), (0, 10), (0, 0), (15, 20)]),
|
||||
(None, 3, 'tb-rl', 4, [(15, 20), (15, 10), (15, 0), (0, 20)]),
|
||||
(None, 3, 'bt-lr', 4, [(0, 0), (0, 10), (0, 20), (15, 0)]),
|
||||
(None, 3, 'bt-rl', 4, [(15, 0), (15, 10), (15, 20), (0, 0)]),
|
||||
]
|
||||
)
|
||||
def test_children_pos(n_cols, n_rows, orientation, n_children, expectation):
|
||||
from kivy.uix.widget import Widget
|
||||
from kivy.uix.gridlayout import GridLayout
|
||||
gl = GridLayout(
|
||||
cols=n_cols, rows=n_rows, orientation=orientation,
|
||||
pos=(0, 0), size=(30, 30))
|
||||
for __ in range(n_children):
|
||||
gl.add_widget(Widget())
|
||||
gl.do_layout()
|
||||
actual_layout = [tuple(c.pos) for c in reversed(gl.children)]
|
||||
assert actual_layout == expectation
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
|
@ -93,8 +93,9 @@ from kivy.logger import Logger
|
|||
from kivy.uix.layout import Layout
|
||||
from kivy.properties import NumericProperty, BooleanProperty, DictProperty, \
|
||||
BoundedNumericProperty, ReferenceListProperty, VariableListProperty, \
|
||||
ObjectProperty, StringProperty
|
||||
ObjectProperty, StringProperty, OptionProperty
|
||||
from math import ceil
|
||||
from itertools import accumulate, product
|
||||
|
||||
|
||||
def nmax(*args):
|
||||
|
@ -251,6 +252,27 @@ class GridLayout(Layout):
|
|||
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)
|
||||
|
@ -268,6 +290,7 @@ class GridLayout(Layout):
|
|||
fbind('children', update)
|
||||
fbind('size', update)
|
||||
fbind('pos', update)
|
||||
fbind('orientation', update)
|
||||
|
||||
def get_max_widgets(self):
|
||||
if self.cols and self.rows:
|
||||
|
@ -283,6 +306,10 @@ class GridLayout(Layout):
|
|||
raise GridLayoutException(
|
||||
'Too many children in GridLayout. Increase rows/cols!')
|
||||
|
||||
@property
|
||||
def _fills_row_first(self):
|
||||
return self.orientation[0] in 'lr'
|
||||
|
||||
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
|
||||
|
@ -295,9 +322,18 @@ class GridLayout(Layout):
|
|||
Logger.warning('%r have no cols or rows set, '
|
||||
'layout is not triggered.' % self)
|
||||
return
|
||||
|
||||
if current_cols is None:
|
||||
if self._fills_row_first:
|
||||
Logger.warning(
|
||||
'Being asked to fill row-first, but a number of columns '
|
||||
'is not defined. You might get an unexpected result.')
|
||||
current_cols = int(ceil(count / float(current_rows)))
|
||||
elif current_rows is None:
|
||||
if not self._fills_row_first:
|
||||
Logger.warning(
|
||||
'Being asked to fill column-first, but a number of rows '
|
||||
'is not defined. You might get an unexpected result.')
|
||||
current_rows = int(ceil(count / float(current_cols)))
|
||||
|
||||
current_cols = max(1, current_cols)
|
||||
|
@ -315,6 +351,8 @@ class GridLayout(Layout):
|
|||
self._rows_sh = [None] * current_rows
|
||||
self._rows_sh_min = [None] * current_rows
|
||||
self._rows_sh_max = [None] * current_rows
|
||||
self._col_and_row_indices = tuple(_create_col_and_row_index_iter(
|
||||
current_cols, current_rows, self.orientation))
|
||||
|
||||
# update minimum size from the dicts
|
||||
items = (i for i in self.cols_minimum.items() if i[0] < len(cols))
|
||||
|
@ -333,13 +371,12 @@ class GridLayout(Layout):
|
|||
cols_sh_max, rows_sh_max = self._cols_sh_max, self._rows_sh_max
|
||||
|
||||
# calculate minimum size for each columns and rows
|
||||
n_cols = len(cols)
|
||||
has_bound_y = has_bound_x = False
|
||||
for i, child in enumerate(reversed(self.children)):
|
||||
for child, (col, row) in zip(reversed(self.children),
|
||||
self._col_and_row_indices):
|
||||
(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
|
||||
row, col = divmod(i, n_cols)
|
||||
|
||||
# compute minimum size / maximum stretch needed
|
||||
if shw is None:
|
||||
|
@ -480,24 +517,40 @@ class GridLayout(Layout):
|
|||
rows[index] += stretch_h * row_stretch / rows_weight
|
||||
|
||||
def _iterate_layout(self, count):
|
||||
selfx = self.x
|
||||
padding_left = self.padding[0]
|
||||
padding_top = self.padding[1]
|
||||
orientation = self.orientation
|
||||
padding = self.padding
|
||||
spacing_x, spacing_y = self.spacing
|
||||
|
||||
i = count - 1
|
||||
y = self.top - padding_top
|
||||
cols = self._cols
|
||||
for row_height in self._rows:
|
||||
x = selfx + padding_left
|
||||
for col_width in cols:
|
||||
if i < 0:
|
||||
break
|
||||
x_list = list(accumulate((
|
||||
self.x + padding[0],
|
||||
*(col_width + spacing_x for col_width in cols[:-1]))))
|
||||
if 'rl' in orientation:
|
||||
cols = reversed(cols)
|
||||
x_list.reverse()
|
||||
|
||||
yield i, x, y - row_height, col_width, row_height
|
||||
i = i - 1
|
||||
x = x + col_width + spacing_x
|
||||
y -= row_height + spacing_y
|
||||
rows = self._rows
|
||||
reversed_rows = list(reversed(rows))
|
||||
y_list = list(accumulate((
|
||||
self.y + padding[3],
|
||||
*(row_height + spacing_y for row_height in reversed_rows[:-1]))))
|
||||
if 'tb' in orientation:
|
||||
y_list.reverse()
|
||||
else:
|
||||
rows = reversed_rows
|
||||
|
||||
if self._fills_row_first:
|
||||
for i, (y, x), (row_height, col_width) in zip(
|
||||
reversed(range(count)),
|
||||
product(y_list, x_list),
|
||||
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_list, y_list),
|
||||
product(cols, rows)):
|
||||
yield i, x, y, col_width, row_height
|
||||
|
||||
def do_layout(self, *largs):
|
||||
children = self.children
|
||||
|
@ -542,3 +595,19 @@ class GridLayout(Layout):
|
|||
c.width = w
|
||||
else:
|
||||
c.size = (w, h)
|
||||
|
||||
|
||||
def _create_col_and_row_index_iter(n_cols, n_rows, orientation):
|
||||
col_indices = list(range(n_cols))
|
||||
if 'rl' in orientation:
|
||||
col_indices.reverse()
|
||||
row_indices = list(range(n_rows))
|
||||
if 'bt' in orientation:
|
||||
row_indices.reverse()
|
||||
|
||||
if orientation[0] in 'rl':
|
||||
return (
|
||||
(col_index, row_index)
|
||||
for row_index, col_index in product(row_indices, col_indices))
|
||||
else:
|
||||
return product(col_indices, row_indices)
|
||||
|
|
|
@ -39,16 +39,17 @@ class RecycleGridLayout(RecycleLayout, GridLayout):
|
|||
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
|
||||
self._cols_count = cols_count = [defaultdict(int) for _ in cols]
|
||||
# !! bottom-to-top, the opposite of the other attributes.
|
||||
self._rows_count = rows_count = [defaultdict(int) for _ in rows]
|
||||
|
||||
# calculate minimum size for each columns and rows
|
||||
n_cols = len(cols)
|
||||
col_and_row_indices = self._col_and_row_indices
|
||||
has_bound_y = has_bound_x = False
|
||||
for i, opt in enumerate(self.view_opts):
|
||||
(shw, shh), (w, h) = opt['size_hint'], opt['size']
|
||||
shw_min, shh_min = opt['size_hint_min']
|
||||
shw_max, shh_max = opt['size_hint_max']
|
||||
row, col = divmod(i, n_cols)
|
||||
col, row = col_and_row_indices[i]
|
||||
|
||||
if shw is None:
|
||||
cols_count[col][w] += 1
|
||||
|
@ -84,7 +85,7 @@ class RecycleGridLayout(RecycleLayout, GridLayout):
|
|||
cols_count, rows_count = self._cols_count, self._rows_count
|
||||
cols, rows = self._cols, self._rows
|
||||
remove_view = self.remove_view
|
||||
n_cols = len(cols_count)
|
||||
col_and_row_indices = self._col_and_row_indices
|
||||
|
||||
# this can be further improved to reduce re-comp, but whatever...
|
||||
for index, widget, (w, h), (wn, hn), sh, shn, sh_min, shn_min, \
|
||||
|
@ -97,7 +98,7 @@ class RecycleGridLayout(RecycleLayout, GridLayout):
|
|||
(w == wn or sh[0] is not None)):
|
||||
remove_view(widget, index)
|
||||
else: # size hint is None, so check if it can be resized inplace
|
||||
row, col = divmod(index, n_cols)
|
||||
col, row = col_and_row_indices[index]
|
||||
|
||||
if w != wn:
|
||||
col_w = cols[col]
|
||||
|
@ -205,26 +206,35 @@ class RecycleGridLayout(RecycleLayout, GridLayout):
|
|||
break
|
||||
iy += 1
|
||||
|
||||
# gridlayout counts from left to right, top to down
|
||||
iy = len(rows) - iy - 1
|
||||
return iy * len(cols) + ix
|
||||
ori = self.orientation
|
||||
if 'rl' in ori:
|
||||
ix = len(cols) - ix - 1
|
||||
if 'tb' in ori:
|
||||
iy = len(rows) - iy - 1
|
||||
return (iy * len(cols) + ix) if self._fills_row_first else \
|
||||
(ix * len(rows) + iy)
|
||||
|
||||
def compute_visible_views(self, data, viewport):
|
||||
if self._cols_pos is None:
|
||||
return []
|
||||
x, y, w, h = viewport
|
||||
# gridlayout counts from left to right, top to down
|
||||
right = x + w
|
||||
top = y + h
|
||||
at_idx = self.get_view_index_at
|
||||
bl = at_idx((x, y))
|
||||
br = at_idx((x + w, y))
|
||||
tl = at_idx((x, y + h))
|
||||
# 'tl' is not actually 'top-left' unless 'orientation' is 'lr-tb'.
|
||||
# But we can pretend it is. Same for 'bl' and 'br'.
|
||||
tl, __, bl, br = sorted((
|
||||
at_idx((x, y)),
|
||||
at_idx((right, y)),
|
||||
at_idx((x, top)),
|
||||
at_idx((right, top)),
|
||||
))
|
||||
|
||||
n = len(data)
|
||||
|
||||
indices = []
|
||||
row = len(self._cols)
|
||||
if row:
|
||||
stride = len(self._cols) if self._fills_row_first else len(self._rows)
|
||||
if stride:
|
||||
x_slice = br - bl + 1
|
||||
for s in range(tl, bl + 1, row):
|
||||
for s in range(tl, bl + 1, stride):
|
||||
indices.extend(range(min(s, n), min(n, s + x_slice)))
|
||||
|
||||
return indices
|
||||
|
|
Loading…
Reference in New Issue