2

In my app, I want to handle background touches and widget touches separately. The Widget documentation ignores how to prevent bubbling from .kv events. Here's a little test case:

from kivy.app import App

class TestApp(App):

  def on_background_touch(self):
    print("Background Touched")
    return True

  def on_button_touch(self):
    print("Button Touched")

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

And the .kv:

#:kivy 1.8.0

BoxLayout:
  orientation: "vertical"
  on_touch_down: app.on_background_touch()
  padding: 50, 50

  Button:
    text: "Touch me!"
    on_touch_down: app.on_button_touch()

The result: touching either the background or button triggers both handlers. Should I perform collision detection, or is there another way?

MadeOfAir
  • 2,933
  • 5
  • 31
  • 39

3 Answers3

5

You should perform collision detection. For instance, in a class definition:

class YourWidget(SomeWidget):
    def on_touch_down(self, touch):
        if self.collide_point(*touch.pos):
            do_stuff()

Edit: Actually, your method won't work anyway because the Button overlaps the BoxLayout. I would probably instead create a BoxLayout subclass and override on_touch_down, calling super first then if it returns False (indicating the touch hasn't been used yet) doing the BoxLayout interaction.

inclement
  • 29,124
  • 4
  • 48
  • 60
2

I wanted a solution that allows me to bind events from .kv files. @inclement solution won't allow me to do that because once you bind the event from .kv, you can't return True anymore to tell the parent you handled the event:

Button:
  # you can't return True here, neither from the handler itself
  on_touch_down: app.button_touched()

So what I've done is to perform collision detection at the parent, emitting a custom on_really_touch_down only if it doesn't hit any children, and performing collision detection yet again at the child, because all children receive the touch regardless of whatever (it's a mess, I know). Here's the complete solution (requires Kivy >= 1.9.0, because of the usage walk method):

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

class CustomTouchMixin(object):

  def __init__(self, *args, **kwargs):
    super(CustomTouchMixin, self).__init__(*args, **kwargs)
    self.register_event_type("on_really_touch_down")

  def on_really_touch_down(self, touch):
    pass

class CustomTouchWidgetMixin(CustomTouchMixin):

  def on_touch_down(self, touch):
    if self.collide_point(*touch.pos):
      self.dispatch("on_really_touch_down", touch)
    return super(CustomTouchWidgetMixin, self).on_touch_down(touch)

class CustomTouchLayoutMixin(CustomTouchMixin):

  def on_touch_down(self, touch):
    for child in self.walk():
      if child is self: continue
      if child.collide_point(*touch.pos):
        # let the touch propagate to children
        return super(CustomTouchLayoutMixin, self).on_touch_down(touch)
    else:
      super(CustomTouchLayoutMixin, self).dispatch("on_really_touch_down", touch)
      return True

class TouchHandlerBoxLayout(CustomTouchLayoutMixin, BoxLayout):
  pass

class TouchAwareButton(CustomTouchWidgetMixin, Button):
  pass

class TestApp(App):

  def on_background_touch(self):
    print("Background Touched")

  def on_button_touch(self, button_text):
    print("'{}' Touched".format(button_text))

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

The .kv:

#:kivy 1.9.0

TouchHandlerBoxLayout:

  padding: 50, 50
  on_really_touch_down: app.on_background_touch()

  TouchAwareButton:
    text: "Button One"
    on_really_touch_down: app.on_button_touch(self.text)

  TouchAwareButton:
    text: "Button Two"
    on_really_touch_down: app.on_button_touch(self.text)

So this allows me to bind touches from .kv.

MadeOfAir
  • 2,933
  • 5
  • 31
  • 39
1

Methods for binding touch events via .kv file/string syntax are possible, here's an example that modifies the caller's background when collisions are detected.

<cLabel@Label>:
    padding: 5, 10
    default_background_color: 0, 0, 0, 0
    selected_background_color: 0, 1, 0, 1
    on_touch_down:
        ## First & second arguments passed when touches happen
        caller = args[0]
        touch = args[1]
        ## True or False for collisions & caller state
        caller_touched = caller.collide_point(*touch.pos)
        background_defaulted = caller.background_color == caller.default_background_color
        ## Modify caller state if touched
        if caller_touched and background_defaulted: caller.background_color = self.selected_background_color
        elif caller_touched and not background_defaulted: caller.background_color = caller.default_background_color

    background_color: 0, 0, 0, 0
    canvas.before:
        Color:
            rgba: self.background_color
        Rectangle:
            pos: self.pos
            size: self.size

And for completeness, here's how to use the above code within a layout that is touch activated only if none of the children (or grandchildren and so on) have also collided with the same event.

<cGrid@GridLayout>:
    on_touch_down:
        caller = args[0]
        touch = args[1]
        caller_touched = caller.collide_point(*touch.pos)
        spawn_touched = [x.collide_point(*touch.pos) for x in self.walk(restrict = True) if x is not self]
        ## Do stuff if touched and none of the spawn have been touched
        if caller_touched and True not in spawn_touched: print('caller -> {0}\ntouch -> {1}'.format(caller, touch))
    cols: 2
    size_hint_y: None
    height: sorted([x.height + x.padding[1] for x in self.children])[-1]
    cLabel:
        text: 'Foo'
        size_hint_y: None
        height: self.texture_size[1]
    cLabel:
        text: 'Bar'
        size_hint_y: None
        height: self.texture_size[1] * 2

I may have gotten the texture_size's backwards, or perhaps not, but the height trickery can be ignored for the most part as it's purpose is to aid in making the parent layout more clickable.

The color changing and printing of caller & touch objects should be replaced with do_stuff() or similar methods, as they're there to make the example self contained, and show another way handling caller saved state when touched.

S0AndS0
  • 860
  • 1
  • 7
  • 20
  • 1
    I had no idea something like this was possible in kv lang -- where I can find more information about the kv lang syntax and what's possible? The docs I could find on kivy.org are pretty lacking when it comes to this stuff. – ByteArts Feb 16 '21 at 00:28
  • 1
    It has been a while but if I remember correctly, reading bits of the official source code was super helpful with figuring out what was possible, as well as plenty of experimentation to confirm suspicions. – S0AndS0 Feb 17 '21 at 16:49