Reputation: 617
I want to use Kivy's RecycleView
to make a multiline scrollable selection list, but I need to set as selected some of the items by default. The user must still be able to unselect them, if they wish (I want to implement some sort of proposed choices).
Based on the example on Kivy documentation, here follows a minimal working code that presents my problem:
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.recycleview import RecycleView
from kivy.uix.recycleview.views import RecycleDataViewBehavior
from kivy.uix.label import Label
from kivy.properties import BooleanProperty
from kivy.uix.recycleboxlayout import RecycleBoxLayout
from kivy.uix.behaviors import FocusBehavior
from kivy.uix.recycleview.layout import LayoutSelectionBehavior
Builder.load_string('''
<SelectableLabel>:
# Draw a background to indicate selection
canvas.before:
Color:
rgba: (.0, 0.9, .1, .3) if self.selected else (0, 0, 0, 1)
Rectangle:
pos: self.pos
size: self.size
<RV>:
viewclass: 'SelectableLabel'
SelectableRecycleBoxLayout:
default_size: None, dp(56)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
multiselect: True
touch_multiselect: True
''')
class SelectableRecycleBoxLayout(FocusBehavior, LayoutSelectionBehavior,
RecycleBoxLayout):
''' Adds selection and focus behaviour to the view. '''
class SelectableLabel(RecycleDataViewBehavior, Label):
''' Add selection support to the Label '''
index = None
selected = BooleanProperty(False)
selectable = BooleanProperty(True)
def refresh_view_attrs(self, rv, index, data):
''' Catch and handle the view changes '''
self.index = index
self.selected = rv.data[self.index]['selected']
return super(SelectableLabel, self).refresh_view_attrs(
rv, index, data)
def on_touch_down(self, touch):
''' Add selection on touch down '''
if super(SelectableLabel, self).on_touch_down(touch):
return True
if self.collide_point(*touch.pos) and self.selectable:
return self.parent.select_with_touch(self.index, touch)
def apply_selection(self, rv, index, is_selected):
''' Respond to the selection of items in the view. '''
self.selected = is_selected # line X, the gaming change
if is_selected:
print("selection changed to {0}".format(rv.data[index]))
else:
print("selection removed for {0}".format(rv.data[index]))
class RV(RecycleView):
def __init__(self, items, **kwargs):
if 'selection' in kwargs:
selection = kwargs['selection']
del kwargs['selection']
else:
selection = [False]*len(items)
super().__init__(**kwargs)
self.data = [{'text': x, 'selected': selection[i]} \
for i, x in enumerate(items)]
items = [
"apple", "dog", "banana", "cat", "pear", "rat",
"pineapple", "bat", "pizza", "ostrich",
"apple", "dog", "banana", "cat", "pear", "rat",
"pineapple", "bat", "pizza", "ostrich",
]
class TestApp(App):
def build(self):
return RV(items,
selection=[x[0] in ['p','a','r'] \
for x in items]
)
if __name__ == '__main__':
test_app = TestApp()
test_app.run()
This code doesn't show the default items actually selected. While I was conducting a proper investigation, I notice that if I comment a single line in the apply_selection
method (line X comment in code above), i.e., if I change it to # self.selected = is_selected
, I finally can see all my items with default selections.
Problem is, as you should probably know, that's the instruction that allows the selection feature to happen (!), i.e., this line while commented wins me my desired default items, but I lose the ability to actually select/unselect items. I think that the is_selected
parameter is some sort of event which somehow detects an actual click selection and, while instantiating the RV class, some other method unselect all items of the list after apply_selection
comes to play.
I tried to look up into documentation, but I don't even know what to search for. I'm missing which method I should overwrite in order to make this default trick finally work together with selection. Any thoughts?
Upvotes: 0
Views: 131
Reputation: 617
After some diggings, I drop the SelectableLabel
approach and adopted Button
to do the trick. I'm registering my working custom RecycleView here to others:
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.button import Button
from kivy.uix.recycleview import RecycleView
from kivy.properties import BooleanProperty
from kivy.uix.gridlayout import GridLayout
Builder.load_string('''
<Item>:
index: None
on_release: root.parent.parent.apply_selection(self.index)
<RV>:
viewclass: 'Item'
RecycleBoxLayout:
default_size_hint: 1, None
default_size: 0, dp(40)
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
''')
class Item(Button): # root
selected = BooleanProperty(False)
class RV(RecycleView):
DEF_COLOR = [0, 0, 0, 1]
SEL_COLOR = [.0, .4, .8, .4]
def __init__(self, **kwargs):
super(RV, self).__init__(**kwargs)
self.data = []
def apply_selection(self, index, sel=None):
''' Respond to the selection of items in the view. '''
sel = not self.data[index]['selected'] if sel is None else sel
self.data[index]['selected'] = sel
self.data[index]['background_color'] = RV.SEL_COLOR \
if sel else RV.DEF_COLOR
self.refresh_from_data()
print("DEBUG:", self.get_selected_indices())
def update(self, data_list, sel=False, default=None):
if default is None:
default = [sel]*len(data_list)
self.data = [{
'index': i,
'text': str(d),
'selected': default[i],
'background_normal': '',
'background_color': RV.SEL_COLOR \
if default[i] else RV.DEF_COLOR,
} for i, d in enumerate(data_list)]
self.refresh_from_data()
def none_selected(self, instance):
self.update(list(d['text'] for d in self.data))
def all_selected(self, instance):
self.update(list(d['text'] for d in self.data), sel=True)
def get_selected_indices(self):
return list(d['index'] for d in self.data if d['selected'])
class TestPage(GridLayout):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.padding = [5]*4
self.cols = 1
# Usage example
rv = RV(size_hint=(1, 0.8))
items = [i for i in range(0, 100)]
my_default_selections = [3, 8, 42]
default = [i in my_default_selections for i \
in range(len(items))]
rv.update([f"item {str(i)}" for i in items],
default=default)
self.add_widget(rv)
# Access to funcionalities (example)
btn_all = Button(text="Select All", size_hint=(1, 0.05))
btn_all.bind(on_release=rv.all_selected)
self.add_widget(btn_all)
btn_none = Button(text="Select None", size_hint=(1, 0.05))
btn_none.bind(on_release=rv.none_selected)
self.add_widget(btn_none)
self.btn_get = Button(size_hint=(1, 0.1))
self.update()
self.btn_get.bind(on_release=self.update)
self.add_widget(self.btn_get)
def update(self, *largs, **kwargs):
self.btn_get.text='Click to update:\n' + \
str(self.rv.get_selected_indices())
if __name__ == '__main__':
class TestApp(App):
def build(self):
return TestPage()
TestApp().run()
Now I can generate a RV instance with multiline selection that can:
I'm still looking for improvements, though.
Upvotes: 0