mirror of https://github.com/kivy/kivy.git
Added tests toward full coverage, revealing bugs in code that had never been reached. Simplified several parts of the API. Merged CollectionAdapter into ListAdapter, so now it goes: Adpater -> SimpleListAdapter, Adapter -> ListAdapter -> DictAdapter, with selection in ListAdapter, and DictAdapter.
This commit is contained in:
parent
29ff3259e9
commit
3f76ea8d3a
|
@ -15,7 +15,10 @@ class FruitDetailView(GridLayout):
|
|||
|
||||
def __init__(self, **kwargs):
|
||||
kwargs['cols'] = 2
|
||||
self.fruit_name = kwargs.get('fruit_name', '')
|
||||
super(FruitDetailView, self).__init__(**kwargs)
|
||||
if self.fruit_name:
|
||||
self.redraw()
|
||||
|
||||
def redraw(self, *args):
|
||||
self.clear_widgets()
|
||||
|
@ -85,7 +88,10 @@ class FruitImageDetailView(BoxLayout):
|
|||
|
||||
def __init__(self, **kwargs):
|
||||
kwargs['orientation'] = 'vertical'
|
||||
self.fruit_name = kwargs.get('fruit_name', '')
|
||||
super(FruitImageDetailView, self).__init__(**kwargs)
|
||||
if self.fruit_name:
|
||||
self.redraw()
|
||||
|
||||
def redraw(self, *args):
|
||||
self.clear_widgets()
|
||||
|
|
|
@ -153,16 +153,14 @@ class CascadingView(GridLayout):
|
|||
|
||||
# Detail view, for a given fruit, on the right:
|
||||
#
|
||||
detail_view = FruitDetailView(size_hint=(.6, 1.0))
|
||||
detail_view = FruitDetailView(
|
||||
fruit_name=fruits_list_adapter.selection[0].text,
|
||||
size_hint=(.6, 1.0))
|
||||
|
||||
fruits_list_adapter.bind(
|
||||
on_selection_change=detail_view.fruit_changed)
|
||||
self.add_widget(detail_view)
|
||||
|
||||
# Force triggering of on_selection_change() for the DetailView, for
|
||||
# correct initial display.
|
||||
fruits_list_adapter.touch_selection()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
|
|
|
@ -81,16 +81,14 @@ class CascadingView(GridLayout):
|
|||
|
||||
# Detail view, for a given fruit, on the right:
|
||||
#
|
||||
detail_view = FruitDetailView(size_hint=(.6, 1.0))
|
||||
detail_view = FruitDetailView(
|
||||
fruit_name=fruits_dict_adapter.selection[0].text,
|
||||
size_hint=(.6, 1.0))
|
||||
|
||||
fruits_dict_adapter.bind(
|
||||
on_selection_change=detail_view.fruit_changed)
|
||||
self.add_widget(detail_view)
|
||||
|
||||
# Force triggering of on_selection_change() for the DetailView, for
|
||||
# correct initial display.
|
||||
fruits_dict_adapter.touch_selection()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
|
|
|
@ -110,15 +110,14 @@ class CascadingView(GridLayout):
|
|||
|
||||
# Detail view, for a given fruit, on the right:
|
||||
#
|
||||
detail_view = FruitImageDetailView(size_hint=(.6, 1.0))
|
||||
detail_view = FruitImageDetailView(
|
||||
fruit_name=fruits_list_adapter.selection[0].fruit_name,
|
||||
size_hint=(.6, 1.0))
|
||||
|
||||
fruits_list_adapter.bind(
|
||||
on_selection_change=detail_view.fruit_changed)
|
||||
self.add_widget(detail_view)
|
||||
|
||||
# Force triggering of on_selection_change() for the DetailView, for
|
||||
# correct initial display. [TODO] Surely there is a way to avoid this.
|
||||
fruits_list_adapter.touch_selection()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
|
|
|
@ -38,9 +38,7 @@ class MainView(GridLayout):
|
|||
'cls_dicts': [{'cls': ListItemButton,
|
||||
'kwargs': {'text': rec['text']}},
|
||||
{'cls': ListItemLabel,
|
||||
'kwargs': {'text': "Middle",
|
||||
'merge_text': True,
|
||||
'delimiter': '-',
|
||||
'kwargs': {'text': "Middle-{0}".format(rec['text']),
|
||||
'is_representing_cls': True}},
|
||||
{'cls': ListItemButton,
|
||||
'kwargs': {'text': rec['text']}}]}
|
||||
|
|
|
@ -35,7 +35,10 @@ class MasterDetailView(GridLayout):
|
|||
|
||||
self.add_widget(master_list_view)
|
||||
|
||||
detail_view = FruitDetailView(size_hint=(.7, 1.0))
|
||||
detail_view = FruitDetailView(
|
||||
fruit_name=dict_adapter.selection[0].text,
|
||||
size_hint=(.7, 1.0))
|
||||
|
||||
self.add_widget(detail_view)
|
||||
|
||||
dict_adapter.bind(on_selection_change=detail_view.fruit_changed)
|
||||
|
|
|
@ -77,13 +77,7 @@ class Adapter(EventDispatcher):
|
|||
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_data_item(self, index):
|
||||
def get_data_item(self):
|
||||
return self.data
|
||||
|
||||
def get_view(self, index): #pragma: no cover
|
||||
|
|
|
@ -1,366 +0,0 @@
|
|||
'''
|
||||
CollectionAdapter
|
||||
=================
|
||||
|
||||
.. versionadded:: 1.5
|
||||
|
||||
: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.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, EventDispatcher):
|
||||
'''
|
||||
A base class for adapters interfacing with lists, dictionaries, or other
|
||||
collection type data, adding selection and view creation and management
|
||||
functonality.
|
||||
'''
|
||||
|
||||
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
|
||||
index of the first selected item, if there is selection.
|
||||
'''
|
||||
raise NotImplementedError
|
||||
|
||||
def trim_right_of_sel(self, *args): #pragma: no cover
|
||||
'''Cut list items with indices in sorted_keys that are greater than
|
||||
the index of the last selected item, if there is selection.
|
||||
'''
|
||||
raise NotImplementedError
|
||||
|
||||
def trim_to_sel(self, *args): #pragma: no cover
|
||||
'''Cut list items with indices in sorted_keys that are les than or
|
||||
greater than the index of the last selected item, if there is
|
||||
selection. This preserves intervening list items within the selected
|
||||
range.
|
||||
'''
|
||||
raise NotImplementedError
|
||||
|
||||
def cut_to_sel(self, *args): #pragma: no cover
|
||||
'''Same as trim_to_sel, but intervening list items within the selected
|
||||
range are cut also, leaving only list items that are selected.
|
||||
'''
|
||||
raise NotImplementedError
|
|
@ -6,7 +6,7 @@ DictAdapter
|
|||
|
||||
:class:`DictAdapter` is an adapter around a python dictionary of records.
|
||||
|
||||
From :class:`Adapter`, :class:`DictAdapter` gets these properties:
|
||||
From :class:`ListAdapter`, :class:`DictAdapter` gets these properties:
|
||||
|
||||
Use only one:
|
||||
|
||||
|
@ -22,9 +22,6 @@ From :class:`Adapter`, :class:`DictAdapter` gets these properties:
|
|||
provided, a default one is set, that assumes that the
|
||||
data items are strings.
|
||||
|
||||
From the :class:`CollectionAdapter` mixin, :class:`DictAdapter` has
|
||||
these properties:
|
||||
|
||||
- selection
|
||||
- selection_mode
|
||||
- allow_empty_selection
|
||||
|
@ -38,11 +35,11 @@ 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.listadapter import ListAdapter
|
||||
from kivy.adapters.models import SelectableDataItem
|
||||
|
||||
|
||||
class DictAdapter(CollectionAdapter):
|
||||
class DictAdapter(ListAdapter):
|
||||
|
||||
sorted_keys = ListProperty([])
|
||||
'''The sorted_keys list property contains a list of hashable objects (can
|
||||
|
@ -75,10 +72,26 @@ class DictAdapter(CollectionAdapter):
|
|||
self.bind(sorted_keys=func)
|
||||
self.bind(data=func)
|
||||
|
||||
# self.data is paramount to self.sorted_keys. If sorted_keys is reset to
|
||||
# mismatch data, force a reset of sorted_keys to data.keys(). So, in order
|
||||
# to do a complete reset of data and sorted_keys, data must be reset
|
||||
# first, followed by a reset of sorted_keys, if needed.
|
||||
def initialize_sorted_keys(self, *args):
|
||||
stale_sorted_keys = False
|
||||
for key in self.sorted_keys:
|
||||
if not key in self.data:
|
||||
stale_sorted_keys = True
|
||||
break
|
||||
if stale_sorted_keys:
|
||||
self.sorted_keys = sorted(self.data.keys())
|
||||
self.delete_cache()
|
||||
self.initialize_selection()
|
||||
|
||||
# Override ListAdapter.update_for_new_data().
|
||||
def update_for_new_data(self, *args):
|
||||
self.initialize_sorted_keys()
|
||||
|
||||
# Note: this is not len(self.data).
|
||||
def get_count(self):
|
||||
return len(self.sorted_keys)
|
||||
|
||||
|
@ -87,29 +100,6 @@ class DictAdapter(CollectionAdapter):
|
|||
return None
|
||||
return self.data[self.sorted_keys[index]]
|
||||
|
||||
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:
|
||||
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.
|
||||
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
'''
|
||||
ListAdapter
|
||||
=================
|
||||
|
||||
.. versionadded:: 1.5
|
||||
|
||||
ListAdapter
|
||||
===========
|
||||
|
||||
|
@ -6,6 +11,8 @@ ListAdapter
|
|||
|
||||
:class:`ListAdapter` is an adapter around a python list.
|
||||
|
||||
Selection operations are a main concern for the class.
|
||||
|
||||
From :class:`Adapter`, :class:`ListAdapter` gets these properties:
|
||||
|
||||
Use only one:
|
||||
|
@ -22,30 +29,138 @@ From :class:`Adapter`, :class:`ListAdapter` gets these properties:
|
|||
provided, a default one is set, that assumes that the
|
||||
data items are strings.
|
||||
|
||||
From the :class:`CollectionAdapter` mixin, :class:`ListAdapter` has
|
||||
these properties:
|
||||
and adds several for selection:
|
||||
|
||||
- selection
|
||||
- selection_mode
|
||||
- allow_empty_selection
|
||||
- selection, a list of selected items.
|
||||
|
||||
- selection_mode, 'single', 'multiple', 'none'
|
||||
|
||||
- allow_empty_selection, a boolean -- False, and a selection is forced;
|
||||
True, and only user or programmatic action will
|
||||
change selection, and it can be empty.
|
||||
|
||||
and several methods used in selection operations.
|
||||
|
||||
If you wish to have a bare-bones list adapter, without selection, use
|
||||
:class:`SimpleListAdapter`.
|
||||
|
||||
:class:`DictAdapter` is a subclass of :class:`ListAdapter`.
|
||||
|
||||
'''
|
||||
|
||||
from kivy.properties import ListProperty, DictProperty, ObjectProperty
|
||||
from kivy.adapters.collectionadapter import CollectionAdapter
|
||||
import inspect
|
||||
from kivy.event import EventDispatcher
|
||||
from kivy.adapters.adapter import Adapter
|
||||
from kivy.adapters.models import SelectableDataItem
|
||||
|
||||
from inspect import isfunction, ismethod
|
||||
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 ListAdapter(CollectionAdapter):
|
||||
class ListAdapter(Adapter, EventDispatcher):
|
||||
'''
|
||||
A base class for adapters interfacing with lists, dictionaries, or other
|
||||
collection type data, adding selection and view creation and management
|
||||
functonality.
|
||||
'''
|
||||
|
||||
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(-1)
|
||||
'''When selection_mode is multiple, if selection_limit is non-negative,
|
||||
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.
|
||||
|
||||
If selection_limit is not set, the default is -1, meaning that no limit
|
||||
will be enforced.
|
||||
'''
|
||||
|
||||
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(ListAdapter, self).__init__(**kwargs)
|
||||
|
||||
self.register_event_type('on_selection_change')
|
||||
|
||||
self.bind(selection_mode=self.selection_mode_changed,
|
||||
allow_empty_selection=self.check_for_empty_selection,
|
||||
data=self.update_for_new_data)
|
||||
|
||||
self.update_for_new_data()
|
||||
|
||||
def delete_cache(self, *args):
|
||||
self.cached_views = {}
|
||||
|
||||
def get_count(self):
|
||||
return len(self.data)
|
||||
|
@ -55,12 +170,220 @@ class ListAdapter(CollectionAdapter):
|
|||
return None
|
||||
return self.data[index]
|
||||
|
||||
def bind_triggers_to_view(self, func):
|
||||
self.bind(data=func)
|
||||
def selection_mode_changed(self, *args):
|
||||
if self.selection_mode == 'none':
|
||||
for selected_view in self.selection:
|
||||
self.deselect_item_view(selected_view)
|
||||
else:
|
||||
self.check_for_empty_selection()
|
||||
|
||||
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 = self.args_converter(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.
|
||||
#
|
||||
if isinstance(item, 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 (inspect.isfunction(item.is_selected)
|
||||
or inspect.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}"
|
||||
raise Exception(msg.format(index))
|
||||
|
||||
view_instance.bind(on_release=self.handle_selection)
|
||||
|
||||
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, hold_dispatch=False, *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 < 0, selection_limit is not active.
|
||||
if self.selection_limit < 0:
|
||||
self.select_item_view(view)
|
||||
else:
|
||||
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:
|
||||
self.deselect_item_view(view)
|
||||
if self.selection_mode != 'none':
|
||||
# If the deselection makes selection empty, the following call
|
||||
# will check allows_empty_selection, and if False, will
|
||||
# select the first item. If view happens to be the first item,
|
||||
# this will be a reselection, and the user will notice no
|
||||
# change, except perhaps a flicker.
|
||||
#
|
||||
self.check_for_empty_selection()
|
||||
|
||||
if not hold_dispatch:
|
||||
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 isinstance(item, SelectableDataItem):
|
||||
item.is_selected = value
|
||||
elif type(item) == dict:
|
||||
item['is_selected'] = value
|
||||
elif hasattr(item, 'is_selected'):
|
||||
if (inspect.isfunction(item.is_selected)
|
||||
or inspect.ismethod(item.is_selected)):
|
||||
item.is_selected()
|
||||
else:
|
||||
item.is_selected = value
|
||||
|
||||
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=True):
|
||||
'''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
|
||||
'''
|
||||
if not extend:
|
||||
self.selection = []
|
||||
|
||||
for view in view_list:
|
||||
self.handle_selection(view, hold_dispatch=True)
|
||||
|
||||
def touch_selection(self, *args):
|
||||
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.handle_selection(view, hold_dispatch=True)
|
||||
|
||||
self.dispatch('on_selection_change')
|
||||
|
||||
# [TODO] Could easily add select_all() and deselect_all().
|
||||
|
||||
def update_for_new_data(self, *args):
|
||||
self.delete_cache()
|
||||
self.initialize_selection()
|
||||
|
||||
def initialize_selection(self, *args):
|
||||
if len(self.selection) > 0:
|
||||
self.selection = []
|
||||
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)
|
||||
|
||||
# [TODO] Also make methods for scroll_to_sel_start, scroll_to_sel_end,
|
||||
# scroll_to_sel_middle.
|
||||
|
||||
|
@ -68,13 +391,18 @@ class ListAdapter(CollectionAdapter):
|
|||
'''Cut list items with indices in sorted_keys that are less than the
|
||||
index of the first selected item, if there is selection.
|
||||
'''
|
||||
pass
|
||||
if len(self.selection) > 0:
|
||||
first_sel_index = min([sel.index for sel in self.selection])
|
||||
self.data = self.data[first_sel_index:]
|
||||
|
||||
def trim_right_of_sel(self, *args):
|
||||
'''Cut list items with indices in sorted_keys that are greater than
|
||||
the index of the last selected item, if there is selection.
|
||||
'''
|
||||
pass
|
||||
if len(self.selection) > 0:
|
||||
last_sel_index = max([sel.index for sel in self.selection])
|
||||
print 'last_sel_index', last_sel_index
|
||||
self.data = self.data[:last_sel_index+1]
|
||||
|
||||
def trim_to_sel(self, *args):
|
||||
'''Cut list items with indices in sorted_keys that are les than or
|
||||
|
@ -82,10 +410,15 @@ class ListAdapter(CollectionAdapter):
|
|||
selection. This preserves intervening list items within the selected
|
||||
range.
|
||||
'''
|
||||
pass
|
||||
if len(self.selection) > 0:
|
||||
sel_indices = [sel.index for sel in self.selection]
|
||||
first_sel_index = min(sel_indices)
|
||||
last_sel_index = max(sel_indices)
|
||||
self.data = self.data[first_sel_index:last_sel_index+1]
|
||||
|
||||
def cut_to_sel(self, *args):
|
||||
'''Same as trim_to_sel, but intervening list items within the selected
|
||||
range are cut also, leaving only list items that are selected.
|
||||
'''
|
||||
pass
|
||||
if len(self.selection) > 0:
|
||||
self.data = self.selection
|
||||
|
|
|
@ -52,8 +52,3 @@ class SelectableDataItem(object):
|
|||
@is_selected.setter
|
||||
def is_selected(self, value):
|
||||
self._is_selected = value
|
||||
|
||||
@is_selected.deleter
|
||||
def is_selected(self):
|
||||
self._is_selected = None
|
||||
|
||||
|
|
|
@ -5,8 +5,10 @@ Adapter tests
|
|||
|
||||
import unittest
|
||||
|
||||
from kivy.uix.selectableview import SelectableView
|
||||
from kivy.uix.listview import SelectableView
|
||||
from kivy.uix.listview import ListItemButton
|
||||
from kivy.uix.listview import ListItemLabel
|
||||
from kivy.uix.listview import CompositeListItem
|
||||
from kivy.uix.label import Label
|
||||
|
||||
from kivy.adapters.models import SelectableDataItem
|
||||
|
@ -15,6 +17,9 @@ from kivy.adapters.simplelistadapter import SimpleListAdapter
|
|||
from kivy.adapters.listadapter import ListAdapter
|
||||
from kivy.adapters.dictadapter import DictAdapter
|
||||
|
||||
from kivy.properties import BooleanProperty
|
||||
from kivy.properties import StringProperty
|
||||
|
||||
from kivy.factory import Factory
|
||||
from kivy.lang import Builder
|
||||
|
||||
|
@ -148,6 +153,7 @@ fruit_data_list_of_dicts = \
|
|||
'data': [80, 0, 0, 0, 0, 0, 270, 8, 21, 7, 1, 4, 20, 1, 30, 25, 2, 4],
|
||||
'is_selected': False}]
|
||||
|
||||
|
||||
fruit_data_attributes = ['(gram weight/ ounce weight)',
|
||||
'Calories',
|
||||
'Calories from Fat',
|
||||
|
@ -163,6 +169,7 @@ fruit_data_attributes = ['(gram weight/ ounce weight)',
|
|||
'Calcium',
|
||||
'Iron']
|
||||
|
||||
|
||||
fruit_data_attribute_units = ['(g)',
|
||||
'(%DV)',
|
||||
'(mg)',
|
||||
|
@ -197,7 +204,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 +213,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):
|
||||
|
@ -257,6 +264,14 @@ Builder.load_string('''
|
|||
is_selected: ctx.is_selected
|
||||
''')
|
||||
|
||||
Builder.load_string('''
|
||||
[CustomSimpleListItem@SelectableView+BoxLayout]:
|
||||
size_hint_y: ctx.size_hint_y
|
||||
height: ctx.height
|
||||
ListItemButton:
|
||||
text: ctx.text
|
||||
''')
|
||||
|
||||
|
||||
class AdaptersTestCase(unittest.TestCase):
|
||||
|
||||
|
@ -265,6 +280,28 @@ class AdaptersTestCase(unittest.TestCase):
|
|||
'size_hint_y': None,
|
||||
'height': 25}
|
||||
|
||||
self.integers_dict = \
|
||||
{str(i): {'text': str(i), 'is_selected': False} for i in xrange(100)}
|
||||
|
||||
# The third of the four cls_dict items has no kwargs nor text, so
|
||||
# rec['text'] will be set for it. Likewise, the fifth item has kwargs,
|
||||
# but it has no 'text' key/value, so should receive the same treatment.
|
||||
self.composite_args_converter = \
|
||||
lambda rec: \
|
||||
{'text': rec['text'],
|
||||
'size_hint_y': None,
|
||||
'height': 25,
|
||||
'cls_dicts': [{'cls': ListItemButton,
|
||||
'kwargs': {'text': rec['text']}},
|
||||
{'cls': ListItemLabel,
|
||||
'kwargs': {'text': "Middle-{0}".format(rec['text']),
|
||||
'is_representing_cls': True}},
|
||||
{'cls': ListItemButton},
|
||||
{'cls': ListItemButton,
|
||||
'kwargs': {'some key': 'some value'}},
|
||||
{'cls': ListItemButton,
|
||||
'kwargs': {'text': rec['text']}}]}
|
||||
|
||||
reset_to_defaults(fruit_data)
|
||||
|
||||
@raises(Exception)
|
||||
|
@ -366,14 +403,29 @@ class AdaptersTestCase(unittest.TestCase):
|
|||
self.assertEqual(adapter_2.args_converter, list_item_args_converter)
|
||||
|
||||
adapter = Adapter(data='cat', cls=Label)
|
||||
self.assertEqual(adapter.get_count(), 1)
|
||||
self.assertEqual(adapter.get_data_item(0), 'cat')
|
||||
self.assertEqual(adapter.get_data_item(), 'cat')
|
||||
|
||||
adapter = Adapter(data=None, cls=Label)
|
||||
self.assertEqual(adapter.get_count(), 0)
|
||||
self.assertEqual(adapter.get_data_item(0), None)
|
||||
self.assertEqual(adapter.get_data_item(), None)
|
||||
|
||||
def test_instantiating_simple_list_adapter(self):
|
||||
def test_instantiating_adapter_bind_triggers_to_view(self):
|
||||
class PetListener(object):
|
||||
def __init__(self, pet):
|
||||
self.current_pet = pet
|
||||
|
||||
def callback(self, *args):
|
||||
self.current_pet = args[1]
|
||||
|
||||
pet_listener = PetListener('cat')
|
||||
|
||||
adapter = Adapter(data='cat', cls=Label)
|
||||
adapter.bind_triggers_to_view(pet_listener.callback)
|
||||
|
||||
self.assertEqual(pet_listener.current_pet, 'cat')
|
||||
adapter.data = 'dog'
|
||||
self.assertEqual(pet_listener.current_pet, 'dog')
|
||||
|
||||
def test_simple_list_adapter_for_exceptions(self):
|
||||
# with no data
|
||||
with self.assertRaises(Exception) as cm:
|
||||
simple_list_adapter = SimpleListAdapter()
|
||||
|
@ -388,6 +440,23 @@ class AdaptersTestCase(unittest.TestCase):
|
|||
msg = 'list adapter: data must be a tuple or list'
|
||||
self.assertEqual(str(cm.exception), msg)
|
||||
|
||||
def test_simple_list_adapter_with_template(self):
|
||||
list_item_args_converter = \
|
||||
lambda obj: {'text': str(obj),
|
||||
'size_hint_y': None,
|
||||
'height': 25}
|
||||
|
||||
simple_list_adapter = \
|
||||
SimpleListAdapter(data=['cat', 'dog'],
|
||||
args_converter=list_item_args_converter,
|
||||
template='CustomSimpleListItem')
|
||||
|
||||
view = simple_list_adapter.get_view(0)
|
||||
self.assertEqual(view.__class__.__name__, 'CustomSimpleListItem')
|
||||
|
||||
# For coverage of __repr__:
|
||||
self.assertEqual(type(str(view)), str)
|
||||
|
||||
def test_simple_list_adapter_methods(self):
|
||||
simple_list_adapter = SimpleListAdapter(data=['cat', 'dog'],
|
||||
cls=Label)
|
||||
|
@ -402,9 +471,196 @@ class AdaptersTestCase(unittest.TestCase):
|
|||
self.assertIsNone(simple_list_adapter.get_view(-1))
|
||||
self.assertIsNone(simple_list_adapter.get_view(2))
|
||||
|
||||
def test_list_adapter_selection_mode_none(self):
|
||||
def test_instantiating_list_adapter(self):
|
||||
str_args_converter = lambda rec: {'text': rec,
|
||||
'size_hint_y': None,
|
||||
'height': 25}
|
||||
|
||||
list_adapter = ListAdapter(data=['cat', 'dog'],
|
||||
args_converter=str_args_converter,
|
||||
cls=ListItemButton)
|
||||
|
||||
self.assertEqual([obj for obj in list_adapter.data],
|
||||
['cat', 'dog'])
|
||||
self.assertEqual(list_adapter.get_count(), 2)
|
||||
|
||||
self.assertEqual(list_adapter.cls, ListItemButton)
|
||||
self.assertEqual(list_adapter.args_converter, str_args_converter)
|
||||
self.assertEqual(list_adapter.template, None)
|
||||
|
||||
cat_data_item = list_adapter.get_data_item(0)
|
||||
self.assertEqual(cat_data_item, 'cat')
|
||||
self.assertTrue(isinstance(cat_data_item, str))
|
||||
|
||||
view = list_adapter.get_view(0)
|
||||
self.assertTrue(isinstance(view, ListItemButton))
|
||||
|
||||
view = list_adapter.create_view(0)
|
||||
self.assertTrue(isinstance(view, ListItemButton))
|
||||
|
||||
view = list_adapter.create_view(-1)
|
||||
self.assertIsNone(view)
|
||||
|
||||
view = list_adapter.create_view(100)
|
||||
self.assertIsNone(view)
|
||||
|
||||
def test_list_adapter_selection_mode_single(self):
|
||||
fruit_data_items[0].is_selected = True
|
||||
|
||||
list_item_args_converter = \
|
||||
lambda selectable: {'text': selectable.name,
|
||||
'size_hint_y': None,
|
||||
'height': 25}
|
||||
|
||||
list_adapter = ListAdapter(data=fruit_data_items,
|
||||
args_converter=self.args_converter,
|
||||
args_converter=list_item_args_converter,
|
||||
selection_mode='single',
|
||||
propagate_selection_to_data=True,
|
||||
allow_empty_selection=False,
|
||||
cls=ListItemButton)
|
||||
|
||||
self.assertEqual(sorted([obj.name for obj in list_adapter.data]),
|
||||
['Apple', 'Avocado', 'Banana', 'Cantaloupe', 'Cherry', 'Grape',
|
||||
'Grapefruit', 'Honeydew', 'Kiwifruit', 'Lemon', 'Lime',
|
||||
'Nectarine', 'Orange', 'Peach', 'Pear', 'Pineapple', 'Plum',
|
||||
'Strawberry', 'Tangerine', 'Watermelon'])
|
||||
|
||||
self.assertEqual(list_adapter.cls, ListItemButton)
|
||||
self.assertEqual(list_adapter.args_converter,
|
||||
list_item_args_converter)
|
||||
self.assertEqual(list_adapter.template, None)
|
||||
|
||||
apple_data_item = list_adapter.get_data_item(0)
|
||||
self.assertTrue(isinstance(apple_data_item, FruitItem))
|
||||
self.assertTrue(isinstance(apple_data_item, SelectableDataItem))
|
||||
self.assertTrue(apple_data_item.is_selected)
|
||||
|
||||
view = list_adapter.get_view(0)
|
||||
self.assertTrue(isinstance(view, ListItemButton))
|
||||
self.assertTrue(view.is_selected)
|
||||
|
||||
def test_list_adapter_with_dict_data(self):
|
||||
alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
|
||||
letters_dicts = \
|
||||
[{'text': l, 'is_selected': False} for l in alphabet]
|
||||
|
||||
letters_dicts[0]['is_selected'] = True
|
||||
|
||||
list_item_args_converter = lambda rec: {'text': rec['text'],
|
||||
'size_hint_y': None,
|
||||
'height': 25}
|
||||
|
||||
list_adapter = ListAdapter(data=letters_dicts,
|
||||
args_converter=list_item_args_converter,
|
||||
selection_mode='single',
|
||||
propagate_selection_to_data=True,
|
||||
allow_empty_selection=False,
|
||||
cls=ListItemButton)
|
||||
|
||||
self.assertEqual(list_adapter.cls, ListItemButton)
|
||||
self.assertEqual(list_adapter.args_converter,
|
||||
list_item_args_converter)
|
||||
self.assertEqual(list_adapter.template, None)
|
||||
|
||||
apple_data_item = list_adapter.get_data_item(0)
|
||||
self.assertTrue(isinstance(apple_data_item, dict))
|
||||
self.assertTrue(apple_data_item['is_selected'])
|
||||
|
||||
view = list_adapter.get_view(0)
|
||||
self.assertTrue(isinstance(view, ListItemButton))
|
||||
self.assertTrue(view.is_selected)
|
||||
|
||||
def test_list_adapter_with_custom_class(self):
|
||||
|
||||
# Use a widget as data item.
|
||||
class DataItem(Label):
|
||||
is_selected = BooleanProperty(True)
|
||||
text = StringProperty('')
|
||||
|
||||
class DataItemWithMethod(DataItem):
|
||||
_is_selected = BooleanProperty(True)
|
||||
|
||||
def is_selected(self):
|
||||
return self._is_selected
|
||||
|
||||
class BadDataItem(Label):
|
||||
text = StringProperty('')
|
||||
|
||||
data_items = []
|
||||
data_items.append(DataItem(text='cat'))
|
||||
data_items.append(DataItemWithMethod(text='dog'))
|
||||
data_items.append(BadDataItem(text='frog'))
|
||||
|
||||
list_item_args_converter = lambda obj: {'text': obj.text,
|
||||
'size_hint_y': None,
|
||||
'height': 25}
|
||||
|
||||
list_adapter = ListAdapter(data=data_items,
|
||||
args_converter=list_item_args_converter,
|
||||
selection_mode='single',
|
||||
propagate_selection_to_data=True,
|
||||
allow_empty_selection=False,
|
||||
cls=ListItemButton)
|
||||
|
||||
self.assertEqual(list_adapter.cls, ListItemButton)
|
||||
self.assertEqual(list_adapter.args_converter,
|
||||
list_item_args_converter)
|
||||
self.assertEqual(list_adapter.template, None)
|
||||
|
||||
apple_data_item = list_adapter.get_data_item(0)
|
||||
self.assertTrue(isinstance(apple_data_item, DataItem))
|
||||
self.assertTrue(apple_data_item.is_selected)
|
||||
|
||||
view = list_adapter.get_view(0)
|
||||
self.assertTrue(isinstance(view, ListItemButton))
|
||||
self.assertTrue(view.is_selected)
|
||||
|
||||
view = list_adapter.get_view(1)
|
||||
self.assertTrue(isinstance(view, ListItemButton))
|
||||
self.assertTrue(view.is_selected)
|
||||
|
||||
with self.assertRaises(Exception) as cm:
|
||||
view = list_adapter.get_view(2)
|
||||
|
||||
msg = "ListAdapter: unselectable data item for 2"
|
||||
self.assertEqual(str(cm.exception), msg)
|
||||
|
||||
def test_instantiating_list_adapter_no_args_converter(self):
|
||||
list_adapter = \
|
||||
ListAdapter(data=['cat', 'dog'],
|
||||
cls=ListItemButton)
|
||||
|
||||
self.assertEqual(list_adapter.get_count(), 2)
|
||||
|
||||
self.assertEqual(list_adapter.cls, ListItemButton)
|
||||
self.assertIsNotNone(list_adapter.args_converter)
|
||||
self.assertEqual(list_adapter.template, None)
|
||||
|
||||
cat_data_item = list_adapter.get_data_item(0)
|
||||
self.assertEqual(cat_data_item, 'cat')
|
||||
self.assertTrue(isinstance(cat_data_item, str))
|
||||
|
||||
view = list_adapter.get_view(0)
|
||||
self.assertTrue(isinstance(view, ListItemButton))
|
||||
|
||||
view = list_adapter.create_view(0)
|
||||
self.assertTrue(isinstance(view, ListItemButton))
|
||||
|
||||
view = list_adapter.create_view(-1)
|
||||
self.assertIsNone(view)
|
||||
|
||||
view = list_adapter.create_view(100)
|
||||
self.assertIsNone(view)
|
||||
|
||||
def test_list_adapter_selection_mode_none(self):
|
||||
list_item_args_converter = \
|
||||
lambda selectable: {'text': selectable.name,
|
||||
'size_hint_y': None,
|
||||
'height': 25}
|
||||
|
||||
list_adapter = ListAdapter(data=fruit_data_items,
|
||||
args_converter=list_item_args_converter,
|
||||
selection_mode='none',
|
||||
allow_empty_selection=True,
|
||||
cls=ListItemButton)
|
||||
|
@ -416,12 +672,54 @@ class AdaptersTestCase(unittest.TestCase):
|
|||
'Strawberry', 'Tangerine', 'Watermelon'])
|
||||
|
||||
self.assertEqual(list_adapter.cls, ListItemButton)
|
||||
self.assertEqual(list_adapter.args_converter, self.args_converter)
|
||||
self.assertEqual(list_adapter.args_converter, list_item_args_converter)
|
||||
self.assertEqual(list_adapter.template, None)
|
||||
|
||||
apple_data_item = list_adapter.get_data_item(0)
|
||||
self.assertTrue(isinstance(apple_data_item, FruitItem))
|
||||
|
||||
def test_list_adapter_selection_mode_multiple_select_list(self):
|
||||
list_item_args_converter = \
|
||||
lambda selectable: {'text': selectable.name,
|
||||
'size_hint_y': None,
|
||||
'height': 25}
|
||||
|
||||
list_adapter = ListAdapter(data=fruit_data_items,
|
||||
args_converter=list_item_args_converter,
|
||||
selection_mode='multiple',
|
||||
allow_empty_selection=True,
|
||||
cls=ListItemButton)
|
||||
|
||||
views = []
|
||||
views.append(list_adapter.get_view(0))
|
||||
views.append(list_adapter.get_view(1))
|
||||
views.append(list_adapter.get_view(2))
|
||||
|
||||
self.assertEqual(len(views), 3)
|
||||
list_adapter.select_list(views)
|
||||
self.assertEqual(len(list_adapter.selection), 3)
|
||||
|
||||
views = []
|
||||
views.append(list_adapter.get_view(3))
|
||||
views.append(list_adapter.get_view(4))
|
||||
views.append(list_adapter.get_view(5))
|
||||
|
||||
self.assertEqual(len(views), 3)
|
||||
list_adapter.select_list(views)
|
||||
self.assertEqual(len(list_adapter.selection), 6)
|
||||
|
||||
views = []
|
||||
views.append(list_adapter.get_view(0))
|
||||
views.append(list_adapter.get_view(1))
|
||||
views.append(list_adapter.get_view(2))
|
||||
|
||||
self.assertEqual(len(views), 3)
|
||||
list_adapter.select_list(views, extend=False)
|
||||
self.assertEqual(len(list_adapter.selection), 3)
|
||||
|
||||
list_adapter.deselect_list(views)
|
||||
self.assertEqual(len(list_adapter.selection), 0)
|
||||
|
||||
def test_list_adapter_with_dicts_as_data(self):
|
||||
bare_minimum_dicts = \
|
||||
[{'text': str(i), 'is_selected': False} for i in xrange(100)]
|
||||
|
@ -445,6 +743,46 @@ class AdaptersTestCase(unittest.TestCase):
|
|||
data_item = list_adapter.get_data_item(0)
|
||||
self.assertTrue(type(data_item), dict)
|
||||
|
||||
# Utility calls for coverage:
|
||||
|
||||
self.assertEqual(list_adapter.get_count(), 100)
|
||||
|
||||
# Bad index:
|
||||
self.assertIsNone(list_adapter.get_data_item(-1))
|
||||
self.assertIsNone(list_adapter.get_data_item(101))
|
||||
|
||||
def test_list_adapter_with_dicts_as_data_multiple_selection(self):
|
||||
bare_minimum_dicts = \
|
||||
[{'text': str(i), 'is_selected': False} for i in xrange(100)]
|
||||
|
||||
args_converter = lambda rec: {'text': rec['text'],
|
||||
'size_hint_y': None,
|
||||
'height': 25}
|
||||
|
||||
list_adapter = ListAdapter(data=bare_minimum_dicts,
|
||||
args_converter=args_converter,
|
||||
selection_mode='multiple',
|
||||
allow_empty_selection=False,
|
||||
cls=ListItemButton)
|
||||
|
||||
self.assertEqual([rec['text'] for rec in list_adapter.data],
|
||||
[str(i) for i in xrange(100)])
|
||||
|
||||
self.assertEqual(list_adapter.cls, ListItemButton)
|
||||
self.assertEqual(list_adapter.args_converter, args_converter)
|
||||
|
||||
for i in range(50):
|
||||
list_adapter.handle_selection(list_adapter.get_view(i))
|
||||
|
||||
self.assertEqual(len(list_adapter.selection), 50)
|
||||
|
||||
# This is for code coverage:
|
||||
list_adapter.selection_mode = 'none'
|
||||
list_adapter.handle_selection(list_adapter.get_view(25))
|
||||
list_adapter.selection_mode = 'single'
|
||||
list_adapter.handle_selection(list_adapter.get_view(24))
|
||||
list_adapter.handle_selection(list_adapter.get_view(24))
|
||||
|
||||
def test_list_adapter_bindings(self):
|
||||
list_item_args_converter = \
|
||||
lambda selectable: {'text': selectable.name,
|
||||
|
@ -540,6 +878,228 @@ class AdaptersTestCase(unittest.TestCase):
|
|||
view = fruit_categories_list_adapter.get_view(0)
|
||||
self.assertEqual(view.__class__.__name__, 'CustomListItem')
|
||||
|
||||
second_view = fruit_categories_list_adapter.get_view(1)
|
||||
fruit_categories_list_adapter.handle_selection(second_view)
|
||||
self.assertEqual(len(fruit_categories_list_adapter.selection), 1)
|
||||
|
||||
def test_list_adapter_operations_trimming(self):
|
||||
alphabet = [l for l in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ']
|
||||
|
||||
list_item_args_converter = lambda letter: {'text': letter,
|
||||
'size_hint_y': None,
|
||||
'height': 25}
|
||||
|
||||
# trim right of sel
|
||||
|
||||
alphabet_adapter = ListAdapter(
|
||||
data=alphabet,
|
||||
args_converter=list_item_args_converter,
|
||||
selection_mode='multiple',
|
||||
selection_limit=1000,
|
||||
allow_empty_selection=True,
|
||||
cls=ListItemButton)
|
||||
|
||||
a_view = alphabet_adapter.get_view(0)
|
||||
self.assertEqual(a_view.text, 'A')
|
||||
|
||||
alphabet_adapter.handle_selection(a_view)
|
||||
self.assertEqual(len(alphabet_adapter.selection), 1)
|
||||
self.assertTrue(a_view.is_selected)
|
||||
|
||||
alphabet_adapter.trim_right_of_sel()
|
||||
self.assertEqual(len(alphabet_adapter.data), 1)
|
||||
|
||||
# trim left of sel
|
||||
|
||||
alphabet_adapter = ListAdapter(
|
||||
data=alphabet,
|
||||
args_converter=list_item_args_converter,
|
||||
selection_mode='multiple',
|
||||
selection_limit=1000,
|
||||
allow_empty_selection=True,
|
||||
cls=ListItemButton)
|
||||
|
||||
z_view = alphabet_adapter.get_view(25)
|
||||
self.assertEqual(z_view.text, 'Z')
|
||||
|
||||
alphabet_adapter.handle_selection(z_view)
|
||||
self.assertEqual(len(alphabet_adapter.selection), 1)
|
||||
self.assertTrue(z_view.is_selected)
|
||||
|
||||
alphabet_adapter.trim_left_of_sel()
|
||||
self.assertEqual(len(alphabet_adapter.data), 1)
|
||||
|
||||
# trim to sel
|
||||
|
||||
alphabet_adapter = ListAdapter(
|
||||
data=alphabet,
|
||||
args_converter=list_item_args_converter,
|
||||
selection_mode='multiple',
|
||||
selection_limit=1000,
|
||||
allow_empty_selection=True,
|
||||
cls=ListItemButton)
|
||||
|
||||
g_view = alphabet_adapter.get_view(6)
|
||||
self.assertEqual(g_view.text, 'G')
|
||||
alphabet_adapter.handle_selection(g_view)
|
||||
|
||||
m_view = alphabet_adapter.get_view(12)
|
||||
self.assertEqual(m_view.text, 'M')
|
||||
alphabet_adapter.handle_selection(m_view)
|
||||
|
||||
alphabet_adapter.trim_to_sel()
|
||||
self.assertEqual(len(alphabet_adapter.data), 7)
|
||||
|
||||
# cut to sel
|
||||
|
||||
alphabet_adapter = ListAdapter(
|
||||
data=alphabet,
|
||||
args_converter=list_item_args_converter,
|
||||
selection_mode='multiple',
|
||||
selection_limit=1000,
|
||||
allow_empty_selection=True,
|
||||
cls=ListItemButton)
|
||||
|
||||
g_view = alphabet_adapter.get_view(6)
|
||||
self.assertEqual(g_view.text, 'G')
|
||||
alphabet_adapter.handle_selection(g_view)
|
||||
|
||||
m_view = alphabet_adapter.get_view(12)
|
||||
self.assertEqual(m_view.text, 'M')
|
||||
alphabet_adapter.handle_selection(m_view)
|
||||
|
||||
alphabet_adapter.cut_to_sel()
|
||||
self.assertEqual(len(alphabet_adapter.data), 2)
|
||||
|
||||
def test_dict_adapter_composite(self):
|
||||
item_strings = ["{0}".format(index) for index in xrange(100)]
|
||||
|
||||
# And now the list adapter, constructed with the item_strings as
|
||||
# the data, a dict to add the required is_selected boolean onto
|
||||
# data records, and the args_converter above that will operate one
|
||||
# each item in the data to produce list item view instances from the
|
||||
# CompositeListItem class.
|
||||
dict_adapter = DictAdapter(sorted_keys=item_strings,
|
||||
data=self.integers_dict,
|
||||
args_converter=self.composite_args_converter,
|
||||
selection_mode='single',
|
||||
allow_empty_selection=False,
|
||||
cls=CompositeListItem)
|
||||
|
||||
self.assertEqual(len(dict_adapter.selection), 1)
|
||||
|
||||
view = dict_adapter.get_view(1)
|
||||
dict_adapter.handle_selection(view)
|
||||
|
||||
self.assertEqual(len(dict_adapter.selection), 1)
|
||||
|
||||
# test that sorted_keys is built, if not provided.
|
||||
def test_dict_adapter_no_sorted_keys(self):
|
||||
dict_adapter = DictAdapter(data=self.integers_dict,
|
||||
args_converter=self.composite_args_converter,
|
||||
selection_mode='single',
|
||||
allow_empty_selection=False,
|
||||
cls=CompositeListItem)
|
||||
|
||||
self.assertEqual(len(dict_adapter.sorted_keys), 100)
|
||||
|
||||
self.assertEqual(len(dict_adapter.selection), 1)
|
||||
|
||||
view = dict_adapter.get_view(1)
|
||||
dict_adapter.handle_selection(view)
|
||||
|
||||
self.assertEqual(len(dict_adapter.selection), 1)
|
||||
|
||||
def test_dict_adapter_bad_sorted_keys(self):
|
||||
with self.assertRaises(Exception) as cm:
|
||||
dict_adapter = DictAdapter(sorted_keys={},
|
||||
data=self.integers_dict,
|
||||
args_converter=self.composite_args_converter,
|
||||
selection_mode='single',
|
||||
allow_empty_selection=False,
|
||||
cls=CompositeListItem)
|
||||
|
||||
msg = 'DictAdapter: sorted_keys must be tuple or list'
|
||||
self.assertEqual(str(cm.exception), msg)
|
||||
|
||||
def test_instantiating_dict_adapter_bind_triggers_to_view(self):
|
||||
class PetListener(object):
|
||||
def __init__(self, pets):
|
||||
self.current_pets = pets
|
||||
|
||||
def callback(self, *args):
|
||||
self.current_pets = args[1]
|
||||
|
||||
pet_listener = PetListener(['cat'])
|
||||
|
||||
list_item_args_converter = lambda rec: {'text': rec['text'],
|
||||
'size_hint_y': None,
|
||||
'height': 25}
|
||||
|
||||
dict_adapter = DictAdapter(sorted_keys=['cat'],
|
||||
data={'cat': {'text': 'cat', 'is_selected': False},
|
||||
'dog': {'text': 'dog', 'is_selected': False}},
|
||||
args_converter=list_item_args_converter,
|
||||
propagate_selection_to_data=True,
|
||||
selection_mode='single',
|
||||
allow_empty_selection=False,
|
||||
cls=ListItemButton)
|
||||
|
||||
dict_adapter.bind_triggers_to_view(pet_listener.callback)
|
||||
|
||||
self.assertEqual(pet_listener.current_pets, ['cat'])
|
||||
dict_adapter.sorted_keys = ['dog']
|
||||
self.assertEqual(pet_listener.current_pets, ['dog'])
|
||||
|
||||
def test_dict_adapter_reset_data(self):
|
||||
class PetListener(object):
|
||||
def __init__(self, pet):
|
||||
self.current_pet = pet
|
||||
|
||||
# This can happen as a result of sorted_keys changing,
|
||||
# or data changing.
|
||||
def callback(self, *args):
|
||||
self.current_pet = args[1]
|
||||
|
||||
pet_listener = PetListener('cat')
|
||||
|
||||
list_item_args_converter = lambda rec: {'text': rec['text'],
|
||||
'size_hint_y': None,
|
||||
'height': 25}
|
||||
|
||||
dict_adapter = DictAdapter(
|
||||
sorted_keys=['cat'],
|
||||
data={'cat': {'text': 'cat', 'is_selected': False}},
|
||||
args_converter=list_item_args_converter,
|
||||
propagate_selection_to_data=True,
|
||||
selection_mode='single',
|
||||
allow_empty_selection=False,
|
||||
cls=ListItemButton)
|
||||
|
||||
dict_adapter.bind_triggers_to_view(pet_listener.callback)
|
||||
|
||||
self.assertEqual(pet_listener.current_pet, 'cat')
|
||||
dog_data = {'dog': {'text': 'dog', 'is_selected': False}}
|
||||
dict_adapter.data = dog_data
|
||||
self.assertEqual(dict_adapter.sorted_keys, ['dog'])
|
||||
self.assertEqual(pet_listener.current_pet, dog_data)
|
||||
cat_dog_data = {'cat': {'text': 'cat', 'is_selected': False},
|
||||
'dog': {'text': 'dog', 'is_selected': False}}
|
||||
dict_adapter.data = cat_dog_data
|
||||
# sorted_keys should remain ['dog'], as it still matches data.
|
||||
self.assertEqual(dict_adapter.sorted_keys, ['dog'])
|
||||
dict_adapter.sorted_keys = ['cat']
|
||||
self.assertEqual(pet_listener.current_pet, ['cat'])
|
||||
|
||||
# Make some utility calls for coverage:
|
||||
|
||||
# 1, because get_count() does len(self.sorted_keys).
|
||||
self.assertEqual(dict_adapter.get_count(), 1)
|
||||
|
||||
# Bad index:
|
||||
self.assertIsNone(dict_adapter.get_data_item(-1))
|
||||
self.assertIsNone(dict_adapter.get_data_item(2))
|
||||
|
||||
def test_dict_adapter_selection_mode_single_without_propagation(self):
|
||||
|
||||
list_item_args_converter = lambda rec: {'text': rec['name'],
|
||||
|
@ -657,3 +1217,102 @@ class AdaptersTestCase(unittest.TestCase):
|
|||
|
||||
tangerine_view = dict_adapter.get_view(2)
|
||||
self.assertEqual(tangerine_view.text, 'Tangerine')
|
||||
|
||||
def test_dict_adapter_operations_trimming(self):
|
||||
alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
|
||||
letters_dict = \
|
||||
{l: {'text': l, 'is_selected': False} for l in alphabet}
|
||||
|
||||
list_item_args_converter = lambda rec: {'text': rec['text'],
|
||||
'size_hint_y': None,
|
||||
'height': 25}
|
||||
|
||||
letters = [l for l in alphabet]
|
||||
|
||||
# trim left of sel
|
||||
|
||||
letters_dict_adapter = DictAdapter(
|
||||
sorted_keys=letters[:],
|
||||
data=letters_dict,
|
||||
args_converter=list_item_args_converter,
|
||||
selection_mode='multiple',
|
||||
selection_limit=1000,
|
||||
allow_empty_selection=True,
|
||||
cls=ListItemButton)
|
||||
|
||||
a_view = letters_dict_adapter.get_view(0)
|
||||
self.assertEqual(a_view.text, 'A')
|
||||
|
||||
letters_dict_adapter.handle_selection(a_view)
|
||||
self.assertEqual(len(letters_dict_adapter.selection), 1)
|
||||
self.assertTrue(a_view.is_selected)
|
||||
|
||||
letters_dict_adapter.trim_right_of_sel()
|
||||
self.assertEqual(len(letters_dict_adapter.data), 1)
|
||||
|
||||
# trim right of sel
|
||||
|
||||
letters_dict_adapter = DictAdapter(
|
||||
sorted_keys=letters[:],
|
||||
data=letters_dict,
|
||||
args_converter=list_item_args_converter,
|
||||
selection_mode='multiple',
|
||||
selection_limit=1000,
|
||||
allow_empty_selection=True,
|
||||
cls=ListItemButton)
|
||||
|
||||
z_view = letters_dict_adapter.get_view(25)
|
||||
self.assertEqual(z_view.text, 'Z')
|
||||
|
||||
letters_dict_adapter.handle_selection(z_view)
|
||||
self.assertEqual(len(letters_dict_adapter.selection), 1)
|
||||
self.assertTrue(z_view.is_selected)
|
||||
|
||||
letters_dict_adapter.trim_left_of_sel()
|
||||
self.assertEqual(len(letters_dict_adapter.data), 1)
|
||||
|
||||
# trim to sel
|
||||
|
||||
letters_dict_adapter = DictAdapter(
|
||||
sorted_keys=letters[:],
|
||||
data=letters_dict,
|
||||
args_converter=list_item_args_converter,
|
||||
selection_mode='multiple',
|
||||
selection_limit=1000,
|
||||
allow_empty_selection=True,
|
||||
cls=ListItemButton)
|
||||
|
||||
g_view = letters_dict_adapter.get_view(6)
|
||||
self.assertEqual(g_view.text, 'G')
|
||||
letters_dict_adapter.handle_selection(g_view)
|
||||
|
||||
m_view = letters_dict_adapter.get_view(12)
|
||||
self.assertEqual(m_view.text, 'M')
|
||||
letters_dict_adapter.handle_selection(m_view)
|
||||
|
||||
letters_dict_adapter.trim_to_sel()
|
||||
self.assertEqual(len(letters_dict_adapter.data), 7)
|
||||
|
||||
# cut to sel
|
||||
|
||||
letters_dict_adapter = DictAdapter(
|
||||
sorted_keys=letters[:],
|
||||
data=letters_dict,
|
||||
args_converter=list_item_args_converter,
|
||||
selection_mode='multiple',
|
||||
selection_limit=1000,
|
||||
allow_empty_selection=True,
|
||||
cls=ListItemButton)
|
||||
|
||||
g_view = letters_dict_adapter.get_view(6)
|
||||
self.assertEqual(g_view.text, 'G')
|
||||
letters_dict_adapter.handle_selection(g_view)
|
||||
|
||||
m_view = letters_dict_adapter.get_view(12)
|
||||
self.assertEqual(m_view.text, 'M')
|
||||
letters_dict_adapter.handle_selection(m_view)
|
||||
|
||||
letters_dict_adapter.cut_to_sel()
|
||||
self.assertEqual(len(letters_dict_adapter.data), 2)
|
||||
|
||||
|
|
|
@ -2,12 +2,6 @@
|
|||
Selection tests
|
||||
===============
|
||||
|
||||
NOTE: In tests, take care calling list_view.get_item_view(0), because this
|
||||
will call list_adapter.get_view(0), which does a selection.
|
||||
Instead, you can get the cached view with direct references such as
|
||||
list_view.item_view_instances[0], paying attention to when one should be
|
||||
available. If you want to trigger a selection as a user touch would do,
|
||||
call list_item_view.get_item_view().
|
||||
'''
|
||||
|
||||
import unittest
|
||||
|
@ -17,7 +11,7 @@ from kivy.uix.listview import ListView, ListItemButton
|
|||
from kivy.properties import NumericProperty, StringProperty
|
||||
from kivy.adapters.listadapter import ListAdapter
|
||||
from kivy.adapters.dictadapter import DictAdapter
|
||||
from kivy.adapters.mixins.selection import SelectableDataItem
|
||||
from kivy.adapters.models import SelectableDataItem
|
||||
|
||||
# The following integers_dict and fruit categories / fruit data dictionaries
|
||||
# are from kivy/examples/widgets/lists/fixtures.py, and the classes are from
|
||||
|
@ -275,6 +269,7 @@ class ListAdapterTestCase(unittest.TestCase):
|
|||
list_adapter = ListAdapter(data=fruit_data_items,
|
||||
args_converter=self.args_converter,
|
||||
selection_mode='single',
|
||||
propagate_selection_to_data=True,
|
||||
allow_empty_selection=True,
|
||||
cls=ListItemButton)
|
||||
list_view = ListView(adapter=list_adapter)
|
||||
|
@ -291,13 +286,10 @@ class ListAdapterTestCase(unittest.TestCase):
|
|||
self.assertEqual(len(list_adapter.selection), 0)
|
||||
|
||||
# Still no selection, but triggering a selection should make len = 1.
|
||||
# Calling get_item_view(0)) here will, in turn, call
|
||||
# list_adapter.get_item_view(0), which does a selection if the
|
||||
# associated data item is selected. So, first we need to select the
|
||||
# associated data item.
|
||||
# So, first we need to select the associated data item.
|
||||
self.assertEqual(fruit_data_items[0].name, 'Apple')
|
||||
fruit_data_items[0].is_selected = True
|
||||
apple = list_view.get_item_view(0)
|
||||
apple = list_view.adapter.get_view(0)
|
||||
self.assertEqual(apple.text, 'Apple')
|
||||
self.assertTrue(apple.is_selected)
|
||||
self.assertEqual(len(list_adapter.selection), 1)
|
||||
|
@ -314,7 +306,7 @@ class ListAdapterTestCase(unittest.TestCase):
|
|||
# at the end of its __init__(), calls check_for_empty_selection()
|
||||
# and triggers the initial selection, because allow_empty_selection is
|
||||
# False.
|
||||
apple = list_view.item_view_instances[0]
|
||||
apple = list_view.adapter.cached_views[0]
|
||||
self.assertEqual(list_adapter.selection[0], apple)
|
||||
self.assertEqual(len(list_adapter.selection), 1)
|
||||
list_adapter.check_for_empty_selection()
|
||||
|
@ -326,6 +318,7 @@ class ListAdapterTestCase(unittest.TestCase):
|
|||
list_adapter = ListAdapter(data=fruit_data_items,
|
||||
args_converter=self.args_converter,
|
||||
selection_mode='multiple',
|
||||
propagate_selection_to_data=True,
|
||||
allow_empty_selection=False,
|
||||
cls=ListItemButton)
|
||||
list_view = ListView(adapter=list_adapter)
|
||||
|
@ -341,7 +334,7 @@ class ListAdapterTestCase(unittest.TestCase):
|
|||
# Add Avocado to the selection, doing necessary steps on data first.
|
||||
self.assertEqual(fruit_data_items[1].name, 'Avocado')
|
||||
fruit_data_items[1].is_selected = True
|
||||
avocado = list_view.get_item_view(1) # does selection
|
||||
avocado = list_view.adapter.get_view(1) # does selection
|
||||
self.assertEqual(avocado.text, 'Avocado')
|
||||
self.assertEqual(len(list_adapter.selection), 2)
|
||||
|
||||
|
@ -362,7 +355,7 @@ class ListAdapterTestCase(unittest.TestCase):
|
|||
# And select some different ones.
|
||||
self.assertEqual(fruit_data_items[2].name, 'Banana')
|
||||
fruit_data_items[2].is_selected = True
|
||||
banana = list_view.get_item_view(2) # does selection
|
||||
banana = list_view.adapter.get_view(2) # does selection
|
||||
self.assertEqual(list_adapter.selection, [apple, avocado, banana])
|
||||
self.assertEqual(len(list_adapter.selection), 3)
|
||||
|
||||
|
@ -370,6 +363,7 @@ class ListAdapterTestCase(unittest.TestCase):
|
|||
list_adapter = ListAdapter(data=fruit_data_items,
|
||||
args_converter=self.args_converter,
|
||||
selection_mode='multiple',
|
||||
propagate_selection_to_data=True,
|
||||
selection_limit=3,
|
||||
allow_empty_selection=True,
|
||||
cls=ListItemButton)
|
||||
|
@ -379,7 +373,7 @@ class ListAdapterTestCase(unittest.TestCase):
|
|||
for i in range(5):
|
||||
# Add item to the selection, doing necessary steps on data first.
|
||||
fruit_data_items[i].is_selected = True
|
||||
list_view.get_item_view(i) # does selection
|
||||
list_view.adapter.get_view(i) # does selection
|
||||
self.assertEqual(len(list_adapter.selection),
|
||||
i + 1 if i < 3 else 3)
|
||||
|
||||
|
@ -387,6 +381,7 @@ class ListAdapterTestCase(unittest.TestCase):
|
|||
list_adapter = ListAdapter(data=fruit_data_items,
|
||||
args_converter=self.args_converter,
|
||||
selection_mode='single',
|
||||
propagate_selection_to_data=True,
|
||||
allow_empty_selection=False,
|
||||
cls=ListItemButton)
|
||||
|
||||
|
@ -396,22 +391,17 @@ class ListAdapterTestCase(unittest.TestCase):
|
|||
|
||||
list_view = ListView(adapter=list_adapter)
|
||||
|
||||
list_adapter.touch_selection()
|
||||
|
||||
# There should have been a call for initial selection, triggered by
|
||||
# the check_for_empty_selection() at the end of ListView.__init__()
|
||||
# followed by the forced dispatch from the call to touch_selection.
|
||||
self.assertEqual(selection_observer.call_count, 2)
|
||||
self.assertEqual(selection_observer.call_count, 0)
|
||||
|
||||
# From the check for initial selection, we should have apple selected.
|
||||
self.assertEqual(list_adapter.selection[0].text, 'Apple')
|
||||
self.assertEqual(len(list_adapter.selection), 1)
|
||||
|
||||
# Go throught the tests routine to trigger selection of banana.
|
||||
# Go through the tests routine to trigger selection of banana.
|
||||
# (See notes above about triggering selection in tests.)
|
||||
self.assertEqual(fruit_data_items[2].name, 'Banana')
|
||||
fruit_data_items[2].is_selected = True
|
||||
banana = list_view.get_item_view(2) # does selection
|
||||
banana = list_view.adapter.get_view(2) # does selection
|
||||
self.assertTrue(banana.is_selected)
|
||||
|
||||
# Now unselect it with handle_selection().
|
||||
|
@ -424,10 +414,10 @@ class ListAdapterTestCase(unittest.TestCase):
|
|||
|
||||
# Call count:
|
||||
#
|
||||
# Apple got selected initally (1), then unselected (2) when Banana was
|
||||
# selected (3). Then banana was unselected(4), causing reselection of
|
||||
# Apple (5). len should be 1.
|
||||
self.assertEqual(selection_observer.call_count, 5)
|
||||
# Apple got selected initally (0), then unselected when Banana was
|
||||
# selected (1). Then banana was unselected, causing reselection of
|
||||
# Apple (3). len should be 1.
|
||||
self.assertEqual(selection_observer.call_count, 3)
|
||||
self.assertEqual(len(list_adapter.selection), 1)
|
||||
|
||||
|
||||
|
|
|
@ -6,7 +6,8 @@ ListView tests
|
|||
import unittest
|
||||
|
||||
from kivy.uix.label import Label
|
||||
from kivy.adapters.listadapter import SimpleListAdapter, ListAdapter
|
||||
from kivy.adapters.listadapter import ListAdapter
|
||||
from kivy.adapters.simplelistadapter import SimpleListAdapter
|
||||
from kivy.uix.listview import ListItemButton, ListView
|
||||
|
||||
|
||||
|
@ -36,7 +37,7 @@ class ListViewTestCase(unittest.TestCase):
|
|||
self.assertEqual(type(list_view.adapter), SimpleListAdapter)
|
||||
self.assertFalse(hasattr(list_view.adapter, 'selection'))
|
||||
self.assertEqual(len(list_view.adapter.data), 100)
|
||||
self.assertEqual(type(list_view.get_item_view(0)), Label)
|
||||
self.assertEqual(type(list_view.adapter.get_view(0)), Label)
|
||||
|
||||
def test_list_view_with_list_of_integers(self):
|
||||
|
||||
|
@ -57,7 +58,26 @@ class ListViewTestCase(unittest.TestCase):
|
|||
self.assertEqual(type(list_view.adapter), ListAdapter)
|
||||
self.assertTrue(hasattr(list_view.adapter, 'selection'))
|
||||
self.assertEqual(len(list_view.adapter.data), 100)
|
||||
self.assertEqual(type(list_view.get_item_view(0)), ListItemButton)
|
||||
self.assertEqual(type(list_view.adapter.get_view(0)), ListItemButton)
|
||||
|
||||
def test_list_view_with_list_of_integers_scrolling(self):
|
||||
|
||||
data = [{'text': str(i), 'is_selected': False} for i in xrange(100)]
|
||||
|
||||
args_converter = lambda rec: {'text': rec['text'],
|
||||
'size_hint_y': None,
|
||||
'height': 25}
|
||||
|
||||
list_adapter = ListAdapter(data=data,
|
||||
args_converter=args_converter,
|
||||
selection_mode='single',
|
||||
allow_empty_selection=False,
|
||||
cls=ListItemButton)
|
||||
|
||||
list_view = ListView(adapter=list_adapter)
|
||||
|
||||
list_view.scroll_to(20)
|
||||
self.assertEqual(list_view._index, 20)
|
||||
|
||||
def test_simple_list_view_deletion(self):
|
||||
|
||||
|
@ -68,3 +88,9 @@ class ListViewTestCase(unittest.TestCase):
|
|||
del list_view.adapter.data[49]
|
||||
self.assertEqual(len(list_view.adapter.data), 99)
|
||||
|
||||
def test_list_view_bad_instantiation(self):
|
||||
with self.assertRaises(Exception) as cm:
|
||||
listview = ListView()
|
||||
|
||||
msg = 'ListView: item_strings needed or an adapter'
|
||||
self.assertEqual(str(cm.exception), msg)
|
||||
|
|
|
@ -5,8 +5,8 @@ Abstract View
|
|||
.. versionadded:: 1.5
|
||||
|
||||
The :class:`AbstractView` widget has an adapter property for an adapter that
|
||||
mediates to data, and an item_view_instances dict property that holds views
|
||||
managed by the adapter.
|
||||
mediates to data. The adapter manages an item_view_instances dict property
|
||||
that holds views for each data item, operating as a cache.
|
||||
|
||||
'''
|
||||
|
||||
|
@ -24,5 +24,3 @@ class AbstractView(FloatLayout):
|
|||
'''The adapter can be one of several defined in kivy/adapters. The most
|
||||
common example is the ListAdapter used for managing data items in a list.
|
||||
'''
|
||||
|
||||
|
||||
|
|
|
@ -62,13 +62,12 @@ 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
|
||||
---------------------------------------------
|
||||
ListAdapter and DictAdapter
|
||||
---------------------------
|
||||
|
||||
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:`CollectionAdapter`, extending its
|
||||
base functionality for selection.
|
||||
:class:`DictAdapter` each contain functionality for selection.
|
||||
|
||||
See the :class:`ListAdapter` docs for details, but here are synopses of
|
||||
its arguments:
|
||||
|
@ -198,9 +197,6 @@ selected, because allow_empty_selection is False.
|
|||
Uses for Selection
|
||||
------------------
|
||||
|
||||
In the previous example, we saw how a listview gains selection support just by
|
||||
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.
|
||||
|
||||
|
@ -244,13 +240,46 @@ from kivy.uix.label import Label
|
|||
from kivy.uix.boxlayout import BoxLayout
|
||||
from kivy.adapters.simplelistadapter import SimpleListAdapter
|
||||
from kivy.uix.abstractview import AbstractView
|
||||
from kivy.uix.selectableview import SelectableView
|
||||
from kivy.properties import ObjectProperty, DictProperty, \
|
||||
NumericProperty, ListProperty, BooleanProperty
|
||||
from kivy.lang import Builder
|
||||
from math import ceil, floor
|
||||
|
||||
|
||||
class SelectableView(object):
|
||||
'''The :class:`SelectableView` mixin is used in list item and other
|
||||
classes that are to be instantiated in a list view, or another class
|
||||
which uses a selection-enabled adapter such as ListAdapter. select() and
|
||||
deselect() are to be overridden with display code to mark items as
|
||||
selected or not, if desired.
|
||||
'''
|
||||
|
||||
index = NumericProperty(-1)
|
||||
'''The index into the underlying data list or the data item this view
|
||||
represents.
|
||||
'''
|
||||
|
||||
is_selected = BooleanProperty(False)
|
||||
'''A SelectableView instance carries this property, which should be kept
|
||||
in sync with the equivalent property in the data item it represents.
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(SelectableView, self).__init__(**kwargs)
|
||||
|
||||
def select(self, *args):
|
||||
'''The list item is responsible for updating the display for
|
||||
being selected, if desired.
|
||||
'''
|
||||
self.is_selected = True
|
||||
|
||||
def deselect(self, *args):
|
||||
'''The list item is responsible for updating the display for
|
||||
being unselected, if desired.
|
||||
'''
|
||||
self.is_selected = False
|
||||
|
||||
|
||||
class ListItemButton(SelectableView, Button):
|
||||
selected_color = ListProperty([1., 0., 0., 1])
|
||||
deselected_color = None
|
||||
|
@ -316,7 +345,7 @@ class CompositeListItem(SelectableView, BoxLayout):
|
|||
selected_color = ListProperty([1., 0., 0., 1])
|
||||
deselected_color = ListProperty([.33, .33, .33, 1])
|
||||
|
||||
representing_cls = ObjectProperty([])
|
||||
representing_cls = ObjectProperty(None)
|
||||
'''Which component view class, if any, should represent for the
|
||||
composite list item in __repr__()?
|
||||
'''
|
||||
|
@ -327,18 +356,12 @@ class CompositeListItem(SelectableView, BoxLayout):
|
|||
# Example data:
|
||||
#
|
||||
# 'cls_dicts': [{'cls': ListItemButton,
|
||||
# 'kwargs': {'text': "Left",
|
||||
# 'merge_text': True,
|
||||
# 'delimiter': '-'}},
|
||||
# 'kwargs': {'text': "Left"}},
|
||||
# 'cls': ListItemLabel,
|
||||
# 'kwargs': {'text': "Middle",
|
||||
# 'merge_text': True,
|
||||
# 'delimiter': '-',
|
||||
# 'is_representing_cls': True}},
|
||||
# 'cls': ListItemButton,
|
||||
# 'kwargs': {'text': "Right",
|
||||
# 'merge_text': True,
|
||||
# 'delimiter': '-'}}]}
|
||||
# 'kwargs': {'text': "Right"}]
|
||||
|
||||
# There is an index to the data item this composite list item view
|
||||
# represents. Get it from kwargs and pass it along to children in the
|
||||
|
@ -347,34 +370,26 @@ class CompositeListItem(SelectableView, BoxLayout):
|
|||
|
||||
for cls_dict in kwargs['cls_dicts']:
|
||||
cls = cls_dict['cls']
|
||||
cls_kwargs = cls_dict['kwargs']
|
||||
cls_kwargs = cls_dict.get('kwargs', None)
|
||||
|
||||
cls_kwargs['index'] = index
|
||||
if cls_kwargs:
|
||||
cls_kwargs['index'] = index
|
||||
|
||||
if 'selection_target' not in cls_kwargs:
|
||||
cls_kwargs['selection_target'] = self
|
||||
if 'selection_target' not in cls_kwargs:
|
||||
cls_kwargs['selection_target'] = self
|
||||
|
||||
if 'merge_text' in cls_kwargs:
|
||||
if cls_kwargs['merge_text'] is True:
|
||||
if 'text' in cls_kwargs:
|
||||
if 'delimiter' in cls_kwargs:
|
||||
cls_kwargs['text'] = "{0}{1}{2}".format(
|
||||
cls_kwargs['text'],
|
||||
cls_kwargs['delimiter'],
|
||||
kwargs['text'])
|
||||
else:
|
||||
cls_kwargs['text'] = "{0}{1}".format(
|
||||
cls_kwargs['text'],
|
||||
kwargs['text'])
|
||||
elif 'text' not in cls_kwargs:
|
||||
if 'text' not in cls_kwargs:
|
||||
cls_kwargs['text'] = kwargs['text']
|
||||
elif 'text' not in cls_kwargs:
|
||||
cls_kwargs['text'] = kwargs['text']
|
||||
|
||||
if 'is_representing_cls' in cls_kwargs:
|
||||
self.representing_cls = cls
|
||||
if 'is_representing_cls' in cls_kwargs:
|
||||
self.representing_cls = cls
|
||||
|
||||
self.add_widget(cls(**cls_kwargs))
|
||||
self.add_widget(cls(**cls_kwargs))
|
||||
else:
|
||||
cls_kwargs = {}
|
||||
if 'text' in kwargs:
|
||||
cls_kwargs['text'] = kwargs['text']
|
||||
self.add_widget(cls(**cls_kwargs))
|
||||
|
||||
def select(self, *args):
|
||||
self.background_color = self.selected_color
|
||||
|
@ -396,7 +411,7 @@ class CompositeListItem(SelectableView, BoxLayout):
|
|||
if self.representing_cls is not None:
|
||||
return str(self.representing_cls)
|
||||
else:
|
||||
return 'unknown'
|
||||
return super(CompositeListItem, self).__repr__()
|
||||
|
||||
|
||||
Builder.load_string('''
|
||||
|
@ -464,7 +479,7 @@ class ListView(AbstractView, EventDispatcher):
|
|||
# provided, raise an exception.
|
||||
if 'adapter' not in kwargs:
|
||||
if 'item_strings' not in kwargs:
|
||||
raise Exception('ListView: input needed, or an adapter')
|
||||
raise Exception('ListView: item_strings needed or an adapter')
|
||||
|
||||
list_adapter = SimpleListAdapter(data=kwargs['item_strings'],
|
||||
cls=Label)
|
||||
|
|
Loading…
Reference in New Issue