""" 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', ) 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 def do_layout(self, *largs): assert False 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) 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