1

Problem

I've been messing around with Kivy's RecycleViews in hopes of creating a list builder for one of my projects. I am working out of the second example on Kivy docs' RecycleView page, as it is already almost what I am trying to create. For reference, the example contains a list where multiple items can be selected or unselected.

My main issue is that I have been unable to find any way to get any sort of list containing all of the selected items in the RecycleView. I thought at the very least, I could have a separate list in the RecycleView containing all of the selected items using the apply_selection() method in the SelectableLabel class, however I am unable to distinguish unselecting a SelectableLabel from moving the label outside of the RecycleView's view.

Code

listBuilder.py

from kivy.app import App
from kivy.lang import Builder
from kivy.uix.screenmanager import Screen
from kivy.uix.recycleview import RecycleView
from kivy.uix.recycleview.datamodel import RecycleDataModel
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

class TestScreen(Screen):
    ''' Screen for testing stuff '''
    def pressed(self):
        print(f'Selected: {self.ids.rv.data}')
        self.ids.rv.data.append({'text': '200'})
            

class RV(RecycleView):
    ''' Recycle View '''
    def __init__(self, **kwargs):
        super(RV, self).__init__(**kwargs)
        self.data =  [{'text': str(x)} for x in range(100)]


class SelectableRecycleBoxLayout(FocusBehavior, LayoutSelectionBehavior,
                                 RecycleBoxLayout, RecycleDataModel):
    ''' Adds selection and focus behaviour to the view. '''
    def on_data_changed(self, **kwargs):
        print('Data changed: ', kwargs)
        super(SelectableRecycleBoxLayout, self).on_data_changed(**kwargs)
    

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
        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
        if is_selected:
            print("selection changed to {0}".format(rv.data[index]))
        else:
            print("selection removed for {0}".format(rv.data[index]))

Builder.load_file('listBuilder.kv')

class MainApp(App):
    def build(self):
        return TestScreen()

if __name__ == '__main__':
    MainApp().run()

listBuilder.kv

#:kivy 1.11
#:import ScrollEffect kivy.effects.scroll.ScrollEffect

<SelectableLabel>:
    # Draw a background to indicate selection
    canvas.before:
        Color:
            rgba: (0.2, 0.2, 0.2, 1) if self.selected else (0.5, 0.5, 0.5, 1)
        Rectangle:
            pos: self.pos
            size: self.size

<RV>:
    viewclass: 'SelectableLabel'

    # Scroll effects
    effect_cls: ScrollEffect # Disable overscroll
    scroll_type: ['bars', 'content']
    bar_width: dp(15)
    scroll_wheel_distance: dp(90)

    # Content of recycle view
    SelectableRecycleBoxLayout:
        default_size: None, dp(30)
        default_size_hint: 1, None
        size_hint_y: None
        height: self.minimum_height
        orientation: 'vertical'
        multiselect: True
        touch_multiselect: True

<TestScreen>:
    name: 'test'
    FloatLayout:
        canvas:
            Color:
                rgba: 1,1,1,1
            Rectangle:
                pos: 0,0
                size: self.width, self.height
        # Container for recycle view
        RV:
            id: rv
            size_hint: 0.3, 0.5
            pos_hint: {'center_x': 0.3, 'center_y': 0.5}
            
        Button:
            text: 'Print selected'
            size_hint: 0.2, 0.05
            pos_hint: {'center_x': 0.8, 'center_y': 0.1}
            on_release:
                root.pressed()

For anyone wondering why I am using screens in this example--it's because this is test code for a larger program that uses screens.

I'm using Kivy 1.11.1 and Python 3.7.8


Any help is appreciated as I do not really understand yet fully grasp the RecycleView data models.

Thanks!

Antyos
  • 455
  • 5
  • 11

2 Answers2

1

I found another solution as I started to delve deeper into my project and thought I'd share it here.

There is a built-in way to get the selected nodes; it's accessed through RecycleView.layout_manger.selected_nodes. It returns a list of the indices selected nodes, though it should be noted that they are not in numerical order but in the order that the nodes were selected.

Here are the changes I made to the original code using the new method:

RV class:

class RV(RecycleView):
    ''' Recycle View '''
    def __init__(self, **kwargs):
        super(RV, self).__init__(**kwargs)
        self.data =  [{'text': str(x)} for x in range(100)]
    
    def get_selected(self):
        ''' Returns list of selected nodes dicts '''
        return [self.data[idx] for idx in self.layout_manager.selected_nodes]

If you just care about the indices, you don't necessarily need a method, but I figured it'd be nice to get the actual dicts.

The pressed method then looks like:

def pressed(self):
    print('Selected:')
    
    for d in self.ids.rv.get_selected():
        ('\t', d)

The main reason I opted to switch to this method is that the selected dictionary key does not correspond to the selected state of the node. In the program, I had to remove certain items from the list and the new items at the old indices become selected. It's a bit odd, but it makes more sense when thinking about the selection as a list of indices rather than the individual items being selected or not.

For anyone who is having trouble with other list items becoming selected after the original ones were removed, I found this helpful: https://www.reddit.com/r/kivy/comments/6b0pfp/dhjh7q4

Antyos
  • 455
  • 5
  • 11
0

If you add selected as a key in your RecycleView data, then you can get what you want like this:

class RV(RecycleView):
    ''' Recycle View '''

    def __init__(self, **kwargs):
        super(RV, self).__init__(**kwargs)
        self.data = [{'text': str(x), 'selected': False} for x in range(100)]

Then, in the SelectableLabel class:

def apply_selection(self, rv, index, is_selected):
    ''' Respond to the selection of items in the view. '''
    self.selected = is_selected

    # change selected in data
    rv.data[index]['selected'] = self.selected
    if is_selected:
        print("selection changed to {0}".format(rv.data[index]))
    else:
        print("selection removed for {0}".format(rv.data[index]))

And, finally, you can assemble the list in the pressed() method:

def pressed(self):
    print('Selected:')
    rv = self.ids.rv
    for d in rv.data:
        if d['selected']:
            print('\t', d)
John Anderson
  • 35,991
  • 4
  • 13
  • 36