Search code examples
pythonkivykivy-languagekivymd

Determining touch execution hierarchy when inheriting from multiple touch classes in Kivy


I'm trying to combine functionality from KivyMD MDCardSwipe and kivy_garden drag_n_drop. MDCardSwipe allows you to swipe a card to the side to reveal an underlying widget -- in my case with a trash icon and delete function -- and drag_n_drop allows you to drag widgets around to reorder them within a layout. Ideally I want swipe-able cards that I can rearrange by dragging.

I have created a class DragCard shown below that inherits from both MDCardSwipe, and DraggableObjectBehavior. When I order this way the MDCardSwipe functionality works, but the DraggableObjectBehavior does not. When I reverse the order, MDCardSwipe no longer works but DraggableObjectBehavior does.

MDCardSwipe has a relatively small area within which the swipe must begin. I want MDCardSwipe to receive the touch if it's within this area, and to pass the touch on to DraggableObjectBehavior if the touch is outside said area.

Any help would be greatly appreciated!

.py file:

from kivy.lang import Builder
from kivymd.app import MDApp
from kivy.core.window import Window
from kivy.uix.label import Label
from kivy.uix.boxlayout import BoxLayout
from kivymd.uix.card import MDCardSwipe
from kivy.uix.widget import Widget
from kivy_garden.drag_n_drop import DraggableLayoutBehavior, DraggableObjectBehavior
from kivy.properties import StringProperty


class DraggableBoxLayout(DraggableLayoutBehavior, BoxLayout):
    def __init__(self, **kwargs):
        super().__init__(
            spacer_widget = MySpacer(),
            spacer_props = {'size_hint_y': None, 'height' : 150 },
             **kwargs)
        

    def compare_pos_to_widget(self, widget, pos):
        if self.orientation == 'vertical':
            return 'before' if pos[1] >= widget.center_y else 'after'
        return 'before' if pos[0] < widget.center_x else 'after'

    def handle_drag_release(self, index, drag_widget):
        self.add_widget(drag_widget, index)


class DragCard(MDCardSwipe, DraggableObjectBehavior):
    text = StringProperty()

    def initiate_drag(self):
        # during a drag, we remove the widget from the original location
        self.parent.remove_widget(self)
        

class MySpacer(Widget):
    """Widget inserted at the location where the dragged widget may be
    dropped to show where it'll be dropped.
    """
    pass

 
class MainApp(MDApp): 
    
    def build(self):
        self.theme_cls.theme_style = 'Dark'
        self.theme_cls.primary_palette = "Cyan"
        Window.size = (375, 740)
        

        return Builder.load_file("drag.kv")

    def on_start(self):
        pass

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

.kv file:

#:kivy 2.0.0

BoxLayout:
    orientation: 'vertical'

    DraggableBoxLayout:
        drag_classes: ['card']
        orientation: 'vertical'

        DragCard:
            text: 'A'
            drag_cls: 'card'
        DragCard:
            text: 'B'
            drag_cls: 'card'
        DragCard:
            text: 'C'
            drag_cls: 'card'
        DragCard:
            text: 'D'
            drag_cls: 'card'
        DragCard:
            text: 'E'
            drag_cls: 'card'
        DragCard:
            text: 'F'
            drag_cls: 'card'
        DragCard:
            text: 'G'
            drag_cls: 'card'
        DragCard:
            text: 'H'
            drag_cls: 'card'


<DragCard>:
    size_hint_y: None
    height: 150


    MDCardSwipeLayerBox:
        padding: "8dp"

        MDIconButton:
            icon: "trash-can"
            pos_hint: {"center_y": .5}
            # on_release: root.delete_template()

    MDCardSwipeFrontBox:

        Label:
            text: root.text
            pos_hint: {'center_x': .5, 'center_y': .5}
            halign: 'center'
            valign: 'center'
            font_style: 'H3'

<MySpacer>:
    canvas:
        Color:
            rgba: [.95, .57, .26, 1]
        Rectangle:
            size: self.size
            pos: self.pos

Solution

  • The following code appears to do what you want. The on_touch_down() method of DragCard calls the on_touch_down() method of DraggableObjectBehavior or MDCardSwipe depending on the location of the touch on the DragCard. The other touch methods determine which method to call based in the grab_current property of the touch (which is set by DraggableObjectBehavior):

    class DragCard(DraggableObjectBehavior, MDCardSwipe):
        text = StringProperty()
    
        def initiate_drag(self):
            # during a drag, we remove the widget from the original location
            self.parent.remove_widget(self)
            
        def on_touch_down(self, touch):
            if self.collide_point(*touch.pos):
                if touch.x > self.center_x:
                    return DraggableObjectBehavior.on_touch_down(self, touch)
                else:
                    return MDCardSwipe.on_touch_down(self, touch)
    
        def on_touch_up(self, touch):
            if touch.grab_current == self:
                return DraggableObjectBehavior.on_touch_up(self, touch)
            else:
                return MDCardSwipe.on_touch_up(self, touch)
    
        def on_touch_move(self, touch):
            if touch.grab_current == self:
                return DraggableObjectBehavior.on_touch_move(self, touch)
            else:
                return MDCardSwipe.on_touch_move(self, touch)