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:
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.
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.