pythonpropertieskivykivy-recycleview

Error passing kivy attribute between classes in ScreenManager


I am new to python and kivy and am learning how to pass information between kivy objects and python. I have the basic concepts down but this problem has me stumped.

I am writing an app to manage GPS waypoints that are classified into groups. The intent is for the user to select a waypoint group from a Spinner, which populates a list of waypoints in a RecycleView. The user then selects a waypoint from the RecycleView list. The selected waypoint is passed on for further processing. It's this last step (the passing) that is failing.

I developed this waypoint management function in its own development program and it works as intended. The problem came when I added the development code into a ScreenManager. This is a small part of a much larger project so I've stripped out all of the distractions in the code below and reorganized to make this easier to debug.

The app has several screens managed with ScreenManager. The waypoint selection screen presents a Spinner for choosing a waypoint group and a RecycleView (called RV()) for choosing a waypoint. The waypoint choice is processed in class RVItem(). The Spinner, RecycleView, and RVItem() work fine. The problem (in the ScreenManager version) occurs when I try to pass the chosen waypoint back to a label in the kivy code. The RVItem.on_release() event handler successfully captures the selected waypoint but I can't figure out how to send the selection back to the Label on the screen. My problem is in the RVItem.on_release() code. The Label's id in the .kv file is route_id. I left a list of some of my attempts to send the waypoint to route_id.text in the RVItem.on_release() code but I can't find anything that works. What am I missing?

I lastly tried accessing the Label using route_id = ObjectProperty(None) in class Route(). I couldn't get that to work either but it doesn't effect how the program runs or crashes so I left the property in the code in case it is helpful.

To duplicate the problem: Copy the code into files main.py and ScreenManager.kv. Start the program and when the Main Menu opens click on the Route button. Click the Choose a group spinner, select a group off of the dropdown list, and then choose a waypoint from the RecycleView list. The program will crash at the end of the RVItem.on_release() code. The error will be KeyError: 'route_id'

and

     AttributeError: 'super' object has no attribute '__getattr__'

I've spent hours trying to figure this out myself. If you can suggest a solution, please also let me know how I should have gone about debugging this myself.

I am running Python 3.8 and Kivy 2.0.

# main.py
# BoatInstruments.222
# Stripped down version to demonstrate the problem passing the 
# RecycleView's response back to the kivy Label
  
from kivy.app import App
from kivy.uix.recycleview import RecycleView
from kivy.factory import Factory
from kivy.uix.screenmanager import ScreenManager, Screen, FadeTransition
from kivy.lang import Builder
from kivy.properties import ObjectProperty

Builder.load_file('ScreenManager.kv')


class ScreenManagement(ScreenManager):
    pass


class MainMenu(Screen):
    pass


class RVItem(Factory.Button):
    # Returns the waypoint selected by RecycleView()
    def get_data_index(self):
        return self.parent.get_view_index_at(self.center)

    @property
    def rv(self):
        return self.parent.recycleview

    def on_release(self):
        app = App.get_running_app()
        data_index = self.get_data_index()
        current_waypoint = app.waypoints[data_index]
        print("\r\ncurrent_waypoint = ", current_waypoint, "\r\n")                              # Successful to this point


        # Write the route (current_waypoint for debugging) to kivy label Route.route_id              # !!! FAIL !!!
        # These are some of the things that I tried.
        print("app.root is ", app.root)
        app.root.ids.route_id.text = current_waypoint                     # This works in dev code (without ScreenManager) because there class WMApp(App) returns the root widget Route()
        # root.ids.route_id.text = current_waypoint
        # root.route_id.text = current_waypoint
        # self.ids.route_id.text = current_waypoint
        # self.parent.ids.route_id.text = current_waypoint
        # scrRoute = app.root.ids.route_id.text
        # root.ids.screen_manager.get_screen('route')
        # scrRoute.ids.route_id.text = current_waypoint
        # self.route_id.text = current_waypoint




class RV(RecycleView):
    def __init__(self, **kwargs):
        super(RV, self).__init__()
        self.data = []                                                          # Initialize the list of Groups


class Route(Screen):
    # Presents a list of waypoint groups in a spinner. After choosing, populates rv_id.data with that group's waypoints.

    route_id = ObjectProperty(None)


    def spinner_clicked(self, value):                                           # value is the selected waypoint group
        # Get the Group's list of waypoints and send them to RV
        app = App.get_running_app()
        self.ids.rv_id.data = [{'text': item} for item in app.waypoints]

    def new_route(self):
        print("Attempting Route.new_route()")
        app = App.get_running_app()
        app.wptroute = []
        app.root.ids.route_id.text = ""                                         # !!! FAIL !!!


    def done_route(self):
        print("Attempting Route.done_route()")



class BoatInstrumentsApp(App):

    groups = ['CYC', 'MHYC', 'GRYC', 'CLYC', 'Cleveland']        # accessed in kivy via app.list_of_groups
    waypoints = ['GRSC A', 'GRSC B', 'GRSC C', 'GRSC D', 'GRSC E', 'GRSC F']
    wptroute = []

    def __init__(self, **kwargs):
        super().__init__(**kwargs)


    def build(self):
        return ScreenManagement()


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




# ScreenManager.kv

<ScreenManagement>:
    id: screen_manager
    MainMenu:
        id: mainmenu                                 
        name: 'mainmenu'                           
        manager: 'screen_manager'
    Route:
        id: route
        name: 'route'
        manager: 'screen_manager'


# ##################################################################
<MainMenu>:
    BoxLayout:
        orientation: 'vertical'
        padding: 120
        spacing: 30

        Label:
            text: "Main Menu"
            font_size: 60

        Button:
            text: "Route"
            font_size: 40
            on_release: app.root.current = 'route'
    
# ##################################################################
<Route>:
    route_id: route_id                                      # I added this property late. It may or may not be useful
    BoxLayout:
        orientation: 'horizontal'
        padding: 5
        spacing: 5

        BoxLayout:                                                             # Left column: Groups and Waypoints
            orientation: 'vertical'
            Spinner:                                                           # Spinner: Waypoint Group
                id: spinner_id
                size_hint: (1, 0.15)
                text: "Choose a group"
                font_size: '40dp'
                values: app.groups
                on_text: root.spinner_clicked(spinner_id.text)

            Label:
                size_hint: (1, 0.04)

            RV:                                                                         # RecycleView: Waypoints
                id: rv_id
                viewclass: 'RVItem'
                RecycleBoxLayout:
                    default_size: None, 30                                              # Set the RV child box height
                    default_size_hint: 1, None
                    size_hint_y: None
                    height: self.minimum_height
                    orientation: 'vertical'
                    spacing: 5



        BoxLayout:                                                                      # Right column: Label, list of waypoints, two buttons
            id: box_id
            orientation: 'vertical'

            Label:
                text: "Route"
                size_hint: (1, 0.15)
                font_size: '40dp'


            # ########### HERE ###########################################
            #Display the route (or current_waypoint for debugging)
            Label:                                                  # This label will contain the waypoints of the route, line by line
                id: route_id
                text: "Route goes here"



            RelativeLayout:
                size_hint: 1, 0.24
                Button:                                                                 # Button: New Route
                    id: new_route_id
                    text: "New Route"
                    font_size: '40dp'
                    size_hint: 0.8, 0.48
                    pos_hint: {"x":0.1, "top":1}
                    on_release: root.new_route()

                Button:                                                                 # Button: Done
                    id: done_route_id
                    text: "Done"
                    font_size: '40dp'
                    size_hint: 0.8, 0.48
                    pos_hint: {"x":0.1, "bottom":1}
                    # on_release: root.done_route()
                    on_release: app.root.current = 'mainmenu'

Solution

  • Since you are using a ScreenManager, you can use the get_screen() method of ScreenManager to access the Screen that contains the route_id Label. Try replacing:

    app.root.ids.route_id.text = current_waypoint
    

    with:

    app.root.get_screen('route').ids.route_id.text = current_waypoint