Refactor for improved selection operations. SelectionSupport is no more as a separate mixin, and a new base adapter, CollectionAdapter, takes this code in, as well as the view caching code from AbstractView. Now CollectionAdapter is the hub for selection and view-handling operations. Made the default for selection to be restricted to views, with a new flag propagate_selection_to_data for this functionality. Started adding tests for adapters in isolation. Simplified initial selection and refreshing operations between adapter and listview.

This commit is contained in:
geojeff 2012-10-22 09:30:43 -05:00
parent 93c735767b
commit 04ab3cccca
16 changed files with 624 additions and 588 deletions

View File

@ -159,4 +159,6 @@ Notes from Adapter.py:
list_item_args_converter, as used in many examples and tests, to help show
the context for use.
- Make kwargs handling follow nice style in models.py, SimpleDataItem

View File

@ -1,5 +1,5 @@
from kivy.adapters.listadapter import ListAdapter
from kivy.adapters.mixins.selection import SelectableDataItem
from kivy.adapters.models import SelectableDataItem
from kivy.uix.gridlayout import GridLayout
from kivy.uix.listview import ListView, ListItemButton

View File

@ -1,5 +1,5 @@
from kivy.adapters.dictadapter import DictAdapter
from kivy.uix.selectableview import SelectableDataItem
from kivy.uix.selectableview import SelectableView
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.listview import ListView, ListItemButton

View File

@ -1,5 +1,6 @@
from kivy.adapters.dictadapter import DictAdapter
from kivy.uix.selectableview import SelectableDataItem
from kivy.adapters.models import SelectableDataItem
from kivy.uix.selectableview import SelectableView
from kivy.uix.listview import ListView, ListItemButton
from kivy.uix.gridlayout import GridLayout
from kivy.uix.boxlayout import BoxLayout

View File

@ -1,6 +1,6 @@
from kivy.adapters.dictadapter import DictAdapter
from kivy.properties import NumericProperty, ListProperty, \
BooleanProperty, AliasProperty
BooleanProperty, AliasProperty, ObjectProperty
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.listview import ListView, ListItemButton
@ -12,6 +12,7 @@ from kivy.uix.widget import Widget
class OpsDictAdapter(DictAdapter):
listview_id = NumericProperty(0)
owning_view = ObjectProperty(None)
def __init__(self, **kwargs):
self.listview_id = kwargs['listview_id']
@ -226,6 +227,8 @@ class OpsView(BoxLayout):
letters_list_view = ListView(adapter=letters_dict_adapter,
size_hint=(1.0, 1.0))
letters_dict_adapter.owning_view = letters_list_view
box_layout.add_widget(listview_header_widgets[listview_id])
box_layout.add_widget(letters_list_view)

View File

@ -74,13 +74,16 @@ class Adapter(EventDispatcher):
super(Adapter, self).__init__(**kwargs)
def bind_triggers_to_view(self, func):
self.bind(data=func)
def get_count(self):
if self.data:
return 1
else:
return 0
def get_item(self, index):
def get_data_item(self, index):
return self.data
def get_view(self, index): #pragma: no cover

View File

