Kivy Tutorials

Kivy Events 2: Bubbling and Capturing of Propagation

This article can be read in about 28 minutes.

Kivy events have a standard propagation mechanism. This article describes the Bubbling and capturing mechanisms of propagation.

Propagation of Kivy Events

Kivy events have a propagation mechanism within the widget tree. Propagation is triggered by the events of the widgets in turn, and the events are executed in each widget. For example, if there are multiple Labels that have an on_touch_down event, clicking on one will cause the other Labels to execute the event in turn.

I have only tried on_touch_down() and on_press(), but events belonging to base classes like Widget and Window classes probably have a propagation mechanism. Widget-specific events like on_press() and on_release(), which are button events, have no propagation mechanism and must be built manually.

The official Kivy documentation does not provide detailed specifications for propagation, so I will explain the general propagation mechanism for the web and other areas, as well as the current state of Kivy. Therefore, this article may contain some mistakes.

Propagation Method

The order of propagation is determined by the Bubble up phase, in which propagation proceeds upward from the first triggered widget to the root widget, and the Capture phase, in which propagation proceeds downward from the root widget to the first triggered widget.

Kivy's Propagation bubble phase and capture phase behavior.
Bubbling and capturing of Propagation for Kivy events

Bubble up Phase

Here is an example of the Bubble up phase.

kivy_events_propagation1.py

from kivy.app import App
from kivy.uix.label import Label
from kivy.uix.boxlayout import BoxLayout

class ParentWidget1(BoxLayout):
    def on_touch_down(self, touch):
        print(f'Touch received - Parent 1')
        if self.collide_point(*touch.pos):
            print("Touched Parent 1")
        return super().on_touch_down(touch)

class ChildLabel(Label):
    def on_touch_down(self, touch):
        print(f'Touch received - {self.text}')
        if self.collide_point(*touch.pos):
            print(f'Touched Child: {self.text}')
            return True
        return super().on_touch_down(touch)

class KivyEventsPropagation1(App):
    def build(self):
        return ParentWidget1()

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

kivyeventspropagation1.kv

<ParentWidget1>:
    orientation: 'vertical'
    Label:
        text: "Bubble-up phase of propagation"
        font_size: 30
        color: 151/255, 96/255, 124/255, 1
    ChildLabel:
        text: "Child 1 Label"
        font_size: 30
    ChildLabel:
        text: "Child 2 Label"
        font_size: 30
    ChildLabel:
        text: "Child 3 Label"
        font_size: 30

Explanation of Bubble up

This sample has the following tree structure.

<ParentWidget1>
+--- BoxLayout # Parent1
+---+--- Label
+---+--- ChildLabel # Child1
+---+--- ChildLabel # Child2
+---+--- ChildLabel # Child3

In the Bubble up phase, the event propagates from the widget that first triggered the event to the widgets in the hierarchy above.

When Child1 is clicked, four on_touch_down() events are propagated and executed in order as shown below.

  1. Parent1 on_touch_down() is executed.
  2. Child3 on_touch_down() is executed.
  3. Child2 on_touch_down() is executed.
  4. Finally, on_touch_down() of Child1, the source of the event, is executed.

The console will display the following

Touch received - Parent 1     RootWidget
Touched Parent 1
Touch received - Child 3 Label  ↑
Touch received - Child 2 Label  ↑
Touch received - Child 1 Label   ↑ 
Touched Child: Child 1 Label   ↑  First triggered event

‘Touched’ in the Child widget is the first triggered event in the widget you clicked on. ‘received’ is the event executed by propagation.

Capture Phase

The official Kivy documentation does not clearly explain this, so I am not sure what the specification is, but currently UI widgets are basically in the bubble up phase and layouts are in the Capture phase. It would be nice to be able to select between these phases, but I could not find such an explanation at this time.

I am not sure if this is correct, but here is a sample that I believe is the Capture phase.

kivy_events_propagation2.py

from kivy.app import App
from kivy.uix.label import Label
from kivy.uix.boxlayout import BoxLayout

class ParentWidget1(BoxLayout):
    def on_touch_down(self, touch):
        print(f'Touch received - Parent 1')
        if self.collide_point(*touch.pos):
            print("Touched Parent 1")
        return super().on_touch_down(touch)
    
class ParentWidget2(BoxLayout):
    def on_touch_down(self, touch):
        print(f'Touch received - Parent 2')
        if self.collide_point(*touch.pos):
            print("Touched Parent 2")
            #return True
        return super().on_touch_down(touch)
    
class ParentWidget3(BoxLayout):
    def on_touch_down(self, touch):
        print(f'Touch received - Parent 3')
        if self.collide_point(*touch.pos):
            print("Touched Parent 3")
            #return True
        return super().on_touch_down(touch)

class ChildLabel(Label):
    def on_touch_down(self, touch):
        print(f'Touch received - {self.text}')
        if self.collide_point(*touch.pos):
            print(f'Touched Child: {self.text}')
            return True
        return super().on_touch_down(touch)

