Search code examples
pythonkivywidgetscreenkivy-language

binding movement and rotation of kivy scatter widgets


Below I have a code that displays 2 scatter widgets in a kivy screen. If one widget is dragged, the other also drags and vice versa.

What I want is in addition to movement, if one is rotated, the other rotates in the same way. In other words, they are exactly mimicking each other with both rotation and position.

The problem I am facing is that kivy's default position is the bottom left of the widget. So, if the user rotates one widget from some random axis point, the other widget rotates at the bottom left. When introducing rotation while the positions are locked, it becomes all messed up, and I cannot find a way to overcome this bug in order to match both rotation and movement.

Here is code with positions locked.

from kivy.uix.scatter import Scatter
from kivy.uix.relativelayout import RelativeLayout
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.lang import Builder

class ScatterWidget(Scatter):
    pass

class SquareWidget(Widget):
    pass

class WindowManager(ScreenManager):
    pass

class GeneralWindow(Screen,RelativeLayout):

    start_position_1=(200,200)
    start_position_2=(300,400)
    def on_touch_move(self, touch):
        app=self.manager.get_screen('General')
        pos1=app.ids['widget10'].parent.to_parent(*self.pos)
        pos2=app.ids['widget20'].parent.to_parent(*self.pos)
        mov1=(pos1[0]-self.start_position_1[0],pos1[1]-self.start_position_1[1])
        mov2=(pos2[0]-self.start_position_2[0],pos2[1]-self.start_position_2[1])

        if self.ids.one.collide_point(*touch.pos):
            app.ids['two'].pos=(mov1[0]+self.start_position_2[0],mov1[1]+self.start_position_2[1])
        if self.ids.two.collide_point(*touch.pos):
            app.ids['one'].pos =(mov2[0]+self.start_position_1[0],mov2[1]+self.start_position_1[1])

KV = Builder.load_string("""

WindowManager:
    GeneralWindow:

<GeneralWindow>:
    name: 'General'
    RelativeLayout:
        canvas: 
            Color: 
                rgba: 0,0,0,0.3
            Rectangle: 
                size: self.size
                pos: self.pos
        
        ScatterWidget:
            do_scale: False
            id: one
            size_hint: None,None
            size: widget10.size
            rotation: 0
            pos: root.start_position_1

            SquareWidget:
                id: widget10
                size: (200,20)
                canvas: 
                    Color:
                        rgba: 1,1,0,0.7
                    Rectangle:
                        size: self.size
                        pos:  self.pos  

        ScatterWidget:
            do_scale: False
            id: two
            size_hint: None,None
            size: widget20.size
            rotation: 0
            pos: root.start_position_2

            SquareWidget:
                id: widget20
                size: (200,20)
                canvas: 
                    Color:
                        rgba: 0,1,0,0.7
                    Rectangle:
                        size: self.size
                        pos:  self.pos 

""")

class TApp(App):

    def build(self):
        return KV

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

Solution

  • The Scatter widget has a rotation property, that performs a rotation about the center of the widget. By using this property, and specifying do_rotation: False for both of your ScatterWidgets, I think your problem is simplified. Also, modifying your movement code to work on the center property of the widgets adds another simplification.

    Here is a modified version of your code that does this. In this code, the left button is used to move the widgets and the right button is used to rotate the widgets:

    from kivy.config import Config
    Config.set('input', 'mouse', 'mouse,disable_multitouch')
    
    from kivy.app import App
    from kivy.uix.scatter import Scatter
    from kivy.uix.relativelayout import RelativeLayout
    from kivy.uix.screenmanager import ScreenManager, Screen
    from kivy.uix.widget import Widget
    from kivy.lang import Builder
    from kivy.vector import Vector
    
    
    class ScatterWidget(Scatter):
    
        def on_touch_down(self, touch):
            if self.collide_point(*touch.pos) and touch.button == 'right':
                touch.grab(self)
                return True
            return super(ScatterWidget, self).on_touch_down(touch)
    
        def on_touch_move(self, touch):
            if touch.grab_current is self and touch.button == 'right':
                self.rotation = -Vector(1, 0).angle(Vector(touch.pos) - Vector(self.center))
                return True
            return super(ScatterWidget, self).on_touch_move(touch)
    
        def on_touch_up(self, touch):
            if touch.grab_current is self:
                touch.ungrab(self)
            return super(ScatterWidget, self).on_touch_up(touch)
    
    
    class SquareWidget(Widget):
        pass
    
    
    class WindowManager(ScreenManager):
        pass
    
    
    class GeneralWindow(Screen, RelativeLayout):
        start_center_1 = (200, 200)
        start_center_2 = (300, 400)
    
        def on_touch_move(self, touch):
            app = self.manager.get_screen('General')
            scatter_one = app.ids['one']
            scatter_two = app.ids['two']
            center1 = scatter_one.center
            center2 = scatter_two.center
            mov1 = (center1[0] - self.start_center_1[0], center1[1] - self.start_center_1[1])
            mov2 = (center2[0] - self.start_center_2[0], center2[1] - self.start_center_2[1])
    
            # convert touch.grab_list of WeakReferences to a grab_list of actual objects
            grab_list = []
            for wr in touch.grab_list:
                grab_list.append(wr())
    
            if scatter_one in grab_list:
                if touch.button == 'right':
                    scatter_two.rotation = scatter_one.rotation
                else:
                    scatter_two.center = (mov1[0] + self.start_center_2[0], mov1[1] + self.start_center_2[1])
            elif scatter_two in grab_list:
                if touch.button == 'right':
                    scatter_one.rotation = scatter_two.rotation
                else:
                    scatter_one.center = (mov2[0] + self.start_center_1[0], mov2[1] + self.start_center_1[1])
    
            return super(GeneralWindow, self).on_touch_move(touch)
    
    
    KV = Builder.load_string("""
    
    WindowManager:
        GeneralWindow:
    
    <GeneralWindow>:
        name: 'General'
        RelativeLayout:
            canvas: 
                Color: 
                    rgba: 0,0,0,0.3
                Rectangle: 
                    size: self.size
                    pos: self.pos
    
            ScatterWidget:
                do_scale: False
                do_rotation: False
                id: one
                size_hint: None,None
                size: widget10.size
                rotation: 0
                center: root.start_center_1
    
                SquareWidget:
                    id: widget10
                    size: (200,20)
                    canvas: 
                        Color:
                            rgba: 1,1,0,0.7
                        Rectangle:
                            size: self.size
                            pos:  self.pos  
    
            ScatterWidget:
                do_scale: False
                do_rotation: False
                id: two
                size_hint: None,None
                size: widget20.size
                rotation: 0
                center: root.start_center_2
    
                SquareWidget:
                    id: widget20
                    size: (200,20)
                    canvas: 
                        Color:
                            rgba: 0,1,0,0.7
                        Rectangle:
                            size: self.size
                            pos:  self.pos 
    
    """)
    
    
    class TApp(App):
    
        def build(self):
            return KV
    
    
    if __name__ == '__main__':
        TApp().run()