"""
RecycleLayout
=============
.. versionadded:: 1.10.0
.. warning::
This module is highly experimental, its API may change in the future and
the documentation is not complete at this time.
"""
from kivy.uix.recycleview.layout import RecycleLayoutManagerBehavior
from kivy.uix.layout import Layout
from kivy.properties import (
ObjectProperty, StringProperty, ReferenceListProperty, NumericProperty
)
from kivy.factory import Factory
__all__ = ('RecycleLayout', )
[docs]class RecycleLayout(RecycleLayoutManagerBehavior, Layout):
"""
RecycleLayout provides the default layout for RecycleViews.
"""
default_width = NumericProperty(100, allownone=True)
'''Default width for items
:attr:`default_width` is a NumericProperty and default to 100
'''
default_height = NumericProperty(100, allownone=True)
'''Default height for items
:attr:`default_height` is a :class:`~kivy.properties.NumericProperty` and
default to 100.
'''
default_size = ReferenceListProperty(default_width, default_height)
'''size (width, height). Each value can be None.
:attr:`default_size` is an :class:`~kivy.properties.ReferenceListProperty`
to [:attr:`default_width`, :attr:`default_height`].
'''
default_size_hint_x = NumericProperty(None, allownone=True)
'''Default size_hint_x for items
:attr:`default_size_hint_x` is a :class:`~kivy.properties.NumericProperty`
and default to None.
'''
default_size_hint_y = NumericProperty(None, allownone=True)
'''Default size_hint_y for items
:attr:`default_size_hint_y` is a :class:`~kivy.properties.NumericProperty`
and default to None.
'''
default_size_hint = ReferenceListProperty(
default_size_hint_x, default_size_hint_y
)
'''size (width, height). Each value can be None.
:attr:`default_size_hint` is an
:class:`~kivy.properties.ReferenceListProperty` to
[:attr:`default_size_hint_x`, :attr:`default_size_hint_y`].
'''
key_size = StringProperty(None, allownone=True)
'''If set, which key in the dict should be used to set the size property of
the item.
:attr:`key_size` is a :class:`~kivy.properties.StringProperty` and defaults
to None.
'''
key_size_hint = StringProperty(None, allownone=True)
'''If set, which key in the dict should be used to set the size_hint
property of the item.
:attr:`key_size_hint` is a :class:`~kivy.properties.StringProperty` and
defaults to None.
'''
key_size_hint_min = StringProperty(None, allownone=True)
'''If set, which key in the dict should be used to set the size_hint_min
property of the item.
:attr:`key_size_hint_min` is a :class:`~kivy.properties.StringProperty` and
defaults to None.
'''
default_size_hint_x_min = NumericProperty(None, allownone=True)
'''Default value for size_hint_x_min of items
:attr:`default_pos_hint_x_min` is a
:class:`~kivy.properties.NumericProperty` and defaults to None.
'''
default_size_hint_y_min = NumericProperty(None, allownone=True)
'''Default value for size_hint_y_min of items
:attr:`default_pos_hint_y_min` is a
:class:`~kivy.properties.NumericProperty` and defaults to None.
'''
default_size_hint_min = ReferenceListProperty(
default_size_hint_x_min,
default_size_hint_y_min
)
'''Default value for size_hint_min of items
:attr:`default_size_min` is a
:class:`~kivy.properties.ReferenceListProperty` to
[:attr:`default_size_hint_x_min`, :attr:`default_size_hint_y_min`].
'''
key_size_hint_max = StringProperty(None, allownone=True)
'''If set, which key in the dict should be used to set the size_hint_max
property of the item.
:attr:`key_size_hint_max` is a :class:`~kivy.properties.StringProperty` and
defaults to None.
'''
default_size_hint_x_max = NumericProperty(None, allownone=True)
'''Default value for size_hint_x_max of items
:attr:`default_pos_hint_x_max` is a
:class:`~kivy.properties.NumericProperty` and defaults to None.
'''
default_size_hint_y_max = NumericProperty(None, allownone=True)
'''Default value for size_hint_y_max of items
:attr:`default_pos_hint_y_max` is a
:class:`~kivy.properties.NumericProperty` and defaults to None.
'''
default_size_hint_max = ReferenceListProperty(
default_size_hint_x_max,
default_size_hint_y_max
)
'''Default value for size_hint_max of items
:attr:`default_size_max` is a
:class:`~kivy.properties.ReferenceListProperty` to
[:attr:`default_size_hint_x_max`, :attr:`default_size_hint_y_max`].
'''
default_pos_hint = ObjectProperty({})
'''Default pos_hint value for items
:attr:`default_pos_hint` is a :class:`~kivy.properties.DictProperty` and
defaults to {}.
'''
key_pos_hint = StringProperty(None, allownone=True)
'''If set, which key in the dict should be used to set the pos_hint of
items.
:attr:`key_pos_hint` is a :class:`~kivy.properties.StringProperty` and
defaults to None.
'''
initial_width = NumericProperty(100)
'''Initial width for the items.
:attr:`initial_width` is a :class:`~kivy.properties.NumericProperty` and
defaults to 100.
'''
initial_height = NumericProperty(100)
'''Initial height for the items.
:attr:`initial_height` is a :class:`~kivy.properties.NumericProperty` and
defaults to 100.
'''
initial_size = ReferenceListProperty(initial_width, initial_height)
'''Initial size of items
:attr:`initial_size` is a :class:`~kivy.properties.ReferenceListProperty`
to [:attr:`initial_width`, :attr:`initial_height`].
'''
view_opts = []
_size_needs_update = False
_changed_views = []
view_indices = {}
def __init__(self, **kwargs):
self.view_indices = {}
self._updated_views = []
self._trigger_layout = self._catch_layout_trigger
super(RecycleLayout, self).__init__(**kwargs)
def attach_recycleview(self, rv):
super(RecycleLayout, self).attach_recycleview(rv)
if rv:
fbind = self.fbind
fbind('default_size', rv.refresh_from_data)
fbind('key_size', rv.refresh_from_data)
fbind('default_size_hint', rv.refresh_from_data)
fbind('key_size_hint', rv.refresh_from_data)
fbind('default_size_hint_min', rv.refresh_from_data)
fbind('key_size_hint_min', rv.refresh_from_data)
fbind('default_size_hint_max', rv.refresh_from_data)
fbind('key_size_hint_max', rv.refresh_from_data)
fbind('default_pos_hint', rv.refresh_from_data)
fbind('key_pos_hint', rv.refresh_from_data)
def detach_recycleview(self):
rv = self.recycleview
if rv:
funbind = self.funbind
funbind('default_size', rv.refresh_from_data)
funbind('key_size', rv.refresh_from_data)
funbind('default_size_hint', rv.refresh_from_data)
funbind('key_size_hint', rv.refresh_from_data)
funbind('default_size_hint_min', rv.refresh_from_data)
funbind('key_size_hint_min', rv.refresh_from_data)
funbind('default_size_hint_max', rv.refresh_from_data)
funbind('key_size_hint_max', rv.refresh_from_data)
funbind('default_pos_hint', rv.refresh_from_data)
funbind('key_pos_hint', rv.refresh_from_data)
super(RecycleLayout, self).detach_recycleview()
def _catch_layout_trigger(self, instance=None, value=None):
rv = self.recycleview
if rv is None:
return
idx = self.view_indices.get(instance)
if idx is not None:
if self._size_needs_update:
return
opt = self.view_opts[idx]
if (instance.size == opt['size'] and
instance.size_hint == opt['size_hint'] and
instance.size_hint_min == opt['size_hint_min'] and
instance.size_hint_max == opt['size_hint_max'] and
instance.pos_hint == opt['pos_hint']):
return
self._size_needs_update = True
rv.refresh_from_layout(view_size=True)
else:
rv.refresh_from_layout()
def compute_sizes_from_data(self, data, flags):
if [f for f in flags if not f]:
# at least one changed data unpredictably
self.clear_layout()
opts = self.view_opts = [None for _ in data]
else:
opts = self.view_opts
changed = False
for flag in flags:
for k, v in flag.items():
changed = True
if k == 'removed':
del opts[v]
elif k == 'appended':
opts.extend([None, ] * (v.stop - v.start))
elif k == 'inserted':
opts.insert(v, None)
elif k == 'modified':
start, stop, step = v.start, v.stop, v.step
r = range(start, stop) if step is None else \
range(start, stop, step)
for i in r:
opts[i] = None
else:
raise Exception('Unrecognized data flag {}'.format(k))
if changed:
self.clear_layout()
assert len(data) == len(opts)
ph_key = self.key_pos_hint
ph_def = self.default_pos_hint
sh_key = self.key_size_hint
sh_def = self.default_size_hint
sh_min_key = self.key_size_hint_min
sh_min_def = self.default_size_hint_min
sh_max_key = self.key_size_hint_max
sh_max_def = self.default_size_hint_max
s_key = self.key_size
s_def = self.default_size
viewcls_def = self.viewclass
viewcls_key = self.key_viewclass
iw, ih = self.initial_size
sh = []
for i, item in enumerate(data):
if opts[i] is not None:
continue
ph = ph_def if ph_key is None else item.get(ph_key, ph_def)
ph = item.get('pos_hint', ph)
sh = sh_def if sh_key is None else item.get(sh_key, sh_def)
sh = item.get('size_hint', sh)
sh = [item.get('size_hint_x', sh[0]),
item.get('size_hint_y', sh[1])]
sh_min = sh_min_def if sh_min_key is None else item.get(sh_min_key,
sh_min_def)
sh_min = item.get('size_hint_min', sh_min)
sh_min = [item.get('size_hint_min_x', sh_min[0]),
item.get('size_hint_min_y', sh_min[1])]
sh_max = sh_max_def if sh_max_key is None else item.get(sh_max_key,
sh_max_def)
sh_max = item.get('size_hint_max', sh_max)
sh_max = [item.get('size_hint_max_x', sh_max[0]),
item.get('size_hint_max_y', sh_max[1])]
s = s_def if s_key is None else item.get(s_key, s_def)
s = item.get('size', s)
w, h = s = item.get('width', s[0]), item.get('height', s[1])
viewcls = None
if viewcls_key is not None:
viewcls = item.get(viewcls_key)
if viewcls is not None:
viewcls = getattr(Factory, viewcls)
if viewcls is None:
viewcls = viewcls_def
opts[i] = {
'size': [(iw if w is None else w), (ih if h is None else h)],
'size_hint': sh, 'size_hint_min': sh_min,
'size_hint_max': sh_max, 'pos': None, 'pos_hint': ph,
'viewclass': viewcls, 'width_none': w is None,
'height_none': h is None}
def compute_layout(self, data, flags):
self._size_needs_update = False
opts = self.view_opts
changed = []
for widget, index in self.view_indices.items():
opt = opts[index]
s = opt['size']
w, h = sn = list(widget.size)
sh = opt['size_hint']
shnw, shnh = shn = list(widget.size_hint)
sh_min = opt['size_hint_min']
shn_min = list(widget.size_hint_min)
sh_max = opt['size_hint_max']
shn_max = list(widget.size_hint_max)
ph = opt['pos_hint']
phn = dict(widget.pos_hint)
if s != sn or sh != shn or ph != phn or sh_min != shn_min or \
sh_max != shn_max:
changed.append((index, widget, s, sn, sh, shn, sh_min, shn_min,
sh_max, shn_max, ph, phn))
if shnw is None:
if shnh is None:
opt['size'] = sn
else:
opt['size'] = [w, s[1]]
elif shnh is None:
opt['size'] = [s[0], h]
opt['size_hint'] = shn
opt['size_hint_min'] = shn_min
opt['size_hint_max'] = shn_max
opt['pos_hint'] = phn
if [f for f in flags if not f]: # need to redo everything
self._changed_views = []
else:
self._changed_views = changed if changed else None
[docs] def do_layout(self, *largs):
assert False
[docs] def set_visible_views(self, indices, data, viewport):
view_opts = self.view_opts
new, remaining, old = self.recycleview.view_adapter.set_visible_views(
indices, data, view_opts)
remove = self.remove_widget
view_indices = self.view_indices
for _, widget in old:
remove(widget)
del view_indices[widget]
# first update the sizing info so that when we update the size
# the widgets are not bound and won't trigger a re-layout
refresh_view_layout = self.refresh_view_layout
for index, widget in new:
# make sure widget is added first so that any sizing updates
# will be recorded
opt = view_opts[index].copy()
del opt['width_none']
del opt['height_none']
refresh_view_layout(index, opt, widget, viewport)
# then add all the visible widgets, which binds size/size_hint
add = self.add_widget
for index, widget in new:
# add to the container if it's not already done
view_indices[widget] = index
if widget.parent is None:
add(widget)
# finally, make sure if the size has changed to cause a re-layout
changed = False
for index, widget in new:
opt = view_opts[index]
if (changed or widget.size == opt['size'] and
widget.size_hint == opt['size_hint'] and
widget.size_hint_min == opt['size_hint_min'] and
widget.size_hint_max == opt['size_hint_max'] and
widget.pos_hint == opt['pos_hint']):
continue
changed = True
if changed:
# we could use LayoutChangeException here, but refresh_views in rv
# needs to be updated to watch for it in the layout phase
self._size_needs_update = True
self.recycleview.refresh_from_layout(view_size=True)
[docs] def refresh_view_layout(self, index, layout, view, viewport):
opt = self.view_opts[index].copy()
width_none = opt.pop('width_none')
height_none = opt.pop('height_none')
opt.update(layout)
w, h = opt['size']
shw, shh = opt['size_hint']
if shw is None and width_none:
w = None
if shh is None and height_none:
h = None
opt['size'] = w, h
super(RecycleLayout, self).refresh_view_layout(
index, opt, view, viewport)
def remove_views(self):
super(RecycleLayout, self).remove_views()
self.clear_widgets()
self.view_indices = {}
def remove_view(self, view, index):
super(RecycleLayout, self).remove_view(view, index)
self.remove_widget(view)
del self.view_indices[view]
def clear_layout(self):
super(RecycleLayout, self).clear_layout()
self.clear_widgets()
self.view_indices = {}
self._size_needs_update = False