class KivyEventsPropagation2(App):
    def build(self):
        return ParentWidget1()

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

kivyeventspropagation2.kv

<ParentWidget1>:
    orientation: 'vertical'
    Label:
        text: "Capture phase of propagation"
        font_size: 30
        color: 151/255, 96/255, 124/255, 1

    ParentWidget2:
        ParentWidget3:
            ChildLabel:
                text: "Child 1 Label"
                font_size: 30

Explanation of Capture Phase

This sample has the following tree structure.

<ParentWidget1>
+--- BoxLayout # Parent1
+---+--- BoxLayout # Parent2
+---+---+--- BoxLayout # Parent3
+---+---+---+--- ChildLabel # Child1

The Capture phase propagates down from the root widget to the widget that first triggered the event.

Clicking on the Label of Child1 will propagate four on_touch_down() and execute the events in order as shown below.

  1. Parent1 on_touch_down() is executed.
  2. Parent2 on_touch_down() is executed.
  3. Parent3 on_touch_down() is executed.
  4. Finally, on_touch_down() of Child1, the source of the event, is executed.

The console will display the following.

Touch received - Parent 1       ↓  RootWidget
Touched Parent 1        
Touch received - Parent 2       ↓
Touched Parent 2                
Touch received - Parent 3       ↓
Touched Parent 3                
Touch received - Child 1 Label  ↓
Touched Child: Child 1 Label       First triggered event.

Propagation Mechanism

Two return control propagation. If you override the event handler, you must control the propagation yourself.

  1. Call the parent’s on_touch_down()
  2. Returns a flag of True

Call Parent’s on_touch_down

You can control propagation by returning super().on_touch_down(touch). In each subclass on_touch_down(), use super to call the parent on_touch_down(). Each widget event is called, and finally on_touch_down() of the Widget class is executed.

The code is the following part.

return super().on_touch_down(touch)

For example, in the following tree structure, each subclass calls a method of the tree parent.

<RootWidget>
+--- BoxLayout # Parent1
+---+--- ChildWidget1 # Child1
+---+--- ChildWidget2 # Child2
  1. Child2 receives the touch event and calls its own on_touch_down().
  2. super().on_touch_down(), which calls on_touch_down() of the parent widget (in this case, RootWidget).
  3. Child2 calls on_touch_down() of Child1.
  4. Child1 calls on_touch_down() of RootWidget.
  5. Finally, RootWidget’s super().on_touch_down() is called and on_touch_down() of the Widget class is executed.

Propagation is enabled by relaying on_touch_down() in this way.

Flow of Kivy Propagation Events.

Returning the True Flag

To stop event propagation, return the return True flag; if False is returned or this line is not written, propagation continues. The default is False.

The term “stop” may be misleading. We do not “stop” propagation in order to not use it; we “stop” it in order to allow propagation to flow correctly.

return True

If the True flag is not written correctly, the propagation will not flow properly.

Why Stop the Propagation Flow?

There are two patterns of on_touch_down(): when it is called by touching a widget, and when it is called for the propagation mechanism described in the previous section.

class ChildButton(Button):
    def on_touch_down(self, touch):
        # Check if the touch is within range of the widget.
        if self.collide_point(*touch.pos):
            print(f'Touched Child: {self.text}')
            return True  # Stop Event Propagation.
        # If the touch is out of range, call a method of the parent class.
        return super().on_touch_down(touch)
  1. Case of touching a widget:
    Check whether the touch was made within the range of the widget with collide_point() and return a True flag if it was within the range.
  2. Case called for propagation mechanism:
    super().on_touch_down() calls the parent’s on_touch_down().

The flow is as follows.

Process flow of Kivy propagation.
<RootWidget>
+--- BoxLayout # Parent1
+---+--- ChildWidget1 # Child1  return True
+---+--- ChildWidget2 # Child2  return True
  1. When Child1 is touched, the flag is set to True, so propagation stops here.
  2. Since Child2 is not touched at this time, on_touch_down() of the parent, Parent1, is called and propagation proceeds.
  3. Since Parent1 has not written the flag, propagation proceeds and on_touch_down() of the parent Widget class is called.

If you set the Child1 and Child2 flags to False or do not write any flags, all widget events will be called no matter which widget is touched. Unless you want it this way, it is usually correct to make sure that widgets below the touched widget are not called according to the order. Conversely, it is possible to use True and False so that only certain widgets are propagated.

In the Case of Bubble up Phase

In the case of the Bubble up phase, the tree runs backwards, so stopping from the top of the widget tree will ensure correct propagation flow. However, if the root widget is stopped, it will not be propagated, so it should not return a flag.

<ParentWidget1>                           Stop from the upper layer.
+--- BoxLayout # Parent1                  Root widget should not be stopped.
+---+--- Label
+---+--- ChildLabel # Child1              ↓True
+---+--- ChildLabel # Child2              ↓True
+---+--- ChildLabel # Child3              ↓True

