mirror of https://github.com/kivy/kivy.git
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:
parent
93c735767b
commit
04ab3cccca
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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.
|
|
@ -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)
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue