Search code examples
pythonkivykivy-language

Kivy ObjectProperty is None when I'm expecting a TextInput instance, what am I missing?


I am trying to build a drop down list using DropDown, the widget should behave similar to the one seen on windows File Explorer.

enter image description here

I have a TextInput and a Button to which I associate the DropDown. When I run the following code, I get the error:

Exception has occurred: AttributeError
'NoneType' object has no attribute 'text'
  File "C:\Users\hanne\Documents\_Educational\_Learning\Python\Kivy\DropDown\B.ii\main.py", line 34, in on_select
    setattr(self.user_choice, 'text', data)
  File "C:\Users\hanne\Documents\_Educational\_Learning\Python\Kivy\DropDown\B.ii\main.py", line 45, in on_touch_down
    self.dropdown.select(self.text)
  File "C:\Users\hanne\Documents\_Educational\_Learning\Python\Kivy\DropDown\B.ii\main.py", line 69, in <module>
    MyApp().run()

main.py

import kivy
kivy.require('2.0.0')

from kivy.app import App
from kivy.uix.anchorlayout import AnchorLayout
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.dropdown import DropDown
from kivy.uix.button import Button
from kivy.uix.label import Label
from kivy.properties import ObjectProperty

class TextEntryDropDown(BoxLayout):

    user_choice = ObjectProperty(None)
    drop_button = ObjectProperty(None)

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

        self.dropdown = DropDownList(self.user_choice)

        for item in ['Option 1', 'Option 2', 'Option 3']:
            label = DropDownItem(self.dropdown, text=item, size_hint_y=None, height=30)
            self.dropdown.add_widget(label)


class DropDownList(DropDown):

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

    def on_select(self, data):
        setattr(self.user_choice, 'text', data)


class DropDownItem(Label):

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

    def on_touch_down(self, touch):
        if self.collide_point(*touch.pos):
            self.dropdown.select(self.text)
        return super().on_touch_down(touch)


class DropButton(Button):
    
    def on_touch_down(self, touch):
        if self.collide_point(*touch.pos):
            dropdown = self.parent.dropdown
            dropdown.open(self)
        return super().on_touch_down(touch)


class MainApp(AnchorLayout):
    pass


class MyApp(App):

    def build(self):
        return MainApp()


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

my.kv

<MainApp>:
    anchor_x: 'left'
    anchor_y: 'top'
    TextEntryDropDown:
        

<TextEntryDropDown>
    size_hint_y: None
    height: 30

    user_choice: user_choice
    drop_button: drop_button

    TextInput:
        id: user_choice
        text: 'Select an option..'
        size_hint: (None, None)
        height: 30
        width: root.width - drop_button.width

    DropButton:
        id: drop_button
        size_hint: (None, None)
        height: user_choice.height
        width: user_choice.width

I'm trying to trace down why user_choice is None, I'm expecting it to be a reference to the TextInput defined in my.kv. Any idea why this might be? I can't put my finger on it.

Thank you!


Solution

  • The problem is that the ObjectProperties (user_choice and drop_button) of the TextEntryDropDown are not yet available in the __init__() method of TextEntryDropDown. A work around is to pass a reference to the TextEntryDropDown when you create the DropDownList, instead of passing the not yet defined user_choice. Like this:

    class TextEntryDropDown(BoxLayout):
        user_choice = ObjectProperty(None)
        drop_button = ObjectProperty(None)
    
        def __init__(self, **kwargs):
            print('TextEntryDropDown.__init__(), self.user_choice =', self.user_choice)
            super().__init__(**kwargs)
    
            self.dropdown = DropDownList(self)  # pass reference to this TextEntryDropDown
    
            for item in ['Option 1', 'Option 2', 'Option 3']:
                label = DropDownItem(self.dropdown, text=item, size_hint_y=None, height=30)
                self.dropdown.add_widget(label)
    

    Then in the DropDownList:

    class DropDownList(DropDown):
    
        def __init__(self, ted, **kwargs):
            super().__init__(**kwargs)
            self.textEntryDropDown = ted  # save reference to the TextEntryDropDown
            # self.user_choice = user_choice
    
        def on_select(self, data):
            self.textEntryDropDown.user_choice.text = data  # use TextEntryDropDown to access now available user_choice ObjectProperty
            # setattr(self.user_choice, 'text', data)