first commit
This commit is contained in:
commit
417e54da96
5696 changed files with 900003 additions and 0 deletions
|
@ -0,0 +1,624 @@
|
|||
"""
|
||||
RecycleView
|
||||
===========
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
|
||||
The RecycleView provides a flexible model for viewing selected sections of
|
||||
large data sets. It aims to prevent the performance degradation that can occur
|
||||
when generating large numbers of widgets in order to display many data items.
|
||||
|
||||
.. warning::
|
||||
|
||||
Because :class:`RecycleView` reuses widgets, any state change to a single
|
||||
widget will stay with that widget as it's reused, even if the
|
||||
:attr:`~RecycleView.data` assigned to it by the :class:`RecycleView`
|
||||
changes, unless the complete state is tracked in :attr:`~RecycleView.data`
|
||||
(see below).
|
||||
|
||||
The view is generated by processing the :attr:`~RecycleView.data`, essentially
|
||||
a list of dicts, and uses these dicts to generate instances of the
|
||||
:attr:`~RecycleView.viewclass` as required. Its design is based on the
|
||||
MVC (`Model-View-Controller
|
||||
<https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller>`_)
|
||||
pattern.
|
||||
|
||||
* Model: The model is formed by :attr:`~RecycleView.data` you pass in via a
|
||||
list of dicts.
|
||||
* View: The View is split across layout and views and implemented using
|
||||
adapters.
|
||||
* Controller: The controller determines the logical interaction and is
|
||||
implemented by :class:`RecycleViewBehavior`.
|
||||
|
||||
These are abstract classes and cannot be used directly. The default concrete
|
||||
implementations are the
|
||||
:class:`~kivy.uix.recycleview.datamodel.RecycleDataModel` for the model, the
|
||||
:class:`~kivy.uix.recyclelayout.RecycleLayout` for the view, and the
|
||||
:class:`RecycleView` for the controller.
|
||||
|
||||
When a RecycleView is instantiated, it automatically creates the views and data
|
||||
classes. However, one must manually create the layout classes and add them to
|
||||
the RecycleView.
|
||||
|
||||
A layout manager is automatically created as a
|
||||
:attr:`~RecycleViewBehavior.layout_manager` when added as the child of the
|
||||
RecycleView. Similarly when removed. A requirement is that the layout manager
|
||||
must be contained as a child somewhere within the RecycleView's widget tree so
|
||||
the view port can be found.
|
||||
|
||||
A minimal example might look something like this::
|
||||
|
||||
from kivy.app import App
|
||||
from kivy.lang import Builder
|
||||
from kivy.uix.recycleview import RecycleView
|
||||
|
||||
|
||||
Builder.load_string('''
|
||||
<RV>:
|
||||
viewclass: 'Label'
|
||||
RecycleBoxLayout:
|
||||
default_size: None, dp(56)
|
||||
default_size_hint: 1, None
|
||||
size_hint_y: None
|
||||
height: self.minimum_height
|
||||
orientation: 'vertical'
|
||||
''')
|
||||
|
||||
class RV(RecycleView):
|
||||
def __init__(self, **kwargs):
|
||||
super(RV, self).__init__(**kwargs)
|
||||
self.data = [{'text': str(x)} for x in range(100)]
|
||||
|
||||
|
||||
class TestApp(App):
|
||||
def build(self):
|
||||
return RV()
|
||||
|
||||
if __name__ == '__main__':
|
||||
TestApp().run()
|
||||
|
||||
In order to support selection in the view, you can add the required behaviors
|
||||
as follows::
|
||||
|
||||
from kivy.app import App
|
||||
from kivy.lang import Builder
|
||||
from kivy.uix.recycleview import RecycleView
|
||||
from kivy.uix.recycleview.views import RecycleDataViewBehavior
|
||||
from kivy.uix.label import Label
|
||||
from kivy.properties import BooleanProperty
|
||||
from kivy.uix.recycleboxlayout import RecycleBoxLayout
|
||||
from kivy.uix.behaviors import FocusBehavior
|
||||
from kivy.uix.recycleview.layout import LayoutSelectionBehavior
|
||||
|
||||
Builder.load_string('''
|
||||
<SelectableLabel>:
|
||||
# Draw a background to indicate selection
|
||||
canvas.before:
|
||||
Color:
|
||||
rgba: (.0, 0.9, .1, .3) if self.selected else (0, 0, 0, 1)
|
||||
Rectangle:
|
||||
pos: self.pos
|
||||
size: self.size
|
||||
<RV>:
|
||||
viewclass: 'SelectableLabel'
|
||||
SelectableRecycleBoxLayout:
|
||||
default_size: None, dp(56)
|
||||
default_size_hint: 1, None
|
||||
size_hint_y: None
|
||||
height: self.minimum_height
|
||||
orientation: 'vertical'
|
||||
multiselect: True
|
||||
touch_multiselect: True
|
||||
''')
|
||||
|
||||
|
||||
class SelectableRecycleBoxLayout(FocusBehavior, LayoutSelectionBehavior,
|
||||
RecycleBoxLayout):
|
||||
''' Adds selection and focus behavior to the view. '''
|
||||
|
||||
|
||||
class SelectableLabel(RecycleDataViewBehavior, Label):
|
||||
''' Add selection support to the Label '''
|
||||
index = None
|
||||
selected = BooleanProperty(False)
|
||||
selectable = BooleanProperty(True)
|
||||
|
||||
def refresh_view_attrs(self, rv, index, data):
|
||||
''' Catch and handle the view changes '''
|
||||
self.index = index
|
||||
return super(SelectableLabel, self).refresh_view_attrs(
|
||||
rv, index, data)
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
''' Add selection on touch down '''
|
||||
if super(SelectableLabel, self).on_touch_down(touch):
|
||||
return True
|
||||
if self.collide_point(*touch.pos) and self.selectable:
|
||||
return self.parent.select_with_touch(self.index, touch)
|
||||
|
||||
def apply_selection(self, rv, index, is_selected):
|
||||
''' Respond to the selection of items in the view. '''
|
||||
self.selected = is_selected
|
||||
if is_selected:
|
||||
print("selection changed to {0}".format(rv.data[index]))
|
||||
else:
|
||||
print("selection removed for {0}".format(rv.data[index]))
|
||||
|
||||
|
||||
class RV(RecycleView):
|
||||
def __init__(self, **kwargs):
|
||||
super(RV, self).__init__(**kwargs)
|
||||
self.data = [{'text': str(x)} for x in range(100)]
|
||||
|
||||
|
||||
class TestApp(App):
|
||||
def build(self):
|
||||
return RV()
|
||||
|
||||
if __name__ == '__main__':
|
||||
TestApp().run()
|
||||
|
||||
|
||||
|
||||
Please see the `examples/widgets/recycleview/basic_data.py` file for a more
|
||||
complete example.
|
||||
|
||||
Viewclass State
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
Because the viewclass widgets are reused or instantiated as needed by the
|
||||
:class:`RecycleView`, the order and content of the widgets are mutable. So any
|
||||
state change to a single widget will stay with that widget, even when the data
|
||||
assigned to it from the :attr:`~RecycleView.data` dict changes, unless
|
||||
:attr:`~RecycleView.data` tracks those changes or they are manually refreshed
|
||||
when re-used.
|
||||
|
||||
There are two methods for managing state changes in viewclass widgets:
|
||||
|
||||
1. Store state in the RecycleView.data Model
|
||||
2. Generate state changes on-the-fly by catching :attr:`~RecycleView.data`
|
||||
updates and manually refreshing.
|
||||
|
||||
An example::
|
||||
|
||||
from kivy.app import App
|
||||
from kivy.lang import Builder
|
||||
from kivy.uix.boxlayout import BoxLayout
|
||||
from kivy.uix.recycleview import RecycleView
|
||||
from kivy.uix.recycleview.views import RecycleDataViewBehavior
|
||||
from kivy.properties import BooleanProperty, StringProperty
|
||||
|
||||
Builder.load_string('''
|
||||
<StatefulLabel>:
|
||||
active: stored_state.active
|
||||
CheckBox:
|
||||
id: stored_state
|
||||
active: root.active
|
||||
on_release: root.store_checkbox_state()
|
||||
Label:
|
||||
text: root.text
|
||||
Label:
|
||||
id: generate_state
|
||||
text: root.generated_state_text
|
||||
|
||||
<RV>:
|
||||
viewclass: 'StatefulLabel'
|
||||
RecycleBoxLayout:
|
||||
size_hint_y: None
|
||||
height: self.minimum_height
|
||||
orientation: 'vertical'
|
||||
''')
|
||||
|
||||
class StatefulLabel(RecycleDataViewBehavior, BoxLayout):
|
||||
text = StringProperty()
|
||||
generated_state_text = StringProperty()
|
||||
active = BooleanProperty()
|
||||
index = 0
|
||||
|
||||
'''
|
||||
To change a viewclass' state as the data assigned to it changes,
|
||||
overload the refresh_view_attrs function (inherited from
|
||||
RecycleDataViewBehavior)
|
||||
'''
|
||||
def refresh_view_attrs(self, rv, index, data):
|
||||
self.index = index
|
||||
if data['text'] == '0':
|
||||
self.generated_state_text = "is zero"
|
||||
elif int(data['text']) % 2 == 1:
|
||||
self.generated_state_text = "is odd"
|
||||
else:
|
||||
self.generated_state_text = "is even"
|
||||
super(StatefulLabel, self).refresh_view_attrs(rv, index, data)
|
||||
|
||||
'''
|
||||
To keep state changes in the viewclass with associated data,
|
||||
they can be explicitly stored in the RecycleView's data object
|
||||
'''
|
||||
def store_checkbox_state(self):
|
||||
rv = App.get_running_app().rv
|
||||
rv.data[self.index]['active'] = self.active
|
||||
|
||||
class RV(RecycleView, App):
|
||||
def __init__(self, **kwargs):
|
||||
super(RV, self).__init__(**kwargs)
|
||||
self.data = [{'text': str(x), 'active': False} for x in range(10)]
|
||||
App.get_running_app().rv = self
|
||||
|
||||
def build(self):
|
||||
return self
|
||||
|
||||
if __name__ == '__main__':
|
||||
RV().run()
|
||||
|
||||
TODO:
|
||||
- Method to clear cached class instances.
|
||||
- Test when views cannot be found (e.g. viewclass is None).
|
||||
- Fix selection goto.
|
||||
|
||||
.. warning::
|
||||
When views are re-used they may not trigger if the data remains the same.
|
||||
"""
|
||||
|
||||
__all__ = ('RecycleViewBehavior', 'RecycleView')
|
||||
|
||||
from copy import deepcopy
|
||||
|
||||
from kivy.uix.scrollview import ScrollView
|
||||
from kivy.properties import AliasProperty
|
||||
from kivy.clock import Clock
|
||||
|
||||
from kivy.uix.recycleview.layout import RecycleLayoutManagerBehavior, \
|
||||
LayoutChangeException
|
||||
from kivy.uix.recycleview.views import RecycleDataAdapter
|
||||
from kivy.uix.recycleview.datamodel import RecycleDataModelBehavior, \
|
||||
RecycleDataModel
|
||||
|
||||
|
||||
class RecycleViewBehavior(object):
|
||||
"""RecycleViewBehavior provides a behavioral model upon which the
|
||||
:class:`RecycleView` is built. Together, they offer an extensible and
|
||||
flexible way to produce views with limited windows over large data sets.
|
||||
|
||||
See the module documentation for more information.
|
||||
"""
|
||||
|
||||
# internals
|
||||
_view_adapter = None
|
||||
_data_model = None
|
||||
_layout_manager = None
|
||||
|
||||
_refresh_flags = {'data': [], 'layout': [], 'viewport': False}
|
||||
_refresh_trigger = None
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._refresh_trigger = Clock.create_trigger(self.refresh_views, -1)
|
||||
self._refresh_flags = deepcopy(self._refresh_flags)
|
||||
super(RecycleViewBehavior, self).__init__(**kwargs)
|
||||
|
||||
def get_viewport(self):
|
||||
pass
|
||||
|
||||
def save_viewport(self):
|
||||
pass
|
||||
|
||||
def restore_viewport(self):
|
||||
pass
|
||||
|
||||
def refresh_views(self, *largs):
|
||||
lm = self.layout_manager
|
||||
flags = self._refresh_flags
|
||||
if lm is None or self.view_adapter is None or self.data_model is None:
|
||||
return
|
||||
|
||||
data = self.data
|
||||
f = flags['data']
|
||||
if f:
|
||||
self.save_viewport()
|
||||
# lm.clear_layout()
|
||||
flags['data'] = []
|
||||
flags['layout'] = [{}]
|
||||
lm.compute_sizes_from_data(data, f)
|
||||
|
||||
while flags['layout']:
|
||||
# if `data` we were re-triggered so finish in the next call.
|
||||
# Otherwise go until fully laid out.
|
||||
self.save_viewport()
|
||||
if flags['data']:
|
||||
return
|
||||
flags['viewport'] = True
|
||||
f = flags['layout']
|
||||
flags['layout'] = []
|
||||
|
||||
try:
|
||||
lm.compute_layout(data, f)
|
||||
except LayoutChangeException:
|
||||
flags['layout'].append({})
|
||||
continue
|
||||
|
||||
if flags['data']: # in case that happened meanwhile
|
||||
return
|
||||
|
||||
# make sure if we were re-triggered in the loop that we won't be
|
||||
# called needlessly later.
|
||||
self._refresh_trigger.cancel()
|
||||
|
||||
self.restore_viewport()
|
||||
|
||||
if flags['viewport']:
|
||||
# TODO: make this also listen to LayoutChangeException
|
||||
flags['viewport'] = False
|
||||
viewport = self.get_viewport()
|
||||
indices = lm.compute_visible_views(data, viewport)
|
||||
lm.set_visible_views(indices, data, viewport)
|
||||
|
||||
def refresh_from_data(self, *largs, **kwargs):
|
||||
"""
|
||||
This should be called when data changes. Data changes typically
|
||||
indicate that everything should be recomputed since the source data
|
||||
changed.
|
||||
|
||||
This method is automatically bound to the
|
||||
:attr:`~RecycleDataModelBehavior.on_data_changed` method of the
|
||||
:class:`~RecycleDataModelBehavior` class and
|
||||
therefore responds to and accepts the keyword arguments of that event.
|
||||
|
||||
It can be called manually to trigger an update.
|
||||
"""
|
||||
self._refresh_flags['data'].append(kwargs)
|
||||
self._refresh_trigger()
|
||||
|
||||
def refresh_from_layout(self, *largs, **kwargs):
|
||||
"""
|
||||
This should be called when the layout changes or needs to change. It is
|
||||
typically called when a layout parameter has changed and therefore the
|
||||
layout needs to be recomputed.
|
||||
"""
|
||||
self._refresh_flags['layout'].append(kwargs)
|
||||
self._refresh_trigger()
|
||||
|
||||
def refresh_from_viewport(self, *largs):
|
||||
"""
|
||||
This should be called when the viewport changes and the displayed data
|
||||
must be updated. Neither the data nor the layout will be recomputed.
|
||||
"""
|
||||
self._refresh_flags['viewport'] = True
|
||||
self._refresh_trigger()
|
||||
|
||||
def _dispatch_prop_on_source(self, prop_name, *largs):
|
||||
# Dispatches the prop of this class when the
|
||||
# view_adapter/layout_manager property changes.
|
||||
getattr(self.__class__, prop_name).dispatch(self)
|
||||
|
||||
def _get_data_model(self):
|
||||
return self._data_model
|
||||
|
||||
def _set_data_model(self, value):
|
||||
data_model = self._data_model
|
||||
if value is data_model:
|
||||
return
|
||||
if data_model is not None:
|
||||
self._data_model = None
|
||||
data_model.detach_recycleview()
|
||||
|
||||
if value is None:
|
||||
return True
|
||||
|
||||
if not isinstance(value, RecycleDataModelBehavior):
|
||||
raise ValueError(
|
||||
'Expected object based on RecycleDataModelBehavior, got {}'.
|
||||
format(value.__class__))
|
||||
|
||||
self._data_model = value
|
||||
value.attach_recycleview(self)
|
||||
self.refresh_from_data()
|
||||
return True
|
||||
|
||||
data_model = AliasProperty(_get_data_model, _set_data_model)
|
||||
"""
|
||||
The Data model responsible for maintaining the data set.
|
||||
|
||||
data_model is an :class:`~kivy.properties.AliasProperty` that gets and sets
|
||||
the current data model.
|
||||
"""
|
||||
|
||||
def _get_view_adapter(self):
|
||||
return self._view_adapter
|
||||
|
||||
def _set_view_adapter(self, value):
|
||||
view_adapter = self._view_adapter
|
||||
if value is view_adapter:
|
||||
return
|
||||
if view_adapter is not None:
|
||||
self._view_adapter = None
|
||||
view_adapter.detach_recycleview()
|
||||
|
||||
if value is None:
|
||||
return True
|
||||
|
||||
if not isinstance(value, RecycleDataAdapter):
|
||||
raise ValueError(
|
||||
'Expected object based on RecycleAdapter, got {}'.
|
||||
format(value.__class__))
|
||||
|
||||
self._view_adapter = value
|
||||
value.attach_recycleview(self)
|
||||
self.refresh_from_layout()
|
||||
return True
|
||||
|
||||
view_adapter = AliasProperty(_get_view_adapter, _set_view_adapter)
|
||||
"""
|
||||
The adapter responsible for providing views that represent items in a data
|
||||
set.
|
||||
|
||||
view_adapter is an :class:`~kivy.properties.AliasProperty` that gets and
|
||||
sets the current view adapter.
|
||||
"""
|
||||
|
||||
def _get_layout_manager(self):
|
||||
return self._layout_manager
|
||||
|
||||
def _set_layout_manager(self, value):
|
||||
lm = self._layout_manager
|
||||
if value is lm:
|
||||
return
|
||||
|
||||
if lm is not None:
|
||||
self._layout_manager = None
|
||||
lm.detach_recycleview()
|
||||
|
||||
if value is None:
|
||||
return True
|
||||
|
||||
if not isinstance(value, RecycleLayoutManagerBehavior):
|
||||
raise ValueError(
|
||||
'Expected object based on RecycleLayoutManagerBehavior, '
|
||||
'got {}'.format(value.__class__))
|
||||
|
||||
self._layout_manager = value
|
||||
value.attach_recycleview(self)
|
||||
self.refresh_from_layout()
|
||||
return True
|
||||
|
||||
layout_manager = AliasProperty(_get_layout_manager, _set_layout_manager)
|
||||
"""
|
||||
The Layout manager responsible for positioning views within the
|
||||
:class:`RecycleView`.
|
||||
|
||||
layout_manager is an :class:`~kivy.properties.AliasProperty` that gets
|
||||
and sets the layout_manger.
|
||||
"""
|
||||
|
||||
|
||||
class RecycleView(RecycleViewBehavior, ScrollView):
|
||||
"""
|
||||
RecycleView is a flexible view for providing a limited window
|
||||
into a large data set.
|
||||
|
||||
See the module documentation for more information.
|
||||
"""
|
||||
def __init__(self, **kwargs):
|
||||
if self.data_model is None:
|
||||
kwargs.setdefault('data_model', RecycleDataModel())
|
||||
if self.view_adapter is None:
|
||||
kwargs.setdefault('view_adapter', RecycleDataAdapter())
|
||||
super(RecycleView, self).__init__(**kwargs)
|
||||
|
||||
fbind = self.fbind
|
||||
fbind('scroll_x', self.refresh_from_viewport)
|
||||
fbind('scroll_y', self.refresh_from_viewport)
|
||||
fbind('size', self.refresh_from_viewport)
|
||||
self.refresh_from_data()
|
||||
|
||||
def _convert_sv_to_lm(self, x, y):
|
||||
lm = self.layout_manager
|
||||
tree = [lm]
|
||||
parent = lm.parent
|
||||
while parent is not None and parent is not self:
|
||||
tree.append(parent)
|
||||
parent = parent.parent
|
||||
|
||||
if parent is not self:
|
||||
raise Exception(
|
||||
'The layout manager must be a sub child of the recycleview. '
|
||||
'Could not find {} in the parent tree of {}'.format(self, lm))
|
||||
|
||||
for widget in reversed(tree):
|
||||
x, y = widget.to_local(x, y)
|
||||
|
||||
return x, y
|
||||
|
||||
def get_viewport(self):
|
||||
lm = self.layout_manager
|
||||
lm_w, lm_h = lm.size
|
||||
w, h = self.size
|
||||
scroll_y = min(1, max(self.scroll_y, 0))
|
||||
scroll_x = min(1, max(self.scroll_x, 0))
|
||||
|
||||
if lm_h <= h:
|
||||
bottom = 0
|
||||
else:
|
||||
above = (lm_h - h) * scroll_y
|
||||
bottom = max(0, lm_h - above - h)
|
||||
|
||||
bottom = max(0, (lm_h - h) * scroll_y)
|
||||
left = max(0, (lm_w - w) * scroll_x)
|
||||
width = min(w, lm_w)
|
||||
height = min(h, lm_h)
|
||||
|
||||
# now convert the sv coordinates into the coordinates of the lm. In
|
||||
# case there's a relative layout type widget in the parent tree
|
||||
# between the sv and the lm.
|
||||
left, bottom = self._convert_sv_to_lm(left, bottom)
|
||||
return left, bottom, width, height
|
||||
|
||||
def save_viewport(self):
|
||||
pass
|
||||
|
||||
def restore_viewport(self):
|
||||
pass
|
||||
|
||||
def add_widget(self, widget, *args, **kwargs):
|
||||
super(RecycleView, self).add_widget(widget, *args, **kwargs)
|
||||
if (isinstance(widget, RecycleLayoutManagerBehavior) and
|
||||
not self.layout_manager):
|
||||
self.layout_manager = widget
|
||||
|
||||
def remove_widget(self, widget, *args, **kwargs):
|
||||
super(RecycleView, self).remove_widget(widget, *args, **kwargs)
|
||||
if self.layout_manager == widget:
|
||||
self.layout_manager = None
|
||||
|
||||
# or easier way to use
|
||||
def _get_data(self):
|
||||
d = self.data_model
|
||||
return d and d.data
|
||||
|
||||
def _set_data(self, value):
|
||||
d = self.data_model
|
||||
if d is not None:
|
||||
d.data = value
|
||||
|
||||
data = AliasProperty(_get_data, _set_data, bind=["data_model"])
|
||||
"""
|
||||
The data used by the current view adapter. This is a list of dicts whose
|
||||
keys map to the corresponding property names of the
|
||||
:attr:`~RecycleView.viewclass`.
|
||||
|
||||
data is an :class:`~kivy.properties.AliasProperty` that gets and sets the
|
||||
data used to generate the views.
|
||||
"""
|
||||
|
||||
def _get_viewclass(self):
|
||||
a = self.layout_manager
|
||||
return a and a.viewclass
|
||||
|
||||
def _set_viewclass(self, value):
|
||||
a = self.layout_manager
|
||||
if a:
|
||||
a.viewclass = value
|
||||
|
||||
viewclass = AliasProperty(_get_viewclass, _set_viewclass,
|
||||
bind=["layout_manager"])
|
||||
"""
|
||||
The viewclass used by the current layout_manager.
|
||||
|
||||
viewclass is an :class:`~kivy.properties.AliasProperty` that gets and sets
|
||||
the class used to generate the individual items presented in the view.
|
||||
"""
|
||||
|
||||
def _get_key_viewclass(self):
|
||||
a = self.layout_manager
|
||||
return a and a.key_viewclass
|
||||
|
||||
def _set_key_viewclass(self, value):
|
||||
a = self.layout_manager
|
||||
if a:
|
||||
a.key_viewclass = value
|
||||
|
||||
key_viewclass = AliasProperty(_get_key_viewclass, _set_key_viewclass,
|
||||
bind=["layout_manager"])
|
||||
"""
|
||||
key_viewclass is an :class:`~kivy.properties.AliasProperty` that gets and
|
||||
sets the key viewclass for the current
|
||||
:attr:`~kivy.uix.recycleview.layout_manager`.
|
||||
"""
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,194 @@
|
|||
'''
|
||||
RecycleView Data Model
|
||||
======================
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
|
||||
The data model part of the RecycleView model-view-controller pattern.
|
||||
|
||||
It defines the models (classes) that store the data associated with a
|
||||
:class:`~kivy.uix.recycleview.RecycleViewBehavior`. Each model (class)
|
||||
determines how the data is stored and emits requests to the controller
|
||||
(:class:`~kivy.uix.recycleview.RecycleViewBehavior`) when the data is
|
||||
modified.
|
||||
'''
|
||||
|
||||
from kivy.properties import ListProperty, ObservableDict, ObjectProperty
|
||||
from kivy.event import EventDispatcher
|
||||
from functools import partial
|
||||
|
||||
__all__ = ('RecycleDataModelBehavior', 'RecycleDataModel')
|
||||
|
||||
|
||||
def recondition_slice_assign(val, last_len, new_len):
|
||||
if not isinstance(val, slice):
|
||||
return slice(val, val + 1)
|
||||
|
||||
diff = new_len - last_len
|
||||
|
||||
start, stop, step = val.start, val.stop, val.step
|
||||
if stop <= start:
|
||||
return slice(0, 0)
|
||||
|
||||
if step is not None and step != 1:
|
||||
assert last_len == new_len
|
||||
if stop < 0:
|
||||
stop = max(0, last_len + stop)
|
||||
stop = min(last_len, stop)
|
||||
|
||||
if start < 0:
|
||||
start = max(0, last_len + start)
|
||||
start = min(last_len, start)
|
||||
|
||||
return slice(start, stop, step)
|
||||
|
||||
if start < 0:
|
||||
start = last_len + start
|
||||
if stop < 0:
|
||||
stop = last_len + stop
|
||||
|
||||
# whatever, too complicated don't try to compute it
|
||||
if (start < 0 or stop < 0 or start > last_len or stop > last_len or
|
||||
new_len != last_len):
|
||||
return None
|
||||
|
||||
return slice(start, stop)
|
||||
|
||||
|
||||
class RecycleDataModelBehavior(object):
|
||||
""":class:`RecycleDataModelBehavior` is the base class for the models
|
||||
that describes and provides the data for the
|
||||
:class:`~kivy.uix.recycleview.RecycleViewBehavior`.
|
||||
|
||||
:Events:
|
||||
`on_data_changed`:
|
||||
Fired when the data changes. The event may dispatch
|
||||
keyword arguments specific to each implementation of the data
|
||||
model.
|
||||
When dispatched, the event and keyword arguments are forwarded to
|
||||
:meth:`~kivy.uix.recycleview.RecycleViewBehavior.\
|
||||
refresh_from_data`.
|
||||
"""
|
||||
|
||||
__events__ = ("on_data_changed", )
|
||||
|
||||
recycleview = ObjectProperty(None, allownone=True)
|
||||
'''The
|
||||
:class:`~kivy.uix.recycleview.RecycleViewBehavior` instance
|
||||
associated with this data model.
|
||||
'''
|
||||
|
||||
def attach_recycleview(self, rv):
|
||||
'''Associates a
|
||||
:class:`~kivy.uix.recycleview.RecycleViewBehavior` with
|
||||
this data model.
|
||||
'''
|
||||
self.recycleview = rv
|
||||
if rv:
|
||||
self.fbind('on_data_changed', rv.refresh_from_data)
|
||||
|
||||
def detach_recycleview(self):
|
||||
'''Removes the
|
||||
:class:`~kivy.uix.recycleview.RecycleViewBehavior`
|
||||
associated with this data model.
|
||||
'''
|
||||
rv = self.recycleview
|
||||
if rv:
|
||||
self.funbind('on_data_changed', rv.refresh_from_data)
|
||||
self.recycleview = None
|
||||
|
||||
def on_data_changed(self, *largs, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class RecycleDataModel(RecycleDataModelBehavior, EventDispatcher):
|
||||
'''An implementation of :class:`RecycleDataModelBehavior` that keeps the
|
||||
data in a indexable list. See :attr:`data`.
|
||||
|
||||
When data changes this class currently dispatches `on_data_changed` with
|
||||
one of the following additional keyword arguments.
|
||||
|
||||
`none`: no keyword argument
|
||||
With no additional argument it means a generic data change.
|
||||
`removed`: a slice or integer
|
||||
The value is a slice or integer indicating the indices removed.
|
||||
`appended`: a slice
|
||||
The slice in :attr:`data` indicating the first and last new items
|
||||
(i.e. the slice pointing to the new items added at the end).
|
||||
`inserted`: a integer
|
||||
The index in :attr:`data` where a new data item was inserted.
|
||||
`modified`: a slice
|
||||
The slice with the indices where the data has been modified.
|
||||
This currently does not allow changing of size etc.
|
||||
'''
|
||||
|
||||
data = ListProperty([])
|
||||
'''Stores the model's data using a list.
|
||||
|
||||
The data for a item at index `i` can also be accessed with
|
||||
:class:`RecycleDataModel` `[i]`.
|
||||
'''
|
||||
|
||||
_last_len = 0
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.fbind('data', self._on_data_callback)
|
||||
super(RecycleDataModel, self).__init__(**kwargs)
|
||||
|
||||
def __getitem__(self, index):
|
||||
return self.data[index]
|
||||
|
||||
@property
|
||||
def observable_dict(self):
|
||||
'''A dictionary instance, which when modified will trigger a `data` and
|
||||
consequently an `on_data_changed` dispatch.
|
||||
'''
|
||||
return partial(ObservableDict, self.__class__.data, self)
|
||||
|
||||
def attach_recycleview(self, rv):
|
||||
super(RecycleDataModel, self).attach_recycleview(rv)
|
||||
if rv:
|
||||
self.fbind('data', rv._dispatch_prop_on_source, 'data')
|
||||
|
||||
def detach_recycleview(self):
|
||||
rv = self.recycleview
|
||||
if rv:
|
||||
self.funbind('data', rv._dispatch_prop_on_source, 'data')
|
||||
super(RecycleDataModel, self).detach_recycleview()
|
||||
|
||||
def _on_data_callback(self, instance, value):
|
||||
last_len = self._last_len
|
||||
new_len = self._last_len = len(self.data)
|
||||
op, val = value.last_op
|
||||
|
||||
if op == '__setitem__':
|
||||
val = recondition_slice_assign(val, last_len, new_len)
|
||||
if val is not None:
|
||||
self.dispatch('on_data_changed', modified=val)
|
||||
else:
|
||||
self.dispatch('on_data_changed')
|
||||
elif op == '__delitem__':
|
||||
self.dispatch('on_data_changed', removed=val)
|
||||
elif op == '__setslice__':
|
||||
val = recondition_slice_assign(slice(*val), last_len, new_len)
|
||||
if val is not None:
|
||||
self.dispatch('on_data_changed', modified=val)
|
||||
else:
|
||||
self.dispatch('on_data_changed')
|
||||
elif op == '__delslice__':
|
||||
self.dispatch('on_data_changed', removed=slice(*val))
|
||||
elif op == '__iadd__' or op == '__imul__':
|
||||
self.dispatch('on_data_changed', appended=slice(last_len, new_len))
|
||||
elif op == 'append':
|
||||
self.dispatch('on_data_changed', appended=slice(last_len, new_len))
|
||||
elif op == 'insert':
|
||||
self.dispatch('on_data_changed', inserted=val)
|
||||
elif op == 'pop':
|
||||
if val:
|
||||
self.dispatch('on_data_changed', removed=val[0])
|
||||
else:
|
||||
self.dispatch('on_data_changed', removed=last_len - 1)
|
||||
elif op == 'extend':
|
||||
self.dispatch('on_data_changed', appended=slice(last_len, new_len))
|
||||
else:
|
||||
self.dispatch('on_data_changed')
|
|
@ -0,0 +1,253 @@
|
|||
'''
|
||||
RecycleView Layouts
|
||||
===================
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
|
||||
The Layouts handle the presentation of views for the
|
||||
:class:`~kivy.uix.recycleview.RecycleView`.
|
||||
|
||||
.. warning::
|
||||
This module is highly experimental, its API may change in the future and
|
||||
the documentation is not complete at this time.
|
||||
'''
|
||||
from kivy.compat import string_types
|
||||
from kivy.factory import Factory
|
||||
from kivy.properties import StringProperty, ObjectProperty
|
||||
from kivy.uix.behaviors import CompoundSelectionBehavior
|
||||
from kivy.uix.recycleview.views import RecycleDataViewBehavior, \
|
||||
_view_base_cache
|
||||
|
||||
|
||||
class LayoutChangeException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class LayoutSelectionBehavior(CompoundSelectionBehavior):
|
||||
'''The :class:`LayoutSelectionBehavior` can be combined with
|
||||
:class:`RecycleLayoutManagerBehavior` to allow its derived classes
|
||||
selection behaviors similarly to how
|
||||
:class:`~kivy.uix.behaviors.compoundselection.CompoundSelectionBehavior`
|
||||
can be used to add selection behaviors to normal layout.
|
||||
|
||||
:class:`RecycleLayoutManagerBehavior` manages its children
|
||||
differently than normal layouts or widgets so this class adapts
|
||||
:class:`~kivy.uix.behaviors.compoundselection.CompoundSelectionBehavior`
|
||||
based selection to work with :class:`RecycleLayoutManagerBehavior` as well.
|
||||
|
||||
Similarly to
|
||||
:class:`~kivy.uix.behaviors.compoundselection.CompoundSelectionBehavior`,
|
||||
one can select using the keyboard or touch, which calls :meth:`select_node`
|
||||
or :meth:`deselect_node`, or one can call these methods directly. When a
|
||||
item is selected or deselected :meth:`apply_selection` is called. See
|
||||
:meth:`apply_selection`.
|
||||
|
||||
|
||||
'''
|
||||
|
||||
key_selection = StringProperty(None, allownone=True)
|
||||
'''The key used to check whether a view of a data item can be selected
|
||||
with touch or the keyboard.
|
||||
|
||||
:attr:`key_selection` is the key in data, which if present and ``True``
|
||||
will enable selection for this item from the keyboard or with a touch.
|
||||
When None, the default, not item will be selectable.
|
||||
|
||||
:attr:`key_selection` is a :class:`StringProperty` and defaults to None.
|
||||
|
||||
.. note::
|
||||
All data items can be selected directly using :meth:`select_node` or
|
||||
:meth:`deselect_node`, even if :attr:`key_selection` is False.
|
||||
'''
|
||||
|
||||
_selectable_nodes = []
|
||||
_nodes_map = {}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.nodes_order_reversed = False
|
||||
super(LayoutSelectionBehavior, self).__init__(**kwargs)
|
||||
|
||||
def compute_sizes_from_data(self, data, flags):
|
||||
# overwrite this method so that when data changes we update
|
||||
# selectable nodes.
|
||||
key = self.key_selection
|
||||
if key is None:
|
||||
nodes = self._selectable_nodes = []
|
||||
else:
|
||||
nodes = self._selectable_nodes = [
|
||||
i for i, d in enumerate(data) if d.get(key)]
|
||||
|
||||
self._nodes_map = {v: k for k, v in enumerate(nodes)}
|
||||
return super(LayoutSelectionBehavior, self).compute_sizes_from_data(
|
||||
data, flags)
|
||||
|
||||
def get_selectable_nodes(self):
|
||||
# the indices of the data is used as the nodes
|
||||
return self._selectable_nodes
|
||||
|
||||
def get_index_of_node(self, node, selectable_nodes):
|
||||
# the indices of the data is used as the nodes, so node
|
||||
return self._nodes_map[node]
|
||||
|
||||
def goto_node(self, key, last_node, last_node_idx):
|
||||
node, idx = super(LayoutSelectionBehavior, self).goto_node(
|
||||
key, last_node, last_node_idx)
|
||||
if node is not last_node:
|
||||
self.goto_view(node)
|
||||
return node, idx
|
||||
|
||||
def select_node(self, node):
|
||||
if super(LayoutSelectionBehavior, self).select_node(node):
|
||||
view = self.recycleview.view_adapter.get_visible_view(node)
|
||||
if view is not None:
|
||||
self.apply_selection(node, view, True)
|
||||
|
||||
def deselect_node(self, node):
|
||||
if super(LayoutSelectionBehavior, self).deselect_node(node):
|
||||
view = self.recycleview.view_adapter.get_visible_view(node)
|
||||
if view is not None:
|
||||
self.apply_selection(node, view, False)
|
||||
|
||||
def apply_selection(self, index, view, is_selected):
|
||||
'''Applies the selection to the view. This is called internally when
|
||||
a view is displayed and it needs to be shown as selected or as not
|
||||
selected.
|
||||
|
||||
It is called when :meth:`select_node` or :meth:`deselect_node` is
|
||||
called or when a view needs to be refreshed. Its function is purely to
|
||||
update the view to reflect the selection state. So the function may be
|
||||
called multiple times even if the selection state may not have changed.
|
||||
|
||||
If the view is a instance of
|
||||
:class:`~kivy.uix.recycleview.views.RecycleDataViewBehavior`, its
|
||||
:meth:`~kivy.uix.recycleview.views.RecycleDataViewBehavior.\
|
||||
apply_selection` method will be called every time the view needs to refresh
|
||||
the selection state. Otherwise, the this method is responsible
|
||||
for applying the selection.
|
||||
|
||||
:Parameters:
|
||||
|
||||
`index`: int
|
||||
The index of the data item that is associated with the view.
|
||||
`view`: widget
|
||||
The widget that is the view of this data item.
|
||||
`is_selected`: bool
|
||||
Whether the item is selected.
|
||||
'''
|
||||
viewclass = view.__class__
|
||||
if viewclass not in _view_base_cache:
|
||||
_view_base_cache[viewclass] = isinstance(view,
|
||||
RecycleDataViewBehavior)
|
||||
|
||||
if _view_base_cache[viewclass]:
|
||||
view.apply_selection(self.recycleview, index, is_selected)
|
||||
|
||||
def refresh_view_layout(self, index, layout, view, viewport):
|
||||
super(LayoutSelectionBehavior, self).refresh_view_layout(
|
||||
index, layout, view, viewport)
|
||||
self.apply_selection(index, view, index in self.selected_nodes)
|
||||
|
||||
|
||||
class RecycleLayoutManagerBehavior(object):
|
||||
"""A RecycleLayoutManagerBehavior is responsible for positioning views into
|
||||
the :attr:`RecycleView.data` within a :class:`RecycleView`. It adds new
|
||||
views into the data when it becomes visible to the user, and removes them
|
||||
when they leave the visible area.
|
||||
"""
|
||||
|
||||
viewclass = ObjectProperty(None)
|
||||
'''See :attr:`RecyclerView.viewclass`.
|
||||
'''
|
||||
key_viewclass = StringProperty(None)
|
||||
'''See :attr:`RecyclerView.key_viewclass`.
|
||||
'''
|
||||
|
||||
recycleview = ObjectProperty(None, allownone=True)
|
||||
|
||||
asked_sizes = None
|
||||
|
||||
def attach_recycleview(self, rv):
|
||||
self.recycleview = rv
|
||||
if rv:
|
||||
fbind = self.fbind
|
||||
# can be made more selective update than refresh_from_data which
|
||||
# causes a full update. But this likely affects most of the data.
|
||||
fbind('viewclass', rv.refresh_from_data)
|
||||
fbind('key_viewclass', rv.refresh_from_data)
|
||||
fbind('viewclass', rv._dispatch_prop_on_source, 'viewclass')
|
||||
fbind('key_viewclass', rv._dispatch_prop_on_source,
|
||||
'key_viewclass')
|
||||
|
||||
def detach_recycleview(self):
|
||||
self.clear_layout()
|
||||
rv = self.recycleview
|
||||
if rv:
|
||||
funbind = self.funbind
|
||||
funbind('viewclass', rv.refresh_from_data)
|
||||
funbind('key_viewclass', rv.refresh_from_data)
|
||||
funbind('viewclass', rv._dispatch_prop_on_source, 'viewclass')
|
||||
funbind('key_viewclass', rv._dispatch_prop_on_source,
|
||||
'key_viewclass')
|
||||
self.recycleview = None
|
||||
|
||||
def compute_sizes_from_data(self, data, flags):
|
||||
pass
|
||||
|
||||
def compute_layout(self, data, flags):
|
||||
pass
|
||||
|
||||
def compute_visible_views(self, data, viewport):
|
||||
'''`viewport` is in coordinates of the layout manager.
|
||||
'''
|
||||
pass
|
||||
|
||||
def set_visible_views(self, indices, data, viewport):
|
||||
'''`viewport` is in coordinates of the layout manager.
|
||||
'''
|
||||
pass
|
||||
|
||||
def refresh_view_layout(self, index, layout, view, viewport):
|
||||
'''`See :meth:`~kivy.uix.recycleview.views.RecycleDataAdapter.\
|
||||
refresh_view_layout`.
|
||||
'''
|
||||
self.recycleview.view_adapter.refresh_view_layout(
|
||||
index, layout, view, viewport)
|
||||
|
||||
def get_view_index_at(self, pos):
|
||||
"""Return the view `index` on which position, `pos`, falls.
|
||||
|
||||
`pos` is in coordinates of the layout manager.
|
||||
"""
|
||||
pass
|
||||
|
||||
def remove_views(self):
|
||||
rv = self.recycleview
|
||||
if rv:
|
||||
adapter = rv.view_adapter
|
||||
if adapter:
|
||||
adapter.make_views_dirty()
|
||||
|
||||
def remove_view(self, view, index):
|
||||
rv = self.recycleview
|
||||
if rv:
|
||||
adapter = rv.view_adapter
|
||||
if adapter:
|
||||
adapter.make_view_dirty(view, index)
|
||||
|
||||
def clear_layout(self):
|
||||
rv = self.recycleview
|
||||
if rv:
|
||||
adapter = rv.view_adapter
|
||||
if adapter:
|
||||
adapter.invalidate()
|
||||
|
||||
def goto_view(self, index):
|
||||
'''Moves the views so that the view corresponding to `index` is
|
||||
visible.
|
||||
'''
|
||||
pass
|
||||
|
||||
def on_viewclass(self, instance, value):
|
||||
# resolve the real class if it was a string.
|
||||
if isinstance(value, string_types):
|
||||
self.viewclass = getattr(Factory, value)
|
|
@ -0,0 +1,423 @@
|
|||
'''
|
||||
RecycleView Views
|
||||
=================
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
|
||||
The adapter part of the RecycleView which together with the layout is the
|
||||
view part of the model-view-controller pattern.
|
||||
|
||||
The view module handles converting the data to a view using the adapter class
|
||||
which is then displayed by the layout. A view can be any Widget based class.
|
||||
However, inheriting from RecycleDataViewBehavior adds methods for converting
|
||||
the data to a view.
|
||||
|
||||
TODO:
|
||||
* Make view caches specific to each view class type.
|
||||
|
||||
'''
|
||||
|
||||
from kivy.properties import ObjectProperty
|
||||
from kivy.event import EventDispatcher
|
||||
from collections import defaultdict
|
||||
|
||||
__all__ = (
|
||||
'RecycleDataViewBehavior', 'RecycleKVIDsDataViewBehavior',
|
||||
'RecycleDataAdapter')
|
||||
|
||||
_view_base_cache = {}
|
||||
'''Cache whose keys are classes and values is a boolean indicating whether the
|
||||
class inherits from :class:`RecycleDataViewBehavior`.
|
||||
'''
|
||||
|
||||
_cached_views = defaultdict(list)
|
||||
'''A size limited cache that contains old views (instances) that are not used.
|
||||
Each key is a class whose value is the list of the instances of that class.
|
||||
'''
|
||||
# current number of unused classes in the class cache
|
||||
_cache_count = 0
|
||||
# maximum number of items in the class cache
|
||||
_max_cache_size = 1000
|
||||
|
||||
|
||||
def _clean_cache():
|
||||
'''Trims _cached_views cache to half the size of `_max_cache_size`.
|
||||
'''
|
||||
# all keys will be reduced to max_size.
|
||||
max_size = (_max_cache_size // 2) // len(_cached_views)
|
||||
global _cache_count
|
||||
for cls, instances in _cached_views.items():
|
||||
_cache_count -= max(0, len(instances) - max_size)
|
||||
del instances[max_size:]
|
||||
|
||||
|
||||
class RecycleDataViewBehavior(object):
|
||||
'''A optional base class for data views (:attr:`RecycleView`.viewclass).
|
||||
If a view inherits from this class, the class's functions will be called
|
||||
when the view needs to be updated due to a data change or layout update.
|
||||
'''
|
||||
|
||||
def refresh_view_attrs(self, rv, index, data):
|
||||
'''Called by the :class:`RecycleAdapter` when the view is initially
|
||||
populated with the values from the `data` dictionary for this item.
|
||||
|
||||
Any pos or size info should be removed because they are set
|
||||
subsequently with :attr:`refresh_view_layout`.
|
||||
|
||||
:Parameters:
|
||||
|
||||
`rv`: :class:`RecycleView` instance
|
||||
The :class:`RecycleView` that caused the update.
|
||||
`data`: dict
|
||||
The data dict used to populate this view.
|
||||
'''
|
||||
sizing_attrs = RecycleDataAdapter._sizing_attrs
|
||||
for key, value in data.items():
|
||||
if key not in sizing_attrs:
|
||||
setattr(self, key, value)
|
||||
|
||||
def refresh_view_layout(self, rv, index, layout, viewport):
|
||||
'''Called when the view's size is updated by the layout manager,
|
||||
:class:`RecycleLayoutManagerBehavior`.
|
||||
|
||||
:Parameters:
|
||||
|
||||
`rv`: :class:`RecycleView` instance
|
||||
The :class:`RecycleView` that caused the update.
|
||||
`viewport`: 4-tuple
|
||||
The coordinates of the bottom left and width height in layout
|
||||
manager coordinates. This may be larger than this view item.
|
||||
|
||||
:raises:
|
||||
`LayoutChangeException`: If the sizing or data changed during a
|
||||
call to this method, raising a `LayoutChangeException` exception
|
||||
will force a refresh. Useful when data changed and we don't want
|
||||
to layout further since it'll be overwritten again soon.
|
||||
'''
|
||||
w, h = layout.pop('size')
|
||||
if w is None:
|
||||
if h is not None:
|
||||
self.height = h
|
||||
else:
|
||||
if h is None:
|
||||
self.width = w
|
||||
else:
|
||||
self.size = w, h
|
||||
|
||||
for name, value in layout.items():
|
||||
setattr(self, name, value)
|
||||
|
||||
def apply_selection(self, rv, index, is_selected):
|
||||
pass
|
||||
|
||||
|
||||
class RecycleKVIDsDataViewBehavior(RecycleDataViewBehavior):
|
||||
"""Similar to :class:`RecycleDataViewBehavior`, except that the data keys
|
||||
can signify properties of an object named with an id in the root KV rule.
|
||||
|
||||
E.g. given a KV rule::
|
||||
|
||||
<MyRule@RecycleKVIDsDataViewBehavior+BoxLayout>:
|
||||
Label:
|
||||
id: name
|
||||
Label:
|
||||
id: value
|
||||
|
||||
Then setting the data list with
|
||||
``rv.data = [{'name.text': 'Kivy user', 'value.text': '12'}]`` would
|
||||
automatically set the corresponding labels.
|
||||
|
||||
So, if the key doesn't have a period, the named property of the root widget
|
||||
will be set to the corresponding value. If there is a period, the named
|
||||
property of the widget with the id listed before the period will be set to
|
||||
the corresponding value.
|
||||
|
||||
.. versionadded:: 2.0.0
|
||||
"""
|
||||
|
||||
def refresh_view_attrs(self, rv, index, data):
|
||||
sizing_attrs = RecycleDataAdapter._sizing_attrs
|
||||
for key, value in data.items():
|
||||
if key not in sizing_attrs:
|
||||
name, *ids = key.split('.')
|
||||
if ids:
|
||||
if len(ids) != 1:
|
||||
raise ValueError(
|
||||
f'Data key "{key}" has more than one period')
|
||||
setattr(self.ids[name], ids[0], value)
|
||||
else:
|
||||
setattr(self, name, value)
|
||||
|
||||
|
||||
class RecycleDataAdapter(EventDispatcher):
|
||||
'''The class that converts data to a view.
|
||||
|
||||
--- Internal details ---
|
||||
A view can have 3 states.
|
||||
|
||||
* It can be completely in sync with the data, which
|
||||
occurs when the view is displayed. These are stored in :attr:`views`.
|
||||
* It can be dirty, which occurs when the view is in sync with the data,
|
||||
except for the size/pos parameters which is controlled by the layout.
|
||||
This occurs when the view is not currently displayed but the data has
|
||||
not changed. These views are stored in :attr:`dirty_views`.
|
||||
* Finally the view can be dead which occurs when the data changes and
|
||||
the view was not updated or when a view is just created. Such views
|
||||
are typically added to the internal cache.
|
||||
|
||||
Typically what happens is that the layout manager lays out the data
|
||||
and then asks for views, using :meth:`set_visible_views`, for some specific
|
||||
data items that it displays.
|
||||
|
||||
These views are gotten from the current views, dirty or global cache. Then
|
||||
depending on the view state :meth:`refresh_view_attrs` is called to bring
|
||||
the view up to date with the data (except for sizing parameters). Finally,
|
||||
the layout manager gets these views, updates their size and displays them.
|
||||
'''
|
||||
|
||||
recycleview = ObjectProperty(None, allownone=True)
|
||||
'''The :class:`~kivy.uix.recycleview.RecycleViewBehavior` associated
|
||||
with this instance.
|
||||
'''
|
||||
|
||||
# internals
|
||||
views = {} # current displayed items
|
||||
# items whose attrs, except for pos/size is still accurate
|
||||
dirty_views = defaultdict(dict)
|
||||
|
||||
_sizing_attrs = {
|
||||
'size', 'width', 'height', 'size_hint', 'size_hint_x', 'size_hint_y',
|
||||
'pos', 'x', 'y', 'center', 'center_x', 'center_y', 'pos_hint',
|
||||
'size_hint_min', 'size_hint_min_x', 'size_hint_min_y', 'size_hint_max',
|
||||
'size_hint_max_x', 'size_hint_max_y'}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""
|
||||
Fix for issue https://github.com/kivy/kivy/issues/5913:
|
||||
Scrolling RV A, then Scrolling RV B, content of A and B seemed
|
||||
to be getting mixed up
|
||||
"""
|
||||
self.views = {}
|
||||
self.dirty_views = defaultdict(dict)
|
||||
super(RecycleDataAdapter, self).__init__(**kwargs)
|
||||
|
||||
def attach_recycleview(self, rv):
|
||||
'''Associates a :class:`~kivy.uix.recycleview.RecycleViewBehavior`
|
||||
with this instance. It is stored in :attr:`recycleview`.
|
||||
'''
|
||||
self.recycleview = rv
|
||||
|
||||
def detach_recycleview(self):
|
||||
'''Removes the :class:`~kivy.uix.recycleview.RecycleViewBehavior`
|
||||
associated with this instance and clears :attr:`recycleview`.
|
||||
'''
|
||||
self.recycleview = None
|
||||
|
||||
def create_view(self, index, data_item, viewclass):
|
||||
'''(internal) Creates and initializes the view for the data at `index`.
|
||||
|
||||
The returned view is synced with the data, except for the pos/size
|
||||
information.
|
||||
'''
|
||||
if viewclass is None:
|
||||
return
|
||||
|
||||
view = viewclass()
|
||||
self.refresh_view_attrs(index, data_item, view)
|
||||
return view
|
||||
|
||||
def get_view(self, index, data_item, viewclass):
|
||||
'''(internal) Returns a view instance for the data at `index`
|
||||
|
||||
It looks through the various caches and finally creates a view if it
|
||||
doesn't exist. The returned view is synced with the data, except for
|
||||
the pos/size information.
|
||||
|
||||
If found in the cache it's removed from the source
|
||||
before returning. It doesn't check the current views.
|
||||
'''
|
||||
# is it in the dirtied views?
|
||||
dirty_views = self.dirty_views
|
||||
if viewclass is None:
|
||||
return
|
||||
stale = False
|
||||
view = None
|
||||
|
||||
if viewclass in dirty_views: # get it first from dirty list
|
||||
dirty_class = dirty_views[viewclass]
|
||||
if index in dirty_class:
|
||||
# we found ourself in the dirty list, no need to update data!
|
||||
view = dirty_class.pop(index)
|
||||
elif _cached_views[viewclass]:
|
||||
# global cache has this class, update data
|
||||
view, stale = _cached_views[viewclass].pop(), True
|
||||
elif dirty_class:
|
||||
# random any dirty view element - update data
|
||||
view, stale = dirty_class.popitem()[1], True
|
||||
elif _cached_views[viewclass]: # otherwise go directly to cache
|
||||
# global cache has this class, update data
|
||||
view, stale = _cached_views[viewclass].pop(), True
|
||||
|
||||
if view is None:
|
||||
view = self.create_view(index, data_item, viewclass)
|
||||
if view is None:
|
||||
return
|
||||
|
||||
if stale:
|
||||
self.refresh_view_attrs(index, data_item, view)
|
||||
return view
|
||||
|
||||
def refresh_view_attrs(self, index, data_item, view):
|
||||
'''(internal) Syncs the view and brings it up to date with the data.
|
||||
|
||||
This method calls :meth:`RecycleDataViewBehavior.refresh_view_attrs`
|
||||
if the view inherits from :class:`RecycleDataViewBehavior`. See that
|
||||
method for more details.
|
||||
|
||||
.. note::
|
||||
Any sizing and position info is skipped when syncing with the data.
|
||||
'''
|
||||
viewclass = view.__class__
|
||||
if viewclass not in _view_base_cache:
|
||||
_view_base_cache[viewclass] = isinstance(view,
|
||||
RecycleDataViewBehavior)
|
||||
|
||||
if _view_base_cache[viewclass]:
|
||||
view.refresh_view_attrs(self.recycleview, index, data_item)
|
||||
else:
|
||||
sizing_attrs = RecycleDataAdapter._sizing_attrs
|
||||
for key, value in data_item.items():
|
||||
if key not in sizing_attrs:
|
||||
setattr(view, key, value)
|
||||
|
||||
def refresh_view_layout(self, index, layout, view, viewport):
|
||||
'''Updates the sizing information of the view.
|
||||
|
||||
viewport is in coordinates of the layout manager.
|
||||
|
||||
This method calls :meth:`RecycleDataViewBehavior.refresh_view_attrs`
|
||||
if the view inherits from :class:`RecycleDataViewBehavior`. See that
|
||||
method for more details.
|
||||
|
||||
.. note::
|
||||
Any sizing and position info is skipped when syncing with the data.
|
||||
'''
|
||||
if view.__class__ not in _view_base_cache:
|
||||
_view_base_cache[view.__class__] = isinstance(
|
||||
view, RecycleDataViewBehavior)
|
||||
|
||||
if _view_base_cache[view.__class__]:
|
||||
view.refresh_view_layout(
|
||||
self.recycleview, index, layout, viewport)
|
||||
else:
|
||||
w, h = layout.pop('size')
|
||||
if w is None:
|
||||
if h is not None:
|
||||
view.height = h
|
||||
else:
|
||||
if h is None:
|
||||
view.width = w
|
||||
else:
|
||||
view.size = w, h
|
||||
|
||||
for name, value in layout.items():
|
||||
setattr(view, name, value)
|
||||
|
||||
def make_view_dirty(self, view, index):
|
||||
'''(internal) Used to flag this view as dirty, ready to be used for
|
||||
others. See :meth:`make_views_dirty`.
|
||||
'''
|
||||
del self.views[index]
|
||||
self.dirty_views[view.__class__][index] = view
|
||||
|
||||
def make_views_dirty(self):
|
||||
'''Makes all the current views dirty.
|
||||
|
||||
Dirty views are still in sync with the corresponding data. However, the
|
||||
size information may go out of sync. Therefore a dirty view can be
|
||||
reused by the same index by just updating the sizing information.
|
||||
|
||||
Once the underlying data of this index changes, the view should be
|
||||
removed from the dirty views and moved to the global cache with
|
||||
:meth:`invalidate`.
|
||||
|
||||
This is typically called when the layout manager needs to re-layout all
|
||||
the data.
|
||||
'''
|
||||
views = self.views
|
||||
if not views:
|
||||
return
|
||||
|
||||
dirty_views = self.dirty_views
|
||||
for index, view in views.items():
|
||||
dirty_views[view.__class__][index] = view
|
||||
self.views = {}
|
||||
|
||||
def invalidate(self):
|
||||
'''Moves all the current views into the global cache.
|
||||
|
||||
As opposed to making a view dirty where the view is in sync with the
|
||||
data except for sizing information, this will completely disconnect the
|
||||
view from the data, as it is assumed the data has gone out of sync with
|
||||
the view.
|
||||
|
||||
This is typically called when the data changes.
|
||||
'''
|
||||
global _cache_count
|
||||
for view in self.views.values():
|
||||
_cached_views[view.__class__].append(view)
|
||||
_cache_count += 1
|
||||
|
||||
for cls, views in self.dirty_views.items():
|
||||
_cached_views[cls].extend(views.values())
|
||||
_cache_count += len(views)
|
||||
|
||||
if _cache_count >= _max_cache_size:
|
||||
_clean_cache()
|
||||
self.views = {}
|
||||
self.dirty_views.clear()
|
||||
|
||||
def set_visible_views(self, indices, data, viewclasses):
|
||||
'''Gets a 3-tuple of the new, remaining, and old views for the current
|
||||
viewport.
|
||||
|
||||
The new views are synced to the data except for the size/pos
|
||||
properties.
|
||||
The old views need to be removed from the layout, and the new views
|
||||
added.
|
||||
|
||||
The new views are not necessarily *new*, but are all the currently
|
||||
visible views.
|
||||
'''
|
||||
visible_views = {}
|
||||
previous_views = self.views
|
||||
ret_new = []
|
||||
ret_remain = []
|
||||
get_view = self.get_view
|
||||
|
||||
# iterate though the visible view
|
||||
# add them into the container if not already done
|
||||
for index in indices:
|
||||
view = previous_views.pop(index, None)
|
||||
if view is not None: # was current view
|
||||
visible_views[index] = view
|
||||
ret_remain.append((index, view))
|
||||
else:
|
||||
view = get_view(index, data[index],
|
||||
viewclasses[index]['viewclass'])
|
||||
if view is None:
|
||||
continue
|
||||
visible_views[index] = view
|
||||
ret_new.append((index, view))
|
||||
|
||||
old_views = previous_views.items()
|
||||
self.make_views_dirty()
|
||||
self.views = visible_views
|
||||
return ret_new, ret_remain, old_views
|
||||
|
||||
def get_visible_view(self, index):
|
||||
'''Returns the currently visible view associated with ``index``.
|
||||
|
||||
If no view is currently displayed for ``index`` it returns ``None``.
|
||||
'''
|
||||
return self.views.get(index)
|
Loading…
Add table
Add a link
Reference in a new issue