Skip to content

Instantly share code, notes, and snippets.

@geojeff
Forked from tito/listview.py
Created July 3, 2012 16:33
Show Gist options
  • Save geojeff/3040858 to your computer and use it in GitHub Desktop.
Save geojeff/3040858 to your computer and use it in GitHub Desktop.
# modified from tito's gist: https://gist.github.com/3010607
from kivy.clock import Clock
from kivy.event import EventDispatcher
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.widget import Widget
from kivy.properties import ObjectProperty, DictProperty, \
NumericProperty, ListProperty, \
BooleanProperty, OptionProperty
from kivy.lang import Builder
from math import ceil, floor
# If you want to generate a bunch of list items as number names, you can use:
# http://pynum2word.sourceforge.net/
#from pynum2word.num2word_EN import n2w, to_card, to_ord, to_ordnum
Builder.load_string('''
<ListView>:
container: container
ScrollView:
pos: root.pos
on_scroll_y: root._scroll(args[1])
do_scroll_x: False
GridLayout:
cols: 1
id: container
size_hint_y: None
''')
class Adapter(EventDispatcher):
'''Adapter is a bridge between an AbstractView and the data.
'''
cls = ObjectProperty(None)
template = ObjectProperty(None)
converter = ObjectProperty(None)
selection = ListProperty([])
# [TODO] Presently unused.
selection_mode = OptionProperty('multiple', options=('single', 'multiple', 'filtered'))
def __init__(self, **kwargs):
self.register_event_type('on_select')
super(Adapter, self).__init__(**kwargs)
if self.cls is None and self.template is None:
raise Exception('A cls or template must be defined')
if self.cls is not None and self.template is not None:
raise Exception('Cannot use cls and template at the same time')
def get_count(self):
raise NotImplementedError()
def get_item(self, index):
raise NotImplementedError()
def get_view(self, index):
item = self.get_item(index)
if item is None:
return None
if self.converter:
item = self.converter(item)
if self.cls:
print 'CREATE VIEW FOR', index
item_instance = self.cls(
listview_selection_callback=self.handle_selection, **item)
return item_instance
return Builder.template(self.template, **item)
def handle_selection(self, item):
if item not in self.selection:
self.selection.append(item)
else:
self.selection.remove(item)
self.dispatch('on_select')
print 'selection is now', self.selection
# This is for the list adapter, if it wants to get selection events.
def on_select(self, *args):
pass
# Things to think about:
#
# There are other possibilities:
#
# For inspiration, see:
#
# https://github.com/sproutcore/sproutcore/blob/master/frameworks/core_foundation/controllers/array.js
#
# From that, additional possibilities, to those stubbed out in
# methods below.
#
# - a boolean for whether or not editing of items is allowed
# - a boolean for whether or not to destroy on removal (if
# applicable)
# - guards for adding, removing, sorting items
#
def add_item(self, item):
pass
def remove_item(self, item):
pass
def replace_item(self, item):
pass
# This method would have an associated sort_key property.
def sorted_items(self):
pass
class ListAdapter(Adapter):
'''Adapter around a simple Python list
'''
def __init__(self, data, **kwargs):
super(ListAdapter, self).__init__(**kwargs)
if type(data) not in (tuple, list, dict):
raise Exception('ListAdapter: data must be a tuple, list, or dict')
self.data = data
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]
class AbstractView(FloatLayout):
'''View using an Adapter as a data provider
'''
adapter = ObjectProperty(None)
items = DictProperty({})
def set_item(self, index, item):
pass
def get_item(self, index):
items = self.items
if index in items:
return items[index]
item = self.adapter.get_view(index)
if item:
items[index] = item
return item
class ListView(AbstractView):
'''Implementation of an Abstract View as a vertical scrollable list.
'''
divider = ObjectProperty(None)
divider_height = NumericProperty(2)
container = ObjectProperty(None)
row_height = NumericProperty(None)
_index = NumericProperty(0)
_sizes = DictProperty({})
_count = NumericProperty(0)
_wstart = NumericProperty(0)
_wend = NumericProperty(None)
def __init__(self, **kwargs):
super(ListView, self).__init__(**kwargs)
self._trigger_populate = Clock.create_trigger(self._spopulate, -1)
self.bind(size=self._trigger_populate,
pos=self._trigger_populate)
self.populate()
def _scroll(self, scroll_y):
if self.row_height is None:
return
scroll_y = 1 - min(1, max(scroll_y, 0))
container = self.container
mstart = (container.height - self.height) * scroll_y
mend = mstart + self.height
# convert distance to index
rh = self.row_height
istart = int(ceil(mstart / rh))
iend = int(floor(mend / rh))
istart = max(0, istart - 1)
iend = max(0, iend - 1)
if istart < self._wstart:
rstart = max(0, istart - 10)
self.populate(rstart, iend)
self._wstart = rstart
self._wend = iend
elif iend > self._wend:
self.populate(istart, iend + 10)
self._wstart = istart
self._wend = iend + 10
def _spopulate(self, *dt):
self.populate()
def populate(self, istart=None, iend=None):
print 'populate', istart, iend
container = self.container
sizes = self._sizes
rh = self.row_height
# ensure we know what we want to show
if istart is None:
istart = self._wstart
iend = self._wend
# clear the view
container.clear_widgets()
# guess only ?
if iend is not None:
# fill with a "padding"
fh = 0
for x in xrange(istart):
fh += sizes[x] if x in sizes else rh
container.add_widget(Widget(size_hint_y=None, height=fh))
# now fill with real item
index = istart
while index <= iend:
item = self.get_item(index)
index += 1
if item is None:
continue
sizes[index] = item.height
container.add_widget(item)
else:
available_height = self.height
real_height = 0
index = self._index
count = 0
while available_height > 0:
item = self.get_item(index)
sizes[index] = item.height
index += 1
count += 1
container.add_widget(item)
available_height -= item.height
real_height += item.height
self._count = count
# extrapolate the full size of the container from the size
# of items
if count:
container.height = \
real_height / count * self.adapter.get_count()
if self.row_height is None:
self.row_height = real_height / count
class ListItemBase:
is_selected = BooleanProperty(False)
listview_selection_callback = ObjectProperty(None)
# The list item must handle the selection AND call the list's
# listview_selection_callback.
def handle_selection(self, *args):
self.listview_selection_callback(*args)
# The list item is responsible for updating the display for
# being selected.
def select(self):
raise NotImplementedError()
# The list item is responsible for updating the display for
# being unselected.
def unselect(self):
raise NotImplementedError()
# "Sub" to indicate that this class is for "sub" list items -- a list item
# could consist of a button on the left, several labels in the middle, and
# another button on the right. Not sure of the merit of allowing, perhaps,
# some "sub" list items to react to touches and others not, if that were to
# be enabled.
#
class ListItemSubButton(ListItemBase, Button):
selected_color = ListProperty([1., 0., 0., 1])
unselected_color = ListProperty([.33, .33, .33, 1])
def __init__(self, listview_selection_callback, **kwargs):
self.listview_selection_callback = listview_selection_callback
super(ListItemSubButton, self).__init__(**kwargs)
self.bind(on_release=self.handle_selection)
def handle_selection(self, button):
if self.is_selected:
self.select()
else:
self.unselect()
# Not this "sub" list item, but the list item.
self.listview_selection_callback(self.parent)
# [TODO] At least there is some action on this set, but
# the color gets somehow composited.
def select(self, *args):
self.background_color = self.selected_color
# [TODO] No effect seen, but it is grey, so might be happening.
def unselect(self, *args):
self.background_color = self.unselected_color
# Same idea as "sub" for button above.
#
class ListItemSubLabel(ListItemBase, Label):
selected_color = ListProperty([1., 0., 0., 1])
unselected_color = ListProperty([.33, .33, .33, 1])
def __init__(self, listview_selection_callback, **kwargs):
self.listview_selection_callback = listview_selection_callback
super(ListItemSubLabel, self).__init__(**kwargs)
self.bind(on_release=self.handle_selection)
def handle_selection(self, button):
if self.is_selected:
self.select()
else:
self.unselect()
# Not this "sub" list item, but the list item (parent).
self.listview_selection_callback(self.parent)
# [TODO] Should Label have background_color, like Button, etc.?
# [TODO] Not tested yet.
def select(self, *args):
self.bold = True
def unselect(self, *args):
self.bold = False
# ListItem (BoxLayout) by default uses orientation='horizontal',
# but could be used also for a side-to-side display of items.
#
class ListItem(ListItemBase, BoxLayout):
# ListItemSubButton sublasses Button, which has background_color.
# Here we must add this property.
background_color = ListProperty([1, 1, 1, 1])
selected_color = ListProperty([1., 0., 0., 1])
unselected_color = ListProperty([.33, .33, .33, 1])
def __init__(self, listview_selection_callback, **kwargs):
self.listview_selection_callback = listview_selection_callback
super(ListItem, self).__init__(size_hint_y=None, height=25)
# Now this button just has text '>', but it would be neat to make the
# left button hold icons -- the list would be heterogeneous, containing
# different ListItem types that could be filtered perhaps (an option
# for selecting all of a given type, for example).
self.icon_button = ListItemSubButton(
listview_selection_callback=listview_selection_callback,
text='>', size_hint_x=.05, size_hint_y=None, height=25)
self.content_button = ListItemSubButton(
listview_selection_callback=listview_selection_callback, **kwargs)
self.add_widget(self.icon_button)
self.add_widget(self.content_button)
self.bind(on_release=self.handle_selection)
def handle_selection(self, item):
if self.is_selected:
self.select()
else:
self.unselect()
self.listview_selection_callback(self)
def select(self, *args):
self.background_color = self.selected_color
def unselect(self, *args):
self.background_color = self.unselected_color
if __name__ == '__main__':
from kivy.base import runTouchApp
'''
from glob import glob
from kivy.uix.image import AsyncImage
adapter = ListAdapter(glob('~/Development/kivy/kivy_statechart/examples/Images/*.jpg'),
converter=lambda x: {'source': x, 'size_hint_y': None, 'height': 640},
cls=AsyncImage)
'''
def selection_changed(*args):
print 'selection changed', args
# if using the pynum2word lib:
# adapter = ListAdapter([to_card(n) for n in range(200)],
adapter = ListAdapter(['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten', 'eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen', 'sixteen', 'seventeen', 'eighteen', 'nineteen', 'twenty', 'twenty-one', 'twenty-two', 'twenty-three', 'twenty-four', 'twenty-five', 'twenty-six', 'twenty-seven', 'twenty-eight', 'twenty-nine', 'thirty', 'thirty-one', 'thirty-two', 'thirty-three', 'thirty-four', 'thirty-five', 'thirty-six', 'thirty-seven', 'thirty-eight', 'thirty-nine', 'forty', 'forty-one', 'forty-two', 'forty-three', 'forty-four', 'forty-five', 'forty-six', 'forty-seven', 'forty-eight', 'forty-nine', 'fifty', 'fifty-one', 'fifty-two', 'fifty-three', 'fifty-four', 'fifty-five', 'fifty-six', 'fifty-seven', 'fifty-eight', 'fifty-nine', 'sixty', 'sixty-one', 'sixty-two', 'sixty-three', 'sixty-four', 'sixty-five', 'sixty-six', 'sixty-seven', 'sixty-eight', 'sixty-nine', 'seventy', 'seventy-one', 'seventy-two', 'seventy-three', 'seventy-four', 'seventy-five', 'seventy-six', 'seventy-seven', 'seventy-eight', 'seventy-nine', 'eighty', 'eighty-one', 'eighty-two', 'eighty-three', 'eighty-four', 'eighty-five', 'eighty-six', 'eighty-seven', 'eighty-eight', 'eighty-nine', 'ninety', 'ninety-one', 'ninety-two', 'ninety-three', 'ninety-four', 'ninety-five', 'ninety-six', 'ninety-seven', 'ninety-eight', 'ninety-nine', 'one hundred', 'one hundred and one', 'one hundred and two', 'one hundred and three', 'one hundred and four', 'one hundred and five', 'one hundred and six', 'one hundred and seven', 'one hundred and eight', 'one hundred and nine', 'one hundred and ten', 'one hundred and eleven', 'one hundred and twelve', 'one hundred and thirteen', 'one hundred and fourteen', 'one hundred and fifteen', 'one hundred and sixteen', 'one hundred and seventeen', 'one hundred and eighteen', 'one hundred and nineteen', 'one hundred and twenty', 'one hundred and twenty-one', 'one hundred and twenty-two', 'one hundred and twenty-three', 'one hundred and twenty-four', 'one hundred and twenty-five', 'one hundred and twenty-six', 'one hundred and twenty-seven', 'one hundred and twenty-eight', 'one hundred and twenty-nine', 'one hundred and thirty', 'one hundred and thirty-one', 'one hundred and thirty-two', 'one hundred and thirty-three', 'one hundred and thirty-four', 'one hundred and thirty-five', 'one hundred and thirty-six', 'one hundred and thirty-seven', 'one hundred and thirty-eight', 'one hundred and thirty-nine', 'one hundred and forty', 'one hundred and forty-one', 'one hundred and forty-two', 'one hundred and forty-three', 'one hundred and forty-four', 'one hundred and forty-five', 'one hundred and forty-six', 'one hundred and forty-seven', 'one hundred and forty-eight', 'one hundred and forty-nine', 'one hundred and fifty', 'one hundred and fifty-one', 'one hundred and fifty-two', 'one hundred and fifty-three', 'one hundred and fifty-four', 'one hundred and fifty-five', 'one hundred and fifty-six', 'one hundred and fifty-seven', 'one hundred and fifty-eight', 'one hundred and fifty-nine', 'one hundred and sixty', 'one hundred and sixty-one', 'one hundred and sixty-two', 'one hundred and sixty-three', 'one hundred and sixty-four', 'one hundred and sixty-five', 'one hundred and sixty-six', 'one hundred and sixty-seven', 'one hundred and sixty-eight', 'one hundred and sixty-nine', 'one hundred and seventy', 'one hundred and seventy-one', 'one hundred and seventy-two', 'one hundred and seventy-three', 'one hundred and seventy-four', 'one hundred and seventy-five', 'one hundred and seventy-six', 'one hundred and seventy-seven', 'one hundred and seventy-eight', 'one hundred and seventy-nine', 'one hundred and eighty', 'one hundred and eighty-one', 'one hundred and eighty-two', 'one hundred and eighty-three', 'one hundred and eighty-four', 'one hundred and eighty-five', 'one hundred and eighty-six', 'one hundred and eighty-seven', 'one hundred and eighty-eight', 'one hundred and eighty-nine', 'one hundred and ninety', 'one hundred and ninety-one', 'one hundred and ninety-two', 'one hundred and ninety-three', 'one hundred and ninety-four', 'one hundred and ninety-five', 'one hundred and ninety-six', 'one hundred and ninety-seven', 'one hundred and ninety-eight', 'one hundred and ninety-nine'],
converter=lambda x: {'text': x, 'size_hint_y': None, 'height': 25},
cls=ListItem)
view = ListView(adapter=adapter)
runTouchApp(view)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment