Search code examples
pythondebuggingkivykivy-language

Multiple Kivy DropDown Lists Strange Bug


I encounter a strange bug with multiple Kivy DropDown widgets. Built the following GUI prototype, where all 10 DropDown widgets are generated by the same method:

enter image description here

But only 6 of them work. The other 4 DropDowns: "Cars", "Green", "Westie", "Chicken" do not show anything when clicked.

My source code is below:

from typing import List, OrderedDict
from functools import partial
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.dropdown import DropDown
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.screenmanager import Screen, ScreenManager
from kivy.uix.widget import Widget

_category_list = ["Food", "Colors", "Dogs", "Cars", "Fruits"]
_food_list = ["Chicken", "Fish"]
_cars_list = [
    "Acura",
    "Audi",
    "BMW",
    "Hyundai",
    "Mercedes",
    "Nissan",
    "Toyota",
    "Volvo",
]
_colors_list = ["Red", "Green", "Blue", "Yellow"]
_dogs_list = [
    "Bulldog",
    "Chihuahua",
    "Dobermann",
    "Husky",
    "Labrador",
    "Pomeranian",
    "Poodle",
    "Retriever",
    "Westie",
]
_fruits_list = ["Apple", "Orange", "Watermelon"]
_node_to_stuff_map = {
    "Cars": _cars_list,
    "Colors": _colors_list,
    "Dogs": _dogs_list,
    "Food": _food_list,
    "Fruits": _fruits_list,
}

class MainScreen(Screen):
    def add_list_node(self, node: Widget):
        self.mylist.add_widget(node)

class Manager(ScreenManager):
    pass

Builder.load_string(
    """
<MainScreen>:
    mylist: id_mylist

    BoxLayout:
        orientation: "vertical"

        BoxLayout:
            orientation: "horizontal"
            size_hint: 1.0, 0.1
            Label:
                text: "Category"
            Label:
                text: "Item List"
            Label:
                text: "Parameters"

        BoxLayout:
            id: id_mylist
            orientation: "vertical"
            size_hint: 1.0, 0.9
"""
)

class DropDownListBug(App):
    def build(self):
        self.main_screen = MainScreen()
        smgr = ScreenManager()
        smgr.add_widget(self.main_screen)
        self.make_my_lists()
        return smgr

    def make_ddl(self, a_node: str, a_node_list) -> DropDown:
        def select(drop_button, text, btn):
            drop_button.text = text

        ddl = DropDown()
        ddl_main = Button(text=a_node, size_hint=(1, None), height=50)
        for node_type in a_node_list:
            btn = Button(text=node_type, size_hint=(1, None), height=50)
            btn.bind(on_release=partial(select, ddl_main, btn.text))
            btn.bind(on_release=ddl.dismiss)
            ddl.add_widget(btn)
        ddl_main.bind(on_press=ddl.open)
        return ddl_main

    def make_my_lists(self):
        my_list = [
            "Cars.Hyundai",
            "Colors.Green",
            "Dogs.Westie",
            "Food.Chicken",
            "Fruits.Apple",
        ]

        for node in my_list:
            toks = node.split(".")
            node_type, node_item = toks[0], toks[1]

            node_panel = BoxLayout(orientation="horizontal")

            dll_category = self.make_ddl(node_type, _category_list)
            node_panel.add_widget(dll_category)

            item_list = _node_to_stuff_map[node_type]
            ddl_items = self.make_ddl(node_item, item_list)
            node_panel.add_widget(ddl_items)

            lbl = Label(text="[params]", size_hint=(1, None), height=50)
            node_panel.add_widget(lbl)

            self.main_screen.add_list_node(node_panel)

if __name__ == "__main__":
    DropDownListBug().run()

To confound the matter even more, if you alter the lengths of the lists, the DropDown widgets that work/do not work change!

E.g. delete "Toyota", "Volvo" from cars and add "White" color: the non-working DropDowns now become "Cars", "Colors", "Dogs", "Chicken". Totally bizarre.


Solution

  • The problem is that python is doing garbage collection, removing unreferenced objects. The ddl variable in your make_ddl() method holds the DropDown instance and that variable becomes unreferenced and eligible for garbage collection as soon as make_ddl() returns. The result is that in some cases, the DropDown no longer exists when the Button gets pressed. A simple hack is to keep a refrence to the DropDown so that it does not get garbage collected. Like this:

        ddl = DropDown()
        ddl_main = Button(text=a_node, size_hint=(1, None), height=50)
        ddl_main.ddl = ddl
    

    Now the Button holds a reference to its DropDown, so it does not become unreferenced.