Pypax
Pypax

Reputation: 113

Kivy: Events with Nested Layouts

I'm new to kivy and I don't understand how to handle event propagation.

A minimal example of what I'm trying to do:

# app.kv
ListItem:
    SomeIcon:
class ListItem(MDBoxLayout):
    def on_touch_down(self, touch):
        if self.collide_point(*touch.pos):
            print('clicked on list item!')
            #return True # uncomment to stop propagation
        return super(ListItem, self).on_touch_down(touch)


class SomeIcon(MDIcon):
    def on_touch_down(self, touch):
        if self.collide_point(*touch.pos):
            print('clicked on some icon!')
            return True
        return super(SomeIcon, self).on_touch_down(touch)

When I click on SomeIcon the event on ListItem is triggered first and then SomeIcon.

How can I prevent this and separate the events?

Thank you for any suggestions

Upvotes: 1

Views: 101

Answers (1)

Bergi
Bergi

Reputation: 664620

SomeIcon should not trigger the event on ListItem

I think you're mistaken on how events work in Kivy. The library does not implement event propagation by itself and then dispatches on_touch_* events on every involved widget, like e.g. the DOM does it on the web. There is no built-in collision detection, Kivy won't know which widgets were touched and which were not, every widget needs to figure that out on its own.

I would argue that the documentation "events bubble up from the first child upwards through the other children. If a widget has children, the event is passed through its children before being passed on to the widget after it." is wrong or at least misleading.

What's actually happening is that the main loop dispatches every input event at the root widget of the application, calling the respective on_touch_* method. This method is then responsible for propagating the event to the children widgets, and with a depth-first tree traversal it can form both event capturing and event bubbling. This recursion is happening in the default on_touch_* method implementations of the Widget class, which you're also doing by calling super().on_touch_down(touch).

Depending on where in the method the recursive calls are happening, one can either handle events in the capturing phase or handle them in the bubbling phase. To do that latter, one has to first propagate the event to the children widgets and see whether one of them handled it, and only when none of them did, act on the event oneself. This is, in fact, what most method implementations do, see for example the on_touch_down implementation of buttons or the recommended way to override the method, and is what you could do as well to get the desired behaviour:

class ListItem(MDBoxLayout):
    def on_touch_down(self, touch):
        if super(ListItem, self).on_touch_down(touch):
            return True
        if self.collide_point(*touch.pos):
            print('clicked on list item but not on the icon inside!')
            return True

class SomeIcon(MDIcon):
    def on_touch_down(self, touch):
        if super(SomeIcon, self).on_touch_down(touch):
            return True
        if self.collide_point(*touch.pos):
            print('clicked on some icon!')
            return True

However, for your example case you should use a behaviour mixin, in particular the ButtonBehaviour:

class ListItem(MDBoxLayout, ButtonBehaviour):
    def on_press(self):
        print('clicked on list item but not on the icon inside!')

class SomeIcon(MDIcon, ButtonBehaviour):
    def on_press(self):
        print('clicked on some icon!')

Upvotes: 1

Related Questions