-
-
Save geojeff/3040858 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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