195 lines
6.4 KiB
Python
195 lines
6.4 KiB
Python
|
'''
|
||
|
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')
|