@ -6,23 +6,338 @@ CollectionAdapter
:class:`CollectionAdapter` is a base class for adapters dedicated to lists or
dicts or other collections of data.
Provides selection and view creation and management functionality to
"collection-style" views, such as :class:`ListView`, and their item views, via
the :class:`ListAdapter` and :class:`DictAdapter` subclasses.
'''
from kivy.properties import ObjectProperty
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.lang import Builder
class CollectionAdapter(Adapter):
class CollectionAdapter(Adapter, EventDispatcher):
'''
A base class for adapters interfacing with lists, dictionaries, or other
collection type data, adding selection and view creation and management
functonality.
'''
owning_view = ObjectProperty(None)
'''Management of selection requires manipulation of key view instances,
which are created in an adapter, but cached in the owning_view, such as a
:class:`ListView` instance. In some operations at the adapter level,
access is needed to the views.
selection = ListProperty([])
'''The selection list property is the container for selected items.
'''
selection_mode = OptionProperty('single',
options=('none', 'single', 'multiple'))
'''Selection modes:
none -- use the list as a simple list (no select action). This option
is here so that selection can be turned off, momentarily or
permanently, for an existing list adapter. :class:`ListAdapter`
is not meant to be used as a primary no-selection list adapter.
Use :class:`SimpleListAdapter` for that.
single -- multi-touch/click ignored. single item selecting only
multiple -- multi-touch / incremental addition to selection allowed;
may be limited to a count by selection_limit
'''
propagate_selection_to_data = BooleanProperty(False)
'''Normally, data items are not selected/deselected, because the data items
might not have an is_selected boolean property -- only the item view for a
given data item is selected/deselected, as part of the maintained selection
list. However, if the data items do have an is_selected property, or if
they mix in :class:`SelectableDataItem`, the selection machinery can
propagate selection to data items. This can be useful for storing selection
state in a local database or backend database for maintaining state in game
play or other similar needs. It is a convenience function.
To propagate selection or not?
Consider a shopping list application for shopping for fruits at the
market. The app allows selection of fruits to buy for each day of the
week, presenting seven lists, one for each day of the week. Each list is
loaded with all the available fruits, but the selection for each is a
subset. There is only one set of fruit data shared between the lists, so
it would not make sense to propagate selection to the data, because
selection in any of the seven lists would clobber and mix with that of the
others.
However, consider a game that uses the same fruits data for selecting
fruits available for fruit-tossing. A given round of play could have a
full fruits list, with fruits available for tossing shown selected. If the
game is saved and rerun, the full fruits list, with selection marked on
each item, would be reloaded fine if selection is always propagated to the
data. You could accomplish the same functionality by writing code to
operate on list selection, but having selection stored on the data might
prove convenient in some cases.
'''
allow_empty_selection = BooleanProperty(True)
'''The allow_empty_selection may be used for cascading selection between
several list views, or between a list view and an observing view. Such
automatic maintainence of selection is important for all but simple
list displays. Set allow_empty_selection False, so that selection is
auto-initialized, and always maintained, and so that any observing views
may likewise be updated to stay in sync.
'''
selection_limit = NumericProperty(0)
'''When selection_mode is multiple, if selection_limit is non-zero, this
number will limit the number of selected items. It can even be 1, which is
equivalent to single selection. This is because a program could be
programmatically changing selection_limit on the fly, and all possible
values should be included.
'''
cached_views = DictProperty({})
'''View instances for data items are instantiated and managed in the
adapter. Here we maintain a dictionary containing the view
instances keyed to the indices in the data.
This dictionary works as a cache. get_view() only asks for a view from
the adapter if one is not already stored for the requested index.
'''
def __init__(self, **kwargs):
super(CollectionAdapter, self).__init__(**kwargs)
self.register_event_type('on_selection_change')
self.bind(selection_mode=self.check_for_empty_selection,
allow_empty_selection=self.check_for_empty_selection,
data=self.update_for_new_data)
self.update_for_new_data()
def set_item_view(self, index, item_view):
pass
def delete_cache(self, *args):
self.cached_views = {}
def get_view(self, index):
if index in self.cached_views:
return self.cached_views[index]
item_view = self.create_view(index)
if item_view:
self.cached_views[index] = item_view
return item_view
def create_view(self, index):
'''This method is more complicated than the one in Adapter and
SimpleListAdapter, because here we create bindings for the data item,
and its children back to self.handle_selection(), and do other
selection-related tasks to keep item views in sync with the data.
'''
item = self.get_data_item(index)
if item is None:
return None
item_args = None
if self.args_converter:
item_args = self.args_converter(item)
else:
item_args = item
item_args['index'] = index
if self.cls:
view_instance = self.cls(**item_args)
else:
view_instance = Builder.template(self.template, **item_args)
if self.propagate_selection_to_data:
# The data item must be a subclass of SelectableDataItem, or must have
# an is_selected boolean or function, so it has is_selected available.
# If is_selected is unavailable on the data item, an exception is
# raised.
#
# [TODO] Only tested boolean is_selected.
#
# [TODO] Wouldn't use of getattr help a lot here?
#
if issubclass(item.__class__, SelectableDataItem):
if item.is_selected:
self.handle_selection(view_instance)
elif type(item) == dict and 'is_selected' in item:
if item['is_selected']:
self.handle_selection(view_instance)
elif hasattr(item, 'is_selected'):
if isfunction(item.is_selected) or ismethod(item.is_selected):
if item.is_selected():
self.handle_selection(view_instance)
else:
if item.is_selected:
self.handle_selection(view_instance)
else:
msg = "CollectionAdapter: unselectable data item for {0}".format(item)
raise Exception(msg)
view_instance.bind(on_release=self.handle_selection)
# [TODO] Tested?
for child in view_instance.children:
child.bind(on_release=self.handle_selection)
return view_instance
def on_selection_change(self, *args):
'''on_selection_change() is the default handler for the
on_selection_change event.
'''
pass
def handle_selection(self, view, *args):
if view not in self.selection:
if self.selection_mode in ['none', 'single'] and \
len(self.selection) > 0:
for selected_view in self.selection:
self.deselect_item_view(selected_view)
if self.selection_mode != 'none':
if self.selection_mode == 'multiple':
if self.allow_empty_selection:
if self.selection_limit > 0:
if len(self.selection) < self.selection_limit:
self.select_item_view(view)
else:
self.select_item_view(view)
else:
self.select_item_view(view)
else:
if self.selection_mode == 'none':
for selected_view in self.selection:
self.deselect_item_view(selected_view)
else:
self.deselect_item_view(view)
# 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,
# this will be a reselection, and the user will notice no
# change, except perhaps a flicker.
#
self.check_for_empty_selection()
self.dispatch('on_selection_change')
def select_data_item(self, item):
self.set_data_item_selection(item, True)
def deselect_data_item(self, item):
self.set_data_item_selection(item, False)
def set_data_item_selection(self, item, value):
if issubclass(item.__class__, SelectableDataItem):
item.is_selected = value
elif type(item) is dict:
item['is_selected'] = value
elif hasattr(item, 'is_selected'):
item.is_selected = value
else:
raise Exception('Selection: data item is not selectable')
def select_item_view(self, view):
view.select()
view.is_selected = True
self.selection.append(view)
# [TODO] sibling selection for composite items
# Needed? Or handled from parent?
# (avoid circular, redundant selection)
#if hasattr(view, 'parent') and hasattr(view.parent, 'children'):
#siblings = [child for child in view.parent.children if child != view]
#for sibling in siblings:
#if hasattr(sibling, 'select'):
#sibling.select()
# child selection
for child in view.children:
if hasattr(child, 'select'):
child.select()
if self.propagate_selection_to_data:
data_item = self.get_data_item(view.index)
self.select_data_item(data_item)
def select_list(self, view_list, extend):
'''The select call is made for the items in the provided view_list.
Arguments:
view_list: the list of item views to become the new selection, or
to add to the existing selection
extend: boolean for whether or not to extend the existing list
'''
# Select all the item views.
for view in view_list:
self.select_item_view(view)
# Extend or set selection.
if extend:
self.selection.extend(view_list)
else:
self.selection = view_list
self.dispatch('on_selection_change')
def deselect_item_view(self, view):
view.deselect()
view.is_selected = False
self.selection.remove(view)
# [TODO] sibling deselection for composite items
# Needed? Or handled from parent?
# (avoid circular, redundant selection)
#if hasattr(view, 'parent') and hasattr(view.parent, 'children'):
#siblings = [child for child in view.parent.children if child != view]
#for sibling in siblings:
#if hasattr(sibling, 'deselect'):
#sibling.deselect()
# child deselection
for child in view.children:
if hasattr(child, 'deselect'):
child.deselect()
if self.propagate_selection_to_data:
item = self.get_data_item(view.index)
self.deselect_data_item(item)
def deselect_list(self, l):
for view in l:
self.deselect_item_view(view)
self.dispatch('on_selection_change')
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 = []
self.dispatch('on_selection_change')
self.check_for_empty_selection()
def check_for_empty_selection(self, *args):
if not self.allow_empty_selection:
if len(self.selection) == 0:
# Select the first item if we have it.
v = self.get_view(0)
if v is not None:
print 'selecting first data item view', v, v.is_selected
self.handle_selection(v)
def trim_left_of_sel(self, *args): #pragma: no cover
'''Cut list items with indices in sorted_keys that are less than the

View File

@ -6,7 +6,7 @@ DictAdapter
:class:`DictAdapter` is an adapter around a python dictionary of records.
From :class:`Adapter`, :class:`SimpleListAdapter` gets these properties:
From :class:`Adapter`, :class:`DictAdapter` gets these properties:
Use only one:
@ -22,7 +22,7 @@ From :class:`Adapter`, :class:`SimpleListAdapter` gets these properties:
provided, a default one is set, that assumes that the
data items are strings.
From the :class:`SelectionSupport` mixin, :class:`DictAdapter` has
From the :class:`CollectionAdapter` mixin, :class:`DictAdapter` has
these properties:
- selection
@ -39,10 +39,10 @@ If you wish to have a bare-bones list adapter, without selection, use
from kivy.properties import ListProperty, DictProperty
from kivy.lang import Builder
from kivy.adapters.collectionadapter import CollectionAdapter
from kivy.adapters.mixins.selection import SelectionSupport
from kivy.adapters.models import SelectableDataItem
class DictAdapter(SelectionSupport, CollectionAdapter):
class DictAdapter(CollectionAdapter):
sorted_keys = ListProperty([])
'''The sorted_keys list property contains a list of hashable objects (can
@ -55,6 +55,8 @@ class DictAdapter(SelectionSupport, CollectionAdapter):
data = DictProperty(None)
'''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.
The values can be strings, class instances, dicts, etc.
'''
def __init__(self, **kwargs):
@ -67,76 +69,46 @@ class DictAdapter(SelectionSupport, CollectionAdapter):
super(DictAdapter, self).__init__(**kwargs)
self.bind(sorted_keys=self.initialize_sorted_keys,
data=self.initialize_data)
self.bind(sorted_keys=self.initialize_sorted_keys)
def bind_primary_key_to_func(self, func):
def bind_triggers_to_view(self, func):
self.bind(sorted_keys=func)
def sort_keys(self):
self.sorted_keys = sorted(self.sorted_keys)
self.bind(data=func)
def initialize_sorted_keys(self, *args):
self.initialize_selection()
def initialize_data(self, *args):
self.sorted_keys = sorted(self.data.keys())
self.delete_cache()
self.initialize_selection()
def get_count(self):
return len(self.sorted_keys)
def get_item(self, index):
def get_data_item(self, index):
if index < 0 or index >= len(self.sorted_keys):
return None
return self.data[self.sorted_keys[index]]
def get_view(self, index):
item = self.get_item(index)
if item is None:
return None
item_args = None
if self.args_converter:
item_args = self.args_converter(item)
def select_data_item(self, item):
# The data item must be a subclass of SelectableDataItem, or must have
# an is_selected boolean or function, so it has is_selected available.
# If is_selected is unavailable on the data item, an exception is
# raised.
#
if issubclass(item.__class__, SelectableDataItem):
item.is_selected = True
elif type(item) == dict and 'is_selected' in item:
item['is_selected'] = True
elif hasattr(item, 'is_selected'):
if isfunction(item.is_selected) or ismethod(item.is_selected):
item.is_selected()
else:
item.is_selected = True
else:
item_args = item
item_args['index'] = index
if self.cls:
#print 'CREATE VIEW FOR', index
view_instance = self.cls(**item_args)
else:
#print 'TEMPLATE item_args', item_args
view_instance = Builder.template(self.template, **item_args)
#print 'TEMPLATE view_instance.index', view_instance.index
if item['is_selected']:
self.handle_selection(view_instance)
# [TODO] if view_instance.handles_event('on_release'): ?
view_instance.bind(on_release=self.handle_selection)
# [TODO] If the whole composite can't respond, should we try to see
# if the children can? No harm, no foul on setting this?
for child in view_instance.children:
# if child.handles_event('on_release'): [TODO] ?
child.bind(on_release=self.handle_selection)
return view_instance
def check_for_empty_selection(self, *args):
if not self.allow_empty_selection:
if len(self.selection) == 0:
# Select the first key if we have it.
v = self.owning_view.get_item_view(0)
if v is not None:
#print 'selecting first list item view', v, v.is_selected
self.handle_selection(v)
msg = "ListAdapter: unselectable data item for {0}".format(item)
raise Exception(msg)
def touch_selection(self, *args):
self.dispatch('on_selection_change')
# [TODO] Also make methods for scroll_to_sel_start, scroll_to_sel_end,
# scroll_to_sel_middle.

View File

@ -4,181 +4,59 @@ ListAdapter
.. versionadded:: 1.5
:class:`SimpleListAdapter` is for simple lists, such as for showing a
text-only display of strings, or a list of views of some type that have
no user interaction.
:class:`ListAdapter` is an adapter around a python list.
:class:`ListAdapter` is has broader application, because it adds selection.
Its data items cannot be simple strings; they must be objects conforming to
the model of selection, providing text and is_selected properties.
From :class:`Adapter`, :class:`ListAdapter` gets these properties:
Use only one:
- cls, for a list key class to use to instantiate key view
instances
- template, a kv template to use to instantiate key view
instances
- args_converter, an optional function to transform data item argument
sets, in preparation for either a cls instantiation,
or a kv template invocation. If no args_converter is
provided, a default one is set, that assumes that the
data items are strings.
From the :class:`CollectionAdapter` mixin, :class:`ListAdapter` has
these properties:
- selection
- selection_mode
- allow_empty_selection
and several methods used in selection operations.
If you wish to have a bare-bones list adapter, without selection, use
:class:`SimpleListAdapter`.
'''
from kivy.properties import ListProperty, DictProperty, ObjectProperty
from kivy.lang import Builder
from kivy.adapters.collectionadapter import CollectionAdapter
from kivy.adapters.mixins.selection import SelectionSupport, \
SelectableDataItem
from kivy.adapters.models import SelectableDataItem
from inspect import isfunction, ismethod
class SimpleListAdapter(CollectionAdapter):
''':class:`SimpleListAdapter` is an adapter around a simple Python list.
From :class:`Adapter`, :class:`SimpleListAdapter` gets these properties:
Use only one:
- cls, for a list item class to use to instantiate item view
instances
- template, a kv template to use to instantiate item view
instances
- args_converter, an optional function to transform data item argument
sets, in preparation for either a cls instantiation,
or a kv template invocation (If an args_converter is
not provided, a default one that assumes simple
strings content is set)
'''
data = ListProperty([])
'''The data list property contains a list of objects (can be strings) that
will be used directly if no args_converter function is provided. If there
is an args_converter, the data objects will be passed to it, for
instantiation of item view class (cls) instances from the data.
'''
def __init__(self, **kwargs):
if 'data' not in kwargs:
raise Exception('list adapter: input must include data argument')
if type(kwargs['data']) not in (tuple, list):
raise Exception('list adapter: data must be a tuple or list')
super(SimpleListAdapter, self).__init__(**kwargs)
def get_count(self):
return len(self.data)
def get_item(self, index):
if index < 0 or index >= len(self.data):
return None
return self.data[index]
# Returns a view instance for an item.
def get_view(self, index):
item = self.get_item(index)
if item is None:
return None
item_args = self.args_converter(item)
if self.cls:
instance = self.cls(**item_args)
return instance
else:
return Builder.template(self.template, **item_args)
class ListAdapter(SelectionSupport, SimpleListAdapter):
'''From the :class:`SelectionSupport` mixin, :class:`ListAdapter` has
these properties:
- selection
- selection_mode
- allow_empty_selection
and several methods used in selection operations.
If you wish to have a bare-bones list adapter, without selection, use
:class:`SimpleListAdapter`.
:class:`ListAdapter`, by adding selection, has the requirement that data
items be instances of a subclass of :class:`SelectableView` (Do not use
simple strings as data items).
'''
class ListAdapter(CollectionAdapter):
def __init__(self, **kwargs):
super(ListAdapter, self).__init__(**kwargs)
# Reset and update selection, in SelectionSupport, if data changes.
self.bind(data=self.initialize_selection)
def get_count(self):
return len(self.data)
def bind_primary_key_to_func(self, func):
self.bind(data=func)
def get_view(self, index):
'''This method is more complicated than the one in Adapter and
SimpleListAdapter, because here we create bindings for the data item,
and its children back to self.handle_selection(), in the mixed-in
:class:`SelectionSupport` class, and do other selection-related tasks
to keep item views in sync with the data.
'''
item = self.get_item(index)
if item is None:
def get_data_item(self, index):
if index < 0 or index >= len(self.data):
return None
return self.data[index]
item_args = None
if self.args_converter:
item_args = self.args_converter(item)
else:
item_args = item
item_args['index'] = index
if self.cls:
print 'CREATE VIEW FOR', index
view_instance = self.cls(**item_args)
else:
print 'TEMPLATE item_args', item_args
view_instance = Builder.template(self.template, **item_args)
print 'TEMPLATE view_instance.index', view_instance.index
# The data item must be a subclass of SelectableView, or must have an
# is_selected boolean or function, so it has is_selected available.
# If is_selected is unavailable on the data item, an exception is
# raised.
#
# [TODO] Only tested boolean is_selected.
#
# [TODO] Wouldn't use of getattr help a lot here?
#
if issubclass(item.__class__, SelectableDataItem):
if item.is_selected:
self.handle_selection(view_instance)
elif type(item) == dict and 'is_selected' in item:
if item['is_selected']:
self.handle_selection(view_instance)
elif hasattr(item, 'is_selected'):
if isfunction(item.is_selected) or ismethod(item.is_selected):
if item.is_selected():
self.handle_selection(view_instance)
else:
if item.is_selected:
self.handle_selection(view_instance)
else:
msg = "ListAdapter: unselectable data item for {0}".format(item)
raise Exception(msg)
# [TODO] if view_instance.handles_event('on_release'): ?
view_instance.bind(on_release=self.handle_selection)
# [TODO] If the whole composite can't respond, should we try to see
# if the children can? No harm, no foul on setting this?
for child in view_instance.children:
# if child.handles_event('on_release'): [TODO] ?
child.bind(on_release=self.handle_selection)
return view_instance
def check_for_empty_selection(self, *args):
if not self.allow_empty_selection:
if len(self.selection) == 0:
# Select the first item if we have it.
v = self.owning_view.get_item_view(0)
if v is not None:
print 'selecting first data item view', v, v.is_selected
self.handle_selection(v)
def bind_triggers_to_view(self, func):
self.bind(data=func)
def touch_selection(self, *args):
self.dispatch('on_selection_change')

View File

@ -1,216 +0,0 @@
'''
SelectableView, SelectionSupport
================================
.. versionadded:: 1.5
Mixin classes for giving selection functionality to "collection-style" views,
such as :class:`ListView`, and their item views, via the intermediating
control of :class:`ListAdapter`, or one of its subclasses.
'''
from kivy.properties import ListProperty, BooleanProperty, \
OptionProperty, NumericProperty
from kivy.event import EventDispatcher
class SelectableDataItem(object):
def __init__(self, **kwargs):
super(SelectableDataItem, self).__init__()
if 'is_selected' in kwargs:
self.is_selected = kwargs['is_selected']
else:
self.is_selected = False
class SelectionSupport(EventDispatcher):
'''The :class:`SelectionSupport` mixin is used for selection. Any
"collection" view, such as :class:`ListView`.
'''
selection = ListProperty([])
'''The selection list property is the container for selected items.
'''
selection_mode = OptionProperty('single',
options=('none', 'single', 'multiple'))
'''Selection modes:
none -- use the list as a simple list (no select action). This option
is here so that selection can be turned off, momentarily or
permanently, for an existing list adapter. :class:`ListAdapter`
is not meant to be used as a primary no-selection list adapter.
Use :class:`SimpleListAdapter` for that.
single -- multi-touch/click ignored. single item selecting only
multiple -- multi-touch / incremental addition to selection allowed;
may be limited to a count by selection_limit
'''
allow_empty_selection = BooleanProperty(True)
'''The allow_empty_selection may be used for cascading selection between
several list views, or between a list view and an observing view. Such
automatic maintainence of selection is important for all but simple
list displays. Set allow_empty_selection False, so that selection is
auto-initialized, and always maintained, and so that any observing views
may likewise be updated to stay in sync.
'''
selection_limit = NumericProperty(0)
'''When selection_mode is multiple, if selection_limit is non-zero, this
number will limit the number of selected items. It can even be 1, which is
equivalent to single selection. This is because a program could be
programmatically changing selection_limit on the fly, and all possible
values should be included.
'''
def __init__(self, **kwargs):
super(SelectionSupport, self).__init__(**kwargs)
self.register_event_type('on_selection_change')
self.bind(selection_mode=self.check_for_empty_selection,
allow_empty_selection=self.check_for_empty_selection)
def on_selection_change(self, *args):
'''on_selection_change() is the default handler for the
on_selection_change event.
'''
pass
def handle_selection(self, view, *args):
if view not in self.selection:
if self.selection_mode in ['none', 'single'] and \
len(self.selection) > 0:
for selected_view in self.selection:
self.deselect_item_view(selected_view)
if self.selection_mode != 'none':
if self.selection_mode == 'multiple':
if self.allow_empty_selection:
if self.selection_limit > 0:
if len(self.selection) < self.selection_limit:
self.select_item_view(view)
else:
self.select_item_view(view)
else:
self.select_item_view(view)
else:
if self.selection_mode == 'none':
for selected_view in self.selection:
self.deselect_item_view(selected_view)
else:
self.deselect_item_view(view)
# 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,
# this will be a reselection, and the user will notice no
# change, except perhaps a flicker.
#
# [TODO] Is this approach OK?
#
self.check_for_empty_selection()
print 'selection for', self, 'is now', self.selection
self.dispatch('on_selection_change')
def select_data_item(self, item):
self.set_data_item_selection(item, True)
def deselect_data_item(self, item):
self.set_data_item_selection(item, False)
def set_data_item_selection(self, item, value):
#print 'set_data_item_selection', item, value
if issubclass(item.__class__, SelectableDataItem):
item.is_selected = value
elif type(item) is dict:
item['is_selected'] = value
elif hasattr(item, 'is_selected'):
item.is_selected = value
else:
raise Exception('Selection: data item is not selectable')
def select_item_view(self, view):
view.select()
view.is_selected = True
#print 'selected', view, view.is_selected
self.selection.append(view)
# [TODO] sibling selection for composite items
# Needed? Or handled from parent?
# (avoid circular, redundant selection)
#if hasattr(view, 'parent') and hasattr(view.parent, 'children'):
#siblings = [child for child in view.parent.children if child != view]
#for sibling in siblings:
#if hasattr(sibling, 'select'):
#sibling.select()
# child selection
for child in view.children:
if hasattr(child, 'select'):
child.select()
data_item = self.get_item(view.index)
self.select_data_item(data_item)
def select_list(self, view_list, extend):
'''The select call is made for the items in the provided view_list.
Arguments:
view_list: the list of item views to become the new selection, or
to add to the existing selection
extend: boolean for whether or not to extend the existing list
'''
# Select all the item views.
for view in view_list:
self.select_item_view(view)
# Extend or set selection.
if extend:
self.selection.extend(view_list)
else:
self.selection = view_list
self.dispatch('on_selection_change')
def deselect_item_view(self, view):
view.deselect()
view.is_selected = False
self.selection.remove(view)
# [TODO] sibling deselection for composite items
# Needed? Or handled from parent?
# (avoid circular, redundant selection)
#if hasattr(view, 'parent') and hasattr(view.parent, 'children'):
#siblings = [child for child in view.parent.children if child != view]
#for sibling in siblings:
#if hasattr(sibling, 'deselect'):
#sibling.deselect()
# child deselection
for child in view.children:
if hasattr(child, 'deselect'):
child.deselect()
item = self.get_item(view.index)
self.deselect_data_item(item)
def deselect_list(self, l):
for view in l:
self.deselect_item_view(view)
self.dispatch('on_selection_change')
def initialize_selection(self, *args):
'''Called when data changes.
'''
if len(self.selection) > 0:
self.selection = []
self.dispatch('on_selection_change')
# NOTE: self.check_for_empty_selection() now called at the end of
# listview.hard_populate(). If called here, it comes before
# the listview builds its new item views.

View File

@ -0,0 +1,71 @@
'''
SimpleListAdapter
=================
.. versionadded:: 1.5
:class:`SimpleListAdapter` is for simple lists, such as for showing a
text-only display of strings, or a list of views of some type that have
no user interaction.
'''
from kivy.adapters.adapter import Adapter
from kivy.properties import ListProperty
from kivy.lang import Builder
class SimpleListAdapter(Adapter):
''':class:`SimpleListAdapter` is an adapter around a simple Python list.
From :class:`Adapter`, :class:`SimpleListAdapter` gets these properties:
Use only one:
- cls, for a list item class to use to instantiate item view
instances
- template, a kv template to use to instantiate item view
instances
- args_converter, an optional function to transform data item argument
sets, in preparation for either a cls instantiation,
or a kv template invocation (If an args_converter is
not provided, a default one that assumes simple
strings content is set)
'''
data = ListProperty([])
'''The data list property contains a list of objects (can be strings) that
will be used directly if no args_converter function is provided. If there
is an args_converter, the data objects will be passed to it, for
instantiation of item view class (cls) instances from the data.
'''
def __init__(self, **kwargs):
if 'data' not in kwargs:
raise Exception('list adapter: input must include data argument')
if type(kwargs['data']) not in (tuple, list):
raise Exception('list adapter: data must be a tuple or list')
super(SimpleListAdapter, self).__init__(**kwargs)
def get_count(self):
return len(self.data)
def get_data_item(self, index):
if index < 0 or index >= len(self.data):
return None
return self.data[index]
# Returns a view instance for an item.
def get_view(self, index):
item = self.get_data_item(index)
if item is None:
return None
item_args = self.args_converter(item)
if self.cls:
instance = self.cls(**item_args)
return instance
else:
return Builder.template(self.template, **item_args)

View File

@ -5,13 +5,13 @@ Adapter tests
import unittest
from kivy.adapters.mixins.selection import SelectableDataItem
from kivy.uix.selectableview import SelectableView
from kivy.uix.listview import ListItemButton
from kivy.uix.label import Label
from kivy.adapters.models import SelectableDataItem
from kivy.adapters.adapter import Adapter
from kivy.adapters.listadapter import SimpleListAdapter
from kivy.adapters.simplelistadapter import SimpleListAdapter
from kivy.adapters.listadapter import ListAdapter
from kivy.adapters.dictadapter import DictAdapter
@ -197,7 +197,7 @@ class CategoryItem(SelectableDataItem):
super(CategoryItem, self).__init__(**kwargs)
self.name = kwargs.get('name', '')
self.fruits = kwargs.get('fruits', [])
self.is_selected = kwargs.get('is_selected', False)
#self.is_selected = kwargs.get('is_selected', False)
class FruitItem(SelectableDataItem):
@ -206,7 +206,7 @@ class FruitItem(SelectableDataItem):
self.name = kwargs.get('name', '')
self.serving_size = kwargs.get('Serving Size', '')
self.data = kwargs.get('data', [])
self.is_selected = kwargs.get('is_selected', False)
#self.is_selected = kwargs.get('is_selected', False)
def reset_to_defaults(db_dict):
@ -223,9 +223,10 @@ fruit_data_items = \
class FruitsListAdapter(ListAdapter):
def __init__(self, **kwargs):
kwargs['args_converter'] = lambda rec: {'text': rec['name'],
'size_hint_y': None,
'height': 25}
kwargs['args_converter'] = \
lambda selectable: {'text': selectable.name,
'size_hint_y': None,
'height': 25}
super(FruitsListAdapter, self).__init__(**kwargs)
def fruit_category_changed(self, fruit_categories_adapter, *args):
@ -366,11 +367,11 @@ class AdaptersTestCase(unittest.TestCase):
adapter = Adapter(data='cat', cls=Label)
self.assertEqual(adapter.get_count(), 1)
self.assertEqual(adapter.get_item(0), 'cat')
self.assertEqual(adapter.get_data_item(0), 'cat')
adapter = Adapter(data=None, cls=Label)
self.assertEqual(adapter.get_count(), 0)
self.assertEqual(adapter.get_item(0), None)
self.assertEqual(adapter.get_data_item(0), None)
def test_instantiating_simple_list_adapter(self):
# with no data
@ -391,10 +392,10 @@ class AdaptersTestCase(unittest.TestCase):
simple_list_adapter = SimpleListAdapter(data=['cat', 'dog'],
cls=Label)
self.assertEqual(simple_list_adapter.get_count(), 2)
self.assertEqual(simple_list_adapter.get_item(0), 'cat')
self.assertEqual(simple_list_adapter.get_item(1), 'dog')
self.assertIsNone(simple_list_adapter.get_item(-1))
self.assertIsNone(simple_list_adapter.get_item(2))
self.assertEqual(simple_list_adapter.get_data_item(0), 'cat')
self.assertEqual(simple_list_adapter.get_data_item(1), 'dog')
self.assertIsNone(simple_list_adapter.get_data_item(-1))
self.assertIsNone(simple_list_adapter.get_data_item(2))
view = simple_list_adapter.get_view(0)
self.assertTrue(isinstance(view, Label))
@ -418,7 +419,7 @@ class AdaptersTestCase(unittest.TestCase):
self.assertEqual(list_adapter.args_converter, self.args_converter)
self.assertEqual(list_adapter.template, None)
apple_data_item = list_adapter.get_item(0)
apple_data_item = list_adapter.get_data_item(0)
self.assertTrue(isinstance(apple_data_item, FruitItem))
def test_list_adapter_with_dicts_as_data(self):
@ -441,7 +442,7 @@ class AdaptersTestCase(unittest.TestCase):
self.assertEqual(list_adapter.cls, ListItemButton)
self.assertEqual(list_adapter.args_converter, args_converter)
data_item = list_adapter.get_item(0)
data_item = list_adapter.get_data_item(0)
self.assertTrue(type(data_item), dict)
def test_list_adapter_bindings(self):
@ -539,7 +540,7 @@ class AdaptersTestCase(unittest.TestCase):
view = fruit_categories_list_adapter.get_view(0)
self.assertEqual(view.__class__.__name__, 'CustomListItem')
def test_dict_adapter_selection_mode_none(self):
def test_dict_adapter_selection_mode_single_without_propagation(self):
list_item_args_converter = lambda rec: {'text': rec['name'],
'size_hint_y': None,
@ -562,6 +563,97 @@ class AdaptersTestCase(unittest.TestCase):
self.assertEqual(dict_adapter.args_converter, list_item_args_converter)
self.assertEqual(dict_adapter.template, None)
apple_data_item = dict_adapter.get_item(0)
apple_data_item = dict_adapter.get_data_item(0)
self.assertTrue(isinstance(apple_data_item, dict))
self.assertEqual(apple_data_item['name'], 'Apple')
apple_view = dict_adapter.get_view(0)
self.assertTrue(isinstance(apple_view, ListItemButton))
self.assertEqual(len(dict_adapter.selection), 1)
self.assertTrue(apple_view.is_selected)
self.assertFalse(apple_data_item['is_selected'])
def test_dict_adapter_selection_mode_single_with_propagation(self):
list_item_args_converter = lambda rec: {'text': rec['name'],
'size_hint_y': None,
'height': 25}
dict_adapter = DictAdapter(sorted_keys=sorted(fruit_data.keys()),
data=fruit_data,
args_converter=list_item_args_converter,
propagate_selection_to_data=True,
selection_mode='single',
allow_empty_selection=False,
cls=ListItemButton)
self.assertEqual(sorted(dict_adapter.data),
['Apple', 'Avocado', 'Banana', 'Cantaloupe', 'Cherry', 'Grape',
'Grapefruit', 'Honeydew', 'Kiwifruit', 'Lemon', 'Lime',
'Nectarine', 'Orange', 'Peach', 'Pear', 'Pineapple', 'Plum',
'Strawberry', 'Tangerine', 'Watermelon'])
self.assertEqual(dict_adapter.cls, ListItemButton)
self.assertEqual(dict_adapter.args_converter, list_item_args_converter)
self.assertEqual(dict_adapter.template, None)
apple_data_item = dict_adapter.get_data_item(0)
self.assertTrue(isinstance(apple_data_item, dict))
self.assertEqual(apple_data_item['name'], 'Apple')
apple_view = dict_adapter.get_view(0)
self.assertTrue(isinstance(apple_view, ListItemButton))
self.assertEqual(len(dict_adapter.selection), 1)
self.assertTrue(apple_view.is_selected)
self.assertTrue(apple_data_item['is_selected'])
def test_dict_adapter_sorted_keys(self):
list_item_args_converter = lambda rec: {'text': rec['name'],
'size_hint_y': None,
'height': 25}
dict_adapter = DictAdapter(sorted_keys=sorted(fruit_data.keys()),
data=fruit_data,
args_converter=list_item_args_converter,
propagate_selection_to_data=True,
selection_mode='single',
allow_empty_selection=False,
cls=ListItemButton)
self.assertEqual(sorted(dict_adapter.data),
['Apple', 'Avocado', 'Banana', 'Cantaloupe', 'Cherry', 'Grape',
'Grapefruit', 'Honeydew', 'Kiwifruit', 'Lemon', 'Lime',
'Nectarine', 'Orange', 'Peach', 'Pear', 'Pineapple', 'Plum',
'Strawberry', 'Tangerine', 'Watermelon'])
apple_view = dict_adapter.get_view(0)
self.assertEqual(apple_view.text, 'Apple')
avocado_view = dict_adapter.get_view(1)
self.assertEqual(avocado_view.text, 'Avocado')
banana_view = dict_adapter.get_view(2)
self.assertEqual(banana_view.text, 'Banana')
dict_adapter.sorted_keys = ['Lemon', 'Pear', 'Tangerine']
self.assertEqual(len(dict_adapter.sorted_keys), 3)
self.assertEqual(sorted(dict_adapter.data),
['Apple', 'Avocado', 'Banana', 'Cantaloupe', 'Cherry', 'Grape',
'Grapefruit', 'Honeydew', 'Kiwifruit', 'Lemon', 'Lime',
'Nectarine', 'Orange', 'Peach', 'Pear', 'Pineapple', 'Plum',
'Strawberry', 'Tangerine', 'Watermelon'])
lemon_view = dict_adapter.get_view(0)
self.assertEqual(lemon_view.text, 'Lemon')
pear_view = dict_adapter.get_view(1)
self.assertEqual(pear_view.text, 'Pear')
tangerine_view = dict_adapter.get_view(2)
self.assertEqual(tangerine_view.text, 'Tangerine')

View File

@ -25,23 +25,4 @@ class AbstractView(FloatLayout):
common example is the ListAdapter used for managing data items in a list.
'''
item_view_instances = DictProperty({})
'''View instances for data items are instantiated and managed in the
associated adapter. Here we maintain a dictionary containing the view
instances keyed to the indices in the data.
Effectively, this dictionary works as a cache, only asking for a view from
the adapter if one is not already stored for the requested index.
'''
def set_item_view(self, index, item_view):
pass
def get_item_view(self, index):
item_view_instances = self.item_view_instances
if index in item_view_instances:
return item_view_instances[index]
item_view = self.adapter.get_view(index)
if item_view:
item_view_instances[index] = item_view
return item_view

View File

@ -13,11 +13,6 @@ From AbstractView we have these properties and methods:
- adapter, an instance of SimpleListAdapter, ListAdapter, or DictAdapter
- item_view_instances, a dict with indices as keys to the list item view
instances created in the adapter
- set_item_view() and get_item_view() methods to list item view instances
Basic Example
-------------
@ -47,11 +42,10 @@ Here we make a listview with 100 items.
Using a ListAdapter
-------------------
Behind the scenes, the basic example above uses
uses :class:`SimpleListAdapter`. When the constructor for
:class:`ListView` sees that only a list of strings is provided as an argument,
called item_strings, it creates an instance of :class:`SimpleListAdapter` with
the list of strings.
Behind the scenes, the basic example above uses :class:`SimpleListAdapter`.
When the constructor for :class:`ListView` sees that only a list of strings is
provided as an argument, called item_strings, it creates an instance of
:class:`SimpleListAdapter` with the list of strings.
Simple in :class:`SimpleListAdapter` means: WITHOUT SELECTION SUPPORT -- it is
just a scrollable list of items, which do not respond to touch events.
@ -64,35 +58,23 @@ do:
cls=Label)
list_view = ListView(adapter=simple_list_adapter)
SelectionSupport: ListAdapter and DictAdapter
The instance of :class:`SimpleListAdapter` has a required data argument, which
contains data items to use as the basis for list items, along with a cls
argument for the class to be instantiated for each list item from the data.
CollectionAdapter: ListAdapter and DictAdapter
---------------------------------------------
For many uses of a list, the data is more than a simple list or strings, or
For many uses of a list, the data is more than a simple list of strings and
selection functionality is needed. :class:`ListAdapter` and
:class:`DictAdapter` each subclass :class:`SelectionSupport`.
:class:`DictAdapter` each subclass :class:`CollectionAdapter`, extending its
base functionality for selection.
See the :class:`ListAdapter` docs for details, but here we have synopses of
See the :class:`ListAdapter` docs for details, but here are synopses of
its arguments:
- data: a list of Python class instances or dicts that must have
a text property and an is_selected property.
When working with classes as data items, the is_selected property
is provided by :class:`SelectableDataItem`, which is intended to
be used as a mixin:
MyCustomDataItem(SelectableDataItem):
def __init__(self, **kwargs):
super(MyCustomDataItem, self).__init__(**kwargs)
self.text = kwargs.get('name', '')
# etc.
data = [MyCustomDataItem(name=n) for n in ['Bill', 'Sally']
Or, you may wish to provide a simple list of dicts:
data = \
[{'text': str(i), 'is_selected': False} for i in [1,2,3]]
- data: strings, class instances, dicts, etc. that form the basis data
for instantiating view item classes.
- cls: a Kivy view that is to be instantiated for each list item. There
are several built-in types available, including ListItemLabel and
@ -101,17 +83,17 @@ its arguments:
or
- template: the name of a Kivy language (kv) template that defines the
view
Kivy view for each list item.
NOTE: Pick only one, cls or template, to provide as an argument.
- args_converter: a function that takes a list item object as input, and
uses the object to build and return an args dict, ready
- args_converter: a function that takes a data item object as input, and
uses it to build and return an args dict, ready
to be used in a call to instantiate the item view cls or
template. In the case of cls, the args dict acts as a
kwargs object. For a template, it is treated as a context
(ctx), but is essentially similar in form. See the
examples and docs for template operation.
examples and docs for template use.
- selection_mode: a string for: 'single', 'multiple' or others (See docs).
@ -144,12 +126,10 @@ except for two things:
1) There is an additional argument, sorted_keys, which must meet the
requirements of normal python dictionary keys.
2) The data argument is not a list of class instances, it is, as you would
expect, a dict. Keys in the dict must include the keys in the
sorted_keys argument, but they may form a superset of the keys in
sorted_keys. Values may be class instances or dicts -- these follow the
same rules as the items of the data argument, described above for
:class:`ListAdapter`.
2) The data argument is, as you would expect, a dict. Keys in the dict
must include the keys in the sorted_keys argument, but they may form a
superset of the keys in sorted_keys. Values may be strings, class
instances, dicts, etc. (The args_converter uses it, accordingly).
Using an Args Converter
-----------------------
@ -173,7 +153,7 @@ specified as a normal Python function:
'size_hint_y': None,
'height': 25}
In args converter example above, the data item is assumed to be an object
In the args converter example above, the data item is assumed to be an object
(class instance), hence the reference an_obj.text.
Here is an example of an args converter that works with list data items that
@ -186,31 +166,6 @@ are dicts:
So, it is the responsibility of the developer to code the args_converter
according to the data at hand.
**An args converter used with cls argument**
Inside the :class:`ListView` code, the args converter function is used with the
provided view argument, in this case cls:
cls(**args_converter(data_item))
Here, if cls is ListItemButton, it would be equivalent to:
ListItemButton(text=an_obj.text, size_hint_y=None, height=25)
for each list data item.
**An args converter used with a template argument**
In the case of a kv template used as a list item view, the args_converter will
provide the context for the template, not really an args dict strictly
speaking, but it looks the same inside :class:`ListView`:
template(**args_converter(data_item))
The only difference between this args converter and the one above is
that the reference is to a dictionary (a_dict['text']), vs. reference to a
class instance (an_obj.text).
An Example ListView
-------------------
@ -244,7 +199,7 @@ Uses for Selection
------------------
In the previous example, we saw how a listview gains selection support just by
using ListAdapter, which subclasses SelectionSupport.
using ListAdapter, which subclasses CollectionAdapter.
What can we do with selection? Combining selection with the system of bindings
in Kivy, we can build a wide range of user interface designs.
@ -287,7 +242,7 @@ from kivy.uix.widget import Widget
from kivy.uix.button import Button
from kivy.uix.label import Label
from kivy.uix.boxlayout import BoxLayout
from kivy.adapters.listadapter import SimpleListAdapter
from kivy.adapters.simplelistadapter import SimpleListAdapter
from kivy.uix.abstractview import AbstractView
from kivy.uix.selectableview import SelectableView
from kivy.properties import ObjectProperty, DictProperty, \
@ -389,7 +344,6 @@ class CompositeListItem(SelectableView, BoxLayout):
# represents. Get it from kwargs and pass it along to children in the
# loop below.
index = kwargs['index']
print 'COMPOSITE list item index', index
for cls_dict in kwargs['cls_dicts']:
cls = cls_dict['cls']
@ -504,44 +458,34 @@ class ListView(AbstractView, EventDispatcher):
_wend = NumericProperty(None)
def __init__(self, **kwargs):
# Intercept for the adapter property, which would pass through to
# AbstractView, to check for its existence. If it doesn't exist, we
# assume that the data list is to be used with SimpleListAdapter
# to make a simple list. If it does exist, and data was also
# provided, raise an exception, because if an adapter is provided, it
# should be a fully-fledged adapter with its own data.
# Check for an adapter argument. If it doesn't exist, we
# assume that item_strings is to be used with SimpleListAdapter
# to make a simple list. In this case, if item_strings was not
# provided, raise an exception.
if 'adapter' not in kwargs:
if 'item_strings' not in kwargs:
raise Exception('ListView: input needed, or an adapter')
list_adapter = SimpleListAdapter(data=kwargs['item_strings'],
cls=Label)
kwargs['adapter'] = list_adapter
super(ListView, self).__init__(**kwargs)
self.adapter.owning_view = self
self.register_event_type('on_scroll_complete')
self._trigger_populate = Clock.create_trigger(self._spopulate, -1)
# [TODO] Is this "hard" scheme needed -- better way?
self._trigger_hard_populate = \
Clock.create_trigger(self._hard_spopulate, -1)
self.bind(size=self._trigger_populate,
pos=self._trigger_populate,
adapter=self._trigger_populate)
self.register_event_type('on_scroll_complete')
# The adapter does not necessarily use the data property for its
# primary key, so we let it set up the binding. This is associated
# with selection operations, which :class:`SimpleListAdapter` does
# not support, so we check if the function is available.
if hasattr(self.adapter, 'bind_primary_key_to_func'):
self.adapter.bind_primary_key_to_func(self._trigger_hard_populate)
# If our adapter supports selection, check the allow_empty_selection
# property and ensure selection if needed.
if hasattr(self.adapter, 'check_for_empty_selection'):
self.adapter.check_for_empty_selection()
# 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_populate)
def _scroll(self, scroll_y):
if self.row_height is None:
@ -572,14 +516,7 @@ class ListView(AbstractView, EventDispatcher):
def _spopulate(self, *dt):
self.populate()
def _hard_spopulate(self, *dt):
print 'hard_populate', dt
self.item_view_instances = {}
self.populate()
self.adapter.check_for_empty_selection()
def populate(self, istart=None, iend=None):
print 'populate', self, istart, iend
container = self.container
sizes = self._sizes
rh = self.row_height
@ -604,8 +541,7 @@ class ListView(AbstractView, EventDispatcher):
# now fill with real item_view
index = istart
while index <= iend:
print '----- ListView get_item_view, iend, index', iend, index
item_view = self.get_item_view(index)
item_view = self.adapter.get_view(index)
index += 1
if item_view is None:
continue
@ -617,8 +553,7 @@ class ListView(AbstractView, EventDispatcher):
index = self._index
count = 0
while available_height > 0:
print '----- ListView get_item_view, index', index
item_view = self.get_item_view(index)
item_view = self.adapter.get_view(index)
if item_view is None:
break
sizes[index] = item_view.height
@ -631,7 +566,7 @@ class ListView(AbstractView, EventDispatcher):
self._count = count
# extrapolate the full size of the container from the size
# of item_view_instances
# of view instances in the adapter
if count:
container.height = \
real_height / count * self.adapter.get_count()

View File

@ -306,7 +306,6 @@ setup(
packages=[
'kivy',
'kivy.adapters',
'kivy.adapters.mixins',
'kivy.core',
'kivy.core.audio',
'kivy.core.camera',