2

I am trying to create a simple attendance app.

When program is launched all labels are in deselected list

Expected behavior: when any label is selected data moves to the selected list and now selected labels are in the end of the joined list. Then RecycleView refreshes to display this change.

So I managed to make the data to move from one list to another, but I can't make RecycleView to refresh

I tried using ids but failed

I hope someone can help me. I think this is a routine problem for people who are knowledgeable, but for noobs like me it is not.

I am asking question on this site for the first time so sorry in advance

here is the code:

from kivy.app import App
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.textinput import TextInput
from kivy.uix.button import Button
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.properties import ListProperty
from kivy.uix.recycleboxlayout import RecycleBoxLayout
from kivy.uix.behaviors import FocusBehavior
from kivy.uix.recycleview.layout import LayoutSelectionBehavior
from datetime import datetime
import kivy
from kivy.config import Config
Config.set('graphics', 'width', '300')
Config.set('graphics', 'height', '500')

importedlist = ['Novella Varela', 'Caroll Faircloth', 'Douglas Schissler',
                'Rolande Hassell', 'Hayley Rivero', 'Niesha Dungy', 'Winfred Dejonge', 'Venetta Milum']
deselected_list = importedlist[:]
selected_list = []

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
        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.
        and add/remove items from lists
        '''
        self.selected = is_selected
        if self.selected and self.text in deselected_list:
            selected_list.append(self.text)
            deselected_list.remove(self.text)
            print(selected_list)
        elif not self.selected and self.text in selected_list:
            deselected_list.append(self.text)
            selected_list.remove(self.text)
            print(deselected_list)

class RV(RecycleView):
    # this needs to be updated every time any label is selected or deselected
    def __init__(self, **kwargs):
        super(RV, self).__init__(**kwargs)
        self.data = ([{'text': str(row)} for row in sorted(deselected_list)]
                     + [{'text': str(row)} for row in sorted(selected_list)])

class Screen(BoxLayout):
    now = datetime.now()

    def nowdate(self):
        return self.now.strftime('%d')

    def nowmonth(self):
        return self.now.strftime('%m')

    def nowyear(self):
        return self.now.strftime('%y')

    def nowhour(self):
        return self.now.strftime('%H')

    def nowminute(self):
        return self.now.strftime('%M')

Builder.load_string('''
#:import datetime datetime

<Screen>:
    orientation: 'vertical'
    BoxLayout:
        size_hint_y: None
        height: 30
        TextInput:
            id: date
            text: root.nowdate()
        TextInput:
            id: date
            text: root.nowmonth()
        TextInput:
            id: date
            text: root.nowyear()
        TextInput:
            id: date
            text: root.nowhour()
        TextInput:
            id: date
            text: root.nowminute()
        Button:
    RV:
        viewclass: 'SelectableLabel'
        SelectableRecycleBoxLayout:
            default_size: None, dp(45)
            default_size_hint: 1, None
            size_hint_y: None
            height: self.minimum_height
            orientation: 'vertical'
            multiselect: True
            touch_multiselect: True
    Button:
        size_hint_y: None
        height: 30

<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

