Created
August 10, 2013 00:49
-
-
Save geojeff/6198496 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
commit e34a323d3631922d85198eae4f80d4c36e53974b | |
Author: geojeff <[email protected]> | |
Date: Tue Jun 18 17:01:22 2013 -0500 | |
Work on data updating in adapters and ListView | |
The adapter and ListView system has several open issues about data | |
updating, and ListView's reaction for scrolling. From discussion with | |
tito and reference to SproutCore, more information needs to be added | |
about the nature of data change, which will be different for | |
ListProperty and DictProperty. | |
So far, for lists, a new cls parameter is added to ListProperty, so | |
that instead of using ObservableList always, it will check to see if | |
an ObservableList variant is provided. In this case cls will be set | |
to a new RangeObservingList for ListAdapter. Something similar will | |
be needed for DictAdapter. | |
As this branch has been open locally for a while, several discussions | |
with quanon about data updating resulted in a fix for how ListView | |
handles its adapter binding, to assure proper bindings to ListView | |
updating methods are reset when adapter changes. | |
Also, in the same area of ListView, the bind_triggers_to_view method | |
has been renamed to data_changed. | |
diff --git a/kivy/adapters/dictadapter.py b/kivy/adapters/dictadapter.py | |
index f1d0651..4ed303b 100755 | |
--- a/kivy/adapters/dictadapter.py | |
+++ b/kivy/adapters/dictadapter.py | |
@@ -20,8 +20,84 @@ If you wish to have a bare-bones list adapter, without selection, use the | |
__all__ = ('DictAdapter', ) | |
-from kivy.properties import ListProperty, DictProperty | |
+from kivy.properties import DictProperty | |
+from kivy.properties import ListProperty | |
+from kivy.properties import ObservableDict | |
from kivy.adapters.listadapter import ListAdapter | |
+from kivy.adapters.listadapter import RangeObservingObservableList | |
+ | |
+ | |
+class RangeObservingObservableDict(ObservableDict): | |
+ | |
+ # range_change is a normal python object consisting of the op name and | |
+ # the keys involved: | |
+ # | |
+ # (data_op, (keys)) | |
+ # | |
+ # If the op does not cause a range change, range_change is set to None. | |
+ # | |
+ # Observers of data changes may consult range_change if needed, for | |
+ # example, listview needs to know details for scrolling. | |
+ # | |
+ # DictAdapter itself, the owner of data, is the first observer of data | |
+ # change that must react to delete ops, if the existing selection is | |
+ # affected. | |
+ # | |
+ | |
+ # TODO: Do something on this one? | |
+ #def __setattr__(self, attr, value): | |
+ # if attr in ('prop', 'obj'): | |
+ # super(ObservableDict, self).__setattr__(attr, value) | |
+ # return | |
+ # self.__setitem__(attr, value) | |
+ | |
+ def __setitem__(self, key, value): | |
+ if value is None: | |
+ # ObservableDict will delete the item if value is None, so this is | |
+ # like a delete op. | |
+ self.range_change = ('delete', (key, )) | |
+ else: | |
+ self.range_change = ('add', (key, )) | |
+ super(RangeObservingObservableDict, self).__setitem__(key, value) | |
+ | |
+ def __delitem__(self, key): | |
+ self.range_change = ('delete', (key, )) | |
+ super(RangeObservingObservableDict, self).__delitem__(key) | |
+ | |
+ def clear(self, *largs): | |
+ # TODO: Should this, and other cases below, be (*largs)? | |
+ self.range_change = ('delete', largs) | |
+ super(RangeObservingObservableDict, self).clear(*largs) | |
+ | |
+ def remove(self, *largs): | |
+ # remove(x) is same as del s[s.index(x)] | |
+ self.range_change = ('delete', largs) | |
+ super(RangeObservingObservableDict, self).remove(*largs) | |
+ | |
+ def pop(self, *largs): | |
+ # This is pop on a specific key. | |
+ # s.pop([i]) is same as x = s[i]; del s[i]; return x | |
+ self.range_change = ('delete', largs) | |
+ return super(RangeObservingObservableDict, self).pop(*largs) | |
+ | |
+ def popitem(self, *largs): | |
+ # From python docs, "Remove and return an arbitrary (key, value) pair | |
+ # from the dictionary." From other reading, arbitrary here effectively | |
+ # means "random" in the loose sense -- for truely random ops, use the | |
+ # proper random module. Nevertheless, the item is deleted and returned. | |
+ self.range_change = ('delete', largs) | |
+ return super(RangeObservingObservableDict, self).popitem(*largs) | |
+ | |
+ def setdefault(self, *largs): | |
+ present_keys = super(RangeObservingObservableDict, self).keys() | |
+ if not largs[0] in present_keys: | |
+ self.range_change = ('add', largs) | |
+ super(RangeObservingObservableDict, self).setdefault(*largs) | |
+ | |
+ def update(self, *largs): | |
+ present_keys = super(RangeObservingObservableDict, self).keys() | |
+ self.range_change = ('add', list(set(largs) - set(present_keys))) | |
+ super(RangeObservingObservableDict, self).update(*largs) | |
class DictAdapter(ListAdapter): | |
@@ -41,10 +117,13 @@ class DictAdapter(ListAdapter): | |
defaults to []. | |
''' | |
- data = DictProperty(None) | |
+ # TODO: This was initialized to None. Problem with setting to {} instead? | |
+ data = DictProperty({}, RangeObservingObservableDict) | |
'''A dict that indexes records by keys that are equivalent to the keys in | |
sorted_keys, or they are a superset of the keys in sorted_keys. | |
+ TODO: Is that last statement about "superset" correct? | |
+ | |
The values can be strings, class instances, dicts, etc. | |
:data:`data` is a :class:`~kivy.properties.DictProperty` and defaults | |
@@ -56,12 +135,49 @@ class DictAdapter(ListAdapter): | |
if type(kwargs['sorted_keys']) not in (tuple, list): | |
msg = 'DictAdapter: sorted_keys must be tuple or list' | |
raise Exception(msg) | |
+ else: | |
+ # Copy the provided sorted_keys, and maintain it internally. | |
+ # The only function in the API for sorted_keys is to reset it | |
+ # wholesale with a call to reset_sorted_keys(). | |
+ self.sorted_keys = list(kwargs['sorted_keys']) | |
+ kwargs.remove('sorted_keys') | |
else: | |
self.sorted_keys = sorted(kwargs['data'].keys()) | |
super(DictAdapter, self).__init__(**kwargs) | |
- self.bind(sorted_keys=self.initialize_sorted_keys) | |
+ self.bind(sorted_keys=self.initialize_sorted_keys, | |
+ data=self.data_changed) | |
+ | |
+ def reset_sorted_keys(self, sorted_keys): | |
+ self.sorted_keys = sorted_keys | |
+ # call update on dict to match? | |
+ | |
+ def data_changed(self, *dt): | |
+ | |
+ print 'DICT ADAPTER data_changed callback', dt | |
+ | |
+ print self.data.range_change | |
+ | |
+ if self.adapter.data.range_change: | |
+ | |
+ data_op, keys = self.data.range_change | |
+ | |
+ if data_op == 'add': | |
+ self.sorted_keys.extend(keys) | |
+ elif data_op == 'delete': | |
+ | |
+ delete_affected_selection = False | |
+ | |
+ for selected_key in [sel.text for sel in self.selection]: | |
+ if selected_key in keys: | |
+ del self.selection[self.selection.index(selected_key)] | |
+ delete_affected_selection = True | |
+ | |
+ if delete_affected_selection: | |
+ self.dispatch('on_selection_change') | |
+ | |
+ self.check_for_empty_selection() | |
def bind_triggers_to_view(self, func): | |
self.bind(sorted_keys=func) | |
diff --git a/kivy/adapters/listadapter.py b/kivy/adapters/listadapter.py | |
index d5bb4f3..54837fe 100644 | |
--- a/kivy/adapters/listadapter.py | |
+++ b/kivy/adapters/listadapter.py | |
@@ -51,14 +51,116 @@ import inspect | |
from kivy.event import EventDispatcher | |
from kivy.adapters.adapter import Adapter | |
from kivy.adapters.models import SelectableDataItem | |
+from kivy.properties import ObjectProperty | |
from kivy.properties import ListProperty | |
from kivy.properties import DictProperty | |
from kivy.properties import BooleanProperty | |
from kivy.properties import OptionProperty | |
from kivy.properties import NumericProperty | |
+from kivy.properties import ObservableList | |
from kivy.lang import Builder | |
+class RangeObservingObservableList(ObservableList): | |
+ '''Adds range-observing intelligence to ObservableList''' | |
+ | |
+ # range_change is a normal python object consisting of: | |
+ # | |
+ # (data_op, (start_index, end_index) | |
+ # | |
+ # If the op does not cause a range change, range_change is set to None. | |
+ # | |
+ # Observers of data changes may consult range_change if needed, for | |
+ # example, listview needs to know details for scrolling. | |
+ # | |
+ # ListAdapter itself, the owner of data, is the first observer of data | |
+ # change that must react to delete ops, if the existing selection is | |
+ # affected. | |
+ # | |
+ | |
+ cached_views_and_data = DictProperty({}) | |
+ '''This is a temporary association used in sorting. It is created when we | |
+ sort, and destroyed by the adapter in its sort op callback. | |
+ ''' | |
+ | |
+ cached_views = ObjectProperty(None) | |
+ '''This is a reference to the containing adapter's cached_views, for use | |
+ in sorting operations. It is set by the adapter when needed. | |
+ ''' | |
+ | |
+ def __init__(self, *largs): | |
+ super(RangeObservingObservableList, self).__init__(*largs) | |
+ | |
+ def __setitem__(self, key, value): | |
+ self.range_change = None | |
+ super(RangeObservingObservableList, self).__setitem__(key, value) | |
+ | |
+ def __delitem__(self, key): | |
+ index = self.index(key) | |
+ self.range_change = ('delete', (index, index)) | |
+ super(RangeObservingObservableList, self).__delitem__(key) | |
+ | |
+ def __setslice__(self, *largs): | |
+ self.range_change = None | |
+ super(RangeObservingObservableList, self).__setslice__(*largs) | |
+ | |
+ def __delslice__(self, *largs): | |
+ start_index = largs[0] | |
+ end_index = largs[-1] | |
+ self.range_change = ('delete', (start_index, end_index)) | |
+ super(RangeObservingObservableList, self).__delslice__(*largs) | |
+ | |
+ def __iadd__(self, *largs): | |
+ self.range_change = None | |
+ super(RangeObservingObservableList, self).__iadd__(*largs) | |
+ | |
+ def __imul__(self, *largs): | |
+ self.range_change = None | |
+ super(RangeObservingObservableList, self).__imul__(*largs) | |
+ | |
+ def append(self, *largs): | |
+ index = len(self) | |
+ self.range_change = ('add', (index, index)) | |
+ super(RangeObservingObservableList, self).append(*largs) | |
+ | |
+ def remove(self, *largs): | |
+ index = self.index(largs[0]) | |
+ self.range_change = ('delete', (index, index)) | |
+ super(RangeObservingObservableList, self).remove(*largs) | |
+ | |
+ def insert(self, *largs): | |
+ index = self.index(largs[0]) | |
+ self.range_change = ('insert', (index, index)) | |
+ super(RangeObservingObservableList, self).insert(*largs) | |
+ | |
+ def pop(self, *largs): | |
+ if largs[0]: | |
+ index = self.index(largs[0]) | |
+ else: | |
+ index = len(self) - 1 | |
+ self.range_change = ('delete', (index, index)) | |
+ return super(RangeObservingObservableList, self).pop(*largs) | |
+ | |
+ def extend(self, *largs): | |
+ start_index = len(self) | |
+ end_index = start_index + len(largs) - 1 | |
+ self.range_change = ('add', (start_index, end_index)) | |
+ super(RangeObservingObservableList, self).extend(*largs) | |
+ | |
+ def sort(self, *largs): | |
+ if self.cached_views: | |
+ for item_view in self.cached_views: | |
+ self.cached_views_and_data[item_view] = \ | |
+ self.data[item_view.index] | |
+ | |
+ self.range_change = ('sort', (0, len(self) - 1)) | |
+ super(RangeObservingObservableList, self).sort(*largs) | |
+ | |
+ def reverse(self, *largs): | |
+ self.range_change = ('sort', (0, len(self) - 1)) | |
+ super(RangeObservingObservableList, self).reverse(*largs) | |
+ | |
+ | |
class ListAdapter(Adapter, EventDispatcher): | |
''' | |
A base class for adapters interfacing with lists, dictionaries or other | |
@@ -66,7 +168,7 @@ class ListAdapter(Adapter, EventDispatcher): | |
functonality. | |
''' | |
- data = ListProperty([]) | |
+ data = ListProperty([], cls=RangeObservingObservableList) | |
'''The data list property is redefined here, overriding its definition as | |
an ObjectProperty in the Adapter class. We bind to data so that any | |
changes will trigger updates. See also how the | |
@@ -115,6 +217,8 @@ class ListAdapter(Adapter, EventDispatcher): | |
for maintaining state in game play or other similar scenarios. It is a | |
convenience function. | |
+ NOTE: This would probably be better named as sync_selection_with_data(). | |
+ | |
To propagate selection or not? | |
Consider a shopping list application for shopping for fruits at the | |
@@ -173,16 +277,100 @@ class ListAdapter(Adapter, EventDispatcher): | |
defaults to {}. | |
''' | |
- __events__ = ('on_selection_change', ) | |
+ __events__ = ('on_selection_change',) | |
def __init__(self, **kwargs): | |
super(ListAdapter, self).__init__(**kwargs) | |
self.bind(selection_mode=self.selection_mode_changed, | |
allow_empty_selection=self.check_for_empty_selection, | |
- data=self.update_for_new_data) | |
+ data=self.data_changed) | |
+ | |
+ # Set a reference in data (an ObservableList instance) to our so that, | |
+ # in the case of sorting-related ops, an association can be made | |
+ # between the item_views in cached_views to the data_items in data, | |
+ # enabling a post-op update of cached_views indices. | |
+ self.data.cached_views = self.cached_views | |
+ | |
+ self.delete_cache() | |
+ self.initialize_selection() | |
+ | |
+ def data_changed(self, *dt): | |
+ | |
+ print 'ADAPTER data_changed callback', dt | |
+ | |
+ print self.data.range_change | |
+ | |
+ if self.data.range_change: | |
+ | |
+ data_op, (start_index, end_index) = self.data.range_change | |
+ | |
+ if data_op == 'add': | |
+ # The add op is an append, so this shouldn't affect anything. | |
+ pass | |
+ | |
+ elif data_op == 'delete': | |
- self.update_for_new_data() | |
+ selection_was_affected = False | |
+ | |
+ deleted_indices = range(start_index, end_index + 1) | |
+ | |
+ # Delete views from cache. | |
+ print 'cached_views', self.cached_views | |
+ deleted_indices = range(start_index, end_index + 1) | |
+ | |
+ new_cached_views = {} | |
+ | |
+ i = 0 | |
+ for k, v in self.cached_views.iteritems(): | |
+ if not k in deleted_indices: | |
+ new_cached_views[i] = self.cached_views[k] | |
+ if k >= start_index: | |
+ new_cached_views[i].index = i | |
+ i += 1 | |
+ | |
+ self.cached_views = new_cached_views | |
+ print 'cached_views', self.cached_views | |
+ | |
+ # Remove deleted views from selection. | |
+ for selected_index in [item.index for item in self.selection]: | |
+ if selected_index in deleted_indices: | |
+ del self.selection[selected_index] | |
+ selection_was_affected = True | |
+ | |
+ if selection_was_affected: | |
+ self.dispatch('on_selection_change') | |
+ | |
+ self.check_for_empty_selection() | |
+ | |
+ elif data_op == 'insert': | |
+ | |
+ inserted_indices = range(start_index, end_index + 1) | |
+ | |
+ new_cached_views = {} | |
+ | |
+ i = 0 | |
+ for k, v in self.cached_views.iteritems(): | |
+ new_cached_views[i] = self.cached_views[k] | |
+ i += 1 | |
+ if k >= start_index: | |
+ new_cached_views[i].index = i | |
+ | |
+ self.cached_views = new_cached_views | |
+ | |
+ elif data_op == 'sort': | |
+ | |
+ for item_view in self.cached_views: | |
+ item_view.index = self.data.index( | |
+ self.data.cached_views_and_data[item_view]) | |
+ | |
+ self.data.cached_views_and_data = {} | |
+ | |
+ def data_will_be_sorted(self, *args): | |
+ self.cached_views_with_data_items = {} | |
+ | |
+ for item_view in self.cached_views: | |
+ self.cached_views_with_data_items[item_view] = self.data[item_view.index] | |
def delete_cache(self, *args): | |
self.cached_views = {} | |
@@ -290,6 +478,7 @@ class ListAdapter(Adapter, EventDispatcher): | |
else: | |
self.deselect_item_view(view) | |
if self.selection_mode != 'none': | |
+ # | |
# If the deselection makes selection empty, the following call | |
# will check allows_empty_selection, and if False, will | |
# select the first item. If view happens to be the first item, | |
@@ -381,10 +570,6 @@ class ListAdapter(Adapter, EventDispatcher): | |
# [TODO] Could easily add select_all() and deselect_all(). | |
- def update_for_new_data(self, *args): | |
- self.delete_cache() | |
- self.initialize_selection() | |
- | |
def initialize_selection(self, *args): | |
if len(self.selection) > 0: | |
self.selection = [] | |
diff --git a/kivy/properties.pxd b/kivy/properties.pxd | |
index 9f1aa90..62a74dd 100644 | |
--- a/kivy/properties.pxd | |
+++ b/kivy/properties.pxd | |
@@ -44,7 +44,7 @@ cdef class StringProperty(Property): | |
pass | |
cdef class ListProperty(Property): | |
- pass | |
+ cdef object cls | |
cdef class DictProperty(Property): | |
pass | |
diff --git a/kivy/properties.pyx b/kivy/properties.pyx | |
index 585fc94..e9e6c2e 100644 | |
--- a/kivy/properties.pyx | |
+++ b/kivy/properties.pyx | |
@@ -578,23 +578,26 @@ cdef class ListProperty(Property): | |
def __init__(self, defaultvalue=None, **kw): | |
defaultvalue = defaultvalue or [] | |
+ self.cls = kw.get('cls', ObservableList) | |
+ | |
super(ListProperty, self).__init__(defaultvalue, **kw) | |
cpdef link(self, EventDispatcher obj, str name): | |
Property.link(self, obj, name) | |
cdef PropertyStorage ps = obj.__storage[self._name] | |
- ps.value = ObservableList(self, obj, ps.value) | |
+ ps.value = self.cls(self, obj, ps.value) | |
cdef check(self, EventDispatcher obj, value): | |
if Property.check(self, obj, value): | |
return True | |
- if type(value) is not ObservableList: | |
- raise ValueError('%s.%s accept only ObservableList' % ( | |
+ if type(value) is not self.cls: | |
+ raise ValueError('{}.{} accept only {}'.format( | |
obj.__class__.__name__, | |
- self.name)) | |
+ self.name, | |
+ self.cls.__name__)) | |
cpdef set(self, EventDispatcher obj, value): | |
- value = ObservableList(self, obj, value) | |
+ value = self.cls(self, obj, value) | |
Property.set(self, obj, value) | |
cdef inline void observable_dict_dispatch(object self): | |
@@ -678,23 +681,25 @@ cdef class DictProperty(Property): | |
def __init__(self, defaultvalue=None, **kw): | |
defaultvalue = defaultvalue or {} | |
+ self.cls = kw.get('cls', ObservableDict) | |
+ | |
super(DictProperty, self).__init__(defaultvalue, **kw) | |
cpdef link(self, EventDispatcher obj, str name): | |
Property.link(self, obj, name) | |
cdef PropertyStorage ps = obj.__storage[self._name] | |
- ps.value = ObservableDict(self, obj, ps.value) | |
+ ps.value = self.cls(self, obj, ps.value) | |
cdef check(self, EventDispatcher obj, value): | |
if Property.check(self, obj, value): | |
return True | |
- if type(value) is not ObservableDict: | |
- raise ValueError('%s.%s accept only ObservableDict' % ( | |
+ if type(value) is not self.cls: | |
+ raise ValueError('{}.{} accept only {}'.format( | |
obj.__class__.__name__, | |
- self.name)) | |
- | |
+ self.name, | |
+ self.cls.__name__)) | |
cpdef set(self, EventDispatcher obj, value): | |
- value = ObservableDict(self, obj, value) | |
+ value = self.cls(self, obj, value) | |
Property.set(self, obj, value) | |
diff --git a/kivy/uix/listview.py b/kivy/uix/listview.py | |
index 9f8acb2..1bd987c 100644 | |
--- a/kivy/uix/listview.py | |
+++ b/kivy/uix/listview.py | |
@@ -927,14 +927,17 @@ class ListView(AbstractView, EventDispatcher): | |
self.bind(size=self._trigger_populate, | |
pos=self._trigger_populate, | |
item_strings=self.item_strings_changed, | |
- adapter=self._trigger_populate) | |
+ adapter=self.adapter_changed) | |
+ #adapter=self._trigger_populate) | |
- # The bindings setup above sets self._trigger_populate() to fire | |
- # when the adapter changes, but we also need this binding for when | |
- # adapter.data and other possible triggers change for view updating. | |
- # We don't know that these are, so we ask the adapter to set up the | |
- # bindings back to the view updating function here. | |
- self.adapter.bind_triggers_to_view(self._trigger_reset_populate) | |
+ print 'binding to adapter.data', self.adapter.data | |
+ | |
+ self.adapter.bind(data=self.data_changed) | |
+ | |
+ def adapter_changed(self, *args): | |
+ self.adapter.bind(data=self.data_changed) | |
+ | |
+ self._trigger_populate() | |
# Added to set data when item_strings is set in a kv template, but it will | |
# be good to have also if item_strings is reset generally. | |
@@ -968,8 +971,23 @@ class ListView(AbstractView, EventDispatcher): | |
self._wstart = istart | |
self._wend = iend + 10 | |
+<<<<<<< HEAD | |
+ def _spopulate(self, *args): | |
+ self.populate() | |
+ | |
+ def _reset_spopulate(self, *args): | |
+ self._wend = None | |
+||||||| merged common ancestors | |
+ def _spopulate(self, *dt): | |
+======= | |
def _spopulate(self, *args): | |
+>>>>>>> Added new ObservableDict variant | |
self.populate() | |
+ # simulate the scroll again, only if we already scrolled before | |
+ # the position might not be the same, mostly because we don't know the | |
+ # size of the new item. | |
+ if hasattr(self, '_scroll_y'): | |
+ self._scroll(self._scroll_y) | |
def _reset_spopulate(self, *args): | |
self._wend = None | |
@@ -1044,5 +1062,78 @@ class ListView(AbstractView, EventDispatcher): | |
self.populate() | |
self.dispatch('on_scroll_complete') | |
+ def scroll_after_add(self): | |
+ if not self.scrolling: | |
+ available_height = self.height | |
+ index = self._index | |
+ | |
+ print 'index', index | |
+ print 'available_height', available_height | |
+ | |
+ while available_height > 0: | |
+ item_view = self.adapter.get_view(index) | |
+ if item_view is None: | |
+ break | |
+ index += 1 | |
+ available_height -= item_view.height | |
+ | |
+ print 'available_height', available_height | |
+ | |
+ if available_height <= 0: | |
+ self._index += 1 | |
+ | |
+ self.scrolling = True | |
+ self.populate() | |
+ self.dispatch('on_scroll_complete') | |
+ | |
def on_scroll_complete(self, *args): | |
self.scrolling = False | |
+ | |
+ def data_changed(self, *dt): | |
+ | |
+ print 'LISTVIEW data_changed callback', dt | |
+ | |
+ print self.adapter.data.range_change | |
+ | |
+ if self.adapter.data.range_change: | |
+ first_item_view = self.container.children[-1] | |
+ last_item_view = self.container.children[0] | |
+ | |
+ data_op, (start_index, end_index) = self.adapter.data.range_change | |
+ | |
+ change_in_range = False | |
+ | |
+ if (first_item_view.index <= start_index <= last_item_view.index | |
+ or | |
+ first_item_view.index <= end_index <= last_item_view.index): | |
+ change_in_range = True | |
+ | |
+ if data_op == 'add': | |
+ | |
+ self.scroll_after_add() | |
+ | |
+ elif data_op == 'delete': | |
+ | |
+ deleted_indices = range(start_index, end_index + 1) | |
+ num_deleted = len(deleted_indices) | |
+ | |
+ for item_view in self.container.children: | |
+ if item_view.index in deleted_indices: | |
+ self.container.remove_widget(item_view) | |
+ elif item_view.index > end_index: | |
+ item_view.index -= num_deleted | |
+ | |
+ if change_in_range: | |
+ self.scrolling = True | |
+ self.populate() | |
+ self.dispatch('on_scroll_complete') | |
+ | |
+ elif data_op == 'insert': | |
+ | |
+ self.scroll_after_add() | |
+ | |
+ elif data_op == 'sort': | |
+ | |
+ self.scrolling = True | |
+ self.populate() | |
+ self.dispatch('on_scroll_complete') |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment