Search code examples
pythonkivyevent-handlingpopup

Kivy: controller not receiving the event dispatched by a popup


I have been trying to receive simple events from a popup on kivy. I can see that the event has been dispatched, but the controller is not receiving anything.

After pulling my hair on this for hours, I tried a simple kivy application but that also did not work.

from kivy.app import App
from kivy.uix.popup import Popup
from kivy.uix.button import Button
from kivy.uix.label import Label
from kivy.event import EventDispatcher
from kivy.uix.boxlayout import BoxLayout
from kivy.clock import Clock

class ManualAnalysisPopup(Popup, EventDispatcher):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.setup_ui()
        self.register_event_types()

    def register_event_types(self):
        self.register_event_type('on_apply_pattern')
        print("ManualAnalysisPopup: Registered on_apply_pattern event")
        
    def on_apply_pattern(self):
        print("ManualAnalysisPopup: on_apply_pattern event fired")

    def setup_ui(self):
        self.title = "Manual Image Analysis"
        self.size_hint = (0.9, 0.9)

        self.image_widget = Label(text="Image Placeholder")

        apply_button = Button(text="Apply Pattern")
        apply_button.bind(on_release=lambda btn: self.apply_pattern())

        button_layout = BoxLayout(orientation='vertical')
        button_layout.add_widget(self.image_widget)
        button_layout.add_widget(apply_button)

        self.content = button_layout

    def apply_pattern(self):
        print("Applying pattern")
        self.dispatch('on_apply_pattern')



class ManualAnalysisController(EventDispatcher):
    def __init__(self):
        super().__init__()
        self.popup = ManualAnalysisPopup()
        Clock.schedule_once(lambda dt: self.bing_view_events())
        self.show_popup()

    def bing_view_events(self):
        self.popup.bind(on_apply_pattern=self.handle_apply_pattern)
        print("ManualAnalysisController: Events bound successfully")

    def show_popup(self):
        self.popup.open()

    def handle_apply_pattern(self, instance, *args):
        print("Handling apply pattern event")


class MyApp(App):
    def build(self):
        controller = ManualAnalysisController()

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

When pressing the button, this code returns:

ManualAnalysisController: Events bound successfully
Applying pattern
ManualAnalysisPopup: on_apply_pattern event fired

Solution

  • The problem is that bind() creates a WeakMethod of the callback. That means that if the binding class gets garbage collected, that WeakMethod disappears. That is what is happening in your code. Your code:

        controller = ManualAnalysisController()
    

    creates an instance of ManualAnalysisController, which binds to the custom Event. But you do not save that instance, so it immediately gets garbage collected and that WeakMethod goes away. Here are two ways to fix this:

    One way is to just save a reference to the ManualAnalysisController:

    self.controller = ManualAnalysisController()
    

    Another is to use fbind() instead of bind():

    self.popup.fbind('on_apply_pattern', self.handle_apply_pattern)
    

    The fbind() saves a real reference to the callback method, so that reference prevents the garbage collection. But preventing some garbage collection can be an issue by creating a possible memory leak.