''')

class TestApp(App):
    def build(self):
        return Screen()

if __name__ == '__main__':
    TestApp().run()
  • 'Expected behavior: when any label is selected data moves to the selected list and now selected labels are in the end of the joined list. Then RecycleView refreshes to display this change.' Do you mean you want to remove the labels when they're added to the new list? So if I click on 'Carrol Faircloth', her name gets removed from the RecycleViewLayout? So we then see everyone else but her on the RV? – AlexAndDraw Feb 10 '18 at 14:04
  • Thanks a lot for the reply! I mean 'Carrol Faircloth' goes to the bottom of this list. She will remain at the bottom in case I want to change my mind and deselect her. All the selected labels will go to the bottom of the list and they will actually be in a separate sorted list there (deselected and selected lists will be one under the other and they are both sorted every time I select or deselect anything). I still try to come up with the way to do that but the main problem is that I am using RecycleView and I don't understand how to bind every list item with this behavior of refreshing – Artsiom Shamsutdzinau Feb 10 '18 at 15:45
  • To clarify further, we have two lists, two recycle views (one for each list) and then a bunch of labels inside those lists? Just making sure before I write an example. – AlexAndDraw Feb 10 '18 at 16:07
  • With ids I also tried to add this to the SelectableLabel: on_selected: self.ids.RV_id.refresh_from_layout() and I am still getting errror Using self.parent doesn't help because SelectableLabel parent is RecycleBoxLayout (not RecycleView) – Artsiom Shamsutdzinau Feb 10 '18 at 16:21
  • Yep. I didn't think of them as two separate RecycleViews but they might be) – Artsiom Shamsutdzinau Feb 10 '18 at 16:22

1 Answers1

2

Alright, so I did actually stumble a few times trying to sort this out but I think I've got it. What I did was create two recycle views and a CustomLabel that has access to ButtonBehaviors so you can use 'on_press' instead of 'on_touch_down' (which propagates through the entire tree rather than the interacted with element).

Example Video

The py file:

from kivy.app import App
from kivy.uix.recycleview import RecycleView
from kivy.uix.label import Label
from kivy.uix.behaviors import ButtonBehavior
from kivy.uix.floatlayout import FloatLayout

# Create a Custom ButtonLabel that can use on_press
class ButtonLabel(ButtonBehavior, Label):

    # App.get_running_app() lets us traverse all the way through our app from
    # the very top, which allows us access to any id. In this case we are accessing
    # the content of our selected_list_view of our app
    @property
    def selected_list_content(self):
        return App.get_running_app().root.ids.selected_list.ids.content

    # And in this case, we're accessing the content of our deselected_list_view
    @property
    def deselected_list_content(self):
        return App.get_running_app().root.ids.deselected_list.ids.content

    # This is our callback that our Label's will call when pressed
    def change_location(self):

        # If the label's parent is equal* the selected list, we remove the label from its
        # parent, and then we add it to the other list
        if self.parent == self.selected_list_content:
            self.parent.remove_widget(self)
            self.deselected_list_content.add_widget(self)

        # If the label's parent is not the selected list, then it is the deselected list
        # so we remove it from its parent and add it to the selected list
        else:
            self.parent.remove_widget(self)
            self.selected_list_content.add_widget(self)

    #* Note: Kivy uses weak references. This is why we use ==, and not 'is'

# We create a CustomRecycleView that we will define in our kv file      
class CustomRecycleView(RecycleView):
    pass

class MainWindow(FloatLayout):
    pass

class ExampleApp(App):

    def build(self):
        # We create an instance of the MainWindow class, so we can access its id
        # to import our list. Otherwise we would have nothing to add the list too
        main_window = MainWindow()
        importedlist = ['Novella Varela', 'Caroll Faircloth', 'Douglas Schissler',
                'Rolande Hassell', 'Hayley Rivero', 'Niesha Dungy', 'Winfred Dejonge', 'Venetta Milum']

        # We create a Label for each Name in our imported list, and then add it
        # to the content of selected list as a default
        # I'm sure you'll be importing our own lists in a different manner
        # This is just for the example
        for name in importedlist:
            NameLabel = ButtonLabel(text=(name))
            main_window.ids.selected_list.ids.content.add_widget(NameLabel)
        return main_window

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

The kv file:

#:kivy 1.10.0

# We create a reference to the ButtonLabel class in our py file
<ButtonLabel>:
    # We add our callback to our ButtonLabels on press event, on_press
    on_press: root.change_location()

# We create a reference to our CustomRecycleView class in our py file
<CustomRecycleView>:
    # We create a GridLayout to store all of the content in our RecycleView
    GridLayout:
        # We give it the id content so we can define the two property values in
        # ButtonLabel class in the py file
        id: content
        size_hint_y: None

        # One column because we want it to be vertical list list
        cols: 1

        # This set up, as well as size_hint_y set to None
        # is so we can scroll vertically without issue
        row_default_height: 60
        height: self.minimum_height

<MainWindow>:
    # We then create two instances of our CustomRecycleView, give them the ids
    # referenced by the ButtonLabel methods as well as give them equal share of the
    # screen space so they do not step on each others toes
    # The canvas here is just for prototyping purposes to make sure they are the
    # properly defined sizes. You can do whatever with them you would like tbh.
    CustomRecycleView:
        id: selected_list
        size_hint: 1, .5
        pos_hint: {'x': 0, 'y': .5}
        canvas:
            Color:
                rgba: 100, 0, 0, .2
            Rectangle:
                size: self.size
                pos: self.pos
    CustomRecycleView:
        id: deselected_list
        size_hint: 1, .45
        canvas:
            Color:
                rgba: 0, 0, 100, .2
            Rectangle:
                size: self.size
                pos: self.pos
AlexAndDraw
  • 620
  • 2
  • 6
  • 12
  • You are infinitely cool. I will definitely try to understand what exactly it is that you did. I will use your implementation until I maybe will be able to make this all work inside of only one RecycleView as I intended in the begining. Thanks again! I really appreciate your help) – Artsiom Shamsutdzinau Feb 10 '18 at 17:45
  • Why do you need to wrap two different lists in the *same* RecycleView?.Seems unnecessary from here. You could probably do something like put both recycle views into a vertical boxlayout so when you move the boxlayout both views move along with it. – AlexAndDraw Feb 10 '18 at 17:56
  • I wanted to have one list because it actually is one list - a list with students. It just so happens that some of them are present. They move to the bottom because I don't need to see them after I selected them. The only time I need to see them is when I misselected or if I want to check if I selected correctly. In your solution we get to see them all the time and also we have two scrolable screens when only one could be enough. Thanks a lot anyway. I still learned a bunch of stuff – Artsiom Shamsutdzinau Feb 10 '18 at 18:08