Below is a table showing the widgets that are called when propagation stops in the Bubble up phase. Basically, it is correct that the own widget and the remaining widgets in the tree are called.

Bubble up phaseStop propagationParent1Child1Child2Child3
Parent1
Child1
Child2
Child3
Widget called when propagation stops in the Bubble up phase.

In the Case of Capture Phase

In the case of Capture phase, propagation flows correctly by stopping from the bottom of the widget tree in order.

<ParentWidget1>                            Stop in order from the lower level.
+--- BoxLayout # Parent1                   ↑True
+---+--- BoxLayout # Parent2               ↑True
+---+---+--- BoxLayout # Parent3           ↑True
+---+---+---+--- ChildLabel # Child1       ↑True

The following table shows the widgets that are called when propagation stops in the Capture phase. Basically, it is correct that the widget itself and the rest of the widgets in the tree are called.

Capture phaseStop propagationParent1Parent2Parent3Child1
Parent1
Parent2
Parent3
Child1
Widget called when propagation stops in the Capture phase.

Does Not Use the Propagation Mechanism

Propagation is not done if the line return super().on_touch_down(touch) is not written.

return super().on_touch_down(touch)

It is not propagated by setting the flag to True and not calling on_touch_down() of the parent as shown below.

class ParentWidget1(BoxLayout):
    def on_touch_down(self, touch):
        if self.collide_point(*touch.pos):
            print("Parent 1 Widget touched")
            return True

As a side note, we could not write the propagation control in the kv language, because the kv language throws an error when return and super() are used.

Changing the Order in Which Events Are Called

The official documentation mentioned that the order of events can be changed with index, but this does not change the order of event propagation, but changes the order in which labels are displayed with index, which results in a change in the order in which events are called. (This was my wrong interpretation.) This cannot be written in the kv language, but might be useful if you want to dynamically change the order of widgets.

The label with text “c” gets the event first, “b” second and “a” last. You can reverse this order by manually specifying the index.

class RootWidget(BoxLayout):
    def __init__(self, **kwargs):
        super(RootWidget, self).__init__(**kwargs)
        self.orientation = 'vertical'
        
        self.label1 = ChildLabel(text='Label1')
        self.label2 = ChildLabel(text='Label2')
        self.label3 = ChildLabel(text='Label3')
        
        self.add_widget(self.label1,index=2)
        self.add_widget(self.label2,index=0)
        self.add_widget(self.label3,index=1)

In the above case, the application’s Label widget will be displayed in the order of the specified index.

Label2
Label3
Label1

The event propagation order is as follows

Touch in BoxLayout 1
@Touch in BoxLayout 1
Touch label - Label2
Touch label - Label3
Touch label - Label1
Touch label: Label1

Propagation of on_press and on_release

The button events on_press() and on_release() do not have a propagation mechanism; you must either use on_touch_* instead or define them as custom events and build the propagation mechanism manually. This example is described in the next article.

Propagation of the Callback Function

The on_touch_* callback function can also be called. The following is an example of a more complex widget tree.

kivy_events_propagation3.py

from kivy.app import App
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.boxlayout import BoxLayout

class ParentWidget1(BoxLayout):
    
    def on_touch_down(self, touch):
        print(f'Touch received - Parent 1')
        if self.collide_point(*touch.pos):
            print("Touched Parent 1")
        return super().on_touch_down(touch)
    
    def on_click(self):
        print("Callback called from Child 4.")

class ParentWidget2(BoxLayout):
    def on_touch_down(self, touch):
        print(f'Touch received - Parent 2')
        if self.collide_point(*touch.pos):
            print("Touched Parent 2")
        return super().on_touch_down(touch)

class ChildLabel(Label):
    def on_touch_down(self, touch):
        print(f'Touch received - {self.text}')
        if self.collide_point(*touch.pos):
            print(f'Touched Child: {self.text}')
            return True
        return super().on_touch_down(touch)
    
class ChildButton(Button):
    def on_touch_down(self, touch):
        print(f'Touch received - {self.text}')
        if self.collide_point(*touch.pos):
            print(f'Touched Child: {self.text}')
            return True
        return super().on_touch_down(touch)

class KivyEventsPropagation3(App):
    def build(self):
        return ParentWidget1()

    def on_click(self):
        print("Callback called from Child 6.")

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

kivyeventspropagation3.kv

<ParentWidget1>:
    orientation: 'vertical'
    ChildLabel:
        text: "Child 1 Label"
    ChildLabel:
        text: "Child 2 Label"
    ChildLabel:
        text: "Child 3 Label"
    ChildButton:
        text: "Child 4 button"
        on_touch_down: root.on_click()

    ParentWidget2:
        ChildLabel:
            text: "Child 5 Label"    
        ChildButton:
            text: "Child 6 button"
            on_touch_down: app.on_click()

<ChildLabel,ChildButton>:
    font_size: 30

Comment

Copied title and URL